@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
JavaScript
;
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
});