diff --git a/index.ts b/index.ts index 77e563e..c813a84 100644 --- a/index.ts +++ b/index.ts @@ -1,6 +1,13 @@ -import {createFilter} from '@rollup/pluginutils'; +import { createFilter } from '@rollup/pluginutils'; +import type { + ArrowFunctionExpression, + Identifier, + Literal, + Node, + TemplateLiteral, +} from 'estree'; import * as transformAst from 'transform-ast'; -import {PluginOption} from 'vite'; +import { PluginOption } from 'vite'; interface PostcssLitOptions { /** @@ -27,18 +34,19 @@ interface PostcssLitOptions { importPackage?: string; } -const escape = (str: string): string => str - .replace(/`/g, '\\`') - .replace(/\\(?!`)/g, '\\\\'); +const CSS_TAG = 'cssTag'; + +const escape = (str: string): string => + str.replace(/`/g, '\\`').replace(/\\(?!`)/g, '\\\\'); export = function postcssLit(options: PostcssLitOptions = {}): PluginOption { const defaultOptions: PostcssLitOptions = { include: '**/*.{css,sss,pcss,styl,stylus,sass,scss,less}?(*)', - exclude: '**/*\?direct*', + exclude: '**/*?direct*', importPackage: 'lit', }; - const opts: PostcssLitOptions = {...defaultOptions, ...options}; + const opts: PostcssLitOptions = { ...defaultOptions, ...options }; const filter = createFilter(opts.include, opts.exclude); return { @@ -47,37 +55,75 @@ export = function postcssLit(options: PostcssLitOptions = {}): PluginOption { transform(code, id) { if (!filter(id)) return; const ast = this.parse(code, {}); - // export default const css; - let defaultExportName; - - // export default '...'; - let isDeclarationLiteral = false; - const magicString = transformAst(code, {ast: ast}, - node => { - if (node.type === 'ExportDefaultDeclaration') { - defaultExportName = node.declaration.name; - isDeclarationLiteral = node.declaration.type === 'Literal'; - } - }, - ); + let defaultExportName: string; + let cssStringNode: Literal | TemplateLiteral; + const magicString = transformAst(code, { ast: ast }); - if (!defaultExportName && !isDeclarationLiteral) { - return; - } - magicString.walk(node => { - if (defaultExportName && node.type === 'VariableDeclaration') { - const exportedVar = node.declarations.find(d => d.id.name === defaultExportName); - if (exportedVar) { - exportedVar.init.edit.update(`cssTag\`${escape(exportedVar.init.value)}\``); + magicString.walk((node: Node) => { + if (node.type === 'ExportDefaultDeclaration') { + switch (node.declaration.type) { + case 'Literal': // export default '...'; + case 'TemplateLiteral': // export default `...`; + cssStringNode = node.declaration; + break; + case 'Identifier': // const css = '...'; export default css; + defaultExportName = node.declaration.name; + break; + case 'CallExpression': { + // export default (() => '...')(); + const arrowFunctionBody = ( + node.declaration.callee as ArrowFunctionExpression + ).body; + if ( + arrowFunctionBody.type === 'Literal' || + arrowFunctionBody.type === 'TemplateLiteral' + ) { + cssStringNode = arrowFunctionBody; + } + break; + } + default: } } + }); - if (isDeclarationLiteral && node.type === 'ExportDefaultDeclaration') { - node.declaration.edit.update(`cssTag\`${escape(node.declaration.value)}\``) + if (!cssStringNode) { + if (!defaultExportName) { + this.warn(`Unrecognized default export in file ${id}`); + return; } - }); - magicString.prepend(`import {css as cssTag} from '${opts.importPackage}';\n`); + magicString.walk((node: Node) => { + if (node.type === 'VariableDeclaration') { + const exportedVar = node.declarations.find( + d => + (d.id as Identifier)?.name === defaultExportName && + (d.init?.type === 'Literal' || + d.init?.type === 'TemplateLiteral'), + ); + if (exportedVar) { + cssStringNode = exportedVar.init as typeof cssStringNode; + } + } + }); + } + + if (!cssStringNode) { + return this.error(`Unrecognized export expression in file ${id}`); + } + + if (cssStringNode.type === 'Literal') { + cssStringNode.edit.update( + `${CSS_TAG}\`${escape(cssStringNode.value as string)}\``, + ); + } else { + cssStringNode.edit.prepend(CSS_TAG); + } + + magicString.prepend( + `import {css as ${CSS_TAG}} from '${opts.importPackage}';\n`, + ); + return { code: magicString.toString(), map: magicString.generateMap({ diff --git a/package.json b/package.json index 12c1beb..6d49cc8 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,8 @@ }, "homepage": "https://github.com/umbopepato/rollup-plugin-postcss-lit#readme", "devDependencies": { + "@types/estree": "^1.0.1", + "glob": "^10.2.7", "@types/mocha": "^10.0.1", "@types/node": "^18.16.1", "jsdom": "^22.1.0", diff --git a/test/entry-literal.mjs b/test/entry-literal.mjs deleted file mode 100644 index 1d95474..0000000 --- a/test/entry-literal.mjs +++ /dev/null @@ -1,2 +0,0 @@ -import test from './test-literal.mjs'; -export default test; diff --git a/test/entry.mjs b/test/entry.mjs index 608d9fb..a96ced5 100644 --- a/test/entry.mjs +++ b/test/entry.mjs @@ -1,2 +1,3 @@ import test from './test.css'; -export default test; \ No newline at end of file + +export default test; diff --git a/test/intermediate-iife.mjs b/test/intermediate-iife.mjs new file mode 100644 index 0000000..7f05a16 --- /dev/null +++ b/test/intermediate-iife.mjs @@ -0,0 +1,2 @@ +export default (() => + '.test {\n color: white;\n background: url("./test.jpg");\n}\n\n.test:after {\n content: "\\2014";\n}\n')(); diff --git a/test/intermediate-literal.mjs b/test/intermediate-literal.mjs new file mode 100644 index 0000000..64a736c --- /dev/null +++ b/test/intermediate-literal.mjs @@ -0,0 +1 @@ +export default '.test {\n color: white;\n background: url("./test.jpg");\n}\n\n.test:after {\n content: "\\2014";\n}\n'; diff --git a/test/intermediate-template-var.mjs b/test/intermediate-template-var.mjs new file mode 100644 index 0000000..7c36329 --- /dev/null +++ b/test/intermediate-template-var.mjs @@ -0,0 +1,3 @@ +const css = `.test {\n color: white;\n background: url("./test.jpg");\n}\n\n.test:after {\n content: "\\2014";\n}\n`; + +export default css; diff --git a/test/intermediate-template.mjs b/test/intermediate-template.mjs new file mode 100644 index 0000000..ec279c7 --- /dev/null +++ b/test/intermediate-template.mjs @@ -0,0 +1 @@ +export default `.test {\n color: white;\n background: url("./test.jpg");\n}\n\n.test:after {\n content: "\\2014";\n}\n`; diff --git a/test/test-literal.mjs b/test/test-literal.mjs deleted file mode 100644 index 1a8f69b..0000000 --- a/test/test-literal.mjs +++ /dev/null @@ -1 +0,0 @@ -export default ".test {\n color: white;\n background: url(\"./test.jpg\");\n}\n\n.test:after {\n content: \"\\2014\";\n}\n"; diff --git a/test/test.spec.mjs b/test/test.spec.mjs index 6688e7a..6322a9f 100644 --- a/test/test.spec.mjs +++ b/test/test.spec.mjs @@ -1,69 +1,91 @@ -import {readFileSync} from 'fs'; -import postcss from 'rollup-plugin-postcss'; +import virtual from '@rollup/plugin-virtual'; +import { strict as assert } from 'assert'; +import { readFileSync, rmSync } from 'fs'; +import { globSync } from 'glob'; +import { CSSResult } from 'lit'; import * as rollup from 'rollup'; -import {CSSResult} from 'lit'; -import {strict as assert} from 'assert'; +import postcss from 'rollup-plugin-postcss'; import postcssLit from '../dist/index.js'; const cssFile = './test/test.css'; const entry = './test/entry.mjs'; -const readFile = (path) => readFileSync(path, 'utf-8'); +const readFile = path => readFileSync(path, 'utf-8'); describe('rollup-plugin-postcss-lit', () => { - it('should wrap an exported style string in the css template literal tag', async () => { - const outFile = 'out.mjs'; - const cssText = readFile(cssFile); - await renderFile(entry, `./test/${outFile}`,[ - postcss({ - inject: false, - }), - postcssLit(), - ]); - const litStyle = await import(`./${outFile}`).then(m => m.default); - assert.ok(litStyle instanceof CSSResult); - assert.equal(litStyle.cssText, cssText); + it('should wrap an exported style string in the css template literal tag', async () => { + const outFile = 'out.mjs'; + const cssText = readFile(cssFile); + await renderFile(entry, `./test/${outFile}`, [ + postcss({ + inject: false, + }), + postcssLit(), + ]); + const litStyle = await import(`./${outFile}`).then(m => m.default); + assert.ok(litStyle instanceof CSSResult); + assert.equal(litStyle.cssText, cssText); - const outFileText = readFile(`./test/${outFile}`); - const hasLitImport = outFileText.includes(`from 'lit';`); - assert.ok(hasLitImport); - }); + const outFileText = readFile(`./test/${outFile}`); + const hasLitImport = outFileText.includes(`from 'lit';`); + assert.ok(hasLitImport); + }); - it('should wrap a default export literal', async () => { - const outFile = 'out-literal.mjs'; - const intermediateFile = 'test-literal.mjs'; - await renderFile('./test/entry-literal.mjs', `./test/${outFile}`,[ - postcssLit({ include: `**/${intermediateFile}` }), - ]); - const cssText = await import(`./${intermediateFile}`).then(m => m.default); - const litStyle = await import(`./${outFile}`).then(m => m.default); - assert.ok(litStyle instanceof CSSResult); - assert.equal(litStyle.cssText, cssText); - }); + it('should support different export expressions', async () => { + for (const intermediateFile of globSync('./intermediate-?(*).mjs', { + cwd: './test', + })) { + const outFile = intermediateFile.replace('intermediate', 'out'); + const input = 'virtual-entry.mjs'; + const bundle = await rollup.rollup({ + input, + plugins: [ + virtual({ + [input]: `import test from './test/${intermediateFile}'; export default test;`, + }), + postcssLit({ include: `**/${intermediateFile}` }), + ], + }); + await bundle.write({ + file: `./test/${outFile}`, + format: 'es', + }); + const cssText = await import(`./${intermediateFile}`).then( + m => m.default, + ); + const litStyle = await import(`./${outFile}`).then(m => m.default); + assert.ok(litStyle instanceof CSSResult); + assert.equal(litStyle.cssText, cssText); + } + }); - it('can accept a different import package', async () => { - const outFile = './test/out-import.mjs'; - await renderFile(entry, outFile,[ - postcss({ - inject: false, - }), - postcssLit({ - importPackage: 'lit-element', - }), - ]); + it('can accept a different import package', async () => { + const outFile = './test/out-import.mjs'; + await renderFile(entry, outFile, [ + postcss({ + inject: false, + }), + postcssLit({ + importPackage: 'lit-element', + }), + ]); - const outFileText = readFile(outFile); - const hasLitElementImport = outFileText.includes(`from 'lit-element';`); - assert.ok(hasLitElementImport); - }); + const outFileText = readFile(outFile); + const hasLitElementImport = outFileText.includes(`from 'lit-element';`); + assert.ok(hasLitElementImport); + }); + + after(() => { + globSync('./test/out*').forEach(f => rmSync(f)); + }); }); const renderFile = async (inFile, outFile, plugins) => { - const bundle = await rollup.rollup({ - input: inFile, - plugins, - }); - await bundle.write({ - file: outFile, - format: 'es', - }); + const bundle = await rollup.rollup({ + input: inFile, + plugins, + }); + await bundle.write({ + file: outFile, + format: 'es', + }); }; diff --git a/tsconfig.json b/tsconfig.json index 22e1ea6..e5d2fc2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,5 @@ "declaration": true, "skipLibCheck": true, "outDir": "dist" - }, - "files": [ "index.ts"] + } } diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 0000000..677faac --- /dev/null +++ b/types.d.ts @@ -0,0 +1,12 @@ +import 'estree'; + +declare module 'estree' { + export interface BaseNodeWithoutComments { + edit: { + source: () => string; + update: (replacement: string) => Node; + append: (append: string) => Node; + prepend: (prepend: string) => Node; + }; + } +}