Respect comments when scanning imports
Use es-module-lexer for import scanning in HMX scripts
This commit is contained in:
parent
d27bd74b05
commit
d75107a20e
8 changed files with 66 additions and 56 deletions
|
@ -14,3 +14,12 @@ export interface JsxItem {
|
||||||
name: string;
|
name: string;
|
||||||
jsx: string;
|
jsx: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TransformResult {
|
||||||
|
script: string;
|
||||||
|
items: JsxItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompileResult {
|
||||||
|
contents: string;
|
||||||
|
}
|
||||||
|
|
|
@ -3,4 +3,4 @@ import type { LogOptions } from '../logger';
|
||||||
export interface CompileOptions {
|
export interface CompileOptions {
|
||||||
logging: LogOptions;
|
logging: LogOptions;
|
||||||
resolve: (p: string) => string;
|
resolve: (p: string) => string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { CompileOptions } from '../@types/compiler';
|
import type { CompileOptions } from '../@types/compiler';
|
||||||
import type { Ast, TemplateNode } from '../compiler/interfaces';
|
import type { Ast, TemplateNode } from '../compiler/interfaces';
|
||||||
import type { JsxItem } from '../@types/astro.js';
|
import type { JsxItem, TransformResult } from '../@types/astro.js';
|
||||||
|
|
||||||
import eslexer from 'es-module-lexer';
|
import eslexer from 'es-module-lexer';
|
||||||
import esbuild from 'esbuild';
|
import esbuild from 'esbuild';
|
||||||
|
@ -14,13 +14,13 @@ interface Attribute {
|
||||||
end: number;
|
end: number;
|
||||||
type: 'Attribute';
|
type: 'Attribute';
|
||||||
name: string;
|
name: string;
|
||||||
value: any
|
value: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CodeGenOptions {
|
interface CodeGenOptions {
|
||||||
compileOptions: CompileOptions;
|
compileOptions: CompileOptions;
|
||||||
filename: string;
|
filename: string;
|
||||||
fileID: string
|
fileID: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function internalImport(internalPath: string) {
|
function internalImport(internalPath: string) {
|
||||||
|
@ -144,14 +144,12 @@ function getComponentWrapper(_name: string, { type, url }: { type: string; url:
|
||||||
throw new Error('Unknown Component Type: ' + name);
|
throw new Error('Unknown Component Type: ' + name);
|
||||||
}
|
}
|
||||||
|
|
||||||
const patternImport = new RegExp(/import(?:["'\s]*([\w*${}\n\r\t, ]+)from\s*)?["'\s]["'\s](.*[@\w_-]+)["'\s].*;$/, 'mg');
|
|
||||||
function compileScriptSafe(raw: string, loader: 'jsx' | 'tsx'): string {
|
function compileScriptSafe(raw: string, loader: 'jsx' | 'tsx'): string {
|
||||||
// esbuild treeshakes unused imports. In our case these are components, so let's keep them.
|
// esbuild treeshakes unused imports. In our case these are components, so let's keep them.
|
||||||
const imports: Array<string> = [];
|
const imports = eslexer
|
||||||
raw.replace(patternImport, (value: string) => {
|
.parse(raw)[0]
|
||||||
imports.push(value);
|
.filter(({ d }) => d === -1)
|
||||||
return value;
|
.map((i: any) => raw.substring(i.ss, i.se));
|
||||||
});
|
|
||||||
|
|
||||||
let { code } = transformSync(raw, {
|
let { code } = transformSync(raw, {
|
||||||
loader,
|
loader,
|
||||||
|
@ -169,7 +167,8 @@ function compileScriptSafe(raw: string, loader: 'jsx' | 'tsx'): string {
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions) {
|
export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Promise<TransformResult> {
|
||||||
|
await eslexer.init;
|
||||||
const script = compileScriptSafe(ast.instance ? ast.instance.content : '', 'tsx');
|
const script = compileScriptSafe(ast.instance ? ast.instance.content : '', 'tsx');
|
||||||
|
|
||||||
// Compile scripts as TypeScript, always
|
// Compile scripts as TypeScript, always
|
||||||
|
@ -339,4 +338,4 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions) {
|
||||||
script: script + '\n' + Array.from(additionalImports).join('\n'),
|
script: script + '\n' + Array.from(additionalImports).join('\n'),
|
||||||
items,
|
items,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ snowpackLogger.level = 'silent';
|
||||||
|
|
||||||
const logging: LogOptions = {
|
const logging: LogOptions = {
|
||||||
level: 'debug',
|
level: 'debug',
|
||||||
dest: defaultLogDestination
|
dest: defaultLogDestination,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function (astroConfig: AstroConfig) {
|
export default async function (astroConfig: AstroConfig) {
|
||||||
|
@ -97,7 +97,7 @@ export default async function (astroConfig: AstroConfig) {
|
||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||||
res.end(html);
|
res.end(html);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
switch(err.code) {
|
switch (err.code) {
|
||||||
case 'parse-error': {
|
case 'parse-error': {
|
||||||
err.filename = pathRelative(projectRoot.pathname, err.filename);
|
err.filename = pathRelative(projectRoot.pathname, err.filename);
|
||||||
debugger;
|
debugger;
|
||||||
|
|
|
@ -10,8 +10,8 @@ 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 (event in visitor) {
|
||||||
if(collection[event].has(nodeName)) {
|
if (collection[event].has(nodeName)) {
|
||||||
collection[event].get(nodeName)!.push(visitor[event]!);
|
collection[event].get(nodeName)!.push(visitor[event]!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,15 +20,15 @@ function addVisitor(visitor: NodeVisitor, collection: VisitorCollection, nodeNam
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectVisitors(optimizer: Optimizer, htmlVisitors: VisitorCollection, cssVisitors: VisitorCollection, finalizers: Array<() => Promise<void>>) {
|
function collectVisitors(optimizer: Optimizer, htmlVisitors: VisitorCollection, cssVisitors: VisitorCollection, finalizers: Array<() => Promise<void>>) {
|
||||||
if(optimizer.visitors) {
|
if (optimizer.visitors) {
|
||||||
if(optimizer.visitors.html) {
|
if (optimizer.visitors.html) {
|
||||||
for(const [nodeName, visitor] of Object.entries(optimizer.visitors.html)) {
|
for (const [nodeName, visitor] of Object.entries(optimizer.visitors.html)) {
|
||||||
addVisitor(visitor, htmlVisitors, nodeName, 'enter');
|
addVisitor(visitor, htmlVisitors, nodeName, 'enter');
|
||||||
addVisitor(visitor, htmlVisitors, nodeName, 'leave');
|
addVisitor(visitor, htmlVisitors, nodeName, 'leave');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(optimizer.visitors.css) {
|
if (optimizer.visitors.css) {
|
||||||
for(const [nodeName, visitor] of Object.entries(optimizer.visitors.css)) {
|
for (const [nodeName, visitor] of Object.entries(optimizer.visitors.css)) {
|
||||||
addVisitor(visitor, cssVisitors, nodeName, 'enter');
|
addVisitor(visitor, cssVisitors, nodeName, 'enter');
|
||||||
addVisitor(visitor, cssVisitors, nodeName, 'leave');
|
addVisitor(visitor, cssVisitors, nodeName, 'leave');
|
||||||
}
|
}
|
||||||
|
@ -47,27 +47,27 @@ function createVisitorCollection() {
|
||||||
function walkAstWithVisitors(tmpl: TemplateNode, collection: VisitorCollection) {
|
function walkAstWithVisitors(tmpl: TemplateNode, collection: VisitorCollection) {
|
||||||
walk(tmpl, {
|
walk(tmpl, {
|
||||||
enter(node) {
|
enter(node) {
|
||||||
if(collection.enter.has(node.type)) {
|
if (collection.enter.has(node.type)) {
|
||||||
const fns = collection.enter.get(node.type)!;
|
const fns = collection.enter.get(node.type)!;
|
||||||
for(let fn of fns) {
|
for (let fn of fns) {
|
||||||
fn(node);
|
fn(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
leave(node) {
|
leave(node) {
|
||||||
if(collection.leave.has(node.type)) {
|
if (collection.leave.has(node.type)) {
|
||||||
const fns = collection.leave.get(node.type)!;
|
const fns = collection.leave.get(node.type)!;
|
||||||
for(let fn of fns) {
|
for (let fn of fns) {
|
||||||
fn(node);
|
fn(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OptimizeOptions {
|
interface OptimizeOptions {
|
||||||
filename: string,
|
filename: string;
|
||||||
fileID: string
|
fileID: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function optimize(ast: Ast, opts: OptimizeOptions) {
|
export async function optimize(ast: Ast, opts: OptimizeOptions) {
|
||||||
|
@ -81,5 +81,5 @@ export async function optimize(ast: Ast, opts: OptimizeOptions) {
|
||||||
walkAstWithVisitors(ast.css, cssVisitors);
|
walkAstWithVisitors(ast.css, cssVisitors);
|
||||||
|
|
||||||
// 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,8 +1,8 @@
|
||||||
import type { Ast, TemplateNode } from '../compiler/interfaces';
|
import type { Ast, TemplateNode } from '../compiler/interfaces';
|
||||||
import type { Optimizer } from './types'
|
import type { Optimizer } from './types';
|
||||||
import { transformStyle } from '../style.js';
|
import { transformStyle } from '../style.js';
|
||||||
|
|
||||||
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 classNames: Set<string> = new Set();
|
||||||
let stylesPromises: any[] = [];
|
let stylesPromises: any[] = [];
|
||||||
|
|
||||||
|
@ -11,20 +11,20 @@ export default function({ filename, fileID }: { filename: string, fileID: string
|
||||||
html: {
|
html: {
|
||||||
Element: {
|
Element: {
|
||||||
enter(node) {
|
enter(node) {
|
||||||
for(let attr of node.attributes) {
|
for (let attr of node.attributes) {
|
||||||
if(attr.name === 'class') {
|
if (attr.name === 'class') {
|
||||||
for(let value of attr.value) {
|
for (let value of attr.value) {
|
||||||
if(value.type === 'Text') {
|
if (value.type === 'Text') {
|
||||||
const classes = value.data.split(' ');
|
const classes = value.data.split(' ');
|
||||||
for(const className in classes) {
|
for (const className in classes) {
|
||||||
classNames.add(className);
|
classNames.add(className);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
css: {
|
css: {
|
||||||
Style: {
|
Style: {
|
||||||
|
@ -39,13 +39,13 @@ export default function({ filename, fileID }: { filename: string, fileID: string
|
||||||
fileID,
|
fileID,
|
||||||
})
|
})
|
||||||
); // TODO: styles needs to go in <head>
|
); // TODO: styles needs to go in <head>
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
async finalize() {
|
async finalize() {
|
||||||
const styles = await Promise.all(stylesPromises); // TODO: clean this up
|
const styles = await Promise.all(stylesPromises); // TODO: clean this up
|
||||||
console.log({ styles });
|
// console.log({ styles });
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,7 +85,7 @@ export async function transformStyle(
|
||||||
}),
|
}),
|
||||||
autoprefixer(),
|
autoprefixer(),
|
||||||
])
|
])
|
||||||
.process(css, { from: filename })
|
.process(css, { from: filename, to: undefined })
|
||||||
.then((result) => result.css);
|
.then((result) => result.css);
|
||||||
|
|
||||||
return { css, cssModules };
|
return { css, cssModules };
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import type { LogOptions } from './logger.js';
|
import type { LogOptions } from './logger.js';
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import esbuild from 'esbuild';
|
|
||||||
import eslexer from 'es-module-lexer';
|
|
||||||
import micromark from 'micromark';
|
import micromark from 'micromark';
|
||||||
import gfmSyntax from 'micromark-extension-gfm';
|
import gfmSyntax from 'micromark-extension-gfm';
|
||||||
import matter from 'gray-matter';
|
import matter from 'gray-matter';
|
||||||
import gfmHtml from 'micromark-extension-gfm/html.js';
|
import gfmHtml from 'micromark-extension-gfm/html.js';
|
||||||
|
import { CompileResult, TransformResult } from './@types/astro';
|
||||||
import { parse } from './compiler/index.js';
|
import { parse } from './compiler/index.js';
|
||||||
import markdownEncode from './markdown-encode.js';
|
import markdownEncode from './markdown-encode.js';
|
||||||
import { defaultLogOptions } from './logger.js';
|
import { defaultLogOptions } from './logger.js';
|
||||||
|
@ -30,12 +29,11 @@ function internalImport(internalPath: string) {
|
||||||
interface ConvertHmxOptions {
|
interface ConvertHmxOptions {
|
||||||
compileOptions: CompileOptions;
|
compileOptions: CompileOptions;
|
||||||
filename: string;
|
filename: string;
|
||||||
fileID: string
|
fileID: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function convertHmxToJsx(template: string, opts: ConvertHmxOptions) {
|
async function convertHmxToJsx(template: string, opts: ConvertHmxOptions): Promise<TransformResult> {
|
||||||
const { filename } = opts;
|
const { filename } = opts;
|
||||||
await eslexer.init;
|
|
||||||
|
|
||||||
// 1. Parse
|
// 1. Parse
|
||||||
const ast = parse(template, {
|
const ast = parse(template, {
|
||||||
|
@ -49,7 +47,10 @@ async function convertHmxToJsx(template: string, opts: ConvertHmxOptions) {
|
||||||
return await codegen(ast, opts);
|
return await codegen(ast, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function convertMdToJsx(contents: string, { compileOptions, filename, fileID }: { compileOptions: CompileOptions; filename: string; fileID: string }) {
|
async function convertMdToJsx(
|
||||||
|
contents: string,
|
||||||
|
{ compileOptions, filename, fileID }: { compileOptions: CompileOptions; filename: string; fileID: string }
|
||||||
|
): Promise<TransformResult> {
|
||||||
// This doesn't work.
|
// This doesn't work.
|
||||||
const { data: _frontmatterData, content } = matter(contents);
|
const { data: _frontmatterData, content } = matter(contents);
|
||||||
const mdHtml = micromark(content, {
|
const mdHtml = micromark(content, {
|
||||||
|
@ -84,7 +85,7 @@ async function convertMdToJsx(contents: string, { compileOptions, filename, file
|
||||||
async function transformFromSource(
|
async function transformFromSource(
|
||||||
contents: string,
|
contents: string,
|
||||||
{ compileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string }
|
{ compileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string }
|
||||||
): Promise<ReturnType<typeof convertHmxToJsx>> {
|
): Promise<TransformResult> {
|
||||||
const fileID = path.relative(projectRoot, filename);
|
const fileID = path.relative(projectRoot, filename);
|
||||||
switch (path.extname(filename)) {
|
switch (path.extname(filename)) {
|
||||||
case '.hmx':
|
case '.hmx':
|
||||||
|
@ -99,8 +100,9 @@ async function transformFromSource(
|
||||||
export async function compilePage(
|
export async function compilePage(
|
||||||
source: string,
|
source: string,
|
||||||
{ compileOptions = defaultCompileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string }
|
{ compileOptions = defaultCompileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string }
|
||||||
) {
|
): Promise<CompileResult> {
|
||||||
const sourceJsx = await transformFromSource(source, { compileOptions, filename, projectRoot });
|
const sourceJsx = await transformFromSource(source, { compileOptions, filename, projectRoot });
|
||||||
|
|
||||||
const headItem = sourceJsx.items.find((item) => item.name === 'head');
|
const headItem = sourceJsx.items.find((item) => item.name === 'head');
|
||||||
const bodyItem = sourceJsx.items.find((item) => item.name === 'body');
|
const bodyItem = sourceJsx.items.find((item) => item.name === 'body');
|
||||||
const headItemJsx = !headItem ? 'null' : headItem.jsx.replace('"head"', 'isRoot ? "head" : Fragment');
|
const headItemJsx = !headItem ? 'null' : headItem.jsx.replace('"head"', 'isRoot ? "head" : Fragment');
|
||||||
|
@ -122,7 +124,7 @@ export function body({title, description, props}, child, isRoot) { return (${bod
|
||||||
export async function compileComponent(
|
export async function compileComponent(
|
||||||
source: string,
|
source: string,
|
||||||
{ compileOptions = defaultCompileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string }
|
{ compileOptions = defaultCompileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string }
|
||||||
) {
|
): 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');
|
const componentJsx = sourceJsx.items.find((item) => item.name === 'Component');
|
||||||
if (!componentJsx) {
|
if (!componentJsx) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue