Skip to content

Commit

Permalink
refactor(svelte/preprocess): use modern Svelte AST (#1068)
Browse files Browse the repository at this point in the history
  • Loading branch information
divdavem authored Dec 18, 2024
1 parent 8a8a9a4 commit 7b19623
Showing 1 changed file with 52 additions and 20 deletions.
72 changes: 52 additions & 20 deletions svelte/preprocess/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
import MagicString from 'magic-string';
import type {PreprocessorGroup} from 'svelte/compiler';
import {parse} from 'svelte/compiler';
import {parse, type AST} from 'svelte/compiler';
import type {Expression} from 'estree';

const isElement = (node: AST.BaseNode): node is AST.BaseElement => 'attributes' in node;
const isEachBlock = (node: AST.BaseNode): node is AST.EachBlock => node.type === 'EachBlock';
const isIfBlock = (node: AST.BaseNode): node is AST.IfBlock => node.type === 'IfBlock';
const isAwaitBlock = (node: AST.BaseNode): node is AST.AwaitBlock => node.type === 'AwaitBlock';
const isKeyBlock = (node: AST.BaseNode): node is AST.KeyBlock => node.type === 'KeyBlock';
const isSnippetBlock = (node: AST.BaseNode): node is AST.SnippetBlock => node.type === 'SnippetBlock';

// Svelte uses acorn to parse expressions
// and acorn adds start and end properties to nodes
// in addition to the properties defined by the ESTree spec
// (cf https://github.com/acornjs/acorn/tree/master/acorn/)
type AcornExpression = Expression & {
start: number;
end: number;
};

export const directivesPreprocess = (): PreprocessorGroup => {
return {
Expand All @@ -13,30 +30,36 @@ export const directivesPreprocess = (): PreprocessorGroup => {
return;
}
const str = new MagicString(content, {filename});
const parsedCode = parse(content, {filename});
const parsedCode = parse(content, {filename, modern: true});
const requiredImports = new Set<string>();

const extractValue = (attribute: any) => {
const extractValue = (attribute: AST.Attribute) => {
const res: string[] = [];
const value = attribute.value;
if (attribute.value === true) {
return 'true';
}
const value = Array.isArray(attribute.value) ? attribute.value : [attribute.value];
for (const part of value) {
if (part.type === 'Text') {
res.push(JSON.stringify(part.data));
} else if (part.type === 'MustacheTag') {
res.push(`(${content.substring(part.expression.start, part.expression.end)})`);
} else if (part.type === 'ExpressionTag') {
const expression = part.expression as AcornExpression;
res.push(`(${content.substring(expression.start, expression.end)})`);
} else {
throw new Error(`Unexpected part type: ${part.type}`);
throw new Error(`Assert failed, unexpected part`);
}
}
return res.join('+');
};

const processItem = (item: any) => {
const processFragment = (items: AST.Fragment | null | undefined) => items?.nodes.forEach(processItem);

const processItem = (item: AST.BaseNode) => {
const actionAttributes = [];
const classAttributes = [];
if (item.attributes) {
if (isElement(item)) {
for (const attribute of item.attributes) {
if (attribute.type === 'Action') {
if (attribute.type === 'UseDirective') {
actionAttributes.push(attribute);
} else if (attribute.type === 'Attribute' && attribute.name === 'class') {
classAttributes.push(attribute);
Expand All @@ -53,8 +76,9 @@ export const directivesPreprocess = (): PreprocessorGroup => {
} else {
str.appendRight(end, `, `);
}
if (attribute.expression) {
str.appendRight(end, `[${attribute.name}, ${content.substring(attribute.expression.start, attribute.expression.end)}]`);
const expression = attribute.expression as null | AcornExpression;
if (expression) {
str.appendRight(end, `[${attribute.name}, ${content.substring(expression.start, expression.end)}]`);
} else {
str.appendRight(end, attribute.name);
}
Expand All @@ -69,17 +93,25 @@ export const directivesPreprocess = (): PreprocessorGroup => {

str.appendRight(end, `)}`);
}
processFragment(item.fragment);
} else if (isEachBlock(item)) {
processFragment(item.body);
processFragment(item.fallback);
} else if (isIfBlock(item)) {
processFragment(item.consequent);
processFragment(item.alternate);
} else if (isAwaitBlock(item)) {
processFragment(item.pending);
processFragment(item.then);
processFragment(item.catch);
} else if (isKeyBlock(item)) {
processFragment(item.fragment);
} else if (isSnippetBlock(item)) {
processFragment(item.body);
}
item.children?.forEach(processItem);
// if/else blocks
item.else?.children?.forEach(processItem);
// await/then/catch blocks
item.pending?.children?.forEach(processItem);
item.then?.children?.forEach(processItem);
item.catch?.children?.forEach(processItem);
};

processItem(parsedCode.html);
processFragment(parsedCode.fragment);
if (requiredImports.size > 0) {
const importStatement = `\nimport {${[...requiredImports].map((importName) => `${importName} as ${varPrefix}${importName}`).join(', ')}} from '@agnos-ui/svelte-headless/utils/directive';\nimport {BROWSER as ${varPrefix}BROWSER} from 'esm-env';\n`;
const moduleOrInstance = parsedCode.module ?? parsedCode.instance;
Expand Down

0 comments on commit 7b19623

Please sign in to comment.