diff --git a/custom-elements-manifest.mjs b/custom-elements-manifest.mjs new file mode 100644 index 000000000..5a444d3a1 --- /dev/null +++ b/custom-elements-manifest.mjs @@ -0,0 +1,217 @@ +import * as path from 'path'; +import { customElementJetBrainsPlugin } from 'custom-element-jet-brains-integration'; +import { customElementVsCodePlugin } from 'custom-element-vs-code-integration'; +import { parse } from 'comment-parser'; +import { pascalCase } from 'pascal-case'; +import commandLineArgs from 'command-line-args'; +import fs from 'fs'; + +const packageData = JSON.parse(fs.readFileSync('./package.json', 'utf8')); +const { name, description, version, author, homepage, license } = packageData; + +const { outdir } = commandLineArgs([ + { name: 'litelement', type: String }, + { name: 'analyze', defaultOption: true }, + { name: 'outdir', type: String } +]); + +function noDash(string) { + return string.replace(/^\s?-/, '').trim(); +} + +function replace(string, terms) { + terms.forEach(({ from, to }) => { + string = string?.replace(from, to); + }); + + return string; +} + +export default { + globs: ['src/components/**/*.component.ts'], + exclude: ['**/*.styles.ts', '**/*.test.ts'], + plugins: [ + // Append package data + { + name: 'wa-package-data', + packageLinkPhase({ customElementsManifest }) { + customElementsManifest.package = { name, description, version, author, homepage, license }; + } + }, + // Infer tag names because we no longer use @customElement decorators. + { + name: 'wa-infer-tag-names', + analyzePhase({ ts, node, moduleDoc }) { + switch (node.kind) { + case ts.SyntaxKind.ClassDeclaration: { + const className = node.name.getText(); + const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className); + + const importPath = moduleDoc.path; + + // This is kind of a best guess at components. "thing.component.ts" + if (!importPath.endsWith('.component.ts')) { + return; + } + + const tagNameWithoutPrefix = path.basename(importPath, '.component.ts'); + const tagName = 'wa-' + tagNameWithoutPrefix; + + classDoc.tagNameWithoutPrefix = tagNameWithoutPrefix; + classDoc.tagName = tagName; + + // This used to be set to true by @customElement + classDoc.customElement = true; + } + } + } + }, + // Parse custom jsDoc tags + { + name: 'wa-custom-tags', + analyzePhase({ ts, node, moduleDoc }) { + switch (node.kind) { + case ts.SyntaxKind.ClassDeclaration: { + const className = node.name.getText(); + const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className); + const customTags = ['animation', 'dependency', 'documentation', 'since', 'status', 'title']; + let customComments = '/**'; + + node.jsDoc?.forEach(jsDoc => { + jsDoc?.tags?.forEach(tag => { + const tagName = tag.tagName.getText(); + + if (customTags.includes(tagName)) { + customComments += `\n * @${tagName} ${tag.comment}`; + } + }); + }); + + // This is what allows us to map JSDOC comments to ReactWrappers. + classDoc['jsDoc'] = node.jsDoc?.map(jsDoc => jsDoc.getFullText()).join('\n'); + + const parsed = parse(`${customComments}\n */`); + parsed[0].tags?.forEach(t => { + switch (t.tag) { + // Animations + case 'animation': + if (!Array.isArray(classDoc['animations'])) { + classDoc['animations'] = []; + } + classDoc['animations'].push({ + name: t.name, + description: noDash(t.description) + }); + break; + + // Dependencies + case 'dependency': + if (!Array.isArray(classDoc['dependencies'])) { + classDoc['dependencies'] = []; + } + classDoc['dependencies'].push(t.name); + break; + + // Value-only metadata tags + case 'documentation': + case 'since': + case 'status': + case 'title': + classDoc[t.tag] = t.name; + break; + + // All other tags + default: + if (!Array.isArray(classDoc[t.tag])) { + classDoc[t.tag] = []; + } + + classDoc[t.tag].push({ + name: t.name, + description: t.description, + type: t.type || undefined + }); + } + }); + } + } + } + }, + { + name: 'wa-react-event-names', + analyzePhase({ ts, node, moduleDoc }) { + switch (node.kind) { + case ts.SyntaxKind.ClassDeclaration: { + const className = node.name.getText(); + const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className); + + if (classDoc?.events) { + classDoc.events.forEach(event => { + event.reactName = `on${pascalCase(event.name)}`; + event.eventName = `${pascalCase(event.name)}Event`; + }); + } + } + } + } + }, + { + name: 'wa-translate-module-paths', + packageLinkPhase({ customElementsManifest }) { + customElementsManifest?.modules?.forEach(mod => { + // + // CEM paths look like this: + // + // src/components/button/button.ts + // + // But we want them to look like this: + // + // components/button/button.js + // + const terms = [ + { from: /^src\//, to: '' }, // Strip the src/ prefix + { from: /\.component.(t|j)sx?$/, to: '.js' } // Convert .ts to .js + ]; + + mod.path = replace(mod.path, terms); + + for (const ex of mod.exports ?? []) { + ex.declaration.module = replace(ex.declaration.module, terms); + } + + for (const dec of mod.declarations ?? []) { + if (dec.kind === 'class') { + for (const member of dec.members ?? []) { + if (member.inheritedFrom) { + member.inheritedFrom.module = replace(member.inheritedFrom.module, terms); + } + } + } + } + }); + } + }, + // Generate custom VS Code data + customElementVsCodePlugin({ + outdir, + cssFileName: null, + referencesTemplate: (_, tag) => [ + { + name: 'Documentation', + url: `https://shoelace.style/components/${tag.replace('wa-', '')}` + } + ] + }), + customElementJetBrainsPlugin({ + outdir: './dist', + excludeCss: true, + packageJson: false, + referencesTemplate: (_, tag) => { + return { + name: 'Documentation', + url: `https://shoelace.style/components/${tag.replace('wa-', '')}` + }; + } + }) + ] +}; diff --git a/web-test-runner.config.js b/web-test-runner.config.js index 1c8d5746c..833398a5c 100644 --- a/web-test-runner.config.js +++ b/web-test-runner.config.js @@ -6,7 +6,9 @@ export default { rootDir: '.', files: 'src/**/*.test.ts', // "default" group concurrentBrowsers: 3, - nodeResolve: true, + nodeResolve: { + exportConditions: ['production', 'default'] + }, testFramework: { config: { timeout: 3000,