Skip to content

Commit

Permalink
feat(tools): ts transform to add static version to elements
Browse files Browse the repository at this point in the history
  • Loading branch information
bennypowers committed Jun 10, 2024
1 parent 77dff66 commit 559bd47
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 39 deletions.
12 changes: 12 additions & 0 deletions .changeset/slick-bats-brake.md
Original file line number Diff line number Diff line change
@@ -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);
```
3 changes: 2 additions & 1 deletion tools/pfe-tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]> (https://github.com/kylebuch8)",
Expand Down
65 changes: 27 additions & 38 deletions tools/pfe-tools/typescript/transformers/css-imports.cjs
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
// @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');

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)) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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('.')) {
Expand All @@ -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)) {
Expand All @@ -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<import('typescript').SourceFile>}
* @param {ts.Program} program
* @param opts
* @param {boolean} opts.inline
* @param {boolean} opts.minify
* @returns {ts.TransformerFactory<ts.SourceFile>}
*/
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);
Expand All @@ -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();
Expand Down Expand Up @@ -210,21 +199,21 @@ 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 =>
ts.isImportDeclaration(x) && x.moduleSpecifier.getText().endsWith('.css'));

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,
[
Expand Down
84 changes: 84 additions & 0 deletions tools/pfe-tools/typescript/transformers/static-version.cjs
Original file line number Diff line number Diff line change
@@ -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<ts.SourceFile>} */
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
);
};

3 changes: 3 additions & 0 deletions tsconfig.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down

0 comments on commit 559bd47

Please sign in to comment.