Skip to content

Commit fc8fc8c

Browse files
fix(adapter): sanitize wrangler project name for Cloudflare compatibility (#861)
Co-authored-by: jycouet <[email protected]>
1 parent 6f8904c commit fc8fc8c

File tree

5 files changed

+114
-11
lines changed

5 files changed

+114
-11
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'sv': patch
3+
---
4+
5+
fix(adapter-cloudflare): sanitize wrangler project name to comply with Cloudflare naming requirements

packages/sv/lib/addons/sveltekit-adapter/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { defineAddon, defineAddonOptions } from '../../core/index.ts';
22
import { exports, functions, imports, object, type AstTypes } from '../../core/tooling/js/index.ts';
33
import { parseJson, parseScript, parseToml } from '../../core/tooling/parsers.ts';
44
import { fileExists, readFile } from '../../cli/add/utils.ts';
5+
import { sanitizeName } from '../../core/sanitize.ts';
56
import { resolveCommand } from 'package-manager-detector';
67
import * as js from '../../core/tooling/js/index.ts';
78

@@ -139,7 +140,7 @@ export default defineAddon({
139140

140141
if (!data.name) {
141142
const pkg = parseJson(readFile(cwd, files.package));
142-
data.name = pkg.data.name;
143+
data.name = sanitizeName(pkg.data.name, 'wrangler');
143144
}
144145

145146
data.compatibility_date ??= new Date().toISOString().split('T')[0];

packages/sv/lib/core/sanitize.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* @param name - The name to sanitize.
3+
* @param style - The sanitization style.
4+
* - `package` for package.json
5+
* - `wrangler` for Cloudflare Wrangler compatibility
6+
* @returns The sanitized name.
7+
*/
8+
export function sanitizeName(name: string, style: 'package' | 'wrangler'): string {
9+
let sanitized = name.trim().toLowerCase().replace(/\s+/g, '-');
10+
if (style === 'package') {
11+
const hasLeadingAt = sanitized.startsWith('@');
12+
sanitized = sanitized
13+
.replace(/\s+/g, '-')
14+
.replace(/^[._]/, '')
15+
.replace(/[^a-z0-9~./-]+/g, '-');
16+
if (hasLeadingAt) sanitized = '@' + sanitized.slice(1);
17+
} else if (style === 'wrangler') {
18+
sanitized = sanitized
19+
.replace(/[^a-z0-9-]/g, '-')
20+
.replace(/-+/g, '-')
21+
.slice(0, 63)
22+
.replace(/^-|-$/g, '');
23+
} else {
24+
throw new Error(`Invalid kind: ${style}`);
25+
}
26+
return sanitized || 'undefined-sv-name';
27+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { sanitizeName } from '../sanitize.ts';
3+
4+
const testCases: Array<{ input: string; expected: string; expectedPackage?: string }> = [
5+
// Basic cases
6+
{ input: 'my-project', expected: 'my-project' },
7+
{ input: 'myproject', expected: 'myproject' },
8+
9+
// Dots
10+
{ input: 'sub.example.com', expected: 'sub-example-com', expectedPackage: 'sub.example.com' },
11+
{ input: 'my.cool.app', expected: 'my-cool-app', expectedPackage: 'my.cool.app' },
12+
13+
// Underscores
14+
{ input: 'my_project_name', expected: 'my-project-name' },
15+
16+
// Mixed cases
17+
{ input: 'My_Project.Name', expected: 'my-project-name', expectedPackage: 'my-project.name' },
18+
{ input: 'MyAwesomeApp', expected: 'myawesomeapp' },
19+
20+
// Special characters
21+
{ input: '@scope/package', expected: 'scope-package', expectedPackage: '@scope/package' },
22+
{ input: 'hello@world!test', expected: 'hello-world-test' },
23+
24+
// Multiple consecutive invalid chars
25+
{ input: 'my..project__name', expected: 'my-project-name', expectedPackage: 'my..project-name' },
26+
27+
// Leading/trailing invalid chars
28+
{ input: '.my-project.', expected: 'my-project', expectedPackage: 'my-project.' },
29+
{ input: '---test---', expected: 'test', expectedPackage: '---test---' },
30+
31+
// Numbers
32+
{ input: 'project123', expected: 'project123' },
33+
{ input: '123project', expected: '123project' },
34+
35+
// Empty/invalid fallback
36+
{ input: '___', expected: 'undefined-sv-name', expectedPackage: '-' },
37+
{ input: '!@#$%', expected: 'undefined-sv-name', expectedPackage: '-' },
38+
{ input: '', expected: 'undefined-sv-name' },
39+
40+
// Length limit (63 chars max)
41+
{ input: 'a'.repeat(70), expected: 'a'.repeat(63), expectedPackage: 'a'.repeat(70) },
42+
{
43+
input: 'my-very-long-project-name-that-exceeds-the-limit-of-63-characters-allowed',
44+
expected: 'my-very-long-project-name-that-exceeds-the-limit-of-63-characte',
45+
expectedPackage: 'my-very-long-project-name-that-exceeds-the-limit-of-63-characters-allowed'
46+
},
47+
48+
// Truncation trap: slice leaves trailing dash
49+
{
50+
input: 'a'.repeat(62) + '-b',
51+
expected: 'a'.repeat(62),
52+
expectedPackage: 'a'.repeat(62) + '-b'
53+
},
54+
55+
// Spaces
56+
{ input: 'my cool project', expected: 'my-cool-project' },
57+
{ input: ' spaced out ', expected: 'spaced-out' },
58+
59+
// Exact boundary (off-by-one check)
60+
{ input: 'a'.repeat(63), expected: 'a'.repeat(63) },
61+
62+
// Unicode / accents / emojis (replaced with dashes)
63+
{ input: 'piñata', expected: 'pi-ata' },
64+
{ input: 'café', expected: 'caf', expectedPackage: 'caf-' },
65+
{ input: 'cool 🚀 app', expected: 'cool-app', expectedPackage: 'cool---app' }
66+
];
67+
68+
describe('sanitizeName wrangler', () => {
69+
it.each(testCases)('sanitizes $input to $expected', ({ input, expected }) => {
70+
expect(sanitizeName(input, 'wrangler')).toBe(expected);
71+
});
72+
});
73+
74+
describe('sanitizeName package', () => {
75+
it.each(testCases)('sanitizes $input to $expected', ({ input, expected, expectedPackage }) => {
76+
expect(sanitizeName(input, 'package')).toBe(expectedPackage ?? expected);
77+
});
78+
});

packages/sv/lib/create/index.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from 'node:fs';
22
import path from 'node:path';
33
import { mkdirp, copy, dist, getSharedFiles } from './utils.ts';
4+
import { sanitizeName } from '../core/sanitize.ts';
45

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

9091
pkg.dependencies = sort_keys(pkg.dependencies);
9192
pkg.devDependencies = sort_keys(pkg.devDependencies);
92-
pkg.name = to_valid_package_name(name);
93+
pkg.name = sanitizeName(name, 'package');
9394

9495
fs.writeFileSync(pkg_file, JSON.stringify(pkg, null, '\t') + '\n');
9596
}
@@ -158,12 +159,3 @@ function sort_files(files: Common['files']) {
158159
return same || different ? 0 : f1_more_generic ? -1 : 1;
159160
});
160161
}
161-
162-
function to_valid_package_name(name: string) {
163-
return name
164-
.trim()
165-
.toLowerCase()
166-
.replace(/\s+/g, '-')
167-
.replace(/^[._]/, '')
168-
.replace(/[^a-z0-9~.-]+/g, '-');
169-
}

0 commit comments

Comments
 (0)