Skip to content

Commit e4af8c7

Browse files
committed
implement proper svelte file processing
1 parent 62444cc commit e4af8c7

File tree

6 files changed

+78
-138
lines changed

6 files changed

+78
-138
lines changed

packages/addons/common.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { imports, exports, common } from '@sveltejs/cli-core/js';
2+
import { toSvelteFragment, type SvelteAst } from '@sveltejs/cli-core/html';
23
import { parseScript, parseSvelte } from '@sveltejs/cli-core/parsers';
34
import process from 'node:process';
45

@@ -64,17 +65,29 @@ export function addEslintConfigPrettier(content: string): string {
6465
}
6566

6667
export function addToDemoPage(content: string, path: string): string {
67-
const { template, generateCode } = parseSvelte(content);
68-
69-
for (const node of template.ast.childNodes) {
70-
if (node.type === 'tag' && node.attribs['href'] === `/demo/${path}`) {
71-
return content;
68+
const { ast, generateCode } = parseSvelte(content);
69+
70+
for (const node of ast.fragment.nodes) {
71+
if (node.type === 'RegularElement') {
72+
const hrefAttribute = node.attributes.find(
73+
(x) => x.type === 'Attribute' && x.name === 'href'
74+
) as SvelteAst.Attribute;
75+
if (!hrefAttribute || !hrefAttribute.value) continue;
76+
77+
if (!Array.isArray(hrefAttribute.value)) continue;
78+
79+
const hasDemo = hrefAttribute.value.find(
80+
(x) => x.type === 'Text' && x.data === `/demo/${path}`
81+
);
82+
if (hasDemo) {
83+
return content;
84+
}
7285
}
7386
}
7487

75-
const newLine = template.source ? '\n' : '';
76-
const src = template.source + `${newLine}<a href="/demo/${path}">${path}</a>`;
77-
return generateCode({ template: src });
88+
ast.fragment.nodes.push(...toSvelteFragment(`<a href="/demo/${path}">${path}</a>`));
89+
90+
return generateCode();
7891
}
7992

8093
/**

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"picocolors": "^1.1.1",
4141
"postcss": "^8.5.6",
4242
"silver-fleece": "^1.2.1",
43+
"svelte": "https://pkg.pr.new/sveltejs/svelte@1377c40",
4344
"yaml": "^2.8.1",
4445
"zimmerframe": "^1.1.2"
4546
},

packages/core/tooling/html/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import {
77
parseHtml
88
} from '../index.ts';
99
import { appendFromString } from '../js/common.ts';
10+
import { parseSvelte } from '../parsers.ts';
11+
import type { AST as SvelteAst } from 'svelte/compiler';
1012

1113
export { HtmlElement, HtmlElementType };
12-
export type { HtmlDocument };
14+
export type { HtmlDocument, SvelteAst };
1315

1416
export function createDiv(attributes: Record<string, string> = {}): HtmlElement {
1517
return createElement('div', attributes);
@@ -58,3 +60,9 @@ export function addSlot(
5860
});
5961
addFromRawHtml(options.htmlAst.childNodes, '{@render children()}');
6062
}
63+
64+
export function toSvelteFragment(content: string): SvelteAst.Fragment['nodes'] {
65+
// TODO write test
66+
const { ast } = parseSvelte(content);
67+
return ast.fragment.nodes;
68+
}

packages/core/tooling/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { print as esrapPrint } from 'esrap';
1717
import ts, { type AdditionalComment } from 'esrap/languages/ts';
1818
import * as acorn from 'acorn';
1919
import { tsPlugin } from '@sveltejs/acorn-typescript';
20+
import { parse as svelteParse, type AST as SvelteAst, print as sveltePrint } from 'svelte/compiler';
2021
import * as yaml from 'yaml';
2122

2223
type AdditionalCommentMap = WeakMap<TsEstree.Node, AdditionalComment[]>;
@@ -41,6 +42,7 @@ export {
4142
export type {
4243
// html
4344
ChildNode as HtmlChildNode,
45+
SvelteAst,
4446

4547
// js
4648
TsEstree as AstTypes,
@@ -216,3 +218,11 @@ export function parseYaml(content: string): ReturnType<typeof yaml.parseDocument
216218
export function serializeYaml(data: ReturnType<typeof yaml.parseDocument>): string {
217219
return yaml.stringify(data, { singleQuote: true });
218220
}
221+
222+
export function parseSvelte(content: string): SvelteAst.Root {
223+
return svelteParse(content, { modern: true });
224+
}
225+
226+
export function serializeSvelte(ast: SvelteAst.Root): string {
227+
return sveltePrint(ast).code;
228+
}

packages/core/tooling/parsers.ts

Lines changed: 5 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as utils from './index.ts';
2-
import MagicString from 'magic-string';
32

43
type ParseBase = {
54
source: string;
@@ -49,136 +48,13 @@ export function parseYaml(
4948
return { data, source, generateCode };
5049
}
5150

52-
type SvelteGenerator = (code: {
53-
script?: string;
54-
module?: string;
55-
css?: string;
56-
template?: string;
57-
}) => string;
58-
export function parseSvelte(
59-
source: string,
60-
options?: { typescript?: boolean }
61-
): {
62-
script: ReturnType<typeof parseScript>;
63-
module: ReturnType<typeof parseScript>;
64-
css: ReturnType<typeof parseCss>;
65-
template: ReturnType<typeof parseHtml>;
66-
generateCode: SvelteGenerator;
67-
} {
68-
// `xTag` captures the whole tag block (ex: <script>...</script>)
69-
// `xSource` is the contents within the tags
70-
const scripts = extractScripts(source);
71-
// instance block
72-
const { tag: scriptTag = '', src: scriptSource = '' } =
73-
scripts.find(({ attrs }) => !attrs.includes('module')) ?? {};
74-
// module block
75-
const { tag: moduleScriptTag = '', src: moduleSource = '' } =
76-
scripts.find(({ attrs }) => attrs.includes('module')) ?? {};
77-
// style block
78-
const { styleTag, cssSource } = extractStyle(source);
79-
// rest of the template
80-
// TODO: needs more testing
81-
const templateSource = source
82-
.replace(moduleScriptTag, '')
83-
.replace(scriptTag, '')
84-
.replace(styleTag, '')
85-
.trim();
86-
87-
const script = parseScript(scriptSource);
88-
const module = parseScript(moduleSource);
89-
const css = parseCss(cssSource);
90-
const template = parseHtml(templateSource);
91-
92-
const generateCode: SvelteGenerator = (code) => {
93-
const ms = new MagicString(source);
94-
// TODO: this is imperfect and needs adjustments
95-
if (code.script !== undefined) {
96-
if (scriptSource.length === 0) {
97-
const ts = options?.typescript ? ' lang="ts"' : '';
98-
const indented = code.script.split('\n').join('\n\t');
99-
const script = `<script${ts}>\n\t${indented}\n</script>\n\n`;
100-
ms.prepend(script);
101-
} else {
102-
const { start, end } = locations(source, scriptSource);
103-
const formatted = indent(code.script, ms.getIndentString());
104-
ms.update(start, end, formatted);
105-
}
106-
}
107-
if (code.module !== undefined) {
108-
if (moduleSource.length === 0) {
109-
const ts = options?.typescript ? ' lang="ts"' : '';
110-
const indented = code.module.split('\n').join('\n\t');
111-
// TODO: make a svelte 5 variant
112-
const module = `<script${ts} context="module">\n\t${indented}\n</script>\n\n`;
113-
ms.prepend(module);
114-
} else {
115-
const { start, end } = locations(source, moduleSource);
116-
const formatted = indent(code.module, ms.getIndentString());
117-
ms.update(start, end, formatted);
118-
}
119-
}
120-
if (code.css !== undefined) {
121-
if (cssSource.length === 0) {
122-
const indented = code.css.split('\n').join('\n\t');
123-
const style = `\n<style>\n\t${indented}\n</style>\n`;
124-
ms.append(style);
125-
} else {
126-
const { start, end } = locations(source, cssSource);
127-
const formatted = indent(code.css, ms.getIndentString());
128-
ms.update(start, end, formatted);
129-
}
130-
}
131-
if (code.template !== undefined) {
132-
if (templateSource.length === 0) {
133-
ms.appendLeft(0, code.template);
134-
} else {
135-
const { start, end } = locations(source, templateSource);
136-
ms.update(start, end, code.template);
137-
}
138-
}
139-
return ms.toString();
140-
};
51+
export function parseSvelte(source: string): { ast: utils.SvelteAst.Root } & ParseBase {
52+
const ast = utils.parseSvelte(source);
53+
const generateCode = () => utils.serializeSvelte(ast);
14154

14255
return {
143-
script: { ...script, source: scriptSource },
144-
module: { ...module, source: moduleSource },
145-
css: { ...css, source: cssSource },
146-
template: { ...template, source: templateSource },
56+
ast,
57+
source,
14758
generateCode
14859
};
14960
}
150-
151-
function locations(source: string, search: string): { start: number; end: number } {
152-
const start = source.indexOf(search);
153-
const end = start + search.length;
154-
return { start, end };
155-
}
156-
157-
function indent(content: string, indent: string): string {
158-
const indented = indent + content.split('\n').join(`\n${indent}`);
159-
return `\n${indented}\n`;
160-
}
161-
162-
// sourced from Svelte: https://github.com/sveltejs/svelte/blob/0d3d5a2a85c0f9eccb2c8dbbecc0532ec918b157/packages/svelte/src/compiler/preprocess/index.js#L253-L256
163-
const regexScriptTags =
164-
/<!--[^]*?-->|<script((?:\s+[^=>'"/\s]+=(?:"[^"]*"|'[^']*'|[^>\s]+)|\s+[^=>'"/\s]+)*\s*)(?:\/>|>([\S\s]*?)<\/script>)/;
165-
const regexStyleTags =
166-
/<!--[^]*?-->|<style((?:\s+[^=>'"/\s]+=(?:"[^"]*"|'[^']*'|[^>\s]+)|\s+[^=>'"/\s]+)*\s*)(?:\/>|>([\S\s]*?)<\/style>)/;
167-
168-
type Script = { tag: string; attrs: string; src: string };
169-
function extractScripts(source: string): Script[] {
170-
const scripts = [];
171-
const [tag = '', attrs = '', src = ''] = regexScriptTags.exec(source) ?? [];
172-
if (tag) {
173-
const stripped = source.replace(tag, '');
174-
scripts.push({ tag, attrs, src }, ...extractScripts(stripped));
175-
return scripts;
176-
}
177-
178-
return [];
179-
}
180-
181-
function extractStyle(source: string) {
182-
const [styleTag = '', attributes = '', cssSource = ''] = regexStyleTags.exec(source) ?? [];
183-
return { styleTag, attributes, cssSource };
184-
}

pnpm-lock.yaml

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)