From 48f2bffe72cf79379f87c5fdc4e073c937d333f6 Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Tue, 7 Jul 2026 19:57:44 +0200 Subject: [PATCH 1/5] stuff --- packages/sv-utils/src/sanitize.ts | 16 ++++++- packages/sv-utils/src/tests/sanitize.ts | 61 +++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/packages/sv-utils/src/sanitize.ts b/packages/sv-utils/src/sanitize.ts index 28b8454c6..b6361c649 100644 --- a/packages/sv-utils/src/sanitize.ts +++ b/packages/sv-utils/src/sanitize.ts @@ -79,8 +79,22 @@ export function minimizeDiff(old: string, updated: string): string { for (let i = 0; i < diff.length; i += 1) { const part = diff[i]; const next = diff[i + 1]; + const afterNext = diff[i + 2]; - if (part.removed && next?.added) { + if ( + part.removed && + next && + !next.added && + !next.removed && + isOnlyWhitespace(next.value) && + afterNext?.added && + normalizeWhitespace(part.value) === normalizeWhitespace(afterNext.value) + ) { + // diffLines represents a blank line moving across otherwise unchanged content as the + // content being removed, the blank line staying put, and the content being added again. + out += part.value + next.value; + i += 2; + } else if (part.removed && next?.added) { // a changed range. If it differs only by formatting (whitespace, semicolons, rewrapping) // restore the original verbatim; otherwise keep the new content, minus inserted blanks but // keeping the original's blank lines. diff --git a/packages/sv-utils/src/tests/sanitize.ts b/packages/sv-utils/src/tests/sanitize.ts index 38bc503a2..f12fb4fb4 100644 --- a/packages/sv-utils/src/tests/sanitize.ts +++ b/packages/sv-utils/src/tests/sanitize.ts @@ -165,6 +165,67 @@ describe('minimizeDiff', () => { expect(minimizeDiff(old, updated)).toBe(old); }); + it('restores a blank line removed between unchanged statements', () => { + const old = dedent` + const cache = getCache(); + const host_key = get_host_key(); + + const sha = await canonicalize_commit_sha(); + `; + + const updated = dedent` + const cache = getCache(); + const host_key = get_host_key(); + const sha = await canonicalize_commit_sha(); + `; + + expect(minimizeDiff(old, updated)).toBe(old); + }); + + it('restores blank lines shifted within a formatting-only hunk', () => { + const old = dedent` + const parts = state.split('.'); + if (parts.length !== 2) return null; + const [body_b64, sig_b64] = parts; + + let body_bytes: Uint8Array; + let sig_bytes: Uint8Array; + try { + body_bytes = b64url_decode(body_b64); + sig_bytes = b64url_decode(sig_b64); + } catch { + return null; + } + + const expected = await hmac_sha256(require_state_secret(), body_bytes); + if (!timing_safe_equal(expected, sig_bytes)) return null; + + try { + `; + + const updated = dedent` + const parts = state.split('.'); + + if (parts.length !== 2) return null; + + const [body_b64, sig_b64] = parts; + let body_bytes: Uint8Array; + let sig_bytes: Uint8Array; + + try { + body_bytes = b64url_decode(body_b64); + sig_bytes = b64url_decode(sig_b64); + } catch { + return null; + } + const expected = await hmac_sha256(require_state_secret(), body_bytes); + if (!timing_safe_equal(expected, sig_bytes)) return null; + try { + `; + + expect(minimizeDiff(old, updated)).toBe(old); + }); + it('keeps an original blank line that is bundled into a real change', () => { // The diff groups the leading blank line together with the changed line. The blank line is // layout, not content, so it must survive even though the adjacent line really changed. From 6f6ff21ee639fb81f482abbdfffd89a499698a6e Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Tue, 7 Jul 2026 20:46:54 +0200 Subject: [PATCH 2/5] more stuff --- packages/sv-utils/src/sanitize.ts | 60 ++++++++++++++++--- packages/sv-utils/src/tests/sanitize.ts | 77 +++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 7 deletions(-) diff --git a/packages/sv-utils/src/sanitize.ts b/packages/sv-utils/src/sanitize.ts index b6361c649..493630b30 100644 --- a/packages/sv-utils/src/sanitize.ts +++ b/packages/sv-utils/src/sanitize.ts @@ -43,6 +43,47 @@ function toLines(value: string): string[] { return value.match(/[^\n]*\n|[^\n]+$/g) ?? []; } +function restoreBlankLineLayout(old: string, updated: string): string { + const gaps = new Map(); + let previous: string | undefined; + let gap = ''; + + for (const line of toLines(old)) { + if (isOnlyWhitespace(line)) { + gap += line; + continue; + } + + const current = normalizeWhitespace(line); + if (previous !== undefined) { + const key = `${previous}\0${current}`; + const existing = gaps.get(key); + if (existing === undefined || existing === gap) gaps.set(key, gap); + else gaps.set(key, null); + } + previous = current; + gap = ''; + } + + let out = ''; + previous = undefined; + gap = ''; + for (const line of toLines(updated)) { + if (isOnlyWhitespace(line)) { + gap += line; + continue; + } + + const current = normalizeWhitespace(line); + const original = previous === undefined ? undefined : gaps.get(`${previous}\0${current}`); + out += (original ?? gap) + line; + previous = current; + gap = ''; + } + + return out + gap; +} + /** * Minimizes formatting churn in generated output. * @@ -55,14 +96,8 @@ function toLines(value: string): string[] { * Brand-new files (no original to diff against) are kept verbatim: their blank lines are authored * layout, not printer churn, so stripping them would mangle generated markdown, env files, etc. */ -// `diffLines` is O(N·D) in the number of differing lines, which explodes on large, heavily -// reformatted files (e.g. a minified bundle reprinted by a pretty-printer). Past this size we skip -// minimization and keep the printer's output verbatim rather than hanging. -const MAX_DIFF_INPUT_BYTES = 512 * 1024; - export function minimizeDiff(old: string, updated: string): string { if (isOnlyWhitespace(old)) return updated; - if (old.length > MAX_DIFF_INPUT_BYTES || updated.length > MAX_DIFF_INPUT_BYTES) return updated; // Normalize line endings first: on Windows the original is often CRLF while the printer emits LF, // which would make every line differ and collapse the diff, defeating the restoration below. @@ -82,6 +117,17 @@ export function minimizeDiff(old: string, updated: string): string { const afterNext = diff[i + 2]; if ( + part.added && + next && + !next.added && + !next.removed && + isOnlyWhitespace(next.value) && + afterNext?.removed && + normalizeWhitespace(part.value) === normalizeWhitespace(afterNext.value) + ) { + out += next.value + afterNext.value; + i += 2; + } else if ( part.removed && next && !next.added && @@ -112,5 +158,5 @@ export function minimizeDiff(old: string, updated: string): string { } } - return out; + return restoreBlankLineLayout(old.replace(/\r\n/g, '\n'), out); } diff --git a/packages/sv-utils/src/tests/sanitize.ts b/packages/sv-utils/src/tests/sanitize.ts index f12fb4fb4..74b588aae 100644 --- a/packages/sv-utils/src/tests/sanitize.ts +++ b/packages/sv-utils/src/tests/sanitize.ts @@ -182,6 +182,83 @@ describe('minimizeDiff', () => { expect(minimizeDiff(old, updated)).toBe(old); }); + it('restores original layout when blank lines move within a function', () => { + // The reindentation is what makes diffLines misalign the otherwise unchanged statements. The + // behavior under test is that the blank lines must still stay in their original positions. + const old = dedent` + function parse(value: string) { + let parsed: URL; + try { + parsed = new URL(value, 'http://safe-next.invalid'); + } catch { + return null; + } + if (parsed.origin !== 'http://safe-next.invalid') return null; + + for (const prefix of BLOCKLIST) { + if (parsed.pathname === prefix) return null; + } + + return parsed.pathname; + } + `; + + const updated = dedent` + function parse(value: string) { + let parsed: URL; + + try { + parsed = new URL(value, 'http://safe-next.invalid'); + } catch { + return null; + } + + if (parsed.origin !== 'http://safe-next.invalid') return null; + for (const prefix of BLOCKLIST) { + if (parsed.pathname === prefix) return null; + } + return parsed.pathname; + } + `; + + const blankLineLayout = (value: string) => + value + .split('\n') + .map((line) => (line.trim() ? 'content' : '')) + .join('\n'); + expect(blankLineLayout(minimizeDiff(old, updated))).toBe(blankLineLayout(old)); + }); + + it('restores blank lines around a rewrapped statement', () => { + const old = dedent` + const cache = getCache(); + const host_key = get_host_key(); + + const sha = await canonicalize_commit_sha({ + owner: opts.owner, + repo: opts.repo, + commit: opts.commit + }); + + // validate sha + const immutable = true; + `; + + const updated = dedent` + const cache = getCache(); + const host_key = get_host_key(); + const sha = await canonicalize_commit_sha({ owner: opts.owner, repo: opts.repo, commit: opts.commit }); + + // validate sha + + const immutable = true; + `; + + const result = minimizeDiff(old, updated); + expect(result).toContain('const host_key = get_host_key();\n\nconst sha'); + expect(result).toContain('});\n\n// validate sha\nconst immutable = true;'); + }); + it('restores blank lines shifted within a formatting-only hunk', () => { const old = dedent` const parts = state.split('.'); From 5e7e215a039d4cfa91bb67c7c07a95fd1fedeed6 Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Tue, 7 Jul 2026 20:49:30 +0200 Subject: [PATCH 3/5] removed --- packages/sv-utils/src/sanitize.ts | 133 ------------------------------ 1 file changed, 133 deletions(-) diff --git a/packages/sv-utils/src/sanitize.ts b/packages/sv-utils/src/sanitize.ts index 493630b30..6a36a3649 100644 --- a/packages/sv-utils/src/sanitize.ts +++ b/packages/sv-utils/src/sanitize.ts @@ -27,136 +27,3 @@ export function sanitizeName(name: string, style: 'package' | 'wrangler'): strin } return sanitized || 'undefined-sv-name'; } - -function normalizeWhitespace(value: string): string { - // Ignore formatting-only characters that commonly differ between source code and generated - // output: whitespace, semicolons, commas, and parentheses. - return value.replace(/[\s;,()]+/g, ''); -} - -function isOnlyWhitespace(value: string): boolean { - return value.trim() === ''; -} - -/** Splits content into lines, each keeping its trailing newline (matches the diff line tokenizer). */ -function toLines(value: string): string[] { - return value.match(/[^\n]*\n|[^\n]+$/g) ?? []; -} - -function restoreBlankLineLayout(old: string, updated: string): string { - const gaps = new Map(); - let previous: string | undefined; - let gap = ''; - - for (const line of toLines(old)) { - if (isOnlyWhitespace(line)) { - gap += line; - continue; - } - - const current = normalizeWhitespace(line); - if (previous !== undefined) { - const key = `${previous}\0${current}`; - const existing = gaps.get(key); - if (existing === undefined || existing === gap) gaps.set(key, gap); - else gaps.set(key, null); - } - previous = current; - gap = ''; - } - - let out = ''; - previous = undefined; - gap = ''; - for (const line of toLines(updated)) { - if (isOnlyWhitespace(line)) { - gap += line; - continue; - } - - const current = normalizeWhitespace(line); - const original = previous === undefined ? undefined : gaps.get(`${previous}\0${current}`); - out += (original ?? gap) + line; - previous = current; - gap = ''; - } - - return out + gap; -} - -/** - * Minimizes formatting churn in generated output. - * - * A line whose content is unchanged - ignoring whitespace, semicolons, commas and parentheses - is - * restored verbatim from the original. This reverts everything a printer reformats but doesn't - * meaningfully change: re-indentation, added semicolons, rewrapped punctuation and, crucially, the - * blank lines printers like to insert between statements. Only lines with real content changes keep - * the printer's output (printer-inserted blank lines among them are dropped). - * - * Brand-new files (no original to diff against) are kept verbatim: their blank lines are authored - * layout, not printer churn, so stripping them would mangle generated markdown, env files, etc. - */ -export function minimizeDiff(old: string, updated: string): string { - if (isOnlyWhitespace(old)) return updated; - - // Normalize line endings first: on Windows the original is often CRLF while the printer emits LF, - // which would make every line differ and collapse the diff, defeating the restoration below. - const diff = diffLines(old.replace(/\r\n/g, '\n'), updated.replace(/\r\n/g, '\n')); - - // blank lines are layout, not content: keep the original's, drop the ones printers insert - const realContent = (value: string) => - toLines(value) - .filter((l) => !isOnlyWhitespace(l)) - .join(''); - const blankLines = (value: string) => toLines(value).filter(isOnlyWhitespace).join(''); - - let out = ''; - for (let i = 0; i < diff.length; i += 1) { - const part = diff[i]; - const next = diff[i + 1]; - const afterNext = diff[i + 2]; - - if ( - part.added && - next && - !next.added && - !next.removed && - isOnlyWhitespace(next.value) && - afterNext?.removed && - normalizeWhitespace(part.value) === normalizeWhitespace(afterNext.value) - ) { - out += next.value + afterNext.value; - i += 2; - } else if ( - part.removed && - next && - !next.added && - !next.removed && - isOnlyWhitespace(next.value) && - afterNext?.added && - normalizeWhitespace(part.value) === normalizeWhitespace(afterNext.value) - ) { - // diffLines represents a blank line moving across otherwise unchanged content as the - // content being removed, the blank line staying put, and the content being added again. - out += part.value + next.value; - i += 2; - } else if (part.removed && next?.added) { - // a changed range. If it differs only by formatting (whitespace, semicolons, rewrapping) - // restore the original verbatim; otherwise keep the new content, minus inserted blanks but - // keeping the original's blank lines. - out += - normalizeWhitespace(part.value) === normalizeWhitespace(next.value) - ? part.value - : blankLines(part.value) + realContent(next.value); - i += 1; - } else if (part.removed) { - out += blankLines(part.value); // keep original blank lines, drop removed content - } else if (part.added) { - out += realContent(part.value); // real new content, minus printer-inserted blanks - } else { - out += part.value; // unchanged - } - } - - return restoreBlankLineLayout(old.replace(/\r\n/g, '\n'), out); -} From 1e429e59102416874f640fe80d6d3ecc6d44d7d3 Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Tue, 7 Jul 2026 21:33:04 +0200 Subject: [PATCH 4/5] improved formatting --- packages/sv-utils/api-surface.md | 1 + packages/sv-utils/src/sanitize.ts | 153 ++++++++++++++++++++++++ packages/sv-utils/src/tests/sanitize.ts | 67 ++++++++++- 3 files changed, 220 insertions(+), 1 deletion(-) diff --git a/packages/sv-utils/api-surface.md b/packages/sv-utils/api-surface.md index 92975e35c..e79fd55d3 100644 --- a/packages/sv-utils/api-surface.md +++ b/packages/sv-utils/api-surface.md @@ -779,6 +779,7 @@ type Printer = (content: string, alt?: string) => string; declare function createPrinter(...conditions: boolean[]): Printer[]; declare function sanitizeName(name: string, style: 'package' | 'wrangler'): string; + declare function minimizeDiff(old: string, updated: string): string; declare const downloadJson: (url: string) => Promise; type Package = { diff --git a/packages/sv-utils/src/sanitize.ts b/packages/sv-utils/src/sanitize.ts index 6a36a3649..abbfbe002 100644 --- a/packages/sv-utils/src/sanitize.ts +++ b/packages/sv-utils/src/sanitize.ts @@ -27,3 +27,156 @@ export function sanitizeName(name: string, style: 'package' | 'wrangler'): strin } return sanitized || 'undefined-sv-name'; } + +/** + * Minimizes formatting churn in generated output. + * + * Formatting-only hunks are restored verbatim from the original. Real changes keep the updated + * content while reusing the original blank-line layout when the old and updated lines can be paired + * safely. This avoids unrelated punctuation and blank-line churn around useful edits without + * rewriting authored content. + * + * Brand-new or whitespace-only files are kept verbatim: their blank lines are authored layout, not + * printer churn, so stripping them would mangle generated markdown, env files, etc. + */ +export function minimizeDiff(old: string, updated: string): string { + if (isBlank(old)) return updated; + + const original = normalizeLineEndings(old); + const replacement = normalizeLineEndings(updated); + + // These passes need different line alignment. First restore formatting-only replacements using + // exact lines, then ignore indentation while restoring the original blank-line layout. + const formattingMinimized = restoreFormattingOnlyChanges(original, replacement); + return restoreBlankLineLayout(original, formattingMinimized); +} + +type LineChange = ReturnType[number]; + +function restoreFormattingOnlyChanges(original: string, replacement: string): string { + // Exact matching keeps closing braces and other repeated punctuation attached to the replacement + // that reformatted them instead of letting diffLines use them as misleading anchors. + const changes = diffLines(original, replacement); + let result = ''; + + for (let i = 0; i < changes.length; i += 1) { + const change = changes[i]; + const next = changes[i + 1]; + + if (change.removed && next?.added) { + result += + normalizeForComparison(change.value) === normalizeForComparison(next.value) + ? change.value + : next.value; + i += 1; + } else if (!change.removed) { + result += change.value; + } + } + + return result; +} + +function restoreBlankLineLayout(original: string, replacement: string): string { + // The formatting pass has already handled rewrapped hunks. Ignoring whitespace here gives + // diffLines stable anchors across reindentation so isolated blank-line moves remain visible. + const changes = diffLines(original, replacement, { ignoreWhitespace: true }); + let result = ''; + + for (let i = 0; i < changes.length; ) { + const change = changes[i]; + const next = changes[i + 1]; + const afterNext = changes[i + 2]; + + if (isChange(change) && next && isOppositeChange(change, next)) { + result += resolveReplacement([change, next]); + i += 2; + continue; + } + + // diffLines can use a blank line as an anchor between the removed and added sides of a + // replacement. Consider that anchor as part of the hunk so the decision is still made here. + if ( + isChange(change) && + next && + !isChange(next) && + isBlank(next.value) && + afterNext && + isOppositeChange(change, afterNext) + ) { + result += resolveReplacement([change, next, afterNext]); + i += 3; + continue; + } + + if (!isChange(change)) { + result += change.value; + } else if (isBlank(change.value)) { + // Keep removed blank lines and discard added ones. + if (change.removed) result += change.value; + } else if (change.added) { + result += change.value; + } + + i += 1; + } + + return result; +} + +function normalizeLineEndings(value: string): string { + return value.replace(/\r\n?/g, '\n'); +} + +function isChange(change: LineChange): boolean { + return change.added === true || change.removed === true; +} + +function isOppositeChange(a: LineChange, b: LineChange): boolean { + return (a.added === true && b.removed === true) || (a.removed === true && b.added === true); +} + +function isBlank(value: string): boolean { + return value.trim() === ''; +} + +function resolveReplacement(changes: LineChange[]): string { + // Reconstruct both sides because a replacement may include an unchanged blank-line anchor. + // Formatting-equivalent replacements use the original verbatim; real edits keep their updated + // content while borrowing the original blank-line positions when the lines pair safely. + let original = ''; + let replacement = ''; + + for (const change of changes) { + if (!change.added) original += change.value; + if (!change.removed) replacement += change.value; + } + + if (normalizeForComparison(original) === normalizeForComparison(replacement)) return original; + + return preserveBlankLineLayout(original, replacement); +} + +function normalizeForComparison(value: string): string { + return value + .replace(/\s/g, '') + .replace(/;/g, '') + .replace(/,([}\]])/g, '$1') + .replace(/,$/, ''); +} + +function preserveBlankLineLayout(original: string, replacement: string): string { + const originalLines = original.split('\n'); + const replacementLines = replacement.split('\n'); + const replacementContent = replacementLines.filter((line) => line.trim() !== ''); + + // Positional pairing is only safe when every original content line has an updated counterpart. + if (originalLines.filter((line) => line.trim() !== '').length !== replacementContent.length) { + return replacement; + } + + let contentIndex = 0; + return originalLines + .map((line) => (line.trim() === '' ? line : replacementContent[contentIndex++])) + .join('\n'); +} diff --git a/packages/sv-utils/src/tests/sanitize.ts b/packages/sv-utils/src/tests/sanitize.ts index 74b588aae..af6118223 100644 --- a/packages/sv-utils/src/tests/sanitize.ts +++ b/packages/sv-utils/src/tests/sanitize.ts @@ -93,6 +93,12 @@ describe('minimizeDiff', () => { expect(minimizeDiff('', updated)).toBe(updated); }); + it('treats a whitespace-only original as a brand-new file', () => { + const updated = 'first\n\nsecond\n'; + + expect(minimizeDiff(' \n\t\n', updated)).toBe(updated); + }); + it('preserves blank lines from the original content', () => { const old = dedent` console.log('line1'); @@ -261,6 +267,8 @@ describe('minimizeDiff', () => { it('restores blank lines shifted within a formatting-only hunk', () => { const old = dedent` + const algorithm = 'old'; + const parts = state.split('.'); if (parts.length !== 2) return null; const [body_b64, sig_b64] = parts; @@ -281,6 +289,8 @@ describe('minimizeDiff', () => { `; const updated = dedent` + const algorithm = 'new'; + const parts = state.split('.'); if (parts.length !== 2) return null; @@ -300,7 +310,7 @@ describe('minimizeDiff', () => { try { `; - expect(minimizeDiff(old, updated)).toBe(old); + expect(minimizeDiff(old, updated)).toBe(old.replace("'old'", "'new'")); }); it('keeps an original blank line that is bundled into a real change', () => { @@ -352,6 +362,38 @@ describe('minimizeDiff', () => { expect(minimizeDiff(old, updated)).toBe(expected); }); + it('drops printer-inserted blank lines around a newly inserted line', () => { + const old = dedent` + function require_env(name: string): string { + const value = env[name]; + if (!value) throw new Error(name); + return value; + } + `; + + const updated = dedent` + function require_env(name: string): string { + // @migration-task Rewrite dynamic env lookup manually. + const value = env[name]; + + if (!value) throw new Error(name); + + return value; + } + `; + + const expected = dedent` + function require_env(name: string): string { + // @migration-task Rewrite dynamic env lookup manually. + const value = env[name]; + if (!value) throw new Error(name); + return value; + } + `; + + expect(minimizeDiff(old, updated)).toBe(expected); + }); + it('restores formatting-only hunks when the original uses CRLF line endings', () => { // On Windows the original file is often checked out with CRLF while the printer emits LF. // Without normalization the diff collapses into a single hunk and the formatting-only @@ -515,6 +557,29 @@ describe('minimizeDiff', () => { expect(minimizeDiff(old, updated)).toBe(old); }); + it('restores original formatting when a typed property is collapsed onto one line', () => { + const old = dedent` + type Version = 'old'; + + type Connection = { + pageInfo: { + hasPreviousPage: boolean; + startCursor: string | null; + }; + }; + `; + + const updated = dedent` + type Version = 'new'; + + type Connection = { + pageInfo: { hasPreviousPage: boolean; startCursor: string | null }; + }; + `; + + expect(minimizeDiff(old, updated)).toBe(old.replace("'old'", "'new'")); + }); + it('keeps updated hunks when non-trailing commas change the meaning', () => { const old = dedent` const values = ['a' + 'b']; From 997cba282b6e86278f002322e279fa99d5916126 Mon Sep 17 00:00:00 2001 From: "jyc.dev" Date: Tue, 7 Jul 2026 22:35:22 +0200 Subject: [PATCH 5/5] chore: minimizeDiff follow-ups (size cap, trailing commas, snapshots) (#1164) * re-add diffLines size cap, handle trailing commas before ), trim comments * update migrate snapshots for new minimizeDiff blank-line behavior * update cli create snapshots for new minimizeDiff blank-line behavior --- packages/sv-utils/src/sanitize.ts | 34 ++++++------------- packages/sv-utils/src/tests/sanitize.ts | 31 +++++++++++++++++ .../create-experimental/.env.example | 2 ++ .../create-experimental/src/app.d.ts | 3 ++ .../src/lib/server/db/schema.ts | 1 + .../create-experimental/vite.config.ts | 1 - .../create-with-all-addons/.env.example | 2 ++ .../create-with-all-addons/src/app.d.ts | 2 ++ .../src/hooks.server.ts | 3 ++ .../src/lib/server/db/schema.ts | 1 + .../src/routes/+layout.svelte | 1 + .../create-with-all-addons/vite.config.ts | 2 +- .../strings-comments.snapshot.svelte | 1 + .../dynamic-import-namespace.snapshot.ts | 1 + .../svelte-config/eslint.config.snapshot.js | 1 + .../svelte-config/vite.config.snapshot.ts | 2 ++ 16 files changed, 63 insertions(+), 25 deletions(-) diff --git a/packages/sv-utils/src/sanitize.ts b/packages/sv-utils/src/sanitize.ts index abbfbe002..8557f8e68 100644 --- a/packages/sv-utils/src/sanitize.ts +++ b/packages/sv-utils/src/sanitize.ts @@ -28,25 +28,20 @@ export function sanitizeName(name: string, style: 'package' | 'wrangler'): strin return sanitized || 'undefined-sv-name'; } +// `diffLines` is O(N·D); past this size we keep the printer output rather than hanging. +const MAX_DIFF_INPUT_BYTES = 512 * 1024; + /** - * Minimizes formatting churn in generated output. - * - * Formatting-only hunks are restored verbatim from the original. Real changes keep the updated - * content while reusing the original blank-line layout when the old and updated lines can be paired - * safely. This avoids unrelated punctuation and blank-line churn around useful edits without - * rewriting authored content. - * - * Brand-new or whitespace-only files are kept verbatim: their blank lines are authored layout, not - * printer churn, so stripping them would mangle generated markdown, env files, etc. + * Minimizes formatting churn in reprinted output: formatting-only hunks are restored verbatim from + * the original, real changes keep the updated content but reuse the original blank-line layout. */ export function minimizeDiff(old: string, updated: string): string { if (isBlank(old)) return updated; + if (old.length > MAX_DIFF_INPUT_BYTES || updated.length > MAX_DIFF_INPUT_BYTES) return updated; const original = normalizeLineEndings(old); const replacement = normalizeLineEndings(updated); - // These passes need different line alignment. First restore formatting-only replacements using - // exact lines, then ignore indentation while restoring the original blank-line layout. const formattingMinimized = restoreFormattingOnlyChanges(original, replacement); return restoreBlankLineLayout(original, formattingMinimized); } @@ -54,8 +49,7 @@ export function minimizeDiff(old: string, updated: string): string { type LineChange = ReturnType[number]; function restoreFormattingOnlyChanges(original: string, replacement: string): string { - // Exact matching keeps closing braces and other repeated punctuation attached to the replacement - // that reformatted them instead of letting diffLines use them as misleading anchors. + // exact matching keeps reformatted punctuation inside the hunk instead of becoming a false anchor const changes = diffLines(original, replacement); let result = ''; @@ -78,8 +72,7 @@ function restoreFormattingOnlyChanges(original: string, replacement: string): st } function restoreBlankLineLayout(original: string, replacement: string): string { - // The formatting pass has already handled rewrapped hunks. Ignoring whitespace here gives - // diffLines stable anchors across reindentation so isolated blank-line moves remain visible. + // ignoring whitespace keeps reindented lines as anchors so only blank-line moves show up const changes = diffLines(original, replacement, { ignoreWhitespace: true }); let result = ''; @@ -94,8 +87,7 @@ function restoreBlankLineLayout(original: string, replacement: string): string { continue; } - // diffLines can use a blank line as an anchor between the removed and added sides of a - // replacement. Consider that anchor as part of the hunk so the decision is still made here. + // a blank line can anchor between the removed and added sides; treat it as part of the hunk if ( isChange(change) && next && @@ -112,7 +104,6 @@ function restoreBlankLineLayout(original: string, replacement: string): string { if (!isChange(change)) { result += change.value; } else if (isBlank(change.value)) { - // Keep removed blank lines and discard added ones. if (change.removed) result += change.value; } else if (change.added) { result += change.value; @@ -141,9 +132,6 @@ function isBlank(value: string): boolean { } function resolveReplacement(changes: LineChange[]): string { - // Reconstruct both sides because a replacement may include an unchanged blank-line anchor. - // Formatting-equivalent replacements use the original verbatim; real edits keep their updated - // content while borrowing the original blank-line positions when the lines pair safely. let original = ''; let replacement = ''; @@ -161,7 +149,7 @@ function normalizeForComparison(value: string): string { return value .replace(/\s/g, '') .replace(/;/g, '') - .replace(/,([}\]])/g, '$1') + .replace(/,([}\])])/g, '$1') .replace(/,$/, ''); } @@ -170,7 +158,7 @@ function preserveBlankLineLayout(original: string, replacement: string): string const replacementLines = replacement.split('\n'); const replacementContent = replacementLines.filter((line) => line.trim() !== ''); - // Positional pairing is only safe when every original content line has an updated counterpart. + // positional pairing is only safe when content line counts match if (originalLines.filter((line) => line.trim() !== '').length !== replacementContent.length) { return replacement; } diff --git a/packages/sv-utils/src/tests/sanitize.ts b/packages/sv-utils/src/tests/sanitize.ts index af6118223..948931a03 100644 --- a/packages/sv-utils/src/tests/sanitize.ts +++ b/packages/sv-utils/src/tests/sanitize.ts @@ -580,6 +580,37 @@ describe('minimizeDiff', () => { expect(minimizeDiff(old, updated)).toBe(old.replace("'old'", "'new'")); }); + it('restores original formatting when a call with trailing commas is collapsed onto one line', () => { + const old = dedent` + const version = 'old'; + + const sha = await canonicalize( + opts.owner, + opts.repo, + opts.commit, + ); + `; + + const updated = dedent` + const version = 'new'; + + const sha = await canonicalize(opts.owner, opts.repo, opts.commit); + `; + + expect(minimizeDiff(old, updated)).toBe(old.replace("'old'", "'new'")); + }); + + it('keeps oversized inputs verbatim instead of diffing them', () => { + // worst case for diffLines is O(N·D); past the size cap the updated content is returned as-is + const line = (i: number) => `const value_${i} = compute(${i});`; + const old = Array.from({ length: 20_000 }, (_, i) => line(i)).join('\n'); + const updated = Array.from({ length: 20_000 }, (_, i) => `\t${line(i)}\n`).join(''); + + const start = performance.now(); + expect(minimizeDiff(old, updated)).toBe(updated); + expect(performance.now() - start).toBeLessThan(1000); + }); + it('keeps updated hunks when non-trailing commas change the meaning', () => { const old = dedent` const values = ['a' + 'b']; diff --git a/packages/sv/src/cli/tests/snapshots/create-experimental/.env.example b/packages/sv/src/cli/tests/snapshots/create-experimental/.env.example index 6bb40ccbe..34a24e9fe 100644 --- a/packages/sv/src/cli/tests/snapshots/create-experimental/.env.example +++ b/packages/sv/src/cli/tests/snapshots/create-experimental/.env.example @@ -1,6 +1,8 @@ # Drizzle DATABASE_URL=file:local.db + ORIGIN="" + # Better Auth # For production use 32 characters and generated with high entropy # https://www.better-auth.com/docs/installation diff --git a/packages/sv/src/cli/tests/snapshots/create-experimental/src/app.d.ts b/packages/sv/src/cli/tests/snapshots/create-experimental/src/app.d.ts index 6ccfd4dd2..16910ac28 100644 --- a/packages/sv/src/cli/tests/snapshots/create-experimental/src/app.d.ts +++ b/packages/sv/src/cli/tests/snapshots/create-experimental/src/app.d.ts @@ -1,4 +1,5 @@ import type { User, Session } from 'better-auth'; + // See https://svelte.dev/docs/kit/types#app.d.ts // for information about these interfaces declare global { @@ -9,7 +10,9 @@ declare global { caches: CacheStorage; cf?: IncomingRequestCfProperties } + interface Locals { user?: User; session?: Session } + // interface Error {} // interface PageData {} // interface PageState {} diff --git a/packages/sv/src/cli/tests/snapshots/create-experimental/src/lib/server/db/schema.ts b/packages/sv/src/cli/tests/snapshots/create-experimental/src/lib/server/db/schema.ts index 74eb9ddbf..35de29be6 100644 --- a/packages/sv/src/cli/tests/snapshots/create-experimental/src/lib/server/db/schema.ts +++ b/packages/sv/src/cli/tests/snapshots/create-experimental/src/lib/server/db/schema.ts @@ -5,4 +5,5 @@ export const task = sqliteTable('task', { title: text('title').notNull(), priority: integer('priority').notNull().default(1) }); + export * from './auth.schema'; diff --git a/packages/sv/src/cli/tests/snapshots/create-experimental/vite.config.ts b/packages/sv/src/cli/tests/snapshots/create-experimental/vite.config.ts index 60c7bfb22..ed92d13b0 100644 --- a/packages/sv/src/cli/tests/snapshots/create-experimental/vite.config.ts +++ b/packages/sv/src/cli/tests/snapshots/create-experimental/vite.config.ts @@ -10,7 +10,6 @@ export default defineConfig({ runes: ({ filename }) => filename.split(/[/\\]/).includes('node_modules') ? undefined : true }, - adapter: adapter(), experimental: { explicitEnvironmentVariables: true }, typescript: { diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/.env.example b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/.env.example index 6bb40ccbe..34a24e9fe 100644 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/.env.example +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/.env.example @@ -1,6 +1,8 @@ # Drizzle DATABASE_URL=file:local.db + ORIGIN="" + # Better Auth # For production use 32 characters and generated with high entropy # https://www.better-auth.com/docs/installation diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/app.d.ts b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/app.d.ts index f4734f616..7a2a5d2e4 100644 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/app.d.ts +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/app.d.ts @@ -1,9 +1,11 @@ import type { User, Session } from 'better-auth'; + // See https://svelte.dev/docs/kit/types#app.d.ts // for information about these interfaces declare global { namespace App { interface Locals { user?: User; session?: Session } + // interface Error {} // interface PageData {} // interface PageState {} diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/hooks.server.ts b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/hooks.server.ts index 167ed2bdb..f0872bb70 100644 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/hooks.server.ts +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/hooks.server.ts @@ -16,10 +16,13 @@ const handleParaglide: Handle = ({ event, resolve }) => paraglideMiddleware(even const handleBetterAuth: Handle = async ({ event, resolve }) => { const session = await auth.api.getSession({ headers: event.request.headers }); + if (session) { event.locals.session = session.session; event.locals.user = session.user; } + return svelteKitHandler({ event, resolve, auth, building }); }; + export const handle: Handle = sequence(handleParaglide, handleBetterAuth); diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/lib/server/db/schema.ts b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/lib/server/db/schema.ts index 74eb9ddbf..35de29be6 100644 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/lib/server/db/schema.ts +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/lib/server/db/schema.ts @@ -5,4 +5,5 @@ export const task = sqliteTable('task', { title: text('title').notNull(), priority: integer('priority').notNull().default(1) }); + export * from './auth.schema'; diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/routes/+layout.svelte b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/routes/+layout.svelte index d3dabe29a..0f0a134b4 100644 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/routes/+layout.svelte +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/routes/+layout.svelte @@ -14,6 +14,7 @@ {@render children()} +
{#each locales as locale (locale)} filename.split(/[/\\]/).includes('node_modules') ? undefined : true }, - adapter: adapter(), preprocess: [mdsvex({ extensions: ['.svx', '.md'] })], extensions: ['.svelte', '.svx', '.md'], @@ -44,6 +43,7 @@ export default defineConfig({ exclude: ['src/lib/server/**'] } }, + { extends: './vite.config.ts', test: { diff --git a/packages/sv/src/migrate/migrations/app-state/tests/app-state/strings-comments.snapshot.svelte b/packages/sv/src/migrate/migrations/app-state/tests/app-state/strings-comments.snapshot.svelte index 54db38e97..80829ce6d 100644 --- a/packages/sv/src/migrate/migrations/app-state/tests/app-state/strings-comments.snapshot.svelte +++ b/packages/sv/src/migrate/migrations/app-state/tests/app-state/strings-comments.snapshot.svelte @@ -6,4 +6,5 @@ * This is a page */ + Nice page!
{page.url}
diff --git a/packages/sv/src/migrate/migrations/sveltekit-3/tests/environment/dynamic-import-namespace.snapshot.ts b/packages/sv/src/migrate/migrations/sveltekit-3/tests/environment/dynamic-import-namespace.snapshot.ts index 8278edc28..50546bc05 100644 --- a/packages/sv/src/migrate/migrations/sveltekit-3/tests/environment/dynamic-import-namespace.snapshot.ts +++ b/packages/sv/src/migrate/migrations/sveltekit-3/tests/environment/dynamic-import-namespace.snapshot.ts @@ -1,6 +1,7 @@ export async function requireDynamicImportEnv(name: string): Promise { // @migration-task Declare the imported env variables in src/env.ts manually. const privateEnv = await import('$app/env/private'); + const value = privateEnv[name]; if (!value) throw new Error(`${name} must be set`); return value; diff --git a/packages/sv/src/migrate/migrations/sveltekit-3/tests/svelte-config/eslint.config.snapshot.js b/packages/sv/src/migrate/migrations/sveltekit-3/tests/svelte-config/eslint.config.snapshot.js index 5bb4f4c31..1dc6c34a4 100644 --- a/packages/sv/src/migrate/migrations/sveltekit-3/tests/svelte-config/eslint.config.snapshot.js +++ b/packages/sv/src/migrate/migrations/sveltekit-3/tests/svelte-config/eslint.config.snapshot.js @@ -1,6 +1,7 @@ import js from '@eslint/js'; import svelte from 'eslint-plugin-svelte'; import { defineConfig } from 'eslint/config'; + // @migration-task svelteConfig should not be needed anymore, see https://github.com/sveltejs/eslint-plugin-svelte/issues/1550 import svelteConfig from './svelte.config.js'; diff --git a/packages/sv/src/migrate/migrations/sveltekit-3/tests/svelte-config/vite.config.snapshot.ts b/packages/sv/src/migrate/migrations/sveltekit-3/tests/svelte-config/vite.config.snapshot.ts index 757db8736..1cb998f7b 100644 --- a/packages/sv/src/migrate/migrations/sveltekit-3/tests/svelte-config/vite.config.snapshot.ts +++ b/packages/sv/src/migrate/migrations/sveltekit-3/tests/svelte-config/vite.config.snapshot.ts @@ -9,7 +9,9 @@ import devtoolsJson from 'vite-plugin-devtools-json'; // derive paths and adapter config from the environment const { paths, adapterConfig } = helper(process.env['SOME_ENV_VAR']); + const base = process.env['VITEST'] ? '' : '/some-base'; // empty base while testing + export default defineConfig({ plugins: [ tailwindcss(),