Skip to content

Commit

Permalink
breaking: deprecate context="module" in favor of module
Browse files Browse the repository at this point in the history
Also reserve a few attributes, which we may or may not use in the future

closes #12637
  • Loading branch information
dummdidumm committed Aug 21, 2024
1 parent af35fb7 commit e7510a0
Show file tree
Hide file tree
Showing 65 changed files with 235 additions and 127 deletions.
5 changes: 5 additions & 0 deletions .changeset/quick-eagles-sit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

breaking: deprecate `context="module"` in favor of `module`
Original file line number Diff line number Diff line change
Expand Up @@ -161,16 +161,16 @@ If you'd like to react to changes to a prop, use the `$derived` or `$effect` run

For more information on reactivity, read the documentation around runes.

## <script context="module">
## <script module>

A `<script>` tag with a `context="module"` attribute runs once when the module first evaluates, rather than for each component instance. Values declared in this block are accessible from a regular `<script>` (and the component markup) but not vice versa.
A `<script>` tag with a `module` attribute runs once when the module first evaluates, rather than for each component instance. Values declared in this block are accessible from a regular `<script>` (and the component markup) but not vice versa.

You can `export` bindings from this block, and they will become exports of the compiled module.

You cannot `export default`, since the default export is the component itself.

```svelte
<script context="module">
<script module>
let totalComponents = 0;
// the export keyword allows this function to imported with e.g.
Expand Down
4 changes: 2 additions & 2 deletions packages/svelte/messages/compile-errors/script.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
## declaration_duplicate_module_import

> Cannot declare same variable name which is imported inside `<script context="module">`
> Cannot declare same variable name which is imported inside `<script module>`
## derived_invalid_export

Expand Down Expand Up @@ -152,7 +152,7 @@
## store_invalid_subscription

> Cannot reference store value inside `<script context="module">`
> Cannot reference store value inside `<script module>`
## store_invalid_subscription_module

Expand Down
10 changes: 9 additions & 1 deletion packages/svelte/messages/compile-errors/template.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,12 +216,20 @@ HTML restricts where certain elements can appear. In case of a violation the bro
## script_duplicate

> A component can have a single top-level `<script>` element and/or a single top-level `<script context="module">` element
> A component can have a single top-level `<script>` element and/or a single top-level `<script module>` element
## script_invalid_attribute_value

> If the %name% attribute is supplied, it must be a boolean attribute
## script_invalid_context

> If the context attribute is supplied, its value must be "module"
## script_reserved_attribute

> The %name% attribute is reserved and cannot be used
## slot_attribute_duplicate

> Duplicate slot name '%name%' in <%component%>
Expand Down
4 changes: 4 additions & 0 deletions packages/svelte/messages/compile-warnings/template.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ HTML restricts where certain elements can appear. In case of a violation the bro

This code will work when the component is rendered on the client (which is why this is a warning rather than an error), but if you use server rendering it will cause hydration to fail.

## script_context_deprecated

> `context="module"` is deprecated, use the `module` attribute instead
## slot_element_deprecated

> Using `<slot>` to render parent content is deprecated. Use `{@render ...}` tags instead
Expand Down
32 changes: 26 additions & 6 deletions packages/svelte/src/compiler/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,12 @@ export function declaration_duplicate(node, name) {
}

/**
* Cannot declare same variable name which is imported inside `<script context="module">`
* Cannot declare same variable name which is imported inside `<script module>`
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function declaration_duplicate_module_import(node) {
e(node, "declaration_duplicate_module_import", "Cannot declare same variable name which is imported inside `<script context=\"module\">`");
e(node, "declaration_duplicate_module_import", "Cannot declare same variable name which is imported inside `<script module>`");
}

/**
Expand Down Expand Up @@ -417,12 +417,12 @@ export function store_invalid_scoped_subscription(node) {
}

/**
* Cannot reference store value inside `<script context="module">`
* Cannot reference store value inside `<script module>`
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function store_invalid_subscription(node) {
e(node, "store_invalid_subscription", "Cannot reference store value inside `<script context=\"module\">`");
e(node, "store_invalid_subscription", "Cannot reference store value inside `<script module>`");
}

/**
Expand Down Expand Up @@ -1044,12 +1044,22 @@ export function render_tag_invalid_spread_argument(node) {
}

/**
* A component can have a single top-level `<script>` element and/or a single top-level `<script context="module">` element
* A component can have a single top-level `<script>` element and/or a single top-level `<script module>` element
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function script_duplicate(node) {
e(node, "script_duplicate", "A component can have a single top-level `<script>` element and/or a single top-level `<script context=\"module\">` element");
e(node, "script_duplicate", "A component can have a single top-level `<script>` element and/or a single top-level `<script module>` element");
}

/**
* If the %name% attribute is supplied, it must be a boolean attribute
* @param {null | number | NodeLike} node
* @param {string} name
* @returns {never}
*/
export function script_invalid_attribute_value(node, name) {
e(node, "script_invalid_attribute_value", `If the ${name} attribute is supplied, it must be a boolean attribute`);
}

/**
Expand All @@ -1061,6 +1071,16 @@ export function script_invalid_context(node) {
e(node, "script_invalid_context", "If the context attribute is supplied, its value must be \"module\"");
}

/**
* The %name% attribute is reserved and cannot be used
* @param {null | number | NodeLike} node
* @param {string} name
* @returns {never}
*/
export function script_reserved_attribute(node, name) {
e(node, "script_reserved_attribute", `The ${name} attribute is reserved and cannot be used`);
}

/**
* Duplicate slot name '%name%' in <%component%>
* @param {null | number | NodeLike} node
Expand Down
14 changes: 10 additions & 4 deletions packages/svelte/src/compiler/migrate/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/** @import { VariableDeclarator, Node, Identifier } from 'estree' */
/** @import { SvelteNode } from '../types/template.js' */
/** @import { Visitors } from 'zimmerframe' */
/** @import { ComponentAnalysis } from '../phases/types.js' */
/** @import { Scope } from '../phases/scope.js' */
Expand Down Expand Up @@ -58,6 +57,13 @@ export function migrate(source) {
needs_run: false
};

if (parsed.module) {
const context = parsed.module.attributes.find((attr) => attr.name === 'context');
if (context) {
state.str.update(context.start, context.end, 'module');
}
}

if (parsed.instance) {
walk(parsed.instance.content, state, instance_script);
}
Expand Down Expand Up @@ -223,7 +229,7 @@ export function migrate(source) {
* }} State
*/

/** @type {Visitors<SvelteNode, State>} */
/** @type {Visitors<Compiler.SvelteNode, State>} */
const instance_script = {
_(node, { state, next }) {
// @ts-expect-error
Expand Down Expand Up @@ -472,7 +478,7 @@ const instance_script = {
}
};

/** @type {Visitors<SvelteNode, State>} */
/** @type {Visitors<Compiler.SvelteNode, State>} */
const template = {
Identifier(node, { state, path }) {
handle_identifier(node, state, path);
Expand Down Expand Up @@ -590,7 +596,7 @@ const template = {
/**
* @param {VariableDeclarator} declarator
* @param {MagicString} str
* @param {Compiler.SvelteNode[]} path
* @param {Array<Compiler.SvelteNode>} path
*/
function extract_type_and_comment(declarator, str, path) {
const parent = path.at(-1);
Expand Down
43 changes: 32 additions & 11 deletions packages/svelte/src/compiler/phases/1-parse/read/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import * as acorn from '../acorn.js';
import { regex_not_newline_characters } from '../../patterns.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';

const regex_closing_script_tag = /<\/script\s*>/;
const regex_starts_with_closing_script_tag = /^<\/script\s*>/;
Expand All @@ -13,22 +14,42 @@ const regex_starts_with_closing_script_tag = /^<\/script\s*>/;
* @returns {string}
*/
function get_context(attributes) {
const context = attributes.find(
/** @param {any} attribute */ (attribute) => attribute.name === 'context'
);
if (!context) return 'default';
for (const attribute of attributes) {
switch (attribute.name) {
case 'context': {
if (attribute.value.length !== 1 || attribute.value[0].type !== 'Text') {
e.script_invalid_context(attribute.start);
}

if (context.value.length !== 1 || context.value[0].type !== 'Text') {
e.script_invalid_context(context.start);
}
const value = attribute.value[0].data;

if (value !== 'module') {
e.script_invalid_context(attribute.start);
}

w.script_context_deprecated(attribute);

const value = context.value[0].data;
return value;
}
case 'module': {
if (attribute.value !== true) {
// Deliberately a generic code to future-proof for potential other attributes
e.script_invalid_attribute_value(attribute.start, attribute.name);
}

if (value !== 'module') {
e.script_invalid_context(context.start);
return 'module';
}
case 'server':
case 'client':
case 'worker':
case 'test':
case 'default': {
e.script_reserved_attribute(attribute.start, attribute.name);
}
}
}

return value;
return 'default';
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/compiler/phases/2-analyze/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ export function analyze_component(root, source, options) {

if (module.ast) {
for (const { node, path } of references) {
// if the reference is inside context="module", error. this is a bit hacky but it works
// if the reference is inside module, error. this is a bit hacky but it works
if (
/** @type {number} */ (node.start) > /** @type {number} */ (module.ast.start) &&
/** @type {number} */ (node.end) < /** @type {number} */ (module.ast.end) &&
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/compiler/types/template.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export interface Root extends BaseNode {
css: Css.StyleSheet | null;
/** The parsed `<script>` element, if exists */
instance: Script | null;
/** The parsed `<script context="module">` element, if exists */
/** The parsed `<script module>` element, if exists */
module: Script | null;
metadata: {
/** Whether the component was parsed with typescript */
Expand Down
9 changes: 9 additions & 0 deletions packages/svelte/src/compiler/warnings.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export const codes = [
"element_invalid_self_closing_tag",
"event_directive_deprecated",
"node_invalid_placement_ssr",
"script_context_deprecated",
"slot_element_deprecated",
"svelte_component_deprecated",
"svelte_element_invalid_this"
Expand Down Expand Up @@ -769,6 +770,14 @@ export function node_invalid_placement_ssr(node, thing, parent) {
w(node, "node_invalid_placement_ssr", `${thing} is invalid inside \`<${parent}>\`. When rendering this component on the server, the resulting HTML will be modified by the browser, likely resulting in a \`hydration_mismatch\` warning`);
}

/**
* `context="module"` is deprecated, use the `module` attribute instead
* @param {null | NodeLike} node
*/
export function script_context_deprecated(node) {
w(node, "script_context_deprecated", "`context=\"module\"` is deprecated, use the `module` attribute instead");
}

/**
* Using `<slot>` to render parent content is deprecated. Use `{@render ...}` tags instead
* @param {null | NodeLike} node
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<script context="module">
<script module>
export const object = $state({
ok: true
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<script context="module">
<script module>
export let bar = ''; // check that it doesn't error here already
</script>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test } from '../../test';
export default test({
error: {
code: 'store_invalid_subscription',
message: 'Cannot reference store value inside `<script context="module">`',
message: 'Cannot reference store value inside `<script module>`',
position: [164, 168]
}
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<script context="module">
<script module>
// this should be fine (state rune is not treated as a store)
const state = $state(0);
// this is not
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<script context="module">
let foo = true;
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<script module>
let foo = true;
</script>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<svelte:options customElement="my-custom-element" runes={true} />

<script context="module" lang="ts">
<script module lang="ts">
</script>

<script lang="ts" generics="T extends { foo: number }">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script context="module">
import Tooltip from './Tooltip.svelte';
<script module>
import Tooltip from './Tooltip.svelte';
export const Widget = { Tooltip };
</script>
export const Widget = { Tooltip };
</script>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script context="module">
import Foo from './Foo.svelte';
<script module>
import Foo from './Foo.svelte';
export const Components = { Foo };
</script>
export const Components = { Foo };
</script>
Loading

0 comments on commit e7510a0

Please sign in to comment.