diff --git a/.changeset/new-houses-roll.md b/.changeset/new-houses-roll.md new file mode 100644 index 000000000000..a2b8370e065f --- /dev/null +++ b/.changeset/new-houses-roll.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: tighten up `export default` validation diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportDefaultDeclaration.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportDefaultDeclaration.js index 0a7461f15589..768f1c6305a4 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportDefaultDeclaration.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportDefaultDeclaration.js @@ -1,13 +1,18 @@ -/** @import { ExportDefaultDeclaration, Node } from 'estree' */ +/** @import { ExportDefaultDeclaration } from 'estree' */ /** @import { Context } from '../types' */ import * as e from '../../../errors.js'; +import { validate_export } from './shared/utils.js'; /** * @param {ExportDefaultDeclaration} node * @param {Context} context */ export function ExportDefaultDeclaration(node, context) { - if (context.state.ast_type === 'instance') { + if (!context.state.ast_type /* .svelte.js module */) { + if (node.declaration.type === 'Identifier') { + validate_export(node, context.state.scope, node.declaration.name); + } + } else { e.module_illegal_default_export(node); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportSpecifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportSpecifier.js index d0d1ccf93211..cfb24970de21 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportSpecifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportSpecifier.js @@ -1,8 +1,6 @@ -/** @import { ExportSpecifier, Node } from 'estree' */ -/** @import { Binding } from '#compiler' */ +/** @import { ExportSpecifier } from 'estree' */ /** @import { Context } from '../types' */ -/** @import { Scope } from '../../scope' */ -import * as e from '../../../errors.js'; +import { validate_export } from './shared/utils.js'; /** * @param {ExportSpecifier} node @@ -30,22 +28,3 @@ export function ExportSpecifier(node, context) { validate_export(node, context.state.scope, local_name); } } - -/** - * - * @param {Node} node - * @param {Scope} scope - * @param {string} name - */ -function validate_export(node, scope, name) { - const binding = scope.get(name); - if (!binding) return; - - if (binding.kind === 'derived') { - e.derived_invalid_export(node); - } - - if ((binding.kind === 'state' || binding.kind === 'raw_state') && binding.reassigned) { - e.state_invalid_export(node); - } -} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js index 8698174c6b53..e265637c404d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js @@ -1,4 +1,4 @@ -/** @import { AssignmentExpression, Expression, Literal, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */ +/** @import { AssignmentExpression, Expression, Literal, Node, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */ /** @import { AST, Binding } from '#compiler' */ /** @import { AnalysisState, Context } from '../../types' */ /** @import { Scope } from '../../../scope' */ @@ -263,3 +263,22 @@ export function validate_identifier_name(binding, function_depth) { } } } + +/** + * Checks that the exported name is not a derived or reassigned state variable. + * @param {Node} node + * @param {Scope} scope + * @param {string} name + */ +export function validate_export(node, scope, name) { + const binding = scope.get(name); + if (!binding) return; + + if (binding.kind === 'derived') { + e.derived_invalid_export(node); + } + + if ((binding.kind === 'state' || binding.kind === 'raw_state') && binding.reassigned) { + e.state_invalid_export(node); + } +} diff --git a/packages/svelte/tests/compiler-errors/samples/export-default-derived-state-indirect/_config.js b/packages/svelte/tests/compiler-errors/samples/export-default-derived-state-indirect/_config.js new file mode 100644 index 000000000000..4a362117699f --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/export-default-derived-state-indirect/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'derived_invalid_export', + message: + 'Cannot export derived state from a module. To expose the current derived value, export a function returning its value', + position: [61, 83] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/export-default-derived-state-indirect/main.svelte.js b/packages/svelte/tests/compiler-errors/samples/export-default-derived-state-indirect/main.svelte.js new file mode 100644 index 000000000000..64a794492b0d --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/export-default-derived-state-indirect/main.svelte.js @@ -0,0 +1,5 @@ +let count = $state(0); + +const double = $derived(count * 2); + +export default double; diff --git a/packages/svelte/tests/compiler-errors/samples/export-default-state-indirect/_config.js b/packages/svelte/tests/compiler-errors/samples/export-default-state-indirect/_config.js new file mode 100644 index 000000000000..99e4faec25d4 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/export-default-state-indirect/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'state_invalid_export', + message: + "Cannot export state from a module if it is reassigned. Either export a function returning the state value or only mutate the state value's properties", + position: [93, 118] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/export-default-state-indirect/main.svelte.js b/packages/svelte/tests/compiler-errors/samples/export-default-state-indirect/main.svelte.js new file mode 100644 index 000000000000..8e52f76533d4 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/export-default-state-indirect/main.svelte.js @@ -0,0 +1,7 @@ +let primitive = $state('nope'); + +export function update_primitive() { + primitive = 'yep'; +} + +export default primitive; diff --git a/packages/svelte/tests/validator/samples/default-export-module/errors.json b/packages/svelte/tests/validator/samples/default-export-module/errors.json new file mode 100644 index 000000000000..9fd2bb7df5ea --- /dev/null +++ b/packages/svelte/tests/validator/samples/default-export-module/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "module_illegal_default_export", + "message": "A component cannot have a default export", + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 19 + } + } +] diff --git a/packages/svelte/tests/validator/samples/default-export-module/input.svelte b/packages/svelte/tests/validator/samples/default-export-module/input.svelte new file mode 100644 index 000000000000..2aeeabb2e154 --- /dev/null +++ b/packages/svelte/tests/validator/samples/default-export-module/input.svelte @@ -0,0 +1,3 @@ +