create-zephyr-apps
Version:
A CLI tool to create web applications using Zephyr.
219 lines • 8.13 kB
JavaScript
var _a;
import { cancel, confirm, intro, isCancel, log, note, select, spinner, text, } from '@clack/prompts';
import { getCatalogsFromWorkspaceManifest } from '@pnpm/catalogs.config';
import { resolveFromCatalog } from '@pnpm/catalogs.resolver';
import { findWorkspacePackagesNoCheck } from '@pnpm/workspace.find-packages';
import { readWorkspaceManifest } from '@pnpm/workspace.read-manifest';
import c from 'chalk-template';
import { exec } from 'node:child_process';
import * as fs from 'node:fs';
import { homedir } from 'node:os';
import path from 'node:path';
import { setImmediate } from 'node:timers/promises';
import { promisify } from 'node:util';
import terminalLink from 'terminal-link';
import { DependencyFields, ProjectTypes, Templates } from './templates.js';
const execAsync = promisify(exec);
// Immediate is required to avoid terminal image flickering
console.clear();
await setImmediate();
if (!process.stdout.isTTY) {
cancel('Please run this command in a TTY terminal.');
process.exit(1);
}
intro(c `Bootstrap your project using {cyan Zephyr}!`);
note(c `The only sane way to do micro-frontends\n{cyan https://docs.zephyr-cloud.io/}`, 'Zephyr Cloud');
let output = await text({
message: 'Where should we create your project?',
placeholder: './my-app',
validate(value) {
if (!value.trim().length) {
return 'Please enter a project name.';
}
return undefined;
},
});
if (isCancel(output)) {
cancel('Operation cancelled.');
process.exit(0);
}
output = path.resolve(output);
const relativeOutput = path.relative(process.cwd(), output) || './';
// ensures output is not a directory with contents
if (fs.existsSync(output)) {
const stats = fs.statSync(output);
if (!stats.isDirectory()) {
cancel(c `{cyan ${relativeOutput}} is not a directory.`);
process.exit(1);
}
const files = fs.readdirSync(output);
if (files.length > 0) {
cancel(c `Output directory {cyan ${relativeOutput}} must be empty.`);
process.exit(1);
}
}
const projectKind = await select({
message: 'What type of project you are creating?',
initialValue: (_a = ProjectTypes[0]) === null || _a === void 0 ? void 0 : _a.value,
options: ProjectTypes,
maxItems: 1,
});
if (isCancel(projectKind)) {
cancel('Operation cancelled.');
process.exit(0);
}
let examplesRepoName;
let subfolder;
if (projectKind === 'web') {
const template = await select({
message: 'Pick a template: ',
initialValue: 'react-rspack-mf',
options: Templates.map((temp) => ({
value: temp.name,
label: temp.label,
hint: temp.hint,
})),
});
if (isCancel(template)) {
cancel('Operation cancelled.');
process.exit(0);
}
examplesRepoName = 'zephyr-examples';
subfolder = `examples/${template}`;
}
else {
examplesRepoName = 'zephyr-repack-example';
subfolder = '';
}
const loading = spinner();
const tmpDir = path.resolve(homedir(), '.zephyr', examplesRepoName);
loading.start('Preparing temporary directory...');
if (!fs.existsSync(tmpDir)) {
// ensures the temporary directory exists
await fs.promises.mkdir(tmpDir, { recursive: true });
}
else {
// cleans the temporary directory
await fs.promises.rm(tmpDir, { recursive: true, force: true });
}
loading.message('Cloning example to temporary directory...');
try {
const { stderr } = await execAsync(`git clone --quiet --depth 1 https://github.com/ZephyrCloudIO/${examplesRepoName}.git -b main ${tmpDir}`, { encoding: 'utf8', timeout: 1000 * 60 * 5 });
if (stderr) {
loading.stop('Error cloning repository to temporary directory... ');
cancel(stderr);
process.exit(1);
}
const pathToCopy = path.join(tmpDir, subfolder);
loading.message('Replacing catalogs...');
// Monorepos uses pnpm catalogs
const [manifest, packages] = await Promise.all([
readWorkspaceManifest(tmpDir),
findWorkspacePackagesNoCheck(tmpDir),
]);
if (manifest) {
const catalogs = getCatalogsFromWorkspaceManifest(manifest);
for (const pkg of packages) {
// Skip packages that are not in the output directory
if (!pkg.rootDirRealPath.startsWith(pathToCopy)) {
continue;
}
for (const field of DependencyFields) {
if (!pkg.manifest[field]) {
continue;
}
for (const [alias, pref] of Object.entries(pkg.manifest[field])) {
const result = resolveFromCatalog(catalogs, { alias, pref });
switch (result.type) {
case 'found':
pkg.manifest[field][alias] = result.resolution.specifier;
break;
case 'misconfiguration':
throw result.error;
}
}
// update the catalog contents
await pkg.writeProjectManifest(pkg.manifest, true);
}
}
}
const dotGitPath = path.join(pathToCopy, '.git');
loading.message(`Extracting template to ${relativeOutput}...`);
await fs.promises.cp(pathToCopy, output, {
recursive: true,
force: true,
dereference: true,
// skip .git folder
filter(source) {
return !source.startsWith(dotGitPath);
},
});
loading.message('Cleaning up temporary directory...');
// no need to wait this operation, as it is not critical for the user experience
void fs.promises.rm(tmpDir, { recursive: true, force: true }).catch(console.error);
loading.stop(c `Project successfully created at {cyan ${relativeOutput}}!`);
}
catch (error) {
cancel(c `Error cloning repository to {cyan ${relativeOutput}}: ${error}`);
loading.stop('Error!', 1);
process.exit(1);
}
const shouldInitGit = await confirm({
message: 'Would you like to initialize a new Git repository?',
initialValue: true,
});
if (isCancel(shouldInitGit)) {
cancel('Operation cancelled');
process.exit(0);
}
if (shouldInitGit) {
// Initialize the repository.
await execAsync('git init', { cwd: output });
// Set temporary Git user configuration.
await execAsync('git config user.email "zephyrbot@zephyr-cloud.io"', {
cwd: output,
});
await execAsync('git config user.name "Zephyr Bot"', { cwd: output });
// Stage all files and commit them.
await execAsync('git add .', { cwd: output });
await execAsync('git commit --no-gpg-sign -m "Initial commit from Zephyr"', {
cwd: output,
});
// Remove the temporary local Git configuration so that the user's global
// settings will be used for future commits.
await execAsync('git config --unset user.email', { cwd: output });
await execAsync('git config --unset user.name', { cwd: output });
}
else {
log.warn('Zephyr requires a Git repository to work properly, please create it manually afterwards.');
}
const repoName = path.basename(output);
if (projectKind === 'web') {
note(`
cd ./${repoName}
pnpm install
pnpm run build
`.trim(), 'Run the application!');
}
else {
note(c `
cd ./${repoName}
pnpm install
git remote add origin https://github.com/{cyan <name>}/{cyan ${repoName}}.git
{cyan ZC=1} pnpm start
`.trim(), 'Run the application!');
note(c `
Make sure to commit and add a remote to the remote repository!
Read more about how Module Federation works with Zephyr:
- {cyan https://docs.zephyr-cloud.io/how-to/mf-guide}
`.trim(), 'Read more about Module Federation');
}
note(c `
- {cyan ${terminalLink('Discord', 'https://zephyr-cloud.io/discord')}}
- {cyan ${terminalLink('Documentation', projectKind === 'web'
? 'https://docs.zephyr-cloud.io/recipes'
: 'https://docs.zephyr-cloud.io/recipes/repack-mf')}}
- {cyan ${terminalLink('Open an issue', 'https://github.com/ZephyrCloudIO/zephyr-packages/issues')}}
`.trim(), 'Next steps.');
//# sourceMappingURL=index.js.map