summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGravatar Drew Powers <1369770+drwpow@users.noreply.github.com> 2021-03-18 11:25:19 -0600
committerGravatar GitHub <noreply@github.com> 2021-03-18 11:25:19 -0600
commit5661b289149761106585abe7695f3ccc2a7a4045 (patch)
tree9f5bc7ad8225475a581f47e37fee010bcfcb3c18 /src
parent48d73e3ab3a03509e2f2ac8720a4cd40633fb939 (diff)
downloadastro-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.ts5
-rw-r--r--src/@types/postcss-modules.d.ts2
-rw-r--r--src/style.ts92
-rw-r--r--src/transform2.ts84
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!`);