Skip to content

Commit d76b2b1

Browse files
Add programmatic interface to create-svelte (sveltejs#3437)
* [feat] add programmatic interface to create-svelte * these should have been snake_cased before Co-authored-by: Rich Harris <[email protected]>
1 parent c653aec commit d76b2b1

File tree

6 files changed

+156
-136
lines changed

6 files changed

+156
-136
lines changed

.changeset/fifty-foxes-tan.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'create-svelte': patch
3+
---
4+
5+
Add a programmatic interface to create-svelte

packages/create-svelte/bin.js

Lines changed: 3 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
#!/usr/bin/env node
22
import fs from 'fs';
33
import path from 'path';
4-
import { fileURLToPath } from 'url';
54
import { bold, cyan, gray, green, red } from 'kleur/colors';
65
import prompts from 'prompts';
7-
import { mkdirp, copy } from './utils.js';
6+
import { create } from './index.js';
7+
import { dist } from './utils.js';
88

99
// prettier-ignore
1010
const disclaimer = `
@@ -36,8 +36,6 @@ async function main() {
3636
process.exit(1);
3737
}
3838
}
39-
} else {
40-
mkdirp(cwd);
4139
}
4240

4341
const options = /** @type {import('./types/internal').Options} */ (
@@ -83,10 +81,7 @@ async function main() {
8381
])
8482
);
8583

86-
const name = path.basename(path.resolve(cwd));
87-
88-
write_template_files(options.template, options.typescript, name, cwd);
89-
write_common_files(cwd, options, name);
84+
await create(cwd, options);
9085

9186
console.log(bold(green('\nYour project is ready!')));
9287

@@ -126,131 +121,4 @@ async function main() {
126121
console.log(`\nStuck? Visit us at ${cyan('https://svelte.dev/chat')}\n`);
127122
}
128123

129-
/**
130-
* @param {string} template
131-
* @param {boolean} typescript
132-
* @param {string} name
133-
* @param {string} cwd
134-
*/
135-
function write_template_files(template, typescript, name, cwd) {
136-
const dir = dist(`templates/${template}`);
137-
copy(`${dir}/assets`, cwd, (name) => name.replace('DOT-', '.'));
138-
copy(`${dir}/package.json`, `${cwd}/package.json`);
139-
140-
const manifest = `${dir}/files.${typescript ? 'ts' : 'js'}.json`;
141-
const files = /** @type {import('./types/internal').File[]} */ (
142-
JSON.parse(fs.readFileSync(manifest, 'utf-8'))
143-
);
144-
145-
files.forEach((file) => {
146-
const dest = path.join(cwd, file.name);
147-
mkdirp(path.dirname(dest));
148-
149-
fs.writeFileSync(dest, file.contents.replace(/~TODO~/g, name));
150-
});
151-
}
152-
153-
/**
154-
*
155-
* @param {string} cwd
156-
* @param {import('./types/internal').Options} options
157-
* @param {string} name
158-
*/
159-
function write_common_files(cwd, options, name) {
160-
const shared = dist('shared.json');
161-
const { files } = /** @type {import('./types/internal').Common} */ (
162-
JSON.parse(fs.readFileSync(shared, 'utf-8'))
163-
);
164-
165-
const pkg_file = path.join(cwd, 'package.json');
166-
const pkg = /** @type {any} */ (JSON.parse(fs.readFileSync(pkg_file, 'utf-8')));
167-
168-
files.forEach((file) => {
169-
const include = file.include.every((condition) => matchesCondition(condition, options));
170-
const exclude = file.exclude.some((condition) => matchesCondition(condition, options));
171-
172-
if (exclude || !include) return;
173-
174-
if (file.name === 'package.json') {
175-
const new_pkg = JSON.parse(file.contents);
176-
merge(pkg, new_pkg);
177-
} else {
178-
const dest = path.join(cwd, file.name);
179-
mkdirp(path.dirname(dest));
180-
fs.writeFileSync(dest, file.contents);
181-
}
182-
});
183-
184-
pkg.dependencies = sort_keys(pkg.dependencies);
185-
pkg.devDependencies = sort_keys(pkg.devDependencies);
186-
pkg.name = toValidPackageName(name);
187-
188-
fs.writeFileSync(pkg_file, JSON.stringify(pkg, null, ' '));
189-
}
190-
191-
/**
192-
* @param {import('./types/internal').Condition} condition
193-
* @param {import('./types/internal').Options} options
194-
* @returns {boolean}
195-
*/
196-
function matchesCondition(condition, options) {
197-
return condition === 'default' || condition === 'skeleton'
198-
? options.template === condition
199-
: options[condition];
200-
}
201-
202-
/**
203-
* @param {any} target
204-
* @param {any} source
205-
*/
206-
function merge(target, source) {
207-
for (const key in source) {
208-
if (key in target) {
209-
const target_value = target[key];
210-
const source_value = source[key];
211-
212-
if (
213-
typeof source_value !== typeof target_value ||
214-
Array.isArray(source_value) !== Array.isArray(target_value)
215-
) {
216-
throw new Error('Mismatched values');
217-
}
218-
219-
merge(target_value, source_value);
220-
} else {
221-
target[key] = source[key];
222-
}
223-
}
224-
}
225-
226-
/** @param {Record<string, any>} obj */
227-
function sort_keys(obj) {
228-
if (!obj) return;
229-
230-
/** @type {Record<string, any>} */
231-
const sorted = {};
232-
Object.keys(obj)
233-
.sort()
234-
.forEach((key) => {
235-
sorted[key] = obj[key];
236-
});
237-
238-
return sorted;
239-
}
240-
241-
/** @param {string} path */
242-
function dist(path) {
243-
return fileURLToPath(new URL(`./dist/${path}`, import.meta.url).href);
244-
}
245-
246-
/** @param {string} name */
247-
function toValidPackageName(name) {
248-
return name
249-
.trim()
250-
.toLowerCase()
251-
.replace(/\s+/g, '-')
252-
.replace(/^[._]/, '')
253-
.replace(/[^a-z0-9~.-]+/g, '-');
254-
}
255-
256124
main();

packages/create-svelte/index.js

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import { mkdirp, copy, dist } from './utils.js';
4+
5+
/**
6+
* Create a new SvelteKit project.
7+
*
8+
* @param {string} cwd - Path to the directory to create
9+
* @param {import('./types/internal').Options} options
10+
*/
11+
export async function create(cwd, options) {
12+
mkdirp(cwd);
13+
14+
const name = path.basename(path.resolve(cwd));
15+
16+
write_template_files(options.template, options.typescript, name, cwd);
17+
write_common_files(cwd, options, name);
18+
}
19+
20+
/**
21+
* @param {string} template
22+
* @param {boolean} typescript
23+
* @param {string} name
24+
* @param {string} cwd
25+
*/
26+
function write_template_files(template, typescript, name, cwd) {
27+
const dir = dist(`templates/${template}`);
28+
copy(`${dir}/assets`, cwd, (name) => name.replace('DOT-', '.'));
29+
copy(`${dir}/package.json`, `${cwd}/package.json`);
30+
31+
const manifest = `${dir}/files.${typescript ? 'ts' : 'js'}.json`;
32+
const files = /** @type {import('./types/internal').File[]} */ (
33+
JSON.parse(fs.readFileSync(manifest, 'utf-8'))
34+
);
35+
36+
files.forEach((file) => {
37+
const dest = path.join(cwd, file.name);
38+
mkdirp(path.dirname(dest));
39+
40+
fs.writeFileSync(dest, file.contents.replace(/~TODO~/g, name));
41+
});
42+
}
43+
44+
/**
45+
*
46+
* @param {string} cwd
47+
* @param {import('./types/internal').Options} options
48+
* @param {string} name
49+
*/
50+
function write_common_files(cwd, options, name) {
51+
const shared = dist('shared.json');
52+
const { files } = /** @type {import('./types/internal').Common} */ (
53+
JSON.parse(fs.readFileSync(shared, 'utf-8'))
54+
);
55+
56+
const pkg_file = path.join(cwd, 'package.json');
57+
const pkg = /** @type {any} */ (JSON.parse(fs.readFileSync(pkg_file, 'utf-8')));
58+
59+
files.forEach((file) => {
60+
const include = file.include.every((condition) => matches_condition(condition, options));
61+
const exclude = file.exclude.some((condition) => matches_condition(condition, options));
62+
63+
if (exclude || !include) return;
64+
65+
if (file.name === 'package.json') {
66+
const new_pkg = JSON.parse(file.contents);
67+
merge(pkg, new_pkg);
68+
} else {
69+
const dest = path.join(cwd, file.name);
70+
mkdirp(path.dirname(dest));
71+
fs.writeFileSync(dest, file.contents);
72+
}
73+
});
74+
75+
pkg.dependencies = sort_keys(pkg.dependencies);
76+
pkg.devDependencies = sort_keys(pkg.devDependencies);
77+
pkg.name = to_valid_package_name(name);
78+
79+
fs.writeFileSync(pkg_file, JSON.stringify(pkg, null, ' '));
80+
}
81+
82+
/**
83+
* @param {import('./types/internal').Condition} condition
84+
* @param {import('./types/internal').Options} options
85+
* @returns {boolean}
86+
*/
87+
function matches_condition(condition, options) {
88+
return condition === 'default' || condition === 'skeleton'
89+
? options.template === condition
90+
: options[condition];
91+
}
92+
93+
/**
94+
* @param {any} target
95+
* @param {any} source
96+
*/
97+
function merge(target, source) {
98+
for (const key in source) {
99+
if (key in target) {
100+
const target_value = target[key];
101+
const source_value = source[key];
102+
103+
if (
104+
typeof source_value !== typeof target_value ||
105+
Array.isArray(source_value) !== Array.isArray(target_value)
106+
) {
107+
throw new Error('Mismatched values');
108+
}
109+
110+
merge(target_value, source_value);
111+
} else {
112+
target[key] = source[key];
113+
}
114+
}
115+
}
116+
117+
/** @param {Record<string, any>} obj */
118+
function sort_keys(obj) {
119+
if (!obj) return;
120+
121+
/** @type {Record<string, any>} */
122+
const sorted = {};
123+
Object.keys(obj)
124+
.sort()
125+
.forEach((key) => {
126+
sorted[key] = obj[key];
127+
});
128+
129+
return sorted;
130+
}
131+
132+
/** @param {string} name */
133+
function to_valid_package_name(name) {
134+
return name
135+
.trim()
136+
.toLowerCase()
137+
.replace(/\s+/g, '-')
138+
.replace(/^[._]/, '')
139+
.replace(/[^a-z0-9~.-]+/g, '-');
140+
}

packages/create-svelte/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"license": "MIT",
1010
"homepage": "https://kit.svelte.dev",
1111
"bin": "./bin.js",
12+
"main": "./index.js",
1213
"dependencies": {
1314
"kleur": "^4.1.4",
1415
"prompts": "^2.4.2"

packages/create-svelte/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@
99
"moduleResolution": "node",
1010
"allowSyntheticDefaultImports": true
1111
},
12-
"include": ["scripts/**/*", "bin.js", "utils.js"]
12+
"include": ["scripts/**/*", "index.js", "bin.js", "utils.js"]
1313
}

packages/create-svelte/utils.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from 'fs';
22
import path from 'path';
3+
import { fileURLToPath } from 'url';
34

45
/** @param {string} dir */
56
export function mkdirp(dir) {
@@ -43,3 +44,8 @@ export function copy(from, to, rename = identity) {
4344
fs.copyFileSync(from, to);
4445
}
4546
}
47+
48+
/** @param {string} path */
49+
export function dist(path) {
50+
return fileURLToPath(new URL(`./dist/${path}`, import.meta.url).href);
51+
}

0 commit comments

Comments
 (0)