Rich Harris 3 months ago
parent 486d10c880
commit 97c5d98f79

@ -70,7 +70,7 @@ export function compileModule(source, options) {
const validated = validate_module_options(options, ''); const validated = validate_module_options(options, '');
state.reset(source, validated); 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); return transform_module(analysis, source, validated);
} }

@ -5,14 +5,27 @@ import { tsPlugin } from '@sveltejs/acorn-typescript';
const ParserWithTS = acorn.Parser.extend(tsPlugin()); const ParserWithTS = acorn.Parser.extend(tsPlugin());
/**
* @typedef {Comment & {
* start: number;
* end: number;
* }} CommentWithLocation
*/
/** /**
* @param {string} source * @param {string} source
* @param {Comment[]} comments
* @param {boolean} typescript * @param {boolean} typescript
* @param {boolean} [is_script] * @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 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 // @ts-ignore
const parse_statement = parser.prototype.parseStatement; const parse_statement = parser.prototype.parseStatement;
@ -53,13 +66,18 @@ export function parse(source, typescript, is_script) {
/** /**
* @param {string} source * @param {string} source
* @param {Comment[]} comments
* @param {boolean} typescript * @param {boolean} typescript
* @param {number} index * @param {number} index
* @returns {acorn.Expression & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }} * @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 parser = typescript ? ParserWithTS : acorn.Parser;
const { onComment, add_comments } = get_comment_handlers(source);
const { onComment, add_comments } = get_comment_handlers(
source,
/** @type {CommentWithLocation[]} */ (comments)
);
const ast = parser.parseExpressionAt(source, index, { const ast = parser.parseExpressionAt(source, index, {
onComment, onComment,
@ -78,18 +96,9 @@ export function parse_expression_at(source, typescript, index) {
* to add them after the fact. They are needed in order to support `svelte-ignore` comments * 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. * in JS code and so that `prettier-plugin-svelte` doesn't remove all comments when formatting.
* @param {string} source * @param {string} source
* @param {CommentWithLocation[]} comments
*/ */
function get_comment_handlers(source) { function get_comment_handlers(source, comments) {
/**
* @typedef {Comment & {
* start: number;
* end: number;
* }} CommentWithLocation
*/
/** @type {CommentWithLocation[]} */
const comments = [];
return { return {
/** /**
* @param {boolean} block * @param {boolean} block
@ -97,7 +106,7 @@ function get_comment_handlers(source) {
* @param {number} start * @param {number} start
* @param {number} end * @param {number} end
*/ */
onComment: (block, value, start, end) => { onComment: (block, value, start, end, start_loc, end_loc) => {
if (block && /\n/.test(value)) { if (block && /\n/.test(value)) {
let a = start; let a = start;
while (a > 0 && source[a - 1] !== '\n') a -= 1; while (a > 0 && source[a - 1] !== '\n') a -= 1;
@ -109,13 +118,21 @@ function get_comment_handlers(source) {
value = value.replace(new RegExp(`^${indentation}`, 'gm'), ''); 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 */ /** @param {acorn.Node & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }} ast */
add_comments(ast) { add_comments(ast) {
if (comments.length === 0) return; if (comments.length === 0) return;
comments = comments.slice();
walk(ast, null, { walk(ast, null, {
_(node, { next, path }) { _(node, { next, path }) {
let comment; let comment;

@ -1,4 +1,5 @@
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Comment } from 'estree' */
// @ts-expect-error acorn type definitions are borked in the release we use // @ts-expect-error acorn type definitions are borked in the release we use
import { isIdentifierStart, isIdentifierChar } from 'acorn'; import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment.js'; import fragment from './state/fragment.js';
@ -87,6 +88,7 @@ export class Parser {
type: 'Root', type: 'Root',
fragment: create_fragment(), fragment: create_fragment(),
options: null, options: null,
comments: [],
metadata: { metadata: {
ts: this.ts ts: this.ts
} }

@ -59,7 +59,12 @@ export default function read_pattern(parser) {
space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1); space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
const expression = /** @type {any} */ ( const expression = /** @type {any} */ (
parse_expression_at(`${space_with_newline}(${pattern_string} = 1)`, parser.ts, start - 1) parse_expression_at(
`${space_with_newline}(${pattern_string} = 1)`,
parser.root.comments,
parser.ts,
start - 1
)
).left; ).left;
expression.typeAnnotation = read_type_annotation(parser); expression.typeAnnotation = read_type_annotation(parser);
@ -96,13 +101,13 @@ function read_type_annotation(parser) {
// parameters as part of a sequence expression instead, and will then error on optional // parameters as part of a sequence expression instead, and will then error on optional
// parameters (`?:`). Therefore replace that sequence with something that will not error. // parameters (`?:`). Therefore replace that sequence with something that will not error.
parser.template.slice(parser.index).replace(/\?\s*:/g, ':'); parser.template.slice(parser.index).replace(/\?\s*:/g, ':');
let expression = parse_expression_at(template, parser.ts, a); let expression = parse_expression_at(template, parser.root.comments, parser.ts, a);
// `foo: bar = baz` gets mangled — fix it // `foo: bar = baz` gets mangled — fix it
if (expression.type === 'AssignmentExpression') { if (expression.type === 'AssignmentExpression') {
let b = expression.right.start; let b = expression.right.start;
while (template[b] !== '=') b -= 1; while (template[b] !== '=') b -= 1;
expression = parse_expression_at(template.slice(0, b), parser.ts, a); expression = parse_expression_at(template.slice(0, b), parser.root.comments, parser.ts, a);
} }
// `array as item: string, index` becomes `string, index`, which is mistaken as a sequence expression - fix that // `array as item: string, index` becomes `string, index`, which is mistaken as a sequence expression - fix that

@ -34,7 +34,12 @@ export function get_loose_identifier(parser, opening_token) {
*/ */
export default function read_expression(parser, opening_token, disallow_loose) { export default function read_expression(parser, opening_token, disallow_loose) {
try { try {
const node = parse_expression_at(parser.template, parser.ts, parser.index); const node = parse_expression_at(
parser.template,
parser.root.comments,
parser.ts,
parser.index
);
let num_parens = 0; let num_parens = 0;

@ -34,7 +34,7 @@ export function read_script(parser, start, attributes) {
let ast; let ast;
try { try {
ast = acorn.parse(source, parser.ts, true); ast = acorn.parse(source, parser.root.comments, parser.ts, true);
} catch (err) { } catch (err) {
parser.acorn_error(err); parser.acorn_error(err);
} }

@ -389,7 +389,12 @@ function open(parser) {
let function_expression = matched let function_expression = matched
? /** @type {ArrowFunctionExpression} */ ( ? /** @type {ArrowFunctionExpression} */ (
parse_expression_at(prelude + `${params} => {}`, parser.ts, params_start) parse_expression_at(
prelude + `${params} => {}`,
parser.root.comments,
parser.ts,
params_start
)
) )
: { params: [] }; : { params: [] };

@ -1,8 +1,9 @@
/** @import { Expression, Node, Program } from 'estree' */ /** @import { Comment, Expression, Node, Program } from 'estree' */
/** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */ /** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { AnalysisState, Visitors } from './types' */ /** @import { AnalysisState, Visitors } from './types' */
/** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */ /** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { parse } from '../1-parse/acorn.js';
import * as e from '../../errors.js'; import * as e from '../../errors.js';
import * as w from '../../warnings.js'; import * as w from '../../warnings.js';
import { extract_identifiers } from '../../utils/ast.js'; import { extract_identifiers } from '../../utils/ast.js';
@ -231,11 +232,16 @@ function get_component_name(filename) {
const RESERVED = ['$$props', '$$restProps', '$$slots']; const RESERVED = ['$$props', '$$restProps', '$$slots'];
/** /**
* @param {Program} ast * @param {string} source
* @param {ValidatedModuleCompileOptions} options * @param {ValidatedModuleCompileOptions} options
* @returns {Analysis} * @returns {Analysis}
*/ */
export function analyze_module(ast, options) { export function analyze_module(source, options) {
/** @type {Comment[]} */
const comments = [];
const ast = parse(source, comments, false, false);
const { scope, scopes } = create_scopes(ast, new ScopeRoot(), false, null); const { scope, scopes } = create_scopes(ast, new ScopeRoot(), false, null);
for (const [name, references] of scope.references) { for (const [name, references] of scope.references) {
@ -259,6 +265,7 @@ export function analyze_module(ast, options) {
runes: true, runes: true,
immutable: true, immutable: true,
tracing: false, tracing: false,
comments,
classes: new Map() classes: new Map()
}; };
@ -429,6 +436,7 @@ export function analyze_component(root, source, options) {
module, module,
instance, instance,
template, template,
comments: root.comments,
elements: [], elements: [],
runes, runes,
tracing: false, tracing: false,

@ -362,6 +362,9 @@ export function client_component(analysis, options) {
.../** @type {ESTree.Statement[]} */ (template.body) .../** @type {ESTree.Statement[]} */ (template.body)
]); ]);
// trick esrap into including comments
component_block.loc = instance.loc;
if (!analysis.runes) { if (!analysis.runes) {
// Bind static exports to props so that people can access them with bind:x // Bind static exports to props so that people can access them with bind:x
for (const { name, alias } of analysis.exports) { for (const { name, alias } of analysis.exports) {

@ -36,7 +36,7 @@ export function transform_component(analysis, source, options) {
const js_source_name = get_source_name(options.filename, options.outputFilename, 'input.svelte'); const js_source_name = get_source_name(options.filename, options.outputFilename, 'input.svelte');
// @ts-ignore TODO // @ts-ignore TODO
const js = print(program, ts(), { const js = print(program, ts({ comments: analysis.comments }), {
// include source content; makes it easier/more robust looking up the source map code // include source content; makes it easier/more robust looking up the source map code
// (else esrap does return null for source and sourceMapContent which may trip up tooling) // (else esrap does return null for source and sourceMapContent which may trip up tooling)
sourceMapContent: source, sourceMapContent: source,
@ -95,14 +95,20 @@ export function transform_module(analysis, source, options) {
]; ];
} }
// @ts-expect-error
const js = print(program, ts({ comments: analysis.comments }), {
// include source content; makes it easier/more robust looking up the source map code
// (else esrap does return null for source and sourceMapContent which may trip up tooling)
sourceMapContent: source,
sourceMapSource: get_source_name(options.filename, undefined, 'input.svelte.js')
});
// prepend comment
js.code = `/* ${basename} generated by Svelte v${VERSION} */\n${js.code}`;
js.map.mappings = ';' + js.map.mappings;
return { return {
// @ts-expect-error js,
js: print(program, ts(), {
// include source content; makes it easier/more robust looking up the source map code
// (else esrap does return null for source and sourceMapContent which may trip up tooling)
sourceMapContent: source,
sourceMapSource: get_source_name(options.filename, undefined, 'input.svelte.js')
}),
css: null, css: null,
metadata: { metadata: {
runes: true runes: true

@ -242,6 +242,9 @@ export function server_component(analysis, options) {
.../** @type {Statement[]} */ (template.body) .../** @type {Statement[]} */ (template.body)
]); ]);
// trick esrap into including comments
component_block.loc = instance.loc;
if (analysis.props_id) { if (analysis.props_id) {
// need to be placed on first line of the component for hydration // need to be placed on first line of the component for hydration
component_block.body.unshift( component_block.body.unshift(

@ -2,6 +2,7 @@ import type { AST, Binding, StateField } from '#compiler';
import type { import type {
AssignmentExpression, AssignmentExpression,
ClassBody, ClassBody,
Comment,
Identifier, Identifier,
LabeledStatement, LabeledStatement,
Node, Node,
@ -37,6 +38,7 @@ export interface Analysis {
runes: boolean; runes: boolean;
immutable: boolean; immutable: boolean;
tracing: boolean; tracing: boolean;
comments: Comment[];
classes: Map<ClassBody, Map<string, StateField>>; classes: Map<ClassBody, Map<string, StateField>>;

@ -9,7 +9,8 @@ import ts from 'esrap/languages/ts';
export function print(ast) { export function print(ast) {
// @ts-expect-error some bullshit // @ts-expect-error some bullshit
return esrap.print(ast, { return esrap.print(ast, {
...ts(), // @ts-expect-error some bullshit
...ts({ comments: ast.type === 'Root' ? ast.comments : [] }),
...visitors ...visitors
}); });
} }
@ -127,7 +128,13 @@ const visitors = {
context.write(`</${node.name}>`); context.write(`</${node.name}>`);
}, },
OnDirective(node, context) {
// TODO
},
TransitionDirective(node, context) { TransitionDirective(node, context) {
// TODO // TODO
},
Comment(node, context) {
// TODO
} }
}; };

@ -72,6 +72,8 @@ export namespace AST {
instance: Script | null; instance: Script | null;
/** The parsed `<script module>` element, if exists */ /** The parsed `<script module>` element, if exists */
module: Script | null; module: Script | null;
/** Comments found in <script> and {expressions} */
comments: import('estree').Comment[];
/** @internal */ /** @internal */
metadata: { metadata: {
/** Whether the component was parsed with typescript */ /** Whether the component was parsed with typescript */

@ -1,6 +1,5 @@
/* module.svelte.js generated by Svelte VERSION */ /* module.svelte.js generated by Svelte VERSION */
import * as $ from 'svelte/internal/client'; import * as $ from 'svelte/internal/client';
import { random } from './export'; import { random } from './export';
export { random }; export { random };

@ -1,6 +1,5 @@
/* module.svelte.js generated by Svelte VERSION */ /* module.svelte.js generated by Svelte VERSION */
import * as $ from 'svelte/internal/server'; import * as $ from 'svelte/internal/server';
import { random } from './export'; import { random } from './export';
export { random }; export { random };

Loading…
Cancel
Save