UNPKG

@wc-toolkit/cem-validator

Version:

A tool to validate the content of the Custom Elements Manifest to ensure the CEM and components are properly configured.

600 lines (592 loc) 20.6 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { cemValidatorPlugin: () => cemValidatorPlugin, failures: () => failures, testCemPublished: () => testCemPublished, testComponentDefinitionPath: () => testComponentDefinitionPath, testComponentExportTypes: () => testComponentExportTypes, testComponentModulePath: () => testComponentModulePath, testComponentTagName: () => testComponentTagName, testComponentTypeDefinitionPath: () => testComponentTypeDefinitionPath, testComponents: () => testComponents, testCustomElementsProperty: () => testCustomElementsProperty, testExportsProperty: () => testExportsProperty, testMainProperty: () => testMainProperty, testManifest: () => testManifest, testModuleProperty: () => testModuleProperty, testPackageJson: () => testPackageJson, testPackageType: () => testPackageType, testSchemaVersion: () => testSchemaVersion, testTypesProperty: () => testTypesProperty, validateCem: () => validateCem }); module.exports = __toCommonJS(index_exports); // src/logger.ts var Logger = class { #debug; constructor(debug = false) { this.#debug = debug; } log(message, color = "\x1B[30m%s\x1B[0m", overrideDebug = false) { if (!this.#debug && !overrideDebug) { return; } console.log(color, message); } red(message, overrideDebug = false) { this.log(message, "\x1B[31m%s\x1B[0m", overrideDebug); } green(message, overrideDebug = false) { this.log(message, "\x1B[32m%s\x1B[0m", overrideDebug); } yellow(message, overrideDebug = false) { this.log(message, "\x1B[33m%s\x1B[0m", overrideDebug); } blue(message, overrideDebug = false) { this.log(message, "\x1B[34m%s\x1B[0m", overrideDebug); } magenta(message, overrideDebug = false) { this.log(message, "\x1B[35m%s\x1B[0m", overrideDebug); } cyan(message, overrideDebug = false) { this.log(message, "\x1B[36m%s\x1B[0m", overrideDebug); } }; // node_modules/.pnpm/@wc-toolkit+cem-utilities@1.2.0/node_modules/@wc-toolkit/cem-utilities/dist/index.js var JS_TYPES = /* @__PURE__ */ new Set([ "any", "bigint", "boolean", "never", "null", "number", "string", "Symbol", "undefined", "unknown" ]); var DOM_EVENTS = /* @__PURE__ */ new Set([ "AnimationEvent", "BeforeUnloadEvent", "ClipboardEvent", "DragEvent", "Event", "FocusEvent", "HashChangeEvent", "InputEvent", "KeyboardEvent", "MessageEvent", "MouseEvent", "MutationObserver", "PageTransitionEvent", "PointerEvent", "PopStateEvent", "ProgressEvent", "StorageEvent", "TouchEvent", "TransitionEvent", "UIEvent", "WebGLContextEvent", "WheelEvent" ]); function getComponentPublicProperties(component) { return component?.members?.filter( (member) => member.kind === "field" && member.privacy !== "private" && member.privacy !== "protected" && !member.static && !member.name.startsWith("#") ) || []; } function getComponentPublicMethods(component) { const getParameter = (p) => p.name + getParamType(p) + getParamDefaultValue(p); const getParamType = (p) => p.type?.text ? `${p.optional ? "?" : ""}: ${p.type?.text}` : ""; const getParamDefaultValue = (p) => p.default ? ` = ${p.default}` : ""; return ( // filter to return only public methods component?.members?.filter( (member) => member.kind === "method" && member.privacy !== "private" && member.privacy !== "protected" && !member.name.startsWith("#") )?.map((m) => { m.type = { text: `${m.name}(${m.parameters?.map((p) => getParameter(p)).join(", ") || ""}) => ${m.return?.type?.text || "void"}` }; return m; }) ); } function getCustomEventDetailTypes(component, excludedTypes) { const types = component?.events?.map((e) => { const eventType = e.type?.text.replace("[]", "").replace(" | undefined", ""); return eventType && !excludedTypes?.includes(eventType) && !JS_TYPES.has(eventType) && !DOM_EVENTS.has(eventType) && !eventType.includes("<") && !eventType.includes(">") && !eventType.includes(`{`) && !eventType.includes("'") && !eventType.includes(`"`) ? eventType : void 0; })?.filter((e) => e !== void 0 && !e?.startsWith("HTML")) || []; return types?.length ? [...new Set(types)] : []; } function deepMerge(target, source) { if (typeof target !== "object" || target === null) { return source; } if (typeof source !== "object" || source === null) { return target; } const targetObj = target; const sourceObj = source; for (const key of Object.keys(source)) { if (sourceObj[key] instanceof Array) { if (!targetObj[key]) { targetObj[key] = []; } targetObj[key] = targetObj[key].concat(sourceObj[key]); } else if (sourceObj[key] instanceof Object) { if (!targetObj[key]) { targetObj[key] = {}; } targetObj[key] = deepMerge(targetObj[key], sourceObj[key]); } else { targetObj[key] = sourceObj[key]; } } return targetObj; } // src/cem-validator.ts var import_fs = __toESM(require("fs"), 1); // src/utilities.ts function isValidFilePath(filePath) { const regex = /^(\/|\.\/|\.\.\/|[a-zA-Z]:[\\/]|\.\\|\.\.\\)([a-zA-Z0-9_\-./\\]+)$/; return regex.test(filePath); } function isLatestPackageVersion(currentVersion, latestVersion) { const isAlphaOrBeta = currentVersion.includes("-"); const parseVersion = (version) => version.split(".").map((x) => parseInt(x)); const [currMajor, currMinor, currPatch] = parseVersion(currentVersion); const [latestMajor, latestMinor, latestPatch] = parseVersion(latestVersion); if (currMajor !== latestMajor) { return currMajor > latestMajor; } if (currMinor !== latestMinor) { return currMinor > latestMinor; } if (currPatch !== latestPatch) { return currPatch > latestPatch; } if (currPatch !== latestPatch && isAlphaOrBeta) { return currPatch > latestPatch; } return !isAlphaOrBeta; } // src/cem-validator.ts var CURRENT_CEM_VERSION = "2.1.0"; var failures = []; var log; var userOptions = { packageJsonPath: "./package.json", cemFileName: "custom-elements.json", rules: { packageJson: { packageType: "warning", main: "warning", module: "warning", types: "warning", exports: "warning", customElementsProperty: "error", publishedCem: "error" }, manifest: { schemaVersion: "warning", modulePath: "warning", definitionPath: "warning", typeDefinitionPath: "warning", exportTypes: "error", tagName: "error" } } }; function validateCem(cem, options = {}) { log = new Logger(options.debug); if (options.skip) { log.yellow("[cem-validator] - Skipped"); return; } userOptions = deepMerge(userOptions, options); const packageJson = getPackageJson(userOptions.packageJsonPath); log.log("[cem-validator] - Validating Custom Elements Manifest..."); testRules(packageJson, cem); reportResults(); log.green("[cem-validator] - Custom Elements Manifest validation complete."); } function testRules(packageJson, cem) { testPackageJson(packageJson); testManifest(cem); } function reportResults() { const localFailures = failures.filter((f) => f.severity !== "off"); failures = []; if (!localFailures.length) { log.green("[cem-validator] - All rules passed. No issues found."); return; } const warnings = localFailures.filter((f) => f.severity === "warning"); if (warnings.length) { log.yellow(`[cem-validator] - ${warnings.length} warning(s) found.`); warnings.forEach((warning) => { log.yellow(` - ${warning.rule}: ${warning.message}`, true); }); } const errors = localFailures.filter((f) => f.severity === "error"); if (errors.length) { let errorMessage = `[cem-validator] - ${errors.length} error(s) found. `; errors.forEach((error) => { errorMessage += ` - ${error.rule}: ${error.message} `; }); if (userOptions.logErrors) { log.red(errorMessage, true); } else { throw new Error(errorMessage); } } } function testPackageJson(packageJson) { const rules = userOptions.rules.packageJson; if (!packageJson) { addFailure( "packageJson", "error", "The package.json file is missing or invalid." ); return; } testPackageType(packageJson.type, rules.packageType); testMainProperty(packageJson.main, rules.main); testModuleProperty(packageJson.module, rules.module); testTypesProperty(packageJson.types, rules.types); testExportsProperty(packageJson.exports, rules.exports); testCustomElementsProperty( packageJson.customElements, rules.customElementsProperty ); testCemPublished( packageJson.files, userOptions.cemFileName, userOptions.rules.packageJson.publishedCem ); } async function testManifest(manifest) { await testSchemaVersion( manifest.schemaVersion, userOptions.rules.manifest.schemaVersion ); testComponents(manifest); } function getPackageJson(packageJsonPath) { if (!isValidFilePath(packageJsonPath)) { throw new Error(`"${packageJsonPath}" is not a valid file path.`); } const packageJson = JSON.parse(import_fs.default.readFileSync(packageJsonPath, "utf-8")); return packageJson; } function getDefinitions(manifest) { const definitions = /* @__PURE__ */ new Map(); manifest.modules.forEach( (mod) => mod.exports?.filter((x) => x.kind === "custom-element-definition")?.forEach((x) => definitions.set(x.name, mod.path)) ); return definitions; } function testComponents(manifest) { const rules = userOptions.rules.manifest; const definitions = getDefinitions(manifest); manifest.modules.forEach((module2) => { module2.declarations?.filter((dec) => dec.customElement).forEach((component) => { testComponentTagName( component.name, component.tagName || "", rules.tagName ); testComponentModulePath(component.name, module2.path, rules.modulePath); testComponentDefinitionPath( component.name, definitions.get(component.name) || "", rules.definitionPath ); testComponentTypeDefinitionPath( component.name, module2["typeDefinitionPath"], rules.typeDefinitionPath ); testComponentExportTypes( component, module2.exports?.map((x) => x.declaration?.name) || [], rules.exportTypes ); }); }); } function testPackageType(moduleType, severity) { if (severity === "off") { return; } if (moduleType !== "module") { addFailure( "packageJson.moduleType", severity, "Package `type` is not 'module'. More information can be found at: https://nodejs.org/api/packages.html#type." ); } } function testMainProperty(main, severity) { if (severity === "off") { return; } if (!main) { addFailure("packageJson.main", severity, "Missing `main` property."); } else if (!isValidFilePath(main)) { addFailure( "packageJson.main", severity, "Invalid file path is set to `main` property. More information can be found at: https://nodejs.org/api/packages.html#main." ); } } function testModuleProperty(module2, severity) { if (severity === "off") { return; } if (!module2) { addFailure("packageJson.module", severity, "Missing `module` property."); } else if (!isValidFilePath(module2)) { addFailure( "packageJson.module", severity, "Invalid file path is set to `module` property. More information can be found at: https://nodejs.org/api/packages.html#module." ); } } function testTypesProperty(types, severity) { if (severity === "off") { return; } if (!types) { addFailure( "packageJson.types", severity, "The package.json is missing a `types` property. More information can be found at: https://nodejs.org/api/packages.html#community-conditions-definitions." ); } else if (!isValidFilePath(types)) { addFailure( "packageJson.types", severity, "Invalid file path is set to `types` property in the package.json. More information can be found at: https://nodejs.org/api/packages.html#community-conditions-definitions" ); } } function testExportsProperty(exports2, severity) { if (severity === "off") { return; } if (!exports2) { addFailure( "packageJson.exports", severity, "The package.json is missing an `exports` property. More information can be found at: https://nodejs.org/api/packages.html#exports." ); } } function testCustomElementsProperty(customElements, severity) { if (severity === "off") { return; } if (!customElements) { addFailure( "packageJson.customElements", severity, "The package.json is missing the `customElements` property. You can find more information at: https://github.com/webcomponents/custom-elements-manifest?tab=readme-ov-file#referencing-manifests-from-npm-packages" ); } else if (!isValidFilePath(customElements)) { addFailure( "packageJson.customElements", severity, "Invalid file path is set to `customElements` property." ); } } function testCemPublished(files, cemFileName, severity) { if (severity === "off" || !files?.length) { return; } const hasCem = files.some((file) => file.endsWith(cemFileName)); if (!hasCem) { addFailure( "packageJson.publishedCem", severity, "The package.json is missing the `custom-elements.json` file in the `files` property. More information can be found at: https://docs.npmjs.com/cli/v10/configuring-npm/package-json?v=true#files." ); } } async function testSchemaVersion(schemaVersion, severity) { if (severity === "off") { return; } if (!schemaVersion) { addFailure( "manifest.schemaVersion", severity, "The manifest is missing the `schemaVersion` property. For more information, check out: https://github.com/webcomponents/custom-elements-manifest?tab=readme-ov-file#schema-versioning" ); return; } if (!isLatestPackageVersion(schemaVersion, CURRENT_CEM_VERSION)) { addFailure( "manifest.schemaVersion", severity, `The manifest schema version is outdated. The latest version is ${CURRENT_CEM_VERSION}. For more information, check out: https://github.com/webcomponents/custom-elements-manifest?tab=readme-ov-file#schema-versioning` ); } } function testComponentModulePath(componentName, modulePath, severity) { if (severity === "off") { return; } let failureMessage = ""; if (!modulePath) { failureMessage = `${componentName} is missing a module path. For help updating this, check out: https://wc-toolkit.com/documentation/module-path-resolver/`; } else if (modulePath.endsWith(".ts") || modulePath.includes("src/")) { failureMessage = `${componentName} module path does not appear to reference the output path. For help updating this, check out: https://wc-toolkit.com/documentation/module-path-resolver/`; } else if (!isValidFilePath(modulePath)) { failureMessage = `${modulePath} module path is invalid. For help updating this, check out: https://wc-toolkit.com/documentation/module-path-resolver/`; } if (failureMessage) { addFailure("manifest.modulePath", severity, failureMessage); } } function testComponentDefinitionPath(componentName, definitionPath, severity) { if (severity === "off") { return; } let failureMessage = ""; if (!definitionPath) { failureMessage = `${componentName} is missing a definition path. For help updating this, check out: https://wc-toolkit.com/documentation/module-path-resolver/`; } else if (definitionPath.endsWith(".ts") || !definitionPath.includes("src/")) { failureMessage = `${componentName} definition path does not appear to reference the output path. For help updating this, check out: https://wc-toolkit.com/documentation/module-path-resolver/`; } else if (!isValidFilePath(definitionPath)) { failureMessage = `${definitionPath} definition path is invalid. For help updating this, check out: https://wc-toolkit.com/documentation/module-path-resolver/`; } if (failureMessage) { addFailure("manifest.definitionPath", severity, failureMessage); } } function testComponentTypeDefinitionPath(componentName, definitionPath, severity) { if (severity === "off") { return; } let failureMessage = ""; if (!definitionPath) { return; } else if (definitionPath.endsWith(".ts") || !definitionPath.includes("src/")) { failureMessage = `${componentName} definition path does not appear to reference the output path. For help updating this, check out: https://wc-toolkit.com/documentation/module-path-resolver/`; } else if (!isValidFilePath(definitionPath)) { failureMessage = `${definitionPath} definition path is invalid. For help updating this, check out: https://wc-toolkit.com/documentation/module-path-resolver/`; } if (failureMessage) { addFailure("manifest.modulePath", severity, failureMessage); } } function testComponentExportTypes(component, exports2, severity) { if (severity === "off") { return; } const eventTypes = getCustomEventDetailTypes(component) || []; const props = getComponentPublicProperties(component) || []; const propTypes = props.map((prop) => prop.type?.text); const methods = getComponentPublicMethods(component) || []; const methodTypes = methods?.map((method) => method.parameters?.map((param) => param?.type?.text)).flat(); const allTypes = [...eventTypes, ...propTypes, ...methodTypes].filter( (type) => type && isNamedType(type) ); allTypes.forEach((type) => { if (!exports2.includes(type)) { addFailure( "manifest.exportTypes", severity, `${component.name} is missing exported type "${type}".` ); } }); } function testComponentTagName(componentName, tagName, severity) { if (severity === "off") { return; } if (!tagName) { addFailure( "manifest.tagName", severity, `${componentName} is missing a tag name. You can add one by using the \`@tag\` and \`@tagName\` JSDoc tag.` ); } } function isNamedType(type) { return !type.startsWith("HTML") && !type.startsWith("SVG") && !JS_TYPES.has(type || "") && !type.includes('"') && !type.includes("'"); } function addFailure(rule, severity, message) { failures.push({ rule, severity, message }); } // src/cem-plugin.ts function cemValidatorPlugin(options = {}) { return { name: "@wc-toolkit/cem-validator", packageLinkPhase({ customElementsManifest }) { validateCem(customElementsManifest, options); } }; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { cemValidatorPlugin, failures, testCemPublished, testComponentDefinitionPath, testComponentExportTypes, testComponentModulePath, testComponentTagName, testComponentTypeDefinitionPath, testComponents, testCustomElementsProperty, testExportsProperty, testMainProperty, testManifest, testModuleProperty, testPackageJson, testPackageType, testSchemaVersion, testTypesProperty, validateCem });