diff --git a/apps/svelte.dev/vite.config.ts b/apps/svelte.dev/vite.config.ts index ba222b1ffd..a2295cbf4d 100644 --- a/apps/svelte.dev/vite.config.ts +++ b/apps/svelte.dev/vite.config.ts @@ -71,7 +71,7 @@ const config: UserConfig = { } }, optimizeDeps: { - exclude: ['@sveltejs/site-kit', '@sveltejs/repl', '@rollup/browser'] + exclude: ['@sveltejs/site-kit', '@sveltejs/repl', '@rollup/browser', 'typestript'] }, ssr: { noExternal: ['@sveltejs/site-kit', '@sveltejs/repl'], diff --git a/packages/repl/package.json b/packages/repl/package.json index 6a40fe14de..9706f32a27 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -64,6 +64,7 @@ "@sveltejs/package": "^2.0.0", "@sveltejs/vite-plugin-svelte": "4.0.0", "@types/estree": "^1.0.5", + "magic-string": "^0.30.11", "prettier": "^3.3.2", "prettier-plugin-svelte": "^3.3.2", "publint": "^0.2.12", @@ -102,6 +103,7 @@ "svelte": "5.23.0", "tailwindcss": "^4.0.15", "tarparser": "^0.0.4", - "zimmerframe": "^1.1.2" + "zimmerframe": "^1.1.2", + "typestript": "workspace:*" } } diff --git a/packages/repl/src/lib/Workspace.svelte.ts b/packages/repl/src/lib/Workspace.svelte.ts index f1af3a5837..b210539428 100644 --- a/packages/repl/src/lib/Workspace.svelte.ts +++ b/packages/repl/src/lib/Workspace.svelte.ts @@ -589,6 +589,10 @@ export class Workspace { extensions.push(javascript()); break; + case 'ts': + extensions.push(javascript({ typescript: true })); + break; + case 'html': extensions.push(html()); break; diff --git a/packages/repl/src/lib/workers/bundler/index.ts b/packages/repl/src/lib/workers/bundler/index.ts index 9c5d29b3e6..ed37a38065 100644 --- a/packages/repl/src/lib/workers/bundler/index.ts +++ b/packages/repl/src/lib/workers/bundler/index.ts @@ -3,6 +3,7 @@ import { walk } from 'zimmerframe'; import '../patch_window'; import { rollup } from '@rollup/browser'; import { DEV } from 'esm-env'; +import strip_types from './plugins/typescript-strip-types'; import commonjs from './plugins/commonjs'; import glsl from './plugins/glsl'; import json from './plugins/json'; @@ -184,7 +185,7 @@ async function get_bundle( if (importer.startsWith(VIRTUAL)) { const url = new URL(importee, importer); - for (const suffix of ['', '.js', '.json']) { + for (const suffix of ['', '.js', '.ts', '.json']) { const with_suffix = `${url.pathname.slice(1)}${suffix}`; const file = virtual.get(with_suffix); @@ -280,15 +281,15 @@ async function get_bundle( const message = `bundling ${id.replace(VIRTUAL + '/', '').replace(NPM + '/', '')}`; self.postMessage({ type: 'status', message }); - if (!/\.(svelte|js)$/.test(id)) return null; + if (!/\.(svelte|js|ts)$/.test(id)) return null; - const name = id.split('/').pop()?.split('.')[0]; + const filename = id.split('/').pop()!; let result: CompileResult; if (id.endsWith('.svelte')) { const compilerOptions: any = { - filename: name + '.svelte', + filename, generate: Number(svelte.VERSION.split('.')[0]) >= 5 ? 'client' : 'dom', dev: true }; @@ -342,9 +343,9 @@ async function get_bundle( $$_styles.push($$__style); `.replace(/\t/g, ''); } - } else if (id.endsWith('.svelte.js')) { + } else if (/\.svelte\.(js|ts)$/.test(id)) { const compilerOptions: any = { - filename: name + '.js', + filename, generate: 'client', dev: true }; @@ -387,6 +388,7 @@ async function get_bundle( input: './__entry.js', cache: previous?.key === key && previous.cache, plugins: [ + strip_types, repl_plugin, commonjs, json, diff --git a/packages/repl/src/lib/workers/bundler/plugins/typescript-strip-types.ts b/packages/repl/src/lib/workers/bundler/plugins/typescript-strip-types.ts new file mode 100644 index 0000000000..7499203829 --- /dev/null +++ b/packages/repl/src/lib/workers/bundler/plugins/typescript-strip-types.ts @@ -0,0 +1,18 @@ +import type { Plugin } from '@rollup/browser'; +import { stripTypes } from 'typestript'; + +const plugin: Plugin = { + name: 'typescript-strip-types', + transform: (code, id) => { + if (!id.endsWith('.ts')) return; + + const s = stripTypes(code); + + return { + code: s.toString(), + map: s.generateMap({ hires: true }) + }; + } +}; + +export default plugin; diff --git a/packages/repl/src/lib/workers/compiler/index.ts b/packages/repl/src/lib/workers/compiler/index.ts index e0cdc4e9b6..7c13626a97 100644 --- a/packages/repl/src/lib/workers/compiler/index.ts +++ b/packages/repl/src/lib/workers/compiler/index.ts @@ -1,6 +1,7 @@ import '@sveltejs/site-kit/polyfills'; import type { CompileResult } from 'svelte/compiler'; import type { ExposedCompilerOptions, File } from '../../Workspace.svelte'; +import { stripTypes } from 'typestript'; import { load_svelte } from '../npm'; // hack for magic-string and Svelte 4 compiler @@ -21,7 +22,7 @@ addEventListener('message', async (event) => { const { can_use_experimental_async, svelte } = (cache[version] ??= await load_svelte(version)); - if (!file.name.endsWith('.svelte') && !svelte.compileModule) { + if (!file.name.endsWith('.svelte') && !file.name.includes('.svelte.') && !svelte.compileModule) { // .svelte.js file compiled with Svelte 3/4 compiler postMessage({ id, @@ -37,7 +38,7 @@ addEventListener('message', async (event) => { let migration = null; - if (svelte.migrate) { + if (file.name.endsWith('.svelte') && svelte.migrate) { try { migration = svelte.migrate(file.contents, { filename: file.name }); } catch (e) { @@ -80,7 +81,8 @@ addEventListener('message', async (event) => { compilerOptions.experimental = { async: true }; } - result = svelte.compileModule(file.contents, compilerOptions); + const code = file.name.endsWith('.ts') ? stripTypes(file.contents).toString() : file.contents; + result = svelte.compileModule(code, compilerOptions); } postMessage({ diff --git a/packages/typestript/.gitignore b/packages/typestript/.gitignore new file mode 100644 index 0000000000..53c37a1660 --- /dev/null +++ b/packages/typestript/.gitignore @@ -0,0 +1 @@ +dist \ No newline at end of file diff --git a/packages/typestript/package.json b/packages/typestript/package.json new file mode 100644 index 0000000000..fe56c2ebd0 --- /dev/null +++ b/packages/typestript/package.json @@ -0,0 +1,28 @@ +{ + "name": "typestript", + "private": true, + "scripts": { + "lint": "prettier --check .", + "format": "prettier --write ." + }, + "exports": { + ".": { + "types": "./src/index.js", + "default": "./src/index.js" + } + }, + "files": [ + "src" + ], + "type": "module", + "dependencies": { + "@sveltejs/acorn-typescript": "^1.0.5", + "acorn": "^8.11.3", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "devDependencies": { + "@typescript-eslint/types": "^8.28.0", + "prettier": "^3.3.2" + } +} diff --git a/packages/typestript/src/index.js b/packages/typestript/src/index.js new file mode 100644 index 0000000000..272fcd27f5 --- /dev/null +++ b/packages/typestript/src/index.js @@ -0,0 +1,84 @@ +/** @import { TSESTree } from '@typescript-eslint/types' */ +import * as acorn from 'acorn'; +import { tsPlugin } from '@sveltejs/acorn-typescript'; +import { walk } from 'zimmerframe'; +import MagicString from 'magic-string'; + +const TSParser = acorn.Parser.extend(tsPlugin()); + +/** + * @param {string} content + * @returns {MagicString} + */ +export function stripTypes(content) { + const ast = /** @type {unknown} */ ( + TSParser.parse(content, { + sourceType: 'module', + ecmaVersion: 13, + locations: true + }) + ); + + const s = new MagicString(content); + + walk(/** @type {TSESTree.Node & { start: number, end: number }} */ (ast), null, { + _: (node, context) => { + if ( + node.type.startsWith('TS') && + !['TSAsExpression', 'TSSatisfiesExpression', 'TSNonNullExpression'].includes(node.type) + ) { + const { start, end } = node; + s.overwrite(start, end, ' '.repeat(end - start)); + } else { + context.next(); + } + }, + TSAsExpression: (node) => { + handle_type_expression(node, s); + }, + TSSatisfiesExpression: (node) => { + handle_type_expression(node, s); + }, + TSNonNullExpression: (node, context) => { + s.overwrite(node.end - 1, node.end, ' '); + context.next(); + }, + ImportDeclaration: (node, context) => { + if ( + node.importKind === 'type' || + node.specifiers.every( + (specifier) => specifier.type === 'ImportSpecifier' && specifier.importKind === 'type' + ) + ) { + const { start, end } = node; + s.overwrite(start, end, ' '.repeat(end - start)); + } else { + context.next(); + } + }, + ImportSpecifier: (node, context) => { + if (node.importKind === 'type') { + const { start, end } = node; + s.overwrite(start, end, ' '.repeat(end - start)); + } else { + context.next(); + } + } + }); + + return s; +} + +/** + * @param {TSESTree.Node} node + * @param {MagicString} s + */ +function handle_type_expression(node, s) { + // @ts-ignore + const start = node.expression.end; + + // @ts-ignore + const end = node.typeAnnotation.end; + + s.overwrite(start, end, ' '.repeat(end - start)); +} diff --git a/packages/typestript/tsconfig.json b/packages/typestript/tsconfig.json new file mode 100644 index 0000000000..3b16fea544 --- /dev/null +++ b/packages/typestript/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "outDir": "dist", + "allowJs": true, + "checkJs": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "lib": ["esnext", "DOM", "DOM.Iterable"], + "moduleResolution": "bundler", + "module": "esnext", + "target": "esnext", + "declaration": true, + "declarationMap": true + }, + "include": ["src/**/*.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 96b73e5354..67b24c894e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -294,6 +294,9 @@ importers: tarparser: specifier: ^0.0.4 version: 0.0.4 + typestript: + specifier: workspace:* + version: link:../typestript zimmerframe: specifier: ^1.1.2 version: 1.1.2 @@ -316,6 +319,9 @@ importers: '@types/estree': specifier: ^1.0.5 version: 1.0.6 + magic-string: + specifier: ^0.30.11 + version: 0.30.12 prettier: specifier: ^3.3.2 version: 3.3.2 @@ -435,6 +441,28 @@ importers: specifier: ^5.4.3 version: 5.4.11(@types/node@20.14.2)(lightningcss@1.25.1) + packages/typestript: + dependencies: + '@sveltejs/acorn-typescript': + specifier: ^1.0.5 + version: 1.0.5(acorn@8.14.1) + acorn: + specifier: ^8.11.3 + version: 8.14.1 + magic-string: + specifier: ^0.30.11 + version: 0.30.12 + zimmerframe: + specifier: ^1.1.2 + version: 1.1.2 + devDependencies: + '@typescript-eslint/types': + specifier: ^8.28.0 + version: 8.28.0 + prettier: + specifier: ^3.3.2 + version: 3.3.2 + packages: '@ampproject/remapping@2.3.0': @@ -1678,6 +1706,10 @@ packages: '@types/ws@8.5.10': resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} + '@typescript-eslint/types@8.28.0': + resolution: {integrity: sha512-bn4WS1bkKEjx7HqiwG2JNB3YJdC1q6Ue7GyGlwPHyt0TnVq6TtD/hiOdTZt71sq0s7UzqBFXD8t8o2e63tXgwA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript/vfs@1.6.0': resolution: {integrity: sha512-hvJUjNVeBMp77qPINuUvYXj4FyWeeMMKZkxEATEU3hqBAQ7qdTBCUFT7Sp0Zu0faeEtFf+ldXxMEDr/bk73ISg==} peerDependencies: @@ -4646,6 +4678,8 @@ snapshots: dependencies: '@types/node': 20.14.2 + '@typescript-eslint/types@8.28.0': {} + '@typescript/vfs@1.6.0(typescript@5.5.4)': dependencies: debug: 4.4.0