feat: add rootDir option and set __svelte_meta.file like in svelte4 (#11627)

* feat: add rootDir option and set __svelte_meta.file like in svelte4

* Update packages/svelte/src/compiler/validate-options.js

* update tests

* centralise logic

* fix

* note to self

* Apply suggestions from code review

* lint

* one dollar towards the windows backslash bugfix foundation please

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
pull/11645/head
Dominik G 1 year ago committed by GitHub
parent 2e7e399160
commit dc9b0d08c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: introduce `rootDir` compiler option, make `filename` relative to it

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: rename `__svelte_meta.filename` to `__svelte_meta.file` to align with svelte 4

@ -19,10 +19,10 @@ export { default as preprocess } from './preprocess/index.js';
* @returns {import('#compiler').CompileResult}
*/
export function compile(source, options) {
try {
state.reset({ source, filename: options.filename });
const validated = validate_component_options(options, '');
state.reset(source, validated);
const validated = validate_component_options(options, '');
try {
let parsed = _parse(source);
const { customElement: customElementOptions, ...parsed_options } = parsed.options || {};
@ -49,7 +49,7 @@ export function compile(source, options) {
return result;
} catch (e) {
if (e instanceof CompileError) {
handle_compile_error(e, options.filename, source);
handle_compile_error(e);
}
throw e;
@ -65,16 +65,16 @@ export function compile(source, options) {
* @returns {import('#compiler').CompileResult}
*/
export function compileModule(source, options) {
try {
state.reset({ source, filename: options.filename });
const validated = validate_module_options(options, '');
state.reset(source, validated);
const validated = validate_module_options(options, '');
try {
const analysis = analyze_module(parse_acorn(source, false), validated);
const result = transform_module(analysis, source, validated);
return result;
} catch (e) {
if (e instanceof CompileError) {
handle_compile_error(e, options.filename, source);
handle_compile_error(e);
}
throw e;
@ -83,11 +83,9 @@ export function compileModule(source, options) {
/**
* @param {import('#compiler').CompileError} error
* @param {string | undefined} filename
* @param {string} source
*/
function handle_compile_error(error, filename, source) {
error.filename = filename;
function handle_compile_error(error) {
error.filename = state.filename;
if (error.position) {
const start = state.locator(error.position[0]);
@ -134,11 +132,11 @@ function handle_compile_error(error, filename, source) {
*
* https://svelte.dev/docs/svelte-compiler#svelte-parse
* @param {string} source
* @param {{ filename?: string; modern?: boolean }} [options]
* @param {{ filename?: string; rootDir?: string; modern?: boolean }} [options]
* @returns {import('#compiler').Root | import('./types/legacy-nodes.js').LegacyRoot}
*/
export function parse(source, options = {}) {
state.reset({ source, filename: options.filename });
export function parse(source, { filename, rootDir, modern } = {}) {
state.reset(source, { filename, rootDir }); // TODO it's weird to require filename/rootDir here. reconsider the API
/** @type {import('#compiler').Root} */
let ast;
@ -146,13 +144,13 @@ export function parse(source, options = {}) {
ast = _parse(source);
} catch (e) {
if (e instanceof CompileError) {
handle_compile_error(e, options.filename, source);
handle_compile_error(e);
}
throw e;
}
return to_public_ast(source, ast, options.modern);
return to_public_ast(source, ast, modern);
}
/**

@ -18,7 +18,7 @@ import { migrate_svelte_ignore } from '../utils/extract_svelte_ignore.js';
*/
export function migrate(source) {
try {
reset({ source, filename: 'migrate.svelte' });
reset(source, { filename: 'migrate.svelte' });
let parsed = parse(source);

@ -8,6 +8,7 @@ import { javascript_visitors_runes } from './visitors/javascript-runes.js';
import { javascript_visitors_legacy } from './visitors/javascript-legacy.js';
import { serialize_get_binding } from './utils.js';
import { render_stylesheet } from '../css/index.js';
import { filename } from '../../../state.js';
/**
* This function ensures visitor sets don't accidentally clobber each other
@ -471,14 +472,7 @@ export function client_component(source, analysis, options) {
}
if (options.dev) {
if (options.filename) {
let filename = options.filename;
if (/(\/|\w:)/.test(options.filename)) {
// filename is absolute — truncate it
const parts = filename.split(/[/\\]/);
filename = parts.length > 3 ? ['...', ...parts.slice(-3)].join('/') : filename;
}
if (filename) {
// add `App.filename = 'App.svelte'` so that we can print useful messages later
body.unshift(
b.stmt(

@ -27,7 +27,7 @@ import { DOMBooleanAttributes, HYDRATION_END, HYDRATION_START } from '../../../.
import { escape_html } from '../../../../escaping.js';
import { sanitize_template_string } from '../../../utils/sanitize_template_string.js';
import { BLOCK_CLOSE, BLOCK_CLOSE_ELSE } from '../../../../internal/server/hydration.js';
import { locator } from '../../../state.js';
import { filename, locator } from '../../../state.js';
export const block_open = t_string(`<!--${HYDRATION_START}-->`);
export const block_close = t_string(`<!--${HYDRATION_END}-->`);
@ -2412,14 +2412,7 @@ export function server_component(analysis, options) {
body.push(b.export_default(component_function));
}
if (options.dev && options.filename) {
let filename = options.filename;
if (/(\/|\w:)/.test(options.filename)) {
// filename is absolute — truncate it
const parts = filename.split(/[/\\]/);
filename = parts.length > 3 ? ['...', ...parts.slice(-3)].join('/') : filename;
}
if (options.dev && filename) {
// add `App.filename = 'App.svelte'` so that we can print useful messages later
body.unshift(
b.stmt(

@ -5,7 +5,11 @@ import { getLocator } from 'locate-character';
/** @type {import('#compiler').Warning[]} */
export let warnings = [];
/** @type {string | undefined} */
/**
* The filename (if specified in the compiler options) relative to the rootDir (if specified).
* This should not be used in the compiler output except in dev mode
* @type {string | undefined}
*/
export let filename;
export let locator = getLocator('', { offsetLine: 1 });
@ -26,14 +30,23 @@ export function pop_ignore() {
}
/**
* @param {{
* source: string;
* filename: string | undefined;
* }} options
* @param {string} source
* @param {{ filename?: string, rootDir?: string }} options
*/
export function reset(options) {
filename = options.filename;
locator = getLocator(options.source, { offsetLine: 1 });
export function reset(source, options) {
const root_dir = options.rootDir?.replace(/\\/g, '/');
filename = options.filename?.replace(/\\/g, '/');
if (
typeof filename === 'string' &&
typeof root_dir === 'string' &&
filename.startsWith(root_dir)
) {
// make filename relative to rootDir
filename = filename.replace(root_dir, '').replace(/^[/\\]/, '');
}
locator = getLocator(source, { offsetLine: 1 });
warnings = [];
ignore_stack = [];
}

@ -216,12 +216,22 @@ export interface ModuleCompileOptions {
* Used for debugging hints and sourcemaps. Your bundler plugin will set it automatically.
*/
filename?: string;
/**
* Used for ensuring filenames don't leak filesystem information. Your bundler plugin will set it automatically.
* @default process.cwd() on node-like environments, undefined elsewhere
*/
rootDir?: string;
}
// The following two somewhat scary looking types ensure that certain types are required but can be undefined still
export type ValidatedModuleCompileOptions = Omit<Required<ModuleCompileOptions>, 'filename'> & {
export type ValidatedModuleCompileOptions = Omit<
Required<ModuleCompileOptions>,
'filename' | 'rootDir'
> & {
filename: ModuleCompileOptions['filename'];
rootDir: ModuleCompileOptions['rootDir'];
};
export type ValidatedCompileOptions = ValidatedModuleCompileOptions &

@ -10,6 +10,10 @@ import * as w from './warnings.js';
const common = {
filename: string(undefined),
// default to process.cwd() where it exists to replicate svelte4 behavior
// see https://github.com/sveltejs/svelte/blob/b62fc8c8fd2640c9b99168f01b9d958cb2f7574f/packages/svelte/src/compiler/compile/Component.js#L211
rootDir: string(typeof process !== 'undefined' ? process.cwd?.() : undefined),
dev: boolean(false),
generate: validator('client', (input, keypath) => {

@ -34,7 +34,7 @@ export function add_locations(fn, filename, locations) {
function assign_location(element, filename, location) {
// @ts-expect-error
element.__svelte_meta = {
loc: { filename, line: location[0], column: location[1] }
loc: { file: filename, line: location[0], column: location[1] }
};
if (location[2]) {

@ -116,7 +116,7 @@ export function element(anchor, get_tag, is_svg, render_fn, get_namespace, locat
// @ts-expect-error
element.__svelte_meta = {
loc: {
filename,
file: filename,
line: location[0],
column: location[1]
}

@ -130,6 +130,7 @@ export function runtime_suite(runes: boolean) {
async function common_setup(cwd: string, runes: boolean | undefined, config: RuntimeTest) {
const compileOptions: CompileOptions = {
generate: 'client',
rootDir: cwd,
...config.compileOptions,
immutable: config.immutable,
accessors: 'accessors' in config ? config.accessors : true,

@ -6,5 +6,5 @@ export default test({
},
error:
'bind_invalid_export\n' +
'Component .../export-binding/counter/index.svelte has an export named `increment` that a consumer component is trying to access using `bind:increment`, which is disallowed. Instead, use `bind:this` (e.g. `<Counter bind:this={component} />`) and then access the property on the bound component instance (e.g. `component.increment`)'
'Component counter/index.svelte has an export named `increment` that a consumer component is trying to access using `bind:increment`, which is disallowed. Instead, use `bind:this` (e.g. `<Counter bind:this={component} />`) and then access the property on the bound component instance (e.g. `component.increment`)'
});

@ -32,7 +32,7 @@ export default test({
if (variant === 'hydrate') {
assert.equal(
log[0],
'`<h1>` (.../samples/invalid-html-ssr/Component.svelte:1:0) cannot contain `<p>` (.../samples/invalid-html-ssr/main.svelte:5:0)\n\n' +
'`<h1>` (Component.svelte:1:0) cannot contain `<p>` (main.svelte:5:0)\n\n' +
'This can cause content to shift around as the browser repairs the HTML, and will likely result in a `hydration_mismatch` warning.'
);
}

@ -26,7 +26,7 @@ export default test({
assert.htmlEqual(target.innerHTML, `<button>clicks: 1</button>`);
assert.deepEqual(warnings, [
'.../samples/non-local-mutation-discouraged/Counter.svelte mutated a value owned by .../samples/non-local-mutation-discouraged/main.svelte. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead'
'Counter.svelte mutated a value owned by main.svelte. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead'
]);
}
});

@ -8,6 +8,6 @@ export default test({
},
warnings: [
'.../samples/non-local-mutation-with-binding-2/Intermediate.svelte passed a value to .../samples/non-local-mutation-with-binding-2/Counter.svelte with `bind:`, but the value is owned by .../samples/non-local-mutation-with-binding-2/main.svelte. Consider creating a binding between .../samples/non-local-mutation-with-binding-2/main.svelte and .../samples/non-local-mutation-with-binding-2/Intermediate.svelte'
'Intermediate.svelte passed a value to Counter.svelte with `bind:`, but the value is owned by main.svelte. Consider creating a binding between main.svelte and Intermediate.svelte'
]
});

@ -33,7 +33,7 @@ export default test({
assert.htmlEqual(target.innerHTML, `<button>clicks: 1</button><button>clicks: 1</button>`);
assert.deepEqual(warnings, [
'.../samples/non-local-mutation-with-binding-3/Counter.svelte mutated a value owned by .../samples/non-local-mutation-with-binding-3/main.svelte. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead'
'Counter.svelte mutated a value owned by main.svelte. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead'
]);
}
});

@ -9,5 +9,5 @@ export default test({
error:
'bind_not_bindable\n' +
'A component is attempting to bind to a non-bindable property `count` belonging to .../samples/props-not-bindable-spread/Counter.svelte (i.e. `<Counter bind:count={...}>`). To mark a property as bindable: `let { count = $bindable() } = $props()`'
'A component is attempting to bind to a non-bindable property `count` belonging to Counter.svelte (i.e. `<Counter bind:count={...}>`). To mark a property as bindable: `let { count = $bindable() } = $props()`'
});

@ -9,5 +9,5 @@ export default test({
error:
'bind_not_bindable\n' +
'A component is attempting to bind to a non-bindable property `count` belonging to .../samples/props-not-bindable/Counter.svelte (i.e. `<Counter bind:count={...}>`). To mark a property as bindable: `let { count = $bindable() } = $props()`'
'A component is attempting to bind to a non-bindable property `count` belonging to Counter.svelte (i.e. `<Counter bind:count={...}>`). To mark a property as bindable: `let { count = $bindable() } = $props()`'
});

@ -14,14 +14,14 @@ export default test({
// @ts-expect-error
assert.deepEqual(ps[0].__svelte_meta.loc, {
filename: '.../samples/svelte-meta-dynamic/main.svelte',
file: 'main.svelte',
line: 7,
column: 0
});
// @ts-expect-error
assert.deepEqual(ps[1].__svelte_meta.loc, {
filename: '.../samples/svelte-meta-dynamic/main.svelte',
file: 'main.svelte',
line: 13,
column: 0
});
@ -32,7 +32,7 @@ export default test({
// @ts-expect-error
assert.deepEqual(strong.__svelte_meta.loc, {
filename: '.../samples/svelte-meta-dynamic/main.svelte',
file: 'main.svelte',
line: 10,
column: 1
});

@ -14,14 +14,14 @@ export default test({
// @ts-expect-error
assert.deepEqual(ps[0].__svelte_meta.loc, {
filename: '.../samples/svelte-meta/main.svelte',
file: 'main.svelte',
line: 7,
column: 0
});
// @ts-expect-error
assert.deepEqual(ps[1].__svelte_meta.loc, {
filename: '.../samples/svelte-meta/main.svelte',
file: 'main.svelte',
line: 13,
column: 0
});
@ -32,7 +32,7 @@ export default test({
// @ts-expect-error
assert.deepEqual(strong.__svelte_meta.loc, {
filename: '.../samples/svelte-meta/main.svelte',
file: 'main.svelte',
line: 10,
column: 1
});

@ -728,6 +728,12 @@ declare module 'svelte/compiler' {
* Used for debugging hints and sourcemaps. Your bundler plugin will set it automatically.
*/
filename?: string;
/**
* Used for ensuring filenames don't leak filesystem information. Your bundler plugin will set it automatically.
* @default process.cwd() on node-like environments, undefined elsewhere
*/
rootDir?: string;
}
type DeclarationKind =
@ -2549,6 +2555,12 @@ declare module 'svelte/types/compiler/interfaces' {
* Used for debugging hints and sourcemaps. Your bundler plugin will set it automatically.
*/
filename?: string;
/**
* Used for ensuring filenames don't leak filesystem information. Your bundler plugin will set it automatically.
* @default process.cwd() on node-like environments, undefined elsewhere
*/
rootDir?: string;
}
/**
* - `html` the default, for e.g. `<div>` or `<span>`

Loading…
Cancel
Save