Skip to content

Commit af8ada5

Browse files
feat: Support TypeScript in the playground! (sveltejs#1221)
* typescript * checkpoint * final not final * nasty fix * convert to JSDoc * tidy up * only strip types from .ts files * only attempt to migrate .svelte files * add prettier * prettier * snake_case * small drive-by fix --------- Co-authored-by: Rich Harris <[email protected]>
1 parent 5dbae29 commit af8ada5

File tree

11 files changed

+202
-11
lines changed

11 files changed

+202
-11
lines changed

apps/svelte.dev/vite.config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ const config: UserConfig = {
7171
}
7272
},
7373
optimizeDeps: {
74-
exclude: ['@sveltejs/site-kit', '@sveltejs/repl', '@rollup/browser']
74+
exclude: ['@sveltejs/site-kit', '@sveltejs/repl', '@rollup/browser', 'typestript']
7575
},
7676
ssr: {
7777
noExternal: ['@sveltejs/site-kit', '@sveltejs/repl'],

packages/repl/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"@sveltejs/package": "^2.0.0",
6565
"@sveltejs/vite-plugin-svelte": "4.0.0",
6666
"@types/estree": "^1.0.5",
67+
"magic-string": "^0.30.11",
6768
"prettier": "^3.3.2",
6869
"prettier-plugin-svelte": "^3.3.2",
6970
"publint": "^0.2.12",
@@ -102,6 +103,7 @@
102103
"svelte": "5.23.0",
103104
"tailwindcss": "^4.0.15",
104105
"tarparser": "^0.0.4",
105-
"zimmerframe": "^1.1.2"
106+
"zimmerframe": "^1.1.2",
107+
"typestript": "workspace:*"
106108
}
107109
}

packages/repl/src/lib/Workspace.svelte.ts

+4
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,10 @@ export class Workspace {
589589
extensions.push(javascript());
590590
break;
591591

592+
case 'ts':
593+
extensions.push(javascript({ typescript: true }));
594+
break;
595+
592596
case 'html':
593597
extensions.push(html());
594598
break;

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

+8-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { walk } from 'zimmerframe';
33
import '../patch_window';
44
import { rollup } from '@rollup/browser';
55
import { DEV } from 'esm-env';
6+
import strip_types from './plugins/typescript-strip-types';
67
import commonjs from './plugins/commonjs';
78
import glsl from './plugins/glsl';
89
import json from './plugins/json';
@@ -184,7 +185,7 @@ async function get_bundle(
184185
if (importer.startsWith(VIRTUAL)) {
185186
const url = new URL(importee, importer);
186187

187-
for (const suffix of ['', '.js', '.json']) {
188+
for (const suffix of ['', '.js', '.ts', '.json']) {
188189
const with_suffix = `${url.pathname.slice(1)}${suffix}`;
189190
const file = virtual.get(with_suffix);
190191

@@ -280,15 +281,15 @@ async function get_bundle(
280281
const message = `bundling ${id.replace(VIRTUAL + '/', '').replace(NPM + '/', '')}`;
281282
self.postMessage({ type: 'status', message });
282283

283-
if (!/\.(svelte|js)$/.test(id)) return null;
284+
if (!/\.(svelte|js|ts)$/.test(id)) return null;
284285

285-
const name = id.split('/').pop()?.split('.')[0];
286+
const filename = id.split('/').pop()!;
286287

287288
let result: CompileResult;
288289

289290
if (id.endsWith('.svelte')) {
290291
const compilerOptions: any = {
291-
filename: name + '.svelte',
292+
filename,
292293
generate: Number(svelte.VERSION.split('.')[0]) >= 5 ? 'client' : 'dom',
293294
dev: true
294295
};
@@ -342,9 +343,9 @@ async function get_bundle(
342343
$$_styles.push($$__style);
343344
`.replace(/\t/g, '');
344345
}
345-
} else if (id.endsWith('.svelte.js')) {
346+
} else if (/\.svelte\.(js|ts)$/.test(id)) {
346347
const compilerOptions: any = {
347-
filename: name + '.js',
348+
filename,
348349
generate: 'client',
349350
dev: true
350351
};
@@ -387,6 +388,7 @@ async function get_bundle(
387388
input: './__entry.js',
388389
cache: previous?.key === key && previous.cache,
389390
plugins: [
391+
strip_types,
390392
repl_plugin,
391393
commonjs,
392394
json,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { Plugin } from '@rollup/browser';
2+
import { stripTypes } from 'typestript';
3+
4+
const plugin: Plugin = {
5+
name: 'typescript-strip-types',
6+
transform: (code, id) => {
7+
if (!id.endsWith('.ts')) return;
8+
9+
const s = stripTypes(code);
10+
11+
return {
12+
code: s.toString(),
13+
map: s.generateMap({ hires: true })
14+
};
15+
}
16+
};
17+
18+
export default plugin;

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

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import '@sveltejs/site-kit/polyfills';
22
import type { CompileResult } from 'svelte/compiler';
33
import type { ExposedCompilerOptions, File } from '../../Workspace.svelte';
4+
import { stripTypes } from 'typestript';
45
import { load_svelte } from '../npm';
56

67
// hack for magic-string and Svelte 4 compiler
@@ -21,7 +22,7 @@ addEventListener('message', async (event) => {
2122

2223
const { can_use_experimental_async, svelte } = (cache[version] ??= await load_svelte(version));
2324

24-
if (!file.name.endsWith('.svelte') && !svelte.compileModule) {
25+
if (!file.name.endsWith('.svelte') && !file.name.includes('.svelte.') && !svelte.compileModule) {
2526
// .svelte.js file compiled with Svelte 3/4 compiler
2627
postMessage({
2728
id,
@@ -37,7 +38,7 @@ addEventListener('message', async (event) => {
3738

3839
let migration = null;
3940

40-
if (svelte.migrate) {
41+
if (file.name.endsWith('.svelte') && svelte.migrate) {
4142
try {
4243
migration = svelte.migrate(file.contents, { filename: file.name });
4344
} catch (e) {
@@ -80,7 +81,8 @@ addEventListener('message', async (event) => {
8081
compilerOptions.experimental = { async: true };
8182
}
8283

83-
result = svelte.compileModule(file.contents, compilerOptions);
84+
const code = file.name.endsWith('.ts') ? stripTypes(file.contents).toString() : file.contents;
85+
result = svelte.compileModule(code, compilerOptions);
8486
}
8587

8688
postMessage({

packages/typestript/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist

packages/typestript/package.json

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "typestript",
3+
"private": true,
4+
"scripts": {
5+
"lint": "prettier --check .",
6+
"format": "prettier --write ."
7+
},
8+
"exports": {
9+
".": {
10+
"types": "./src/index.js",
11+
"default": "./src/index.js"
12+
}
13+
},
14+
"files": [
15+
"src"
16+
],
17+
"type": "module",
18+
"dependencies": {
19+
"@sveltejs/acorn-typescript": "^1.0.5",
20+
"acorn": "^8.11.3",
21+
"magic-string": "^0.30.11",
22+
"zimmerframe": "^1.1.2"
23+
},
24+
"devDependencies": {
25+
"@typescript-eslint/types": "^8.28.0",
26+
"prettier": "^3.3.2"
27+
}
28+
}

packages/typestript/src/index.js

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/** @import { TSESTree } from '@typescript-eslint/types' */
2+
import * as acorn from 'acorn';
3+
import { tsPlugin } from '@sveltejs/acorn-typescript';
4+
import { walk } from 'zimmerframe';
5+
import MagicString from 'magic-string';
6+
7+
const TSParser = acorn.Parser.extend(tsPlugin());
8+
9+
/**
10+
* @param {string} content
11+
* @returns {MagicString}
12+
*/
13+
export function stripTypes(content) {
14+
const ast = /** @type {unknown} */ (
15+
TSParser.parse(content, {
16+
sourceType: 'module',
17+
ecmaVersion: 13,
18+
locations: true
19+
})
20+
);
21+
22+
const s = new MagicString(content);
23+
24+
walk(/** @type {TSESTree.Node & { start: number, end: number }} */ (ast), null, {
25+
_: (node, context) => {
26+
if (
27+
node.type.startsWith('TS') &&
28+
!['TSAsExpression', 'TSSatisfiesExpression', 'TSNonNullExpression'].includes(node.type)
29+
) {
30+
const { start, end } = node;
31+
s.overwrite(start, end, ' '.repeat(end - start));
32+
} else {
33+
context.next();
34+
}
35+
},
36+
TSAsExpression: (node) => {
37+
handle_type_expression(node, s);
38+
},
39+
TSSatisfiesExpression: (node) => {
40+
handle_type_expression(node, s);
41+
},
42+
TSNonNullExpression: (node, context) => {
43+
s.overwrite(node.end - 1, node.end, ' ');
44+
context.next();
45+
},
46+
ImportDeclaration: (node, context) => {
47+
if (
48+
node.importKind === 'type' ||
49+
node.specifiers.every(
50+
(specifier) => specifier.type === 'ImportSpecifier' && specifier.importKind === 'type'
51+
)
52+
) {
53+
const { start, end } = node;
54+
s.overwrite(start, end, ' '.repeat(end - start));
55+
} else {
56+
context.next();
57+
}
58+
},
59+
ImportSpecifier: (node, context) => {
60+
if (node.importKind === 'type') {
61+
const { start, end } = node;
62+
s.overwrite(start, end, ' '.repeat(end - start));
63+
} else {
64+
context.next();
65+
}
66+
}
67+
});
68+
69+
return s;
70+
}
71+
72+
/**
73+
* @param {TSESTree.Node} node
74+
* @param {MagicString} s
75+
*/
76+
function handle_type_expression(node, s) {
77+
// @ts-ignore
78+
const start = node.expression.end;
79+
80+
// @ts-ignore
81+
const end = node.typeAnnotation.end;
82+
83+
s.overwrite(start, end, ' '.repeat(end - start));
84+
}

packages/typestript/tsconfig.json

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"compilerOptions": {
3+
"outDir": "dist",
4+
"allowJs": true,
5+
"checkJs": true,
6+
"verbatimModuleSyntax": true,
7+
"isolatedModules": true,
8+
"lib": ["esnext", "DOM", "DOM.Iterable"],
9+
"moduleResolution": "bundler",
10+
"module": "esnext",
11+
"target": "esnext",
12+
"declaration": true,
13+
"declarationMap": true
14+
},
15+
"include": ["src/**/*.ts"]
16+
}

pnpm-lock.yaml

+34
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)