diff --git a/community-addon-template/src/index.js b/community-addon-template/src/index.js
index 61dc312ea..57d34e1d1 100644
--- a/community-addon-template/src/index.js
+++ b/community-addon-template/src/index.js
@@ -1,5 +1,6 @@
import { defineAddon, defineAddonOptions } from '@sveltejs/cli-core';
import { imports } from '@sveltejs/cli-core/js';
+import * as svelte from '@sveltejs/cli-core/svelte';
import { parseSvelte } from '@sveltejs/cli-core/parsers';
export const options = defineAddonOptions()
@@ -26,9 +27,10 @@ export default defineAddon({
sv.file('src/DemoComponent.svelte', (content) => {
if (!options.demo) return content;
- const { script, generateCode } = parseSvelte(content, { typescript });
- imports.addDefault(script.ast, { from: '../addon-template-demo.txt?raw', as: 'demo' });
- return generateCode({ script: script.generateCode(), template: '{demo}' });
+ const { ast, generateCode } = parseSvelte(content);
+ const scriptAst = svelte.ensureScript(ast, { langTs: typescript });
+ imports.addDefault(scriptAst, { from: '../addon-template-demo.txt?raw', as: 'demo' });
+ return generateCode();
});
}
});
diff --git a/packages/addons/_tests/mdsvex/test.ts b/packages/addons/_tests/mdsvex/test.ts
index 07c7a0ecd..ebf622898 100644
--- a/packages/addons/_tests/mdsvex/test.ts
+++ b/packages/addons/_tests/mdsvex/test.ts
@@ -3,7 +3,7 @@ import path from 'node:path';
import { expect } from '@playwright/test';
import { parseSvelte } from '@sveltejs/cli-core/parsers';
import { imports } from '@sveltejs/cli-core/js';
-import * as html from '@sveltejs/cli-core/html';
+import * as svelte from '@sveltejs/cli-core/svelte';
import { setupTest } from '../_setup/suite.ts';
import { svxFile } from './fixtures.ts';
import mdsvex from '../../mdsvex/index.ts';
@@ -40,19 +40,32 @@ function addFixture(cwd: string, variant: string) {
}
const src = fs.readFileSync(page, 'utf8');
- const { script, template, generateCode } = parseSvelte(src);
- imports.addDefault(script.ast, { from: './Demo.svx', as: 'Demo' });
+ const { ast, generateCode } = parseSvelte(src);
+ const scriptAst = svelte.ensureScript(ast);
+ imports.addDefault(scriptAst, { from: './Demo.svx', as: 'Demo' });
- const div = html.createDiv({ class: 'mdsvex' });
- html.appendElement(template.ast.childNodes, div);
- const mdsvexNode = html.createElement('Demo');
- html.appendElement(div.childNodes, mdsvexNode);
-
- const content = generateCode({
- script: script.generateCode(),
- template: template.generateCode()
+ ast.fragment.nodes.push({
+ type: 'RegularElement',
+ name: 'div',
+ attributes: [
+ {
+ type: 'Attribute',
+ name: 'class',
+ value: [{ type: 'Text', data: 'mdsvex', raw: 'mdsvex', start: 0, end: 0 }],
+ start: 0,
+ end: 0
+ }
+ ],
+ fragment: {
+ type: 'Fragment',
+ nodes: svelte.toFragment(' ')
+ },
+ start: 0,
+ end: 0
});
+ const content = generateCode();
+
fs.writeFileSync(page, content, 'utf8');
fs.writeFileSync(svx, svxFile, 'utf8');
}
diff --git a/packages/addons/common.ts b/packages/addons/common.ts
index 2b318a62c..f7f73c868 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 { toFragment, type SvelteAst, ensureScript } from '@sveltejs/cli-core/svelte';
import { parseScript, parseSvelte } from '@sveltejs/cli-core/parsers';
import process from 'node:process';
@@ -63,22 +64,34 @@ export function addEslintConfigPrettier(content: string): string {
return generateCode();
}
-export function addToDemoPage(existingContent: string, path: string, typescript: boolean): string {
- const { script, template, generateCode } = parseSvelte(existingContent, { typescript });
-
- for (const node of template.ast.childNodes) {
- // we use includes as it could be "/demo/${path}" or "resolve("demo/${path}")" or "resolve('demo/${path}')"
- if (node.type === 'tag' && node.attribs['href'].includes(`/demo/${path}`)) {
- return existingContent;
+export function addToDemoPage(existingContent: string, path: string, langTs: boolean): string {
+ const { ast, generateCode } = parseSvelte(existingContent);
+
+ 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.some(
+ // we use includes as it could be "/demo/${path}" or "resolve("demo/${path}")" or "resolve('demo/${path}')"
+ (x) => x.type === 'Text' && x.data.includes(`/demo/${path}`)
+ );
+ if (hasDemo) {
+ return existingContent;
+ }
}
}
- imports.addNamed(script.ast, { imports: ['resolve'], from: '$app/paths' });
+ imports.addNamed(ensureScript(ast, { langTs }), { imports: ['resolve'], from: '$app/paths' });
+
+ ast.fragment.nodes.unshift(...toFragment(`${path} `));
+ ast.fragment.nodes.unshift();
- return generateCode({
- script: script.generateCode(),
- template: `${path} \n${template.source}`
- });
+ return generateCode();
}
/**
diff --git a/packages/addons/paraglide/index.ts b/packages/addons/paraglide/index.ts
index b8d4d2de0..81b73dc62 100644
--- a/packages/addons/paraglide/index.ts
+++ b/packages/addons/paraglide/index.ts
@@ -1,7 +1,7 @@
-import MagicString from 'magic-string';
import { colors, defineAddon, defineAddonOptions, log } from '@sveltejs/cli-core';
import { common, imports, variables, exports, kit as kitJs, vite } from '@sveltejs/cli-core/js';
import * as html from '@sveltejs/cli-core/html';
+import * as svelte from '@sveltejs/cli-core/svelte';
import { parseHtml, parseJson, parseScript, parseSvelte } from '@sveltejs/cli-core/parsers';
import { addToDemoPage } from '../common.ts';
@@ -183,35 +183,34 @@ export default defineAddon({
// add usage example
sv.file(`${kit.routesDirectory}/demo/paraglide/+page.svelte`, (content) => {
- const { script, template, generateCode } = parseSvelte(content, { typescript });
- imports.addNamed(script.ast, { from: '$lib/paraglide/messages.js', imports: ['m'] });
- imports.addNamed(script.ast, { from: '$app/navigation', imports: ['goto'] });
- imports.addNamed(script.ast, { from: '$app/state', imports: ['page'] });
- imports.addNamed(script.ast, { from: '$lib/paraglide/runtime', imports: ['setLocale'] });
+ const { ast, generateCode } = parseSvelte(content);
+ const scriptAst = svelte.ensureScript(ast, { langTs: typescript });
- const scriptCode = new MagicString(script.generateCode());
-
- const templateCode = new MagicString(template.source);
+ imports.addNamed(scriptAst, { imports: { m: 'm' }, from: '$lib/paraglide/messages.js' });
+ imports.addNamed(scriptAst, {
+ imports: {
+ setLocale: 'setLocale'
+ },
+ from: '$lib/paraglide/runtime'
+ });
// 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}
`;
+
+ templateCode +=
+ 'If you use VSCode, install the Sherlock i18n extension for a better i18n experience.
';
- return generateCode({ script: scriptCode.toString(), template: templateCode.toString() });
+ ast.fragment.nodes.push(...svelte.toFragment(templateCode));
+
+ return generateCode();
});
}
diff --git a/packages/addons/tailwindcss/index.ts b/packages/addons/tailwindcss/index.ts
index 8c56eb72e..3d3540bbc 100644
--- a/packages/addons/tailwindcss/index.ts
+++ b/packages/addons/tailwindcss/index.ts
@@ -1,7 +1,7 @@
import { defineAddon, defineAddonOptions } from '@sveltejs/cli-core';
import { imports, vite } from '@sveltejs/cli-core/js';
+import * as svelte from '@sveltejs/cli-core/svelte';
import { parseCss, parseJson, parseScript, parseSvelte } from '@sveltejs/cli-core/parsers';
-import { addSlot } from '@sveltejs/cli-core/html';
const plugins = [
{
@@ -32,7 +32,7 @@ export default defineAddon({
shortDescription: 'css framework',
homepage: 'https://tailwindcss.com',
options,
- run: ({ sv, options, files, typescript, kit, dependencyVersion }) => {
+ run: ({ sv, options, files, kit, dependencyVersion, typescript }) => {
const prettierInstalled = Boolean(dependencyVersion('prettier'));
sv.devDependency('tailwindcss', '^4.1.17');
@@ -97,30 +97,28 @@ export default defineAddon({
const appSvelte = 'src/App.svelte';
const stylesheetRelative = files.getRelative({ from: appSvelte, to: files.stylesheet });
sv.file(appSvelte, (content) => {
- const { script, generateCode } = parseSvelte(content, { typescript });
- imports.addEmpty(script.ast, { from: stylesheetRelative });
- return generateCode({ script: script.generateCode() });
+ const { ast, generateCode } = parseSvelte(content);
+ const scriptAst = svelte.ensureScript(ast, { langTs: typescript });
+ imports.addEmpty(scriptAst, { from: stylesheetRelative });
+ return generateCode();
});
} else {
const layoutSvelte = `${kit?.routesDirectory}/+layout.svelte`;
const stylesheetRelative = files.getRelative({ from: layoutSvelte, to: files.stylesheet });
sv.file(layoutSvelte, (content) => {
- const { script, template, generateCode } = parseSvelte(content, { typescript });
- imports.addEmpty(script.ast, { from: stylesheetRelative });
+ const { ast, generateCode } = parseSvelte(content);
+ const scriptAst = svelte.ensureScript(ast, { langTs: typescript });
+ imports.addEmpty(scriptAst, { from: stylesheetRelative });
if (content.length === 0) {
const svelteVersion = dependencyVersion('svelte');
if (!svelteVersion) throw new Error('Failed to determine svelte version');
- addSlot(script.ast, {
- htmlAst: template.ast,
+ svelte.addSlot(ast, {
svelteVersion
});
}
- return generateCode({
- script: script.generateCode(),
- template: content.length === 0 ? template.generateCode() : undefined
- });
+ return generateCode();
});
}
diff --git a/packages/cli/commands/create.ts b/packages/cli/commands/create.ts
index 35addd565..616956b1d 100644
--- a/packages/cli/commands/create.ts
+++ b/packages/cli/commands/create.ts
@@ -255,11 +255,7 @@ async function createProject(cwd: ProjectPath, options: Options) {
});
if (options.fromPlayground) {
- await createProjectFromPlayground(
- options.fromPlayground,
- projectPath,
- language === 'typescript'
- );
+ await createProjectFromPlayground(options.fromPlayground, projectPath);
}
p.log.success('Project created');
@@ -322,11 +318,7 @@ async function createProject(cwd: ProjectPath, options: Options) {
return { directory: projectPath, addOnNextSteps, packageManager };
}
-async function createProjectFromPlayground(
- url: string,
- cwd: string,
- typescript: boolean
-): Promise {
+async function createProjectFromPlayground(url: string, cwd: string): Promise {
const urlData = parsePlaygroundUrl(url);
const playground = await downloadPlaygroundData(urlData);
@@ -334,7 +326,7 @@ async function createProjectFromPlayground(
const dependencies = detectPlaygroundDependencies(playground.files);
const installDependencies = await confirmExternalDependencies(Array.from(dependencies.keys()));
- setupPlaygroundProject(url, playground, cwd, installDependencies, typescript);
+ setupPlaygroundProject(url, playground, cwd, installDependencies);
}
async function confirmExternalDependencies(dependencies: string[]): Promise {
diff --git a/packages/cli/tests/snapshots/create-with-all-addons/src/routes/+layout.svelte b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/+layout.svelte
index 8b9bd05ca..0d8eb0307 100644
--- a/packages/cli/tests/snapshots/create-with-all-addons/src/routes/+layout.svelte
+++ b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/+layout.svelte
@@ -1,12 +1,9 @@
-
-
-
-
+
{@render children()}
diff --git a/packages/cli/tests/snapshots/create-with-all-addons/src/routes/demo/paraglide/+page.svelte b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/demo/paraglide/+page.svelte
index 04d3480cc..671434223 100644
--- a/packages/cli/tests/snapshots/create-with-all-addons/src/routes/demo/paraglide/+page.svelte
+++ b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/demo/paraglide/+page.svelte
@@ -1,16 +1,20 @@
-
-
{m.hello_world({ name: 'SvelteKit User' })}
setLocale('en')}>en
setLocale('es')}>es
-
-If you use VSCode, install the Sherlock i18n extension for a better i18n experience.
+
+
+ If you use VSCode, install the
+
+ Sherlock i18n extension
+
+ for a better i18n experience.
diff --git a/packages/core/package.json b/packages/core/package.json
index 7019a1447..4e08e1088 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -22,6 +22,7 @@
"./css": "./tooling/css/index.ts",
"./html": "./tooling/html/index.ts",
"./js": "./tooling/js/index.ts",
+ "./svelte": "./tooling/svelte/index.ts",
"./parsers": "./tooling/parsers.ts"
},
"devDependencies": {
@@ -40,6 +41,7 @@
"picocolors": "^1.1.1",
"postcss": "^8.5.6",
"silver-fleece": "^1.2.1",
+ "svelte": "^5.45.0",
"yaml": "^2.8.1",
"zimmerframe": "^1.1.4"
},
diff --git a/packages/core/tests/js/object/property-node/input.ts b/packages/core/tests/js/object/property-node/input.ts
index 4b075890b..537cd089c 100644
--- a/packages/core/tests/js/object/property-node/input.ts
+++ b/packages/core/tests/js/object/property-node/input.ts
@@ -1,4 +1,4 @@
const test = {
- /** a comment */
+ // prettier-ignore
foo: 1
};
diff --git a/packages/core/tests/js/object/property-node/output.ts b/packages/core/tests/js/object/property-node/output.ts
index 3719acb91..c163865df 100644
--- a/packages/core/tests/js/object/property-node/output.ts
+++ b/packages/core/tests/js/object/property-node/output.ts
@@ -1,6 +1,6 @@
const test = {
- /** a comment */
+ /*a comment updated*/ // prettier-ignore
foo: 1,
- james: '007'
+ /*aka: bond, james bond*/ james: '007'
};
diff --git a/packages/core/tests/js/object/property-node/run.ts b/packages/core/tests/js/object/property-node/run.ts
index 9db2672ea..18372acb4 100644
--- a/packages/core/tests/js/object/property-node/run.ts
+++ b/packages/core/tests/js/object/property-node/run.ts
@@ -1,18 +1,18 @@
-import { object, common, type AstTypes } from '@sveltejs/cli-core/js';
+import { object, common, type AstTypes, type Comments } from '@sveltejs/cli-core/js';
import { getTestObjectExpression } from '../objectTestHelper.ts';
-export function run(ast: AstTypes.Program): void {
+export function run(ast: AstTypes.Program, comments: Comments): void {
const obj = getTestObjectExpression(ast);
const p1 = object.propertyNode(obj, {
name: 'foo',
fallback: object.create({})
});
- p1.leadingComments = [{ type: 'Block', value: 'a comment updated' }];
+ comments.add(p1, { type: 'Block', value: 'a comment updated' });
const p2 = object.propertyNode(obj, {
name: 'james',
fallback: common.createLiteral('007')
});
- p2.leadingComments = [{ type: 'Block', value: 'aka: bond, james bond' }];
+ comments.add(p2, { type: 'Block', value: 'aka: bond, james bond' });
}
diff --git a/packages/core/tests/svelte/common/ensure-script-ts/input.svelte b/packages/core/tests/svelte/common/ensure-script-ts/input.svelte
new file mode 100644
index 000000000..175b82b52
--- /dev/null
+++ b/packages/core/tests/svelte/common/ensure-script-ts/input.svelte
@@ -0,0 +1 @@
+Nothing to see here
diff --git a/packages/core/tests/svelte/common/ensure-script-ts/output.svelte b/packages/core/tests/svelte/common/ensure-script-ts/output.svelte
new file mode 100644
index 000000000..ff42b6951
--- /dev/null
+++ b/packages/core/tests/svelte/common/ensure-script-ts/output.svelte
@@ -0,0 +1,3 @@
+
+
+Nothing to see here
diff --git a/packages/core/tests/svelte/common/ensure-script-ts/run.ts b/packages/core/tests/svelte/common/ensure-script-ts/run.ts
new file mode 100644
index 000000000..2895bd1cd
--- /dev/null
+++ b/packages/core/tests/svelte/common/ensure-script-ts/run.ts
@@ -0,0 +1,5 @@
+import { type SvelteAst, ensureScript } from '@sveltejs/cli-core/svelte';
+
+export function run(ast: SvelteAst.Root): void {
+ ensureScript(ast, { langTs: true });
+}
diff --git a/packages/core/tests/svelte/common/ensure-script/input.svelte b/packages/core/tests/svelte/common/ensure-script/input.svelte
new file mode 100644
index 000000000..f7cb47851
--- /dev/null
+++ b/packages/core/tests/svelte/common/ensure-script/input.svelte
@@ -0,0 +1,3 @@
+
+
This is a Svelte component without script block
+
diff --git a/packages/core/tests/svelte/common/ensure-script/output.svelte b/packages/core/tests/svelte/common/ensure-script/output.svelte
new file mode 100644
index 000000000..c0e89bbf3
--- /dev/null
+++ b/packages/core/tests/svelte/common/ensure-script/output.svelte
@@ -0,0 +1,5 @@
+
+
+
+
This is a Svelte component without script block
+
diff --git a/packages/core/tests/svelte/common/ensure-script/run.ts b/packages/core/tests/svelte/common/ensure-script/run.ts
new file mode 100644
index 000000000..27992909c
--- /dev/null
+++ b/packages/core/tests/svelte/common/ensure-script/run.ts
@@ -0,0 +1,5 @@
+import { type SvelteAst, ensureScript } from '@sveltejs/cli-core/svelte';
+
+export function run(ast: SvelteAst.Root): void {
+ ensureScript(ast);
+}
diff --git a/packages/core/tests/svelte/common/keep-script-ts/input.svelte b/packages/core/tests/svelte/common/keep-script-ts/input.svelte
new file mode 100644
index 000000000..3382f954b
--- /dev/null
+++ b/packages/core/tests/svelte/common/keep-script-ts/input.svelte
@@ -0,0 +1,3 @@
+
+
+A script tag with ts lang attribute
diff --git a/packages/core/tests/svelte/common/keep-script-ts/output.svelte b/packages/core/tests/svelte/common/keep-script-ts/output.svelte
new file mode 100644
index 000000000..3382f954b
--- /dev/null
+++ b/packages/core/tests/svelte/common/keep-script-ts/output.svelte
@@ -0,0 +1,3 @@
+
+
+A script tag with ts lang attribute
diff --git a/packages/core/tests/svelte/common/keep-script-ts/run.ts b/packages/core/tests/svelte/common/keep-script-ts/run.ts
new file mode 100644
index 000000000..27992909c
--- /dev/null
+++ b/packages/core/tests/svelte/common/keep-script-ts/run.ts
@@ -0,0 +1,5 @@
+import { type SvelteAst, ensureScript } from '@sveltejs/cli-core/svelte';
+
+export function run(ast: SvelteAst.Root): void {
+ ensureScript(ast);
+}
diff --git a/packages/core/tests/svelte/common/slot-svelte-4/output.svelte b/packages/core/tests/svelte/common/slot-svelte-4/output.svelte
new file mode 100644
index 000000000..13e0e91ed
--- /dev/null
+++ b/packages/core/tests/svelte/common/slot-svelte-4/output.svelte
@@ -0,0 +1 @@
+
diff --git a/packages/core/tests/svelte/common/slot-svelte-4/run.ts b/packages/core/tests/svelte/common/slot-svelte-4/run.ts
new file mode 100644
index 000000000..11d06ce56
--- /dev/null
+++ b/packages/core/tests/svelte/common/slot-svelte-4/run.ts
@@ -0,0 +1,5 @@
+import { type SvelteAst, addSlot } from '@sveltejs/cli-core/svelte';
+
+export function run(ast: SvelteAst.Root): void {
+ addSlot(ast, { svelteVersion: '4.0.0' });
+}
diff --git a/packages/core/tests/svelte/common/slot-svelte-5/output.svelte b/packages/core/tests/svelte/common/slot-svelte-5/output.svelte
new file mode 100644
index 000000000..f82c4d3da
--- /dev/null
+++ b/packages/core/tests/svelte/common/slot-svelte-5/output.svelte
@@ -0,0 +1,5 @@
+
+
+{@render children()}
diff --git a/packages/core/tests/svelte/common/slot-svelte-5/run.ts b/packages/core/tests/svelte/common/slot-svelte-5/run.ts
new file mode 100644
index 000000000..5ee31421e
--- /dev/null
+++ b/packages/core/tests/svelte/common/slot-svelte-5/run.ts
@@ -0,0 +1,5 @@
+import { type SvelteAst, addSlot } from '@sveltejs/cli-core/svelte';
+
+export function run(ast: SvelteAst.Root): void {
+ addSlot(ast, { svelteVersion: '5.0.0' });
+}
diff --git a/packages/core/tests/svelte/common/to-fragment/input.svelte b/packages/core/tests/svelte/common/to-fragment/input.svelte
new file mode 100644
index 000000000..f4f500fb8
--- /dev/null
+++ b/packages/core/tests/svelte/common/to-fragment/input.svelte
@@ -0,0 +1,5 @@
+
+
+
This is a Svelte component.
+
+
diff --git a/packages/core/tests/svelte/common/to-fragment/output.svelte b/packages/core/tests/svelte/common/to-fragment/output.svelte
new file mode 100644
index 000000000..ed8025189
--- /dev/null
+++ b/packages/core/tests/svelte/common/to-fragment/output.svelte
@@ -0,0 +1,3 @@
+
+This is a Svelte component.
+Appended Fragment
diff --git a/packages/core/tests/svelte/common/to-fragment/run.ts b/packages/core/tests/svelte/common/to-fragment/run.ts
new file mode 100644
index 000000000..fad358c42
--- /dev/null
+++ b/packages/core/tests/svelte/common/to-fragment/run.ts
@@ -0,0 +1,5 @@
+import { type SvelteAst, toFragment } from '@sveltejs/cli-core/svelte';
+
+export function run(ast: SvelteAst.Root): void {
+ ast.fragment.nodes.push(...toFragment('Appended Fragment '));
+}
diff --git a/packages/core/tests/svelte/index.ts b/packages/core/tests/svelte/index.ts
new file mode 100644
index 000000000..08b5c238a
--- /dev/null
+++ b/packages/core/tests/svelte/index.ts
@@ -0,0 +1,38 @@
+import fs from 'node:fs';
+import { join, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { describe, expect, test } from 'vitest';
+import { parseSvelte, serializeSvelte } from '../../tooling/index.ts';
+
+const baseDir = resolve(fileURLToPath(import.meta.url), '..');
+const categoryDirectories = getDirectoryNames(baseDir);
+
+for (const categoryDirectory of categoryDirectories) {
+ describe(categoryDirectory, () => {
+ const testNames = getDirectoryNames(join(baseDir, categoryDirectory));
+ for (const testName of testNames) {
+ test(testName, async () => {
+ const testDirectoryPath = join(baseDir, categoryDirectory, testName);
+
+ const inputFilePath = join(testDirectoryPath, 'input.svelte');
+ const input = fs.existsSync(inputFilePath) ? fs.readFileSync(inputFilePath, 'utf8') : '';
+ const ast = parseSvelte(input);
+
+ // dynamic imports always need to provide the path inline for static analysis
+ const module = await import(`./${categoryDirectory}/${testName}/run.ts`);
+ module.run(ast);
+
+ let output = serializeSvelte(ast);
+ if (!output.endsWith('\n')) output += '\n';
+ await expect(output).toMatchFileSnapshot(`${testDirectoryPath}/output.svelte`);
+ });
+ }
+ });
+}
+
+function getDirectoryNames(dir: string) {
+ return fs
+ .readdirSync(dir, { withFileTypes: true })
+ .filter((dirent) => dirent.isDirectory())
+ .map((dirent) => dirent.name);
+}
diff --git a/packages/core/tests/utils.ts b/packages/core/tests/utils.ts
index 585c6fcaa..476b78610 100644
--- a/packages/core/tests/utils.ts
+++ b/packages/core/tests/utils.ts
@@ -290,86 +290,3 @@ describe('yaml', () => {
`);
});
});
-
-// TODO: fix https://github.com/rolldown/tsdown/issues/575 to remove the `skip`
-test.skip('tsdown escapes script tags in bundled source code', async () => {
- const { execSync } = await import('node:child_process');
- const fs = await import('node:fs');
- const path = await import('node:path');
-
- const testDir = path.join('../..', '.test-output', `tsdown-test`);
- fs.rmSync(testDir, { recursive: true, force: true });
- fs.mkdirSync(testDir, { recursive: true });
-
- // Create a test file that uses dedent with script tags
- const testFileLiteral = path.join(testDir, 'testLiteral.ts');
- fs.writeFileSync(
- testFileLiteral,
- `import dedent from 'dedent';
-
-export const result = dedent\`
-
-\`;
-`
- );
-
- const testFileFunction = path.join(testDir, 'testFunction.ts');
- fs.writeFileSync(
- testFileFunction,
- `import dedent from 'dedent';
-
-export const result = dedent(\`
-
-\`);
-`
- );
-
- // Create a tsdown config
- const configFile = path.join(testDir, 'tsdown.config.ts');
- fs.writeFileSync(
- configFile,
- `import { defineConfig } from 'tsdown';
-
-export default defineConfig({
- entry: ['testLiteral.ts', 'testFunction.ts'],
- format: ['esm'],
- outDir: 'dist',
-});
-`
- );
-
- // Create package.json with tsdown
- const pkgJson = {
- name: 'test',
- type: 'module',
- devDependencies: {
- tsdown: '^0.15.2',
- dedent: '^1.6.0'
- }
- };
- fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify(pkgJson, null, 2));
-
- // Install dependencies and build
- execSync('npm install', { cwd: testDir, stdio: 'pipe' });
- execSync('npx tsdown', { cwd: testDir, stdio: 'pipe' });
-
- // Read the bundled output
- const bundledFileLiteral = path.join(testDir, 'dist', 'testLiteral.js');
- const bundledFileFunction = path.join(testDir, 'dist', 'testFunction.js');
- const bundledCodeLiteral = fs.readFileSync(bundledFileLiteral, 'utf-8');
- const bundledCodeFunction = fs.readFileSync(bundledFileFunction, 'utf-8');
-
- // Check if the bundled code contains escaped script tags
- const hasEscapedScriptTagLiteral = bundledCodeLiteral.includes('<\\/script>');
- const hasEscapedScriptTagFunction = bundledCodeFunction.includes('<\\/script>');
-
- // This test demonstrates the issue: tsdown escapes in the bundled source
- // Expected: Bundled code should NOT contain escaped script tags
- // Actual: Bundled code contains <\/script> when using dedent`...` syntax
- expect(hasEscapedScriptTagLiteral).toBe(false);
- expect(hasEscapedScriptTagFunction).toBe(false);
-}, 30000); // 30s timeout for npm install and build
diff --git a/packages/core/tooling/html/index.ts b/packages/core/tooling/html/index.ts
index 60b2ae078..b79c5074c 100644
--- a/packages/core/tooling/html/index.ts
+++ b/packages/core/tooling/html/index.ts
@@ -1,12 +1,10 @@
import {
- type AstTypes,
type HtmlChildNode,
type HtmlDocument,
HtmlElement,
HtmlElementType,
parseHtml
} from '../index.ts';
-import { appendFromString } from '../js/common.ts';
export { HtmlElement, HtmlElementType };
export type { HtmlDocument };
@@ -38,23 +36,3 @@ export function addFromRawHtml(childNodes: HtmlChildNode[], html: string): void
childNodes.push(childNode);
}
}
-
-export function addSlot(
- jsAst: AstTypes.Program,
- options: { htmlAst: HtmlDocument; svelteVersion: string }
-): void {
- const slotSyntax =
- options.svelteVersion &&
- (options.svelteVersion.startsWith('4') || options.svelteVersion.startsWith('3'));
-
- if (slotSyntax) {
- const slot = createElement('slot');
- appendElement(options.htmlAst.childNodes, slot);
- return;
- }
-
- appendFromString(jsAst, {
- code: 'let { children } = $props();'
- });
- addFromRawHtml(options.htmlAst.childNodes, '{@render children()}');
-}
diff --git a/packages/core/tooling/index.ts b/packages/core/tooling/index.ts
index c9fcc7074..9fb28cfa3 100644
--- a/packages/core/tooling/index.ts
+++ b/packages/core/tooling/index.ts
@@ -17,6 +17,7 @@ import { print as esrapPrint } from 'esrap';
import ts from 'esrap/languages/ts';
import * as acorn from 'acorn';
import { tsPlugin } from '@sveltejs/acorn-typescript';
+import { parse as svelteParse, type AST as SvelteAst, print as sveltePrint } from 'svelte/compiler';
import * as yaml from 'yaml';
import type { BaseNode } from 'estree';
@@ -40,6 +41,7 @@ export {
export type {
// html
ChildNode as HtmlChildNode,
+ SvelteAst,
// js
TsEstree as AstTypes,
@@ -291,3 +293,11 @@ interface CommentsInternal {
function transformToInternal(comments: Comments | undefined): CommentsInternal {
return (comments ?? new Comments()) as unknown as CommentsInternal;
}
+
+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 94b913fed..24fd027a3 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;
@@ -48,136 +47,13 @@ export function parseYaml(
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 =
- /|