Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 11 additions & 23 deletions packages/sv-utils/src/sanitize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,34 +28,28 @@ 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);
}

type LineChange = ReturnType<typeof diffLines>[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 = '';

Expand All @@ -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 = '';

Expand All @@ -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 &&
Expand All @@ -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;
Expand Down Expand Up @@ -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 = '';

Expand All @@ -161,7 +149,7 @@ function normalizeForComparison(value: string): string {
return value
.replace(/\s/g, '')
.replace(/;/g, '')
.replace(/,([}\]])/g, '$1')
.replace(/,([}\])])/g, '$1')
.replace(/,$/, '');
}

Expand All @@ -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;
}
Expand Down
31 changes: 31 additions & 0 deletions packages/sv-utils/src/tests/sanitize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -9,7 +10,9 @@ declare global {
caches: CacheStorage;
cf?: IncomingRequestCfProperties
}

interface Locals { user?: User; session?: Session }

// interface Error {}
// interface PageData {}
// interface PageState {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export const task = sqliteTable('task', {
title: text('title').notNull(),
priority: integer('priority').notNull().default(1)
});

export * from './auth.schema';
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export default defineConfig({
runes: ({ filename }) =>
filename.split(/[/\\]/).includes('node_modules') ? undefined : true
},

adapter: adapter(),
experimental: { explicitEnvironmentVariables: true },
typescript: {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export const task = sqliteTable('task', {
title: text('title').notNull(),
priority: integer('priority').notNull().default(1)
});

export * from './auth.schema';
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
</svelte:head>

{@render children()}

<div style="display:none">
{#each locales as locale (locale)}
<a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export default defineConfig({
runes: ({ filename }) =>
filename.split(/[/\\]/).includes('node_modules') ? undefined : true
},

adapter: adapter(),
preprocess: [mdsvex({ extensions: ['.svx', '.md'] })],
extensions: ['.svelte', '.svx', '.md'],
Expand Down Expand Up @@ -44,6 +43,7 @@ export default defineConfig({
exclude: ['src/lib/server/**']
}
},

{
extends: './vite.config.ts',
test: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
* This is a page
*/
</script>

Nice page! <div>{page.url}</div>
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export async function requireDynamicImportEnv(name: string): Promise<string> {
// @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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Loading