Skip to content

Commit fd1202d

Browse files
authored
feat(tools): css import caching transformer (#2689)
1 parent e504281 commit fd1202d

File tree

2 files changed

+95
-21
lines changed

2 files changed

+95
-21
lines changed

.changeset/long-eyes-mate.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
"@patternfly/pfe-tools": patch
3+
---
4+
`typescript/css-imports`: prevent shared css modules from being inlined to files; emit them instead.

tools/pfe-tools/typescript/transformers/css-imports.cjs

Lines changed: 91 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// @ts-check
22
const ts = require('typescript/lib/typescript');
33
const fs = require('node:fs');
4+
const path = require('node:path');
45
const { pathToFileURL } = require('node:url');
56

67
const SEEN_SOURCES = new WeakSet();
@@ -84,22 +85,86 @@ function minifyCss(stylesheet, filePath) {
8485
}
8586
}
8687

88+
/**
89+
* @param{import('typescript').ImportDeclaration} node
90+
*/
91+
function getImportSpecifier(node) {
92+
return node.moduleSpecifier.getText().replace(/^'(.*)'$/, '$1');
93+
}
94+
95+
/**
96+
* @param{import('typescript').Node} node
97+
* @return {node is import('typescript').ImportDeclaration}
98+
*/
99+
function isCssImportNode(node) {
100+
if (ts.isImportDeclaration(node) && !node.importClause?.isTypeOnly) {
101+
const specifier = getImportSpecifier(node);
102+
return specifier.endsWith('.css');
103+
} else {
104+
return false;
105+
}
106+
}
107+
108+
/** map from (abspath to import spec) to (set of abspaths to importers) */
109+
const cssImportSpecImporterMap = new Map();
110+
111+
/** map from (abspath to import spec) to (abspaths to manually written transformed module) */
112+
const cssImportFakeEmitMap = new Map();
113+
114+
// abspath to file
115+
/** @param{import('typescript').ImportDeclaration} node */
116+
function getImportAbsPathOrBareSpec(node) {
117+
const specifier = getImportSpecifier(node);
118+
if (!specifier.startsWith('.')) {
119+
return specifier;
120+
} else {
121+
const { fileName } = node.getSourceFile();
122+
const specifierRelative = path.resolve(path.dirname(fileName), specifier);
123+
return specifierRelative;
124+
}
125+
}
126+
127+
/**
128+
* @param {import('typescript').SourceFile} sourceFile
129+
*/
130+
function cacheCssImportSpecsAbsolute(sourceFile) {
131+
sourceFile.forEachChild(node => {
132+
if (isCssImportNode(node)) {
133+
const specifierAbs = getImportAbsPathOrBareSpec(node);
134+
cssImportSpecImporterMap.set(specifierAbs, new Set([
135+
...cssImportSpecImporterMap.get(specifierAbs) ?? [],
136+
node.getSourceFile().fileName,
137+
]));
138+
}
139+
});
140+
}
141+
87142
/**
88143
* Replace .css import specifiers with .css.js import specifiers
89-
* @param {import('typescript').Program} _program
90-
* @return {import('typescript').TransformerFactory<import('typescript').Node>}
144+
* If the inline option is set, remove the import specifier and print the css
145+
* object in place, except if that module is imported elsewhere in the project,
146+
* in which case leave a `.css.js` import
147+
* @param {import('typescript').Program} program
148+
* @return {import('typescript').TransformerFactory<import('typescript').SourceFile>}
91149
*/
92-
module.exports = function(_program, { inline = false, minify = false } = {}) {
150+
module.exports = function(program, { inline = false, minify = false } = {}) {
93151
return ctx => {
94-
/**
95-
* @param {import('typescript').Node} node
96-
*/
97-
function visitor(node) {
98-
if (ts.isImportDeclaration(node) && !node.importClause?.isTypeOnly) {
99-
const specifier = node.moduleSpecifier.getText().replace(/^'(.*)'$/, '$1');
100-
if (specifier.endsWith('.css')) {
101-
if (inline) {
102-
const { fileName } = node.getSourceFile();
152+
for (const sourceFileName of program.getRootFileNames()) {
153+
const sourceFile = program.getSourceFile(sourceFileName);
154+
if (sourceFile && !sourceFile.isDeclarationFile) {
155+
cacheCssImportSpecsAbsolute(sourceFile);
156+
}
157+
}
158+
159+
/** @param{import('typescript').Node} node */
160+
function rewriteOrInlineVisitor(node) {
161+
if (isCssImportNode(node)) {
162+
const { fileName } = node.getSourceFile();
163+
const specifier = getImportSpecifier(node);
164+
const specifierAbs = getImportAbsPathOrBareSpec(node);
165+
if (inline) {
166+
const cached = cssImportSpecImporterMap.get(specifierAbs);
167+
if (cached?.size === 1) {
103168
const dir = pathToFileURL(fileName);
104169
const url = new URL(specifier, dir);
105170
const content = fs.readFileSync(url, 'utf-8');
@@ -108,21 +173,26 @@ module.exports = function(_program, { inline = false, minify = false } = {}) {
108173
createLitCssImportStatement(ctx, node.getSourceFile()),
109174
createLitCssTaggedTemplateLiteral(ctx, stylesheet, node.importClause?.name?.getText()),
110175
];
111-
} else {
112-
return ctx.factory.createImportDeclaration(
113-
node.modifiers,
114-
node.importClause,
115-
ctx.factory.createStringLiteral(`${specifier}.js`)
116-
);
176+
} else if (!cssImportFakeEmitMap.get(specifierAbs)) {
177+
const outPath = `${specifierAbs}.js`;
178+
const css = fs.readFileSync(specifierAbs, 'utf8');
179+
const stylesheet = minify ? minifyCss(css, specifierAbs) : css;
180+
fs.writeFileSync(outPath, `import { css } from 'lit';\nexport default css\`${stylesheet}\`;`, 'utf8');
181+
cssImportFakeEmitMap.set(specifierAbs, outPath);
117182
}
118183
}
184+
return ctx.factory.createImportDeclaration(
185+
node.modifiers,
186+
node.importClause,
187+
ctx.factory.createStringLiteral(`${specifier}.js`)
188+
);
119189
}
120-
return ts.visitEachChild(node, visitor, ctx);
190+
return ts.visitEachChild(node, rewriteOrInlineVisitor, ctx);
121191
}
122192

123193
return sourceFile => {
124194
const children = sourceFile.getChildren();
125-
const litImportBindings = /** @type {ts.ImportDeclaration|undefined} */(children.find(x =>
195+
const litImportBindings = /** @type{import('typescript').ImportDeclaration}*/(children.find(x =>
126196
!ts.isTypeOnlyImportOrExportDeclaration(x) &&
127197
!ts.isNamespaceImport(x) &&
128198
ts.isImportDeclaration(x) &&
@@ -142,7 +212,7 @@ module.exports = function(_program, { inline = false, minify = false } = {}) {
142212
);
143213
}
144214
}
145-
return ts.visitEachChild(sourceFile, visitor, ctx);
215+
return ts.visitEachChild(sourceFile, rewriteOrInlineVisitor, ctx);
146216
};
147217
};
148218
};

0 commit comments

Comments
 (0)