diff --git a/.changeset/slick-bats-brake.md b/.changeset/slick-bats-brake.md new file mode 100644 index 0000000000..6e09899203 --- /dev/null +++ b/.changeset/slick-bats-brake.md @@ -0,0 +1,12 @@ +--- +"@patternfly/pfe-tools": minor +--- +**TypeScript**: Add static version transformer. This adds a runtime-only +static `version` field to custom element classes. + +```js +import '@patternfly/elements/pf-button/pf-button.js'; +const PFE_VERSION = + await customElements.whenDefined('pf-button') + .then(PfButton => PfButton.version); +``` diff --git a/tools/pfe-tools/package.json b/tools/pfe-tools/package.json index 35474b8b04..3f518f545a 100644 --- a/tools/pfe-tools/package.json +++ b/tools/pfe-tools/package.json @@ -40,7 +40,8 @@ "./test/render-to-string.js": "./test/render-to-string.js", "./test/stub-logger.js": "./test/stub-logger.js", "./test/utils.js": "./test/utils.js", - "./typescript/transformers/css-imports.cjs": "./typescript/transformers/css-imports.cjs" + "./typescript/transformers/css-imports.cjs": "./typescript/transformers/css-imports.cjs", + "./typescript/transformers/static-version.cjs": "./typescript/transformers/static-version.cjs" }, "contributors": [ "Kyle Buchanan (https://github.com/kylebuch8)", diff --git a/tools/pfe-tools/typescript/transformers/css-imports.cjs b/tools/pfe-tools/typescript/transformers/css-imports.cjs index c3682fe7ad..61414c18f7 100644 --- a/tools/pfe-tools/typescript/transformers/css-imports.cjs +++ b/tools/pfe-tools/typescript/transformers/css-imports.cjs @@ -1,5 +1,4 @@ -// @ts-check -const ts = require('typescript/lib/typescript'); +const ts = require('typescript'); const fs = require('node:fs'); const path = require('node:path'); const { pathToFileURL } = require('node:url'); @@ -7,8 +6,8 @@ const { pathToFileURL } = require('node:url'); const SEEN_SOURCES = new WeakSet(); /** - * @param {import('typescript').CoreTransformationContext} ctx - * @param {import('typescript').SourceFile} sourceFile + * @param {ts.CoreTransformationContext} ctx + * @param {ts.SourceFile} sourceFile */ function createLitCssImportStatement(ctx, sourceFile) { if (SEEN_SOURCES.has(sourceFile)) { @@ -45,8 +44,8 @@ function createLitCssImportStatement(ctx, sourceFile) { } /** - * @param {import('typescript').CoreTransformationContext} ctx - * @param {string} stylesheet + * @param {ts.CoreTransformationContext} ctx + * @param {ts.SourceFile} sourceFile * @param {string} [name] */ function createLitCssTaggedTemplateLiteral(ctx, stylesheet, name) { @@ -87,18 +86,14 @@ function minifyCss(stylesheet, filePath) { } } -/** - * @param node - * @param{import('typescript').ImportDeclaration} node - */ +/** @param {ts.ImportDeclaration} node */ function getImportSpecifier(node) { return node.moduleSpecifier.getText().replace(/^'(.*)'$/, '$1'); } /** - * @param node - * @param{import('typescript').Node} node - * @returns {node is import('typescript').ImportDeclaration} + * @param {ts.Node} node + * @returns {node is ts.ImportDeclaration} */ function isCssImportNode(node) { if (ts.isImportDeclaration(node) && !node.importClause?.isTypeOnly) { @@ -115,11 +110,7 @@ const cssImportSpecImporterMap = new Map(); /** map from (abspath to import spec) to (abspaths to manually written transformed module) */ const cssImportFakeEmitMap = new Map(); -// abspath to file -/** - * @param node - * @param{import('typescript').ImportDeclaration} node - */ +/** @param {ts.ImportDeclaration} node */ function getImportAbsPathOrBareSpec(node) { const specifier = getImportSpecifier(node); if (!specifier.startsWith('.')) { @@ -131,9 +122,7 @@ function getImportAbsPathOrBareSpec(node) { } } -/** - * @param {import('typescript').SourceFile} sourceFile - */ +/** @param {ts.SourceFile} sourceFile */ function cacheCssImportSpecsAbsolute(sourceFile) { sourceFile.forEachChild(node => { if (isCssImportNode(node)) { @@ -151,13 +140,16 @@ function cacheCssImportSpecsAbsolute(sourceFile) { * If the inline option is set, remove the import specifier and print the css * object in place, except if that module is imported elsewhere in the project, * in which case leave a `.css.js` import - * @param {import('typescript').Program} program - * @param root0 - * @param root0.inline - * @param root0.minify - * @returns {import('typescript').TransformerFactory} + * @param {ts.Program} program + * @param opts + * @param {boolean} opts.inline + * @param {boolean} opts.minify + * @returns {ts.TransformerFactory} */ -module.exports = function(program, { inline = false, minify = false } = {}) { +module.exports = function(program, { + inline = false, + minify = false, +} = {}) { return ctx => { for (const sourceFileName of program.getRootFileNames()) { const sourceFile = program.getSourceFile(sourceFileName); @@ -166,10 +158,7 @@ module.exports = function(program, { inline = false, minify = false } = {}) { } } - /** - * @param node - * @param{import('typescript').Node} node - */ + /** @param {ts.Node} node */ function rewriteOrInlineVisitor(node) { if (isCssImportNode(node)) { const { fileName } = node.getSourceFile(); @@ -210,12 +199,12 @@ module.exports = function(program, { inline = false, minify = false } = {}) { return sourceFile => { const children = sourceFile.getChildren(); const litImportBindings = - /** @type{import('typescript').ImportDeclaration}*/(children.find(x => + (children.find(/** @returns {x is ts.ImportDeclaration} */x => !ts.isTypeOnlyImportOrExportDeclaration(x) - && !ts.isNamespaceImport(x) - && ts.isImportDeclaration(x) - && x.moduleSpecifier.getText() === 'lit' - && x.importClause?.namedBindings + && !ts.isNamespaceImport(x) + && ts.isImportDeclaration(x) + && x.moduleSpecifier.getText() === 'lit' + && !!x.importClause?.namedBindings ))?.importClause?.namedBindings; const hasStyleImports = children.find(x => @@ -223,8 +212,8 @@ module.exports = function(program, { inline = false, minify = false } = {}) { if (hasStyleImports) { if (litImportBindings - && ts.isNamedImports(litImportBindings) - && !litImportBindings.elements?.some(x => x.getText() === 'css')) { + && ts.isNamedImports(litImportBindings) + && !litImportBindings.elements?.some(x => x.getText() === 'css')) { ctx.factory.updateNamedImports( litImportBindings, [ diff --git a/tools/pfe-tools/typescript/transformers/static-version.cjs b/tools/pfe-tools/typescript/transformers/static-version.cjs new file mode 100644 index 0000000000..3f221417d5 --- /dev/null +++ b/tools/pfe-tools/typescript/transformers/static-version.cjs @@ -0,0 +1,84 @@ +const ts = require('typescript'); +const fs = require('node:fs'); +const path = require('node:path'); + +/** + * @param {ts.ModifierLike} mod + * @returns {mod is ts.ExportKeyword} + */ +const isExportKeyword = mod => + mod.kind === ts.SyntaxKind.ExportKeyword; + +/** + * @param {ts.ModifierLike} mod + * @returns {mod is ts.Decorator} + */ +const isCustomElementDecorator = mod => + ts.isDecorator(mod) + && ts.isCallExpression(mod.expression) + && ts.isIdentifier(mod.expression.expression) + && mod.expression.expression.escapedText === 'customElement'; + +/** + * @param {ts.Node} node + * @returns {node is ts.ClassDeclaration} + */ +const isExportCustomElementClass = node => + ts.isClassDeclaration(node) + && !!node.modifiers?.some(isExportKeyword) + && !!node.modifiers?.some(isCustomElementDecorator); + +/** @param {string} dir */ +function findPackageDir(dir) { + if (fs.existsSync(path.join(dir, 'package.json'))) { + return dir; + } + const parentDir = path.resolve(dir, '..'); + if (dir === parentDir) { + return null; + } + return findPackageDir(parentDir); +} + +/** @param {string} filePath */ +function getNearestPackageJson(filePath) { + const parentDir = path.dirname(filePath); + const packageDir = findPackageDir(parentDir); + if (packageDir) { + const filePath = path.normalize(`${packageDir}/package.json`); + return require(filePath); + } else { + return null; + } +} + +/** @returns {ts.TransformerFactory} */ +module.exports = () => ctx => { + return sourceFile => ts.visitEachChild( + sourceFile, + function addVersionVisitor(node) { + if (isExportCustomElementClass(node)) { + const { fileName } = node.getSourceFile(); + const packageJson = getNearestPackageJson(fileName); + if (packageJson?.version) { + return ctx.factory.createClassDeclaration( + node.modifiers, + node.name, + node.typeParameters, + node.heritageClauses, + node.members.concat(ctx.factory.createPropertyDeclaration( + [ctx.factory.createModifier(ts.SyntaxKind.StaticKeyword)], + 'version', + undefined, + undefined, + ctx.factory.createStringLiteral(packageJson.version) + )) + ); + } + } + return node; + }, + ctx + ); +}; + diff --git a/tsconfig.settings.json b/tsconfig.settings.json index 5a7bbf2bae..ecd77cb42f 100644 --- a/tsconfig.settings.json +++ b/tsconfig.settings.json @@ -40,6 +40,9 @@ "transform": "@patternfly/pfe-tools/typescript/transformers/css-imports.cjs", "inline": true }, + { + "transform": "@patternfly/pfe-tools/typescript/transformers/static-version.cjs" + }, { "name": "typescript-lit-html-plugin" },