Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add programmatic interface to create-svelte #3437

Merged
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/fifty-foxes-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'create-svelte': patch
---

Add a programmatic interface to create-svelte
138 changes: 3 additions & 135 deletions packages/create-svelte/bin.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#!/usr/bin/env node
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { bold, cyan, gray, green, red } from 'kleur/colors';
import prompts from 'prompts';
import { mkdirp, copy } from './utils.js';
import { create } from './index.js';
import { dist } from './utils.js';

// prettier-ignore
const disclaimer = `
Expand Down Expand Up @@ -36,8 +36,6 @@ async function main() {
process.exit(1);
}
}
} else {
mkdirp(cwd);
}

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

const name = path.basename(path.resolve(cwd));

write_template_files(options.template, options.typescript, name, cwd);
write_common_files(cwd, options, name);
await create(cwd, options);

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

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

/**
* @param {string} template
* @param {boolean} typescript
* @param {string} name
* @param {string} cwd
*/
function write_template_files(template, typescript, name, cwd) {
const dir = dist(`templates/${template}`);
copy(`${dir}/assets`, cwd, (name) => name.replace('DOT-', '.'));
copy(`${dir}/package.json`, `${cwd}/package.json`);

const manifest = `${dir}/files.${typescript ? 'ts' : 'js'}.json`;
const files = /** @type {import('./types/internal').File[]} */ (
JSON.parse(fs.readFileSync(manifest, 'utf-8'))
);

files.forEach((file) => {
const dest = path.join(cwd, file.name);
mkdirp(path.dirname(dest));

fs.writeFileSync(dest, file.contents.replace(/~TODO~/g, name));
});
}

/**
*
* @param {string} cwd
* @param {import('./types/internal').Options} options
* @param {string} name
*/
function write_common_files(cwd, options, name) {
const shared = dist('shared.json');
const { files } = /** @type {import('./types/internal').Common} */ (
JSON.parse(fs.readFileSync(shared, 'utf-8'))
);

const pkg_file = path.join(cwd, 'package.json');
const pkg = /** @type {any} */ (JSON.parse(fs.readFileSync(pkg_file, 'utf-8')));

files.forEach((file) => {
const include = file.include.every((condition) => matchesCondition(condition, options));
const exclude = file.exclude.some((condition) => matchesCondition(condition, options));

if (exclude || !include) return;

if (file.name === 'package.json') {
const new_pkg = JSON.parse(file.contents);
merge(pkg, new_pkg);
} else {
const dest = path.join(cwd, file.name);
mkdirp(path.dirname(dest));
fs.writeFileSync(dest, file.contents);
}
});

pkg.dependencies = sort_keys(pkg.dependencies);
pkg.devDependencies = sort_keys(pkg.devDependencies);
pkg.name = toValidPackageName(name);

fs.writeFileSync(pkg_file, JSON.stringify(pkg, null, ' '));
}

/**
* @param {import('./types/internal').Condition} condition
* @param {import('./types/internal').Options} options
* @returns {boolean}
*/
function matchesCondition(condition, options) {
return condition === 'default' || condition === 'skeleton'
? options.template === condition
: options[condition];
}

/**
* @param {any} target
* @param {any} source
*/
function merge(target, source) {
for (const key in source) {
if (key in target) {
const target_value = target[key];
const source_value = source[key];

if (
typeof source_value !== typeof target_value ||
Array.isArray(source_value) !== Array.isArray(target_value)
) {
throw new Error('Mismatched values');
}

merge(target_value, source_value);
} else {
target[key] = source[key];
}
}
}

/** @param {Record<string, any>} obj */
function sort_keys(obj) {
if (!obj) return;

/** @type {Record<string, any>} */
const sorted = {};
Object.keys(obj)
.sort()
.forEach((key) => {
sorted[key] = obj[key];
});

return sorted;
}

/** @param {string} path */
function dist(path) {
return fileURLToPath(new URL(`./dist/${path}`, import.meta.url).href);
}

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

main();
140 changes: 140 additions & 0 deletions packages/create-svelte/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import fs from 'fs';
import path from 'path';
import { mkdirp, copy, dist } from './utils.js';

/**
* Create a new SvelteKit project.
*
* @param {string} cwd - Path to the directory to create
* @param {import('./types/internal').Options} options
*/
export async function create(cwd, options) {
mkdirp(cwd);

const name = path.basename(path.resolve(cwd));

write_template_files(options.template, options.typescript, name, cwd);
write_common_files(cwd, options, name);
}

/**
* @param {string} template
* @param {boolean} typescript
* @param {string} name
* @param {string} cwd
*/
function write_template_files(template, typescript, name, cwd) {
const dir = dist(`templates/${template}`);
copy(`${dir}/assets`, cwd, (name) => name.replace('DOT-', '.'));
copy(`${dir}/package.json`, `${cwd}/package.json`);

const manifest = `${dir}/files.${typescript ? 'ts' : 'js'}.json`;
const files = /** @type {import('./types/internal').File[]} */ (
JSON.parse(fs.readFileSync(manifest, 'utf-8'))
);

files.forEach((file) => {
const dest = path.join(cwd, file.name);
mkdirp(path.dirname(dest));

fs.writeFileSync(dest, file.contents.replace(/~TODO~/g, name));
});
}

/**
*
* @param {string} cwd
* @param {import('./types/internal').Options} options
* @param {string} name
*/
function write_common_files(cwd, options, name) {
const shared = dist('shared.json');
const { files } = /** @type {import('./types/internal').Common} */ (
JSON.parse(fs.readFileSync(shared, 'utf-8'))
);

const pkg_file = path.join(cwd, 'package.json');
const pkg = /** @type {any} */ (JSON.parse(fs.readFileSync(pkg_file, 'utf-8')));

files.forEach((file) => {
const include = file.include.every((condition) => matches_condition(condition, options));
const exclude = file.exclude.some((condition) => matches_condition(condition, options));

if (exclude || !include) return;

if (file.name === 'package.json') {
const new_pkg = JSON.parse(file.contents);
merge(pkg, new_pkg);
} else {
const dest = path.join(cwd, file.name);
mkdirp(path.dirname(dest));
fs.writeFileSync(dest, file.contents);
}
});

pkg.dependencies = sort_keys(pkg.dependencies);
pkg.devDependencies = sort_keys(pkg.devDependencies);
pkg.name = to_valid_package_name(name);

fs.writeFileSync(pkg_file, JSON.stringify(pkg, null, ' '));
}

/**
* @param {import('./types/internal').Condition} condition
* @param {import('./types/internal').Options} options
* @returns {boolean}
*/
function matches_condition(condition, options) {
return condition === 'default' || condition === 'skeleton'
? options.template === condition
: options[condition];
}

/**
* @param {any} target
* @param {any} source
*/
function merge(target, source) {
for (const key in source) {
if (key in target) {
const target_value = target[key];
const source_value = source[key];

if (
typeof source_value !== typeof target_value ||
Array.isArray(source_value) !== Array.isArray(target_value)
) {
throw new Error('Mismatched values');
}

merge(target_value, source_value);
} else {
target[key] = source[key];
}
}
}

/** @param {Record<string, any>} obj */
function sort_keys(obj) {
if (!obj) return;

/** @type {Record<string, any>} */
const sorted = {};
Object.keys(obj)
.sort()
.forEach((key) => {
sorted[key] = obj[key];
});

return sorted;
}

/** @param {string} name */
function to_valid_package_name(name) {
return name
.trim()
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/^[._]/, '')
.replace(/[^a-z0-9~.-]+/g, '-');
}
1 change: 1 addition & 0 deletions packages/create-svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"license": "MIT",
"homepage": "https://kit.svelte.dev",
"bin": "./bin.js",
"main": "./index.js",
"dependencies": {
"kleur": "^4.1.4",
"prompts": "^2.4.2"
Expand Down
2 changes: 1 addition & 1 deletion packages/create-svelte/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
"moduleResolution": "node",
"allowSyntheticDefaultImports": true
},
"include": ["scripts/**/*", "bin.js", "utils.js"]
"include": ["scripts/**/*", "index.js", "bin.js", "utils.js"]
}
6 changes: 6 additions & 0 deletions packages/create-svelte/utils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

/** @param {string} dir */
export function mkdirp(dir) {
Expand Down Expand Up @@ -43,3 +44,8 @@ export function copy(from, to, rename = identity) {
fs.copyFileSync(from, to);
}
}

/** @param {string} path */
export function dist(path) {
return fileURLToPath(new URL(`./dist/${path}`, import.meta.url).href);
}