diff --git a/packages/svelte/package.json b/packages/svelte/package.json index ee35cda2bd..e0f6afe6fc 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -136,7 +136,7 @@ ], "scripts": { "build": "node scripts/process-messages && rollup -c && pnpm generate:types && node scripts/check-treeshakeability.js", - "dev": "node scripts/process-messages && rollup -cw", + "dev": "node scripts/process-messages -w & rollup -cw", "check": "tsc --project tsconfig.runtime.json && tsc && cd ./tests/types && tsc", "check:watch": "tsc --watch", "generate:version": "node ./scripts/generate-version.js", diff --git a/packages/svelte/scripts/process-messages/index.js b/packages/svelte/scripts/process-messages/index.js index 80619acfa7..81c59271de 100644 --- a/packages/svelte/scripts/process-messages/index.js +++ b/packages/svelte/scripts/process-messages/index.js @@ -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>} */ -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>} */ + 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`, + '\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`, - '\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();