= [];
imports.addDefault(ast, { from: './svelte.config.js', as: 'svelteConfig' });
@@ -85,18 +85,20 @@ export default defineAddon({
if (rules.properties[0].type !== 'Property') {
throw new Error('rules.properties[0].type !== "Property"');
}
- rules.properties[0].key.leadingComments = [
+ additionalComments.set(rules.properties[0].key, [
{
type: 'Line',
value:
- ' typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.'
+ ' typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.',
+ position: 'leading'
},
{
type: 'Line',
value:
- ' see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors'
+ ' see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors',
+ position: 'leading'
}
- ];
+ ]);
const globalsConfig = object.create({
languageOptions: {
@@ -153,7 +155,9 @@ export default defineAddon({
// type annotate config
if (!typescript)
- common.addJsDocTypeComment(astNode, { type: "import('eslint').Linter.Config[]" });
+ common.addJsDocTypeComment(astNode, additionalComments, {
+ type: "import('eslint').Linter.Config[]"
+ });
if (typescript) imports.addDefault(ast, { from: 'typescript-eslint', as: 'ts' });
imports.addDefault(ast, { from: 'globals', as: 'globals' });
diff --git a/packages/addons/paraglide/index.ts b/packages/addons/paraglide/index.ts
index 02663e041..90298f38b 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 { common, imports, variables, exports, kit as kitJs, vite } from '@sveltejs/cli-core/js';
import * as html from '@sveltejs/cli-core/html';
@@ -183,35 +182,46 @@ 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 scriptCode = new MagicString(script.generateCode());
+ 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
+ };
+ }
- 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}
`;
- 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/addons/sveltekit-adapter/index.ts b/packages/addons/sveltekit-adapter/index.ts
index 7c12859b7..1650966f4 100644
--- a/packages/addons/sveltekit-adapter/index.ts
+++ b/packages/addons/sveltekit-adapter/index.ts
@@ -49,7 +49,7 @@ export default defineAddon({
sv.devDependency(adapter.package, adapter.version);
sv.file('svelte.config.js', (content) => {
- const { ast, generateCode } = parseScript(content);
+ const { ast, comments, generateCode } = parseScript(content);
// finds any existing adapter's import declaration
const importDecls = ast.body.filter((n) => n.type === 'ImportDeclaration');
@@ -86,8 +86,21 @@ export default defineAddon({
if (adapter.package !== '@sveltejs/adapter-auto') {
const fallback = object.create({});
const cfgKitValue = object.property(config, { name: 'kit', fallback });
- const cfgAdapter = object.propertyNode(cfgKitValue, { name: 'adapter', fallback });
- cfgAdapter.leadingComments = [];
+
+ // removes any existing adapter auto comments
+ const adapterAutoComments = comments.filter(
+ (c) =>
+ c.loc &&
+ cfgKitValue.loc &&
+ c.loc.start.line >= cfgKitValue.loc.start.line &&
+ c.loc.end.line <= cfgKitValue.loc.end.line
+ );
+ // modify the array in place
+ comments.splice(
+ 0,
+ comments.length,
+ ...comments.filter((c) => !adapterAutoComments.includes(c))
+ );
}
return generateCode();
diff --git a/packages/core/package.json b/packages/core/package.json
index 1138f0314..ddaeab0fa 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -34,12 +34,13 @@
"dom-serializer": "^2.0.0",
"domhandler": "^5.0.3",
"domutils": "^3.2.2",
- "esrap": "^1.4.9",
+ "esrap": "https://pkg.pr.new/sveltejs/esrap@af12b38",
"htmlparser2": "^9.1.0",
"magic-string": "^0.30.17",
"picocolors": "^1.1.1",
"postcss": "^8.5.6",
"silver-fleece": "^1.2.1",
+ "svelte": "https://pkg.pr.new/sveltejs/svelte@1377c40",
"yaml": "^2.8.1",
"zimmerframe": "^1.1.2"
},
diff --git a/packages/core/tests/js/common/jsdoc-comment/run.ts b/packages/core/tests/js/common/jsdoc-comment/run.ts
index c0b6c06f1..15d0369b9 100644
--- a/packages/core/tests/js/common/jsdoc-comment/run.ts
+++ b/packages/core/tests/js/common/jsdoc-comment/run.ts
@@ -1,9 +1,9 @@
-import { common, type AstTypes } from '@sveltejs/cli-core/js';
+import { common, type AdditionalCommentMap, type AstTypes } from '@sveltejs/cli-core/js';
-export function run(ast: AstTypes.Program): void {
+export function run(ast: AstTypes.Program, additionalComments: AdditionalCommentMap): void {
const functionDeclaration = ast.body[0] as AstTypes.FunctionDeclaration;
- common.addJsDocComment(functionDeclaration, {
+ common.addJsDocComment(functionDeclaration, additionalComments, {
params: { 'import("$lib/paraglide/runtime").AvailableLanguageTag': 'newLanguage' }
});
}
diff --git a/packages/core/tests/js/common/jsdoc-type-comment/output.ts b/packages/core/tests/js/common/jsdoc-type-comment/output.ts
index 1aa734391..e8a715681 100644
--- a/packages/core/tests/js/common/jsdoc-type-comment/output.ts
+++ b/packages/core/tests/js/common/jsdoc-type-comment/output.ts
@@ -1,2 +1 @@
-/** @type {number} */
-const foo = 42;
+/** @type {number} */ const foo = 42;
diff --git a/packages/core/tests/js/common/jsdoc-type-comment/run.ts b/packages/core/tests/js/common/jsdoc-type-comment/run.ts
index 9b5e5a065..5074afd58 100644
--- a/packages/core/tests/js/common/jsdoc-type-comment/run.ts
+++ b/packages/core/tests/js/common/jsdoc-type-comment/run.ts
@@ -1,13 +1,13 @@
-import { common, variables, type AstTypes } from '@sveltejs/cli-core/js';
+import { common, variables, type AdditionalCommentMap, type AstTypes } from '@sveltejs/cli-core/js';
-export function run(ast: AstTypes.Program): void {
+export function run(ast: AstTypes.Program, additionalComments: AdditionalCommentMap): void {
const declaration = variables.declaration(ast, {
kind: 'const',
name: 'foo',
value: { type: 'Literal', value: 42 }
});
- common.addJsDocTypeComment(declaration, {
+ common.addJsDocTypeComment(declaration, additionalComments, {
type: 'number'
});
diff --git a/packages/core/tests/js/index.ts b/packages/core/tests/js/index.ts
index c7f2021e6..bc32a5c83 100644
--- a/packages/core/tests/js/index.ts
+++ b/packages/core/tests/js/index.ts
@@ -16,13 +16,13 @@ for (const categoryDirectory of categoryDirectories) {
const inputFilePath = join(testDirectoryPath, 'input.ts');
const input = fs.existsSync(inputFilePath) ? fs.readFileSync(inputFilePath, 'utf8') : '';
- const ast = parseScript(input);
+ const { ast, comments, additionalComments } = parseScript(input);
// dynamic imports always need to provide the path inline for static analysis
const module = await import(`./${categoryDirectory}/${testName}/run.ts`);
- module.run(ast);
+ module.run(ast, additionalComments);
- let output = serializeScript(ast, input);
+ let output = serializeScript(ast, comments, input, additionalComments);
if (!output.endsWith('\n')) output += '\n';
await expect(output).toMatchFileSnapshot(`${testDirectoryPath}/output.ts`);
});
diff --git a/packages/core/tests/js/object/create/output.ts b/packages/core/tests/js/object/create/output.ts
index 28ad96e4f..df190f0f4 100644
--- a/packages/core/tests/js/object/create/output.ts
+++ b/packages/core/tests/js/object/create/output.ts
@@ -1,15 +1,9 @@
const empty = {};
const created = { foo: 1, bar: 'string' };
-// prettier-ignore
const created2 = {
foo: 1,
bar: 'string',
object: { foo: 'hello', nested: { bar: 'world' } },
- array: [
- 123,
- 'hello',
- { foo: 'bar', bool: true },
- [456, '789']
- ]
+ array: [123, 'hello', { foo: 'bar', bool: true }, [456, '789']]
};
diff --git a/packages/core/tests/js/object/create/run.ts b/packages/core/tests/js/object/create/run.ts
index 8bdd272f5..5b19937e0 100644
--- a/packages/core/tests/js/object/create/run.ts
+++ b/packages/core/tests/js/object/create/run.ts
@@ -37,6 +37,5 @@ export function run(ast: AstTypes.Program): void {
name: 'created2',
value: createdObject2
});
- createdVariable2.leadingComments = [{ type: 'Line', value: ' prettier-ignore' }];
ast.body.push(createdVariable2);
}
diff --git a/packages/core/tests/js/object/ensure-nested-property/output.ts b/packages/core/tests/js/object/ensure-nested-property/output.ts
index 08bd5fbf9..b3824d36a 100644
--- a/packages/core/tests/js/object/ensure-nested-property/output.ts
+++ b/packages/core/tests/js/object/ensure-nested-property/output.ts
@@ -1 +1,8 @@
-const test = { a: { /** a comment */ keep: 'you', b: { c: '007' } } };
+const test = {
+ a: {
+ /** a comment */
+ keep: 'you',
+
+ b: { c: '007' }
+ }
+};
diff --git a/packages/core/tests/js/object/override-property/output.ts b/packages/core/tests/js/object/override-property/output.ts
index fbdef76fe..987c50888 100644
--- a/packages/core/tests/js/object/override-property/output.ts
+++ b/packages/core/tests/js/object/override-property/output.ts
@@ -1 +1,7 @@
-const test = { /** a comment */ foo: 2, bar: 'string2', lorem: false };
+const test = {
+ /** a comment */
+ foo: 2,
+
+ bar: 'string2',
+ lorem: false
+};
diff --git a/packages/core/tests/js/object/property-node/output.ts b/packages/core/tests/js/object/property-node/output.ts
index 98da8dea4..3719acb91 100644
--- a/packages/core/tests/js/object/property-node/output.ts
+++ b/packages/core/tests/js/object/property-node/output.ts
@@ -1 +1,6 @@
-const test = { /*a comment updated*/ foo: 1, /*aka: bond, james bond*/ james: '007' };
+const test = {
+ /** a comment */
+ foo: 1,
+
+ james: '007'
+};
diff --git a/packages/core/tests/js/vite/add-plugin-mode/output.ts b/packages/core/tests/js/vite/add-plugin-mode/output.ts
index f112d5fcc..df6d4692a 100644
--- a/packages/core/tests/js/vite/add-plugin-mode/output.ts
+++ b/packages/core/tests/js/vite/add-plugin-mode/output.ts
@@ -7,8 +7,10 @@ import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
firstPlugin(),
+
// a default plugin
sveltekit(),
+
middlePlugin(),
lastPlugin()
]
diff --git a/packages/core/tests/js/vite/with-satisfies/output.ts b/packages/core/tests/js/vite/with-satisfies/output.ts
index 4d6ed36e0..9c43348e2 100644
--- a/packages/core/tests/js/vite/with-satisfies/output.ts
+++ b/packages/core/tests/js/vite/with-satisfies/output.ts
@@ -18,23 +18,24 @@ const config = defineConfig({
plugins: [
// all plugins
examples,
+
tailwindcss(),
sveltekit(),
kitRoutes(),
myPlugin()
],
+
resolve: { alias: { $lib, $routes, $scripts, $actions } },
- build: {
- sourcemap: true,
- target: 'esnext',
- cssMinify: 'lightningcss'
- },
+ build: { sourcemap: true, target: 'esnext', cssMinify: 'lightningcss' },
+
css: {
transformer: 'lightningcss',
+
lightningcss: {
targets: browserslistToTargets(browserslist('defaults, not ie 11'))
}
},
+
experimental: { enableNativePlugin: true }
}) satisfies UserConfig;
diff --git a/packages/core/tests/utils.ts b/packages/core/tests/utils.ts
index 01139fbc4..88e95f125 100644
--- a/packages/core/tests/utils.ts
+++ b/packages/core/tests/utils.ts
@@ -4,7 +4,6 @@ import {
parseScript,
serializeScript,
guessIndentString,
- guessQuoteStyle,
type AstTypes,
serializeYaml,
parseYaml
@@ -50,57 +49,6 @@ test('guessIndentString - eight spaces', () => {
expect(guessIndentString(code)).toBe(' ');
});
-test('guessQuoteStyle - single simple', () => {
- const code = dedent`
- console.log('asd');
- `;
- const ast = parseScript(code);
-
- expect(guessQuoteStyle(ast)).toBe('single');
-});
-
-test('guessQuoteStyle - single complex', () => {
- const code = dedent`
- import foo from 'bar';
-
- console.log("bar");
- const foobar = 'foo';
- `;
- const ast = parseScript(code);
-
- expect(guessQuoteStyle(ast)).toBe('single');
-});
-
-test('guessQuoteStyle - double simple', () => {
- const code = dedent`
- console.log("asd");
- `;
- const ast = parseScript(code);
-
- expect(guessQuoteStyle(ast)).toBe('double');
-});
-
-test('guessQuoteStyle - double complex', () => {
- const code = dedent`
- import foo from 'bar';
-
- console.log("bar");
- const foobar = "foo";
- `;
- const ast = parseScript(code);
-
- expect(guessQuoteStyle(ast)).toBe('double');
-});
-
-test('guessQuoteStyle - no quotes', () => {
- const code = dedent`
- const foo = true;
- `;
- const ast = parseScript(code);
-
- expect(guessQuoteStyle(ast)).toBe(undefined);
-});
-
const newVariableDeclaration: AstTypes.VariableDeclaration = {
type: 'VariableDeclaration',
kind: 'const',
@@ -128,20 +76,20 @@ test('integration - simple', () => {
const foobar = "foo";
}
`;
- const ast = parseScript(code);
+ const { ast, comments } = parseScript(code);
const method = ast.body[1] as AstTypes.FunctionDeclaration;
method.body.body.push(newVariableDeclaration);
// new variable is added with correct indentation and matching quotes
- expect(serializeScript(ast, code)).toMatchInlineSnapshot(`
+ expect(serializeScript(ast, comments, code)).toMatchInlineSnapshot(`
"import foo from 'bar';
function bar() {
console.log("bar");
const foobar = "foo";
- const foobar2 = "test";
+ const foobar2 = 'test';
}"
`);
});
@@ -155,13 +103,13 @@ test('integration - simple 2', () => {
const foobar = 'foo';
}
`;
- const ast = parseScript(code);
+ const { ast, comments } = parseScript(code);
const method = ast.body[1] as AstTypes.FunctionDeclaration;
method.body.body.push(newVariableDeclaration);
// new variable is added with correct indentation and matching quotes
- expect(serializeScript(ast, code)).toMatchInlineSnapshot(`
+ expect(serializeScript(ast, comments, code)).toMatchInlineSnapshot(`
"import foo from 'bar';
function bar() {
@@ -178,9 +126,9 @@ test('integration - preserves comments', () => {
/** @type {string} */
let foo = 'bar';
`;
- const ast = parseScript(code);
+ const { ast, comments } = parseScript(code);
- expect(serializeScript(ast, code)).toMatchInlineSnapshot(`
+ expect(serializeScript(ast, comments, code)).toMatchInlineSnapshot(`
"/** @type {string} */
let foo = 'bar';"
`);
diff --git a/packages/core/tooling/html/index.ts b/packages/core/tooling/html/index.ts
index 60b2ae078..e245c3bd9 100644
--- a/packages/core/tooling/html/index.ts
+++ b/packages/core/tooling/html/index.ts
@@ -7,9 +7,11 @@ import {
parseHtml
} from '../index.ts';
import { appendFromString } from '../js/common.ts';
+import { parseSvelte } from '../parsers.ts';
+import type { AST as SvelteAst } from 'svelte/compiler';
export { HtmlElement, HtmlElementType };
-export type { HtmlDocument };
+export type { HtmlDocument, SvelteAst };
export function createDiv(attributes: Record = {}): HtmlElement {
return createElement('div', attributes);
@@ -58,3 +60,9 @@ export function addSlot(
});
addFromRawHtml(options.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 68f3dc0b4..bcc9e04a1 100644
--- a/packages/core/tooling/index.ts
+++ b/packages/core/tooling/index.ts
@@ -14,10 +14,14 @@ import {
} from 'postcss';
import * as fleece from 'silver-fleece';
import { print as esrapPrint } from 'esrap';
+import ts, { type AdditionalComment } 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';
+type AdditionalCommentMap = WeakMap;
+
export {
// html
Document as HtmlDocument,
@@ -38,9 +42,11 @@ export {
export type {
// html
ChildNode as HtmlChildNode,
+ SvelteAst,
// js
TsEstree as AstTypes,
+ AdditionalCommentMap,
//css
CssChildNode
@@ -48,19 +54,22 @@ export type {
/**
* Parses as string to an AST. Code below is taken from `esrap` to ensure compatibilty.
- * https://github.com/sveltejs/esrap/blob/9daf5dd43b31f17f596aa7da91678f2650666dd0/test/common.js#L12
+ * https://github.com/sveltejs/esrap/blob/920491535d31484ac5fae2327c7826839d851aed/test/common.js#L14
*/
-export function parseScript(content: string): TsEstree.Program {
+export function parseScript(content: string): {
+ ast: TsEstree.Program;
+ comments: TsEstree.Comment[];
+ additionalComments: AdditionalCommentMap;
+} {
const comments: TsEstree.Comment[] = [];
const acornTs = acorn.Parser.extend(tsPlugin());
- // Acorn doesn't add comments to the AST by itself. This factory returns the capabilities to add them after the fact.
const ast = acornTs.parse(content, {
ecmaVersion: 'latest',
sourceType: 'module',
locations: true,
- onComment: (block, value, start, end) => {
+ onComment: (block, value, start, end, startLoc, endLoc) => {
if (block && /\n/.test(value)) {
let a = start;
while (a > 0 && content[a - 1] !== '\n') a -= 1;
@@ -72,38 +81,33 @@ export function parseScript(content: string): TsEstree.Program {
value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
}
- comments.push({ type: block ? 'Block' : 'Line', value, start, end });
+ comments.push({
+ type: block ? 'Block' : 'Line',
+ value,
+ start,
+ end,
+ loc: { start: startLoc as TsEstree.Position, end: endLoc as TsEstree.Position }
+ });
}
}) as TsEstree.Program;
- Walker.walk(ast as TsEstree.Node, null, {
- _(commentNode, { next }) {
- let comment: TsEstree.Comment;
-
- while (comments[0] && commentNode.start && comments[0].start! < commentNode.start) {
- comment = comments.shift()!;
- (commentNode.leadingComments ??= []).push(comment);
- }
-
- next();
-
- if (comments[0]) {
- const slice = content.slice(commentNode.end, comments[0].start);
-
- if (/^[,) \t]*$/.test(slice)) {
- commentNode.trailingComments = [comments.shift()!];
- }
- }
- }
- });
-
- return ast;
+ return {
+ ast,
+ comments,
+ additionalComments: new WeakMap()
+ };
}
-export function serializeScript(ast: TsEstree.Node, previousContent?: string): string {
- const { code } = esrapPrint(ast, {
- indent: guessIndentString(previousContent),
- quotes: guessQuoteStyle(ast)
+export function serializeScript(
+ ast: TsEstree.Node,
+ comments: TsEstree.Comment[],
+ previousContent?: string,
+ additionalComments?: AdditionalCommentMap
+): string {
+ // @ts-expect-error we are still using `estree` while `esrap` is using `@typescript-eslint/types`
+ // which is causing these errors. But they are simmilar enough to work together.
+ const { code } = esrapPrint(ast, ts({ comments, additionalComments }), {
+ indent: guessIndentString(previousContent)
});
return code;
}
@@ -207,39 +211,6 @@ export function guessIndentString(str: string | undefined): string {
}
}
-export function guessQuoteStyle(ast: TsEstree.Node): 'single' | 'double' | undefined {
- let singleCount = 0;
- let doubleCount = 0;
-
- Walker.walk(ast, null, {
- Literal(node) {
- if (node.raw && node.raw.length >= 2) {
- // we have at least two characters in the raw string that could represent both quotes
- const quotes = [node.raw[0], node.raw[node.raw.length - 1]];
- for (const quote of quotes) {
- switch (quote) {
- case "'":
- singleCount++;
- break;
- case '"':
- doubleCount++;
- break;
- default:
- break;
- }
- }
- }
- }
- });
-
- if (singleCount === 0 && doubleCount === 0) {
- // new file or file without any quotes
- return undefined;
- }
-
- return singleCount > doubleCount ? 'single' : 'double';
-}
-
export function parseYaml(content: string): ReturnType {
return yaml.parseDocument(content);
}
@@ -247,3 +218,11 @@ export function parseYaml(content: string): ReturnType): string {
return yaml.stringify(data, { singleQuote: true });
}
+
+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/js/common.ts b/packages/core/tooling/js/common.ts
index 8812660e2..1f51dcf6a 100644
--- a/packages/core/tooling/js/common.ts
+++ b/packages/core/tooling/js/common.ts
@@ -1,18 +1,30 @@
-import { type AstTypes, Walker, parseScript, serializeScript, stripAst } from '../index.ts';
+import {
+ type AdditionalCommentMap,
+ type AstTypes,
+ Walker,
+ parseScript,
+ serializeScript,
+ stripAst
+} from '../index.ts';
import decircular from 'decircular';
import dedent from 'dedent';
-export function addJsDocTypeComment(node: AstTypes.Node, options: { type: string }): void {
+export function addJsDocTypeComment(
+ node: AstTypes.Node,
+ additionalComments: AdditionalCommentMap,
+ options: { type: string }
+): void {
const comment: AstTypes.Comment = {
type: 'Block',
value: `* @type {${options.type}} `
};
- addComment(node, comment);
+ addComment(node, additionalComments, comment);
}
export function addJsDocComment(
node: AstTypes.Node,
+ additionalComments: AdditionalCommentMap,
options: { params: Record }
): void {
const commentLines: string[] = [];
@@ -25,16 +37,23 @@ export function addJsDocComment(
value: `*\n * ${commentLines.join('\n * ')}\n `
};
- addComment(node, comment);
+ addComment(node, additionalComments, comment);
}
-function addComment(node: AstTypes.Node, comment: AstTypes.Comment) {
- node.leadingComments ??= [];
-
- const found = node.leadingComments.find(
- (item) => item.type === 'Block' && item.value === comment.value
- );
- if (!found) node.leadingComments.push(comment);
+function addComment(
+ node: AstTypes.Node,
+ additionalComments: AdditionalCommentMap,
+ comment: AstTypes.Comment
+) {
+ const found = additionalComments
+ .get(node)
+ ?.find((item) => item.type === 'Block' && item.value === comment.value);
+
+ if (!found) {
+ const comments = additionalComments.get(node) ?? [];
+ comments.push({ ...comment, position: 'leading' });
+ additionalComments.set(node, comments);
+ }
}
export function typeAnnotate(
@@ -93,7 +112,7 @@ export function areNodesEqual(node: AstTypes.Node, otherNode: AstTypes.Node): bo
const nodeClone = stripAst(decircular(node), ['loc', 'raw']);
const otherNodeClone = stripAst(decircular(otherNode), ['loc', 'raw']);
- return serializeScript(nodeClone) === serializeScript(otherNodeClone);
+ return serializeScript(nodeClone, []) === serializeScript(otherNodeClone, []);
}
export function createBlockStatement(): AstTypes.BlockStatement {
@@ -118,18 +137,18 @@ export function appendFromString(
node: AstTypes.BlockStatement | AstTypes.Program,
options: { code: string }
): void {
- const program = parseScript(dedent(options.code));
+ const { ast } = parseScript(dedent(options.code));
- for (const childNode of program.body) {
+ for (const childNode of ast.body) {
// @ts-expect-error
node.body.push(childNode);
}
}
export function parseExpression(code: string): AstTypes.Expression {
- const program = parseScript(dedent(code));
- stripAst(program, ['raw']);
- const statement = program.body[0]!;
+ const { ast } = parseScript(dedent(code));
+ stripAst(ast, ['raw']);
+ const statement = ast.body[0]!;
if (statement.type !== 'ExpressionStatement') {
throw new Error('Code provided was not an expression');
}
@@ -142,8 +161,8 @@ export function parseStatement(code: string): AstTypes.Statement {
}
export function parseFromString(code: string): T {
- const program = parseScript(dedent(code));
- const statement = program.body[0]!;
+ const { ast } = parseScript(dedent(code));
+ const statement = ast.body[0]!;
return statement as T;
}
diff --git a/packages/core/tooling/js/index.ts b/packages/core/tooling/js/index.ts
index 0b4c6e632..206d49d5a 100644
--- a/packages/core/tooling/js/index.ts
+++ b/packages/core/tooling/js/index.ts
@@ -7,4 +7,4 @@ export * as variables from './variables.ts';
export * as exports from './exports.ts';
export * as kit from './kit.ts';
export * as vite from './vite.ts';
-export type { AstTypes } from '../index.ts';
+export type { AstTypes, AdditionalCommentMap } from '../index.ts';
diff --git a/packages/core/tooling/parsers.ts b/packages/core/tooling/parsers.ts
index 21b6b0993..b030a16b5 100644
--- a/packages/core/tooling/parsers.ts
+++ b/packages/core/tooling/parsers.ts
@@ -1,16 +1,19 @@
import * as utils from './index.ts';
-import MagicString from 'magic-string';
type ParseBase = {
source: string;
generateCode(): string;
};
-export function parseScript(source: string): { ast: utils.AstTypes.Program } & ParseBase {
- const ast = utils.parseScript(source);
- const generateCode = () => utils.serializeScript(ast, source);
+export function parseScript(source: string): {
+ ast: utils.AstTypes.Program;
+ comments: utils.AstTypes.Comment[];
+ additionalComments: utils.AdditionalCommentMap;
+} & ParseBase {
+ const { ast, comments, additionalComments } = utils.parseScript(source);
+ const generateCode = () => utils.serializeScript(ast, comments, source, additionalComments);
- return { ast, source, generateCode };
+ return { ast, comments, additionalComments, source, generateCode };
}
export function parseCss(source: string): { ast: utils.CssAst } & ParseBase {
@@ -45,136 +48,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 =
- /|