From 8ebc077cb0d9f50aae22d2651bd5ef13fe4641d3 Mon Sep 17 00:00:00 2001 From: Drew Powers <1369770+drwpow@users.noreply.github.com> Date: Fri, 19 Mar 2021 14:55:06 -0600 Subject: [PATCH] Inject styling in HTML AST (#9) * Inject styling in HTML AST * Restore optimize structure --- snowpack-plugin.cjs | 31 +-- src/@types/estree-walker.d.ts | 11 + .../types.ts => @types/optimizer.ts} | 11 +- src/codegen/index.ts | 9 +- src/dev.ts | 1 + src/optimize/index.ts | 18 +- src/optimize/styles.ts | 203 +++++++++++++++--- src/style.ts | 92 -------- src/transform2.ts | 15 +- 9 files changed, 236 insertions(+), 155 deletions(-) rename src/{optimize/types.ts => @types/optimizer.ts} (66%) delete mode 100644 src/style.ts diff --git a/snowpack-plugin.cjs b/snowpack-plugin.cjs index bbc21fa49..82be28b13 100644 --- a/snowpack-plugin.cjs +++ b/snowpack-plugin.cjs @@ -1,30 +1,33 @@ -const { readFile } = require("fs").promises; +const { readFile } = require('fs').promises; // Snowpack plugins must be CommonJS :( -const transformPromise = import("./lib/transform2.js"); +const transformPromise = import('./lib/transform2.js'); module.exports = function (snowpackConfig, { resolve } = {}) { return { - name: "snowpack-hmx", - knownEntrypoints: ["deepmerge"], + name: 'snowpack-hmx', + knownEntrypoints: ['deepmerge'], resolve: { - input: [".hmx", ".md"], - output: [".js"], + input: ['.hmx', '.md'], + output: ['.js'], }, async load({ filePath }) { const { compilePage, compileComponent } = await transformPromise; 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 }); return result.contents; } - - const result = await compilePage(contents, { compileOptions: { resolve }, filename: filePath, projectRoot }); + const result = await compilePage(contents, { + compileOptions: { resolve }, + filename: filePath, + projectRoot, + }); try { - return /* js */ ` + return /* js */ ` ${result.contents} export default async (childDatas, childRenderFns) => { @@ -36,12 +39,12 @@ module.exports = function (snowpackConfig, { resolve } = {}) { if (_data.layout) { const renderLayout = (await import('/_hmx/layouts/' + _data.layout.replace(/.*layouts\\//, "").replace(/\.hmx$/, '.js'))).default; return renderLayout( - [...(childDatas || []), _data], + [...(childDatas || []), _data], [...(childRenderFns || []), renderHmx] ); } const data = merge.all([_data, ...(childDatas || [])]); - let headResult; + let headResult; let bodyResult; for (const renderFn of (childRenderFns || [])) { let headAndBody = await Promise.all([ @@ -54,7 +57,7 @@ module.exports = function (snowpackConfig, { resolve } = {}) { return h(Fragment, null, [ renderHmx.head(data, headResult, true), renderHmx.body(data, bodyResult, true), - ]); + ]); }; `; } catch (err) { diff --git a/src/@types/estree-walker.d.ts b/src/@types/estree-walker.d.ts index 5afb476cb..a3b7da859 100644 --- a/src/@types/estree-walker.d.ts +++ b/src/@types/estree-walker.d.ts @@ -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; } ): T; + + export function asyncWalk( + 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; } diff --git a/src/optimize/types.ts b/src/@types/optimizer.ts similarity index 66% rename from src/optimize/types.ts rename to src/@types/optimizer.ts index e22700cba..c62976068 100644 --- a/src/optimize/types.ts +++ b/src/@types/optimizer.ts @@ -1,6 +1,5 @@ import type { TemplateNode } from '../compiler/interfaces'; - export type VisitorFn = (node: TemplateNode) => void; export interface NodeVisitor { @@ -10,8 +9,8 @@ export interface NodeVisitor { export interface Optimizer { visitors?: { - html?: Record, - css?: Record - }, - finalize: () => Promise -} \ No newline at end of file + html?: Record; + css?: Record; + }; + finalize: () => Promise; +} diff --git a/src/codegen/index.ts b/src/codegen/index.ts index 3257d9936..9b3104f0a 100644 --- a/src/codegen/index.ts +++ b/src/codegen/index.ts @@ -190,7 +190,6 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro let collectionItem: JsxItem | undefined; let currentItemName: string | undefined; let currentDepth = 0; - const classNames: Set = new Set(); walk(ast.html, { enter(node: TemplateNode) { @@ -275,6 +274,11 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro this.skip(); 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': { const text = getTextFromAttribute(node); if (mode === 'SLOT') { @@ -328,6 +332,9 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro collectionItem = undefined; } return; + case 'Style': { + return; + } default: throw new Error('Unexpected node type: ' + node.type); } diff --git a/src/dev.ts b/src/dev.ts index 524379dd1..0872ffe74 100644 --- a/src/dev.ts +++ b/src/dev.ts @@ -105,6 +105,7 @@ export default async function (astroConfig: AstroConfig) { break; } default: { + console.error(err.code, err); error(logging, 'running hmx', err); break; } diff --git a/src/optimize/index.ts b/src/optimize/index.ts index a0604b1c8..9f8ec2f05 100644 --- a/src/optimize/index.ts +++ b/src/optimize/index.ts @@ -1,7 +1,6 @@ -import type { Ast, TemplateNode } from '../compiler/interfaces'; -import { NodeVisitor, Optimizer, VisitorFn } from './types'; import { walk } from 'estree-walker'; - +import type { Ast, TemplateNode } from '../compiler/interfaces'; +import { NodeVisitor, Optimizer, VisitorFn } from '../@types/optimizer'; import optimizeStyles from './styles.js'; interface VisitorCollection { @@ -10,13 +9,12 @@ interface VisitorCollection { } function addVisitor(visitor: NodeVisitor, collection: VisitorCollection, nodeName: string, event: 'enter' | 'leave') { - if (event in visitor) { - if (collection[event].has(nodeName)) { - collection[event].get(nodeName)!.push(visitor[event]!); - } + if (typeof visitor[event] !== 'function') return; + if (!collection[event]) collection[event] = new Map(); - 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>) { @@ -77,8 +75,8 @@ export async function optimize(ast: Ast, opts: OptimizeOptions) { collectVisitors(optimizeStyles(opts), htmlVisitors, cssVisitors, finalizers); - walkAstWithVisitors(ast.html, htmlVisitors); walkAstWithVisitors(ast.css, cssVisitors); + walkAstWithVisitors(ast.html, htmlVisitors); // Run all of the finalizer functions in parallel because why not. await Promise.all(finalizers.map((fn) => fn())); diff --git a/src/optimize/styles.ts b/src/optimize/styles.ts index 6d15cb602..1353cb006 100644 --- a/src/optimize/styles.ts +++ b/src/optimize/styles.ts @@ -1,51 +1,200 @@ -import type { Ast, TemplateNode } from '../compiler/interfaces'; -import type { Optimizer } from './types'; -import { transformStyle } from '../style.js'; +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'; +import { Optimizer } from '../@types/optimizer'; +import type { TemplateNode } from '../compiler/interfaces'; + +type StyleType = 'text/css' | 'text/scss' | 'text/sass' | 'text/postcss'; + +const getStyleType: Map = 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 = { + 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; + type: StyleType; +} + +async function transformStyle(code: string, { type, filename, fileID }: { type?: string; filename: string; fileID: string }): Promise { + 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: