CSS plugin

This commit is contained in:
Jake Archibald
2020-09-24 11:49:41 +01:00
parent f5d9023ff3
commit 02e4eaf4b5
9 changed files with 91 additions and 199 deletions

View File

@@ -19,7 +19,6 @@ import postcss from 'postcss';
import postCSSNested from 'postcss-nested';
import postCSSUrl from 'postcss-url';
import postCSSModules from 'postcss-modules';
import postCSSImport from 'postcss-import';
import postCSSSimpleVars from 'postcss-simple-vars';
import cssNano from 'cssnano';
import camelCase from 'lodash.camelcase';
@@ -28,9 +27,19 @@ import glob from 'glob';
const globP = promisify(glob);
const moduleSuffix = '.css';
const prefix = 'css-bundle:';
const sourcePrefix = 'css:';
const addPrefix = 'add-css:';
const assetRe = new RegExp('/fake/path/to/asset/([^/]+)/', 'g');
const appendCssModule = '\0appendCss';
const appendCssSource = `
export default function appendCss(css) {
const style = document.createElement('style');
style.textContent = css;
document.head.append(style);
}
`;
export default function (resolveFileUrl) {
/** @type {string[]} */
let emittedCSSIds;
@@ -39,61 +48,6 @@ export default function (resolveFileUrl) {
/** @type {Map<string, { module: string, css: string }>} */
let pathToResult;
async function loadBundledCSS(path, rollupContext) {
const parsedPath = parsePath(path);
if (!pathToResult.has(path)) {
throw Error(`Cannot find ${path} in pathToResult`);
}
const file = pathToResult.get(path).css;
const cssResult = await postcss([
postCSSImport({
path: ['./', 'static-build/'],
load(path) {
if (!pathToResult.has(path)) {
throw Error(`Cannot find ${path} in pathToResult`);
}
return pathToResult.get(path).css;
},
}),
postCSSUrl({
url: ({ relativePath, url }) => {
if (/^https?:\/\//.test(url)) return url;
const parsedPath = parsePath(relativePath);
const source = readFileSync(resolvePath(dirname(path), relativePath));
const fileId = rollupContext.emitFile({
type: 'asset',
name: parsedPath.base,
source,
});
const hash = createHash('md5');
hash.update(source);
const md5 = hash.digest('hex');
hashToId.set(md5, fileId);
return `/fake/path/to/asset/${md5}/`;
},
}),
cssNano,
]).process(file, {
from: path,
});
const fileId = rollupContext.emitFile({
type: 'asset',
source: cssResult.css,
name: parsedPath.base,
});
emittedCSSIds.push(fileId);
return [
`export default import.meta.ROLLUP_FILE_URL_${fileId}`,
`export const inline = ${JSON.stringify(cssResult.css)}`,
].join('\n');
}
return {
name: 'css',
async buildStart() {
@@ -120,8 +74,28 @@ export default function (resolveFileUrl) {
moduleJSON = json;
},
}),
postCSSUrl({
url: ({ relativePath, url }) => {
if (/^https?:\/\//.test(url)) return url;
const parsedPath = parsePath(relativePath);
const source = readFileSync(
resolvePath(dirname(path), relativePath),
);
const fileId = this.emitFile({
type: 'asset',
name: parsedPath.base,
source,
});
const hash = createHash('md5');
hash.update(source);
const md5 = hash.digest('hex');
hashToId.set(md5, fileId);
return `/fake/path/to/asset/${md5}/`;
},
}),
cssNano,
]).process(file, {
from: undefined,
from: path,
});
const cssClassExports = Object.entries(moduleJSON).map(
@@ -152,14 +126,45 @@ export default function (resolveFileUrl) {
);
},
async resolveId(id, importer) {
if (!id.startsWith(prefix)) return;
if (id === appendCssModule) return id;
const prefix = id.startsWith(sourcePrefix)
? sourcePrefix
: id.startsWith(addPrefix)
? addPrefix
: undefined;
if (!prefix) return;
const resolved = await this.resolve(id.slice(prefix.length), importer);
if (!resolved) throw Error(`Couldn't resolve ${id} from ${importer}`);
return prefix + resolved.id;
},
async load(id) {
if (id.startsWith(prefix)) {
return loadBundledCSS(id.slice(prefix.length), this);
if (id === appendCssModule) return appendCssSource;
if (id.startsWith(sourcePrefix)) {
const path = id.slice(sourcePrefix.length);
if (!pathToResult.has(path)) {
throw Error(`Cannot find ${path} in pathToResult`);
}
const cssStr = JSON.stringify(pathToResult.get(path).css).replace(
assetRe,
(match, hash) =>
`" + import.meta.ROLLUP_FILE_URL_${hashToId.get(hash)} + "`,
);
return `export default ${cssStr};`;
}
if (id.startsWith(addPrefix)) {
const path = id.slice(addPrefix.length);
return (
`import css from 'css:${path}';\n` +
`import appendCss from '${appendCssModule}';\n` +
`appendCss(css);\n`
);
}
if (id.endsWith(moduleSuffix)) {
if (!pathToResult.has(id)) {
@@ -169,27 +174,5 @@ export default function (resolveFileUrl) {
return pathToResult.get(id).module;
}
},
async generateBundle(options, bundle) {
const cssAssets = emittedCSSIds.map((id) => this.getFileName(id));
for (const cssAsset of cssAssets) {
bundle[cssAsset].source = bundle[cssAsset].source.replace(
assetRe,
(_, p1) =>
resolveFileUrl({
fileName: this.getFileName(hashToId.get(p1)),
}),
);
}
for (const item of Object.values(bundle)) {
if (item.type === 'asset') continue;
item.code = item.code.replace(assetRe, (match, p1) =>
resolveFileUrl({
fileName: this.getFileName(hashToId.get(p1)),
}),
);
}
},
};
}

5
missing-types.d.ts vendored
View File

@@ -22,4 +22,9 @@ declare module 'omt:*' {
export default value;
}
declare module 'css:*' {
const source: string;
export default source;
}
declare const __PRODUCTION__: boolean;

107
package-lock.json generated
View File

@@ -2795,12 +2795,6 @@
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
"dev": true
},
"pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
},
"pkg-dir": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
@@ -3431,98 +3425,6 @@
}
}
},
"postcss-import": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-12.0.1.tgz",
"integrity": "sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw==",
"dev": true,
"requires": {
"postcss": "^7.0.1",
"postcss-value-parser": "^3.2.3",
"read-cache": "^1.0.0",
"resolve": "^1.1.7"
},
"dependencies": {
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"dependencies": {
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
},
"postcss": {
"version": "7.0.34",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.34.tgz",
"integrity": "sha512-H/7V2VeNScX9KE83GDrDZNiGT1m2H+UTnlinIzhjlLX9hfMUn1mHNnGeX81a1c8JSBdBvqk7c2ZOG6ZPn5itGw==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
"source-map": "^0.6.1",
"supports-color": "^6.1.0"
}
},
"postcss-value-parser": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
"integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
"dev": true
},
"supports-color": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
"integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"postcss-merge-longhand": {
"version": "4.0.11",
"resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz",
@@ -5975,15 +5877,6 @@
"strip-json-comments": "~2.0.1"
}
},
"read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
"integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=",
"dev": true,
"requires": {
"pify": "^2.3.0"
}
},
"registry-auth-token": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz",

View File

@@ -23,7 +23,6 @@
"lint-staged": "^10.4.0",
"lodash.camelcase": "^4.3.0",
"postcss": "^8.0.8",
"postcss-import": "^12.0.1",
"postcss-modules": "^3.2.2",
"postcss-nested": "^5.0.0",
"postcss-simple-vars": "^6.0.0",

View File

@@ -6,6 +6,7 @@ import { h, Component } from 'preact';
import { linkRef } from 'client/initial-app/util';
import * as style from './style.css';
import 'add-css:./style.css';
import 'file-drop-element';
import 'client/initial-app/custom-els/snack-bar';
//import Intro from '../intro';

View File

@@ -21,4 +21,6 @@ interface Navigator {
readonly standalone: boolean;
}
declare module 'add-css:*' {}
declare module 'preact/debug' {}

View File

@@ -17,9 +17,3 @@ declare module 'client-bundle:*' {
export default url;
export const imports: string[];
}
declare module 'css-bundle:*' {
const url: string;
export default url;
export const inline: string;
}

View File

@@ -12,9 +12,10 @@
*/
import { h, FunctionalComponent } from 'preact';
import styles from 'css-bundle:./all.css';
import css from 'css:./all.css';
import clientBundleURL, { imports } from 'client-bundle:client/initial-app';
import favicon from 'url:static-build/assets/favicon.ico';
import { escapeStyleScriptContent } from 'static-build/utils';
interface Props {}
@@ -35,7 +36,9 @@ const Index: FunctionalComponent<Props> = () => (
<link rel="shortcut icon" href={favicon} />
<meta name="theme-color" content="#f78f21" />
<link rel="manifest" href="/manifest.json" />
<link rel="stylesheet" href={styles} />
<style
dangerouslySetInnerHTML={{ __html: escapeStyleScriptContent(css) }}
/>
<script src={clientBundleURL} defer />
{imports.map((v) => (
<link rel="preload" as="script" href={v} />

View File

@@ -44,3 +44,15 @@ export function writeFiles(toOutput: OutputMap) {
process.exit(1);
});
}
/**
* Escape a string for insertion in a style or script tag
*/
export function escapeStyleScriptContent(str: string): string {
return str
.replace(/<!--/g, '<\\!--')
.replace(/<script/g, '<\\script')
.replace(/<\/script/g, '<\\/script')
.replace(/<style/g, '<\\style')
.replace(/<\/style/g, '<\\/style');
}