Skip to content

Commit

Permalink
feat: add support for more export expressions
Browse files Browse the repository at this point in the history
Refs #60
  • Loading branch information
umbopepato committed Jun 21, 2023
1 parent cab989a commit 41e8bb2
Show file tree
Hide file tree
Showing 12 changed files with 178 additions and 92 deletions.
110 changes: 78 additions & 32 deletions index.ts
Original file line number Diff line number Diff line change
@@ -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 {
/**
Expand All @@ -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 {
Expand All @@ -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({
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 0 additions & 2 deletions test/entry-literal.mjs

This file was deleted.

3 changes: 2 additions & 1 deletion test/entry.mjs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
import test from './test.css';
export default test;

export default test;
2 changes: 2 additions & 0 deletions test/intermediate-iife.mjs
Original file line number Diff line number Diff line change
@@ -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')();
1 change: 1 addition & 0 deletions test/intermediate-literal.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default '.test {\n color: white;\n background: url("./test.jpg");\n}\n\n.test:after {\n content: "\\2014";\n}\n';
3 changes: 3 additions & 0 deletions test/intermediate-template-var.mjs
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions test/intermediate-template.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default `.test {\n color: white;\n background: url("./test.jpg");\n}\n\n.test:after {\n content: "\\2014";\n}\n`;
1 change: 0 additions & 1 deletion test/test-literal.mjs

This file was deleted.

130 changes: 76 additions & 54 deletions test/test.spec.mjs
Original file line number Diff line number Diff line change
@@ -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',
});
};
3 changes: 1 addition & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,5 @@
"declaration": true,
"skipLibCheck": true,
"outDir": "dist"
},
"files": [ "index.ts"]
}
}
12 changes: 12 additions & 0 deletions types.d.ts
Original file line number Diff line number Diff line change
@@ -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;
};
}
}

0 comments on commit 41e8bb2

Please sign in to comment.