Skip to content

Commit 7e84b23

Browse files
committed
chore: runtime linting
- check that the runtime doesn't use methods that are too new - add linting rule to prevent references to the compiler in the runtime (this is important for the first check, else the ambient node typings would be included, which includes a definition for `at()`, which means we no longer would get errors when violating the "don't use new methods" rule in the runtime) - fix code as a result of these new checks closes #10438
1 parent 831552f commit 7e84b23

File tree

13 files changed

+145
-88
lines changed

13 files changed

+145
-88
lines changed

eslint.config.js

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,38 @@
11
import svelte_config from '@sveltejs/eslint-config';
22
import lube from 'eslint-plugin-lube';
33

4+
const no_compiler_imports = {
5+
meta: {
6+
type: /** @type {const} */ ('problem'),
7+
docs: {
8+
description:
9+
'Enforce that there are no imports to the compiler in runtime code. ' +
10+
'This prevent accidental inclusion of the compiler runtime and ' +
11+
"ensures that TypeScript does not pick up more ambient types (for example from Node) that shouldn't be available in the browser."
12+
}
13+
},
14+
create(context) {
15+
return {
16+
Program: () => {
17+
// Do a simple string search because ESLint doesn't provide a way to check JSDoc comments.
18+
// The string search could in theory yielf false positives, but in practice it's unlikely.
19+
const text = context.sourceCode.getText();
20+
const idx = Math.max(text.indexOf('../compiler/'), text.indexOf('#compiler'));
21+
if (idx !== -1) {
22+
context.report({
23+
loc: {
24+
start: context.sourceCode.getLocFromIndex(idx),
25+
end: context.sourceCode.getLocFromIndex(idx + 12)
26+
},
27+
message:
28+
'References to compiler code are forbidden in runtime code (both for type and value imports)'
29+
});
30+
}
31+
}
32+
};
33+
}
34+
};
35+
436
/** @type {import('eslint').Linter.FlatConfig[]} */
537
export default [
638
...svelte_config,
@@ -11,7 +43,8 @@ export default [
1143
}
1244
},
1345
plugins: {
14-
lube
46+
lube,
47+
custom: { rules: { no_compiler_imports } }
1548
},
1649
rules: {
1750
'@typescript-eslint/await-thenable': 'error',
@@ -37,6 +70,13 @@ export default [
3770
'no-console': 'off'
3871
}
3972
},
73+
{
74+
files: ['packages/svelte/src/**/*'],
75+
ignores: ['packages/svelte/src/compiler/**/*'],
76+
rules: {
77+
'custom/no_compiler_imports': 'error'
78+
}
79+
},
4080
{
4181
ignores: [
4282
'**/*.d.ts',

packages/svelte/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@
106106
"scripts": {
107107
"build": "node scripts/process-messages && rollup -c && pnpm generate:types && node scripts/check-treeshakeability.js",
108108
"dev": "node scripts/process-messages && rollup -cw",
109-
"check": "tsc && cd ./tests/types && tsc",
109+
"check": "tsc --project tsconfig.runtime.json && tsc && cd ./tests/types && tsc",
110110
"check:watch": "tsc --watch",
111111
"generate:version": "node ./scripts/generate-version.js",
112112
"generate:types": "node ./scripts/generate-types.js && tsc -p tsconfig.generated.json",

packages/svelte/src/compiler/phases/1-parse/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { isIdentifierStart, isIdentifierChar } from 'acorn';
44
import fragment from './state/fragment.js';
55
import { regex_whitespace } from '../patterns.js';
6-
import { reserved } from './utils/names.js';
6+
import { reserved } from '../../../constants.js';
77
import full_char_code_at from './utils/full_char_code_at.js';
88
import * as e from '../../errors.js';
99
import { create_fragment } from './utils/create.js';

packages/svelte/src/compiler/phases/1-parse/state/element.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/** @import { Parser } from '../index.js' */
22
/** @import * as Compiler from '#compiler' */
3-
import { is_void } from '../utils/names.js';
3+
import { is_void } from '../../../../constants.js';
44
import read_expression from '../read/expression.js';
55
import { read_script } from '../read/script.js';
66
import read_style from '../read/style.js';

packages/svelte/src/compiler/phases/1-parse/utils/names.js

Lines changed: 0 additions & 74 deletions
This file was deleted.

packages/svelte/src/compiler/phases/3-transform/client/types.d.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type {
88
import type { Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler';
99
import type { TransformState } from '../types.js';
1010
import type { ComponentAnalysis } from '../../types.js';
11-
import type { Location } from 'locate-character';
11+
import type { SourceLocation } from '#shared';
1212

1313
export interface ClientTransformState extends TransformState {
1414
readonly private_state: Map<string, StateField>;
@@ -24,10 +24,6 @@ export interface ClientTransformState extends TransformState {
2424
readonly legacy_reactive_statements: Map<LabeledStatement, Statement>;
2525
}
2626

27-
export type SourceLocation =
28-
| [line: number, column: number]
29-
| [line: number, column: number, SourceLocation[]];
30-
3127
export interface ComponentClientTransformState extends ClientTransformState {
3228
readonly analysis: ComponentAnalysis;
3329
readonly options: ValidatedCompileOptions;

packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,7 +1051,7 @@ function serialize_bind_this(bind_this, context, node) {
10511051
}
10521052

10531053
/**
1054-
* @param {import('../types.js').SourceLocation[]} locations
1054+
* @param {import('#shared').SourceLocation[]} locations
10551055
*/
10561056
function serialize_locations(locations) {
10571057
return b.array(
@@ -1905,7 +1905,7 @@ export const template_visitors = {
19051905
state.init.push(b.stmt(b.call('$.transition', ...args)));
19061906
},
19071907
RegularElement(node, context) {
1908-
/** @type {import('../types.js').SourceLocation} */
1908+
/** @type {import('#shared').SourceLocation} */
19091909
let location = [-1, -1];
19101910

19111911
if (context.state.options.dev) {
@@ -2133,7 +2133,7 @@ export const template_visitors = {
21332133

21342134
context.state.template.push('>');
21352135

2136-
/** @type {import('../types.js').SourceLocation[]} */
2136+
/** @type {import('#shared').SourceLocation[]} */
21372137
const child_locations = [];
21382138

21392139
/** @type {import('../types').ComponentClientTransformState} */

packages/svelte/src/constants.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,78 @@ export function is_capture_event(name, mode = 'exclude-on') {
293293
? name !== 'gotpointercapture' && name !== 'lostpointercapture'
294294
: name !== 'ongotpointercapture' && name !== 'onlostpointercapture';
295295
}
296+
297+
export const reserved = [
298+
'arguments',
299+
'await',
300+
'break',
301+
'case',
302+
'catch',
303+
'class',
304+
'const',
305+
'continue',
306+
'debugger',
307+
'default',
308+
'delete',
309+
'do',
310+
'else',
311+
'enum',
312+
'eval',
313+
'export',
314+
'extends',
315+
'false',
316+
'finally',
317+
'for',
318+
'function',
319+
'if',
320+
'implements',
321+
'import',
322+
'in',
323+
'instanceof',
324+
'interface',
325+
'let',
326+
'new',
327+
'null',
328+
'package',
329+
'private',
330+
'protected',
331+
'public',
332+
'return',
333+
'static',
334+
'super',
335+
'switch',
336+
'this',
337+
'throw',
338+
'true',
339+
'try',
340+
'typeof',
341+
'var',
342+
'void',
343+
'while',
344+
'with',
345+
'yield'
346+
];
347+
348+
const void_element_names = [
349+
'area',
350+
'base',
351+
'br',
352+
'col',
353+
'command',
354+
'embed',
355+
'hr',
356+
'img',
357+
'input',
358+
'keygen',
359+
'link',
360+
'meta',
361+
'param',
362+
'source',
363+
'track',
364+
'wbr'
365+
];
366+
367+
/** @param {string} name */
368+
export function is_void(name) {
369+
return void_element_names.includes(name) || name.toLowerCase() === '!doctype';
370+
}

packages/svelte/src/internal/client/dev/elements.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { SourceLocation } from '../../../compiler/phases/3-transform/client/types.js' */
1+
/** @import { SourceLocation } from '#shared' */
22
import { HYDRATION_END, HYDRATION_START } from '../../../constants.js';
33
import { hydrating } from '../dom/hydration.js';
44

packages/svelte/src/internal/client/runtime.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ function handle_error(error, effect, component_context) {
224224
let current_context = component_context;
225225

226226
while (current_context !== null) {
227+
/** @type {string} */
227228
var filename = current_context.function?.filename;
228229

229230
if (filename) {

packages/svelte/src/internal/shared/types.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@ export type Store<V> = {
22
subscribe: (run: (value: V) => void) => () => void;
33
set(value: V): void;
44
};
5+
6+
export type SourceLocation =
7+
| [line: number, column: number]
8+
| [line: number, column: number, SourceLocation[]];

packages/svelte/src/internal/shared/validate.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { is_void } from '../../compiler/phases/1-parse/utils/names.js';
1+
import { is_void } from '../../constants.js';
22
import * as w from './warnings.js';
33
import * as e from './errors.js';
44

packages/svelte/tsconfig.runtime.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
// Ensure we don't use any methods that are not available in all browser for a long period of time
5+
// so that people don't need to polyfill them. Example array.at(...) was introduced to Safari only in 2022
6+
"target": "es2021",
7+
"lib": ["es2021", "DOM", "DOM.Iterable"],
8+
"types": [] // prevent automatic inclusion of @types/node
9+
},
10+
"include": ["./src/"],
11+
// Compiler is allowed to use more recent methods; people using it in the browser are expected to know
12+
// how to polyfill missing methods. Also make sure to not include test files as these include Vitest
13+
// which then loads node globals.
14+
"exclude": ["./src/compiler/**/*", "./src/**/*.test.ts"]
15+
}

0 commit comments

Comments
 (0)