diff --git a/package.json b/package.json
index 391cd4f3..059e4299 100644
--- a/package.json
+++ b/package.json
@@ -46,4 +46,4 @@
"esbuild"
]
}
-}
+}
\ No newline at end of file
diff --git a/packages/addons/common.ts b/packages/addons/common.ts
index b03d9d5e..2e068420 100644
--- a/packages/addons/common.ts
+++ b/packages/addons/common.ts
@@ -1,4 +1,5 @@
import { imports, exports, common } from '@sveltejs/cli-core/js';
+import { toSvelteFragment, type SvelteAst } from '@sveltejs/cli-core/html';
import { parseScript, parseSvelte } from '@sveltejs/cli-core/parsers';
import process from 'node:process';
@@ -64,17 +65,29 @@ export function addEslintConfigPrettier(content: string): string {
}
export function addToDemoPage(content: string, path: string): string {
- const { template, generateCode } = parseSvelte(content);
-
- for (const node of template.ast.childNodes) {
- if (node.type === 'tag' && node.attribs['href'] === `/demo/${path}`) {
- return content;
+ const { ast, generateCode } = parseSvelte(content);
+
+ for (const node of ast.fragment.nodes) {
+ if (node.type === 'RegularElement') {
+ const hrefAttribute = node.attributes.find(
+ (x) => x.type === 'Attribute' && x.name === 'href'
+ ) as SvelteAst.Attribute;
+ if (!hrefAttribute || !hrefAttribute.value) continue;
+
+ if (!Array.isArray(hrefAttribute.value)) continue;
+
+ const hasDemo = hrefAttribute.value.find(
+ (x) => x.type === 'Text' && x.data === `/demo/${path}`
+ );
+ if (hasDemo) {
+ return content;
+ }
}
}
- const newLine = template.source ? '\n' : '';
- const src = template.source + `${newLine}${path} `;
- return generateCode({ template: src });
+ ast.fragment.nodes.push(...toSvelteFragment(`${path} `));
+
+ return generateCode();
}
/**
diff --git a/packages/addons/paraglide/index.ts b/packages/addons/paraglide/index.ts
index 455ee24b..644b7bd4 100644
--- a/packages/addons/paraglide/index.ts
+++ b/packages/addons/paraglide/index.ts
@@ -1,4 +1,3 @@
-import MagicString from 'magic-string';
import { colors, defineAddon, defineAddonOptions, log } from '@sveltejs/cli-core';
import {
array,
@@ -187,38 +186,44 @@ export default defineAddon({
// add usage example
sv.file(`${kit.routesDirectory}/demo/paraglide/+page.svelte`, (content) => {
- const { script, template, generateCode } = parseSvelte(content, { typescript });
+ console.log(content);
+ const { ast, generateCode } = parseSvelte(content);
+
+ let scriptAst = ast.instance?.content;
+ if (!scriptAst) {
+ scriptAst = parseScript('').ast;
+ ast.instance = {
+ type: 'Script',
+ start: 0,
+ end: 0,
+ context: 'default',
+ attributes: [],
+ content: scriptAst
+ };
+ }
- imports.addNamed(script.ast, '$lib/paraglide/messages.js', { m: 'm' });
- imports.addNamed(script.ast, '$app/navigation', { goto: 'goto' });
- imports.addNamed(script.ast, '$app/state', { page: 'page' });
- imports.addNamed(script.ast, '$lib/paraglide/runtime', {
+ imports.addNamed(scriptAst, '$lib/paraglide/messages.js', { m: 'm' });
+ imports.addNamed(scriptAst, '$lib/paraglide/runtime', {
setLocale: 'setLocale'
});
- const scriptCode = new MagicString(script.generateCode());
-
- const templateCode = new MagicString(template.source);
-
// add localized message
- templateCode.append("\n\n
{m.hello_world({ name: 'SvelteKit User' })} \n");
+ let templateCode = "{m.hello_world({ name: 'SvelteKit User' })} ";
// add links to other localized pages, the first one is the default
// language, thus it does not require any localized route
const { validLanguageTags } = parseLanguageTagInput(options.languageTags);
const links = validLanguageTags
- .map(
- (x) =>
- `${templateCode.getIndentString()} setLocale('${x}')}>${x} `
- )
- .join('\n');
- templateCode.append(`\n${links}\n
`);
-
- templateCode.append(
- '\nIf you use VSCode, install the Sherlock i18n extension for a better i18n experience.\n
'
- );
+ .map((x) => ` setLocale('${x}')}>${x} `)
+ .join('');
+ templateCode += `${links}
`;
- return generateCode({ script: scriptCode.toString(), template: templateCode.toString() });
+ templateCode +=
+ 'If you use VSCode, install the Sherlock i18n extension for a better i18n experience.
';
+
+ ast.fragment.nodes.push(...html.toSvelteFragment(templateCode));
+
+ return generateCode();
});
}
diff --git a/packages/core/package.json b/packages/core/package.json
index 41232984..d4a0bab3 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -57,7 +57,9 @@
"picocolors": "^1.1.1",
"postcss": "^8.4.49",
"silver-fleece": "^1.2.1",
- "zimmerframe": "^1.1.2"
+ "zimmerframe": "^1.1.2",
+ "svelte": "^5.30.2",
+ "svelte-ast-print": "^1.0.1"
},
"keywords": [
"create",
diff --git a/packages/core/tooling/html/index.ts b/packages/core/tooling/html/index.ts
index 94dcd881..7339f840 100644
--- a/packages/core/tooling/html/index.ts
+++ b/packages/core/tooling/html/index.ts
@@ -1,3 +1,4 @@
+import type { AST as SvelteAst } from 'svelte/compiler';
import {
type AstTypes,
type HtmlChildNode,
@@ -7,9 +8,10 @@ import {
parseHtml
} from '../index.ts';
import { addFromString } from '../js/common.ts';
+import { parseSvelte } from '../parsers.ts';
export { HtmlElement, HtmlElementType };
-export type { HtmlDocument };
+export type { HtmlDocument, SvelteAst };
export function div(attributes: Record = {}): HtmlElement {
return element('div', attributes);
@@ -53,3 +55,9 @@ export function addSlot(
addFromString(jsAst, 'let { children } = $props();');
addFromRawHtml(htmlAst.childNodes, '{@render children()}');
}
+
+export function toSvelteFragment(content: string): SvelteAst.Fragment['nodes'] {
+ // TODO write test
+ const { ast } = parseSvelte(content);
+ return ast.fragment.nodes;
+}
diff --git a/packages/core/tooling/index.ts b/packages/core/tooling/index.ts
index 27e41cbe..fe5be511 100644
--- a/packages/core/tooling/index.ts
+++ b/packages/core/tooling/index.ts
@@ -16,6 +16,8 @@ import * as fleece from 'silver-fleece';
import { print as esrapPrint } from 'esrap';
import * as acorn from 'acorn';
import { tsPlugin } from '@sveltejs/acorn-typescript';
+import { parse as svelteParse, type AST as SvelteAst } from 'svelte/compiler';
+import { print as sveltePrint } from 'svelte-ast-print';
export {
// html
@@ -37,11 +39,12 @@ export {
export type {
// html
ChildNode as HtmlChildNode,
+ SvelteAst,
// js
TsEstree as AstTypes,
- //css
+ // css
CssChildNode
};
@@ -240,3 +243,11 @@ export function guessQuoteStyle(ast: TsEstree.Node): 'single' | 'double' | undef
return singleCount > doubleCount ? 'single' : 'double';
}
+
+export function parseSvelte(content: string): SvelteAst.Root {
+ return svelteParse(content, { modern: true });
+}
+
+export function serializeSvelte(ast: SvelteAst.Root): string {
+ return sveltePrint(ast).code;
+}
diff --git a/packages/core/tooling/parsers.ts b/packages/core/tooling/parsers.ts
index f7764d34..ceee301c 100644
--- a/packages/core/tooling/parsers.ts
+++ b/packages/core/tooling/parsers.ts
@@ -1,5 +1,4 @@
import * as utils from './index.ts';
-import MagicString from 'magic-string';
type ParseBase = {
source: string;
@@ -35,136 +34,13 @@ export function parseJson(source: string): { data: any } & ParseBase {
return { data, source, generateCode };
}
-type SvelteGenerator = (code: {
- script?: string;
- module?: string;
- css?: string;
- template?: string;
-}) => string;
-export function parseSvelte(
- source: string,
- options?: { typescript?: boolean }
-): {
- script: ReturnType;
- module: ReturnType;
- css: ReturnType;
- template: ReturnType;
- generateCode: SvelteGenerator;
-} {
- // `xTag` captures the whole tag block (ex: )
- // `xSource` is the contents within the tags
- const scripts = extractScripts(source);
- // instance block
- const { tag: scriptTag = '', src: scriptSource = '' } =
- scripts.find(({ attrs }) => !attrs.includes('module')) ?? {};
- // module block
- const { tag: moduleScriptTag = '', src: moduleSource = '' } =
- scripts.find(({ attrs }) => attrs.includes('module')) ?? {};
- // style block
- const { styleTag, cssSource } = extractStyle(source);
- // rest of the template
- // TODO: needs more testing
- const templateSource = source
- .replace(moduleScriptTag, '')
- .replace(scriptTag, '')
- .replace(styleTag, '')
- .trim();
-
- const script = parseScript(scriptSource);
- const module = parseScript(moduleSource);
- const css = parseCss(cssSource);
- const template = parseHtml(templateSource);
-
- const generateCode: SvelteGenerator = (code) => {
- const ms = new MagicString(source);
- // TODO: this is imperfect and needs adjustments
- if (code.script !== undefined) {
- if (scriptSource.length === 0) {
- const ts = options?.typescript ? ' lang="ts"' : '';
- const indented = code.script.split('\n').join('\n\t');
- const script = `\n\n`;
- ms.prepend(script);
- } else {
- const { start, end } = locations(source, scriptSource);
- const formatted = indent(code.script, ms.getIndentString());
- ms.update(start, end, formatted);
- }
- }
- if (code.module !== undefined) {
- if (moduleSource.length === 0) {
- const ts = options?.typescript ? ' lang="ts"' : '';
- const indented = code.module.split('\n').join('\n\t');
- // TODO: make a svelte 5 variant
- const module = `\n\n`;
- ms.prepend(module);
- } else {
- const { start, end } = locations(source, moduleSource);
- const formatted = indent(code.module, ms.getIndentString());
- ms.update(start, end, formatted);
- }
- }
- if (code.css !== undefined) {
- if (cssSource.length === 0) {
- const indented = code.css.split('\n').join('\n\t');
- const style = `\n\n`;
- ms.append(style);
- } else {
- const { start, end } = locations(source, cssSource);
- const formatted = indent(code.css, ms.getIndentString());
- ms.update(start, end, formatted);
- }
- }
- if (code.template !== undefined) {
- if (templateSource.length === 0) {
- ms.appendLeft(0, code.template);
- } else {
- const { start, end } = locations(source, templateSource);
- ms.update(start, end, code.template);
- }
- }
- return ms.toString();
- };
+export function parseSvelte(source: string): { ast: utils.SvelteAst.Root } & ParseBase {
+ const ast = utils.parseSvelte(source);
+ const generateCode = () => utils.serializeSvelte(ast);
return {
- script: { ...script, source: scriptSource },
- module: { ...module, source: moduleSource },
- css: { ...css, source: cssSource },
- template: { ...template, source: templateSource },
+ ast,
+ source,
generateCode
};
}
-
-function locations(source: string, search: string): { start: number; end: number } {
- const start = source.indexOf(search);
- const end = start + search.length;
- return { start, end };
-}
-
-function indent(content: string, indent: string): string {
- const indented = indent + content.split('\n').join(`\n${indent}`);
- return `\n${indented}\n`;
-}
-
-// sourced from Svelte: https://github.com/sveltejs/svelte/blob/0d3d5a2a85c0f9eccb2c8dbbecc0532ec918b157/packages/svelte/src/compiler/preprocess/index.js#L253-L256
-const regexScriptTags =
- /|