Skip to content

Commit 74eee76

Browse files
authored
Strip TS types ourselves (#1376)
Instead of using `ts-blank-space` we're using custom logic that closely follows that of https://github.com/sveltejs/svelte/blob/main/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js . That way we shrink the worker bundle by about 9x.
1 parent ec7aae4 commit 74eee76

File tree

6 files changed

+273
-17
lines changed

6 files changed

+273
-17
lines changed

packages/repl/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
"@replit/codemirror-vim": "^6.0.14",
9191
"@rich_harris/svelte-split-pane": "^2.0.0",
9292
"@rollup/browser": "^4.17.2",
93+
"@sveltejs/acorn-typescript": "^1.0.0",
9394
"@sveltejs/site-kit": "workspace:*",
9495
"@sveltejs/svelte-json-tree": "^2.2.1",
9596
"acorn": "^8.11.3",
@@ -98,12 +99,12 @@
9899
"esrap": "^1.2.2",
99100
"icons": "workspace:*",
100101
"locate-character": "^3.0.0",
102+
"magic-string": "^0.30.0",
101103
"marked": "^14.1.2",
102104
"resolve.exports": "^2.0.2",
103105
"svelte": "5.33.0",
104106
"tailwindcss": "^4.0.15",
105107
"tarparser": "^0.0.4",
106-
"ts-blank-space": "^0.6.1",
107108
"zimmerframe": "^1.1.2"
108109
}
109110
}

packages/repl/src/lib/workers/bundler/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { walk } from 'zimmerframe';
33
import '../patch_window';
44
import { rollup } from '@rollup/browser';
55
import { DEV } from 'esm-env';
6-
import typescript_strip_types from './plugins/typescript-strip-types';
6+
import typescript_strip_types from './plugins/typescript';
77
import commonjs from './plugins/commonjs';
88
import glsl from './plugins/glsl';
99
import json from './plugins/json';

packages/repl/src/lib/workers/bundler/plugins/typescript-strip-types.ts renamed to packages/repl/src/lib/workers/bundler/plugins/typescript.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import { strip_types } from '../../typescript-strip-types';
12
import type { Plugin } from '@rollup/browser';
2-
import tsBlankSpace from 'ts-blank-space';
33

44
const plugin: Plugin = {
55
name: 'typescript-strip-types',
@@ -8,7 +8,7 @@ const plugin: Plugin = {
88
if (!match) return;
99

1010
return {
11-
code: tsBlankSpace(code)
11+
code: strip_types(code)
1212
};
1313
}
1414
};

packages/repl/src/lib/workers/compiler/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import '@sveltejs/site-kit/polyfills';
22
import type { CompileResult } from 'svelte/compiler';
3-
import tsBlankSpace from 'ts-blank-space';
43
import type { ExposedCompilerOptions, File } from '../../Workspace.svelte';
54
import { load_svelte } from '../npm';
5+
import { strip_types } from '../typescript-strip-types';
66

77
// hack for magic-string and Svelte 4 compiler
88
// do not put this into a separate module and import it, would be treeshaken in prod
@@ -88,7 +88,7 @@ addEventListener('message', async (event) => {
8888
compilerOptions.experimental = { async: true };
8989
}
9090

91-
const content = tsBlankSpace(file.contents);
91+
const content = file.basename.endsWith('.ts') ? strip_types(file.contents) : file.contents;
9292
result = svelte.compileModule(content, compilerOptions);
9393
}
9494

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import * as acorn from 'acorn';
2+
import { walk, type Context, type Visitors } from 'zimmerframe';
3+
import { tsPlugin } from '@sveltejs/acorn-typescript';
4+
import MagicString from 'magic-string';
5+
6+
const ParserWithTS = acorn.Parser.extend(tsPlugin());
7+
8+
/**
9+
* @param {FunctionExpression | FunctionDeclaration} node
10+
* @param {Context<any, any>} context
11+
*/
12+
function remove_this_param(
13+
node: acorn.FunctionExpression | acorn.FunctionDeclaration,
14+
context: Context<any, any>
15+
) {
16+
const param = node.params[0] as any;
17+
if (param?.type === 'Identifier' && param.name === 'this') {
18+
if (param.typeAnnotation) {
19+
// the type annotation is blanked by another visitor, do it in two parts to prevent an overwrite error
20+
ts_blank_space(context, { start: param.start, end: param.typeAnnotation.start });
21+
ts_blank_space(context, {
22+
start: param.typeAnnotation.end,
23+
end: node.params[1]?.start || param.end
24+
});
25+
} else {
26+
ts_blank_space(context, {
27+
start: param.start,
28+
end: node.params[1]?.start || param.end
29+
});
30+
}
31+
}
32+
return context.next();
33+
}
34+
35+
function typescript_invalid_feature(node: any, feature: string) {
36+
const e = new Error(`The REPL does not support ${feature}. Please remove it from your code.`);
37+
// @ts-expect-error Our REPL error handling needs this
38+
e.position = [node.start, node.end];
39+
throw e;
40+
}
41+
42+
const empty = {
43+
type: 'EmptyStatement'
44+
};
45+
46+
function ts_blank_space(context: Context<any, { ms: MagicString }>, node: any): void {
47+
const { start, end } = node;
48+
let i = start;
49+
while (i < end) {
50+
// Skip whitespace
51+
while (i < end && /\s/.test(context.state.ms.original[i])) i++;
52+
if (i >= end) break;
53+
// Find next whitespace or end
54+
let j = i + 1;
55+
while (j < end && !/\s/.test(context.state.ms.original[j])) j++;
56+
context.state.ms.overwrite(i, j, ' '.repeat(j - i));
57+
i = j;
58+
}
59+
}
60+
61+
const visitors: Visitors<any, { ms: MagicString }> = {
62+
_(node, context) {
63+
if (node.typeAnnotation) ts_blank_space(context, node.typeAnnotation);
64+
if (node.typeParameters) ts_blank_space(context, node.typeParameters);
65+
if (node.typeArguments) ts_blank_space(context, node.typeArguments);
66+
if (node.returnType) ts_blank_space(context, node.returnType);
67+
if (node.accessibility) {
68+
ts_blank_space(context, { start: node.start, end: node.start + node.accessibility.length });
69+
}
70+
71+
context.next();
72+
},
73+
Decorator(node, context) {
74+
ts_blank_space(context, node);
75+
},
76+
ImportDeclaration(node, context) {
77+
if (node.importKind === 'type') {
78+
ts_blank_space(context, node);
79+
return empty;
80+
}
81+
82+
if (node.specifiers?.length > 0) {
83+
const specifiers = node.specifiers.filter((s: any, i: number) => {
84+
if (s.importKind !== 'type') return true;
85+
86+
ts_blank_space(context, {
87+
start: s.start,
88+
end: node.specifiers[i + 1]?.start || s.end
89+
});
90+
});
91+
92+
if (specifiers.length === 0) {
93+
ts_blank_space(context, node);
94+
}
95+
}
96+
},
97+
ExportNamedDeclaration(node, context) {
98+
if (node.exportKind === 'type') {
99+
ts_blank_space(context, node);
100+
return empty;
101+
}
102+
103+
if (node.declaration) {
104+
const result = context.next();
105+
if (result?.declaration?.type === 'EmptyStatement') {
106+
ts_blank_space(context, node);
107+
return empty;
108+
}
109+
return result;
110+
}
111+
112+
if (node.specifiers) {
113+
const specifiers = node.specifiers.filter((s: any, i: number) => {
114+
if (s.exportKind !== 'type') return true;
115+
116+
ts_blank_space(context, {
117+
start: s.start,
118+
end: node.specifiers[i + 1]?.start || s.end
119+
});
120+
});
121+
122+
if (specifiers.length === 0) {
123+
ts_blank_space(context, node);
124+
}
125+
return;
126+
}
127+
},
128+
ExportDefaultDeclaration(node, context) {
129+
if (node.exportKind === 'type') {
130+
ts_blank_space(context, node);
131+
return empty;
132+
} else {
133+
context.next();
134+
}
135+
},
136+
ExportAllDeclaration(node, context) {
137+
if (node.exportKind === 'type') {
138+
ts_blank_space(context, node);
139+
return empty;
140+
} else {
141+
context.next();
142+
}
143+
},
144+
PropertyDefinition(node, context) {
145+
if (node.accessor) {
146+
typescript_invalid_feature(node, 'accessor fields (related TSC proposal is not stage 4 yet)');
147+
} else {
148+
context.next();
149+
}
150+
},
151+
TSAsExpression(node, context) {
152+
ts_blank_space(context, { start: node.expression.end, end: node.end });
153+
context.visit(node.expression);
154+
},
155+
TSSatisfiesExpression(node, context) {
156+
ts_blank_space(context, { start: node.expression.end, end: node.end });
157+
context.visit(node.expression);
158+
},
159+
TSNonNullExpression(node, context) {
160+
ts_blank_space(context, { start: node.expression.end, end: node.end });
161+
context.visit(node.expression);
162+
},
163+
TSInterfaceDeclaration(node, context) {
164+
ts_blank_space(context, node);
165+
return empty;
166+
},
167+
TSTypeAliasDeclaration(node, context) {
168+
ts_blank_space(context, node);
169+
return empty;
170+
},
171+
TSTypeAssertion(node, context) {
172+
ts_blank_space(context, { start: node.start, end: node.expression.start });
173+
context.visit(node.expression);
174+
},
175+
TSEnumDeclaration(node, context) {
176+
typescript_invalid_feature(node, 'enums');
177+
},
178+
TSParameterProperty(node, context) {
179+
if ((node.readonly || node.accessibility) && context.path.at(-2)?.kind === 'constructor') {
180+
typescript_invalid_feature(node, 'accessibility modifiers on constructor parameters');
181+
}
182+
ts_blank_space(context, { start: node.start, end: node.parameter.start });
183+
context.visit(node.parameter);
184+
},
185+
TSInstantiationExpression(node, context) {
186+
ts_blank_space(context, { start: node.start, end: node.expression.start });
187+
context.visit(node.expression);
188+
},
189+
FunctionExpression: remove_this_param,
190+
FunctionDeclaration: remove_this_param,
191+
TSDeclareFunction(node, context) {
192+
ts_blank_space(context, node);
193+
return empty;
194+
},
195+
ClassDeclaration(node, context) {
196+
if (node.declare || node.abstract) {
197+
ts_blank_space(context, node);
198+
return empty;
199+
}
200+
201+
if (node.implements?.length) {
202+
const implements_keyword_start = context.state.ms.original.lastIndexOf(
203+
'implements',
204+
node.implements[0].start
205+
);
206+
ts_blank_space(context, {
207+
start: implements_keyword_start,
208+
end: node.implements[node.implements.length - 1].end
209+
});
210+
}
211+
context.next();
212+
},
213+
MethodDefinition(node, context) {
214+
if (node.abstract) {
215+
ts_blank_space(context, { start: node.start, end: node.start + 'abstract'.length });
216+
return empty;
217+
}
218+
context.next();
219+
},
220+
VariableDeclaration(node, context) {
221+
if (node.declare) {
222+
ts_blank_space(context, node);
223+
return empty;
224+
}
225+
context.next();
226+
},
227+
TSModuleDeclaration(node, context) {
228+
if (!node.body) {
229+
ts_blank_space(context, node);
230+
return;
231+
}
232+
// namespaces can contain non-type nodes
233+
const cleaned = node.body.body.map((entry) => context.visit(entry));
234+
if (cleaned.some((entry) => entry !== empty)) {
235+
typescript_invalid_feature(node, 'namespaces with non-type nodes');
236+
}
237+
ts_blank_space(context, node);
238+
}
239+
};
240+
241+
/**
242+
* Strips type-only constructs from TypeScript code and replaces them with blank spaces.
243+
* Errors on non-type constructs that are not supported in the REPL.
244+
*
245+
* This implementation closely follows the logic of https://github.com/sveltejs/svelte/blob/main/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js
246+
*
247+
* Used instead of`ts-blank-space` because the latter means we need to bundle all of TypeScript, which increases the worker bundles by 9x.
248+
*/
249+
export function strip_types(code: string): string {
250+
const ms = new MagicString(code);
251+
const ast = ParserWithTS.parse(code, {
252+
sourceType: 'module',
253+
ecmaVersion: 16,
254+
locations: true
255+
});
256+
257+
walk(ast, { ms }, visitors);
258+
259+
return ms.toString();
260+
}

pnpm-lock.yaml

Lines changed: 6 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)