Skip to content

Commit ac7f745

Browse files
committed
feat(typescript-transform-lit-css): add typescript-transform-lit-css
1 parent aef6238 commit ac7f745

File tree

6 files changed

+341
-0
lines changed

6 files changed

+341
-0
lines changed

packages/esbuild-plugin-lit-css/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,4 @@ await esbuild.build({
141141

142142
Looking for webpack? [lit-css-loader](../lit-css-loader)
143143
Looking for rollup? [rollup-plugin-lit-css](../rollup-plugin-lit-css)
144+
Looking for typescript? [typescript-transform-lit-css](../typescript-transform-lit-css)

packages/lit-css-loader/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,4 @@ module.exports = {
118118

119119
Looking for rollup? [rollup-plugin-lit-css](../rollup-plugin-lit-css)
120120
Looking for esbuild? [esbuild-plugin-lit-css](../esbuild-plugin-lit-css)
121+
Looking for typescript? [typescript-transform-lit-css](../typescript-transform-lit-css)

packages/rollup-plugin-lit-css/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,7 @@ export default {
177177
]
178178
}
179179
```
180+
181+
Looking for webpack? [lit-css-loader](../lit-css-loader)
182+
Looking for esbuild? [esbuild-plugin-lit-css](../esbuild-plugin-lit-css)
183+
Looking for typescript? [typescript-transform-lit-css](../typescript-transform-lit-css)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# typescript-transform-lit-css
2+
3+
TypeScript transformer to import css files as JavaScript tagged-template literal objects.
4+
Use it with [ts-patch](https://npm.im/ts-patch)
5+
6+
> _The "Lit" stands for "Literal"_
7+
8+
You can use it to import CSS for various libraries like `lit-element`, `@microsoft/fast-element`, or others.
9+
10+
## Do I Need This?
11+
12+
No. This is an optional package who's sole purpose is to make it easier to write CSS-in-CSS while working on lit-element projects. You can just as easily write your CSS in some '`styles.css.js`' modules a la:
13+
14+
```js
15+
import { css } from 'lit-element';
16+
export default css`:host { display: block; }`;
17+
```
18+
19+
And this may actually be preferred.
20+
21+
Hopefully this package will become quickly obsolete when the [CSS Modules Proposal](https://github.com/w3c/webcomponents/issues/759) (or something like it) is accepted and implemented.
22+
23+
In the mean time, enjoy importing your CSS into your component files.
24+
25+
## Options
26+
27+
| Name | Accepts | Default |
28+
| ----------- | -------------------------------------------------------------------------------------- | ----------- |
29+
| `filter` | RegExp of file names to apply to | `/\.css$/i` |
30+
| `uglify` | Boolean or Object of [uglifycss](https://www.npmjs.com/package/uglifycss#api) options. | `false` |
31+
| `specifier` | Package to import `css` from | `lit` |
32+
| `tag` | Name of the template-tag function | `css` |
33+
| `transform` | Optional function (sync or async) which transforms css sources (e.g. postcss) | `x => x` |
34+
35+
## Usage
36+
37+
```json5
38+
{
39+
"compilerOptions": {
40+
"plugins": [
41+
{
42+
"transform": "typescript-transform-lit-css",
43+
},
44+
]
45+
}
46+
}
47+
```
48+
49+
Then import your CSS:
50+
51+
```css
52+
:host {
53+
display: block;
54+
}
55+
56+
h1 {
57+
color: hotpink;
58+
}
59+
```
60+
61+
```ts
62+
import { LitElement, customElement, html } from 'lit-element';
63+
64+
import style from './css-in-css.css';
65+
66+
@customElement('css-in-css')
67+
class CSSInCSS extends LitElement {
68+
static get styles() {
69+
return [style];
70+
}
71+
72+
render() {
73+
return html`<h1>It's Lit!</h1>`;
74+
}
75+
}
76+
```
77+
78+
### Usage with FAST
79+
80+
```json5
81+
{
82+
"compilerOptions": {
83+
"plugins": [
84+
{
85+
"transform": "typescript-transform-lit-css",
86+
"specifier": "@microsoft/fast-element",
87+
},
88+
]
89+
}
90+
}
91+
```
92+
93+
```ts
94+
import { FASTElement, customElement, html } from '@microsoft/fast-element';
95+
96+
import styles from './css-in-css.css';
97+
98+
const template = html<CSSinCSS>`<h1>It's Lit!</h1>`;
99+
100+
@customElement({ name: 'css-in-css', template, styles })
101+
class CSSinCSS extends FASTElement {}
102+
```
103+
104+
Looking for esbuild? [esbuild-plugin-lit-css](../esbuild-plugin-lit-css)
105+
Looking for webpack? [lit-css-loader](../lit-css-loader)
106+
Looking for rollup? [rollup-plugin-lit-css](../rollup-plugin-lit-css)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"name": "typescript-transform-lit-css",
3+
"description": "import CSS files as tagged template literals",
4+
"version": "1.0.0",
5+
"type": "module",
6+
"main": "typescript-transform-lit-css.js",
7+
"types": "typescript-transform-lit-css.d.ts",
8+
"exports": {
9+
"import": "./typescript-transform-lit-css.js",
10+
"require": "./typescript-transform-lit-css.cjs"
11+
},
12+
"author": "Benny Powers <[email protected]>",
13+
"license": "ISC",
14+
"repository": {
15+
"type": "git",
16+
"url": "git+ssh://[email protected]/bennypowers/lit-css.git",
17+
"directory": "packages/typescript-transform-lit-css"
18+
},
19+
"bugs": {
20+
"url": "https://github.com/bennypowers/lit-css/issues"
21+
},
22+
"keywords": [
23+
"typescript",
24+
"transform",
25+
"lit",
26+
"css",
27+
"webcomponents"
28+
],
29+
"files": [
30+
"typescript-transform-lit-css.cjs",
31+
"typescript-transform-lit-css.js",
32+
"typescript-transform-lit-css.d.ts"
33+
],
34+
"dependencies": {
35+
"@pwrs/lit-css": "^2.0.0"
36+
},
37+
"peerDependencies": {
38+
"typescript": "^5",
39+
"ts-patch": "^3.0",
40+
"lit": "^2.7.2 || ^3.0.0"
41+
}
42+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import type { Options } from '@pwrs/lit-css/lit-css';
2+
import type { TransformerExtras, PluginConfig } from 'ts-patch';
3+
4+
import fs from 'node:fs';
5+
import { URL, pathToFileURL } from 'node:url';
6+
7+
import CleanCSS from 'clean-css';
8+
9+
import ts, { ImportDeclaration, SourceFile } from 'typescript';
10+
11+
export interface LitCSSOptions extends Pick<Options, 'specifier'|'filePath'|'tag'> {
12+
filter: string;
13+
uglify: boolean;
14+
inline: boolean;
15+
}
16+
17+
const SEEN_SOURCES = new WeakSet();
18+
19+
function createLitCssImportStatement(
20+
ctx: ts.CoreTransformationContext,
21+
sourceFile: ts.SourceFile,
22+
specifier: string,
23+
tag: string,
24+
) {
25+
if (SEEN_SOURCES.has(sourceFile))
26+
return;
27+
28+
// eslint-disable-next-line easy-loops/easy-loops
29+
for (const statement of sourceFile.statements) {
30+
if (
31+
ts.isImportDeclaration(statement) &&
32+
statement.moduleSpecifier.getText() === specifier) {
33+
// eslint-disable-next-line easy-loops/easy-loops
34+
for (const binding of statement.importClause?.namedBindings?.getChildren() ?? []) {
35+
if (binding.getText() === tag) {
36+
SEEN_SOURCES.add(sourceFile);
37+
return;
38+
}
39+
}
40+
}
41+
}
42+
43+
SEEN_SOURCES.add(sourceFile);
44+
45+
return ctx.factory.createImportDeclaration(
46+
undefined,
47+
ctx.factory.createImportClause(
48+
false,
49+
undefined,
50+
ctx.factory.createNamedImports([
51+
ctx.factory.createImportSpecifier(
52+
false,
53+
undefined,
54+
ctx.factory.createIdentifier('css')
55+
),
56+
]),
57+
),
58+
ctx.factory.createStringLiteral(specifier),
59+
);
60+
}
61+
62+
function createLitCssTaggedTemplateLiteral(
63+
ctx: ts.CoreTransformationContext,
64+
stylesheet: string,
65+
name: string,
66+
tag: string,
67+
) {
68+
return ctx.factory.createVariableStatement(
69+
undefined,
70+
ctx.factory.createVariableDeclarationList([
71+
ctx.factory.createVariableDeclaration(
72+
name ?? 'style',
73+
undefined,
74+
undefined,
75+
ctx.factory.createTaggedTemplateExpression(
76+
ctx.factory.createIdentifier(tag),
77+
undefined,
78+
ctx.factory.createNoSubstitutionTemplateLiteral(stylesheet),
79+
)
80+
),
81+
], ts.NodeFlags.Const)
82+
);
83+
}
84+
85+
/**
86+
* @param {string} stylesheet
87+
* @param {string} filePath
88+
*/
89+
function minifyCss(stylesheet: string, filePath: string) {
90+
try {
91+
const clean = new CleanCSS({ returnPromise: false });
92+
const { styles } = clean.minify(stylesheet);
93+
return styles;
94+
} catch (e) {
95+
// eslint-disable-next-line no-console
96+
console.log('Could not minify ', filePath);
97+
// eslint-disable-next-line no-console
98+
console.error(e);
99+
return stylesheet;
100+
}
101+
}
102+
103+
/**
104+
* Replace .css import specifiers with .css.js import specifiers
105+
*/
106+
export default function(
107+
program: ts.Program,
108+
pluginConfig: PluginConfig & LitCSSOptions,
109+
extras: TransformerExtras,
110+
) {
111+
const tagPkgSpecifier = pluginConfig.specifier ?? 'lit';
112+
const tag = pluginConfig.tag ?? 'css';
113+
return (ctx: ts.TransformationContext) => {
114+
function visitor(node: ts.Node) {
115+
if (ts.isImportDeclaration(node) && !node.importClause?.isTypeOnly) {
116+
const importedStyleSheetSpecifier =
117+
node.moduleSpecifier.getText().replace(/^'(.*)'$/, '$1');
118+
if (importedStyleSheetSpecifier.endsWith('.css')) {
119+
if (pluginConfig.inline) {
120+
const { fileName } = node.getSourceFile();
121+
const dir = pathToFileURL(fileName);
122+
const url = new URL(importedStyleSheetSpecifier, dir);
123+
const content = fs.readFileSync(url, 'utf-8');
124+
const stylesheet = pluginConfig.uglify ? minifyCss(content, url.pathname) : content;
125+
return [
126+
createLitCssImportStatement(
127+
ctx,
128+
node.getSourceFile(),
129+
tagPkgSpecifier,
130+
tag,
131+
),
132+
createLitCssTaggedTemplateLiteral(
133+
ctx,
134+
stylesheet,
135+
node.importClause?.name?.getText(),
136+
tag,
137+
),
138+
];
139+
} else {
140+
return ctx.factory.createImportDeclaration(
141+
node.modifiers,
142+
node.importClause,
143+
ctx.factory.createStringLiteral(`${importedStyleSheetSpecifier}.js`)
144+
);
145+
}
146+
}
147+
}
148+
return ts.visitEachChild(node, visitor, ctx);
149+
}
150+
151+
return (sourceFile: SourceFile) => {
152+
const children = sourceFile.getChildren();
153+
154+
const decl = (children.find(x =>
155+
!ts.isTypeOnlyImportOrExportDeclaration(x) &&
156+
!ts.isNamespaceImport(x) &&
157+
ts.isImportDeclaration(x) &&
158+
x.moduleSpecifier.getText() === tagPkgSpecifier &&
159+
x.importClause?.namedBindings
160+
)) as ImportDeclaration;
161+
162+
const litImportBindings = decl?.importClause?.namedBindings;
163+
164+
const hasStyleImports = children.find(x =>
165+
ts.isImportDeclaration(x) && x.moduleSpecifier.getText().endsWith('.css'));
166+
167+
if (hasStyleImports) {
168+
if (litImportBindings &&
169+
ts.isNamedImports(litImportBindings) &&
170+
!litImportBindings.elements?.some(x => x.getText() === tag)) {
171+
ctx.factory.updateNamedImports(
172+
litImportBindings,
173+
[
174+
...litImportBindings.elements,
175+
ctx.factory.createImportSpecifier(
176+
false,
177+
undefined,
178+
ctx.factory.createIdentifier(tag),
179+
),
180+
]
181+
);
182+
}
183+
}
184+
return ts.visitEachChild(sourceFile, visitor, ctx);
185+
};
186+
};
187+
}

0 commit comments

Comments
 (0)