diff options
author | 2021-03-18 11:25:19 -0600 | |
---|---|---|
committer | 2021-03-18 11:25:19 -0600 | |
commit | 5661b289149761106585abe7695f3ccc2a7a4045 (patch) | |
tree | 9f5bc7ad8225475a581f47e37fee010bcfcb3c18 /src | |
parent | 48d73e3ab3a03509e2f2ac8720a4cd40633fb939 (diff) | |
download | astro-5661b289149761106585abe7695f3ccc2a7a4045.tar.gz astro-5661b289149761106585abe7695f3ccc2a7a4045.tar.zst astro-5661b289149761106585abe7695f3ccc2a7a4045.zip |
Add style transforms (#7)
* Add style transforms
* Let crawler be sync
Diffstat (limited to 'src')
-rw-r--r-- | src/@types/astro.ts | 5 | ||||
-rw-r--r-- | src/@types/postcss-modules.d.ts | 2 | ||||
-rw-r--r-- | src/style.ts | 92 | ||||
-rw-r--r-- | src/transform2.ts | 84 |
4 files changed, 161 insertions, 22 deletions
diff --git a/src/@types/astro.ts b/src/@types/astro.ts index 710067599..02dcc8cf3 100644 --- a/src/@types/astro.ts +++ b/src/@types/astro.ts @@ -9,3 +9,8 @@ export interface AstroConfig { projectRoot: URL; hmxRoot: URL; } + +export interface JsxItem { + name: string; + jsx: string; +} diff --git a/src/@types/postcss-modules.d.ts b/src/@types/postcss-modules.d.ts new file mode 100644 index 000000000..4035404bd --- /dev/null +++ b/src/@types/postcss-modules.d.ts @@ -0,0 +1,2 @@ +// don’t need types; just a plugin +declare module 'postcss-modules'; diff --git a/src/style.ts b/src/style.ts new file mode 100644 index 000000000..527c13f99 --- /dev/null +++ b/src/style.ts @@ -0,0 +1,92 @@ +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 }) + .then((result) => result.css); + + return { css, cssModules }; +} diff --git a/src/transform2.ts b/src/transform2.ts index 15f36c777..e54845baa 100644 --- a/src/transform2.ts +++ b/src/transform2.ts @@ -12,6 +12,8 @@ import { parse } from './compiler/index.js'; import markdownEncode from './markdown-encode.js'; import { TemplateNode } from './compiler/interfaces.js'; import { defaultLogOptions, info } from './logger.js'; +import { transformStyle } from './style.js'; +import { JsxItem } from './@types/astro.js'; const { transformSync } = esbuild; @@ -30,7 +32,7 @@ interface CompileOptions { const defaultCompileOptions: CompileOptions = { logging: defaultLogOptions, - resolve: (p: string) => p + resolve: (p: string) => p, }; function internalImport(internalPath: string) { @@ -179,12 +181,12 @@ function compileScriptSafe(raw: string, loader: 'jsx' | 'tsx'): string { return code; } -async function convertHmxToJsx(template: string, filename: string, compileOptions: CompileOptions) { +async function convertHmxToJsx(template: string, { compileOptions, filename, fileID }: { compileOptions: CompileOptions; filename: string; fileID: string }) { await eslexer.init; const ast = parse(template, { - filename - }); + filename, + }); const script = compileScriptSafe(ast.instance ? ast.instance.content : '', 'tsx'); // Compile scripts as TypeScript, always @@ -201,11 +203,12 @@ async function convertHmxToJsx(template: string, filename: string, compileOption ); const additionalImports = new Set<string>(); - let items: { name: string; jsx: string }[] = []; + let items: JsxItem[] = []; let mode: 'JSX' | 'SCRIPT' | 'SLOT' = 'JSX'; - let collectionItem: { name: string; jsx: string } | undefined; + let collectionItem: JsxItem | undefined; let currentItemName: string | undefined; let currentDepth = 0; + const classNames: Set<string> = new Set(); walk(ast.html, { enter(node, parent, prop, index) { @@ -249,6 +252,7 @@ async function convertHmxToJsx(template: string, filename: string, compileOption if (!collectionItem) { return; } + break; case 'InlineComponent': case 'Element': const name: string = node.name; @@ -263,6 +267,14 @@ async function convertHmxToJsx(template: string, filename: string, compileOption collectionItem = { name, jsx: '' }; items.push(collectionItem); } + if (attributes.class) { + attributes.class + .replace(/^"/, '') + .replace(/"$/, '') + .split(' ') + .map((c) => c.trim()) + .forEach((c) => classNames.add(c)); + } collectionItem.jsx += collectionItem.jsx === '' ? '' : ','; const COMPONENT_NAME_SCANNER = /^[A-Z]/; if (!COMPONENT_NAME_SCANNER.test(name)) { @@ -282,6 +294,7 @@ async function convertHmxToJsx(template: string, filename: string, compileOption if (wrapperImport) { additionalImports.add(wrapperImport); } + collectionItem.jsx += `h(${wrapper}, ${attributes ? generateAttributes(attributes) : 'null'}`; return; case 'Attribute': { @@ -347,13 +360,31 @@ async function convertHmxToJsx(template: string, filename: string, compileOption }, }); - /* - console.log({ - additionalImports, - script, - items, + let stylesPromises: any[] = []; + walk(ast.css, { + enter(node) { + if (node.type !== 'Style') return; + + const code = node.content.styles; + const typeAttr = node.attributes && node.attributes.find(({ name }) => name === 'type'); + stylesPromises.push( + transformStyle(code, { + type: (typeAttr.value[0] && typeAttr.value[0].raw) || undefined, + classNames, + filename, + fileID, + }) + ); // TODO: styles needs to go in <head> + }, }); - */ + const styles = await Promise.all(stylesPromises); // TODO: clean this up + console.log({ styles }); + + // console.log({ + // additionalImports, + // script, + // items, + // }); return { script: script + '\n' + Array.from(additionalImports).join('\n'), @@ -361,7 +392,7 @@ async function convertHmxToJsx(template: string, filename: string, compileOption }; } -async function convertMdToJsx(contents: string, filename: string, compileOptions: CompileOptions) { +async function convertMdToJsx(contents: string, { compileOptions, filename, fileID }: { compileOptions: CompileOptions; filename: string; fileID: string }) { // This doesn't work. const { data: _frontmatterData, content } = matter(contents); const mdHtml = micromark(content, { @@ -389,24 +420,30 @@ async function convertMdToJsx(contents: string, filename: string, compileOptions `<script hmx="setup">export function setup() { return ${JSON.stringify(setupData)}; }</script><head></head><body>${mdHtml}</body>`, - filename, - compileOptions + { compileOptions, filename, fileID } ); } -async function transformFromSource(contents: string, filename: string, compileOptions: CompileOptions): Promise<ReturnType<typeof convertHmxToJsx>> { +async function transformFromSource( + contents: string, + { compileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string } +): Promise<ReturnType<typeof convertHmxToJsx>> { + const fileID = path.relative(projectRoot, filename); switch (path.extname(filename)) { case '.hmx': - return convertHmxToJsx(contents, filename, compileOptions); + return convertHmxToJsx(contents, { compileOptions, filename, fileID }); case '.md': - return convertMdToJsx(contents, filename, compileOptions); + return convertMdToJsx(contents, { compileOptions, filename, fileID }); default: throw new Error('Not Supported!'); } } -export async function compilePage(source: string, filename: string, opts: CompileOptions = defaultCompileOptions) { - const sourceJsx = await transformFromSource(source, filename, opts); +export async function compilePage( + source: string, + { compileOptions = defaultCompileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string } +) { + const sourceJsx = await transformFromSource(source, { compileOptions, filename, projectRoot }); const headItem = sourceJsx.items.find((item) => item.name === 'head'); const bodyItem = sourceJsx.items.find((item) => item.name === 'body'); const headItemJsx = !headItem ? 'null' : headItem.jsx.replace('"head"', 'isRoot ? "head" : Fragment'); @@ -425,8 +462,11 @@ export function body({title, description, props}, child, isRoot) { return (${bod }; } -export async function compileComponent(source: string, filename: string, opts: CompileOptions = defaultCompileOptions) { - const sourceJsx = await transformFromSource(source, filename, opts); +export async function compileComponent( + source: string, + { compileOptions = defaultCompileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string } +) { + const sourceJsx = await transformFromSource(source, { compileOptions, filename, projectRoot }); const componentJsx = sourceJsx.items.find((item) => item.name === 'Component'); if (!componentJsx) { throw new Error(`${filename} <Component> expected!`); |