Inject styling in HTML AST (#9)
* Inject styling in HTML AST * Restore optimize structure
This commit is contained in:
parent
d75107a20e
commit
8ebc077cb0
9 changed files with 236 additions and 155 deletions
|
@ -1,30 +1,33 @@
|
||||||
const { readFile } = require("fs").promises;
|
const { readFile } = require('fs').promises;
|
||||||
|
|
||||||
// Snowpack plugins must be CommonJS :(
|
// Snowpack plugins must be CommonJS :(
|
||||||
const transformPromise = import("./lib/transform2.js");
|
const transformPromise = import('./lib/transform2.js');
|
||||||
|
|
||||||
module.exports = function (snowpackConfig, { resolve } = {}) {
|
module.exports = function (snowpackConfig, { resolve } = {}) {
|
||||||
return {
|
return {
|
||||||
name: "snowpack-hmx",
|
name: 'snowpack-hmx',
|
||||||
knownEntrypoints: ["deepmerge"],
|
knownEntrypoints: ['deepmerge'],
|
||||||
resolve: {
|
resolve: {
|
||||||
input: [".hmx", ".md"],
|
input: ['.hmx', '.md'],
|
||||||
output: [".js"],
|
output: ['.js'],
|
||||||
},
|
},
|
||||||
async load({ filePath }) {
|
async load({ filePath }) {
|
||||||
const { compilePage, compileComponent } = await transformPromise;
|
const { compilePage, compileComponent } = await transformPromise;
|
||||||
const projectRoot = snowpackConfig.root;
|
const projectRoot = snowpackConfig.root;
|
||||||
const contents = await readFile(filePath, "utf-8");
|
const contents = await readFile(filePath, 'utf-8');
|
||||||
|
|
||||||
if (!filePath.includes("/pages/") && !filePath.includes("/layouts/")) {
|
if (!filePath.includes('/pages/') && !filePath.includes('/layouts/')) {
|
||||||
const result = await compileComponent(contents, { compileOptions: { resolve }, filename: filePath, projectRoot });
|
const result = await compileComponent(contents, { compileOptions: { resolve }, filename: filePath, projectRoot });
|
||||||
return result.contents;
|
return result.contents;
|
||||||
}
|
}
|
||||||
|
const result = await compilePage(contents, {
|
||||||
const result = await compilePage(contents, { compileOptions: { resolve }, filename: filePath, projectRoot });
|
compileOptions: { resolve },
|
||||||
|
filename: filePath,
|
||||||
|
projectRoot,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return /* js */ `
|
return /* js */ `
|
||||||
${result.contents}
|
${result.contents}
|
||||||
|
|
||||||
export default async (childDatas, childRenderFns) => {
|
export default async (childDatas, childRenderFns) => {
|
||||||
|
@ -36,12 +39,12 @@ module.exports = function (snowpackConfig, { resolve } = {}) {
|
||||||
if (_data.layout) {
|
if (_data.layout) {
|
||||||
const renderLayout = (await import('/_hmx/layouts/' + _data.layout.replace(/.*layouts\\//, "").replace(/\.hmx$/, '.js'))).default;
|
const renderLayout = (await import('/_hmx/layouts/' + _data.layout.replace(/.*layouts\\//, "").replace(/\.hmx$/, '.js'))).default;
|
||||||
return renderLayout(
|
return renderLayout(
|
||||||
[...(childDatas || []), _data],
|
[...(childDatas || []), _data],
|
||||||
[...(childRenderFns || []), renderHmx]
|
[...(childRenderFns || []), renderHmx]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const data = merge.all([_data, ...(childDatas || [])]);
|
const data = merge.all([_data, ...(childDatas || [])]);
|
||||||
let headResult;
|
let headResult;
|
||||||
let bodyResult;
|
let bodyResult;
|
||||||
for (const renderFn of (childRenderFns || [])) {
|
for (const renderFn of (childRenderFns || [])) {
|
||||||
let headAndBody = await Promise.all([
|
let headAndBody = await Promise.all([
|
||||||
|
@ -54,7 +57,7 @@ module.exports = function (snowpackConfig, { resolve } = {}) {
|
||||||
return h(Fragment, null, [
|
return h(Fragment, null, [
|
||||||
renderHmx.head(data, headResult, true),
|
renderHmx.head(data, headResult, true),
|
||||||
renderHmx.body(data, bodyResult, true),
|
renderHmx.body(data, bodyResult, true),
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
`;
|
`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
11
src/@types/estree-walker.d.ts
vendored
11
src/@types/estree-walker.d.ts
vendored
|
@ -11,4 +11,15 @@ declare module 'estree-walker' {
|
||||||
leave?: (this: { skip: () => void; remove: () => void; replace: (node: T) => void }, node: T, parent: T, key: string, index: number) => void;
|
leave?: (this: { skip: () => void; remove: () => void; replace: (node: T) => void }, node: T, parent: T, key: string, index: number) => void;
|
||||||
}
|
}
|
||||||
): T;
|
): T;
|
||||||
|
|
||||||
|
export function asyncWalk<T = BaseNode>(
|
||||||
|
ast: T,
|
||||||
|
{
|
||||||
|
enter,
|
||||||
|
leave,
|
||||||
|
}: {
|
||||||
|
enter?: (this: { skip: () => void; remove: () => void; replace: (node: T) => void }, node: T, parent: T, key: string, index: number) => void;
|
||||||
|
leave?: (this: { skip: () => void; remove: () => void; replace: (node: T) => void }, node: T, parent: T, key: string, index: number) => void;
|
||||||
|
}
|
||||||
|
): T;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import type { TemplateNode } from '../compiler/interfaces';
|
import type { TemplateNode } from '../compiler/interfaces';
|
||||||
|
|
||||||
|
|
||||||
export type VisitorFn = (node: TemplateNode) => void;
|
export type VisitorFn = (node: TemplateNode) => void;
|
||||||
|
|
||||||
export interface NodeVisitor {
|
export interface NodeVisitor {
|
||||||
|
@ -10,8 +9,8 @@ export interface NodeVisitor {
|
||||||
|
|
||||||
export interface Optimizer {
|
export interface Optimizer {
|
||||||
visitors?: {
|
visitors?: {
|
||||||
html?: Record<string, NodeVisitor>,
|
html?: Record<string, NodeVisitor>;
|
||||||
css?: Record<string, NodeVisitor>
|
css?: Record<string, NodeVisitor>;
|
||||||
},
|
};
|
||||||
finalize: () => Promise<void>
|
finalize: () => Promise<void>;
|
||||||
}
|
}
|
|
@ -190,7 +190,6 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
|
||||||
let collectionItem: JsxItem | undefined;
|
let collectionItem: JsxItem | undefined;
|
||||||
let currentItemName: string | undefined;
|
let currentItemName: string | undefined;
|
||||||
let currentDepth = 0;
|
let currentDepth = 0;
|
||||||
const classNames: Set<string> = new Set();
|
|
||||||
|
|
||||||
walk(ast.html, {
|
walk(ast.html, {
|
||||||
enter(node: TemplateNode) {
|
enter(node: TemplateNode) {
|
||||||
|
@ -275,6 +274,11 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
|
||||||
this.skip();
|
this.skip();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
case 'Style': {
|
||||||
|
const attributes = getAttributes(node.attributes);
|
||||||
|
items.push({ name: 'style', jsx: `h("style", ${attributes ? generateAttributes(attributes) : 'null'}, ${JSON.stringify(node.content.styles)})` });
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'Text': {
|
case 'Text': {
|
||||||
const text = getTextFromAttribute(node);
|
const text = getTextFromAttribute(node);
|
||||||
if (mode === 'SLOT') {
|
if (mode === 'SLOT') {
|
||||||
|
@ -328,6 +332,9 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
|
||||||
collectionItem = undefined;
|
collectionItem = undefined;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
case 'Style': {
|
||||||
|
return;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error('Unexpected node type: ' + node.type);
|
throw new Error('Unexpected node type: ' + node.type);
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,6 +105,7 @@ export default async function (astroConfig: AstroConfig) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
console.error(err.code, err);
|
||||||
error(logging, 'running hmx', err);
|
error(logging, 'running hmx', err);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import type { Ast, TemplateNode } from '../compiler/interfaces';
|
|
||||||
import { NodeVisitor, Optimizer, VisitorFn } from './types';
|
|
||||||
import { walk } from 'estree-walker';
|
import { walk } from 'estree-walker';
|
||||||
|
import type { Ast, TemplateNode } from '../compiler/interfaces';
|
||||||
|
import { NodeVisitor, Optimizer, VisitorFn } from '../@types/optimizer';
|
||||||
import optimizeStyles from './styles.js';
|
import optimizeStyles from './styles.js';
|
||||||
|
|
||||||
interface VisitorCollection {
|
interface VisitorCollection {
|
||||||
|
@ -10,13 +9,12 @@ interface VisitorCollection {
|
||||||
}
|
}
|
||||||
|
|
||||||
function addVisitor(visitor: NodeVisitor, collection: VisitorCollection, nodeName: string, event: 'enter' | 'leave') {
|
function addVisitor(visitor: NodeVisitor, collection: VisitorCollection, nodeName: string, event: 'enter' | 'leave') {
|
||||||
if (event in visitor) {
|
if (typeof visitor[event] !== 'function') return;
|
||||||
if (collection[event].has(nodeName)) {
|
if (!collection[event]) collection[event] = new Map<string, VisitorFn[]>();
|
||||||
collection[event].get(nodeName)!.push(visitor[event]!);
|
|
||||||
}
|
|
||||||
|
|
||||||
collection.enter.set(nodeName, [visitor[event]!]);
|
const visitors = collection[event].get(nodeName) || [];
|
||||||
}
|
visitors.push(visitor[event] as any);
|
||||||
|
collection[event].set(nodeName, visitors);
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectVisitors(optimizer: Optimizer, htmlVisitors: VisitorCollection, cssVisitors: VisitorCollection, finalizers: Array<() => Promise<void>>) {
|
function collectVisitors(optimizer: Optimizer, htmlVisitors: VisitorCollection, cssVisitors: VisitorCollection, finalizers: Array<() => Promise<void>>) {
|
||||||
|
@ -77,8 +75,8 @@ export async function optimize(ast: Ast, opts: OptimizeOptions) {
|
||||||
|
|
||||||
collectVisitors(optimizeStyles(opts), htmlVisitors, cssVisitors, finalizers);
|
collectVisitors(optimizeStyles(opts), htmlVisitors, cssVisitors, finalizers);
|
||||||
|
|
||||||
walkAstWithVisitors(ast.html, htmlVisitors);
|
|
||||||
walkAstWithVisitors(ast.css, cssVisitors);
|
walkAstWithVisitors(ast.css, cssVisitors);
|
||||||
|
walkAstWithVisitors(ast.html, htmlVisitors);
|
||||||
|
|
||||||
// Run all of the finalizer functions in parallel because why not.
|
// Run all of the finalizer functions in parallel because why not.
|
||||||
await Promise.all(finalizers.map((fn) => fn()));
|
await Promise.all(finalizers.map((fn) => fn()));
|
||||||
|
|
|
@ -1,51 +1,200 @@
|
||||||
import type { Ast, TemplateNode } from '../compiler/interfaces';
|
import crypto from 'crypto';
|
||||||
import type { Optimizer } from './types';
|
import path from 'path';
|
||||||
import { transformStyle } from '../style.js';
|
import autoprefixer from 'autoprefixer';
|
||||||
|
import postcss from 'postcss';
|
||||||
|
import postcssModules from 'postcss-modules';
|
||||||
|
import sass from 'sass';
|
||||||
|
import { Optimizer } from '../@types/optimizer';
|
||||||
|
import type { TemplateNode } from '../compiler/interfaces';
|
||||||
|
|
||||||
|
type StyleType = 'text/css' | 'text/scss' | 'text/sass' | 'text/postcss';
|
||||||
|
|
||||||
|
const getStyleType: Map<string, StyleType> = new Map([
|
||||||
|
['.css', 'text/css'],
|
||||||
|
['.pcss', 'text/postcss'],
|
||||||
|
['.sass', 'text/sass'],
|
||||||
|
['.scss', 'text/scss'],
|
||||||
|
['css', 'text/css'],
|
||||||
|
['postcss', 'text/postcss'],
|
||||||
|
['sass', 'text/sass'],
|
||||||
|
['scss', 'text/scss'],
|
||||||
|
['text/css', 'text/css'],
|
||||||
|
['text/postcss', 'text/postcss'],
|
||||||
|
['text/sass', 'text/sass'],
|
||||||
|
['text/scss', 'text/scss'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const SASS_OPTIONS: Partial<sass.Options> = {
|
||||||
|
outputStyle: 'compressed',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Should be deterministic, given a unique filename */
|
||||||
|
function hashFromFilename(filename: string): string {
|
||||||
|
const hash = crypto.createHash('sha256');
|
||||||
|
return hash
|
||||||
|
.update(filename.replace(/\\/g, '/'))
|
||||||
|
.digest('base64')
|
||||||
|
.toString()
|
||||||
|
.replace(/[^A-Za-z0-9-]/g, '')
|
||||||
|
.substr(0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StyleTransformResult {
|
||||||
|
css: string;
|
||||||
|
cssModules: Map<string, string>;
|
||||||
|
type: StyleType;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function transformStyle(code: string, { type, filename, fileID }: { type?: string; filename: string; fileID: string }): Promise<StyleTransformResult> {
|
||||||
|
let styleType: StyleType = 'text/css'; // important: assume CSS as default
|
||||||
|
if (type) {
|
||||||
|
styleType = getStyleType.get(type) || styleType;
|
||||||
|
}
|
||||||
|
|
||||||
|
let css = '';
|
||||||
|
switch (styleType) {
|
||||||
|
case 'text/css': {
|
||||||
|
css = code;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'text/sass':
|
||||||
|
case 'text/scss': {
|
||||||
|
css = sass
|
||||||
|
.renderSync({
|
||||||
|
...SASS_OPTIONS,
|
||||||
|
data: code,
|
||||||
|
includePaths: [path.dirname(filename)],
|
||||||
|
})
|
||||||
|
.css.toString('utf8');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'text/postcss': {
|
||||||
|
css = code; // TODO
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unsupported: <style type="${styleType}">`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssModules = new Map<string, string>();
|
||||||
|
|
||||||
|
css = await postcss([
|
||||||
|
postcssModules({
|
||||||
|
generateScopedName(name: string) {
|
||||||
|
return `${name}__${hashFromFilename(fileID)}`;
|
||||||
|
},
|
||||||
|
getJSON(_: string, json: any) {
|
||||||
|
Object.entries(json).forEach(([k, v]: any) => {
|
||||||
|
if (k !== v) cssModules.set(k, v);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
autoprefixer(),
|
||||||
|
])
|
||||||
|
.process(css, { from: filename, to: undefined })
|
||||||
|
.then((result) => result.css);
|
||||||
|
|
||||||
|
return {
|
||||||
|
css,
|
||||||
|
cssModules,
|
||||||
|
type: styleType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default function ({ filename, fileID }: { filename: string; fileID: string }): Optimizer {
|
export default function ({ filename, fileID }: { filename: string; fileID: string }): Optimizer {
|
||||||
const classNames: Set<string> = new Set();
|
const elementNodes: TemplateNode[] = []; // elements that need CSS Modules class names
|
||||||
let stylesPromises: any[] = [];
|
const styleNodes: TemplateNode[] = []; // <style> tags to be updated
|
||||||
|
const styleTransformPromises: Promise<StyleTransformResult>[] = []; // async style transform results to be finished in finalize();
|
||||||
|
let rootNode: TemplateNode; // root node which needs <style> tags
|
||||||
|
|
||||||
return {
|
return {
|
||||||
visitors: {
|
visitors: {
|
||||||
html: {
|
html: {
|
||||||
Element: {
|
Element: {
|
||||||
enter(node) {
|
enter(node) {
|
||||||
|
// Find the root node to inject the <style> tag in later
|
||||||
|
if (node.name === 'head') {
|
||||||
|
rootNode = node; // If this is <head>, this is what we want. Always take this if found. However, this may not always exist (it won’t for Component subtrees).
|
||||||
|
} else if (!rootNode) {
|
||||||
|
rootNode = node; // If no <head> (yet), then take the first element we come to and assume it‘s the “root” (but if we find a <head> later, then override this per the above)
|
||||||
|
}
|
||||||
|
|
||||||
for (let attr of node.attributes) {
|
for (let attr of node.attributes) {
|
||||||
if (attr.name === 'class') {
|
if (attr.name !== 'class') continue;
|
||||||
for (let value of attr.value) {
|
elementNodes.push(node);
|
||||||
if (value.type === 'Text') {
|
|
||||||
const classes = value.data.split(' ');
|
|
||||||
for (const className in classes) {
|
|
||||||
classNames.add(className);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// CSS: compile styles, apply CSS Modules scoping
|
||||||
css: {
|
css: {
|
||||||
Style: {
|
Style: {
|
||||||
enter(node: TemplateNode) {
|
enter(node) {
|
||||||
const code = node.content.styles;
|
const code = node.content.styles;
|
||||||
const typeAttr = node.attributes && node.attributes.find(({ name }: { name: string }) => name === 'type');
|
const typeAttr = (node.attributes || []).find(({ name }: { name: string }) => name === 'type');
|
||||||
stylesPromises.push(
|
styleNodes.push(node);
|
||||||
transformStyle(code, {
|
styleTransformPromises.push(transformStyle(code, { type: (typeAttr.value[0] && typeAttr.value[0].raw) || undefined, filename, fileID }));
|
||||||
type: (typeAttr.value[0] && typeAttr.value[0].raw) || undefined,
|
|
||||||
classNames,
|
// TODO: we should delete the old untransformed <style> node after we’re done.
|
||||||
filename,
|
// However, the svelte parser left it in ast.css, not ast.html. At the final step, this just gets ignored, so it will be deleted, in a sense.
|
||||||
fileID,
|
// If we ever end up scanning ast.css for something else, then we’ll need to actually delete the node (or transform it to the processed version)
|
||||||
})
|
|
||||||
); // TODO: styles needs to go in <head>
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async finalize() {
|
async finalize() {
|
||||||
const styles = await Promise.all(stylesPromises); // TODO: clean this up
|
const allCssModules = new Map<string, string>(); // note: this may theoretically have conflicts, but when written, it shouldn’t because we’re processing everything per-component (if we change this to run across the whole document at once, revisit this)
|
||||||
// console.log({ styles });
|
const styleTransforms = await Promise.all(styleTransformPromises);
|
||||||
|
|
||||||
|
if (!rootNode) {
|
||||||
|
throw new Error(`No root node found`); // TODO: remove this eventually; we should always find it, but for now alert if there’s a bug in our code
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. transform <style> tags
|
||||||
|
styleTransforms.forEach((result, n) => {
|
||||||
|
if (styleNodes[n].attributes) {
|
||||||
|
// Add to global CSS Module class list for step 2
|
||||||
|
for (const [k, v] of result.cssModules) {
|
||||||
|
allCssModules.set(k, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update original <style> node with finished results
|
||||||
|
styleNodes[n].attributes = styleNodes[n].attributes.map((attr: any) => {
|
||||||
|
if (attr.name === 'type') {
|
||||||
|
attr.value[0].raw = 'text/css';
|
||||||
|
attr.value[0].data = 'text/css';
|
||||||
|
}
|
||||||
|
return attr;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
styleNodes[n].content.styles = result.css;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. inject finished <style> tags into root node
|
||||||
|
rootNode.children = [...styleNodes, ...(rootNode.children || [])];
|
||||||
|
|
||||||
|
// 3. update HTML classes
|
||||||
|
for (let i = 0; i < elementNodes.length; i++) {
|
||||||
|
if (!elementNodes[i].attributes) continue;
|
||||||
|
const node = elementNodes[i];
|
||||||
|
for (let j = 0; j < node.attributes.length; j++) {
|
||||||
|
if (node.attributes[j].name !== 'class') continue;
|
||||||
|
const attr = node.attributes[j];
|
||||||
|
for (let k = 0; k < attr.value.length; k++) {
|
||||||
|
if (attr.value[k].type !== 'Text') continue;
|
||||||
|
const elementClassNames = (attr.value[k].raw as string)
|
||||||
|
.split(' ')
|
||||||
|
.map((c) => {
|
||||||
|
let className = c.trim();
|
||||||
|
return allCssModules.get(className) || className; // if className matches exactly, replace; otherwise keep original
|
||||||
|
})
|
||||||
|
.join(' ');
|
||||||
|
attr.value[k].raw = elementClassNames;
|
||||||
|
attr.value[k].data = elementClassNames;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
92
src/style.ts
92
src/style.ts
|
@ -1,92 +0,0 @@
|
||||||
import crypto from 'crypto';
|
|
||||||
import path from 'path';
|
|
||||||
import autoprefixer from 'autoprefixer';
|
|
||||||
import postcss from 'postcss';
|
|
||||||
import postcssModules from 'postcss-modules';
|
|
||||||
import sass from 'sass';
|
|
||||||
|
|
||||||
type StyleType = 'text/css' | 'text/scss' | 'text/sass' | 'text/postcss';
|
|
||||||
|
|
||||||
const getStyleType: Map<string, StyleType> = new Map([
|
|
||||||
['.css', 'text/css'],
|
|
||||||
['.pcss', 'text/postcss'],
|
|
||||||
['.sass', 'text/sass'],
|
|
||||||
['.scss', 'text/scss'],
|
|
||||||
['css', 'text/css'],
|
|
||||||
['postcss', 'text/postcss'],
|
|
||||||
['sass', 'text/sass'],
|
|
||||||
['scss', 'text/scss'],
|
|
||||||
['text/css', 'text/css'],
|
|
||||||
['text/postcss', 'text/postcss'],
|
|
||||||
['text/sass', 'text/sass'],
|
|
||||||
['text/scss', 'text/scss'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
const SASS_OPTIONS: Partial<sass.Options> = {
|
|
||||||
outputStyle: 'compressed',
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Should be deterministic, given a unique filename */
|
|
||||||
function hashFromFilename(filename: string): string {
|
|
||||||
const hash = crypto.createHash('sha256');
|
|
||||||
return hash.update(filename.replace(/\\/g, '/')).digest('base64').toString().substr(0, 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function transformStyle(
|
|
||||||
code: string,
|
|
||||||
{ type, classNames, filename, fileID }: { type?: string; classNames?: Set<string>; filename: string; fileID: string }
|
|
||||||
): Promise<{ css: string; cssModules: Map<string, string> }> {
|
|
||||||
let styleType: StyleType = 'text/css'; // important: assume CSS as default
|
|
||||||
if (type) {
|
|
||||||
styleType = getStyleType.get(type) || styleType;
|
|
||||||
}
|
|
||||||
|
|
||||||
let css = '';
|
|
||||||
switch (styleType) {
|
|
||||||
case 'text/css': {
|
|
||||||
css = code;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'text/sass':
|
|
||||||
case 'text/scss': {
|
|
||||||
css = sass
|
|
||||||
.renderSync({
|
|
||||||
...SASS_OPTIONS,
|
|
||||||
data: code,
|
|
||||||
includePaths: [path.dirname(filename)],
|
|
||||||
})
|
|
||||||
.css.toString('utf8');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'text/postcss': {
|
|
||||||
css = code; // TODO
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
throw new Error(`Unsupported: <style type="${styleType}">`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cssModules = new Map<string, string>();
|
|
||||||
|
|
||||||
css = await postcss([
|
|
||||||
postcssModules({
|
|
||||||
generateScopedName(name: string) {
|
|
||||||
if (classNames && classNames.has(name)) {
|
|
||||||
return `${name}__${hashFromFilename(fileID)}`;
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
},
|
|
||||||
getJSON(_: string, json: any) {
|
|
||||||
Object.entries(json).forEach(([k, v]: any) => {
|
|
||||||
if (k !== v) cssModules.set(k, v);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
autoprefixer(),
|
|
||||||
])
|
|
||||||
.process(css, { from: filename, to: undefined })
|
|
||||||
.then((result) => result.css);
|
|
||||||
|
|
||||||
return { css, cssModules };
|
|
||||||
}
|
|
|
@ -126,13 +126,18 @@ export async function compileComponent(
|
||||||
{ compileOptions = defaultCompileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string }
|
{ compileOptions = defaultCompileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string }
|
||||||
): Promise<CompileResult> {
|
): Promise<CompileResult> {
|
||||||
const sourceJsx = await transformFromSource(source, { compileOptions, filename, projectRoot });
|
const sourceJsx = await transformFromSource(source, { compileOptions, filename, projectRoot });
|
||||||
const componentJsx = sourceJsx.items.find((item) => item.name === 'Component');
|
|
||||||
if (!componentJsx) {
|
// throw error if <Component /> missing
|
||||||
throw new Error(`${filename} <Component> expected!`);
|
if (!sourceJsx.items.find(({ name }) => name === 'Component')) throw new Error(`${filename} <Component> expected!`);
|
||||||
}
|
|
||||||
|
// sort <style> tags first
|
||||||
|
// TODO: remove these and inject in <head>
|
||||||
|
sourceJsx.items.sort((a, b) => (a.name === 'style' && b.name !== 'style' ? -1 : 0));
|
||||||
|
|
||||||
|
// return template
|
||||||
const modJsx = `
|
const modJsx = `
|
||||||
import { h, Fragment } from '${internalImport('h.js')}';
|
import { h, Fragment } from '${internalImport('h.js')}';
|
||||||
export default function(props) { return h(Fragment, null, ${componentJsx.jsx}); }
|
export default function(props) { return h(Fragment, null, ${sourceJsx.items.map(({ jsx }) => jsx).join(',')}); }
|
||||||
`.trim();
|
`.trim();
|
||||||
return {
|
return {
|
||||||
contents: modJsx,
|
contents: modJsx,
|
||||||
|
|
Loading…
Add table
Reference in a new issue