diff --git a/.gitignore b/.gitignore index 963ae9f26e..c2cac230a3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,17 +4,17 @@ node_modules *.map /src/compiler/compile/internal_exports.js +/src/shared/version.js /compiler.d.ts -/compiler.*js -/index.*js -/ssr.*js -/action -/internal -/store -/easing -/motion -/transition -/animate +/compiler.cjs +/index.d.ts +/action.d.ts +/internal.d.ts +/store.d.ts +/easing.d.ts +/motion.d.ts +/transition.d.ts +/animate.d.ts /scratch/ /test/*/samples/_ /test/runtime/shards diff --git a/.prettierignore b/.prettierignore index 58dfd819cd..519034cd2e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,6 +3,7 @@ !/scripts !/src src/compiler/compile/internal_exports.js +src/shared/version.js !/test !documentation !sites @@ -12,4 +13,5 @@ src/compiler/compile/internal_exports.js /test/**/expected* /test/**/_output /types +!rollup.config.js !vitest.config.js diff --git a/CHANGELOG.md b/CHANGELOG.md index e3eac6d40e..299e4d7dce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * **breaking** Bundlers must specify the `browser` condition when building a frontend bundle for the browser ([#8516](https://github.com/sveltejs/svelte/pull/8516)) * **breaking** Minimum supported vite-plugin-svelte version is now 2.1.1. SvelteKit users can upgrade to 1.15.9 or newer to ensure a compatible version ([#8516](https://github.com/sveltejs/svelte/pull/8516)) * **breaking** Minimum supported TypeScript version is now TypeScript 5 (it will likely work with lower versions, but we make no guarantees about that) ([#8488](https://github.com/sveltejs/svelte/pull/8488)) +* **breaking** Remove `svelte/register` hook, CJS runtime version and CJS compiler output ([#8613](https://github.com/sveltejs/svelte/pull/8613)) * **breaking** Stricter types for `createEventDispatcher` (see PR for migration instructions) ([#7224](https://github.com/sveltejs/svelte/pull/7224)) * **breaking** Stricter types for `Action` and `ActionReturn` (see PR for migration instructions) ([#7224](https://github.com/sveltejs/svelte/pull/7224)) * **breaking** Stricter types for `onMount` - now throws a type error when returning a function asynchronously to catch potential mistakes around callback functions (see PR for migration instructions) ([#8136](https://github.com/sveltejs/svelte/pull/8136)) @@ -14,6 +15,8 @@ * **breaking** Deprecate `SvelteComponentTyped`, use `SvelteComponent` instead ([#8512](https://github.com/sveltejs/svelte/pull/8512)) * **breaking** Error on falsy values instead of stores passed to `derived` ([#7947](https://github.com/sveltejs/svelte/pull/7947)) * **breaking** Custom store implementers now need to pass an `update` function additionally to the `set` function ([#6750](https://github.com/sveltejs/svelte/pull/6750)) +* **breaking** Change order in which preprocessors are applied ([#8618](https://github.com/sveltejs/svelte/pull/8618)) +* Add a way to modify attributes for script/style preprocessors ([#8618](https://github.com/sveltejs/svelte/pull/8618)) * Improve hydration speed by adding `data-svelte-h` attribute to detect unchanged HTML elements ([#7426](https://github.com/sveltejs/svelte/pull/7426)) * Add `a11y no-noninteractive-element-interactions` rule ([#8391](https://github.com/sveltejs/svelte/pull/8391)) * Add `a11y-no-static-element-interactions`rule ([#8251](https://github.com/sveltejs/svelte/pull/8251)) diff --git a/action/index.d.ts b/action/index.d.ts new file mode 100644 index 0000000000..229aeb9859 --- /dev/null +++ b/action/index.d.ts @@ -0,0 +1 @@ +export * from '../types/runtime/action/index.js'; \ No newline at end of file diff --git a/action/index.js b/action/index.js new file mode 100644 index 0000000000..eb109abbed --- /dev/null +++ b/action/index.js @@ -0,0 +1,2 @@ +'use strict'; + diff --git a/action/index.mjs b/action/index.mjs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/action/index.mjs @@ -0,0 +1 @@ + diff --git a/action/package.json b/action/package.json new file mode 100644 index 0000000000..598aeeaf51 --- /dev/null +++ b/action/package.json @@ -0,0 +1,5 @@ +{ + "main": "./index", + "module": "./index.mjs", + "types": "./index.d.ts" +} \ No newline at end of file diff --git a/animate/index.d.ts b/animate/index.d.ts new file mode 100644 index 0000000000..c6671ee75d --- /dev/null +++ b/animate/index.d.ts @@ -0,0 +1 @@ +export * from '../types/runtime/animate/index.js'; \ No newline at end of file diff --git a/animate/index.js b/animate/index.js new file mode 100644 index 0000000000..6e52923a2f --- /dev/null +++ b/animate/index.js @@ -0,0 +1,33 @@ +'use strict'; + +var easing = require('../easing/index.js'); +var Component = require('../internal/Component-9c4b98a2.js'); + +/** + * @param {Element} node + * @param {{ from: DOMRect; to: DOMRect }} fromTo + * @param {import('./public.js').FlipParams} params + * @returns {import('./public.js').AnimationConfig} + */ +function flip(node, { from, to }, params = {}) { + const style = getComputedStyle(node); + const transform = style.transform === 'none' ? '' : style.transform; + const [ox, oy] = style.transformOrigin.split(' ').map(parseFloat); + const dx = from.left + (from.width * ox) / to.width - (to.left + ox); + const dy = from.top + (from.height * oy) / to.height - (to.top + oy); + const { delay = 0, duration = (d) => Math.sqrt(d) * 120, easing: easing$1 = easing.cubicOut } = params; + return { + delay, + duration: Component.is_function(duration) ? duration(Math.sqrt(dx * dx + dy * dy)) : duration, + easing: easing$1, + css: (t, u) => { + const x = u * dx; + const y = u * dy; + const sx = t + (u * from.width) / to.width; + const sy = t + (u * from.height) / to.height; + return `transform: ${transform} translate(${x}px, ${y}px) scale(${sx}, ${sy});`; + } + }; +} + +exports.flip = flip; diff --git a/animate/index.mjs b/animate/index.mjs new file mode 100644 index 0000000000..5add71c114 --- /dev/null +++ b/animate/index.mjs @@ -0,0 +1,31 @@ +import { cubicOut } from '../easing/index.mjs'; +import { is_function } from '../internal/Component-cd97939e.mjs'; + +/** + * @param {Element} node + * @param {{ from: DOMRect; to: DOMRect }} fromTo + * @param {import('./public.js').FlipParams} params + * @returns {import('./public.js').AnimationConfig} + */ +function flip(node, { from, to }, params = {}) { + const style = getComputedStyle(node); + const transform = style.transform === 'none' ? '' : style.transform; + const [ox, oy] = style.transformOrigin.split(' ').map(parseFloat); + const dx = from.left + (from.width * ox) / to.width - (to.left + ox); + const dy = from.top + (from.height * oy) / to.height - (to.top + oy); + const { delay = 0, duration = (d) => Math.sqrt(d) * 120, easing = cubicOut } = params; + return { + delay, + duration: is_function(duration) ? duration(Math.sqrt(dx * dx + dy * dy)) : duration, + easing, + css: (t, u) => { + const x = u * dx; + const y = u * dy; + const sx = t + (u * from.width) / to.width; + const sy = t + (u * from.height) / to.height; + return `transform: ${transform} translate(${x}px, ${y}px) scale(${sx}, ${sy});`; + } + }; +} + +export { flip }; diff --git a/animate/package.json b/animate/package.json new file mode 100644 index 0000000000..598aeeaf51 --- /dev/null +++ b/animate/package.json @@ -0,0 +1,5 @@ +{ + "main": "./index", + "module": "./index.mjs", + "types": "./index.d.ts" +} \ No newline at end of file diff --git a/check_publish_env.js b/check_publish_env.js deleted file mode 100644 index ce6f066cff..0000000000 --- a/check_publish_env.js +++ /dev/null @@ -1,4 +0,0 @@ -if (!process.env.PUBLISH) { - console.error('npm publish must be run with the PUBLISH environment variable set'); - process.exit(1); -} diff --git a/codemod-lazy-props.mjs b/codemod-lazy-props.mjs deleted file mode 100644 index d1a92c4348..0000000000 --- a/codemod-lazy-props.mjs +++ /dev/null @@ -1,47 +0,0 @@ -import { existsSync, fstat, readFileSync, readdirSync, writeFileSync } from 'fs'; -import { resolve } from 'path'; -import { parse } from 'acorn'; -import { walk } from 'estree-walker'; -import { inspect } from 'util'; - -import { p, print } from 'code-red'; - -const samples = resolve(`vitest/runtime/runtime/samples`); - -for (const dir of readdirSync(samples)) { - const cwd = resolve(samples, dir); - const file = resolve(cwd, '_config.js'); - - if (!existsSync(file)) continue; - const contents = readFileSync(file, 'utf-8'); - const ast = parse(contents, { - sourceType: 'module', - ecmaVersion: 'latest', - sourceFile: file, - ranges: true - }); - - walk(ast, { - enter(node) { - if ( - node.type === 'ExportDefaultDeclaration' && - node.declaration.type === 'ObjectExpression' - ) { - this.skip(); - - const props = node.declaration.properties.find((prop) => prop.key.name === 'props'); - if (!props) return; - const { range } = props; - - const [start, end] = range; - - const code = - contents.slice(0, start) + - print(p`get ${props.key}() { return ${props.value}}`).code + - contents.slice(end); - - writeFileSync(file, code); - } - } - }); -} diff --git a/compiler.js b/compiler.js new file mode 100644 index 0000000000..a31b2644db --- /dev/null +++ b/compiler.js @@ -0,0 +1,43153 @@ +'use strict'; + +var acorn = require('acorn'); +var cssTree = require('css-tree'); +var tokenizer = require('css-tree/tokenizer'); +var MagicString = require('magic-string'); + +function _interopNamespaceDefault(e) { + var n = Object.create(null); + if (e) { + Object.keys(e).forEach(function (k) { + if (k !== 'default') { + var d = Object.getOwnPropertyDescriptor(e, k); + Object.defineProperty(n, k, d.get ? d : { + enumerable: true, + get: function () { return e[k]; } + }); + } + }); + } + n.default = e; + return Object.freeze(n); +} + +var acorn__namespace = /*#__PURE__*/_interopNamespaceDefault(acorn); + +const now = + typeof process !== 'undefined' && process.hrtime + ? () => { + const t = process.hrtime(); + return t[0] * 1e3 + t[1] / 1e6; + } + : () => self.performance.now(); + +/** @param {any} timings */ +function collapse_timings(timings) { + const result = {}; + timings.forEach( + /** @param {any} timing */ (timing) => { + result[timing.label] = Object.assign( + { + total: timing.end - timing.start + }, + timing.children && collapse_timings(timing.children) + ); + } + ); + return result; +} + +class Stats { + /** + * @typedef {Object} Timing + * @property {string} label + * @property {number} start + * @property {number} end + * @property {Timing[]} children + */ + + /** @type {number} */ + + + /** @type {Timing} */ + + + /** @type {Timing[]} */ + + + /** @type {Timing[]} */ + + + /** @type {Timing[]} */ + + constructor() { + this.start_time = now(); + this.stack = []; + this.current_children = this.timings = []; + } + + /** @param {any} label */ + start(label) { + const timing = { + label, + start: now(), + end: null, + children: [] + }; + this.current_children.push(timing); + this.stack.push(timing); + this.current_timing = timing; + this.current_children = timing.children; + } + + /** @param {any} label */ + stop(label) { + if (label !== this.current_timing.label) { + throw new Error( + `Mismatched timing labels (expected ${this.current_timing.label}, got ${label})` + ); + } + this.current_timing.end = now(); + this.stack.pop(); + this.current_timing = this.stack[this.stack.length - 1]; + this.current_children = this.current_timing ? this.current_timing.children : this.timings; + } + render() { + const timings = Object.assign( + { + total: now() - this.start_time + }, + collapse_timings(this.timings) + ); + return { + timings + }; + } +} + +/** + * @template T + * @overload + * @param {T[][]} nodes + * @param {T[]} [target] + * @returns {T[]} + */ + +/** + * @template T + * @overload + * @param {T[]} nodes + * @param {T[]} [target] + * @returns {T[]} + */ + +/** + * @param {any[]} nodes + * @param {any[]} [target] + * @returns {any[]} + */ +function flatten$1(nodes, target = []) { + for (let i = 0; i < nodes.length; i += 1) { + const node = nodes[i]; + if (Array.isArray(node)) { + flatten$1(node, target); + } else { + target.push(node); + } + } + + return target; +} + +const regex_whitespace = /\s/; +const regex_whitespaces = /\s+/; +const regex_starts_with_whitespace = /^\s/; +const regex_starts_with_whitespaces = /^[ \t\r\n]*/; +const regex_ends_with_whitespace = /\s$/; +const regex_ends_with_whitespaces = /[ \t\r\n]*$/; +const regex_only_whitespaces = /^[ \t\n\r\f]+$/; + +const regex_whitespace_characters = /\s/g; +const regex_non_whitespace_character = /\S/; + +const regex_starts_with_newline = /^\r?\n/; +const regex_not_newline_characters = /[^\n]/g; + +const regex_double_quotes = /"/g; + +const regex_backslashes = /\\/g; + +const regex_starts_with_underscore = /^_/; +const regex_ends_with_underscore = /_$/; + +const regex_dimensions = /^(?:offset|client)(?:Width|Height)$/; + +const regex_content_rect = /^(?:contentRect)$/; +const regex_content_box_size = /^(?:contentBoxSize)$/; +const regex_border_box_size = /^(?:borderBoxSize)$/; +const regex_device_pixel_content_box_size = /^(?:devicePixelContentBoxSize)$/; +const regex_box_size = + /^(?:contentRect|contentBoxSize|borderBoxSize|devicePixelContentBoxSize)$/; + +const regex_svelte_ignore = /^\s*svelte-ignore\s+([\s\S]+)\s*$/m; + +/** + * @param {string} text + * @returns {string[]} + */ +function extract_svelte_ignore(text) { + const match = regex_svelte_ignore.exec(text); + return match + ? match[1] + .split(regex_whitespace) + .map((x) => x.trim()) + .filter(Boolean) + : []; +} + +/** + * @param {import('estree').Node} node + * @returns {string[]} + */ +function extract_svelte_ignore_from_comments(node) { + return flatten$1( + (node.leadingComments || []).map((comment) => extract_svelte_ignore(comment.value)) + ); +} + +/** + * @param {number} position + * @param {import('../interfaces.js').TemplateNode[]} template_nodes + * @returns {string[]} + */ +function extract_ignores_above_position(position, template_nodes) { + const previous_node_idx = template_nodes.findIndex((child) => child.end === position); + if (previous_node_idx === -1) { + return []; + } + for (let i = previous_node_idx; i >= 0; i--) { + const node = template_nodes[i]; + if (node.type !== 'Comment' && node.type !== 'Text') { + return []; + } + if (node.type === 'Comment') { + if (node.ignores.length) { + return node.ignores; + } + } + } + return []; +} + +/** + * @param {import('../compile/nodes/interfaces.js').INode} node + * @returns {string[]} + */ +function extract_ignores_above_node(node) { + /** + * This utilizes the fact that node has a prev and a next attribute + * which means that it can find svelte-ignores along + * the nodes on the same level as itself who share the same parent. + */ + let cur_node = node.prev; + while (cur_node) { + if (cur_node.type !== 'Comment' && cur_node.type !== 'Text') { + return []; + } + if (cur_node.type === 'Comment' && cur_node.ignores.length) { + return cur_node.ignores; + } + cur_node = cur_node.prev; + } + return []; +} + +/** + * @param {string} name + * @param {string[]} names + */ +function fuzzymatch(name, names) { + const set = new FuzzySet(names); + const matches = set.get(name); + return matches && matches[0] && matches[0][0] > 0.7 ? matches[0][1] : null; +} + +// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js +// BSD Licensed +const GRAM_SIZE_LOWER = 2; +const GRAM_SIZE_UPPER = 3; +// return an edit distance from 0 to 1 + +/** + * @param {string} str1 + * @param {string} str2 + */ +function _distance(str1, str2) { + if (str1 === null && str2 === null) { + throw 'Trying to compare two null values'; + } + if (str1 === null || str2 === null) return 0; + str1 = String(str1); + str2 = String(str2); + const distance = levenshtein(str1, str2); + if (str1.length > str2.length) { + return 1 - distance / str1.length; + } else { + return 1 - distance / str2.length; + } +} + +// helper functions + +/** + * @param {string} str1 + * @param {string} str2 + */ +function levenshtein(str1, str2) { + /** + * @type {number[]} + */ + const current = []; + let prev; + let value; + for (let i = 0; i <= str2.length; i++) { + for (let j = 0; j <= str1.length; j++) { + if (i && j) { + if (str1.charAt(j - 1) === str2.charAt(i - 1)) { + value = prev; + } else { + value = Math.min(current[j], current[j - 1], prev) + 1; + } + } else { + value = i + j; + } + prev = current[j]; + current[j] = value; + } + } + return current.pop(); +} + +const non_word_regex = /[^\w, ]+/; + +/** + * @param {string} value + * @param {any} gram_size + */ +function iterate_grams(value, gram_size = 2) { + const simplified = '-' + value.toLowerCase().replace(non_word_regex, '') + '-'; + const len_diff = gram_size - simplified.length; + const results = []; + if (len_diff > 0) { + for (let i = 0; i < len_diff; ++i) { + value += '-'; + } + } + for (let i = 0; i < simplified.length - gram_size + 1; ++i) { + results.push(simplified.slice(i, i + gram_size)); + } + return results; +} + +/** + * @param {string} value + * @param {any} gram_size + */ +function gram_counter(value, gram_size = 2) { + // return an object where key=gram, value=number of occurrences + const result = {}; + const grams = iterate_grams(value, gram_size); + let i = 0; + for (i; i < grams.length; ++i) { + if (grams[i] in result) { + result[grams[i]] += 1; + } else { + result[grams[i]] = 1; + } + } + return result; +} + +/** + * @param {any} a + * @param {any} b + */ +function sort_descending(a, b) { + return b[0] - a[0]; +} + +class FuzzySet { + __init() {this.exact_set = {};} + __init2() {this.match_dict = {};} + __init3() {this.items = {};} + + /** + * @param {string[]} arr + */ + constructor(arr) {FuzzySet.prototype.__init.call(this);FuzzySet.prototype.__init2.call(this);FuzzySet.prototype.__init3.call(this); + // initialization + for (let i = GRAM_SIZE_LOWER; i < GRAM_SIZE_UPPER + 1; ++i) { + this.items[i] = []; + } + // add all the items to the set + for (let i = 0; i < arr.length; ++i) { + this.add(arr[i]); + } + } + + /** + * @param {string} value + */ + add(value) { + const normalized_value = value.toLowerCase(); + if (normalized_value in this.exact_set) { + return false; + } + let i = GRAM_SIZE_LOWER; + for (i; i < GRAM_SIZE_UPPER + 1; ++i) { + this._add(value, i); + } + } + + /** + * @param {string} value + * @param {number} gram_size + */ + _add(value, gram_size) { + const normalized_value = value.toLowerCase(); + const items = this.items[gram_size] || []; + const index = items.length; + items.push(0); + const gram_counts = gram_counter(normalized_value, gram_size); + let sum_of_square_gram_counts = 0; + let gram; + let gram_count; + for (gram in gram_counts) { + gram_count = gram_counts[gram]; + sum_of_square_gram_counts += Math.pow(gram_count, 2); + if (gram in this.match_dict) { + this.match_dict[gram].push([index, gram_count]); + } else { + this.match_dict[gram] = [[index, gram_count]]; + } + } + const vector_normal = Math.sqrt(sum_of_square_gram_counts); + items[index] = [vector_normal, normalized_value]; + this.items[gram_size] = items; + this.exact_set[normalized_value] = value; + } + + /** + * @param {string} value + */ + get(value) { + const normalized_value = value.toLowerCase(); + const result = this.exact_set[normalized_value]; + if (result) { + return [[1, result]]; + } + let results = []; + // start with high gram size and if there are no results, go to lower gram sizes + for (let gram_size = GRAM_SIZE_UPPER; gram_size >= GRAM_SIZE_LOWER; --gram_size) { + results = this.__get(value, gram_size); + if (results) { + return results; + } + } + return null; + } + + /** + * @param {string} value + * @param {number} gram_size + */ + __get(value, gram_size) { + const normalized_value = value.toLowerCase(); + const matches = {}; + const gram_counts = gram_counter(normalized_value, gram_size); + const items = this.items[gram_size]; + let sum_of_square_gram_counts = 0; + let gram; + let gram_count; + let i; + let index; + let other_gram_count; + for (gram in gram_counts) { + gram_count = gram_counts[gram]; + sum_of_square_gram_counts += Math.pow(gram_count, 2); + if (gram in this.match_dict) { + for (i = 0; i < this.match_dict[gram].length; ++i) { + index = this.match_dict[gram][i][0]; + other_gram_count = this.match_dict[gram][i][1]; + if (index in matches) { + matches[index] += gram_count * other_gram_count; + } else { + matches[index] = gram_count * other_gram_count; + } + } + } + } + const vector_normal = Math.sqrt(sum_of_square_gram_counts); + let results = []; + let match_score; + // build a results list of [score, str] + for (const match_index in matches) { + match_score = matches[match_index]; + results.push([match_score / (vector_normal * items[match_index][0]), items[match_index][1]]); + } + results.sort(sort_descending); + let new_results = []; + const end_index = Math.min(50, results.length); + // truncate somewhat arbitrarily to 50 + for (let i = 0; i < end_index; ++i) { + new_results.push([_distance(results[i][1], normalized_value), results[i][1]]); + } + results = new_results; + results.sort(sort_descending); + new_results = []; + for (let i = 0; i < results.length; ++i) { + if (results[i][0] == results[0][0]) { + new_results.push([results[i][0], this.exact_set[results[i][1]]]); + } + } + return new_results; + } +} + +/** regex of all html void element names */ +const void_element_names = + /^(?:area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/; + +/** regex of all html element names. svg and math are omitted because they belong to the svg elements namespace */ +const html_element_names = + /^(?:a|abbr|address|area|article|aside|audio|b|base|bdi|bdo|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|data|datalist|dd|del|details|dfn|dialog|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|head|header|hr|html|i|iframe|img|input|ins|kbd|label|legend|li|link|main|map|mark|meta|meter|nav|noscript|object|ol|optgroup|option|output|p|param|picture|pre|progress|q|rp|rt|ruby|s|samp|script|section|select|small|source|span|strong|style|sub|summary|sup|table|tbody|td|template|textarea|tfoot|th|thead|time|title|tr|track|u|ul|var|video|wbr)$/; + +/** regex of all svg element names */ +const svg$1 = + /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|svg|switch|symbol|text|textPath|tref|tspan|unknown|use|view|vkern)$/; + +/** + * @param {string} name + * @returns {boolean} + */ +function is_void(name) { + return void_element_names.test(name) || name.toLowerCase() === '!doctype'; +} + +/** + * @param {string} name + * @returns {boolean} + */ +function is_html(name) { + return html_element_names.test(name); +} + +/** + * @param {string} name + * @returns {boolean} + */ +function is_svg(name) { + return svg$1.test(name); +} + +/** + * @param {string[]} items + * @param {string} [conjunction] + */ +function list(items, conjunction = 'or') { + if (items.length === 1) return items[0]; + return `${items.slice(0, -1).join(', ')} ${conjunction} ${items[items.length - 1]}`; +} + +// All parser errors should be listed and accessed from here + +/** + * @internal + */ +var parser_errors = { + /** + * @param {string} message + */ + css_syntax_error: (message) => ({ + code: 'css-syntax-error', + message + }), + duplicate_attribute: { + code: 'duplicate-attribute', + message: 'Attributes need to be unique' + }, + /** + * @param {string} slug + * @param {string} name + */ + duplicate_element: (slug, name) => ({ + code: `duplicate-${slug}`, + message: `A component can only have one <${name}> tag` + }), + duplicate_style: { + code: 'duplicate-style', + message: 'You can only have one top-level \ No newline at end of file diff --git a/test/preprocess/samples/script/_config.js b/test/preprocess/samples/script/_config.js index 1a21045465..535917ade6 100644 --- a/test/preprocess/samples/script/_config.js +++ b/test/preprocess/samples/script/_config.js @@ -1,8 +1,17 @@ +import MagicString from 'magic-string'; + export default { preprocess: { - script: ({ content }) => { + script: ({ content, filename }) => { + const s = new MagicString(content); + s.overwrite( + content.indexOf('__THE_ANSWER__'), + content.indexOf('__THE_ANSWER__') + '__THE_ANSWER__'.length, + '42' + ); return { - code: content.replace('__THE_ANSWER__', '42') + code: s.toString(), + map: s.generateMap({ hires: true, file: filename }) }; } } diff --git a/test/preprocess/samples/script/expected_map.json b/test/preprocess/samples/script/expected_map.json new file mode 100644 index 0000000000..d5bf98483f --- /dev/null +++ b/test/preprocess/samples/script/expected_map.json @@ -0,0 +1,8 @@ +{ + "version": 3, + "mappings": "AAAA,CAAC,MAAM,CAAC;AACR,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAc,CAAC,CAAC;AAC7B,CAAC,CAAC,MAAM", + "names": [], + "sources": [ + "input.svelte" + ] +} \ No newline at end of file diff --git a/test/preprocess/samples/style-attributes-modified-longer/_config.js b/test/preprocess/samples/style-attributes-modified-longer/_config.js new file mode 100644 index 0000000000..1fbb8b3d23 --- /dev/null +++ b/test/preprocess/samples/style-attributes-modified-longer/_config.js @@ -0,0 +1,12 @@ +import * as assert from 'node:assert'; + +export default { + preprocess: { + style: ({ attributes }) => { + assert.deepEqual(attributes, { + lang: 'scss' + }); + return { code: 'PROCESSED', attributes: { sth: 'wayyyyyyyyyyyyy looooooonger' } }; + } + } +}; diff --git a/test/preprocess/samples/style-attributes-modified-longer/expected_map.json b/test/preprocess/samples/style-attributes-modified-longer/expected_map.json new file mode 100644 index 0000000000..28397f95d9 --- /dev/null +++ b/test/preprocess/samples/style-attributes-modified-longer/expected_map.json @@ -0,0 +1,8 @@ +{ + "version": 3, + "mappings": "AAAA;;AAEA,MAAM,mCAAa,UAAM,CAAC,CAAC,KAAK;;AAEhC", + "names": [], + "sources": [ + "input.svelte" + ] +} \ No newline at end of file diff --git a/test/preprocess/samples/style-attributes-modified-longer/input.svelte b/test/preprocess/samples/style-attributes-modified-longer/input.svelte new file mode 100644 index 0000000000..8b2713472a --- /dev/null +++ b/test/preprocess/samples/style-attributes-modified-longer/input.svelte @@ -0,0 +1,5 @@ +foo + + + +bar diff --git a/test/preprocess/samples/style-attributes-modified-longer/output.svelte b/test/preprocess/samples/style-attributes-modified-longer/output.svelte new file mode 100644 index 0000000000..e9971f7f18 --- /dev/null +++ b/test/preprocess/samples/style-attributes-modified-longer/output.svelte @@ -0,0 +1,5 @@ +foo + + + +bar diff --git a/test/preprocess/samples/style-attributes-modified/_config.js b/test/preprocess/samples/style-attributes-modified/_config.js new file mode 100644 index 0000000000..cfeae0c027 --- /dev/null +++ b/test/preprocess/samples/style-attributes-modified/_config.js @@ -0,0 +1,14 @@ +import * as assert from 'node:assert'; + +export default { + preprocess: { + style: ({ attributes }) => { + assert.deepEqual(attributes, { + lang: 'scss', + 'data-foo': 'bar', + bool: true + }); + return { code: 'PROCESSED', attributes: { sth: 'else' } }; + } + } +}; diff --git a/test/preprocess/samples/style-attributes-modified/expected_map.json b/test/preprocess/samples/style-attributes-modified/expected_map.json new file mode 100644 index 0000000000..65f340c1c0 --- /dev/null +++ b/test/preprocess/samples/style-attributes-modified/expected_map.json @@ -0,0 +1,8 @@ +{ + "version": 3, + "mappings": "AAAA;;AAEA,MAAM,WAAiC,UAAM,CAAC,CAAC,KAAK;;AAEpD", + "names": [], + "sources": [ + "input.svelte" + ] +} \ No newline at end of file diff --git a/test/preprocess/samples/style-attributes-modified/input.svelte b/test/preprocess/samples/style-attributes-modified/input.svelte new file mode 100644 index 0000000000..2b2b0e4423 --- /dev/null +++ b/test/preprocess/samples/style-attributes-modified/input.svelte @@ -0,0 +1,5 @@ +foo + + + +bar diff --git a/test/preprocess/samples/style-attributes-modified/output.svelte b/test/preprocess/samples/style-attributes-modified/output.svelte new file mode 100644 index 0000000000..ff8ca98bf9 --- /dev/null +++ b/test/preprocess/samples/style-attributes-modified/output.svelte @@ -0,0 +1,5 @@ +foo + + + +bar diff --git a/test/preprocess/samples/style-attributes/expected_map.json b/test/preprocess/samples/style-attributes/expected_map.json new file mode 100644 index 0000000000..2eca3ec763 --- /dev/null +++ b/test/preprocess/samples/style-attributes/expected_map.json @@ -0,0 +1,8 @@ +{ + "version": 3, + "mappings": "AAAA,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,UAAO,CAAC,CAAC,KAAK", + "names": [], + "sources": [ + "input.svelte" + ] +} \ No newline at end of file diff --git a/test/preprocess/samples/style-attributes/input.svelte b/test/preprocess/samples/style-attributes/input.svelte index 3a55a5a3f6..c513b8bf21 100644 --- a/test/preprocess/samples/style-attributes/input.svelte +++ b/test/preprocess/samples/style-attributes/input.svelte @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/test/runtime/runtime.shared.js b/test/runtime/runtime.shared.js index 95a648771b..0d317fb360 100644 --- a/test/runtime/runtime.shared.js +++ b/test/runtime/runtime.shared.js @@ -56,7 +56,6 @@ async function run_test(dir) { const cwd = path.resolve(`${__dirname}/samples/${dir}`); const compileOptions = Object.assign({}, config.compileOptions || {}, { - format: 'cjs', hydratable: hydrate, immutable: config.immutable, accessors: 'accessors' in config ? config.accessors : true diff --git a/test/runtime/runtime_base.test.js b/test/runtime/runtime_base.test.js index 756c63c187..4f912eb444 100644 --- a/test/runtime/runtime_base.test.js +++ b/test/runtime/runtime_base.test.js @@ -3,7 +3,7 @@ import { create_loader } from '../helpers'; import { assert, it } from 'vitest'; -const load = create_loader({ generate: 'dom', dev: true, format: 'cjs' }, __dirname); +const load = create_loader({ generate: 'dom', dev: true }, __dirname); const { default: App } = await load('App.svelte'); it('fails if options.target is missing in dev mode', async () => { diff --git a/test/runtime/samples/component-slot-duplicate-error-2/Nested.svelte b/test/runtime/samples/component-slot-duplicate-error-2/Nested.svelte deleted file mode 100644 index 32eee1534a..0000000000 --- a/test/runtime/samples/component-slot-duplicate-error-2/Nested.svelte +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/test/runtime/samples/component-slot-duplicate-error-3/Nested.svelte b/test/runtime/samples/component-slot-duplicate-error-3/Nested.svelte deleted file mode 100644 index 32eee1534a..0000000000 --- a/test/runtime/samples/component-slot-duplicate-error-3/Nested.svelte +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/test/runtime/samples/component-slot-duplicate-error-4/Nested.svelte b/test/runtime/samples/component-slot-duplicate-error-4/Nested.svelte deleted file mode 100644 index 0385342cef..0000000000 --- a/test/runtime/samples/component-slot-duplicate-error-4/Nested.svelte +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/test/runtime/samples/component-slot-duplicate-error/Nested.svelte b/test/runtime/samples/component-slot-duplicate-error/Nested.svelte deleted file mode 100644 index 32eee1534a..0000000000 --- a/test/runtime/samples/component-slot-duplicate-error/Nested.svelte +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/test/runtime/samples/component-slot-nested-error-2/Nested.svelte b/test/runtime/samples/component-slot-nested-error-2/Nested.svelte deleted file mode 100644 index c6f086d96c..0000000000 --- a/test/runtime/samples/component-slot-nested-error-2/Nested.svelte +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/test/runtime/samples/component-slot-nested-error-3/Nested.svelte b/test/runtime/samples/component-slot-nested-error-3/Nested.svelte deleted file mode 100644 index c6f086d96c..0000000000 --- a/test/runtime/samples/component-slot-nested-error-3/Nested.svelte +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/test/runtime/samples/component-slot-nested-error/Nested.svelte b/test/runtime/samples/component-slot-nested-error/Nested.svelte deleted file mode 100644 index c6f086d96c..0000000000 --- a/test/runtime/samples/component-slot-nested-error/Nested.svelte +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/test/runtime/samples/store-imported-module-b/foo.js b/test/runtime/samples/store-imported-module-b/foo.js deleted file mode 100644 index b88c1cc577..0000000000 --- a/test/runtime/samples/store-imported-module-b/foo.js +++ /dev/null @@ -1,3 +0,0 @@ -import { writable } from 'svelte/store'; - -export default writable(42); diff --git a/test/server-side-rendering/ssr-1.test.js b/test/server-side-rendering/ssr-1.test.js index 9eb19aae41..43732e8eb7 100644 --- a/test/server-side-rendering/ssr-1.test.js +++ b/test/server-side-rendering/ssr-1.test.js @@ -30,8 +30,7 @@ describe('ssr', async () => { const compileOptions = { ...config.compileOptions, - generate: 'ssr', - format: 'cjs' + generate: 'ssr' }; const load = create_loader(compileOptions, dir); diff --git a/test/server-side-rendering/ssr-2.test.js b/test/server-side-rendering/ssr-2.test.js index acf823d0b0..f24edfc2c7 100644 --- a/test/server-side-rendering/ssr-2.test.js +++ b/test/server-side-rendering/ssr-2.test.js @@ -33,8 +33,7 @@ function run_runtime_samples(suite) { it_fn(dir, async () => { const compileOptions = { ...config.compileOptions, - generate: 'ssr', - format: 'cjs' + generate: 'ssr' }; const load = create_loader(compileOptions, cwd); diff --git a/transition/index.d.ts b/transition/index.d.ts new file mode 100644 index 0000000000..ac81403395 --- /dev/null +++ b/transition/index.d.ts @@ -0,0 +1 @@ +export * from '../types/runtime/transition/index.js'; \ No newline at end of file diff --git a/transition/index.js b/transition/index.js new file mode 100644 index 0000000000..b981c3b717 --- /dev/null +++ b/transition/index.js @@ -0,0 +1,244 @@ +'use strict'; + +var easing = require('../easing/index.js'); +var Component = require('../internal/Component-9c4b98a2.js'); + +/** + * @param {Element} node + * @param {import('./public').BlurParams} [params] + * @returns {import('./public').TransitionConfig} + */ +function blur( + node, + { delay = 0, duration = 400, easing: easing$1 = easing.cubicInOut, amount = 5, opacity = 0 } = {} +) { + const style = getComputedStyle(node); + const target_opacity = +style.opacity; + const f = style.filter === 'none' ? '' : style.filter; + const od = target_opacity * (1 - opacity); + const [value, unit] = Component.split_css_unit(amount); + return { + delay, + duration, + easing: easing$1, + css: (_t, u) => `opacity: ${target_opacity - od * u}; filter: ${f} blur(${u * value}${unit});` + }; +} + +/** + * @param {Element} node + * @param {import('./public').FadeParams} [params] + * @returns {import('./public').TransitionConfig} + */ +function fade(node, { delay = 0, duration = 400, easing = Component.identity } = {}) { + const o = +getComputedStyle(node).opacity; + return { + delay, + duration, + easing, + css: (t) => `opacity: ${t * o}` + }; +} + +/** + * @param {Element} node + * @param {import('./public').FlyParams} [params] + * @returns {import('./public').TransitionConfig} + */ +function fly( + node, + { delay = 0, duration = 400, easing: easing$1 = easing.cubicOut, x = 0, y = 0, opacity = 0 } = {} +) { + const style = getComputedStyle(node); + const target_opacity = +style.opacity; + const transform = style.transform === 'none' ? '' : style.transform; + const od = target_opacity * (1 - opacity); + const [xValue, xUnit] = Component.split_css_unit(x); + const [yValue, yUnit] = Component.split_css_unit(y); + return { + delay, + duration, + easing: easing$1, + css: (t, u) => ` + transform: ${transform} translate(${(1 - t) * xValue}${xUnit}, ${(1 - t) * yValue}${yUnit}); + opacity: ${target_opacity - od * u}` + }; +} + +/** + * @param {Element} node + * @param {import('./public').SlideParams} [params] + * @returns {import('./public').TransitionConfig} + */ +function slide(node, { delay = 0, duration = 400, easing: easing$1 = easing.cubicOut, axis = 'y' } = {}) { + const style = getComputedStyle(node); + const opacity = +style.opacity; + const primary_property = axis === 'y' ? 'height' : 'width'; + const primary_property_value = parseFloat(style[primary_property]); + const secondary_properties = axis === 'y' ? ['top', 'bottom'] : ['left', 'right']; + const capitalized_secondary_properties = secondary_properties.map( + (e) => `${e[0].toUpperCase()}${e.slice(1)}` + ); + const padding_start_value = parseFloat(style[`padding${capitalized_secondary_properties[0]}`]); + const padding_end_value = parseFloat(style[`padding${capitalized_secondary_properties[1]}`]); + const margin_start_value = parseFloat(style[`margin${capitalized_secondary_properties[0]}`]); + const margin_end_value = parseFloat(style[`margin${capitalized_secondary_properties[1]}`]); + const border_width_start_value = parseFloat( + style[`border${capitalized_secondary_properties[0]}Width`] + ); + const border_width_end_value = parseFloat( + style[`border${capitalized_secondary_properties[1]}Width`] + ); + return { + delay, + duration, + easing: easing$1, + css: (t) => + 'overflow: hidden;' + + `opacity: ${Math.min(t * 20, 1) * opacity};` + + `${primary_property}: ${t * primary_property_value}px;` + + `padding-${secondary_properties[0]}: ${t * padding_start_value}px;` + + `padding-${secondary_properties[1]}: ${t * padding_end_value}px;` + + `margin-${secondary_properties[0]}: ${t * margin_start_value}px;` + + `margin-${secondary_properties[1]}: ${t * margin_end_value}px;` + + `border-${secondary_properties[0]}-width: ${t * border_width_start_value}px;` + + `border-${secondary_properties[1]}-width: ${t * border_width_end_value}px;` + }; +} + +/** + * @param {Element} node + * @param {import('./public').ScaleParams} [params] + * @returns {import('./public').TransitionConfig} + */ +function scale( + node, + { delay = 0, duration = 400, easing: easing$1 = easing.cubicOut, start = 0, opacity = 0 } = {} +) { + const style = getComputedStyle(node); + const target_opacity = +style.opacity; + const transform = style.transform === 'none' ? '' : style.transform; + const sd = 1 - start; + const od = target_opacity * (1 - opacity); + return { + delay, + duration, + easing: easing$1, + css: (_t, u) => ` + transform: ${transform} scale(${1 - sd * u}); + opacity: ${target_opacity - od * u} + ` + }; +} + +/** + * @param {SVGElement & { getTotalLength(): number }} node + * @param {import('./public').DrawParams} [params] + * @returns {import('./public').TransitionConfig} + */ +function draw(node, { delay = 0, speed, duration, easing: easing$1 = easing.cubicInOut } = {}) { + let len = node.getTotalLength(); + const style = getComputedStyle(node); + if (style.strokeLinecap !== 'butt') { + len += parseInt(style.strokeWidth); + } + if (duration === undefined) { + if (speed === undefined) { + duration = 800; + } else { + duration = len / speed; + } + } else if (typeof duration === 'function') { + duration = duration(len); + } + return { + delay, + duration, + easing: easing$1, + css: (_, u) => ` + stroke-dasharray: ${len}; + stroke-dashoffset: ${u * len}; + ` + }; +} + +/** + * @param {import('./public').CrossfadeParams & { + * fallback?: (node: Element, params: import('./public').CrossfadeParams, intro: boolean) => import('./public').TransitionConfig; + * }} params + * @returns {[(node: any, params: import('./public').CrossfadeParams & { key: any; }) => () => import('./public').TransitionConfig, (node: any, params: import('./public').CrossfadeParams & { key: any; }) => () => import('./public').TransitionConfig]} + */ +function crossfade({ fallback, ...defaults }) { + /** @type {Map} */ + const to_receive = new Map(); + /** @type {Map} */ + const to_send = new Map(); + /** + * @param {Element} from_node + * @param {Element} node + * @param {import('./public').CrossfadeParams} params + * @returns {import('./public').TransitionConfig} + */ + function crossfade(from_node, node, params) { + const { + delay = 0, + duration = (d) => Math.sqrt(d) * 30, + easing: easing$1 = easing.cubicOut + } = Component.assign(Component.assign({}, defaults), params); + const from = from_node.getBoundingClientRect(); + const to = node.getBoundingClientRect(); + const dx = from.left - to.left; + const dy = from.top - to.top; + const dw = from.width / to.width; + const dh = from.height / to.height; + const d = Math.sqrt(dx * dx + dy * dy); + const style = getComputedStyle(node); + const transform = style.transform === 'none' ? '' : style.transform; + const opacity = +style.opacity; + return { + delay, + duration: Component.is_function(duration) ? duration(d) : duration, + easing: easing$1, + css: (t, u) => ` + opacity: ${t * opacity}; + transform-origin: top left; + transform: ${transform} translate(${u * dx}px,${u * dy}px) scale(${t + (1 - t) * dw}, ${ + t + (1 - t) * dh + }); + ` + }; + } + + /** + * @param {Map} items + * @param {Map} counterparts + * @param {boolean} intro + * @returns {(node: any, params: import('./public').CrossfadeParams & { key: any; }) => () => import('./public').TransitionConfig} + */ + function transition(items, counterparts, intro) { + return (node, params) => { + items.set(params.key, node); + return () => { + if (counterparts.has(params.key)) { + const other_node = counterparts.get(params.key); + counterparts.delete(params.key); + return crossfade(other_node, node, params); + } + // if the node is disappearing altogether + // (i.e. wasn't claimed by the other list) + // then we need to supply an outro + items.delete(params.key); + return fallback && fallback(node, params, intro); + }; + }; + } + return [transition(to_send, to_receive, false), transition(to_receive, to_send, true)]; +} + +exports.blur = blur; +exports.crossfade = crossfade; +exports.draw = draw; +exports.fade = fade; +exports.fly = fly; +exports.scale = scale; +exports.slide = slide; diff --git a/transition/index.mjs b/transition/index.mjs new file mode 100644 index 0000000000..5168c39a17 --- /dev/null +++ b/transition/index.mjs @@ -0,0 +1,236 @@ +import { cubicInOut, cubicOut } from '../easing/index.mjs'; +import { split_css_unit, identity, assign, is_function } from '../internal/Component-cd97939e.mjs'; + +/** + * @param {Element} node + * @param {import('./public').BlurParams} [params] + * @returns {import('./public').TransitionConfig} + */ +function blur( + node, + { delay = 0, duration = 400, easing = cubicInOut, amount = 5, opacity = 0 } = {} +) { + const style = getComputedStyle(node); + const target_opacity = +style.opacity; + const f = style.filter === 'none' ? '' : style.filter; + const od = target_opacity * (1 - opacity); + const [value, unit] = split_css_unit(amount); + return { + delay, + duration, + easing, + css: (_t, u) => `opacity: ${target_opacity - od * u}; filter: ${f} blur(${u * value}${unit});` + }; +} + +/** + * @param {Element} node + * @param {import('./public').FadeParams} [params] + * @returns {import('./public').TransitionConfig} + */ +function fade(node, { delay = 0, duration = 400, easing = identity } = {}) { + const o = +getComputedStyle(node).opacity; + return { + delay, + duration, + easing, + css: (t) => `opacity: ${t * o}` + }; +} + +/** + * @param {Element} node + * @param {import('./public').FlyParams} [params] + * @returns {import('./public').TransitionConfig} + */ +function fly( + node, + { delay = 0, duration = 400, easing = cubicOut, x = 0, y = 0, opacity = 0 } = {} +) { + const style = getComputedStyle(node); + const target_opacity = +style.opacity; + const transform = style.transform === 'none' ? '' : style.transform; + const od = target_opacity * (1 - opacity); + const [xValue, xUnit] = split_css_unit(x); + const [yValue, yUnit] = split_css_unit(y); + return { + delay, + duration, + easing, + css: (t, u) => ` + transform: ${transform} translate(${(1 - t) * xValue}${xUnit}, ${(1 - t) * yValue}${yUnit}); + opacity: ${target_opacity - od * u}` + }; +} + +/** + * @param {Element} node + * @param {import('./public').SlideParams} [params] + * @returns {import('./public').TransitionConfig} + */ +function slide(node, { delay = 0, duration = 400, easing = cubicOut, axis = 'y' } = {}) { + const style = getComputedStyle(node); + const opacity = +style.opacity; + const primary_property = axis === 'y' ? 'height' : 'width'; + const primary_property_value = parseFloat(style[primary_property]); + const secondary_properties = axis === 'y' ? ['top', 'bottom'] : ['left', 'right']; + const capitalized_secondary_properties = secondary_properties.map( + (e) => `${e[0].toUpperCase()}${e.slice(1)}` + ); + const padding_start_value = parseFloat(style[`padding${capitalized_secondary_properties[0]}`]); + const padding_end_value = parseFloat(style[`padding${capitalized_secondary_properties[1]}`]); + const margin_start_value = parseFloat(style[`margin${capitalized_secondary_properties[0]}`]); + const margin_end_value = parseFloat(style[`margin${capitalized_secondary_properties[1]}`]); + const border_width_start_value = parseFloat( + style[`border${capitalized_secondary_properties[0]}Width`] + ); + const border_width_end_value = parseFloat( + style[`border${capitalized_secondary_properties[1]}Width`] + ); + return { + delay, + duration, + easing, + css: (t) => + 'overflow: hidden;' + + `opacity: ${Math.min(t * 20, 1) * opacity};` + + `${primary_property}: ${t * primary_property_value}px;` + + `padding-${secondary_properties[0]}: ${t * padding_start_value}px;` + + `padding-${secondary_properties[1]}: ${t * padding_end_value}px;` + + `margin-${secondary_properties[0]}: ${t * margin_start_value}px;` + + `margin-${secondary_properties[1]}: ${t * margin_end_value}px;` + + `border-${secondary_properties[0]}-width: ${t * border_width_start_value}px;` + + `border-${secondary_properties[1]}-width: ${t * border_width_end_value}px;` + }; +} + +/** + * @param {Element} node + * @param {import('./public').ScaleParams} [params] + * @returns {import('./public').TransitionConfig} + */ +function scale( + node, + { delay = 0, duration = 400, easing = cubicOut, start = 0, opacity = 0 } = {} +) { + const style = getComputedStyle(node); + const target_opacity = +style.opacity; + const transform = style.transform === 'none' ? '' : style.transform; + const sd = 1 - start; + const od = target_opacity * (1 - opacity); + return { + delay, + duration, + easing, + css: (_t, u) => ` + transform: ${transform} scale(${1 - sd * u}); + opacity: ${target_opacity - od * u} + ` + }; +} + +/** + * @param {SVGElement & { getTotalLength(): number }} node + * @param {import('./public').DrawParams} [params] + * @returns {import('./public').TransitionConfig} + */ +function draw(node, { delay = 0, speed, duration, easing = cubicInOut } = {}) { + let len = node.getTotalLength(); + const style = getComputedStyle(node); + if (style.strokeLinecap !== 'butt') { + len += parseInt(style.strokeWidth); + } + if (duration === undefined) { + if (speed === undefined) { + duration = 800; + } else { + duration = len / speed; + } + } else if (typeof duration === 'function') { + duration = duration(len); + } + return { + delay, + duration, + easing, + css: (_, u) => ` + stroke-dasharray: ${len}; + stroke-dashoffset: ${u * len}; + ` + }; +} + +/** + * @param {import('./public').CrossfadeParams & { + * fallback?: (node: Element, params: import('./public').CrossfadeParams, intro: boolean) => import('./public').TransitionConfig; + * }} params + * @returns {[(node: any, params: import('./public').CrossfadeParams & { key: any; }) => () => import('./public').TransitionConfig, (node: any, params: import('./public').CrossfadeParams & { key: any; }) => () => import('./public').TransitionConfig]} + */ +function crossfade({ fallback, ...defaults }) { + /** @type {Map} */ + const to_receive = new Map(); + /** @type {Map} */ + const to_send = new Map(); + /** + * @param {Element} from_node + * @param {Element} node + * @param {import('./public').CrossfadeParams} params + * @returns {import('./public').TransitionConfig} + */ + function crossfade(from_node, node, params) { + const { + delay = 0, + duration = (d) => Math.sqrt(d) * 30, + easing = cubicOut + } = assign(assign({}, defaults), params); + const from = from_node.getBoundingClientRect(); + const to = node.getBoundingClientRect(); + const dx = from.left - to.left; + const dy = from.top - to.top; + const dw = from.width / to.width; + const dh = from.height / to.height; + const d = Math.sqrt(dx * dx + dy * dy); + const style = getComputedStyle(node); + const transform = style.transform === 'none' ? '' : style.transform; + const opacity = +style.opacity; + return { + delay, + duration: is_function(duration) ? duration(d) : duration, + easing, + css: (t, u) => ` + opacity: ${t * opacity}; + transform-origin: top left; + transform: ${transform} translate(${u * dx}px,${u * dy}px) scale(${t + (1 - t) * dw}, ${ + t + (1 - t) * dh + }); + ` + }; + } + + /** + * @param {Map} items + * @param {Map} counterparts + * @param {boolean} intro + * @returns {(node: any, params: import('./public').CrossfadeParams & { key: any; }) => () => import('./public').TransitionConfig} + */ + function transition(items, counterparts, intro) { + return (node, params) => { + items.set(params.key, node); + return () => { + if (counterparts.has(params.key)) { + const other_node = counterparts.get(params.key); + counterparts.delete(params.key); + return crossfade(other_node, node, params); + } + // if the node is disappearing altogether + // (i.e. wasn't claimed by the other list) + // then we need to supply an outro + items.delete(params.key); + return fallback && fallback(node, params, intro); + }; + }; + } + return [transition(to_send, to_receive, false), transition(to_receive, to_send, true)]; +} + +export { blur, crossfade, draw, fade, fly, scale, slide }; diff --git a/transition/package.json b/transition/package.json new file mode 100644 index 0000000000..598aeeaf51 --- /dev/null +++ b/transition/package.json @@ -0,0 +1,5 @@ +{ + "main": "./index", + "module": "./index.mjs", + "types": "./index.d.ts" +} \ No newline at end of file