-
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(typescript-transform-lit-css): add typescript-transform-lit-css
- Loading branch information
1 parent
aef6238
commit ac7f745
Showing
6 changed files
with
341 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
# typescript-transform-lit-css | ||
|
||
TypeScript transformer to import css files as JavaScript tagged-template literal objects. | ||
Use it with [ts-patch](https://npm.im/ts-patch) | ||
|
||
> _The "Lit" stands for "Literal"_ | ||
You can use it to import CSS for various libraries like `lit-element`, `@microsoft/fast-element`, or others. | ||
|
||
## Do I Need This? | ||
|
||
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: | ||
|
||
```js | ||
import { css } from 'lit-element'; | ||
export default css`:host { display: block; }`; | ||
``` | ||
|
||
And this may actually be preferred. | ||
|
||
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. | ||
|
||
In the mean time, enjoy importing your CSS into your component files. | ||
|
||
## Options | ||
|
||
| Name | Accepts | Default | | ||
| ----------- | -------------------------------------------------------------------------------------- | ----------- | | ||
| `filter` | RegExp of file names to apply to | `/\.css$/i` | | ||
| `uglify` | Boolean or Object of [uglifycss](https://www.npmjs.com/package/uglifycss#api) options. | `false` | | ||
| `specifier` | Package to import `css` from | `lit` | | ||
| `tag` | Name of the template-tag function | `css` | | ||
| `transform` | Optional function (sync or async) which transforms css sources (e.g. postcss) | `x => x` | | ||
|
||
## Usage | ||
|
||
```json5 | ||
{ | ||
"compilerOptions": { | ||
"plugins": [ | ||
{ | ||
"transform": "typescript-transform-lit-css", | ||
}, | ||
] | ||
} | ||
} | ||
``` | ||
|
||
Then import your CSS: | ||
|
||
```css | ||
:host { | ||
display: block; | ||
} | ||
|
||
h1 { | ||
color: hotpink; | ||
} | ||
``` | ||
|
||
```ts | ||
import { LitElement, customElement, html } from 'lit-element'; | ||
|
||
import style from './css-in-css.css'; | ||
|
||
@customElement('css-in-css') | ||
class CSSInCSS extends LitElement { | ||
static get styles() { | ||
return [style]; | ||
} | ||
|
||
render() { | ||
return html`<h1>It's Lit!</h1>`; | ||
} | ||
} | ||
``` | ||
|
||
### Usage with FAST | ||
|
||
```json5 | ||
{ | ||
"compilerOptions": { | ||
"plugins": [ | ||
{ | ||
"transform": "typescript-transform-lit-css", | ||
"specifier": "@microsoft/fast-element", | ||
}, | ||
] | ||
} | ||
} | ||
``` | ||
|
||
```ts | ||
import { FASTElement, customElement, html } from '@microsoft/fast-element'; | ||
|
||
import styles from './css-in-css.css'; | ||
|
||
const template = html<CSSinCSS>`<h1>It's Lit!</h1>`; | ||
|
||
@customElement({ name: 'css-in-css', template, styles }) | ||
class CSSinCSS extends FASTElement {} | ||
``` | ||
|
||
Looking for esbuild? [esbuild-plugin-lit-css](../esbuild-plugin-lit-css) | ||
Looking for webpack? [lit-css-loader](../lit-css-loader) | ||
Looking for rollup? [rollup-plugin-lit-css](../rollup-plugin-lit-css) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
{ | ||
"name": "typescript-transform-lit-css", | ||
"description": "import CSS files as tagged template literals", | ||
"version": "1.0.0", | ||
"type": "module", | ||
"main": "typescript-transform-lit-css.js", | ||
"types": "typescript-transform-lit-css.d.ts", | ||
"exports": { | ||
"import": "./typescript-transform-lit-css.js", | ||
"require": "./typescript-transform-lit-css.cjs" | ||
}, | ||
"author": "Benny Powers <[email protected]>", | ||
"license": "ISC", | ||
"repository": { | ||
"type": "git", | ||
"url": "git+ssh://[email protected]/bennypowers/lit-css.git", | ||
"directory": "packages/typescript-transform-lit-css" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/bennypowers/lit-css/issues" | ||
}, | ||
"keywords": [ | ||
"typescript", | ||
"transform", | ||
"lit", | ||
"css", | ||
"webcomponents" | ||
], | ||
"files": [ | ||
"typescript-transform-lit-css.cjs", | ||
"typescript-transform-lit-css.js", | ||
"typescript-transform-lit-css.d.ts" | ||
], | ||
"dependencies": { | ||
"@pwrs/lit-css": "^2.0.0" | ||
}, | ||
"peerDependencies": { | ||
"typescript": "^5", | ||
"ts-patch": "^3.0", | ||
"lit": "^2.7.2 || ^3.0.0" | ||
} | ||
} |
187 changes: 187 additions & 0 deletions
187
packages/typescript-transform-lit-css/typescript-transform-lit-css.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
import type { Options } from '@pwrs/lit-css/lit-css'; | ||
import type { TransformerExtras, PluginConfig } from 'ts-patch'; | ||
|
||
import fs from 'node:fs'; | ||
import { URL, pathToFileURL } from 'node:url'; | ||
|
||
import CleanCSS from 'clean-css'; | ||
|
||
import ts, { ImportDeclaration, SourceFile } from 'typescript'; | ||
|
||
export interface LitCSSOptions extends Pick<Options, 'specifier'|'filePath'|'tag'> { | ||
filter: string; | ||
uglify: boolean; | ||
inline: boolean; | ||
} | ||
|
||
const SEEN_SOURCES = new WeakSet(); | ||
|
||
function createLitCssImportStatement( | ||
ctx: ts.CoreTransformationContext, | ||
sourceFile: ts.SourceFile, | ||
specifier: string, | ||
tag: string, | ||
) { | ||
if (SEEN_SOURCES.has(sourceFile)) | ||
return; | ||
|
||
// eslint-disable-next-line easy-loops/easy-loops | ||
for (const statement of sourceFile.statements) { | ||
if ( | ||
ts.isImportDeclaration(statement) && | ||
statement.moduleSpecifier.getText() === specifier) { | ||
// eslint-disable-next-line easy-loops/easy-loops | ||
for (const binding of statement.importClause?.namedBindings?.getChildren() ?? []) { | ||
if (binding.getText() === tag) { | ||
SEEN_SOURCES.add(sourceFile); | ||
return; | ||
} | ||
} | ||
} | ||
} | ||
|
||
SEEN_SOURCES.add(sourceFile); | ||
|
||
return ctx.factory.createImportDeclaration( | ||
undefined, | ||
ctx.factory.createImportClause( | ||
false, | ||
undefined, | ||
ctx.factory.createNamedImports([ | ||
ctx.factory.createImportSpecifier( | ||
false, | ||
undefined, | ||
ctx.factory.createIdentifier('css') | ||
), | ||
]), | ||
), | ||
ctx.factory.createStringLiteral(specifier), | ||
); | ||
} | ||
|
||
function createLitCssTaggedTemplateLiteral( | ||
ctx: ts.CoreTransformationContext, | ||
stylesheet: string, | ||
name: string, | ||
tag: string, | ||
) { | ||
return ctx.factory.createVariableStatement( | ||
undefined, | ||
ctx.factory.createVariableDeclarationList([ | ||
ctx.factory.createVariableDeclaration( | ||
name ?? 'style', | ||
undefined, | ||
undefined, | ||
ctx.factory.createTaggedTemplateExpression( | ||
ctx.factory.createIdentifier(tag), | ||
undefined, | ||
ctx.factory.createNoSubstitutionTemplateLiteral(stylesheet), | ||
) | ||
), | ||
], ts.NodeFlags.Const) | ||
); | ||
} | ||
|
||
/** | ||
* @param {string} stylesheet | ||
* @param {string} filePath | ||
*/ | ||
function minifyCss(stylesheet: string, filePath: string) { | ||
try { | ||
const clean = new CleanCSS({ returnPromise: false }); | ||
const { styles } = clean.minify(stylesheet); | ||
return styles; | ||
} catch (e) { | ||
// eslint-disable-next-line no-console | ||
console.log('Could not minify ', filePath); | ||
// eslint-disable-next-line no-console | ||
console.error(e); | ||
return stylesheet; | ||
} | ||
} | ||
|
||
/** | ||
* Replace .css import specifiers with .css.js import specifiers | ||
*/ | ||
export default function( | ||
program: ts.Program, | ||
pluginConfig: PluginConfig & LitCSSOptions, | ||
extras: TransformerExtras, | ||
) { | ||
const tagPkgSpecifier = pluginConfig.specifier ?? 'lit'; | ||
const tag = pluginConfig.tag ?? 'css'; | ||
return (ctx: ts.TransformationContext) => { | ||
function visitor(node: ts.Node) { | ||
if (ts.isImportDeclaration(node) && !node.importClause?.isTypeOnly) { | ||
const importedStyleSheetSpecifier = | ||
node.moduleSpecifier.getText().replace(/^'(.*)'$/, '$1'); | ||
if (importedStyleSheetSpecifier.endsWith('.css')) { | ||
if (pluginConfig.inline) { | ||
const { fileName } = node.getSourceFile(); | ||
const dir = pathToFileURL(fileName); | ||
const url = new URL(importedStyleSheetSpecifier, dir); | ||
const content = fs.readFileSync(url, 'utf-8'); | ||
const stylesheet = pluginConfig.uglify ? minifyCss(content, url.pathname) : content; | ||
return [ | ||
createLitCssImportStatement( | ||
ctx, | ||
node.getSourceFile(), | ||
tagPkgSpecifier, | ||
tag, | ||
), | ||
createLitCssTaggedTemplateLiteral( | ||
ctx, | ||
stylesheet, | ||
node.importClause?.name?.getText(), | ||
tag, | ||
), | ||
]; | ||
} else { | ||
return ctx.factory.createImportDeclaration( | ||
node.modifiers, | ||
node.importClause, | ||
ctx.factory.createStringLiteral(`${importedStyleSheetSpecifier}.js`) | ||
); | ||
} | ||
} | ||
} | ||
return ts.visitEachChild(node, visitor, ctx); | ||
} | ||
|
||
return (sourceFile: SourceFile) => { | ||
const children = sourceFile.getChildren(); | ||
|
||
const decl = (children.find(x => | ||
!ts.isTypeOnlyImportOrExportDeclaration(x) && | ||
!ts.isNamespaceImport(x) && | ||
ts.isImportDeclaration(x) && | ||
x.moduleSpecifier.getText() === tagPkgSpecifier && | ||
x.importClause?.namedBindings | ||
)) as ImportDeclaration; | ||
|
||
const litImportBindings = decl?.importClause?.namedBindings; | ||
|
||
const hasStyleImports = children.find(x => | ||
ts.isImportDeclaration(x) && x.moduleSpecifier.getText().endsWith('.css')); | ||
|
||
if (hasStyleImports) { | ||
if (litImportBindings && | ||
ts.isNamedImports(litImportBindings) && | ||
!litImportBindings.elements?.some(x => x.getText() === tag)) { | ||
ctx.factory.updateNamedImports( | ||
litImportBindings, | ||
[ | ||
...litImportBindings.elements, | ||
ctx.factory.createImportSpecifier( | ||
false, | ||
undefined, | ||
ctx.factory.createIdentifier(tag), | ||
), | ||
] | ||
); | ||
} | ||
} | ||
return ts.visitEachChild(sourceFile, visitor, ctx); | ||
}; | ||
}; | ||
} |