mirror of https://github.com/sveltejs/svelte
thunkify-deriveds-on-server
commit
22e666c438
@ -1,5 +0,0 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: improve error message for migration errors when slot would be renamed
|
@ -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 '../index-client.js';
|
||||
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,18 @@
|
||||
const svg_attributes =
|
||||
'accent-height accumulate additive alignment-baseline allowReorder alphabetic amplitude arabic-form ascent attributeName attributeType autoReverse azimuth baseFrequency baseline-shift baseProfile bbox begin bias by calcMode cap-height class clip clipPathUnits clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering contentScriptType contentStyleType cursor cx cy d decelerate descent diffuseConstant direction display divisor dominant-baseline dur dx dy edgeMode elevation enable-background end exponent externalResourcesRequired fill fill-opacity fill-rule filter filterRes filterUnits flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format from fr fx fy g1 g2 glyph-name glyph-orientation-horizontal glyph-orientation-vertical glyphRef gradientTransform gradientUnits hanging height href horiz-adv-x horiz-origin-x id ideographic image-rendering in in2 intercept k k1 k2 k3 k4 kernelMatrix kernelUnitLength kerning keyPoints keySplines keyTimes lang lengthAdjust letter-spacing lighting-color limitingConeAngle local marker-end marker-mid marker-start markerHeight markerUnits markerWidth mask maskContentUnits maskUnits mathematical max media method min mode name numOctaves offset onabort onactivate onbegin onclick onend onerror onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup onrepeat onresize onscroll onunload opacity operator order orient orientation origin overflow overline-position overline-thickness panose-1 paint-order pathLength patternContentUnits patternTransform patternUnits pointer-events points pointsAtX pointsAtY pointsAtZ preserveAlpha preserveAspectRatio primitiveUnits r radius refX refY rendering-intent repeatCount repeatDur requiredExtensions requiredFeatures restart result rotate rx ry scale seed shape-rendering slope spacing specularConstant specularExponent speed spreadMethod startOffset stdDeviation stemh stemv stitchTiles stop-color stop-opacity strikethrough-position strikethrough-thickness string stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale systemLanguage tabindex tableValues target targetX targetY text-anchor text-decoration text-rendering textLength to transform type u1 u2 underline-position underline-thickness unicode unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical values version vert-adv-y vert-origin-x vert-origin-y viewBox viewTarget visibility width widths word-spacing writing-mode x x-height x1 x2 xChannelSelector xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y y1 y2 yChannelSelector z zoomAndPan'.split(
|
||||
' '
|
||||
);
|
||||
|
||||
const svg_attribute_lookup = new Map();
|
||||
|
||||
svg_attributes.forEach((name) => {
|
||||
svg_attribute_lookup.set(name.toLowerCase(), name);
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
export default function fix_attribute_casing(name) {
|
||||
name = name.toLowerCase();
|
||||
return svg_attribute_lookup.get(name) || name;
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
/** @import { Location } from 'locate-character' */
|
||||
/** @import { Namespace } from '#compiler' */
|
||||
/** @import { ComponentClientTransformState } from '../types.js' */
|
||||
/** @import { Node } from './types.js' */
|
||||
import { TEMPLATE_USE_MATHML, TEMPLATE_USE_SVG } from '../../../../../constants.js';
|
||||
import { dev, locator } from '../../../../state.js';
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
|
||||
/**
|
||||
* @param {Node[]} nodes
|
||||
*/
|
||||
function build_locations(nodes) {
|
||||
const array = b.array([]);
|
||||
|
||||
for (const node of nodes) {
|
||||
if (node.type !== 'element') continue;
|
||||
|
||||
const { line, column } = /** @type {Location} */ (locator(node.start));
|
||||
|
||||
const expression = b.array([b.literal(line), b.literal(column)]);
|
||||
const children = build_locations(node.children);
|
||||
|
||||
if (children.elements.length > 0) {
|
||||
expression.elements.push(children);
|
||||
}
|
||||
|
||||
array.elements.push(expression);
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ComponentClientTransformState} state
|
||||
* @param {Namespace} namespace
|
||||
* @param {number} [flags]
|
||||
*/
|
||||
export function transform_template(state, namespace, flags = 0) {
|
||||
const tree = state.options.fragments === 'tree';
|
||||
|
||||
const expression = tree ? state.template.as_tree() : state.template.as_html();
|
||||
|
||||
if (tree) {
|
||||
if (namespace === 'svg') flags |= TEMPLATE_USE_SVG;
|
||||
if (namespace === 'mathml') flags |= TEMPLATE_USE_MATHML;
|
||||
}
|
||||
|
||||
let call = b.call(
|
||||
tree ? `$.from_tree` : `$.from_${namespace}`,
|
||||
expression,
|
||||
flags ? b.literal(flags) : undefined
|
||||
);
|
||||
|
||||
if (state.template.contains_script_tag) {
|
||||
call = b.call(`$.with_script`, call);
|
||||
}
|
||||
|
||||
if (dev) {
|
||||
call = b.call(
|
||||
'$.add_locations',
|
||||
call,
|
||||
b.member(b.id(state.analysis.name), '$.FILENAME', true),
|
||||
build_locations(state.template.nodes)
|
||||
);
|
||||
}
|
||||
|
||||
return call;
|
||||
}
|
@ -0,0 +1,162 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Node, Element } from './types'; */
|
||||
import { escape_html } from '../../../../../escaping.js';
|
||||
import { is_void } from '../../../../../utils.js';
|
||||
import * as b from '#compiler/builders';
|
||||
import fix_attribute_casing from './fix-attribute-casing.js';
|
||||
import { regex_starts_with_newline } from '../../../patterns.js';
|
||||
|
||||
export class Template {
|
||||
/**
|
||||
* `true` if HTML template contains a `<script>` tag. In this case we need to invoke a special
|
||||
* template instantiation function (see `create_fragment_with_script_from_html` for more info)
|
||||
*/
|
||||
contains_script_tag = false;
|
||||
|
||||
/** `true` if the HTML template needs to be instantiated with `importNode` */
|
||||
needs_import_node = false;
|
||||
|
||||
/** @type {Node[]} */
|
||||
nodes = [];
|
||||
|
||||
/** @type {Node[][]} */
|
||||
#stack = [this.nodes];
|
||||
|
||||
/** @type {Element | undefined} */
|
||||
#element;
|
||||
|
||||
#fragment = this.nodes;
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {number} start
|
||||
*/
|
||||
push_element(name, start) {
|
||||
this.#element = {
|
||||
type: 'element',
|
||||
name,
|
||||
attributes: {},
|
||||
children: [],
|
||||
start
|
||||
};
|
||||
|
||||
this.#fragment.push(this.#element);
|
||||
|
||||
this.#fragment = /** @type {Element} */ (this.#element).children;
|
||||
this.#stack.push(this.#fragment);
|
||||
}
|
||||
|
||||
/** @param {string} [data] */
|
||||
push_comment(data) {
|
||||
this.#fragment.push({ type: 'comment', data });
|
||||
}
|
||||
|
||||
/** @param {AST.Text[]} nodes */
|
||||
push_text(nodes) {
|
||||
this.#fragment.push({ type: 'text', nodes });
|
||||
}
|
||||
|
||||
pop_element() {
|
||||
this.#stack.pop();
|
||||
this.#fragment = /** @type {Node[]} */ (this.#stack.at(-1));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {string | undefined} value
|
||||
*/
|
||||
set_prop(key, value) {
|
||||
/** @type {Element} */ (this.#element).attributes[key] = value;
|
||||
}
|
||||
|
||||
as_html() {
|
||||
return b.template([b.quasi(this.nodes.map(stringify).join(''), true)], []);
|
||||
}
|
||||
|
||||
as_tree() {
|
||||
// if the first item is a comment we need to add another comment for effect.start
|
||||
if (this.nodes[0].type === 'comment') {
|
||||
this.nodes.unshift({ type: 'comment', data: undefined });
|
||||
}
|
||||
|
||||
return b.array(this.nodes.map(objectify));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Node} item
|
||||
*/
|
||||
function stringify(item) {
|
||||
if (item.type === 'text') {
|
||||
return item.nodes.map((node) => node.raw).join('');
|
||||
}
|
||||
|
||||
if (item.type === 'comment') {
|
||||
return item.data ? `<!--${item.data}-->` : '<!>';
|
||||
}
|
||||
|
||||
let str = `<${item.name}`;
|
||||
|
||||
for (const key in item.attributes) {
|
||||
const value = item.attributes[key];
|
||||
|
||||
str += ` ${key}`;
|
||||
if (value !== undefined) str += `="${escape_html(value, true)}"`;
|
||||
}
|
||||
|
||||
if (is_void(item.name)) {
|
||||
str += '/>'; // XHTML compliance
|
||||
} else {
|
||||
str += `>`;
|
||||
str += item.children.map(stringify).join('');
|
||||
str += `</${item.name}>`;
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
/** @param {Node} item */
|
||||
function objectify(item) {
|
||||
if (item.type === 'text') {
|
||||
return b.literal(item.nodes.map((node) => node.data).join(''));
|
||||
}
|
||||
|
||||
if (item.type === 'comment') {
|
||||
return item.data ? b.array([b.literal(`// ${item.data}`)]) : null;
|
||||
}
|
||||
|
||||
const element = b.array([b.literal(item.name)]);
|
||||
|
||||
const attributes = b.object([]);
|
||||
|
||||
for (const key in item.attributes) {
|
||||
const value = item.attributes[key];
|
||||
|
||||
attributes.properties.push(
|
||||
b.prop(
|
||||
'init',
|
||||
b.key(fix_attribute_casing(key)),
|
||||
value === undefined ? b.void0 : b.literal(value)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (attributes.properties.length > 0 || item.children.length > 0) {
|
||||
element.elements.push(attributes.properties.length > 0 ? attributes : b.null);
|
||||
}
|
||||
|
||||
if (item.children.length > 0) {
|
||||
const children = item.children.map(objectify);
|
||||
element.elements.push(...children);
|
||||
|
||||
// special case — strip leading newline from `<pre>` and `<textarea>`
|
||||
if (item.name === 'pre' || item.name === 'textarea') {
|
||||
const first = children[0];
|
||||
if (first?.type === 'Literal') {
|
||||
first.value = /** @type {string} */ (first.value).replace(regex_starts_with_newline, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import type { AST } from '#compiler';
|
||||
|
||||
export interface Element {
|
||||
type: 'element';
|
||||
name: string;
|
||||
attributes: Record<string, string | undefined>;
|
||||
children: Node[];
|
||||
/** used for populating __svelte_meta */
|
||||
start: number;
|
||||
}
|
||||
|
||||
export interface Text {
|
||||
type: 'text';
|
||||
nodes: AST.Text[];
|
||||
}
|
||||
|
||||
export interface Comment {
|
||||
type: 'comment';
|
||||
data: string | undefined;
|
||||
}
|
||||
|
||||
export type Node = Element | Text | Comment;
|
@ -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,111 @@
|
||||
/** @import { ClassBody, Expression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition } from 'estree' */
|
||||
/** @import { Context, StateField } from '../types' */
|
||||
/** @import { CallExpression, ClassBody, ClassDeclaration, ClassExpression, 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 { dev } from '../../../../state.js';
|
||||
import { get_parent } from '../../../../utils/ast.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
|
||||
|
||||
const key = b.key(name);
|
||||
|
||||
body.push(
|
||||
b.prop_def(field.key, null),
|
||||
|
||||
b.method('get', key, [], [b.return(b.call('$.get', member))]),
|
||||
|
||||
b.method(
|
||||
'set',
|
||||
key,
|
||||
[b.id('value')],
|
||||
[b.stmt(b.call('$.set', member, b.id('value'), should_proxy && b.true))]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 declaration = /** @type {ClassDeclaration | ClassExpression} */ (
|
||||
get_parent(context.path, -1)
|
||||
);
|
||||
|
||||
// Replace parts of the class body
|
||||
for (const definition of node.body) {
|
||||
if (definition.type !== 'PropertyDefinition') {
|
||||
body.push(
|
||||
/** @type {MethodDefinition | StaticBlock} */ (context.visit(definition, child_state))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
private_ids.push(deconflicted);
|
||||
field.id = b.private_id(deconflicted);
|
||||
}
|
||||
const name = get_name(definition.key);
|
||||
const field = name && /** @type {StateField} */ (state_fields.get(name));
|
||||
|
||||
/** @type {Array<MethodDefinition | PropertyDefinition>} */
|
||||
const body = [];
|
||||
if (!field) {
|
||||
body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state)));
|
||||
continue;
|
||||
}
|
||||
|
||||
const child_state = { ...context.state, public_state, private_state };
|
||||
if (name[0] === '#') {
|
||||
let value = definition.value
|
||||
? /** @type {CallExpression} */ (context.visit(definition.value, child_state))
|
||||
: undefined;
|
||||
|
||||
// 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 (dev && field.node === definition) {
|
||||
value = b.call('$.tag', value, b.literal(`${declaration.id?.name ?? '[class]'}.${name}`));
|
||||
}
|
||||
}
|
||||
|
||||
body.push(/** @type {MethodDefinition} **/ (context.visit(definition, child_state)));
|
||||
}
|
||||
body.push(b.prop_def(definition.key, value));
|
||||
} else if (field.node === definition) {
|
||||
let call = /** @type {CallExpression} */ (context.visit(field.value, child_state));
|
||||
|
||||
return { ...node, body };
|
||||
}
|
||||
if (dev) {
|
||||
call = b.call('$.tag', call, b.literal(`${declaration.id?.name ?? '[class]'}.${name}`));
|
||||
}
|
||||
const member = b.member(b.this, field.key);
|
||||
const should_proxy = field.type === '$state' && true; // TODO
|
||||
|
||||
/**
|
||||
* @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;
|
||||
body.push(
|
||||
b.prop_def(field.key, call),
|
||||
|
||||
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 };
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue