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
5 changes: 5 additions & 0 deletions .changeset/sanitize-wrangler-name.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'sv': patch
---

fix(adapter-cloudflare): sanitize wrangler project name to comply with Cloudflare naming requirements
3 changes: 2 additions & 1 deletion packages/sv/lib/addons/sveltekit-adapter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { defineAddon, defineAddonOptions } from '../../core/index.ts';
import { exports, functions, imports, object, type AstTypes } from '../../core/tooling/js/index.ts';
import { parseJson, parseScript, parseToml } from '../../core/tooling/parsers.ts';
import { fileExists, readFile } from '../../cli/add/utils.ts';
import { sanitizeName } from '../../core/sanitize.ts';
import { resolveCommand } from 'package-manager-detector';
import * as js from '../../core/tooling/js/index.ts';

Expand Down Expand Up @@ -139,7 +140,7 @@ export default defineAddon({

if (!data.name) {
const pkg = parseJson(readFile(cwd, files.package));
data.name = pkg.data.name;
data.name = sanitizeName(pkg.data.name, 'wrangler');
}

data.compatibility_date ??= new Date().toISOString().split('T')[0];
Expand Down
27 changes: 27 additions & 0 deletions packages/sv/lib/core/sanitize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* @param name - The name to sanitize.
* @param style - The sanitization style.
* - `package` for package.json
* - `wrangler` for Cloudflare Wrangler compatibility
* @returns The sanitized name.
*/
export function sanitizeName(name: string, style: 'package' | 'wrangler'): string {
let sanitized = name.trim().toLowerCase().replace(/\s+/g, '-');
if (style === 'package') {
const hasLeadingAt = sanitized.startsWith('@');
sanitized = sanitized
.replace(/\s+/g, '-')
.replace(/^[._]/, '')
.replace(/[^a-z0-9~./-]+/g, '-');
if (hasLeadingAt) sanitized = '@' + sanitized.slice(1);
} else if (style === 'wrangler') {
sanitized = sanitized
.replace(/[^a-z0-9-]/g, '-')
.replace(/-+/g, '-')
.slice(0, 63)
.replace(/^-|-$/g, '');
} else {
throw new Error(`Invalid kind: ${style}`);
}
return sanitized || 'undefined-sv-name';
}
78 changes: 78 additions & 0 deletions packages/sv/lib/core/tests/sanitize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { describe, it, expect } from 'vitest';
import { sanitizeName } from '../sanitize.ts';

const testCases: Array<{ input: string; expected: string; expectedPackage?: string }> = [
// Basic cases
{ input: 'my-project', expected: 'my-project' },
{ input: 'myproject', expected: 'myproject' },

// Dots
{ input: 'sub.example.com', expected: 'sub-example-com', expectedPackage: 'sub.example.com' },
{ input: 'my.cool.app', expected: 'my-cool-app', expectedPackage: 'my.cool.app' },

// Underscores
{ input: 'my_project_name', expected: 'my-project-name' },

// Mixed cases
{ input: 'My_Project.Name', expected: 'my-project-name', expectedPackage: 'my-project.name' },
{ input: 'MyAwesomeApp', expected: 'myawesomeapp' },

// Special characters
{ input: '@scope/package', expected: 'scope-package', expectedPackage: '@scope/package' },
{ input: 'hello@world!test', expected: 'hello-world-test' },

// Multiple consecutive invalid chars
{ input: 'my..project__name', expected: 'my-project-name', expectedPackage: 'my..project-name' },

// Leading/trailing invalid chars
{ input: '.my-project.', expected: 'my-project', expectedPackage: 'my-project.' },
{ input: '---test---', expected: 'test', expectedPackage: '---test---' },

// Numbers
{ input: 'project123', expected: 'project123' },
{ input: '123project', expected: '123project' },

// Empty/invalid fallback
{ input: '___', expected: 'undefined-sv-name', expectedPackage: '-' },
{ input: '!@#$%', expected: 'undefined-sv-name', expectedPackage: '-' },
{ input: '', expected: 'undefined-sv-name' },

// Length limit (63 chars max)
{ input: 'a'.repeat(70), expected: 'a'.repeat(63), expectedPackage: 'a'.repeat(70) },
{
input: 'my-very-long-project-name-that-exceeds-the-limit-of-63-characters-allowed',
expected: 'my-very-long-project-name-that-exceeds-the-limit-of-63-characte',
expectedPackage: 'my-very-long-project-name-that-exceeds-the-limit-of-63-characters-allowed'
},

// Truncation trap: slice leaves trailing dash
{
input: 'a'.repeat(62) + '-b',
expected: 'a'.repeat(62),
expectedPackage: 'a'.repeat(62) + '-b'
},

// Spaces
{ input: 'my cool project', expected: 'my-cool-project' },
{ input: ' spaced out ', expected: 'spaced-out' },

// Exact boundary (off-by-one check)
{ input: 'a'.repeat(63), expected: 'a'.repeat(63) },

// Unicode / accents / emojis (replaced with dashes)
{ input: 'piñata', expected: 'pi-ata' },
{ input: 'café', expected: 'caf', expectedPackage: 'caf-' },
{ input: 'cool 🚀 app', expected: 'cool-app', expectedPackage: 'cool---app' }
];

describe('sanitizeName wrangler', () => {
it.each(testCases)('sanitizes $input to $expected', ({ input, expected }) => {
expect(sanitizeName(input, 'wrangler')).toBe(expected);
});
});

describe('sanitizeName package', () => {
it.each(testCases)('sanitizes $input to $expected', ({ input, expected, expectedPackage }) => {
expect(sanitizeName(input, 'package')).toBe(expectedPackage ?? expected);
});
});
12 changes: 2 additions & 10 deletions packages/sv/lib/create/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { mkdirp, copy, dist, getSharedFiles } from './utils.ts';
import { sanitizeName } from '../core/sanitize.ts';

export type TemplateType = (typeof templateTypes)[number];
export type LanguageType = (typeof languageTypes)[number];
Expand Down Expand Up @@ -89,7 +90,7 @@ function write_common_files(cwd: string, options: Options, name: string) {

pkg.dependencies = sort_keys(pkg.dependencies);
pkg.devDependencies = sort_keys(pkg.devDependencies);
pkg.name = to_valid_package_name(name);
pkg.name = sanitizeName(name, 'package');

fs.writeFileSync(pkg_file, JSON.stringify(pkg, null, '\t') + '\n');
}
Expand Down Expand Up @@ -158,12 +159,3 @@ function sort_files(files: Common['files']) {
return same || different ? 0 : f1_more_generic ? -1 : 1;
});
}

function to_valid_package_name(name: string) {
return name
.trim()
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/^[._]/, '')
.replace(/[^a-z0-9~.-]+/g, '-');
}
Loading