mirror of
https://github.com/LukeHagar/skeleton.git
synced 2025-12-09 12:47:44 +00:00
334 lines
11 KiB
JavaScript
Executable File
334 lines
11 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
import { SkeletonOptions, createSkeleton } from './creator.js';
|
|
import fs from 'fs-extra';
|
|
import mri from 'mri';
|
|
import { bold, cyan, gray, grey, red } from 'kleur/colors';
|
|
import { intro, text, select, multiselect, spinner } from '@clack/prompts';
|
|
import { dist, getHelpText, goodbye, whichPMRuns, checkIfDirSafeToInstall } from './utils.js';
|
|
import { resolve, join, relative, dirname, basename } from 'path';
|
|
import semver from 'semver';
|
|
import fg from 'fast-glob';
|
|
|
|
// Minimum version required for Svelte Kit
|
|
const requiredVersion = '16.14.0';
|
|
|
|
async function main() {
|
|
if (semver.lt(process.version, requiredVersion)) {
|
|
console.error(`You need to be running Node ${requiredVersion} to use Svelte Kit`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const startPath = process.cwd();
|
|
let defaults = new SkeletonOptions();
|
|
|
|
// grab any passed arguments from the command line
|
|
let opts = await parseArgs();
|
|
// need to set some defaults if they are not passed in
|
|
if (!('quiet' in opts)) opts.quiet = false;
|
|
// if no templatedir is provided we have to account for the dist location
|
|
if (!('skeletontemplatedir' in opts)) {
|
|
opts.skeletontemplatedir = resolve(dist('.'), '../templates');
|
|
} else {
|
|
// Resolve can handle multiple absolute paths so passing in a relative or absolute path is fine
|
|
opts.skeletontemplatedir = resolve(process.cwd(), opts.skeletontemplatedir);
|
|
}
|
|
|
|
try {
|
|
checkIfDirSafeToInstall(opts.path);
|
|
} catch (e) {
|
|
console.error(red(`\n${e.message}`));
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!opts.quiet) {
|
|
opts = await askForMissingParams(opts);
|
|
}
|
|
opts = Object.assign(defaults, opts);
|
|
opts.packagemanager = whichPMRuns()?.name;
|
|
|
|
// Now that we have all of the options, lets create it.
|
|
const s = spinner();
|
|
if (!opts.quiet) {
|
|
s.start('Installing');
|
|
}
|
|
|
|
try {
|
|
await createSkeleton(opts);
|
|
} catch (e) {
|
|
console.error(red(`\n${e.message}`));
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!opts.quiet) {
|
|
s.stop('Done installing');
|
|
// And give the user some final information on what to do Next
|
|
|
|
const pm = opts.packagemanager;
|
|
let runString = `${pm} install\n${pm} dev\n`;
|
|
|
|
if (pm == 'npm') {
|
|
runString = 'npm install\nnpm run dev\n';
|
|
}
|
|
let finalInstructions = bold(cyan(`\nDone! You can now:\n\n`));
|
|
if (startPath != opts.path) {
|
|
finalInstructions += bold(cyan(`cd ${relative(startPath, opts.path)}\n`));
|
|
}
|
|
finalInstructions += bold(cyan(runString));
|
|
finalInstructions += grey(`Need some help or found an issue? Visit us on Discord https://discord.gg/EXqV7W8MtY`);
|
|
console.log(finalInstructions);
|
|
}
|
|
process.exit();
|
|
}
|
|
|
|
async function parseArgs() {
|
|
const argv = process.argv.slice(2);
|
|
|
|
// mri will parse argv and expand any shorthand args. Accepted args are the literal props of SkelOptions
|
|
/** @type {SkeletonOptions} */
|
|
const opts = mri(argv, {
|
|
alias: {
|
|
h: 'help',
|
|
p: 'path',
|
|
t: 'skeletontheme',
|
|
m: 'monorepo',
|
|
q: 'quiet',
|
|
v: 'verbose',
|
|
},
|
|
boolean: [
|
|
'help',
|
|
'quiet',
|
|
'monorepo',
|
|
'library',
|
|
'prettier',
|
|
'eslint',
|
|
'playwright',
|
|
'vitest',
|
|
'inspector',
|
|
'codeblocks',
|
|
'popups',
|
|
'forms',
|
|
'typography',
|
|
'mdsvex',
|
|
],
|
|
});
|
|
|
|
// If a user invokes 'create-app blah foo', it falls into the _ catch all list, the best we can do is take the first one and use that as the path
|
|
// if args are passed in incorrectly such as --prettier=0 instead of --prettier=false then a 0 will be added to the _ collection, we check that the
|
|
// first one isn't a bungled arg set to 0
|
|
if (opts._.length && opts._[0] != 0) {
|
|
opts.path = opts._[0];
|
|
}
|
|
// Show help if specified regardless of how many other options are specified, have fun updating the text string in utils.ts :(
|
|
if ('help' in opts) {
|
|
console.log(getHelpText());
|
|
process.exit();
|
|
}
|
|
return opts;
|
|
}
|
|
|
|
/**
|
|
* @param {SkeletonOptions} opts
|
|
*/
|
|
async function askForMissingParams(opts) {
|
|
const { version } = JSON.parse(fs.readFileSync(dist('../package.json'), 'utf-8'));
|
|
|
|
intro(`Create Skeleton App ${gray(`(version ${version})`)}
|
|
|
|
${bold(cyan('Welcome to Skeleton 💀! A UI toolkit for Svelte + Tailwind'))}
|
|
|
|
Problems? Open an issue on ${cyan('https://github.com/skeletonlabs/skeleton/issues')} if none exists already.`);
|
|
|
|
//NOTE: When doing checks here, make sure to test for the presence of the prop, not the prop value as it may be set to false deliberately.
|
|
if (!('path' in opts)) {
|
|
opts.path = await text({
|
|
message: 'Where should we install your project (Enter for current directory)?',
|
|
placeholder: '',
|
|
validate(value) {
|
|
if (value.length === 0) value = '.';
|
|
try {
|
|
checkIfDirSafeToInstall(resolve(process.cwd(), value));
|
|
} catch (e) {
|
|
return e.message;
|
|
}
|
|
},
|
|
});
|
|
goodbye(opts.path);
|
|
}
|
|
if (opts?.path == undefined) opts.path = '.';
|
|
// name to set in package.json
|
|
opts.name = basename(opts.path);
|
|
|
|
// Skeleton Template Selection
|
|
// We have to ask for the template first as it may dictate things like required packages and typechecking
|
|
// skeletontemplatedir is the path to the templates directory, it's either passed in as an arg or set to cwd
|
|
// it may be a single directory with a csa-meta in the root,
|
|
// or it holds multiple directories with csa-meta files in them and skeletontemplate selects that sub folder.
|
|
|
|
let templateFound = false;
|
|
if (opts?.skeletontemplate) {
|
|
// they have asked for a specific template within the folder
|
|
opts.skeletontemplate = resolve(opts.skeletontemplatedir, opts.skeletontemplate, 'csa-meta.json');
|
|
//check that it exists
|
|
if (!fs.existsSync(opts.skeletontemplate)) {
|
|
console.error(`The template ${opts.skeletontemplate} does not exist`);
|
|
process.exit(1);
|
|
}
|
|
templateFound = true;
|
|
}
|
|
// no template specified, so scan the templatedir for csa-meta files
|
|
if (!templateFound) {
|
|
const metaFiles = fg.sync(['**/csa-meta.json'], { cwd: opts.skeletontemplatedir, deep: 2 });
|
|
if (metaFiles.length === 0) {
|
|
console.error(`No templates found in ${opts.skeletontemplatedir}`);
|
|
process.exit(1);
|
|
}
|
|
let parsedChoices = [];
|
|
metaFiles.forEach((meta_file) => {
|
|
const path = join(opts.skeletontemplatedir, meta_file);
|
|
const { position, label, description, enabled } = JSON.parse(fs.readFileSync(path, 'utf8'));
|
|
if (enabled) {
|
|
parsedChoices.push({ position, label, hint: description, value: path });
|
|
}
|
|
});
|
|
parsedChoices.sort((a, b) => a.position - b.position);
|
|
opts.skeletontemplate = await select({
|
|
message: 'Which Skeleton app template?',
|
|
options: parsedChoices,
|
|
});
|
|
goodbye(opts.skeletontemplate);
|
|
}
|
|
// Now that we have the template, lets get the meta data from it and the base path
|
|
opts.meta = JSON.parse(fs.readFileSync(opts.skeletontemplate, 'utf8'));
|
|
if (opts.meta.requiredFeatures) {
|
|
opts.meta.requiredFeatures.forEach((val) => {
|
|
Object.assign(opts, val);
|
|
});
|
|
}
|
|
opts.skeletontemplatedir = dirname(opts.skeletontemplate);
|
|
|
|
// If it's a premium template, wording needs to be change to indicate that there is a theme already built in
|
|
// Skeleton Theme Selection
|
|
if (!('skeletontheme' in opts)) {
|
|
let themeChoices = [
|
|
{ label: 'Skeleton', value: 'skeleton' },
|
|
{ label: 'Wintry', value: 'wintry' },
|
|
{ label: 'Modern', value: 'modern' },
|
|
{ label: 'Hamlindigo', value: 'hamlindigo' },
|
|
{ label: 'Rocket', value: 'rocket' },
|
|
{ label: 'Sahara', value: 'sahara' },
|
|
{ label: 'Gold Nouveau', value: 'gold-nouveau' },
|
|
{ label: 'Vintage', value: 'vintage' },
|
|
{ label: 'Seafoam', value: 'seafoam' },
|
|
{ label: 'Crimson', value: 'crimson' },
|
|
{ label: cyan('Custom'), value: 'custom', hint: 'Will ask for a name next' },
|
|
];
|
|
if (opts.meta.type === 'premium') {
|
|
themeChoices.unshift({ label: 'Use templates built in theme', value: 'builtin' });
|
|
}
|
|
opts.skeletontheme = await multiselect({
|
|
message: 'Select a theme (top most selection will be default):',
|
|
options: themeChoices,
|
|
required: true,
|
|
});
|
|
goodbye(opts.skeletontheme);
|
|
}
|
|
|
|
if (opts.skeletontheme.includes('custom')) {
|
|
let customName = await text({
|
|
message: 'Enter a name for your custom theme:',
|
|
placeholder: 'theme_name',
|
|
validate(value) {
|
|
if (value.length === 0) {
|
|
return 'Please enter a name for your custom theme';
|
|
}
|
|
// regex to check if value can be used as a variable name, it cannot allow hyphens
|
|
if (!/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(value)) {
|
|
return 'Name for theme must be a valid syntax for a Javascript variable name';
|
|
}
|
|
},
|
|
});
|
|
opts.skeletontheme.pop('custom');
|
|
opts.skeletontheme.push({ custom: customName });
|
|
goodbye(customName);
|
|
}
|
|
|
|
// Additional packages to install - these can be influenced by the template selected
|
|
let packages = [
|
|
{ value: 'forms', label: 'Add Tailwind forms?', package: '@tailwindcss/forms', force: false },
|
|
{ value: 'typography', label: 'Add Tailwind typography?', package: '@tailwindcss/typography', force: false },
|
|
{ value: 'codeblocks', label: 'Add CodeBlock (installs highlight.js)?', package: 'highlight.js', force: false },
|
|
{ value: 'popups', label: 'Add Popups (installs floating-ui)?', package: '@floating-ui/dom', force: false },
|
|
// { value: 'mdsvex', label: 'Add Markdown support (installs mdsvex)?', package: 'mdsvex', force: false },
|
|
];
|
|
// Force the packages that are required by the template
|
|
packages.forEach((pkg) => {
|
|
if (opts[pkg.value] != undefined) pkg.force = true;
|
|
});
|
|
// Now we can ask the user about any options that are not forced to be installed
|
|
let optionalPackages = packages.filter((pkg) => !pkg.force);
|
|
// Get list of forced packages to display to the user
|
|
let msg = '';
|
|
packages.forEach((p) => {
|
|
if (p.force) msg += p.package + '\n';
|
|
});
|
|
if (msg.length > 0) {
|
|
msg = `\nThe following packages will be installed because they are required by the template:\n\n${msg}\nWhat other packages would you like to install:`;
|
|
} else {
|
|
msg = `\nWhat other packages would you like to install:`;
|
|
}
|
|
|
|
if (optionalPackages.length > 0) {
|
|
// check which options are set and fill the initialValues array
|
|
const packageChoices = await multiselect({
|
|
message: msg,
|
|
options: optionalPackages,
|
|
required: false,
|
|
});
|
|
goodbye(packageChoices);
|
|
if (Array.isArray(packageChoices)) {
|
|
packageChoices.forEach((value) => (opts[value] = true));
|
|
}
|
|
}
|
|
|
|
if (!('types' in opts)) {
|
|
opts.types = await select({
|
|
message: 'Add type checking with TypeScript?',
|
|
options: [
|
|
{ value: 'typescript', label: 'Yes, using TypeScript syntax' },
|
|
{ value: 'checkjs', label: 'Yes, using JavaScript with JSDoc comments' },
|
|
{ value: null, label: 'No' },
|
|
],
|
|
required: true,
|
|
});
|
|
goodbye(opts.types);
|
|
}
|
|
|
|
// Setup dev oriented packages and settings
|
|
if (
|
|
!['eslint', 'prettier', 'playwright', 'vitest', 'inspector'].every((value) => {
|
|
return Object.keys(opts).includes(value);
|
|
})
|
|
) {
|
|
const optionalInstalls = await multiselect({
|
|
message: 'What would you like setup in your project:',
|
|
// test opts for which values have been provided and prefill them
|
|
initialValues: ['eslint', 'prettier', 'playwright', 'vitest', 'inspector'].filter((value) => {
|
|
return Object.keys(opts).includes(value);
|
|
}),
|
|
options: [
|
|
{ value: 'eslint', label: 'Add ESLint for code linting?' },
|
|
{ value: 'prettier', label: 'Add Prettier for code formatting?' },
|
|
{ value: 'playwright', label: 'Add Playwright for browser testing?' },
|
|
{ value: 'vitest', label: 'Add Vitest for unit testing?' },
|
|
{ value: 'inspector', label: 'Add Svelte Inspector for quick access to your source files from the browser?' },
|
|
],
|
|
required: false,
|
|
});
|
|
goodbye(optionalInstalls);
|
|
optionalInstalls.forEach((value) => (opts[value] = true));
|
|
}
|
|
return opts;
|
|
}
|
|
|
|
main().catch(console.error);
|