From 188f447b9722baea40c5b3f8f0bf8bba0db130c6 Mon Sep 17 00:00:00 2001 From: Jack Sullivan <122506503+KnightOkami@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:04:14 -0400 Subject: [PATCH 1/6] Initial Push Create validate-package-json.js. Add it to the package.json as a script so we can add it to the workflow ci.yml. Need to verify with dev about what fields are needed before creating any testing. --- .github/workflows/ci.yml | 3 + package.json | 3 +- scripts/validate-package-json.js | 203 +++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 scripts/validate-package-json.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8863cd8..55bd5c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,9 @@ jobs: - name: Lint run: npm run lint + - name: Check package schema + run: npm run check-package-schema + - name: Check benchmark schema run: npm run check-benchmark-schema diff --git a/package.json b/package.json index 10dcebf..d4ef2c7 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "lint": "eslint src benchmarks --ext .ts --max-warnings=0", "check-benchmark-schema": "node scripts/check-benchmark-schema.js", "check-bundle-size": "bun run scripts/check-bundle-size.js", - "prepublishOnly": "npm run build" + "prepublishOnly": "npm run build", + "check-package-schema": "node scripts/check-package-schema.js" }, "repository": { "type": "git", diff --git a/scripts/validate-package-json.js b/scripts/validate-package-json.js new file mode 100644 index 0000000..bac5f9a --- /dev/null +++ b/scripts/validate-package-json.js @@ -0,0 +1,203 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const packageSchema = { + strings: [ + ["name"], + ["version"], + ["description"], + ["main"], + ["module"], + ["types"], + ["author"], + ["license"], + ["type"], + ["homepage"], + ["repository", "type"], + ["repository", "url"], + ["funding", "type"], + ["funding", "url"], + ["bugs", "url"], + ["scripts","build"], + ["scripts","dev"], + ["scripts","test"], + ["scripts","lint"], + ["scripts","check-benchmark-schema"], + ["scripts","check-bundle-size"], + ["scripts","prepublishOnly"], + ["scripts","check-package-schema"] + + ], + + arrays: [ + ["keywords"], + ["files"], + ], + + booleans: [ + ["sideEffects"], + ], + + exports: [ + ".", + "./logger", + "./core/*", + "./transports/*", + ], +}; + +function validateExportEntry(exportName, entry, errors) { + const label = `exports["${exportName}"]`; + + if ( + entry === null || + typeof entry !== "object" || + Array.isArray(entry) + ) { + errors.push(`${label} must be an object`); + return; + } + + validateExportPath(entry.types, "types", ".d.ts", label, errors); + validateExportPath(entry.import, "import", ".js", label, errors); + validateExportPath(entry.require, "require", ".cjs", label, errors); + + if (exportName.includes("*")) { + for (const condition of ["types", "import", "require"]) { + const value = entry[condition]; + + if (typeof value === "string" && !value.includes("*")) { + errors.push( + `${label}.${condition} must contain "*" for a wildcard export` + ); + } + } + } +} + +function validateExportPath(value, condition, extension, label, errors) { + const fieldLabel = `${label}.${condition}`; + + if (typeof value !== "string") { + errors.push(`${fieldLabel} must be a string`); + return; + } + + if (!value.startsWith("./")) { + errors.push(`${fieldLabel} must start with "./"`); + } + + if (!value.endsWith(extension)) { + errors.push(`${fieldLabel} must end with "${extension}"`); + } +} + +function getNestedValue(data, path) { + let current = data; + + for (const key of path) { + if ( + current === null || + typeof current !== "object" || + !(key in current) + ) { + return undefined; + } + + current = current[key]; + } + + return current; +} + +function validate(data, schema) { + const errors = []; + + for (const path of schema.strings) { + const value = getNestedValue(data, path); + const label = path.join("."); + + if (value === undefined) { + errors.push(`Missing required field: ${label}`); + } else if (typeof value !== "string") { + errors.push(`${label} must be a string`); + } + } + + for (const path of schema.arrays) { + const value = getNestedValue(data, path); + const label = path.join("."); + + if (value === undefined) { + errors.push(`Missing required field: ${label}`); + } else if (!Array.isArray(value)) { + errors.push(`${label} must be an array`); + } + } + + for (const path of schema.booleans) { + const value = getNestedValue(data, path); + const label = path.join("."); + + if (value === undefined) { + errors.push(`Missing required field: ${label}`); + } else if (typeof value !== "boolean") { + errors.push(`${label} must be a boolean`); + } + } + + if ( + data.exports === null || + typeof data.exports !== "object" || + Array.isArray(data.exports) + ) { + errors.push("exports must be an object"); + } else { + for (const exportName of schema.exports) { + validateExportEntry( + exportName, + data.exports[exportName], + errors + ); + } + } + + return errors; +} + + +function checkFile(filePath, schema ) { + + try { +const contents = fs.readFileSync(filePath, "utf8"); + const data = JSON.parse(contents); + + const errors = validate(data, schema); + + if (errors.length > 0) { + console.error(`Invalid file structure: package.json`); + + for (const error of errors) { + console.error(`- ${error}`); + } + + process.exit(1); + } + + console.log(`Valid file structure: package.json`); + } catch (error) { + const message = + error instanceof Error ? error.message : String(error); + + console.error(`Could not validate package.json: ${message}`); + process.exit(1); + } +} + + +const packagePath = path.resolve(__dirname, "../package.json"); +checkFile(packagePath, packageSchema); \ No newline at end of file From ff8865d60e6e0072bc3849bb6601f32f5464dd34 Mon Sep 17 00:00:00 2001 From: Jack Sullivan <122506503+KnightOkami@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:14:04 -0400 Subject: [PATCH 2/6] Update validate-package-json.js Added initial comments for personal use. Will neaten up once done testing. --- scripts/validate-package-json.js | 301 ++++++++++++++++--------------- 1 file changed, 158 insertions(+), 143 deletions(-) diff --git a/scripts/validate-package-json.js b/scripts/validate-package-json.js index bac5f9a..382a32f 100644 --- a/scripts/validate-package-json.js +++ b/scripts/validate-package-json.js @@ -1,201 +1,216 @@ import * as fs from 'fs'; import * as path from 'path'; -import { fileURLToPath } from 'url'; +import { + fileURLToPath +} from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const packageSchema = { - strings: [ - ["name"], - ["version"], - ["description"], - ["main"], - ["module"], - ["types"], - ["author"], - ["license"], - ["type"], - ["homepage"], - ["repository", "type"], - ["repository", "url"], - ["funding", "type"], - ["funding", "url"], - ["bugs", "url"], - ["scripts","build"], - ["scripts","dev"], - ["scripts","test"], - ["scripts","lint"], - ["scripts","check-benchmark-schema"], - ["scripts","check-bundle-size"], - ["scripts","prepublishOnly"], - ["scripts","check-package-schema"] - - ], - - arrays: [ - ["keywords"], - ["files"], - ], - - booleans: [ - ["sideEffects"], - ], - - exports: [ - ".", - "./logger", - "./core/*", - "./transports/*", - ], + strings: [ + ["name"], + ["version"], + ["description"], + ["main"], + ["module"], + ["types"], + ["author"], + ["license"], + ["type"], + ["homepage"], + ["repository", "type"], + ["repository", "url"], + ["funding", "type"], + ["funding", "url"], + ["bugs", "url"], + ["scripts", "build"], + ["scripts", "dev"], + ["scripts", "test"], + ["scripts", "lint"], + ["scripts", "check-benchmark-schema"], + ["scripts", "check-bundle-size"], + ["scripts", "prepublishOnly"], + ["scripts", "check-package-schema"] + + ], + + arrays: [ + ["keywords"], + ["files"], + ], + + booleans: [ + ["sideEffects"], + ], + + exports: [ + ".", + "./logger", + "./core/*", + "./transports/*", + ], }; +//Checks the export part of the file function validateExportEntry(exportName, entry, errors) { - const label = `exports["${exportName}"]`; - - if ( - entry === null || - typeof entry !== "object" || - Array.isArray(entry) - ) { - errors.push(`${label} must be an object`); - return; - } - - validateExportPath(entry.types, "types", ".d.ts", label, errors); - validateExportPath(entry.import, "import", ".js", label, errors); - validateExportPath(entry.require, "require", ".cjs", label, errors); - - if (exportName.includes("*")) { - for (const condition of ["types", "import", "require"]) { - const value = entry[condition]; - - if (typeof value === "string" && !value.includes("*")) { - errors.push( - `${label}.${condition} must contain "*" for a wildcard export` - ); - } + const label = `exports["${exportName}"]`; + + if ( + entry === null || + typeof entry !== "object" || + Array.isArray(entry) + ) { + errors.push(`${label} must be an object`); + return; + } + + validateExportPath(entry.types, "types", ".d.ts", label, errors); + validateExportPath(entry.import, "import", ".js", label, errors); + validateExportPath(entry.require, "require", ".cjs", label, errors); + + if (exportName.includes("*")) { + for (const condition of ["types", "import", "require"]) { + const value = entry[condition]; + + if (typeof value === "string" && !value.includes("*")) { + errors.push( + `${label}.${condition} must contain "*" for a wildcard export` + ); + } + } } - } } +//given a section of export to check, the type, the extension on the file, the label in the schema, check the format of that given section of exports. function validateExportPath(value, condition, extension, label, errors) { - const fieldLabel = `${label}.${condition}`; + const fieldLabel = `${label}.${condition}`; - if (typeof value !== "string") { - errors.push(`${fieldLabel} must be a string`); - return; - } + if (typeof value !== "string") { + errors.push(`${fieldLabel} must be a string`); + return; + } - if (!value.startsWith("./")) { - errors.push(`${fieldLabel} must start with "./"`); - } + if (!value.startsWith("./")) { + errors.push(`${fieldLabel} must start with "./"`); + } - if (!value.endsWith(extension)) { - errors.push(`${fieldLabel} must end with "${extension}"`); - } + if (!value.endsWith(extension)) { + errors.push(`${fieldLabel} must end with "${extension}"`); + } } +//Given a json and a path (field), return the value of that field +//Return undefined if the data is null, data isnt an object, or the field isnt in the data function getNestedValue(data, path) { - let current = data; + let current = data; + + for (const key of path) { + if ( + current === null || + typeof current !== "object" || + !(key in current) + ) { + return undefined; + } - for (const key of path) { - if ( - current === null || - typeof current !== "object" || - !(key in current) - ) { - return undefined; + current = current[key]; } - current = current[key]; - } - - return current; + return current; } +//Main function for validating the package.json +//Expects the package.json as a json object in data +//Expects the schema of the package.json in schema function validate(data, schema) { - const errors = []; + const errors = []; - for (const path of schema.strings) { - const value = getNestedValue(data, path); - const label = path.join("."); + //Check all the fields that contain strings to ensure theyre valid format and exist in the file + for (const path of schema.strings) { + const value = getNestedValue(data, path); + const label = path.join("."); - if (value === undefined) { - errors.push(`Missing required field: ${label}`); - } else if (typeof value !== "string") { - errors.push(`${label} must be a string`); + if (value === undefined) { + errors.push(`Missing required field: ${label}`); + } else if (typeof value !== "string") { + errors.push(`${label} must be a string`); + } } - } - for (const path of schema.arrays) { - const value = getNestedValue(data, path); - const label = path.join("."); + //Check all the fields that contain arrays to ensure theyre valid format and exist in the file + for (const path of schema.arrays) { + const value = getNestedValue(data, path); + const label = path.join("."); - if (value === undefined) { - errors.push(`Missing required field: ${label}`); - } else if (!Array.isArray(value)) { - errors.push(`${label} must be an array`); + if (value === undefined) { + errors.push(`Missing required field: ${label}`); + } else if (!Array.isArray(value)) { + errors.push(`${label} must be an array`); + } } - } - for (const path of schema.booleans) { - const value = getNestedValue(data, path); - const label = path.join("."); + //Check all the fields that contain booleans to ensure theyre valid format and exist in the file + for (const path of schema.booleans) { + const value = getNestedValue(data, path); + const label = path.join("."); - if (value === undefined) { - errors.push(`Missing required field: ${label}`); - } else if (typeof value !== "boolean") { - errors.push(`${label} must be a boolean`); + if (value === undefined) { + errors.push(`Missing required field: ${label}`); + } else if (typeof value !== "boolean") { + errors.push(`${label} must be a boolean`); + } } - } - - if ( - data.exports === null || - typeof data.exports !== "object" || - Array.isArray(data.exports) - ) { - errors.push("exports must be an object"); - } else { - for (const exportName of schema.exports) { - validateExportEntry( - exportName, - data.exports[exportName], - errors - ); + + //Check all the fields that contain strings to ensure theyre valid format and exist in the file + if ( + data.exports === null || + typeof data.exports !== "object" || + Array.isArray(data.exports) + ) { + errors.push("exports must be an object"); + } else { + for (const exportName of schema.exports) { + validateExportEntry( + exportName, + data.exports[exportName], + errors + ); + } } - } - return errors; + return errors; } +//Main function. Arguments are the filepath to package.json and expected schema of the package json. +function checkFile(filePath, schema) { -function checkFile(filePath, schema ) { - - try { -const contents = fs.readFileSync(filePath, "utf8"); + try { + //Get contents of package.json + const contents = fs.readFileSync(filePath, "utf8"); const data = JSON.parse(contents); + //Save any errors from the validation function. const errors = validate(data, schema); if (errors.length > 0) { - console.error(`Invalid file structure: package.json`); + console.error(`Invalid file structure: package.json`); - for (const error of errors) { - console.error(`- ${error}`); - } + for (const error of errors) { + console.error(`- ${error}`); + } - process.exit(1); + process.exit(1); } console.log(`Valid file structure: package.json`); } catch (error) { const message = - error instanceof Error ? error.message : String(error); + error instanceof Error ? error.message : String(error); console.error(`Could not validate package.json: ${message}`); process.exit(1); - } + } } From 7e3be186decaa7f212086350566964133fedc245 Mon Sep 17 00:00:00 2001 From: Jack Sullivan <122506503+KnightOkami@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:09:40 -0400 Subject: [PATCH 3/6] Change to cjs Changed the js file to cjs to allow testing. Made a testing file for the package check. --- scripts/validate-package-json.cjs | 340 ++++++++++++ scripts/validate-package-json.js | 218 -------- tests/scripts/validate-package-json.test.ts | 582 ++++++++++++++++++++ 3 files changed, 922 insertions(+), 218 deletions(-) create mode 100644 scripts/validate-package-json.cjs delete mode 100644 scripts/validate-package-json.js create mode 100644 tests/scripts/validate-package-json.test.ts diff --git a/scripts/validate-package-json.cjs b/scripts/validate-package-json.cjs new file mode 100644 index 0000000..c0a93ac --- /dev/null +++ b/scripts/validate-package-json.cjs @@ -0,0 +1,340 @@ +const fs = require("fs"); +const path = require("path"); + +const packageSchema = { + strings: [ + ["name"], + ["version"], + ["description"], + ["main"], + ["module"], + ["types"], + ["author"], + ["license"], + ["type"], + ["homepage"], + ["repository", "type"], + ["repository", "url"], + ["funding", "type"], + ["funding", "url"], + ["bugs", "url"], + ["scripts", "build"], + ["scripts", "dev"], + ["scripts", "test"], + ["scripts", "lint"], + ["scripts", "check-benchmark-schema"], + ["scripts", "check-bundle-size"], + ["scripts", "prepublishOnly"], + ["scripts", "check-package-schema"], + ], + + arrays: [ + ["keywords"], + ["files"], + ], + + booleans: [ + ["sideEffects"], + ], + + exports: [ + ".", + "./logger", + "./core/*", + "./transports/*", + ], +}; + +/** + * Validate one path inside an exports entry. + */ +function validateExportPath( + value, + condition, + extension, + label, + errors +) { + const fieldLabel = `${label}.${condition}`; + + if (typeof value !== "string") { + errors.push(`${fieldLabel} must be a string`); + return; + } + + if (!value.startsWith("./")) { + errors.push(`${fieldLabel} must start with "./"`); + } + + if (!value.endsWith(extension)) { + errors.push( + `${fieldLabel} must end with "${extension}"` + ); + } +} + +/** + * Validate one complete entry inside package.json exports. + */ +function validateExportEntry( + exportName, + entry, + errors +) { + const label = `exports["${exportName}"]`; + + if ( + entry === null || + typeof entry !== "object" || + Array.isArray(entry) + ) { + errors.push(`${label} must be an object`); + return; + } + + validateExportPath( + entry.types, + "types", + ".d.ts", + label, + errors + ); + + validateExportPath( + entry.import, + "import", + ".js", + label, + errors + ); + + validateExportPath( + entry.require, + "require", + ".cjs", + label, + errors + ); + + if (exportName.includes("*")) { + for (const condition of [ + "types", + "import", + "require", + ]) { + const value = entry[condition]; + + if ( + typeof value === "string" && + !value.includes("*") + ) { + errors.push( + `${label}.${condition} must contain "*" for a wildcard export` + ); + } + } + } +} + +/** + * Retrieve a nested value using an array of field names. + * + * Example: + * getNestedValue(data, ["repository", "url"]) + */ +function getNestedValue(data, fieldPath) { + let current = data; + + for (const key of fieldPath) { + if ( + current === null || + typeof current !== "object" || + !(key in current) + ) { + return undefined; + } + + current = current[key]; + } + + return current; +} + +/** + * Validate a package.json object against the schema. + * + * Returns an array of error messages. + * An empty array means the object is valid. + */ +function validate(data, schema) { + const errors = []; + + if ( + data === null || + typeof data !== "object" || + Array.isArray(data) + ) { + return [ + "package.json must contain a JSON object", + ]; + } + + // Validate string fields. + for (const fieldPath of schema.strings) { + const value = getNestedValue( + data, + fieldPath + ); + + const label = fieldPath.join("."); + + if (value === undefined) { + errors.push( + `Missing required field: ${label}` + ); + } else if (typeof value !== "string") { + errors.push( + `${label} must be a string` + ); + } + } + + // Validate array fields. + for (const fieldPath of schema.arrays) { + const value = getNestedValue( + data, + fieldPath + ); + + const label = fieldPath.join("."); + + if (value === undefined) { + errors.push( + `Missing required field: ${label}` + ); + } else if (!Array.isArray(value)) { + errors.push( + `${label} must be an array` + ); + } + } + + // Validate boolean fields. + for (const fieldPath of schema.booleans) { + const value = getNestedValue( + data, + fieldPath + ); + + const label = fieldPath.join("."); + + if (value === undefined) { + errors.push( + `Missing required field: ${label}` + ); + } else if (typeof value !== "boolean") { + errors.push( + `${label} must be a boolean` + ); + } + } + + // Validate exports. + if ( + data.exports === null || + typeof data.exports !== "object" || + Array.isArray(data.exports) + ) { + errors.push( + "exports must be an object" + ); + } else { + for (const exportName of schema.exports) { + validateExportEntry( + exportName, + data.exports[exportName], + errors + ); + } + } + + return errors; +} + +/** + * Read and validate package.json. + * + * Returns true when valid and false when invalid. + */ +function checkFile(filePath, schema) { + try { + const contents = fs.readFileSync( + filePath, + "utf8" + ); + + const data = JSON.parse(contents); + const errors = validate(data, schema); + + if (errors.length > 0) { + console.error( + "Invalid file structure: package.json" + ); + + for (const error of errors) { + console.error(`- ${error}`); + } + + return false; + } + + console.log( + "Valid file structure: package.json" + ); + + return true; + } catch (error) { + const message = + error instanceof Error + ? error.message + : String(error); + + console.error( + `Could not validate package.json: ${message}` + ); + + return false; + } +} + +/** + * Export functions so Jest can import and test them. + */ +module.exports = { + packageSchema, + validateExportPath, + validateExportEntry, + getNestedValue, + validate, + checkFile, +}; + +/** + * Only validate the real package.json when this script + * is executed directly. + * + * Importing it from Jest will not run this section. + */ +if (require.main === module) { + const packagePath = path.resolve( + __dirname, + "../package.json" + ); + + const valid = checkFile( + packagePath, + packageSchema + ); + + process.exitCode = valid ? 0 : 1; +} \ No newline at end of file diff --git a/scripts/validate-package-json.js b/scripts/validate-package-json.js deleted file mode 100644 index 382a32f..0000000 --- a/scripts/validate-package-json.js +++ /dev/null @@ -1,218 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { - fileURLToPath -} from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const packageSchema = { - strings: [ - ["name"], - ["version"], - ["description"], - ["main"], - ["module"], - ["types"], - ["author"], - ["license"], - ["type"], - ["homepage"], - ["repository", "type"], - ["repository", "url"], - ["funding", "type"], - ["funding", "url"], - ["bugs", "url"], - ["scripts", "build"], - ["scripts", "dev"], - ["scripts", "test"], - ["scripts", "lint"], - ["scripts", "check-benchmark-schema"], - ["scripts", "check-bundle-size"], - ["scripts", "prepublishOnly"], - ["scripts", "check-package-schema"] - - ], - - arrays: [ - ["keywords"], - ["files"], - ], - - booleans: [ - ["sideEffects"], - ], - - exports: [ - ".", - "./logger", - "./core/*", - "./transports/*", - ], -}; - -//Checks the export part of the file -function validateExportEntry(exportName, entry, errors) { - const label = `exports["${exportName}"]`; - - if ( - entry === null || - typeof entry !== "object" || - Array.isArray(entry) - ) { - errors.push(`${label} must be an object`); - return; - } - - validateExportPath(entry.types, "types", ".d.ts", label, errors); - validateExportPath(entry.import, "import", ".js", label, errors); - validateExportPath(entry.require, "require", ".cjs", label, errors); - - if (exportName.includes("*")) { - for (const condition of ["types", "import", "require"]) { - const value = entry[condition]; - - if (typeof value === "string" && !value.includes("*")) { - errors.push( - `${label}.${condition} must contain "*" for a wildcard export` - ); - } - } - } -} - -//given a section of export to check, the type, the extension on the file, the label in the schema, check the format of that given section of exports. -function validateExportPath(value, condition, extension, label, errors) { - const fieldLabel = `${label}.${condition}`; - - if (typeof value !== "string") { - errors.push(`${fieldLabel} must be a string`); - return; - } - - if (!value.startsWith("./")) { - errors.push(`${fieldLabel} must start with "./"`); - } - - if (!value.endsWith(extension)) { - errors.push(`${fieldLabel} must end with "${extension}"`); - } -} - -//Given a json and a path (field), return the value of that field -//Return undefined if the data is null, data isnt an object, or the field isnt in the data -function getNestedValue(data, path) { - let current = data; - - for (const key of path) { - if ( - current === null || - typeof current !== "object" || - !(key in current) - ) { - return undefined; - } - - current = current[key]; - } - - return current; -} - -//Main function for validating the package.json -//Expects the package.json as a json object in data -//Expects the schema of the package.json in schema -function validate(data, schema) { - const errors = []; - - //Check all the fields that contain strings to ensure theyre valid format and exist in the file - for (const path of schema.strings) { - const value = getNestedValue(data, path); - const label = path.join("."); - - if (value === undefined) { - errors.push(`Missing required field: ${label}`); - } else if (typeof value !== "string") { - errors.push(`${label} must be a string`); - } - } - - //Check all the fields that contain arrays to ensure theyre valid format and exist in the file - for (const path of schema.arrays) { - const value = getNestedValue(data, path); - const label = path.join("."); - - if (value === undefined) { - errors.push(`Missing required field: ${label}`); - } else if (!Array.isArray(value)) { - errors.push(`${label} must be an array`); - } - } - - //Check all the fields that contain booleans to ensure theyre valid format and exist in the file - for (const path of schema.booleans) { - const value = getNestedValue(data, path); - const label = path.join("."); - - if (value === undefined) { - errors.push(`Missing required field: ${label}`); - } else if (typeof value !== "boolean") { - errors.push(`${label} must be a boolean`); - } - } - - //Check all the fields that contain strings to ensure theyre valid format and exist in the file - if ( - data.exports === null || - typeof data.exports !== "object" || - Array.isArray(data.exports) - ) { - errors.push("exports must be an object"); - } else { - for (const exportName of schema.exports) { - validateExportEntry( - exportName, - data.exports[exportName], - errors - ); - } - } - - return errors; -} - -//Main function. Arguments are the filepath to package.json and expected schema of the package json. -function checkFile(filePath, schema) { - - try { - //Get contents of package.json - const contents = fs.readFileSync(filePath, "utf8"); - const data = JSON.parse(contents); - - //Save any errors from the validation function. - const errors = validate(data, schema); - - if (errors.length > 0) { - console.error(`Invalid file structure: package.json`); - - for (const error of errors) { - console.error(`- ${error}`); - } - - process.exit(1); - } - - console.log(`Valid file structure: package.json`); - } catch (error) { - const message = - error instanceof Error ? error.message : String(error); - - console.error(`Could not validate package.json: ${message}`); - process.exit(1); - } -} - - -const packagePath = path.resolve(__dirname, "../package.json"); -checkFile(packagePath, packageSchema); \ No newline at end of file diff --git a/tests/scripts/validate-package-json.test.ts b/tests/scripts/validate-package-json.test.ts new file mode 100644 index 0000000..d53d492 --- /dev/null +++ b/tests/scripts/validate-package-json.test.ts @@ -0,0 +1,582 @@ +const { + getNestedValue, + packageSchema, + validate, + validateExportEntry, + validateExportPath, +} = require( + "../../scripts/validate-package-json.cjs" +); + +function createValidPackage() { + return { + name: "zario", + version: "0.8.0", + description: + "Fast and simple logging library for TypeScript.", + main: "./dist/index.cjs", + module: "./dist/index.js", + types: "./dist/index.d.ts", + author: "Dev-Dami", + license: "MIT", + type: "module", + homepage: + "https://github.com/Dev-Dami/zario#readme", + + repository: { + type: "git", + url: "git+https://github.com/Dev-Dami/zario.git", + }, + + funding: { + type: "github", + url: "https://github.com/sponsors/Dev-Dami", + }, + + bugs: { + url: "https://github.com/Dev-Dami/zario/issues", + }, + + scripts: { + build: "tsup", + dev: "tsup --watch", + test: "jest", + lint: "eslint src benchmarks --ext .ts --max-warnings=0", + "check-benchmark-schema": + "node scripts/check-benchmark-schema.js", + "check-bundle-size": + "bun run scripts/check-bundle-size.js", + prepublishOnly: "npm run build", + "check-package-schema": + "node scripts/validate-package-json.cjs", + }, + + keywords: [ + "logging", + "logger", + "typescript", + ], + + files: [ + "dist/**/*", + "README.md", + ], + + sideEffects: false, + + exports: { + ".": { + types: "./dist/index.d.ts", + import: "./dist/index.js", + require: "./dist/index.cjs", + }, + + "./logger": { + types: "./dist/logger.d.ts", + import: "./dist/logger.js", + require: "./dist/logger.cjs", + }, + + "./core/*": { + types: "./dist/core/*.d.ts", + import: "./dist/core/*.js", + require: "./dist/core/*.cjs", + }, + + "./transports/*": { + types: "./dist/transports/*.d.ts", + import: "./dist/transports/*.js", + require: "./dist/transports/*.cjs", + }, + }, + }; +} + +describe("getNestedValue", () => { + test("returns a top-level value", () => { + const data = { + name: "zario", + }; + + expect( + getNestedValue(data, ["name"]) + ).toBe("zario"); + }); + + test("returns a nested value", () => { + const data = { + repository: { + type: "git", + }, + }; + + expect( + getNestedValue( + data, + ["repository", "type"] + ) + ).toBe("git"); + }); + + test("returns undefined when a field is missing", () => { + const data = { + repository: {}, + }; + + expect( + getNestedValue( + data, + ["repository", "url"] + ) + ).toBeUndefined(); + }); + + test("returns undefined when an intermediate value is null", () => { + const data = { + repository: null, + }; + + expect( + getNestedValue( + data, + ["repository", "url"] + ) + ).toBeUndefined(); + }); + + test("returns undefined when an intermediate value is not an object", () => { + const data = { + repository: "git", + }; + + expect( + getNestedValue( + data, + ["repository", "url"] + ) + ).toBeUndefined(); + }); +}); + +describe("validateExportPath", () => { + test("accepts a valid export path", () => { + const errors: string[] = []; + + validateExportPath( + "./dist/index.js", + "import", + ".js", + 'exports["."]', + errors + ); + + expect(errors).toEqual([]); + }); + + test("rejects a non-string export path", () => { + const errors: string[] = []; + + validateExportPath( + null, + "import", + ".js", + 'exports["."]', + errors + ); + + expect(errors).toEqual([ + 'exports["."].import must be a string', + ]); + }); + + test('requires a path to start with "./"', () => { + const errors: string[] = []; + + validateExportPath( + "dist/index.js", + "import", + ".js", + 'exports["."]', + errors + ); + + expect(errors).toContain( + 'exports["."].import must start with "./"' + ); + }); + + test("requires the correct extension", () => { + const errors: string[] = []; + + validateExportPath( + "./dist/index.cjs", + "import", + ".js", + 'exports["."]', + errors + ); + + expect(errors).toContain( + 'exports["."].import must end with ".js"' + ); + }); + + test("reports multiple problems for one path", () => { + const errors: string[] = []; + + validateExportPath( + "dist/index.cjs", + "import", + ".js", + 'exports["."]', + errors + ); + + expect(errors).toEqual([ + 'exports["."].import must start with "./"', + 'exports["."].import must end with ".js"', + ]); + }); +}); + +describe("validateExportEntry", () => { + test("accepts a valid export entry", () => { + const errors: string[] = []; + + validateExportEntry( + ".", + { + types: "./dist/index.d.ts", + import: "./dist/index.js", + require: "./dist/index.cjs", + }, + errors + ); + + expect(errors).toEqual([]); + }); + + test("rejects a missing export entry", () => { + const errors: string[] = []; + + validateExportEntry( + ".", + undefined, + errors + ); + + expect(errors).toEqual([ + 'exports["."] must be an object', + ]); + }); + + test("rejects null as an export entry", () => { + const errors: string[] = []; + + validateExportEntry( + ".", + null, + errors + ); + + expect(errors).toEqual([ + 'exports["."] must be an object', + ]); + }); + + test("rejects an array as an export entry", () => { + const errors: string[] = []; + + validateExportEntry( + ".", + [], + errors + ); + + expect(errors).toEqual([ + 'exports["."] must be an object', + ]); + }); + + test("requires types, import, and require", () => { + const errors: string[] = []; + + validateExportEntry( + ".", + { + types: "./dist/index.d.ts", + }, + errors + ); + + expect(errors).toContain( + 'exports["."].import must be a string' + ); + + expect(errors).toContain( + 'exports["."].require must be a string' + ); + }); + + test("accepts a valid wildcard export", () => { + const errors: string[] = []; + + validateExportEntry( + "./core/*", + { + types: "./dist/core/*.d.ts", + import: "./dist/core/*.js", + require: "./dist/core/*.cjs", + }, + errors + ); + + expect(errors).toEqual([]); + }); + + test("requires wildcard export paths to contain an asterisk", () => { + const errors: string[] = []; + + validateExportEntry( + "./core/*", + { + types: "./dist/core/index.d.ts", + import: "./dist/core/index.js", + require: "./dist/core/index.cjs", + }, + errors + ); + + expect(errors).toEqual([ + 'exports["./core/*"].types must contain "*" for a wildcard export', + 'exports["./core/*"].import must contain "*" for a wildcard export', + 'exports["./core/*"].require must contain "*" for a wildcard export', + ]); + }); +}); + +describe("validate", () => { + test("accepts a valid package", () => { + const data = createValidPackage(); + + expect( + validate(data, packageSchema) + ).toEqual([]); + }); + + test("rejects null package data", () => { + expect( + validate(null, packageSchema) + ).toEqual([ + "package.json must contain a JSON object", + ]); + }); + + test("rejects array package data", () => { + expect( + validate([], packageSchema) + ).toEqual([ + "package.json must contain a JSON object", + ]); + }); + + test("reports a missing top-level string", () => { + const data = createValidPackage(); + + delete data.name; + + const errors = validate( + data, + packageSchema + ); + + expect(errors).toContain( + "Missing required field: name" + ); + }); + + test("reports a missing nested string", () => { + const data = createValidPackage(); + + delete data.repository.url; + + const errors = validate( + data, + packageSchema + ); + + expect(errors).toContain( + "Missing required field: repository.url" + ); + }); + + test("reports a string with the wrong type", () => { + const data = createValidPackage(); + + data.version = 8; + + const errors = validate( + data, + packageSchema + ); + + expect(errors).toContain( + "version must be a string" + ); + }); + + test("reports a missing array", () => { + const data = createValidPackage(); + + delete data.files; + + const errors = validate( + data, + packageSchema + ); + + expect(errors).toContain( + "Missing required field: files" + ); + }); + + test("reports an array with the wrong type", () => { + const data = createValidPackage(); + + data.keywords = "logging"; + + const errors = validate( + data, + packageSchema + ); + + expect(errors).toContain( + "keywords must be an array" + ); + }); + + test("reports a missing boolean", () => { + const data = createValidPackage(); + + delete data.sideEffects; + + const errors = validate( + data, + packageSchema + ); + + expect(errors).toContain( + "Missing required field: sideEffects" + ); + }); + + test("reports a boolean with the wrong type", () => { + const data = createValidPackage(); + + data.sideEffects = "false"; + + const errors = validate( + data, + packageSchema + ); + + expect(errors).toContain( + "sideEffects must be a boolean" + ); + }); + + test.each([ + null, + [], + "exports", + false, + ])( + "rejects invalid exports value: %p", + (exportsValue) => { + const data = createValidPackage(); + + data.exports = exportsValue; + + const errors = validate( + data, + packageSchema + ); + + expect(errors).toContain( + "exports must be an object" + ); + } + ); + + test("reports a missing required export", () => { + const data = createValidPackage(); + + delete data.exports["./logger"]; + + const errors = validate( + data, + packageSchema + ); + + expect(errors).toContain( + 'exports["./logger"] must be an object' + ); + }); + + test("reports incorrect export extensions", () => { + const data = createValidPackage(); + + data.exports["."].types = + "./dist/index.js"; + + data.exports["."].import = + "./dist/index.cjs"; + + data.exports["."].require = + "./dist/index.js"; + + const errors = validate( + data, + packageSchema + ); + + expect(errors).toContain( + 'exports["."].types must end with ".d.ts"' + ); + + expect(errors).toContain( + 'exports["."].import must end with ".js"' + ); + + expect(errors).toContain( + 'exports["."].require must end with ".cjs"' + ); + }); + + test("reports multiple independent errors", () => { + const data = createValidPackage(); + + delete data.name; + delete data.repository.url; + + data.keywords = "logging"; + data.sideEffects = "false"; + + delete data.exports["./logger"]; + + const errors = validate( + data, + packageSchema + ); + + expect(errors).toEqual( + expect.arrayContaining([ + "Missing required field: name", + "Missing required field: repository.url", + "keywords must be an array", + "sideEffects must be a boolean", + 'exports["./logger"] must be an object', + ]) + ); + }); +}); \ No newline at end of file From 1d0150304c4f438162e4c23d6367a43a644a8d64 Mon Sep 17 00:00:00 2001 From: Jack Sullivan <122506503+KnightOkami@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:15:16 -0400 Subject: [PATCH 4/6] Test format Update readme to include the new script folder for testing and rename the test file to maintain style. --- tests/README.md | 4 ++++ ...idate-package-json.test.ts => ValidatePackageJson.test.ts} | 0 2 files changed, 4 insertions(+) rename tests/scripts/{validate-package-json.test.ts => ValidatePackageJson.test.ts} (100%) diff --git a/tests/README.md b/tests/README.md index dd21158..7268e4e 100644 --- a/tests/README.md +++ b/tests/README.md @@ -40,6 +40,10 @@ tests/ ├── Logger.test.ts # Core Logger class tests ├── LogLevel.test.ts # Log level functionality tests ├── Formatter.test.ts # Output formatting tests +│ +│ +├── scripts #General script testing +│ ├── ValidatePackageJson.test.ts #Package.json check tests ├── transports/ # Transport implementation tests │ ├── Transport.test.ts # Base transport interface │ ├── ConsoleTransport.test.ts # Console output tests diff --git a/tests/scripts/validate-package-json.test.ts b/tests/scripts/ValidatePackageJson.test.ts similarity index 100% rename from tests/scripts/validate-package-json.test.ts rename to tests/scripts/ValidatePackageJson.test.ts From c1f3c70c9e6a120c770257849f70a60aca7a7f1c Mon Sep 17 00:00:00 2001 From: KnightOkami <122506503+KnightOkami@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:38:27 -0400 Subject: [PATCH 5/6] Update package.json Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d4ef2c7..0010e93 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "check-benchmark-schema": "node scripts/check-benchmark-schema.js", "check-bundle-size": "bun run scripts/check-bundle-size.js", "prepublishOnly": "npm run build", - "check-package-schema": "node scripts/check-package-schema.js" + "check-package-schema": "node scripts/validate-package-json.cjs" }, "repository": { "type": "git", From c959b05da62170314ac9f9e868cfcab5e07978bd Mon Sep 17 00:00:00 2001 From: KnightOkami <122506503+KnightOkami@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:39:32 -0400 Subject: [PATCH 6/6] Update tests/scripts/ValidatePackageJson.test.ts Tested locally, works. Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- tests/scripts/ValidatePackageJson.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/scripts/ValidatePackageJson.test.ts b/tests/scripts/ValidatePackageJson.test.ts index d53d492..1f37493 100644 --- a/tests/scripts/ValidatePackageJson.test.ts +++ b/tests/scripts/ValidatePackageJson.test.ts @@ -1,12 +1,10 @@ -const { +import { getNestedValue, packageSchema, validate, validateExportEntry, validateExportPath, -} = require( - "../../scripts/validate-package-json.cjs" -); +} from "../../scripts/validate-package-json.cjs"; function createValidPackage() { return {