Skip to content

feat: add print(...) function #16188

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,13 @@
"typescript-eslint": "^8.24.0",
"v8-natives": "^1.2.5",
"vitest": "^2.1.9"
},
"pnpm": {
"overrides": {
"esrap": "link:../../esrap"
}
},
"dependencies": {
"esrap": "link:../../../../esrap"
}
}
4 changes: 2 additions & 2 deletions packages/svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -164,14 +164,14 @@
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5",
"acorn": "^8.12.1",
"@sveltejs/acorn-typescript": "^1.0.5",
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"esm-env": "^1.2.1",
"esrap": "^1.4.8",
"esrap": "https://pkg.pr.new/sveltejs/esrap@a275a5c",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
Expand Down
133 changes: 50 additions & 83 deletions packages/svelte/scripts/process-messages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import fs from 'node:fs';
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import * as esrap from 'esrap';
import ts from 'esrap/languages/ts';

const DIR = '../../documentation/docs/98-reference/.generated';

Expand Down Expand Up @@ -98,55 +99,18 @@ function run() {
.replace(/\r\n/g, '\n');

/**
* @type {Array<{
* type: string;
* value: string;
* start: number;
* end: number
* }>}
* @type {any[]}
*/
const comments = [];

let ast = acorn.parse(source, {
ecmaVersion: 'latest',
sourceType: 'module',
onComment: (block, value, start, end) => {
if (block && /\n/.test(value)) {
let a = start;
while (a > 0 && source[a - 1] !== '\n') a -= 1;

let b = a;
while (/[ \t]/.test(source[b])) b += 1;

const indentation = source.slice(a, b);
value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
}

comments.push({ type: block ? 'Block' : 'Line', value, start, end });
}
locations: true,
onComment: comments
});

ast = walk(ast, null, {
_(node, { next }) {
let comment;

while (comments[0] && comments[0].start < node.start) {
comment = comments.shift();
// @ts-expect-error
(node.leadingComments ||= []).push(comment);
}

next();

if (comments[0]) {
const slice = source.slice(node.end, comments[0].start);

if (/^[,) \t]*$/.test(slice)) {
// @ts-expect-error
node.trailingComments = [comments.shift()];
}
}
},
// @ts-expect-error
Identifier(node, context) {
if (node.name === 'CODES') {
Expand All @@ -161,11 +125,6 @@ function run() {
}
});

if (comments.length > 0) {
// @ts-expect-error
(ast.trailingComments ||= []).push(...comments);
}

const category = messages[name];

// find the `export function CODE` node
Expand All @@ -184,6 +143,16 @@ function run() {
const template_node = ast.body[index];
ast.body.splice(index, 1);

const jsdoc = comments.findLast((comment) => comment.start < template_node.start);

const printed = esrap.print(
ast,
// @ts-expect-error
ts({
comments: comments.filter((comment) => comment !== jsdoc)
})
);

for (const code in category) {
const { messages } = category[code];
/** @type {string[]} */
Expand Down Expand Up @@ -273,41 +242,6 @@ function run() {
}

const clone = walk(/** @type {import('estree').Node} */ (template_node), null, {
// @ts-expect-error Block is a block comment, which is not recognised
Block(node, context) {
if (!node.value.includes('PARAMETER')) return;

const value = /** @type {string} */ (node.value)
.split('\n')
.map((line) => {
if (line === ' * MESSAGE') {
return messages[messages.length - 1]
.split('\n')
.map((line) => ` * ${line}`)
.join('\n');
}

if (line.includes('PARAMETER')) {
return vars
.map((name, i) => {
const optional = i >= group[0].vars.length;

return optional
? ` * @param {string | undefined | null} [${name}]`
: ` * @param {string} ${name}`;
})
.join('\n');
}

return line;
})
.filter((x) => x !== '')
.join('\n');

if (value !== node.value) {
return { ...node, value };
}
},
FunctionDeclaration(node, context) {
if (node.id.name !== 'CODE') return;

Expand Down Expand Up @@ -394,16 +328,49 @@ function run() {
}
});

const jsdoc_clone = {
...jsdoc,
value: /** @type {string} */ (jsdoc.value)
.split('\n')
.map((line) => {
if (line === ' * MESSAGE') {
return messages[messages.length - 1]
.split('\n')
.map((line) => ` * ${line}`)
.join('\n');
}

if (line.includes('PARAMETER')) {
return vars
.map((name, i) => {
const optional = i >= group[0].vars.length;

return optional
? ` * @param {string | undefined | null} [${name}]`
: ` * @param {string} ${name}`;
})
.join('\n');
}

return line;
})
.filter((x) => x !== '')
.join('\n')
};

// @ts-expect-error
const block = esrap.print({ ...ast, body: [clone] }, ts({ comments: [jsdoc_clone] })).code;

printed.code += `\n\n${block}`;

// @ts-expect-error
ast.body.push(clone);
}

const module = esrap.print(ast);

fs.writeFileSync(
dest,
`/* This file is generated by scripts/process-messages/index.js. Do not edit! */\n\n` +
module.code,
printed.code,
'utf-8'
);
}
Expand Down
18 changes: 12 additions & 6 deletions packages/svelte/src/compiler/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,25 @@ import { CompileDiagnostic } from './utils/compile_diagnostic.js';

/** @typedef {{ start?: number, end?: number }} NodeLike */
class InternalCompileError extends Error {
message = ''; // ensure this property is enumerable
message = '';

// ensure this property is enumerable
#diagnostic;

/**
* @param {string} code
* @param {string} message
* @param {[number, number] | undefined} position
*/
* @param {string} code
* @param {string} message
* @param {[number, number] | undefined} position
*/
constructor(code, message, position) {
super(message);
this.stack = ''; // avoid unnecessary noise; don't set it as a class property or it becomes enumerable

// We want to extend from Error so that various bundler plugins properly handle it.
// But we also want to share the same object shape with that of warnings, therefore
// we create an instance of the shared class an copy over its properties.
this.#diagnostic = new CompileDiagnostic(code, message, position);

Object.assign(this, this.#diagnostic);
this.name = 'CompileError';
}
Expand Down Expand Up @@ -816,7 +820,9 @@ export function bind_invalid_expression(node) {
* @returns {never}
*/
export function bind_invalid_name(node, name, explanation) {
e(node, 'bind_invalid_name', `${explanation ? `\`bind:${name}\` is not a valid binding. ${explanation}` : `\`bind:${name}\` is not a valid binding`}\nhttps://svelte.dev/e/bind_invalid_name`);
e(node, 'bind_invalid_name', `${explanation
? `\`bind:${name}\` is not a valid binding. ${explanation}`
: `\`bind:${name}\` is not a valid binding`}\nhttps://svelte.dev/e/bind_invalid_name`);
}

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/svelte/src/compiler/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { transform_component, transform_module } from './phases/3-transform/inde
import { validate_component_options, validate_module_options } from './validate-options.js';
import * as state from './state.js';
export { default as preprocess } from './preprocess/index.js';
export { print } from './print/index.js';

/**
* `compile` converts your `.svelte` source code into a JavaScript module that exports a component
Expand Down Expand Up @@ -69,7 +70,7 @@ export function compileModule(source, options) {
const validated = validate_module_options(options, '');
state.reset(source, validated);

const analysis = analyze_module(parse_acorn(source, false), validated);
const analysis = analyze_module(source, validated);
return transform_module(analysis, source, validated);
}

Expand Down
1 change: 1 addition & 0 deletions packages/svelte/src/compiler/legacy.js
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ export function convert(source, ast) {
SpreadAttribute(node) {
return { ...node, type: 'Spread' };
},
// @ts-ignore
StyleSheet(node, context) {
return {
...node,
Expand Down
55 changes: 38 additions & 17 deletions packages/svelte/src/compiler/phases/1-parse/acorn.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,27 @@ import { tsPlugin } from '@sveltejs/acorn-typescript';

const ParserWithTS = acorn.Parser.extend(tsPlugin());

/**
* @typedef {Comment & {
* start: number;
* end: number;
* }} CommentWithLocation
*/

/**
* @param {string} source
* @param {Comment[]} comments
* @param {boolean} typescript
* @param {boolean} [is_script]
*/
export function parse(source, typescript, is_script) {
export function parse(source, comments, typescript, is_script) {
const parser = typescript ? ParserWithTS : acorn.Parser;
const { onComment, add_comments } = get_comment_handlers(source);

const { onComment, add_comments } = get_comment_handlers(
source,
/** @type {CommentWithLocation[]} */ (comments)
);

// @ts-ignore
const parse_statement = parser.prototype.parseStatement;

Expand Down Expand Up @@ -53,13 +66,19 @@ export function parse(source, typescript, is_script) {

/**
* @param {string} source
* @param {Comment[]} comments
* @param {boolean} typescript
* @param {number} index
* @returns {acorn.Expression & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }}
*/
export function parse_expression_at(source, typescript, index) {
export function parse_expression_at(source, comments, typescript, index) {
const parser = typescript ? ParserWithTS : acorn.Parser;
const { onComment, add_comments } = get_comment_handlers(source);

const { onComment, add_comments } = get_comment_handlers(
source,
/** @type {CommentWithLocation[]} */ (comments),
index
);

const ast = parser.parseExpressionAt(source, index, {
onComment,
Expand All @@ -78,26 +97,18 @@ export function parse_expression_at(source, typescript, index) {
* to add them after the fact. They are needed in order to support `svelte-ignore` comments
* in JS code and so that `prettier-plugin-svelte` doesn't remove all comments when formatting.
* @param {string} source
* @param {CommentWithLocation[]} comments
* @param {number} index
*/
function get_comment_handlers(source) {
/**
* @typedef {Comment & {
* start: number;
* end: number;
* }} CommentWithLocation
*/

/** @type {CommentWithLocation[]} */
const comments = [];

function get_comment_handlers(source, comments, index = 0) {
return {
/**
* @param {boolean} block
* @param {string} value
* @param {number} start
* @param {number} end
*/
onComment: (block, value, start, end) => {
onComment: (block, value, start, end, start_loc, end_loc) => {
if (block && /\n/.test(value)) {
let a = start;
while (a > 0 && source[a - 1] !== '\n') a -= 1;
Expand All @@ -109,13 +120,23 @@ function get_comment_handlers(source) {
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: start_loc, end: end_loc }
});
},

/** @param {acorn.Node & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }} ast */
add_comments(ast) {
if (comments.length === 0) return;

comments = comments
.filter((comment) => comment.start >= index)
.map(({ type, value, start, end }) => ({ type, value, start, end }));

walk(ast, null, {
_(node, { next, path }) {
let comment;
Expand Down
Loading
Loading