mirror of https://github.com/sveltejs/svelte
commit
53b9b8f335
@ -1,5 +0,0 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: allow characters in the supplementary special-purpose plane
|
@ -0,0 +1,166 @@
|
||||
---
|
||||
title: {@attach ...}
|
||||
---
|
||||
|
||||
Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates.
|
||||
|
||||
Optionally, they can return a function that is called before the attachment re-runs, or after the element is later removed from the DOM.
|
||||
|
||||
> [!NOTE]
|
||||
> Attachments are available in Svelte 5.29 and newer.
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
/** @type {import('svelte/attachments').Attachment} */
|
||||
function myAttachment(element) {
|
||||
console.log(element.nodeName); // 'DIV'
|
||||
|
||||
return () => {
|
||||
console.log('cleaning up');
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div {@attach myAttachment}>...</div>
|
||||
```
|
||||
|
||||
An element can have any number of attachments.
|
||||
|
||||
## Attachment factories
|
||||
|
||||
A useful pattern is for a function, such as `tooltip` in this example, to _return_ an attachment ([demo](/playground/untitled#H4sIAAAAAAAAE3VT0XLaMBD8lavbDiaNCUlbHhTItG_5h5AH2T5ArdBppDOEMv73SkbGJGnH47F9t3un3TsfMyO3mInsh2SW1Sa7zlZKo8_E0zHjg42pGAjxBPxp7cTvUHOMldLjv-IVGUbDoUw295VTlh-WZslqa8kxsLL2ACtHWxh175NffnQfAAGikSGxYQGfPEvGfPSIWtOH0TiBVo2pWJEBJtKhQp4YYzjG9JIdcuMM5IZqHMPioY8vOSA997zQoevf4a7heO7cdp34olRiTGr07OhwH1IdoO2A7dLMbwahZq6MbRhKZWqxk7rBxTGVbuHmhCgb5qDgmIx_J6XtHHukHTrYYqx_YpzYng8aO4RYayql7hU-1ZJl0akqHBE_D9KLolwL-Dibzc7iSln9XjtqTF1UpMkJ2EmXR-BgQErsN4pxIJKr0RVO1qrxAqaTO4fbc9bKulZm3cfDY3aZDgvFGErWjmzhN7KmfX5rXyDeX8Pt1mU-hXjdBOrtuB97vK4GPUtmJ41XcRMEGDLD8do0nJ73zhUhSlyRw0t3vPqD8cjfLs-axiFgNBrkUd9Ulp50c-GLxlXAVlJX-ffpZyiSn7H0eLCUySZQcQdXlxj4El0Yv_FZvIKElqqGTruVLhzu7VRKCh22_5toOyxsWqLwwzK-cCbYNdg-hy-p9D7sbiZWUnts_wLUOF3CJgQAAA==)):
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import tippy from 'tippy.js';
|
||||
|
||||
let content = $state('Hello!');
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @returns {import('svelte/attachments').Attachment}
|
||||
*/
|
||||
function tooltip(content) {
|
||||
return (element) => {
|
||||
const tooltip = tippy(element, { content });
|
||||
return tooltip.destroy;
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<input bind:value={content} />
|
||||
|
||||
<button {@attach tooltip(content)}>
|
||||
Hover me
|
||||
</button>
|
||||
```
|
||||
|
||||
Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
|
||||
|
||||
## Inline attachments
|
||||
|
||||
Attachments can also be created inline ([demo](/playground/untitled#H4sIAAAAAAAAE71Wf3OaWBT9KoyTTnW3MS-I3dYmnWXVtnRAazRJzbozRSQEApiRhwKO333vuY8m225m_9yZGOT9OPfcc84D943UTfxGr_G7K6Xr3TVeNW7D2M8avT_3DVk-YAoDNF4vNB8e2tnWjyXGlm7mPzfurVPpp5JgGmeZtwkf5PtFupCxLzVvHa832rl2lElX-s2Xm2DZFNqp_hs-rZetd4v07ORpT3qmQHu7MF2td0BZp8k6z_xkvfXP902_pZ2_1_aYWEiqm0kN8I4r79qbdZ6umnq3q_2iNf22F4dE6qt2oimwdpim_uY6XMm7Fuo-IQT_iTD_CeGTHwZ38ieIJUFQRxirR1Xf39Dw0X5z0I72Af4tD61vvPNwWKQnqmfPTbduhsEd2J3vO_oBd3dc6fF2X7umNdWGf0vBRhSS6qoV7cCXfTXWfKmvWG61_si_vfU92Wz-E4RhsLhNIYinsox9QKGVd8-tuACCeKXRX12P-T_eKf7fhTq0Hvt-f3ailtSeoxJHRo1-58NoPe1UiBc1hkL8Yeh45y_vQ3mcuNl9T8s3cXPRWLnS7YWJG_gn2Tb4tUjid8jua-PVl08j_ab8I14mH8Llx0s5Tz5Err4ql52r_GYg0mVy1bEGZuD0ze64b5TWYFiM-16wSuJ4JT5vfVpDcztrcG_YkRU4s6HxufzDWF4XuVeJ1P10IbzBemt3Vp1V2e04ZXfrJd7Wicyd039brRIv_RIVu_nXi7X1cfL2sy66ztToUp1TO7qJ7NlwZ0f30pld5qNSVE5o6PbMojFHjgZB7oSicPpGteyLclQap7SvY0dXtM_LR1NT2JFHey3aaxa0VxCeYJ7RMHemoiCcgPZV9pR7o7kgcOjeGliYk9hjDZx8FAq6enwlTPSZj_vYPw9Il64dXdIY8ZmapzwfEd8-1ZyaxWhqkIZOibXUd-6Upqi1pD4uMicCV1GA_7zi73UN8BaF4sC8peJtMjfmjbHZBFwq5ov50qRaE0l96NZggnW4KqypYRAW-uhSz9ADvklwJF2J-5W0Z5fQPBhDX92R6I_0IFxRgDftge4l4dP-gH1hjD7uqU6fsOEZ9UNrCdPB-nys6uXgY6O3ZMd9sy5T9PghqrWHdjo4jB51CgLiKJaDYYA-7WgYONf1FbjkI-mE3EAfUY_rijfuJ_CVPaR50oe9JF7Q0pI8Dw3osxxYHdYPGbp2CnwHF8KvwJv2wEv0Z3ilQI6U9uwbZxbYJXvEmjjQjjCHkvNLvNg3yhzXQd1olamsT4IRrZmX0MUDpwL7R8zzHj7pSh9hPHFSHjLezKqAST51uC5zmtQ87skDUaneLokT5RbXkPWSYz53Abgjc8_o4KFGUZ-Hgv2Z1l5OTYM9D-HfUD0L-EwxH5wRnIG61gS-khfgY1bq7IAP_DA4l5xRuh9xlm8yGjutc8t-wHtkhWv3hc7aqGwiK5KzgvM5xRkZYn193uEln-su55j1GaIv7oM4iPrsVHiG0Dx7TR9-1lBfqFdwfvSd5LNL5xyZVp5NoHFZ57FkfiF6vKs4k5zvIfrX5xX6MXmt0gM5MTu8DjnhukrHHzTRd3jm0dma0_f_x5cxP9f4jBdqHvmbq2fUjzqcKh2Cp-yWj9ntcHanXmBXxhu7Q--eyjhfNFpaV7zgz4nWEUb7zUOhpevjjf_gu_KZ99pxFlZ-T3sttkmYqrco_26q35v0Ewzv5EZPbnL_8BfduWGMnyyN3q0bZ_7hb_7KG_L4CQAA)):
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<canvas
|
||||
width={32}
|
||||
height={32}
|
||||
{@attach (canvas) => {
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
$effect(() => {
|
||||
context.fillStyle = color;
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
});
|
||||
}}
|
||||
></canvas>
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The nested effect runs whenever `color` changes, while the outer effect (where `canvas.getContext(...)` is called) only runs once, since it doesn't read any reactive state.
|
||||
|
||||
## Passing attachments to components
|
||||
|
||||
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments.
|
||||
|
||||
This allows you to create _wrapper components_ that augment elements ([demo](/playground/untitled#H4sIAAAAAAAAE3VUS3ObMBD-KxvajnFqsJM2PhA7TXrKob31FjITAbKtRkiMtDhJPfz3LiAMdpxhGJvdb1_fPnaeYjn3Iu-WIbJ04028lZDcetHDzsO3olbVApI74F1RhHbLJdayhFl-Sp5qhVwhufEWNjWiwJtYxSjyQhsEFEXxBiujcxg1_8O_dnQ9APwsEbVyiHDafjrvDZCgkiO4MLCEzxYZcn90z6XUZ6OxA61KlaIgV6i1pFC-sxjDrlbHaDiWRoGvdMbHsLzp5DES0mJnRxGaRBvcBHb7yFUTCQeunEWYcYtGv12TqgFUDbCK1WLaM6IWQhUlQiJUFm2ZLPly51xXMG0Rjoyd69C7UqqG2nu95QZyXvtvLVpri2-SN4hoLXXCZFfhQ8aQBU1VgdEaH_vSgyBZR_BpPp_vi0tY-rw2ulRZkGqpTQRbZvwa2BPgFC8bgbw31CbjJjAsE6WNYBZeGp7vtQXLMqHWnZx-5kM1TR5ycpkZXQR2wzL94l8Ur1C_3-g168SfQf1MyfRi3LW9fs77emJEw5QV9SREoLTq06tcczq7d6xEUcJX2vAhO1b843XK34e5unZEMBr15ekuKEusluWAF8lXhE2ZTP2r2RcIHJ-163FPKerCgYJLOB9i4GvNwviI5-gAQiFFBk3tBTOU3HFXEk0R8o86WvUD64aINhv5K3oRmpJXkw8uxMG6Hh6JY9X7OwGSqfUy9tDG3sHNoEi0d_d_fv9qndxRU0VClFqo3KVo3U655Hnt1PXB3Qra2Y2QGdEwgTAMCxopsoxOe6SD0gD8movDhT0LAnhqlE8gVCpLWnRoV7OJCkFAwEXitrYL1W7p7pbiE_P7XH6E_rihODm5s52XtiH9Ekaw0VgI9exadWL1uoEYjPtg2672k5szsxbKyWB2fdT0w5Y_0hcT8oXOlRetmLS8-g-6TLXXQgYAAA==)):
|
||||
|
||||
```svelte
|
||||
<!--- file: Button.svelte --->
|
||||
<script>
|
||||
/** @type {import('svelte/elements').HTMLButtonAttributes} */
|
||||
let { children, ...props } = $props();
|
||||
</script>
|
||||
|
||||
<!-- `props` includes attachments -->
|
||||
<button {...props}>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import tippy from 'tippy.js';
|
||||
import Button from './Button.svelte';
|
||||
|
||||
let content = $state('Hello!');
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @returns {import('svelte/attachments').Attachment}
|
||||
*/
|
||||
function tooltip(content) {
|
||||
return (element) => {
|
||||
const tooltip = tippy(element, { content });
|
||||
return tooltip.destroy;
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<input bind:value={content} />
|
||||
|
||||
<Button {@attach tooltip(content)}>
|
||||
Hover me
|
||||
</Button>
|
||||
```
|
||||
|
||||
## Controlling when attachments re-run
|
||||
|
||||
Attachments, unlike [actions](use), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
|
||||
|
||||
```js
|
||||
// @errors: 7006 2304 2552
|
||||
function foo(bar) {
|
||||
return (node) => {
|
||||
veryExpensiveSetupWork(node);
|
||||
update(node, bar);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
In the rare case that this is a problem (for example, if `foo` does expensive and unavoidable setup work) consider passing the data inside a function and reading it in a child effect:
|
||||
|
||||
```js
|
||||
// @errors: 7006 2304 2552
|
||||
function foo(+++getBar+++) {
|
||||
return (node) => {
|
||||
veryExpensiveSetupWork(node);
|
||||
|
||||
+++ $effect(() => {
|
||||
update(node, getBar());
|
||||
});+++
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Creating attachments programmatically
|
||||
|
||||
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey).
|
||||
|
||||
## Converting actions to attachments
|
||||
|
||||
If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](svelte-attachments#fromAction), allowing you to (for example) use them with components.
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
title: svelte/attachments
|
||||
---
|
||||
|
||||
> MODULE: svelte/attachments
|
@ -1,409 +1,441 @@
|
||||
// @ts-check
|
||||
import process from 'node:process';
|
||||
import fs from 'node:fs';
|
||||
import * as acorn from 'acorn';
|
||||
import { walk } from 'zimmerframe';
|
||||
import * as esrap from 'esrap';
|
||||
|
||||
/** @type {Record<string, Record<string, { messages: string[], details: string | null }>>} */
|
||||
const messages = {};
|
||||
const seen = new Set();
|
||||
|
||||
const DIR = '../../documentation/docs/98-reference/.generated';
|
||||
fs.rmSync(DIR, { force: true, recursive: true });
|
||||
fs.mkdirSync(DIR);
|
||||
|
||||
for (const category of fs.readdirSync('messages')) {
|
||||
if (category.startsWith('.')) continue;
|
||||
const watch = process.argv.includes('-w');
|
||||
|
||||
messages[category] = {};
|
||||
function run() {
|
||||
/** @type {Record<string, Record<string, { messages: string[], details: string | null }>>} */
|
||||
const messages = {};
|
||||
const seen = new Set();
|
||||
|
||||
for (const file of fs.readdirSync(`messages/${category}`)) {
|
||||
if (!file.endsWith('.md')) continue;
|
||||
fs.rmSync(DIR, { force: true, recursive: true });
|
||||
fs.mkdirSync(DIR);
|
||||
|
||||
const markdown = fs
|
||||
.readFileSync(`messages/${category}/${file}`, 'utf-8')
|
||||
.replace(/\r\n/g, '\n');
|
||||
for (const category of fs.readdirSync('messages')) {
|
||||
if (category.startsWith('.')) continue;
|
||||
|
||||
const sorted = [];
|
||||
messages[category] = {};
|
||||
|
||||
for (const match of markdown.matchAll(/## ([\w]+)\n\n([^]+?)(?=$|\n\n## )/g)) {
|
||||
const [_, code, text] = match;
|
||||
for (const file of fs.readdirSync(`messages/${category}`)) {
|
||||
if (!file.endsWith('.md')) continue;
|
||||
|
||||
if (seen.has(code)) {
|
||||
throw new Error(`Duplicate message code ${category}/${code}`);
|
||||
}
|
||||
const markdown = fs
|
||||
.readFileSync(`messages/${category}/${file}`, 'utf-8')
|
||||
.replace(/\r\n/g, '\n');
|
||||
|
||||
sorted.push({ code, _ });
|
||||
const sorted = [];
|
||||
|
||||
const sections = text.trim().split('\n\n');
|
||||
const details = [];
|
||||
for (const match of markdown.matchAll(/## ([\w]+)\n\n([^]+?)(?=$|\n\n## )/g)) {
|
||||
const [_, code, text] = match;
|
||||
|
||||
while (!sections[sections.length - 1].startsWith('> ')) {
|
||||
details.unshift(/** @type {string} */ (sections.pop()));
|
||||
}
|
||||
if (seen.has(code)) {
|
||||
throw new Error(`Duplicate message code ${category}/${code}`);
|
||||
}
|
||||
|
||||
sorted.push({ code, _ });
|
||||
|
||||
if (sections.length === 0) {
|
||||
throw new Error('No message text');
|
||||
const sections = text.trim().split('\n\n');
|
||||
const details = [];
|
||||
|
||||
while (!sections[sections.length - 1].startsWith('> ')) {
|
||||
details.unshift(/** @type {string} */ (sections.pop()));
|
||||
}
|
||||
|
||||
if (sections.length === 0) {
|
||||
throw new Error('No message text');
|
||||
}
|
||||
|
||||
seen.add(code);
|
||||
messages[category][code] = {
|
||||
messages: sections.map((section) => section.replace(/^> /gm, '').replace(/^>\n/gm, '\n')),
|
||||
details: details.join('\n\n')
|
||||
};
|
||||
}
|
||||
|
||||
seen.add(code);
|
||||
messages[category][code] = {
|
||||
messages: sections.map((section) => section.replace(/^> /gm, '').replace(/^>\n/gm, '\n')),
|
||||
details: details.join('\n\n')
|
||||
};
|
||||
sorted.sort((a, b) => (a.code < b.code ? -1 : 1));
|
||||
|
||||
fs.writeFileSync(
|
||||
`messages/${category}/${file}`,
|
||||
sorted.map((x) => x._.trim()).join('\n\n') + '\n'
|
||||
);
|
||||
}
|
||||
|
||||
sorted.sort((a, b) => (a.code < b.code ? -1 : 1));
|
||||
fs.writeFileSync(
|
||||
`messages/${category}/${file}`,
|
||||
sorted.map((x) => x._.trim()).join('\n\n') + '\n'
|
||||
`${DIR}/${category}.md`,
|
||||
'<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->\n\n' +
|
||||
Object.entries(messages[category])
|
||||
.map(([code, { messages, details }]) => {
|
||||
const chunks = [
|
||||
`### ${code}`,
|
||||
...messages.map((message) => '```\n' + message + '\n```')
|
||||
];
|
||||
|
||||
if (details) {
|
||||
chunks.push(details);
|
||||
}
|
||||
|
||||
return chunks.join('\n\n');
|
||||
})
|
||||
.sort()
|
||||
.join('\n\n') +
|
||||
'\n'
|
||||
);
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
`${DIR}/${category}.md`,
|
||||
'<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->\n\n' +
|
||||
Object.entries(messages[category])
|
||||
.map(([code, { messages, details }]) => {
|
||||
const chunks = [`### ${code}`, ...messages.map((message) => '```\n' + message + '\n```')];
|
||||
|
||||
if (details) {
|
||||
chunks.push(details);
|
||||
}
|
||||
|
||||
return chunks.join('\n\n');
|
||||
})
|
||||
.sort()
|
||||
.join('\n\n') +
|
||||
'\n'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {string} dest
|
||||
*/
|
||||
function transform(name, dest) {
|
||||
const source = fs
|
||||
.readFileSync(new URL(`./templates/${name}.js`, import.meta.url), 'utf-8')
|
||||
.replace(/\r\n/g, '\n');
|
||||
|
||||
/**
|
||||
* @type {Array<{
|
||||
* type: string;
|
||||
* value: string;
|
||||
* start: number;
|
||||
* end: number
|
||||
* }>}
|
||||
* @param {string} name
|
||||
* @param {string} dest
|
||||
*/
|
||||
const comments = [];
|
||||
|
||||
let ast = acorn.parse(source, {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
onComment: (block, value, start, end) => {
|
||||
if (block && /\n/.test(value)) {
|
||||
let a = start;
|
||||
while (a > 0 && source[a - 1] !== '\n') a -= 1;
|
||||
|
||||
let b = a;
|
||||
while (/[ \t]/.test(source[b])) b += 1;
|
||||
|
||||
const indentation = source.slice(a, b);
|
||||
value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
|
||||
}
|
||||
|
||||
comments.push({ type: block ? 'Block' : 'Line', value, start, end });
|
||||
}
|
||||
});
|
||||
function transform(name, dest) {
|
||||
const source = fs
|
||||
.readFileSync(new URL(`./templates/${name}.js`, import.meta.url), 'utf-8')
|
||||
.replace(/\r\n/g, '\n');
|
||||
|
||||
ast = walk(ast, null, {
|
||||
_(node, { next }) {
|
||||
let comment;
|
||||
/**
|
||||
* @type {Array<{
|
||||
* type: string;
|
||||
* value: string;
|
||||
* start: number;
|
||||
* end: number
|
||||
* }>}
|
||||
*/
|
||||
const comments = [];
|
||||
|
||||
let ast = acorn.parse(source, {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
onComment: (block, value, start, end) => {
|
||||
if (block && /\n/.test(value)) {
|
||||
let a = start;
|
||||
while (a > 0 && source[a - 1] !== '\n') a -= 1;
|
||||
|
||||
let b = a;
|
||||
while (/[ \t]/.test(source[b])) b += 1;
|
||||
|
||||
const indentation = source.slice(a, b);
|
||||
value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
|
||||
}
|
||||
|
||||
while (comments[0] && comments[0].start < node.start) {
|
||||
comment = comments.shift();
|
||||
// @ts-expect-error
|
||||
(node.leadingComments ||= []).push(comment);
|
||||
comments.push({ type: block ? 'Block' : 'Line', value, start, end });
|
||||
}
|
||||
});
|
||||
|
||||
next();
|
||||
|
||||
if (comments[0]) {
|
||||
const slice = source.slice(node.end, comments[0].start);
|
||||
ast = walk(ast, null, {
|
||||
_(node, { next }) {
|
||||
let comment;
|
||||
|
||||
if (/^[,) \t]*$/.test(slice)) {
|
||||
while (comments[0] && comments[0].start < node.start) {
|
||||
comment = comments.shift();
|
||||
// @ts-expect-error
|
||||
node.trailingComments = [comments.shift()];
|
||||
(node.leadingComments ||= []).push(comment);
|
||||
}
|
||||
}
|
||||
},
|
||||
// @ts-expect-error
|
||||
Identifier(node, context) {
|
||||
if (node.name === 'CODES') {
|
||||
return {
|
||||
type: 'ArrayExpression',
|
||||
elements: Object.keys(messages[name]).map((code) => ({
|
||||
type: 'Literal',
|
||||
value: code
|
||||
}))
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (comments.length > 0) {
|
||||
// @ts-expect-error
|
||||
(ast.trailingComments ||= []).push(...comments);
|
||||
}
|
||||
|
||||
const category = messages[name];
|
||||
|
||||
// find the `export function CODE` node
|
||||
const index = ast.body.findIndex((node) => {
|
||||
if (
|
||||
node.type === 'ExportNamedDeclaration' &&
|
||||
node.declaration &&
|
||||
node.declaration.type === 'FunctionDeclaration'
|
||||
) {
|
||||
return node.declaration.id.name === 'CODE';
|
||||
}
|
||||
});
|
||||
|
||||
if (index === -1) throw new Error(`missing export function CODE in ${name}.js`);
|
||||
|
||||
const template_node = ast.body[index];
|
||||
ast.body.splice(index, 1);
|
||||
next();
|
||||
|
||||
for (const code in category) {
|
||||
const { messages } = category[code];
|
||||
/** @type {string[]} */
|
||||
const vars = [];
|
||||
if (comments[0]) {
|
||||
const slice = source.slice(node.end, comments[0].start);
|
||||
|
||||
const group = messages.map((text, i) => {
|
||||
for (const match of text.matchAll(/%(\w+)%/g)) {
|
||||
const name = match[1];
|
||||
if (!vars.includes(name)) {
|
||||
vars.push(match[1]);
|
||||
if (/^[,) \t]*$/.test(slice)) {
|
||||
// @ts-expect-error
|
||||
node.trailingComments = [comments.shift()];
|
||||
}
|
||||
}
|
||||
},
|
||||
// @ts-expect-error
|
||||
Identifier(node, context) {
|
||||
if (node.name === 'CODES') {
|
||||
return {
|
||||
type: 'ArrayExpression',
|
||||
elements: Object.keys(messages[name]).map((code) => ({
|
||||
type: 'Literal',
|
||||
value: code
|
||||
}))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
text,
|
||||
vars: vars.slice()
|
||||
};
|
||||
});
|
||||
|
||||
/** @type {import('estree').Expression} */
|
||||
let message = { type: 'Literal', value: '' };
|
||||
let prev_vars;
|
||||
if (comments.length > 0) {
|
||||
// @ts-expect-error
|
||||
(ast.trailingComments ||= []).push(...comments);
|
||||
}
|
||||
|
||||
for (let i = 0; i < group.length; i += 1) {
|
||||
const { text, vars } = group[i];
|
||||
const category = messages[name];
|
||||
|
||||
if (vars.length === 0) {
|
||||
message = {
|
||||
type: 'Literal',
|
||||
value: text
|
||||
};
|
||||
prev_vars = vars;
|
||||
continue;
|
||||
// find the `export function CODE` node
|
||||
const index = ast.body.findIndex((node) => {
|
||||
if (
|
||||
node.type === 'ExportNamedDeclaration' &&
|
||||
node.declaration &&
|
||||
node.declaration.type === 'FunctionDeclaration'
|
||||
) {
|
||||
return node.declaration.id.name === 'CODE';
|
||||
}
|
||||
});
|
||||
|
||||
const parts = text.split(/(%\w+%)/);
|
||||
if (index === -1) throw new Error(`missing export function CODE in ${name}.js`);
|
||||
|
||||
/** @type {import('estree').Expression[]} */
|
||||
const expressions = [];
|
||||
const template_node = ast.body[index];
|
||||
ast.body.splice(index, 1);
|
||||
|
||||
/** @type {import('estree').TemplateElement[]} */
|
||||
const quasis = [];
|
||||
for (const code in category) {
|
||||
const { messages } = category[code];
|
||||
/** @type {string[]} */
|
||||
const vars = [];
|
||||
|
||||
for (let i = 0; i < parts.length; i += 1) {
|
||||
const part = parts[i];
|
||||
if (i % 2 === 0) {
|
||||
const str = part.replace(/(`|\${)/g, '\\$1');
|
||||
quasis.push({
|
||||
type: 'TemplateElement',
|
||||
value: { raw: str, cooked: str },
|
||||
tail: i === parts.length - 1
|
||||
});
|
||||
} else {
|
||||
expressions.push({
|
||||
type: 'Identifier',
|
||||
name: part.slice(1, -1)
|
||||
});
|
||||
const group = messages.map((text, i) => {
|
||||
for (const match of text.matchAll(/%(\w+)%/g)) {
|
||||
const name = match[1];
|
||||
if (!vars.includes(name)) {
|
||||
vars.push(match[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
text,
|
||||
vars: vars.slice()
|
||||
};
|
||||
});
|
||||
|
||||
/** @type {import('estree').Expression} */
|
||||
const expression = {
|
||||
type: 'TemplateLiteral',
|
||||
expressions,
|
||||
quasis
|
||||
};
|
||||
|
||||
if (prev_vars) {
|
||||
if (vars.length === prev_vars.length) {
|
||||
throw new Error('Message overloads must have new parameters');
|
||||
let message = { type: 'Literal', value: '' };
|
||||
let prev_vars;
|
||||
|
||||
for (let i = 0; i < group.length; i += 1) {
|
||||
const { text, vars } = group[i];
|
||||
|
||||
if (vars.length === 0) {
|
||||
message = {
|
||||
type: 'Literal',
|
||||
value: text
|
||||
};
|
||||
prev_vars = vars;
|
||||
continue;
|
||||
}
|
||||
|
||||
message = {
|
||||
type: 'ConditionalExpression',
|
||||
test: {
|
||||
type: 'Identifier',
|
||||
name: vars[prev_vars.length]
|
||||
},
|
||||
consequent: expression,
|
||||
alternate: message
|
||||
};
|
||||
} else {
|
||||
message = expression;
|
||||
}
|
||||
const parts = text.split(/(%\w+%)/);
|
||||
|
||||
prev_vars = vars;
|
||||
}
|
||||
/** @type {import('estree').Expression[]} */
|
||||
const expressions = [];
|
||||
|
||||
const clone = walk(/** @type {import('estree').Node} */ (template_node), null, {
|
||||
// @ts-expect-error Block is a block comment, which is not recognised
|
||||
Block(node, context) {
|
||||
if (!node.value.includes('PARAMETER')) return;
|
||||
|
||||
const value = /** @type {string} */ (node.value)
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
if (line === ' * MESSAGE') {
|
||||
return messages[messages.length - 1]
|
||||
.split('\n')
|
||||
.map((line) => ` * ${line}`)
|
||||
.join('\n');
|
||||
}
|
||||
/** @type {import('estree').TemplateElement[]} */
|
||||
const quasis = [];
|
||||
|
||||
if (line.includes('PARAMETER')) {
|
||||
return vars
|
||||
.map((name, i) => {
|
||||
const optional = i >= group[0].vars.length;
|
||||
for (let i = 0; i < parts.length; i += 1) {
|
||||
const part = parts[i];
|
||||
if (i % 2 === 0) {
|
||||
const str = part.replace(/(`|\${)/g, '\\$1');
|
||||
quasis.push({
|
||||
type: 'TemplateElement',
|
||||
value: { raw: str, cooked: str },
|
||||
tail: i === parts.length - 1
|
||||
});
|
||||
} else {
|
||||
expressions.push({
|
||||
type: 'Identifier',
|
||||
name: part.slice(1, -1)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return optional
|
||||
? ` * @param {string | undefined | null} [${name}]`
|
||||
: ` * @param {string} ${name}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
/** @type {import('estree').Expression} */
|
||||
const expression = {
|
||||
type: 'TemplateLiteral',
|
||||
expressions,
|
||||
quasis
|
||||
};
|
||||
|
||||
return line;
|
||||
})
|
||||
.filter((x) => x !== '')
|
||||
.join('\n');
|
||||
if (prev_vars) {
|
||||
if (vars.length === prev_vars.length) {
|
||||
throw new Error('Message overloads must have new parameters');
|
||||
}
|
||||
|
||||
if (value !== node.value) {
|
||||
return { ...node, value };
|
||||
message = {
|
||||
type: 'ConditionalExpression',
|
||||
test: {
|
||||
type: 'Identifier',
|
||||
name: vars[prev_vars.length]
|
||||
},
|
||||
consequent: expression,
|
||||
alternate: message
|
||||
};
|
||||
} else {
|
||||
message = expression;
|
||||
}
|
||||
},
|
||||
FunctionDeclaration(node, context) {
|
||||
if (node.id.name !== 'CODE') return;
|
||||
|
||||
const params = [];
|
||||
prev_vars = vars;
|
||||
}
|
||||
|
||||
for (const param of node.params) {
|
||||
if (param.type === 'Identifier' && param.name === 'PARAMETER') {
|
||||
params.push(...vars.map((name) => ({ type: 'Identifier', name })));
|
||||
} else {
|
||||
params.push(param);
|
||||
const clone = walk(/** @type {import('estree').Node} */ (template_node), null, {
|
||||
// @ts-expect-error Block is a block comment, which is not recognised
|
||||
Block(node, context) {
|
||||
if (!node.value.includes('PARAMETER')) return;
|
||||
|
||||
const value = /** @type {string} */ (node.value)
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
if (line === ' * MESSAGE') {
|
||||
return messages[messages.length - 1]
|
||||
.split('\n')
|
||||
.map((line) => ` * ${line}`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
if (line.includes('PARAMETER')) {
|
||||
return vars
|
||||
.map((name, i) => {
|
||||
const optional = i >= group[0].vars.length;
|
||||
|
||||
return optional
|
||||
? ` * @param {string | undefined | null} [${name}]`
|
||||
: ` * @param {string} ${name}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
return line;
|
||||
})
|
||||
.filter((x) => x !== '')
|
||||
.join('\n');
|
||||
|
||||
if (value !== node.value) {
|
||||
return { ...node, value };
|
||||
}
|
||||
}
|
||||
},
|
||||
FunctionDeclaration(node, context) {
|
||||
if (node.id.name !== 'CODE') return;
|
||||
|
||||
const params = [];
|
||||
|
||||
return /** @type {import('estree').FunctionDeclaration} */ ({
|
||||
.../** @type {import('estree').FunctionDeclaration} */ (context.next()),
|
||||
params,
|
||||
id: {
|
||||
...node.id,
|
||||
name: code
|
||||
for (const param of node.params) {
|
||||
if (param.type === 'Identifier' && param.name === 'PARAMETER') {
|
||||
params.push(...vars.map((name) => ({ type: 'Identifier', name })));
|
||||
} else {
|
||||
params.push(param);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
TemplateLiteral(node, context) {
|
||||
/** @type {import('estree').TemplateElement} */
|
||||
let quasi = {
|
||||
type: 'TemplateElement',
|
||||
value: {
|
||||
...node.quasis[0].value
|
||||
},
|
||||
tail: node.quasis[0].tail
|
||||
};
|
||||
|
||||
/** @type {import('estree').TemplateLiteral} */
|
||||
let out = {
|
||||
type: 'TemplateLiteral',
|
||||
quasis: [quasi],
|
||||
expressions: []
|
||||
};
|
||||
return /** @type {import('estree').FunctionDeclaration} */ ({
|
||||
.../** @type {import('estree').FunctionDeclaration} */ (context.next()),
|
||||
params,
|
||||
id: {
|
||||
...node.id,
|
||||
name: code
|
||||
}
|
||||
});
|
||||
},
|
||||
TemplateLiteral(node, context) {
|
||||
/** @type {import('estree').TemplateElement} */
|
||||
let quasi = {
|
||||
type: 'TemplateElement',
|
||||
value: {
|
||||
...node.quasis[0].value
|
||||
},
|
||||
tail: node.quasis[0].tail
|
||||
};
|
||||
|
||||
for (let i = 0; i < node.expressions.length; i += 1) {
|
||||
const q = structuredClone(node.quasis[i + 1]);
|
||||
const e = node.expressions[i];
|
||||
/** @type {import('estree').TemplateLiteral} */
|
||||
let out = {
|
||||
type: 'TemplateLiteral',
|
||||
quasis: [quasi],
|
||||
expressions: []
|
||||
};
|
||||
|
||||
if (e.type === 'Literal' && e.value === 'CODE') {
|
||||
quasi.value.raw += code + q.value.raw;
|
||||
continue;
|
||||
}
|
||||
for (let i = 0; i < node.expressions.length; i += 1) {
|
||||
const q = structuredClone(node.quasis[i + 1]);
|
||||
const e = node.expressions[i];
|
||||
|
||||
if (e.type === 'Identifier' && e.name === 'MESSAGE') {
|
||||
if (message.type === 'Literal') {
|
||||
const str = /** @type {string} */ (message.value).replace(/(`|\${)/g, '\\$1');
|
||||
quasi.value.raw += str + q.value.raw;
|
||||
if (e.type === 'Literal' && e.value === 'CODE') {
|
||||
quasi.value.raw += code + q.value.raw;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message.type === 'TemplateLiteral') {
|
||||
const m = structuredClone(message);
|
||||
quasi.value.raw += m.quasis[0].value.raw;
|
||||
out.quasis.push(...m.quasis.slice(1));
|
||||
out.expressions.push(...m.expressions);
|
||||
quasi = m.quasis[m.quasis.length - 1];
|
||||
quasi.value.raw += q.value.raw;
|
||||
continue;
|
||||
if (e.type === 'Identifier' && e.name === 'MESSAGE') {
|
||||
if (message.type === 'Literal') {
|
||||
const str = /** @type {string} */ (message.value).replace(/(`|\${)/g, '\\$1');
|
||||
quasi.value.raw += str + q.value.raw;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message.type === 'TemplateLiteral') {
|
||||
const m = structuredClone(message);
|
||||
quasi.value.raw += m.quasis[0].value.raw;
|
||||
out.quasis.push(...m.quasis.slice(1));
|
||||
out.expressions.push(...m.expressions);
|
||||
quasi = m.quasis[m.quasis.length - 1];
|
||||
quasi.value.raw += q.value.raw;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
out.quasis.push((quasi = q));
|
||||
out.expressions.push(/** @type {import('estree').Expression} */ (context.visit(e)));
|
||||
}
|
||||
|
||||
out.quasis.push((quasi = q));
|
||||
out.expressions.push(/** @type {import('estree').Expression} */ (context.visit(e)));
|
||||
return out;
|
||||
},
|
||||
Literal(node) {
|
||||
if (node.value === 'CODE') {
|
||||
return {
|
||||
type: 'Literal',
|
||||
value: code
|
||||
};
|
||||
}
|
||||
},
|
||||
Identifier(node) {
|
||||
if (node.name !== 'MESSAGE') return;
|
||||
return message;
|
||||
}
|
||||
});
|
||||
|
||||
return out;
|
||||
},
|
||||
Literal(node) {
|
||||
if (node.value === 'CODE') {
|
||||
return {
|
||||
type: 'Literal',
|
||||
value: code
|
||||
};
|
||||
}
|
||||
},
|
||||
Identifier(node) {
|
||||
if (node.name !== 'MESSAGE') return;
|
||||
return message;
|
||||
}
|
||||
});
|
||||
// @ts-expect-error
|
||||
ast.body.push(clone);
|
||||
}
|
||||
|
||||
const module = esrap.print(ast);
|
||||
|
||||
// @ts-expect-error
|
||||
ast.body.push(clone);
|
||||
fs.writeFileSync(
|
||||
dest,
|
||||
`/* This file is generated by scripts/process-messages/index.js. Do not edit! */\n\n` +
|
||||
module.code,
|
||||
'utf-8'
|
||||
);
|
||||
}
|
||||
|
||||
const module = esrap.print(ast);
|
||||
transform('compile-errors', 'src/compiler/errors.js');
|
||||
transform('compile-warnings', 'src/compiler/warnings.js');
|
||||
|
||||
fs.writeFileSync(
|
||||
dest,
|
||||
`/* This file is generated by scripts/process-messages/index.js. Do not edit! */\n\n` +
|
||||
module.code,
|
||||
'utf-8'
|
||||
);
|
||||
transform('client-warnings', 'src/internal/client/warnings.js');
|
||||
transform('client-errors', 'src/internal/client/errors.js');
|
||||
transform('server-errors', 'src/internal/server/errors.js');
|
||||
transform('shared-errors', 'src/internal/shared/errors.js');
|
||||
transform('shared-warnings', 'src/internal/shared/warnings.js');
|
||||
}
|
||||
|
||||
transform('compile-errors', 'src/compiler/errors.js');
|
||||
transform('compile-warnings', 'src/compiler/warnings.js');
|
||||
if (watch) {
|
||||
let running = false;
|
||||
let timeout;
|
||||
|
||||
fs.watch('messages', { recursive: true }, (type, file) => {
|
||||
if (running) {
|
||||
timeout ??= setTimeout(() => {
|
||||
running = false;
|
||||
timeout = null;
|
||||
});
|
||||
} else {
|
||||
running = true;
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Regenerating messages...');
|
||||
run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
transform('client-warnings', 'src/internal/client/warnings.js');
|
||||
transform('client-errors', 'src/internal/client/errors.js');
|
||||
transform('server-errors', 'src/internal/server/errors.js');
|
||||
transform('shared-errors', 'src/internal/shared/errors.js');
|
||||
transform('shared-warnings', 'src/internal/shared/warnings.js');
|
||||
run();
|
||||
|
@ -0,0 +1,113 @@
|
||||
/** @import { Action, ActionReturn } from '../action/public' */
|
||||
/** @import { Attachment } from './public' */
|
||||
import { noop, render_effect } from 'svelte/internal/client';
|
||||
import { ATTACHMENT_KEY } from '../constants.js';
|
||||
import { untrack } from 'svelte';
|
||||
import { teardown } from '../internal/client/reactivity/effects.js';
|
||||
|
||||
/**
|
||||
* Creates an object key that will be recognised as an attachment when the object is spread onto an element,
|
||||
* as a programmatic alternative to using `{@attach ...}`. This can be useful for library authors, though
|
||||
* is generally not needed when building an app.
|
||||
*
|
||||
* ```svelte
|
||||
* <script>
|
||||
* import { createAttachmentKey } from 'svelte/attachments';
|
||||
*
|
||||
* const props = {
|
||||
* class: 'cool',
|
||||
* onclick: () => alert('clicked'),
|
||||
* [createAttachmentKey()]: (node) => {
|
||||
* node.textContent = 'attached!';
|
||||
* }
|
||||
* };
|
||||
* </script>
|
||||
*
|
||||
* <button {...props}>click me</button>
|
||||
* ```
|
||||
* @since 5.29
|
||||
*/
|
||||
export function createAttachmentKey() {
|
||||
return Symbol(ATTACHMENT_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior.
|
||||
* It's useful if you want to start using attachments on components but you have actions provided by a library.
|
||||
*
|
||||
* Note that the second argument, if provided, must be a function that _returns_ the argument to the
|
||||
* action function, not the argument itself.
|
||||
*
|
||||
* ```svelte
|
||||
* <!-- with an action -->
|
||||
* <div use:foo={bar}>...</div>
|
||||
*
|
||||
* <!-- with an attachment -->
|
||||
* <div {@attach fromAction(foo, () => bar)}>...</div>
|
||||
* ```
|
||||
* @template {EventTarget} E
|
||||
* @template {unknown} T
|
||||
* @overload
|
||||
* @param {Action<E, T> | ((element: E, arg: T) => void | ActionReturn<T>)} action The action function
|
||||
* @param {() => T} fn A function that returns the argument for the action
|
||||
* @returns {Attachment<E>}
|
||||
*/
|
||||
/**
|
||||
* Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior.
|
||||
* It's useful if you want to start using attachments on components but you have actions provided by a library.
|
||||
*
|
||||
* Note that the second argument, if provided, must be a function that _returns_ the argument to the
|
||||
* action function, not the argument itself.
|
||||
*
|
||||
* ```svelte
|
||||
* <!-- with an action -->
|
||||
* <div use:foo={bar}>...</div>
|
||||
*
|
||||
* <!-- with an attachment -->
|
||||
* <div {@attach fromAction(foo, () => bar)}>...</div>
|
||||
* ```
|
||||
* @template {EventTarget} E
|
||||
* @overload
|
||||
* @param {Action<E, void> | ((element: E) => void | ActionReturn<void>)} action The action function
|
||||
* @returns {Attachment<E>}
|
||||
*/
|
||||
/**
|
||||
* Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior.
|
||||
* It's useful if you want to start using attachments on components but you have actions provided by a library.
|
||||
*
|
||||
* Note that the second argument, if provided, must be a function that _returns_ the argument to the
|
||||
* action function, not the argument itself.
|
||||
*
|
||||
* ```svelte
|
||||
* <!-- with an action -->
|
||||
* <div use:foo={bar}>...</div>
|
||||
*
|
||||
* <!-- with an attachment -->
|
||||
* <div {@attach fromAction(foo, () => bar)}>...</div>
|
||||
* ```
|
||||
*
|
||||
* @template {EventTarget} E
|
||||
* @template {unknown} T
|
||||
* @param {Action<E, T> | ((element: E, arg: T) => void | ActionReturn<T>)} action The action function
|
||||
* @param {() => T} fn A function that returns the argument for the action
|
||||
* @returns {Attachment<E>}
|
||||
* @since 5.32
|
||||
*/
|
||||
export function fromAction(action, fn = /** @type {() => T} */ (noop)) {
|
||||
return (element) => {
|
||||
const { update, destroy } = untrack(() => action(element, fn()) ?? {});
|
||||
|
||||
if (update) {
|
||||
var ran = false;
|
||||
render_effect(() => {
|
||||
const arg = fn();
|
||||
if (ran) update(arg);
|
||||
});
|
||||
ran = true;
|
||||
}
|
||||
|
||||
if (destroy) {
|
||||
teardown(destroy);
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* An [attachment](https://svelte.dev/docs/svelte/@attach) is a function that runs when an element is mounted
|
||||
* to the DOM, and optionally returns a function that is called when the element is later removed.
|
||||
*
|
||||
* It can be attached to an element with an `{@attach ...}` tag, or by spreading an object containing
|
||||
* a property created with [`createAttachmentKey`](https://svelte.dev/docs/svelte/svelte-attachments#createAttachmentKey).
|
||||
*/
|
||||
export interface Attachment<T extends EventTarget = Element> {
|
||||
(element: T): void | (() => void);
|
||||
}
|
||||
|
||||
export * from './index.js';
|
@ -0,0 +1,13 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
|
||||
import { mark_subtree_dynamic } from './shared/fragment.js';
|
||||
|
||||
/**
|
||||
* @param {AST.AttachTag} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function AttachTag(node, context) {
|
||||
mark_subtree_dynamic(context.path);
|
||||
context.next({ ...context.state, expression: node.metadata.expression });
|
||||
}
|
@ -1,30 +1,107 @@
|
||||
/** @import { ClassBody } from 'estree' */
|
||||
/** @import { AssignmentExpression, CallExpression, ClassBody, PropertyDefinition, Expression, PrivateIdentifier, MethodDefinition } from 'estree' */
|
||||
/** @import { StateField } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import * as b from '#compiler/builders';
|
||||
import { get_rune } from '../../scope.js';
|
||||
import * as e from '../../../errors.js';
|
||||
import { is_state_creation_rune } from '../../../../utils.js';
|
||||
import { get_name } from '../../nodes.js';
|
||||
import { regex_invalid_identifier_chars } from '../../patterns.js';
|
||||
|
||||
/**
|
||||
* @param {ClassBody} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function ClassBody(node, context) {
|
||||
/** @type {{name: string, private: boolean}[]} */
|
||||
const derived_state = [];
|
||||
if (!context.state.analysis.runes) {
|
||||
context.next();
|
||||
return;
|
||||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
const private_ids = [];
|
||||
|
||||
for (const definition of node.body) {
|
||||
for (const prop of node.body) {
|
||||
if (
|
||||
definition.type === 'PropertyDefinition' &&
|
||||
(definition.key.type === 'PrivateIdentifier' || definition.key.type === 'Identifier') &&
|
||||
definition.value?.type === 'CallExpression'
|
||||
(prop.type === 'MethodDefinition' || prop.type === 'PropertyDefinition') &&
|
||||
prop.key.type === 'PrivateIdentifier'
|
||||
) {
|
||||
const rune = get_rune(definition.value, context.state.scope);
|
||||
if (rune === '$derived' || rune === '$derived.by') {
|
||||
derived_state.push({
|
||||
name: definition.key.name,
|
||||
private: definition.key.type === 'PrivateIdentifier'
|
||||
});
|
||||
private_ids.push(prop.key.name);
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Map<string, StateField>} */
|
||||
const state_fields = new Map();
|
||||
|
||||
context.state.analysis.classes.set(node, state_fields);
|
||||
|
||||
/** @type {MethodDefinition | null} */
|
||||
let constructor = null;
|
||||
|
||||
/**
|
||||
* @param {PropertyDefinition | AssignmentExpression} node
|
||||
* @param {Expression | PrivateIdentifier} key
|
||||
* @param {Expression | null | undefined} value
|
||||
*/
|
||||
function handle(node, key, value) {
|
||||
const name = get_name(key);
|
||||
if (name === null) return;
|
||||
|
||||
const rune = get_rune(value, context.state.scope);
|
||||
|
||||
if (rune && is_state_creation_rune(rune)) {
|
||||
if (state_fields.has(name)) {
|
||||
e.state_field_duplicate(node, name);
|
||||
}
|
||||
|
||||
state_fields.set(name, {
|
||||
node,
|
||||
type: rune,
|
||||
// @ts-expect-error for public state this is filled out in a moment
|
||||
key: key.type === 'PrivateIdentifier' ? key : null,
|
||||
value: /** @type {CallExpression} */ (value)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const child of node.body) {
|
||||
if (child.type === 'PropertyDefinition' && !child.computed && !child.static) {
|
||||
handle(child, child.key, child.value);
|
||||
}
|
||||
|
||||
if (child.type === 'MethodDefinition' && child.kind === 'constructor') {
|
||||
constructor = child;
|
||||
}
|
||||
}
|
||||
|
||||
if (constructor) {
|
||||
for (const statement of constructor.value.body.body) {
|
||||
if (statement.type !== 'ExpressionStatement') continue;
|
||||
if (statement.expression.type !== 'AssignmentExpression') continue;
|
||||
|
||||
const { left, right } = statement.expression;
|
||||
|
||||
if (left.type !== 'MemberExpression') continue;
|
||||
if (left.object.type !== 'ThisExpression') continue;
|
||||
if (left.computed && left.property.type !== 'Literal') continue;
|
||||
|
||||
handle(statement.expression, left.property, right);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [name, field] of state_fields) {
|
||||
if (name[0] === '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let deconflicted = name.replace(regex_invalid_identifier_chars, '_');
|
||||
while (private_ids.includes(deconflicted)) {
|
||||
deconflicted = '_' + deconflicted;
|
||||
}
|
||||
|
||||
private_ids.push(deconflicted);
|
||||
field.key = b.private_id(deconflicted);
|
||||
}
|
||||
|
||||
context.next({ ...context.state, derived_state });
|
||||
context.next({ ...context.state, state_fields });
|
||||
}
|
||||
|
@ -0,0 +1,14 @@
|
||||
/** @import { Literal } from 'estree' */
|
||||
import * as w from '../../../warnings.js';
|
||||
import { regex_bidirectional_control_characters } from '../../patterns.js';
|
||||
|
||||
/**
|
||||
* @param {Literal} node
|
||||
*/
|
||||
export function Literal(node) {
|
||||
if (typeof node.value === 'string') {
|
||||
if (regex_bidirectional_control_characters.test(node.value)) {
|
||||
w.bidirectional_control_characters(node);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
/** @import { PropertyDefinition } from 'estree' */
|
||||
/** @import { Context } from '../types' */
|
||||
import * as e from '../../../errors.js';
|
||||
import { get_name } from '../../nodes.js';
|
||||
|
||||
/**
|
||||
* @param {PropertyDefinition} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function PropertyDefinition(node, context) {
|
||||
const name = get_name(node.key);
|
||||
const field = name && context.state.state_fields.get(name);
|
||||
|
||||
if (field && node !== field.node && node.value) {
|
||||
if (/** @type {number} */ (node.start) < /** @type {number} */ (field.node.start)) {
|
||||
e.state_field_invalid_assignment(node);
|
||||
}
|
||||
}
|
||||
|
||||
context.next();
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
/** @import { TemplateElement } from 'estree' */
|
||||
import * as w from '../../../warnings.js';
|
||||
import { regex_bidirectional_control_characters } from '../../patterns.js';
|
||||
|
||||
/**
|
||||
* @param {TemplateElement} node
|
||||
*/
|
||||
export function TemplateElement(node) {
|
||||
if (regex_bidirectional_control_characters.test(node.value.cooked ?? '')) {
|
||||
w.bidirectional_control_characters(node);
|
||||
}
|
||||
}
|
@ -1,20 +1,52 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import { is_tag_valid_with_parent } from '../../../../html-tree-validation.js';
|
||||
import { regex_not_whitespace } from '../../patterns.js';
|
||||
import { regex_bidirectional_control_characters, regex_not_whitespace } from '../../patterns.js';
|
||||
import * as e from '../../../errors.js';
|
||||
import * as w from '../../../warnings.js';
|
||||
import { extract_svelte_ignore } from '../../../utils/extract_svelte_ignore.js';
|
||||
|
||||
/**
|
||||
* @param {AST.Text} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function Text(node, context) {
|
||||
const in_template = context.path.at(-1)?.type === 'Fragment';
|
||||
const parent = /** @type {AST.SvelteNode} */ (context.path.at(-1));
|
||||
|
||||
if (in_template && context.state.parent_element && regex_not_whitespace.test(node.data)) {
|
||||
if (
|
||||
parent.type === 'Fragment' &&
|
||||
context.state.parent_element &&
|
||||
regex_not_whitespace.test(node.data)
|
||||
) {
|
||||
const message = is_tag_valid_with_parent('#text', context.state.parent_element);
|
||||
if (message) {
|
||||
e.node_invalid_placement(node, message);
|
||||
}
|
||||
}
|
||||
|
||||
regex_bidirectional_control_characters.lastIndex = 0;
|
||||
for (const match of node.data.matchAll(regex_bidirectional_control_characters)) {
|
||||
let is_ignored = false;
|
||||
|
||||
// if we have a svelte-ignore comment earlier in the text, bail
|
||||
// (otherwise we can only use svelte-ignore on parent elements/blocks)
|
||||
if (parent.type === 'Fragment') {
|
||||
for (const child of parent.nodes) {
|
||||
if (child === node) break;
|
||||
|
||||
if (child.type === 'Comment') {
|
||||
is_ignored ||= extract_svelte_ignore(
|
||||
child.start + 4,
|
||||
child.data,
|
||||
context.state.analysis.runes
|
||||
).includes('bidirectional_control_characters');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_ignored) {
|
||||
let start = match.index + node.start;
|
||||
w.bidirectional_control_characters({ start, end: start + match[0].length });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,21 @@
|
||||
/** @import { Expression } from 'estree' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types' */
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
|
||||
/**
|
||||
* @param {AST.AttachTag} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function AttachTag(node, context) {
|
||||
context.state.init.push(
|
||||
b.stmt(
|
||||
b.call(
|
||||
'$.attach',
|
||||
context.state.node,
|
||||
b.thunk(/** @type {Expression} */ (context.visit(node.expression)))
|
||||
)
|
||||
)
|
||||
);
|
||||
context.next();
|
||||
}
|
@ -1,184 +1,96 @@
|
||||
/** @import { ClassBody, Expression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition } from 'estree' */
|
||||
/** @import { Context, StateField } from '../types' */
|
||||
/** @import { CallExpression, ClassBody, MethodDefinition, PropertyDefinition, StaticBlock } from 'estree' */
|
||||
/** @import { StateField } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import * as b from '#compiler/builders';
|
||||
import { regex_invalid_identifier_chars } from '../../../patterns.js';
|
||||
import { get_rune } from '../../../scope.js';
|
||||
import { should_proxy } from '../utils.js';
|
||||
import { get_name } from '../../../nodes.js';
|
||||
|
||||
/**
|
||||
* @param {ClassBody} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function ClassBody(node, context) {
|
||||
if (!context.state.analysis.runes) {
|
||||
const state_fields = context.state.analysis.classes.get(node);
|
||||
|
||||
if (!state_fields) {
|
||||
// in legacy mode, do nothing
|
||||
context.next();
|
||||
return;
|
||||
}
|
||||
|
||||
/** @type {Map<string, StateField>} */
|
||||
const public_state = new Map();
|
||||
/** @type {Array<MethodDefinition | PropertyDefinition | StaticBlock>} */
|
||||
const body = [];
|
||||
|
||||
/** @type {Map<string, StateField>} */
|
||||
const private_state = new Map();
|
||||
const child_state = { ...context.state, state_fields };
|
||||
|
||||
/** @type {Map<(MethodDefinition|PropertyDefinition)["key"], string>} */
|
||||
const definition_names = new Map();
|
||||
for (const [name, field] of state_fields) {
|
||||
if (name[0] === '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
const private_ids = [];
|
||||
// insert backing fields for stuff declared in the constructor
|
||||
if (field.node.type === 'AssignmentExpression') {
|
||||
const member = b.member(b.this, field.key);
|
||||
|
||||
for (const definition of node.body) {
|
||||
if (
|
||||
(definition.type === 'PropertyDefinition' || definition.type === 'MethodDefinition') &&
|
||||
(definition.key.type === 'Identifier' ||
|
||||
definition.key.type === 'PrivateIdentifier' ||
|
||||
definition.key.type === 'Literal')
|
||||
) {
|
||||
const type = definition.key.type;
|
||||
const name = get_name(definition.key, public_state);
|
||||
if (!name) continue;
|
||||
|
||||
// we store the deconflicted name in the map so that we can access it later
|
||||
definition_names.set(definition.key, name);
|
||||
|
||||
const is_private = type === 'PrivateIdentifier';
|
||||
if (is_private) private_ids.push(name);
|
||||
|
||||
if (definition.value?.type === 'CallExpression') {
|
||||
const rune = get_rune(definition.value, context.state.scope);
|
||||
if (
|
||||
rune === '$state' ||
|
||||
rune === '$state.raw' ||
|
||||
rune === '$derived' ||
|
||||
rune === '$derived.by'
|
||||
) {
|
||||
/** @type {StateField} */
|
||||
const field = {
|
||||
kind:
|
||||
rune === '$state'
|
||||
? 'state'
|
||||
: rune === '$state.raw'
|
||||
? 'raw_state'
|
||||
: rune === '$derived.by'
|
||||
? 'derived_by'
|
||||
: 'derived',
|
||||
// @ts-expect-error this is set in the next pass
|
||||
id: is_private ? definition.key : null
|
||||
};
|
||||
|
||||
if (is_private) {
|
||||
private_state.set(name, field);
|
||||
} else {
|
||||
public_state.set(name, field);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const should_proxy = field.type === '$state' && true; // TODO
|
||||
|
||||
// each `foo = $state()` needs a backing `#foo` field
|
||||
for (const [name, field] of public_state) {
|
||||
let deconflicted = name;
|
||||
while (private_ids.includes(deconflicted)) {
|
||||
deconflicted = '_' + deconflicted;
|
||||
}
|
||||
const key = b.key(name);
|
||||
|
||||
private_ids.push(deconflicted);
|
||||
field.id = b.private_id(deconflicted);
|
||||
}
|
||||
body.push(
|
||||
b.prop_def(field.key, null),
|
||||
|
||||
/** @type {Array<MethodDefinition | PropertyDefinition>} */
|
||||
const body = [];
|
||||
b.method('get', key, [], [b.return(b.call('$.get', member))]),
|
||||
|
||||
const child_state = { ...context.state, public_state, private_state };
|
||||
b.method(
|
||||
'set',
|
||||
key,
|
||||
[b.id('value')],
|
||||
[b.stmt(b.call('$.set', member, b.id('value'), should_proxy && b.true))]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Replace parts of the class body
|
||||
for (const definition of node.body) {
|
||||
if (
|
||||
definition.type === 'PropertyDefinition' &&
|
||||
(definition.key.type === 'Identifier' ||
|
||||
definition.key.type === 'PrivateIdentifier' ||
|
||||
definition.key.type === 'Literal')
|
||||
) {
|
||||
const name = definition_names.get(definition.key);
|
||||
if (!name) continue;
|
||||
|
||||
const is_private = definition.key.type === 'PrivateIdentifier';
|
||||
const field = (is_private ? private_state : public_state).get(name);
|
||||
|
||||
if (definition.value?.type === 'CallExpression' && field !== undefined) {
|
||||
let value = null;
|
||||
|
||||
if (definition.value.arguments.length > 0) {
|
||||
const init = /** @type {Expression} **/ (
|
||||
context.visit(definition.value.arguments[0], child_state)
|
||||
);
|
||||
|
||||
value =
|
||||
field.kind === 'state'
|
||||
? b.call(
|
||||
'$.state',
|
||||
should_proxy(init, context.state.scope) ? b.call('$.proxy', init) : init
|
||||
)
|
||||
: field.kind === 'raw_state'
|
||||
? b.call('$.state', init)
|
||||
: field.kind === 'derived_by'
|
||||
? b.call('$.derived', init)
|
||||
: b.call('$.derived', b.thunk(init));
|
||||
} else {
|
||||
// if no arguments, we know it's state as `$derived()` is a compile error
|
||||
value = b.call('$.state');
|
||||
}
|
||||
|
||||
if (is_private) {
|
||||
body.push(b.prop_def(field.id, value));
|
||||
} else {
|
||||
// #foo;
|
||||
const member = b.member(b.this, field.id);
|
||||
body.push(b.prop_def(field.id, value));
|
||||
|
||||
// get foo() { return this.#foo; }
|
||||
body.push(b.method('get', definition.key, [], [b.return(b.call('$.get', member))]));
|
||||
|
||||
// set foo(value) { this.#foo = value; }
|
||||
const val = b.id('value');
|
||||
|
||||
body.push(
|
||||
b.method(
|
||||
'set',
|
||||
definition.key,
|
||||
[val],
|
||||
[b.stmt(b.call('$.set', member, val, field.kind === 'state' && b.true))]
|
||||
)
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (definition.type !== 'PropertyDefinition') {
|
||||
body.push(
|
||||
/** @type {MethodDefinition | StaticBlock} */ (context.visit(definition, child_state))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
body.push(/** @type {MethodDefinition} **/ (context.visit(definition, child_state)));
|
||||
}
|
||||
const name = get_name(definition.key);
|
||||
const field = name && /** @type {StateField} */ (state_fields.get(name));
|
||||
|
||||
return { ...node, body };
|
||||
}
|
||||
if (!field) {
|
||||
body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state)));
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Identifier | PrivateIdentifier | Literal} node
|
||||
* @param {Map<string, StateField>} public_state
|
||||
*/
|
||||
function get_name(node, public_state) {
|
||||
if (node.type === 'Literal') {
|
||||
let name = node.value?.toString().replace(regex_invalid_identifier_chars, '_');
|
||||
|
||||
// the above could generate conflicts because it has to generate a valid identifier
|
||||
// so stuff like `0` and `1` or `state%` and `state^` will result in the same string
|
||||
// so we have to de-conflict. We can only check `public_state` because private state
|
||||
// can't have literal keys
|
||||
while (name && public_state.has(name)) {
|
||||
name = '_' + name;
|
||||
if (name[0] === '#') {
|
||||
body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state)));
|
||||
} else if (field.node === definition) {
|
||||
const member = b.member(b.this, field.key);
|
||||
|
||||
const should_proxy = field.type === '$state' && true; // TODO
|
||||
|
||||
body.push(
|
||||
b.prop_def(
|
||||
field.key,
|
||||
/** @type {CallExpression} */ (context.visit(field.value, child_state))
|
||||
),
|
||||
|
||||
b.method('get', definition.key, [], [b.return(b.call('$.get', member))]),
|
||||
|
||||
b.method(
|
||||
'set',
|
||||
definition.key,
|
||||
[b.id('value')],
|
||||
[b.stmt(b.call('$.set', member, b.id('value'), should_proxy && b.true))]
|
||||
)
|
||||
);
|
||||
}
|
||||
return name;
|
||||
} else {
|
||||
return node.name;
|
||||
}
|
||||
|
||||
return { ...node, body };
|
||||
}
|
||||
|
@ -1,23 +0,0 @@
|
||||
/** @import { MemberExpression } from 'estree' */
|
||||
/** @import { Context } from '../types.js' */
|
||||
import * as b from '#compiler/builders';
|
||||
|
||||
/**
|
||||
* @param {MemberExpression} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function MemberExpression(node, context) {
|
||||
if (
|
||||
context.state.analysis.runes &&
|
||||
node.object.type === 'ThisExpression' &&
|
||||
node.property.type === 'PrivateIdentifier'
|
||||
) {
|
||||
const field = context.state.private_derived.get(node.property.name);
|
||||
|
||||
if (field) {
|
||||
return b.call(node);
|
||||
}
|
||||
}
|
||||
|
||||
context.next();
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/** @import { Effect } from '#client' */
|
||||
import { block, branch, effect, destroy_effect } from '../../reactivity/effects.js';
|
||||
|
||||
// TODO in 6.0 or 7.0, when we remove legacy mode, we can simplify this by
|
||||
// getting rid of the block/branch stuff and just letting the effect rip.
|
||||
// see https://github.com/sveltejs/svelte/pull/15962
|
||||
|
||||
/**
|
||||
* @param {Element} node
|
||||
* @param {() => (node: Element) => void} get_fn
|
||||
*/
|
||||
export function attach(node, get_fn) {
|
||||
/** @type {false | undefined | ((node: Element) => void)} */
|
||||
var fn = undefined;
|
||||
|
||||
/** @type {Effect | null} */
|
||||
var e;
|
||||
|
||||
block(() => {
|
||||
if (fn !== (fn = get_fn())) {
|
||||
if (e) {
|
||||
destroy_effect(e);
|
||||
e = null;
|
||||
}
|
||||
|
||||
if (fn) {
|
||||
e = branch(() => {
|
||||
effect(() => /** @type {(node: Element) => void} */ (fn)(node));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue