hydrogen/packages/create-hydrogen-app/index.js
2021-11-04 15:22:30 -07:00

209 lines
5.4 KiB
JavaScript
Executable file

#!/usr/bin/env node
// @ts-check
// Inspired by and borrowed from https://github.com/vitejs/vite/blob/main/packages/create-app/index.js
const fs = require('fs');
const path = require('path');
const argv = require('minimist')(process.argv.slice(2));
const {prompt} = require('enquirer');
const {yellow} = require('kolorist');
const {copy} = require('./scripts/utils.js');
const cwd = process.cwd();
const TEMPLATES = ['hydrogen'];
const renameFiles = {
_gitignore: '.gitignore',
};
async function init() {
let targetDir = argv._[0];
if (!targetDir) {
/**
* @type {{ projectName: string }}
*/
const {projectName} = await prompt({
type: 'input',
name: 'projectName',
message: `Project name:`,
initial: 'hydrogen-app',
});
targetDir = projectName;
}
const packageName = await getValidPackageName(targetDir);
const root = path.join(cwd, targetDir);
console.log(`\nScaffolding Hydrogen app in ${root}...`);
if (!fs.existsSync(root)) {
fs.mkdirSync(root, {recursive: true});
} else {
const existing = fs.readdirSync(root);
if (existing.length) {
/**
* @type {{ yes: boolean }}
*/
const {yes} = await prompt({
type: 'confirm',
name: 'yes',
initial: 'Y',
message:
`Target directory ${targetDir} is not empty.\n` +
`Remove existing files and continue?`,
});
if (yes) {
emptyDir(root);
} else {
return;
}
}
}
const firstAndOnlyTemplate =
TEMPLATES && TEMPLATES.length && TEMPLATES.length === 1 && TEMPLATES[0];
// Determine template
let template = argv.t || argv.template || firstAndOnlyTemplate;
let message = 'Select a template:';
let isValidTemplate = false;
// --template expects a value
if (typeof template === 'string') {
isValidTemplate = TEMPLATES.includes(template);
message = `${template} isn't a valid template. Please choose from below:`;
}
if (!template || !isValidTemplate) {
/**
* @type {{ t: string }}
*/
const {t} = await prompt({
type: 'select',
name: 't',
message,
choices: TEMPLATES,
});
template = t;
}
const templateDir = path.join(__dirname, `template-${template}`);
const write = (file, content) => {
const targetPath = renameFiles[file]
? path.join(root, renameFiles[file])
: path.join(root, file);
if (content) {
fs.writeFileSync(targetPath, content);
} else {
copy(path.join(templateDir, file), targetPath);
}
};
const files = fs.readdirSync(templateDir);
const skipFiles = ['package.json', 'node_modules', 'dist'];
for (const file of files.filter((f) => !skipFiles.includes(f))) {
write(file);
}
const pkg = require(path.join(templateDir, `package.json`));
pkg.name = packageName;
/**
* When the user is running a LOCAL version of hydrogen external from the
* monorepo, they expect to use the local version of the library instead
* of the registry version. We need to use a file reference here because
* yarn fails to link scoped packages.
**/
if (process.env.LOCAL) {
pkg.dependencies['@shopify/hydrogen'] =
'file:../../Shopify/hydrogen/packages/hydrogen';
}
/**
* Rewrite some scripts to strip out custom environment variables
* we add for use in the monorepo (LOCAL_DEV).
*/
for (const scriptName of ['dev']) {
const match = pkg.scripts[scriptName].match(/(vite( .*)?)$/);
if (match) {
pkg.scripts[scriptName] = match[0];
}
}
write('package.json', JSON.stringify(pkg, null, 2));
const pkgManager = /yarn/.test(process.env.npm_execpath) ? 'yarn' : 'npm';
console.log(`\nDone. Now:\n`);
console.log(
` Update ${yellow(
packageName + '/shopify.config.js'
)} with the values for your storefront. If you want to test your Hydrogen app using the demo store, you can skip this step.`
);
console.log(`\nand then run:\n`);
if (root !== cwd) {
console.log(` cd ${path.relative(cwd, root)}`);
}
/**
* The LOCAL option only works with Yarn due to issues with NPM
* and symlinking yarn monorepos.
*/
const usesYarn = pkgManager === 'yarn' || process.env.LOCAL;
console.log(` ${usesYarn ? `yarn` : `npm install --legacy-peer-deps`}`);
console.log(` ${usesYarn ? `yarn dev` : `npm run dev`}`);
console.log();
}
async function getValidPackageName(projectName) {
const packageNameRegExp =
/^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
if (packageNameRegExp.test(projectName)) {
return projectName;
} else {
const suggestedPackageName = projectName
.trim()
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/^[._]/, '')
.replace(/[^a-z0-9-~]+/g, '-');
/**
* @type {{ inputPackageName: string }}
*/
const {inputPackageName} = await prompt({
type: 'input',
name: 'inputPackageName',
message: `Package name:`,
initial: suggestedPackageName,
validate: (input) =>
packageNameRegExp.test(input) ? true : 'Invalid package.json name',
});
return inputPackageName;
}
}
function emptyDir(dir) {
if (!fs.existsSync(dir)) {
return;
}
for (const file of fs.readdirSync(dir)) {
const abs = path.resolve(dir, file);
if (fs.lstatSync(abs).isDirectory()) {
emptyDir(abs);
fs.rmdirSync(abs);
} else {
fs.unlinkSync(abs);
}
}
}
init().catch((e) => {
console.error(e);
});