11// @ts -check
22const ts = require ( 'typescript/lib/typescript' ) ;
33const fs = require ( 'node:fs' ) ;
4+ const path = require ( 'node:path' ) ;
45const { pathToFileURL } = require ( 'node:url' ) ;
56
67const 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