Skip to content

Commit

Permalink
fix: error at compile time on unsupported TypeScript language features (
Browse files Browse the repository at this point in the history
#12982)

part of #11502 - to close it completely, we also need to look at using and possibly implement heuristics within bundler plugins to give more details

---------

Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
  • Loading branch information
Rich-Harris and dummdidumm authored Sep 3, 2024
1 parent 81b32d8 commit 194570d
Show file tree
Hide file tree
Showing 16 changed files with 184 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/perfect-cooks-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: error at compile time on unsupported TypeScript language features
4 changes: 4 additions & 0 deletions packages/svelte/messages/compile-errors/script.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,7 @@
> Cannot reference store value outside a `.svelte` file
Using a `$` prefix to refer to the value of a store is only possible inside `.svelte` files, where Svelte can automatically create subscriptions when a component is mounted and unsubscribe when the component is unmounted. Consider migrating to runes instead.

## typescript_invalid_feature

> TypeScript language features like %feature% are not natively supported, and their use is generally discouraged. Outside of `<script>` tags, these features are not supported. For use within `<script>` tags, you will need to use a preprocessor to convert it to JavaScript before it gets passed to the Svelte compiler. If you are using `vitePreprocess`, make sure to specifically enable preprocessing script tags (`vitePreprocess({ script: true })`)
10 changes: 10 additions & 0 deletions packages/svelte/src/compiler/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,16 @@ export function store_invalid_subscription_module(node) {
e(node, "store_invalid_subscription_module", "Cannot reference store value outside a `.svelte` file");
}

/**
* TypeScript language features like %feature% are not natively supported, and their use is generally discouraged. Outside of `<script>` tags, these features are not supported. For use within `<script>` tags, you will need to use a preprocessor to convert it to JavaScript before it gets passed to the Svelte compiler. If you are using `vitePreprocess`, make sure to specifically enable preprocessing script tags (`vitePreprocess({ script: true })`)
* @param {null | number | NodeLike} node
* @param {string} feature
* @returns {never}
*/
export function typescript_invalid_feature(node, feature) {
e(node, "typescript_invalid_feature", `TypeScript language features like ${feature} are not natively supported, and their use is generally discouraged. Outside of \`<script>\` tags, these features are not supported. For use within \`<script>\` tags, you will need to use a preprocessor to convert it to JavaScript before it gets passed to the Svelte compiler. If you are using \`vitePreprocess\`, make sure to specifically enable preprocessing script tags (\`vitePreprocess({ script: true })\`)`);
}

/**
* Declaration cannot be empty
* @param {null | number | NodeLike} node
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/** @import { FunctionExpression, FunctionDeclaration } from 'estree' */
import { walk } from 'zimmerframe';
import * as b from '../../utils/builders.js';
import * as e from '../../errors.js';

/**
* @param {FunctionExpression | FunctionDeclaration} node
Expand All @@ -16,6 +17,9 @@ function remove_this_param(node, context) {

/** @type {Visitors<any, null>} */
const visitors = {
Decorator(node) {
e.typescript_invalid_feature(node, 'decorators (related TSC proposal is not stage 4 yet)');
},
ImportDeclaration(node) {
if (node.importKind === 'type') return b.empty;

Expand Down Expand Up @@ -52,6 +56,14 @@ const visitors = {
if (node.exportKind === 'type') return b.empty;
return node;
},
PropertyDefinition(node) {
if (node.accessor) {
e.typescript_invalid_feature(
node,
'accessor fields (related TSC proposal is not stage 4 yet)'
);
}
},
TSAsExpression(node, context) {
return context.visit(node.expression);
},
Expand All @@ -73,10 +85,13 @@ const visitors = {
TSTypeParameterInstantiation() {
return b.empty;
},
TSEnumDeclaration() {
return b.empty;
TSEnumDeclaration(node) {
e.typescript_invalid_feature(node, 'enums');
},
TSParameterProperty(node) {
TSParameterProperty(node, context) {
if (node.accessibility && context.path.at(-2)?.kind === 'constructor') {
e.typescript_invalid_feature(node, 'accessibility modifiers on constructor parameters');
}
return node.parameter;
},
Identifier(node) {
Expand All @@ -89,7 +104,33 @@ const visitors = {
return node;
},
FunctionExpression: remove_this_param,
FunctionDeclaration: remove_this_param
FunctionDeclaration: remove_this_param,
TSDeclareFunction() {
return b.empty;
},
ClassDeclaration(node, context) {
if (node.declare) {
return b.empty;
}
return context.next();
},
VariableDeclaration(node, context) {
if (node.declare) {
return b.empty;
}
return context.next();
},
TSModuleDeclaration(node, context) {
if (!node.body) return b.empty;

// namespaces can contain non-type nodes
const cleaned = /** @type {any[]} */ (node.body.body).map((entry) => context.visit(entry));
if (cleaned.some((entry) => entry !== b.empty)) {
e.typescript_invalid_feature(node, 'namespaces with non-type nodes');
}

return b.empty;
}
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,18 @@
}
class Foo {
constructor(public readonly name: string) {}
constructor(readonly name: string) {}
}
declare const declared_const: number;
declare function declared_fn(): void;
declare class declared_class {
foo: number;
}
declare module 'foobar' {}
namespace SomeNamespace {
export type Foo = true
}
export type { Hello };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"code": "typescript_invalid_feature",
"message": "TypeScript language features like accessor fields (related TSC proposal is not stage 4 yet) are not natively supported, and their use is generally discouraged. Outside of `<script>` tags, these features are not supported. For use within `<script>` tags, you will need to use a preprocessor to convert it to JavaScript before it gets passed to the Svelte compiler. If you are using `vitePreprocess`, make sure to specifically enable preprocessing script tags (`vitePreprocess({ script: true })`)",
"start": {
"line": 3,
"column": 2
},
"end": {
"line": 3,
"column": 17
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script lang="ts">
class Foo {
accessor y = 1;
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"code": "typescript_invalid_feature",
"message": "TypeScript language features like decorators (related TSC proposal is not stage 4 yet) are not natively supported, and their use is generally discouraged. Outside of `<script>` tags, these features are not supported. For use within `<script>` tags, you will need to use a preprocessor to convert it to JavaScript before it gets passed to the Svelte compiler. If you are using `vitePreprocess`, make sure to specifically enable preprocessing script tags (`vitePreprocess({ script: true })`)",
"start": {
"line": 2,
"column": 4
},
"end": {
"line": 2,
"column": 10
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<script lang="ts">
@foo()
class Foo {}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"code": "typescript_invalid_feature",
"message": "TypeScript language features like enums are not natively supported, and their use is generally discouraged. Outside of `<script>` tags, these features are not supported. For use within `<script>` tags, you will need to use a preprocessor to convert it to JavaScript before it gets passed to the Svelte compiler. If you are using `vitePreprocess`, make sure to specifically enable preprocessing script tags (`vitePreprocess({ script: true })`)",
"start": {
"line": 2,
"column": 1
},
"end": {
"line": 4,
"column": 2
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script lang="ts">
enum Foo {
bar = 1
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"code": "typescript_invalid_feature",
"message": "TypeScript language features like accessibility modifiers on constructor parameters are not natively supported, and their use is generally discouraged. Outside of `<script>` tags, these features are not supported. For use within `<script>` tags, you will need to use a preprocessor to convert it to JavaScript before it gets passed to the Svelte compiler. If you are using `vitePreprocess`, make sure to specifically enable preprocessing script tags (`vitePreprocess({ script: true })`)",
"start": {
"line": 3,
"column": 14
},
"end": {
"line": 3,
"column": 31
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script lang="ts">
class Foo {
constructor(private x: number) {}
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"code": "typescript_invalid_feature",
"message": "TypeScript language features like namespaces with non-type nodes are not natively supported, and their use is generally discouraged. Outside of `<script>` tags, these features are not supported. For use within `<script>` tags, you will need to use a preprocessor to convert it to JavaScript before it gets passed to the Svelte compiler. If you are using `vitePreprocess`, make sure to specifically enable preprocessing script tags (`vitePreprocess({ script: true })`)",
"start": {
"line": 2,
"column": 1
},
"end": {
"line": 4,
"column": 2
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script module lang="ts">
namespace SomeNamespace {
export const foo = true;
}
</script>
18 changes: 14 additions & 4 deletions packages/svelte/tests/validator/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,20 @@ const { test, run } = suite<ValidatorTest>(async (config, cwd) => {
const expected = expected_errors && expected_errors[0];

if (error && expected) {
assert.equal(error.code, expected.code);
assert.equal(error.message, expected.message);
assert.deepEqual({ line: error.start?.line, column: error.start?.column }, expected.start);
assert.deepEqual({ line: error.end?.line, column: error.end?.column }, expected.end);
assert.deepEqual(
{
code: error.code,
message: error.message,
start: { line: error.start?.line, column: error.start?.column },
end: { line: error.end?.line, column: error.end?.column }
},
{
code: expected.code,
message: expected.message,
start: expected.start,
end: expected.end
}
);
} else if (expected) {
throw new Error(`Expected an error: ${expected.message}`);
} else if (error) {
Expand Down

0 comments on commit 194570d

Please sign in to comment.