From ec1d85c89e345529b9108b770c68cf89b808f3b0 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 10 Apr 2025 16:50:05 -0700 Subject: [PATCH 1/3] fix: add snippet argument validation in dev (#15521) * init * fix * make `Payload` a class * doh * lint * tweak changeset * fix * only export things that should be available on $ * tweak message * fix --------- Co-authored-by: Rich Harris --- .changeset/bright-jeans-compare.md | 5 ++ .../98-reference/.generated/shared-errors.md | 6 ++ .../svelte/messages/shared-errors/errors.md | 4 ++ .../client/visitors/SnippetBlock.js | 17 ++++- .../server/visitors/SnippetBlock.js | 5 +- .../src/internal/client/dev/validation.js | 15 +++++ packages/svelte/src/internal/client/index.js | 1 + .../src/internal/server/blocks/snippet.js | 2 +- packages/svelte/src/internal/server/dev.js | 13 +++- packages/svelte/src/internal/server/index.js | 57 ++--------------- .../svelte/src/internal/server/payload.js | 64 +++++++++++++++++++ .../svelte/src/internal/server/types.d.ts | 13 ---- packages/svelte/src/internal/shared/errors.js | 15 +++++ 13 files changed, 147 insertions(+), 70 deletions(-) create mode 100644 .changeset/bright-jeans-compare.md create mode 100644 packages/svelte/src/internal/client/dev/validation.js create mode 100644 packages/svelte/src/internal/server/payload.js diff --git a/.changeset/bright-jeans-compare.md b/.changeset/bright-jeans-compare.md new file mode 100644 index 000000000000..eaec658e0343 --- /dev/null +++ b/.changeset/bright-jeans-compare.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: add snippet argument validation in dev diff --git a/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md index 0102aafcbca1..4c81d7b89452 100644 --- a/documentation/docs/98-reference/.generated/shared-errors.md +++ b/documentation/docs/98-reference/.generated/shared-errors.md @@ -30,6 +30,12 @@ This error would be thrown in a setup like this: Here, `List.svelte` is using `{@render children(item)` which means it expects `Parent.svelte` to use snippets. Instead, `Parent.svelte` uses the deprecated `let:` directive. This combination of APIs is incompatible, hence the error. +### invalid_snippet_arguments + +``` +A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}` +``` + ### lifecycle_outside_component ``` diff --git a/packages/svelte/messages/shared-errors/errors.md b/packages/svelte/messages/shared-errors/errors.md index 8b4c61303a07..20f3d193d928 100644 --- a/packages/svelte/messages/shared-errors/errors.md +++ b/packages/svelte/messages/shared-errors/errors.md @@ -26,6 +26,10 @@ This error would be thrown in a setup like this: Here, `List.svelte` is using `{@render children(item)` which means it expects `Parent.svelte` to use snippets. Instead, `Parent.svelte` uses the deprecated `let:` directive. This combination of APIs is incompatible, hence the error. +## invalid_snippet_arguments + +> A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}` + ## lifecycle_outside_component > `%name%(...)` can only be used during component initialisation diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js index 7a0d6981b52a..7eb043aa5d11 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js @@ -1,4 +1,4 @@ -/** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */ +/** @import { AssignmentPattern, BlockStatement, Expression, Identifier, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import { dev } from '../../../../state.js'; @@ -12,7 +12,7 @@ import { get_value } from './shared/declarations.js'; */ export function SnippetBlock(node, context) { // TODO hoist where possible - /** @type {Pattern[]} */ + /** @type {(Identifier | AssignmentPattern)[]} */ const args = [b.id('$$anchor')]; /** @type {BlockStatement} */ @@ -66,7 +66,18 @@ export function SnippetBlock(node, context) { } } } - + if (dev) { + declarations.unshift( + b.stmt( + b.call( + '$.validate_snippet_args', + .../** @type {Identifier[]} */ ( + args.map((arg) => (arg?.type === 'Identifier' ? arg : arg?.left)) + ) + ) + ) + ); + } body = b.block([ ...declarations, .../** @type {BlockStatement} */ (context.visit(node.body, child_state)).body diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js index eb839179271a..cae3e7d79c94 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js @@ -1,6 +1,7 @@ /** @import { BlockStatement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ +import { dev } from '../../../../state.js'; import * as b from '../../../../utils/builders.js'; /** @@ -13,7 +14,9 @@ export function SnippetBlock(node, context) { [b.id('$$payload'), ...node.parameters], /** @type {BlockStatement} */ (context.visit(node.body)) ); - + if (dev) { + fn.body.body.unshift(b.stmt(b.call('$.validate_snippet_args', b.id('$$payload')))); + } // @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone fn.___snippet = true; diff --git a/packages/svelte/src/internal/client/dev/validation.js b/packages/svelte/src/internal/client/dev/validation.js new file mode 100644 index 000000000000..e41e4c46283d --- /dev/null +++ b/packages/svelte/src/internal/client/dev/validation.js @@ -0,0 +1,15 @@ +import { invalid_snippet_arguments } from '../../shared/errors.js'; +/** + * @param {Node} anchor + * @param {...(()=>any)[]} args + */ +export function validate_snippet_args(anchor, ...args) { + if (typeof anchor !== 'object' || !(anchor instanceof Node)) { + invalid_snippet_arguments(); + } + for (let arg of args) { + if (typeof arg !== 'function') { + invalid_snippet_arguments(); + } + } +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index a865419c5f1b..e977bf3b0fe9 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -8,6 +8,7 @@ export { create_ownership_validator } from './dev/ownership.js'; export { check_target, legacy_api } from './dev/legacy.js'; export { trace } from './dev/tracing.js'; export { inspect } from './dev/inspect.js'; +export { validate_snippet_args } from './dev/validation.js'; export { await_block as await } from './dom/blocks/await.js'; export { if_block as if } from './dom/blocks/if.js'; export { key_block as key } from './dom/blocks/key.js'; diff --git a/packages/svelte/src/internal/server/blocks/snippet.js b/packages/svelte/src/internal/server/blocks/snippet.js index 3c5e8607905b..9e96ae34305d 100644 --- a/packages/svelte/src/internal/server/blocks/snippet.js +++ b/packages/svelte/src/internal/server/blocks/snippet.js @@ -1,5 +1,5 @@ /** @import { Snippet } from 'svelte' */ -/** @import { Payload } from '#server' */ +/** @import { Payload } from '../payload' */ /** @import { Getters } from '#shared' */ /** diff --git a/packages/svelte/src/internal/server/dev.js b/packages/svelte/src/internal/server/dev.js index ecf4e67429ac..34849196b7b6 100644 --- a/packages/svelte/src/internal/server/dev.js +++ b/packages/svelte/src/internal/server/dev.js @@ -1,10 +1,12 @@ -/** @import { Component, Payload } from '#server' */ +/** @import { Component } from '#server' */ import { FILENAME } from '../../constants.js'; import { is_tag_valid_with_ancestor, is_tag_valid_with_parent } from '../../html-tree-validation.js'; import { current_component } from './context.js'; +import { invalid_snippet_arguments } from '../shared/errors.js'; +import { Payload } from './payload.js'; /** * @typedef {{ @@ -98,3 +100,12 @@ export function push_element(payload, tag, line, column) { export function pop_element() { parent = /** @type {Element} */ (parent).parent; } + +/** + * @param {Payload} payload + */ +export function validate_snippet_args(payload) { + if (typeof payload !== 'object' || !(payload instanceof Payload)) { + invalid_snippet_arguments(); + } +} diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index ff34c0713263..d711778a44ea 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -1,5 +1,5 @@ /** @import { ComponentType, SvelteComponent } from 'svelte' */ -/** @import { Component, Payload, RenderOutput } from '#server' */ +/** @import { Component, RenderOutput } from '#server' */ /** @import { Store } from '#shared' */ export { FILENAME, HMR } from '../../constants.js'; import { attr, clsx, to_class, to_style } from '../shared/attributes.js'; @@ -17,43 +17,13 @@ import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN, BLOCK_OPEN_ELSE } from './hydra import { validate_store } from '../shared/validate.js'; import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js'; import { reset_elements } from './dev.js'; +import { Payload } from './payload.js'; // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 // https://infra.spec.whatwg.org/#noncharacter const INVALID_ATTR_NAME_CHAR_REGEX = /[\s'">/=\u{FDD0}-\u{FDEF}\u{FFFE}\u{FFFF}\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u; -/** - * @param {Payload} to_copy - * @returns {Payload} - */ -export function copy_payload({ out, css, head, uid }) { - return { - out, - css: new Set(css), - head: { - title: head.title, - out: head.out, - css: new Set(head.css), - uid: head.uid - }, - uid - }; -} - -/** - * Assigns second payload to first - * @param {Payload} p1 - * @param {Payload} p2 - * @returns {void} - */ -export function assign_payload(p1, p2) { - p1.out = p2.out; - p1.css = p2.css; - p1.head = p2.head; - p1.uid = p2.uid; -} - /** * @param {Payload} payload * @param {string} tag @@ -87,16 +57,6 @@ export function element(payload, tag, attributes_fn = noop, children_fn = noop) */ export let on_destroy = []; -/** - * Creates an ID generator - * @param {string} prefix - * @returns {() => string} - */ -function props_id_generator(prefix) { - let uid = 1; - return () => `${prefix}s${uid++}`; -} - /** * Only available on the server and when compiling with the `server` option. * Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app. @@ -106,14 +66,7 @@ function props_id_generator(prefix) { * @returns {RenderOutput} */ export function render(component, options = {}) { - const uid = props_id_generator(options.idPrefix ? options.idPrefix + '-' : ''); - /** @type {Payload} */ - const payload = { - out: '', - css: new Set(), - head: { title: '', out: '', css: new Set(), uid }, - uid - }; + const payload = new Payload(options.idPrefix ? options.idPrefix + '-' : ''); const prev_on_destroy = on_destroy; on_destroy = []; @@ -545,7 +498,9 @@ export { html } from './blocks/html.js'; export { push, pop } from './context.js'; -export { push_element, pop_element } from './dev.js'; +export { push_element, pop_element, validate_snippet_args } from './dev.js'; + +export { assign_payload, copy_payload } from './payload.js'; export { snapshot } from '../shared/clone.js'; diff --git a/packages/svelte/src/internal/server/payload.js b/packages/svelte/src/internal/server/payload.js new file mode 100644 index 000000000000..03bcaf492ea9 --- /dev/null +++ b/packages/svelte/src/internal/server/payload.js @@ -0,0 +1,64 @@ +export class Payload { + /** @type {Set<{ hash: string; code: string }>} */ + css = new Set(); + out = ''; + uid = () => ''; + + head = { + /** @type {Set<{ hash: string; code: string }>} */ + css: new Set(), + title: '', + out: '', + uid: () => '' + }; + + constructor(id_prefix = '') { + this.uid = props_id_generator(id_prefix); + this.head.uid = this.uid; + } +} + +/** + * Used in legacy mode to handle bindings + * @param {Payload} to_copy + * @returns {Payload} + */ +export function copy_payload({ out, css, head, uid }) { + const payload = new Payload(); + + payload.out = out; + payload.css = new Set(css); + payload.uid = uid; + + payload.head = { + title: head.title, + out: head.out, + css: new Set(head.css), + uid: head.uid + }; + + return payload; +} + +/** + * Assigns second payload to first + * @param {Payload} p1 + * @param {Payload} p2 + * @returns {void} + */ +export function assign_payload(p1, p2) { + p1.out = p2.out; + p1.css = p2.css; + p1.head = p2.head; + p1.uid = p2.uid; +} + +/** + * Creates an ID generator + * @param {string} prefix + * @returns {() => string} + */ +function props_id_generator(prefix) { + let uid = 1; + return () => `${prefix}s${uid++}`; +} diff --git a/packages/svelte/src/internal/server/types.d.ts b/packages/svelte/src/internal/server/types.d.ts index 2fffdbbdf0bb..6b0fc146c419 100644 --- a/packages/svelte/src/internal/server/types.d.ts +++ b/packages/svelte/src/internal/server/types.d.ts @@ -11,19 +11,6 @@ export interface Component { function?: any; } -export interface Payload { - out: string; - css: Set<{ hash: string; code: string }>; - head: { - title: string; - out: string; - uid: () => string; - css: Set<{ hash: string; code: string }>; - }; - /** Function that generates a unique ID */ - uid: () => string; -} - export interface RenderOutput { /** HTML that goes into the `` */ head: string; diff --git a/packages/svelte/src/internal/shared/errors.js b/packages/svelte/src/internal/shared/errors.js index 26d6822cdb29..2e89dc1ad109 100644 --- a/packages/svelte/src/internal/shared/errors.js +++ b/packages/svelte/src/internal/shared/errors.js @@ -17,6 +17,21 @@ export function invalid_default_snippet() { } } +/** + * A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}` + * @returns {never} + */ +export function invalid_snippet_arguments() { + if (DEV) { + const error = new Error(`invalid_snippet_arguments\nA snippet function was passed invalid arguments. Snippets should only be instantiated via \`{@render ...}\`\nhttps://svelte.dev/e/invalid_snippet_arguments`); + + error.name = 'Svelte error'; + throw error; + } else { + throw new Error(`https://svelte.dev/e/invalid_snippet_arguments`); + } +} + /** * `%name%(...)` can only be used during component initialisation * @param {string} name From 01171096cef8652704b92053e57156ae4ed9e5d9 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Fri, 11 Apr 2025 04:42:44 +0200 Subject: [PATCH 2/3] feat: add `css.hasGlobal` to `compile` output (#15450) * feat: add `hasUnscopedGlobalCss` to `compile` metadata * chore: rename to `has_unscoped_global` * fix: handle `-global` keyframes * chore: guard the check if the value is already true * update types * add tests * tweak * tweak * regenerate types * Update .changeset/plenty-hotels-mix.md * fix test, add failing test * fix * fix * fix jsdoc * unused * fix * lint * rename * rename * reduce indirection * tidy up * revert * tweak * lint --------- Co-authored-by: Rich Harris --- .changeset/plenty-hotels-mix.md | 5 ++ .../src/compiler/phases/1-parse/read/style.js | 2 + .../phases/2-analyze/css/css-analyze.js | 70 ++++++++++++++----- .../src/compiler/phases/2-analyze/index.js | 3 +- .../compiler/phases/3-transform/css/index.js | 3 +- .../svelte/src/compiler/phases/types.d.ts | 1 + packages/svelte/src/compiler/types/css.d.ts | 5 ++ packages/svelte/src/compiler/types/index.d.ts | 2 + .../css/samples/global-keyframes/_config.js | 5 ++ .../samples/global-local-nested/_config.js | 5 ++ .../samples/global-local-nested/expected.css | 12 ++++ .../samples/global-local-nested/input.svelte | 15 ++++ .../tests/css/samples/global-local/_config.js | 5 ++ .../css/samples/global-local/expected.css | 8 +++ .../css/samples/global-local/input.svelte | 11 +++ .../samples/global-with-nesting/_config.js | 4 +- .../tests/css/samples/global/_config.js | 5 ++ packages/svelte/tests/css/test.ts | 9 +++ packages/svelte/tests/helpers.js | 4 ++ packages/svelte/types/index.d.ts | 2 + 20 files changed, 155 insertions(+), 21 deletions(-) create mode 100644 .changeset/plenty-hotels-mix.md create mode 100644 packages/svelte/tests/css/samples/global-keyframes/_config.js create mode 100644 packages/svelte/tests/css/samples/global-local-nested/_config.js create mode 100644 packages/svelte/tests/css/samples/global-local-nested/expected.css create mode 100644 packages/svelte/tests/css/samples/global-local-nested/input.svelte create mode 100644 packages/svelte/tests/css/samples/global-local/_config.js create mode 100644 packages/svelte/tests/css/samples/global-local/expected.css create mode 100644 packages/svelte/tests/css/samples/global-local/input.svelte create mode 100644 packages/svelte/tests/css/samples/global/_config.js diff --git a/.changeset/plenty-hotels-mix.md b/.changeset/plenty-hotels-mix.md new file mode 100644 index 000000000000..5e7aa834da77 --- /dev/null +++ b/.changeset/plenty-hotels-mix.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: add `css.hasGlobal` to `compile` output diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index f8c39f1b1dd1..56dbe124b7bf 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -118,6 +118,7 @@ function read_rule(parser) { metadata: { parent_rule: null, has_local_selectors: false, + has_global_selectors: false, is_global_block: false } }; @@ -342,6 +343,7 @@ function read_selector(parser, inside_pseudo_class = false) { children, metadata: { rule: null, + is_global: false, used: false } }; diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js index 362ac9dcad50..76cb2f56e995 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js @@ -7,13 +7,15 @@ import { is_keyframes_node } from '../../css.js'; import { is_global, is_unscoped_pseudo_class } from './utils.js'; /** - * @typedef {Visitors< - * AST.CSS.Node, - * { - * keyframes: string[]; - * rule: AST.CSS.Rule | null; - * } - * >} CssVisitors + * @typedef {{ + * keyframes: string[]; + * rule: AST.CSS.Rule | null; + * analysis: ComponentAnalysis; + * }} CssState + */ + +/** + * @typedef {Visitors} CssVisitors */ /** @@ -28,6 +30,15 @@ function is_global_block_selector(simple_selector) { ); } +/** + * @param {AST.SvelteNode[]} path + */ +function is_unscoped(path) { + return path + .filter((node) => node.type === 'Rule') + .every((node) => node.metadata.has_global_selectors); +} + /** * * @param {Array} path @@ -42,6 +53,9 @@ const css_visitors = { if (is_keyframes_node(node)) { if (!node.prelude.startsWith('-global-') && !is_in_global_block(context.path)) { context.state.keyframes.push(node.prelude); + } else if (node.prelude.startsWith('-global-')) { + // we don't check if the block.children.length because the keyframe is still added even if empty + context.state.analysis.css.has_global ||= is_unscoped(context.path); } } @@ -99,10 +113,12 @@ const css_visitors = { node.metadata.rule = context.state.rule; - node.metadata.used ||= node.children.every( + node.metadata.is_global = node.children.every( ({ metadata }) => metadata.is_global || metadata.is_global_like ); + node.metadata.used ||= node.metadata.is_global; + if ( node.metadata.rule?.metadata.parent_rule && node.children[0]?.selectors[0]?.type === 'NestingSelector' @@ -190,6 +206,7 @@ const css_visitors = { if (idx !== -1) { is_global_block = true; + for (let i = idx + 1; i < child.selectors.length; i++) { walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, { ComplexSelector(node) { @@ -242,16 +259,26 @@ const css_visitors = { } } - context.next({ - ...context.state, - rule: node - }); + const state = { ...context.state, rule: node }; - node.metadata.has_local_selectors = node.prelude.children.some((selector) => { - return selector.children.some( - ({ metadata }) => !metadata.is_global && !metadata.is_global_like - ); - }); + // visit selector list first, to populate child selector metadata + context.visit(node.prelude, state); + + for (const selector of node.prelude.children) { + node.metadata.has_global_selectors ||= selector.metadata.is_global; + node.metadata.has_local_selectors ||= !selector.metadata.is_global; + } + + // if this rule has a ComplexSelector whose RelativeSelector children are all + // `:global(...)`, and the rule contains declarations (rather than just + // nested rules) then the component as a whole includes global CSS + context.state.analysis.css.has_global ||= + node.metadata.has_global_selectors && + node.block.children.filter((child) => child.type === 'Declaration').length > 0 && + is_unscoped(context.path); + + // visit block list, so parent rule metadata is populated + context.visit(node.block, state); }, NestingSelector(node, context) { const rule = /** @type {AST.CSS.Rule} */ (context.state.rule); @@ -289,5 +316,12 @@ const css_visitors = { * @param {ComponentAnalysis} analysis */ export function analyze_css(stylesheet, analysis) { - walk(stylesheet, { keyframes: analysis.css.keyframes, rule: null }, css_visitors); + /** @type {CssState} */ + const css_state = { + keyframes: analysis.css.keyframes, + rule: null, + analysis + }; + + walk(stylesheet, css_state, css_visitors); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index c62fb03e8fef..a6eb9565cb3b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -456,7 +456,8 @@ export function analyze_component(root, source, options) { hash }) : '', - keyframes: [] + keyframes: [], + has_global: false }, source, undefined_exports: new Map(), diff --git a/packages/svelte/src/compiler/phases/3-transform/css/index.js b/packages/svelte/src/compiler/phases/3-transform/css/index.js index 5b0dcd558893..dff034f8aad6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/css/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/css/index.js @@ -59,7 +59,8 @@ export function render_stylesheet(source, analysis, options) { // generateMap takes care of calculating source relative to file source: options.filename, file: options.cssOutputFilename || options.filename - }) + }), + hasGlobal: analysis.css.has_global }; merge_with_preprocessor_map(css, options, css.map.sources[0]); diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index f09b88130305..f98cbe141567 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -74,6 +74,7 @@ export interface ComponentAnalysis extends Analysis { ast: AST.CSS.StyleSheet | null; hash: string; keyframes: string[]; + has_global: boolean; }; source: string; undefined_exports: Map; diff --git a/packages/svelte/src/compiler/types/css.d.ts b/packages/svelte/src/compiler/types/css.d.ts index 7b2e6ae5f710..154a06ffb11c 100644 --- a/packages/svelte/src/compiler/types/css.d.ts +++ b/packages/svelte/src/compiler/types/css.d.ts @@ -34,6 +34,10 @@ export namespace _CSS { metadata: { parent_rule: null | Rule; has_local_selectors: boolean; + /** + * `true` if the rule contains a ComplexSelector whose RelativeSelectors are all global or global-like + */ + has_global_selectors: boolean; /** * `true` if the rule contains a `:global` selector, and therefore everything inside should be unscoped */ @@ -64,6 +68,7 @@ export namespace _CSS { /** @internal */ metadata: { rule: null | Rule; + is_global: boolean; /** True if this selector applies to an element. For global selectors, this is defined in css-analyze, for others in css-prune while scoping */ used: boolean; }; diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index eec41bad9d25..616c346ad35a 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -18,6 +18,8 @@ export interface CompileResult { code: string; /** A source map */ map: SourceMap; + /** Whether or not the CSS includes global rules */ + hasGlobal: boolean; }; /** * An array of warning objects that were generated during compilation. Each warning has several properties: diff --git a/packages/svelte/tests/css/samples/global-keyframes/_config.js b/packages/svelte/tests/css/samples/global-keyframes/_config.js new file mode 100644 index 000000000000..30953854ad59 --- /dev/null +++ b/packages/svelte/tests/css/samples/global-keyframes/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + hasGlobal: true +}); diff --git a/packages/svelte/tests/css/samples/global-local-nested/_config.js b/packages/svelte/tests/css/samples/global-local-nested/_config.js new file mode 100644 index 000000000000..5a7796ebac7e --- /dev/null +++ b/packages/svelte/tests/css/samples/global-local-nested/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + hasGlobal: false +}); diff --git a/packages/svelte/tests/css/samples/global-local-nested/expected.css b/packages/svelte/tests/css/samples/global-local-nested/expected.css new file mode 100644 index 000000000000..8eadf2b948c4 --- /dev/null +++ b/packages/svelte/tests/css/samples/global-local-nested/expected.css @@ -0,0 +1,12 @@ + + div.svelte-xyz { + .whatever { + color: green; + } + } + + .whatever { + div.svelte-xyz { + color: green; + } + } diff --git a/packages/svelte/tests/css/samples/global-local-nested/input.svelte b/packages/svelte/tests/css/samples/global-local-nested/input.svelte new file mode 100644 index 000000000000..60210be75363 --- /dev/null +++ b/packages/svelte/tests/css/samples/global-local-nested/input.svelte @@ -0,0 +1,15 @@ +
{@html whatever}
+ + diff --git a/packages/svelte/tests/css/samples/global-local/_config.js b/packages/svelte/tests/css/samples/global-local/_config.js new file mode 100644 index 000000000000..5a7796ebac7e --- /dev/null +++ b/packages/svelte/tests/css/samples/global-local/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + hasGlobal: false +}); diff --git a/packages/svelte/tests/css/samples/global-local/expected.css b/packages/svelte/tests/css/samples/global-local/expected.css new file mode 100644 index 000000000000..c4fc74fb1aaf --- /dev/null +++ b/packages/svelte/tests/css/samples/global-local/expected.css @@ -0,0 +1,8 @@ + + div.svelte-xyz .whatever { + color: green; + } + + .whatever div.svelte-xyz { + color: green; + } diff --git a/packages/svelte/tests/css/samples/global-local/input.svelte b/packages/svelte/tests/css/samples/global-local/input.svelte new file mode 100644 index 000000000000..bff97ab485a2 --- /dev/null +++ b/packages/svelte/tests/css/samples/global-local/input.svelte @@ -0,0 +1,11 @@ +
{@html whatever}
+ + diff --git a/packages/svelte/tests/css/samples/global-with-nesting/_config.js b/packages/svelte/tests/css/samples/global-with-nesting/_config.js index 292c6c49ac9d..6cec7c236045 100644 --- a/packages/svelte/tests/css/samples/global-with-nesting/_config.js +++ b/packages/svelte/tests/css/samples/global-with-nesting/_config.js @@ -1,5 +1,7 @@ import { test } from '../../test'; export default test({ - warnings: [] + warnings: [], + + hasGlobal: false }); diff --git a/packages/svelte/tests/css/samples/global/_config.js b/packages/svelte/tests/css/samples/global/_config.js new file mode 100644 index 000000000000..30953854ad59 --- /dev/null +++ b/packages/svelte/tests/css/samples/global/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + hasGlobal: true +}); diff --git a/packages/svelte/tests/css/test.ts b/packages/svelte/tests/css/test.ts index dd51f52eabc3..8846b1d9862e 100644 --- a/packages/svelte/tests/css/test.ts +++ b/packages/svelte/tests/css/test.ts @@ -34,6 +34,7 @@ interface CssTest extends BaseTest { compileOptions?: Partial; warnings?: Warning[]; props?: Record; + hasGlobal?: boolean; } /** @@ -78,6 +79,14 @@ const { test, run } = suite(async (config, cwd) => { // assert_html_equal(actual_ssr, expected.html); } + if (config.hasGlobal !== undefined) { + const metadata = JSON.parse( + fs.readFileSync(`${cwd}/_output/client/input.svelte.css.json`, 'utf-8') + ); + + assert.equal(metadata.hasGlobal, config.hasGlobal); + } + const dom_css = fs.readFileSync(`${cwd}/_output/client/input.svelte.css`, 'utf-8').trim(); const ssr_css = fs.readFileSync(`${cwd}/_output/server/input.svelte.css`, 'utf-8').trim(); diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js index 87bcb473e7e2..f853d5873c57 100644 --- a/packages/svelte/tests/helpers.js +++ b/packages/svelte/tests/helpers.js @@ -146,6 +146,10 @@ export async function compile_directory( if (compiled.css) { write(`${output_dir}/${file}.css`, compiled.css.code); + write( + `${output_dir}/${file}.css.json`, + JSON.stringify({ hasGlobal: compiled.css.hasGlobal }) + ); if (output_map) { write(`${output_dir}/${file}.css.map`, JSON.stringify(compiled.css.map, null, '\t')); } diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index c6000fc4b67f..6f12daf18778 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -753,6 +753,8 @@ declare module 'svelte/compiler' { code: string; /** A source map */ map: SourceMap; + /** Whether or not the CSS includes global rules */ + hasGlobal: boolean; }; /** * An array of warning objects that were generated during compilation. Each warning has several properties: From 02448a9acdf781ea44b696d4a60f387b8856e82c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 10 Apr 2025 23:19:23 -0400 Subject: [PATCH 3/3] Version Packages (#15731) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/bright-jeans-compare.md | 5 ----- .changeset/plenty-hotels-mix.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 .changeset/bright-jeans-compare.md delete mode 100644 .changeset/plenty-hotels-mix.md diff --git a/.changeset/bright-jeans-compare.md b/.changeset/bright-jeans-compare.md deleted file mode 100644 index eaec658e0343..000000000000 --- a/.changeset/bright-jeans-compare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: add snippet argument validation in dev diff --git a/.changeset/plenty-hotels-mix.md b/.changeset/plenty-hotels-mix.md deleted file mode 100644 index 5e7aa834da77..000000000000 --- a/.changeset/plenty-hotels-mix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: add `css.hasGlobal` to `compile` output diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index f27a6640bd27..68a880366361 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.26.0 + +### Minor Changes + +- feat: add `css.hasGlobal` to `compile` output ([#15450](https://github.com/sveltejs/svelte/pull/15450)) + +### Patch Changes + +- fix: add snippet argument validation in dev ([#15521](https://github.com/sveltejs/svelte/pull/15521)) + ## 5.25.12 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 67f6186fc730..8267cb521852 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.25.12", + "version": "5.26.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index c554e9018c9c..ade51aaf178a 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.25.12'; +export const VERSION = '5.26.0'; export const PUBLIC_VERSION = '5'; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy