tutorialkit
Version:
Interactive tutorials powered by WebContainer API
1,263 lines (1,234 loc) • 35.7 kB
JavaScript
// src/index.ts
import chalk7 from "chalk";
import yargs2 from "yargs-parser";
// src/commands/create/index.ts
import * as prompts6 from "@clack/prompts";
import chalk5 from "chalk";
import { execa } from "execa";
import fs6 from "node:fs";
import path6 from "node:path";
import "yargs-parser";
// package.json
var package_default = {
name: "tutorialkit",
version: "0.1.2",
description: "Interactive tutorials powered by WebContainer API",
author: "StackBlitz Inc.",
type: "module",
bugs: "https://github.com/stackblitz/tutorialkit/issues",
homepage: "https://github.com/stackblitz/tutorialkit",
license: "MIT",
repository: {
type: "git",
url: "git+https://github.com/stackblitz/tutorialkit.git",
directory: "packages/cli"
},
bin: {
tutorialkit: "dist/index.js"
},
main: "./dist/index.js",
exports: {
".": "./dist/index.js"
},
scripts: {
build: "node scripts/build.js",
"build-release": "node scripts/build-release.js",
test: "vitest --testTimeout=300000"
},
files: [
"dist",
"template",
"template/.gitignore"
],
dependencies: {
"@babel/generator": "7.24.5",
"@babel/parser": "7.24.5",
"@babel/traverse": "7.24.5",
"@babel/types": "7.24.5",
"@clack/prompts": "^0.7.0",
chalk: "^5.3.0",
"detect-indent": "7.0.1",
execa: "^9.2.0",
ignore: "^5.3.1",
lookpath: "^1.2.2",
"which-pm": "2.2.0",
"yargs-parser": "^21.1.1"
},
devDependencies: {
"@types/babel__generator": "7.6.8",
"@types/babel__traverse": "7.20.5",
"@types/fs-extra": "^11.0.4",
"@types/node": "^20.14.6",
"@types/yargs-parser": "^21.0.3",
esbuild: "^0.20.2",
"esbuild-node-externals": "^1.13.1",
"fs-extra": "^11.2.0",
tempy: "^3.1.0",
vitest: "^1.6.0"
},
engines: {
node: ">=18.18.0"
}
};
// src/utils/messages.ts
import chalk2 from "chalk";
// src/utils/colors.ts
import chalk from "chalk";
var primaryBlue = chalk.bgHex("#0d6fe8");
// src/utils/messages.ts
function primaryLabel(text2) {
return primaryBlue(` ${chalk2.whiteBright(text2)} `);
}
function errorLabel(text2) {
return chalk2.bgRed(` ${chalk2.white(text2 ?? "ERROR")} `);
}
function warnLabel(text2) {
return chalk2.bgYellow(` ${chalk2.black(text2 ?? "WARN")} `);
}
function printHelp({ commandName, usage, tables, prolog, epilog }) {
const helpMessage = [];
let printNewline = false;
if (prolog) {
helpMessage.push(prolog);
printNewline = true;
}
if (usage) {
if (printNewline) {
helpMessage.push("");
}
printNewline = true;
const _usage = Array.isArray(usage) ? usage : [usage];
const label = "Usage:";
const indentation = " ".repeat(label.length + 1);
helpMessage.push(`${chalk2.bold.underline(label)} ${chalk2.green(commandName)} ${chalk2.bold(_usage[0])}`);
for (const usageLines of _usage.slice(1)) {
helpMessage.push(`${indentation}${chalk2.green(commandName)} ${chalk2.bold(usageLines)}`);
}
}
if (tables) {
let i = 0;
const tableEntries = Object.entries(tables);
if (tableEntries.length > 0 && printNewline) {
helpMessage.push("");
printNewline = true;
}
for (const [sectionTitle, tableRows] of tableEntries) {
const padding = Object.values(tableRows).reduce((maxLength, table) => {
const title = table[0];
if (title.length > maxLength) {
return title.length;
}
return maxLength;
}, 0);
helpMessage.push(chalk2.bold.underline(`${sectionTitle}:`));
for (const row of tableRows) {
const [command, description] = row;
helpMessage.push(` ${command.padEnd(padding, " ")} ${chalk2.dim(description)}`);
}
if (i++ < tableEntries.length - 1) {
helpMessage.push("");
}
}
}
if (epilog) {
if (printNewline) {
helpMessage.push("");
}
helpMessage.push(epilog);
}
console.log(helpMessage.join("\n"));
}
// src/utils/random.ts
function randomValueFromArray(array) {
return array[Math.floor(array.length * Math.random())];
}
// src/utils/words.ts
var adjectives = [
"aged",
"ancient",
"autumn",
"billowing",
"bitter",
"black",
"blue",
"bold",
"broad",
"broken",
"calm",
"cold",
"cool",
"crimson",
"curly",
"damp",
"dark",
"dawn",
"delicate",
"divine",
"dry",
"empty",
"falling",
"fancy",
"flat",
"floral",
"fragrant",
"frosty",
"gentle",
"green",
"hidden",
"holy",
"icy",
"jolly",
"late",
"lingering",
"little",
"lively",
"long",
"lucky",
"misty",
"morning",
"muddy",
"mute",
"nameless",
"noisy",
"odd",
"old",
"orange",
"patient",
"plain",
"polished",
"proud",
"purple",
"quiet",
"rapid",
"raspy",
"red",
"restless",
"rough",
"round",
"royal",
"shiny",
"shrill",
"shy",
"silent",
"small",
"snowy",
"soft",
"solitary",
"sparkling",
"spring",
"square",
"steep",
"still",
"summer",
"super",
"sweet",
"throbbing",
"tight",
"tiny",
"twilight",
"wandering",
"weathered",
"white",
"wild",
"winter",
"wispy",
"withered",
"yellow",
"young"
];
var nouns = [
"art",
"band",
"bar",
"base",
"bird",
"block",
"boat",
"bonus",
"bread",
"breeze",
"brook",
"bush",
"butterfly",
"cake",
"cell",
"cherry",
"cloud",
"credit",
"darkness",
"dawn",
"dew",
"disk",
"dream",
"dust",
"feather",
"field",
"fire",
"firefly",
"flower",
"fog",
"forest",
"frog",
"frost",
"glade",
"glitter",
"grass",
"hall",
"hat",
"haze",
"heart",
"hill",
"king",
"lab",
"lake",
"leaf",
"limit",
"math",
"meadow",
"mode",
"moon",
"morning",
"mountain",
"mouse",
"mud",
"night",
"paper",
"pine",
"poetry",
"pond",
"queen",
"rain",
"recipe",
"resonance",
"rice",
"river",
"salad",
"scene",
"sea",
"shadow",
"shape",
"silence",
"sky",
"smoke",
"snow",
"snowflake",
"sound",
"star",
"sun",
"sun",
"sunset",
"surf",
"term",
"thunder",
"tooth",
"tree",
"truth",
"union",
"unit",
"violet",
"voice",
"water",
"waterfall",
"wave",
"wildflower",
"wind",
"wood"
];
// src/utils/project.ts
function generateProjectName() {
const adjective = randomValueFromArray(adjectives);
const noun = randomValueFromArray(nouns);
return `${adjective}-${noun}`;
}
// src/utils/tasks.ts
import * as prompts from "@clack/prompts";
function assertNotCanceled(value, exitCode = 0) {
if (prompts.isCancel(value)) {
prompts.cancel("Command aborted");
console.log("Until next time!");
process.exit(exitCode);
}
}
async function runTask(task) {
if (task.disabled === true) {
return;
}
if (task.dryRun) {
prompts.log.warn(task.dryRunMessage ?? `Skipped '${task.title}'`);
return;
}
const spinner2 = prompts.spinner();
spinner2.start(task.title);
try {
const result = await task.task(spinner2.message);
spinner2.stop(result || task.title);
} catch (error) {
spinner2.stop(`${errorLabel()} ${error.message ?? "Task failed"}`, 1);
}
}
// src/utils/workspace-version.ts
function updateWorkspaceVersions(dependencies, version, filterDependency = allowAll) {
for (const dependency in dependencies) {
const depVersion = dependencies[dependency];
if (depVersion === "workspace:*" && filterDependency(dependency)) {
dependencies[dependency] = version;
}
}
}
function allowAll() {
return true;
}
// src/commands/create/enterprise.ts
import fs2 from "node:fs";
import path from "node:path";
// src/utils/astro-config.ts
import fs from "node:fs/promises";
// src/utils/babel.ts
import generator from "@babel/generator";
import parser from "@babel/parser";
import traverse from "@babel/traverse";
import * as t from "@babel/types";
var visit = traverse.default;
function generate(ast) {
const astToText = generator.default;
const { code } = astToText(ast);
return code;
}
var parse = (code) => parser.parse(code, { sourceType: "unambiguous", plugins: ["typescript"] });
// src/utils/astro-config.ts
async function parseAstroConfig(astroConfigPath) {
const source = await fs.readFile(astroConfigPath, { encoding: "utf-8" });
const result = parse(source);
if (!result) {
throw new Error("Unknown error parsing astro config");
}
if (result.errors.length > 0) {
throw new Error("Error parsing astro config: " + JSON.stringify(result.errors));
}
return result;
}
function generateAstroConfig(astroConfig) {
const defaultExport = "export default defineConfig";
let output = generate(astroConfig);
output = output.replace(defaultExport, `
${defaultExport}`);
return output;
}
function replaceArgs(newTutorialKitArgs, ast) {
const integrationImport = "@tutorialkit/astro";
let integrationId;
visit(ast, {
ImportDeclaration(path8) {
if (path8.node.source.value === integrationImport) {
const defaultImport = path8.node.specifiers.find((specifier) => specifier.type === "ImportDefaultSpecifier");
if (defaultImport) {
integrationId = defaultImport.local;
}
}
}
});
if (!integrationId) {
throw new Error(`Could not find import to '${integrationImport}'`);
}
visit(ast, {
ExportDefaultDeclaration(path8) {
if (!t.isCallExpression(path8.node.declaration)) {
return;
}
const configObject = path8.node.declaration.arguments[0];
if (!t.isObjectExpression(configObject)) {
throw new Error("TutorialKit is not part of the exported config");
}
const integrationsProp = configObject.properties.find((prop) => {
if (prop.type !== "ObjectProperty") {
return false;
}
if (prop.key.type === "Identifier") {
if (prop.key.name === "integrations") {
return true;
}
}
if (prop.key.type === "StringLiteral") {
if (prop.key.value === "integrations") {
return true;
}
}
return false;
});
if (integrationsProp.value.type !== "ArrayExpression") {
throw new Error("Unable to parse integrations in Astro config");
}
let integrationCall = integrationsProp.value.elements.find((expr) => {
return t.isCallExpression(expr) && t.isIdentifier(expr.callee) && expr.callee.name === integrationId.name;
});
if (!integrationCall) {
integrationCall = t.callExpression(integrationId, []);
integrationsProp.value.elements.push(integrationCall);
}
const integrationArgs = integrationCall.arguments;
if (integrationArgs.length === 0) {
const objectArgs = fromValue(newTutorialKitArgs);
if (objectArgs.properties.length > 0) {
integrationArgs.push(objectArgs);
}
return;
}
if (!t.isObjectExpression(integrationArgs[0])) {
throw new Error("Only updating an existing object literal as the config is supported");
}
updateObject(newTutorialKitArgs, integrationArgs[0]);
}
});
}
function updateObject(properties, object) {
if (typeof properties !== "object") {
return;
}
object ??= t.objectExpression([]);
for (const property in properties) {
const propertyInObject = object.properties.find((prop) => {
return prop.type === "ObjectProperty" && prop.key.type === "Identifier" && prop.key.name === property;
});
if (!propertyInObject) {
object.properties.push(t.objectProperty(t.identifier(property), fromValue(properties[property])));
} else {
if (typeof properties[property] === "object" && t.isObjectExpression(propertyInObject.value)) {
updateObject(properties[property], propertyInObject.value);
} else {
propertyInObject.value = fromValue(properties[property]);
}
}
}
}
function fromValue(value) {
if (value == null) {
return t.nullLiteral();
}
if (typeof value === "string") {
return t.stringLiteral(value);
}
if (typeof value === "number") {
return t.numericLiteral(value);
}
if (typeof value === "boolean") {
return t.booleanLiteral(value);
}
if (Array.isArray(value)) {
return t.arrayExpression(value.map(fromValue));
}
return t.objectExpression(
Object.keys(value).map((key) => t.objectProperty(t.identifier(key), fromValue(value[key])))
);
}
// src/commands/create/enterprise.ts
async function setupEnterpriseConfig(dest, flags) {
if (!flags.defaults && flags.enterprise === void 0) {
return;
}
let editorOrigin = flags.enterprise;
if (editorOrigin) {
const error = validateEditorOrigin(editorOrigin);
if (error) {
throw error;
}
editorOrigin = new URL(editorOrigin).origin;
}
const configPath = path.resolve(dest, "astro.config.ts");
if (!flags.dryRun && editorOrigin) {
const astroConfig = await parseAstroConfig(configPath);
replaceArgs(
{
enterprise: {
clientId: "wc_api",
editorOrigin,
scope: "turbo"
}
},
astroConfig
);
fs2.writeFileSync(configPath, generateAstroConfig(astroConfig));
}
}
function validateEditorOrigin(value) {
if (!value) {
return "Please provide an origin!";
}
try {
const url = new URL(value);
if (url.protocol !== "http:" && url.protocol !== "https:") {
return "Please provide an origin starting with http:// or https://";
}
} catch {
return "Please provide a valid origin URL!";
}
}
// src/commands/create/git.ts
import * as prompts2 from "@clack/prompts";
import chalk3 from "chalk";
import fs3 from "node:fs";
import path3 from "node:path";
// src/utils/shell.ts
import { spawn } from "node:child_process";
async function runShellCommand(command, flags, opts = {}) {
let child;
let stdout = "";
let stderr = "";
try {
child = spawn(command, flags, {
cwd: opts.cwd,
shell: true,
stdio: opts.stdio,
timeout: opts.timeout
});
const done = new Promise((resolve, reject) => {
child.on("close", (code) => {
if (code !== 0) {
reject(code);
return;
}
resolve(code);
});
child.on("error", (code) => {
reject(code);
});
});
child.stdout?.setEncoding("utf8");
child.stderr?.setEncoding("utf8");
child.stdout?.on("data", (data) => {
stdout += data;
});
child.stderr?.on("data", (data) => {
stderr += data;
});
await done;
} catch (error) {
throw { stdout, stderr, exitCode: error };
}
return { stdout, stderr, exitCode: child.exitCode };
}
// src/commands/create/options.ts
import path2 from "node:path";
import { fileURLToPath } from "node:url";
var __dirname = path2.dirname(fileURLToPath(import.meta.url));
var templatePath = path2.resolve(__dirname, "../template");
var DEFAULT_VALUES = {
git: !process.env.CI,
install: true,
start: true,
dryRun: false,
force: false,
packageManager: "npm"
};
function readFlag(flags, flag) {
let value = flags[flag];
if (flags.defaults) {
value ??= DEFAULT_VALUES[flag];
}
return value;
}
// src/commands/create/git.ts
async function initGitRepo(cwd, flags) {
let shouldInitGitRepo = readFlag(flags, "git");
if (shouldInitGitRepo === void 0) {
const answer = await prompts2.confirm({
message: "Initialize a new git repository?",
initialValue: DEFAULT_VALUES.git
});
assertNotCanceled(answer);
shouldInitGitRepo = answer;
}
if (shouldInitGitRepo) {
await runTask({
title: "Initializing git repository",
dryRun: flags.dryRun,
dryRunMessage: `${warnLabel("DRY RUN")} Skipped initializing git repository`,
task: async () => {
const message = await _initGitRepo(cwd);
return message ?? "Git repository initialized";
}
});
} else {
prompts2.log.message(`${chalk3.blue("git [skip]")} You can always run ${chalk3.yellow("git init")} manually.`);
}
}
async function _initGitRepo(cwd) {
if (fs3.existsSync(path3.join(cwd, ".git"))) {
return `${chalk3.cyan("Nice!")} Git has already been initialized`;
}
try {
await runShellCommand("git", ["init"], { cwd, stdio: "ignore" });
await runShellCommand("git", ["add", "-A"], { cwd, stdio: "ignore" });
await runShellCommand(
"git",
["commit", "-m", `"feat: initial commit from ${package_default.name}"`, '--author="StackBlitz <hello@stackblitz.com>"'],
{
cwd,
stdio: "ignore"
}
);
return void 0;
} catch {
throw new Error("Failed to initialize local git repository");
}
}
// src/commands/create/install-start.ts
import * as prompts3 from "@clack/prompts";
async function installAndStart(flags) {
const installDeps = readFlag(flags, "install");
const startProject2 = readFlag(flags, "start");
if (installDeps === false) {
return { install: false, start: false };
}
if (startProject2) {
return { install: true, start: true };
}
if (installDeps) {
if (startProject2 === false) {
return { install: true, start: false };
} else {
const answer2 = await prompts3.confirm({
message: "Start project?",
initialValue: DEFAULT_VALUES.install
});
assertNotCanceled(answer2);
return { install: true, start: answer2 };
}
}
const answer = await prompts3.confirm({
message: "Install dependencies and start project?",
initialValue: DEFAULT_VALUES.install
});
assertNotCanceled(answer);
return { install: answer, start: answer };
}
// src/commands/create/package-manager.ts
import fs4 from "node:fs";
import path4 from "node:path";
import * as prompts4 from "@clack/prompts";
import chalk4 from "chalk";
import { lookpath } from "lookpath";
var LOCK_FILES = /* @__PURE__ */ new Map([
["npm", "package-lock.json"],
["pnpm", "pnpm-lock.yaml"],
["yarn", "yarn.lock"]
]);
async function selectPackageManager(cwd, flags) {
const packageManager = await resolvePackageManager(flags);
for (const [pkgManager, lockFile] of LOCK_FILES) {
if (pkgManager !== packageManager) {
fs4.rmSync(path4.join(cwd, lockFile), { force: true });
}
}
return packageManager;
}
async function resolvePackageManager(flags) {
if (flags.packageManager) {
if (await lookpath(String(flags.packageManager))) {
return flags.packageManager;
}
prompts4.log.warn(
`The specified package manager '${chalk4.yellow(flags.packageManager)}' doesn't seem to be installed!`
);
}
if (flags.defaults) {
return DEFAULT_VALUES.packageManager;
}
return await getPackageManager();
}
async function getPackageManager() {
const installedPackageManagers = await getInstalledPackageManagers();
let initialValue = process.env.npm_config_user_agent?.split("/")[0];
if (!installedPackageManagers.includes(initialValue)) {
initialValue = "npm";
}
const answer = await prompts4.select({
message: "What package manager should we use?",
initialValue,
options: [
{ label: "npm", value: "npm" },
...installedPackageManagers.map((pkgManager) => {
return { label: pkgManager, value: pkgManager };
})
]
});
assertNotCanceled(answer);
return answer;
}
async function getInstalledPackageManagers() {
const packageManagers = [];
for (const pkgManager of ["yarn", "pnpm", "bun"]) {
try {
if (await lookpath(pkgManager)) {
packageManagers.push(pkgManager);
}
} catch {
}
}
return packageManagers;
}
// src/commands/create/template.ts
import * as prompts5 from "@clack/prompts";
import ignore from "ignore";
import fs5 from "node:fs";
import fsPromises from "node:fs/promises";
import path5 from "node:path";
async function copyTemplate(dest, flags) {
if (flags.dryRun) {
prompts5.log.warn(`${warnLabel("DRY RUN")} Skipped copying template`);
return;
}
const gitignore = ignore.default().add(readIgnoreFile());
const toCopy = [];
const folders = await fsPromises.readdir(templatePath);
for (const file of folders) {
if (gitignore.ignores(file)) {
continue;
}
toCopy.push(file);
}
for (const fileName of toCopy) {
const sourceFilePath = path5.join(templatePath, fileName);
const destFileName = fileName === ".npmignore" ? ".gitignore" : fileName;
const destFilePath = path5.join(dest, destFileName);
const stats = await fsPromises.stat(sourceFilePath);
if (stats.isDirectory()) {
await fsPromises.cp(sourceFilePath, destFilePath, { recursive: true });
} else if (stats.isFile()) {
await fsPromises.copyFile(sourceFilePath, destFilePath);
}
}
}
function readIgnoreFile() {
try {
return fs5.readFileSync(path5.resolve(templatePath, ".npmignore"), "utf8");
} catch {
return fs5.readFileSync(path5.resolve(templatePath, ".gitignore"), "utf8");
}
}
// src/commands/create/index.ts
var TUTORIALKIT_VERSION = package_default.version;
async function createTutorial(flags) {
if (flags._[1] === "help" || flags.help || flags.h) {
printHelp({
commandName: `${package_default.name} create`,
usage: "[name] [...options]",
tables: {
Options: [
["--dir, -d", "The folder in which the tutorial gets created"],
["--install, --no-install", `Install dependencies (default ${chalk5.yellow(DEFAULT_VALUES.install)})`],
["--start, --no-start", `Start project (default ${chalk5.yellow(DEFAULT_VALUES.start)})`],
["--git, --no-git", `Initialize a local git repository (default ${chalk5.yellow(DEFAULT_VALUES.git)})`],
["--dry-run", `Walk through steps without executing (default ${chalk5.yellow(DEFAULT_VALUES.dryRun)})`],
[
"--package-manager <name>, -p <name>",
`The package used to install dependencies (default ${chalk5.yellow(DEFAULT_VALUES.packageManager)})`
],
[
"--enterprise <origin>, -e <origin>",
`The origin of your StackBlitz Enterprise instance (if not provided authentication is not turned on and your project will use ${chalk5.yellow("https://stackblitz.com")})`
],
[
"--force",
`Overwrite existing files in the target directory without prompting (default ${chalk5.yellow(DEFAULT_VALUES.force)})`
],
["--defaults", "Skip all prompts and initialize the tutorial using the defaults"]
]
}
});
return 0;
}
applyAliases(flags);
try {
verifyFlags(flags);
} catch (error) {
console.error(`${errorLabel()} ${error.message}`);
process.exit(1);
}
try {
return _createTutorial(flags);
} catch (error) {
console.error(`${errorLabel()} Command failed`);
if (error.stack) {
console.error(`
${error.stack}`);
}
process.exit(1);
}
}
async function _createTutorial(flags) {
prompts6.intro(primaryLabel(package_default.name));
let tutorialName = flags._[1] !== void 0 ? String(flags._[1]) : void 0;
if (tutorialName === void 0) {
const randomName = generateProjectName();
if (flags.defaults) {
tutorialName = randomName;
} else {
const answer = await prompts6.text({
message: `What's the name of your tutorial?`,
placeholder: randomName,
validate: (value) => {
if (!value) {
return "Please provide a name!";
}
return void 0;
}
});
assertNotCanceled(answer);
tutorialName = answer;
}
}
prompts6.log.info(`We'll call your tutorial ${chalk5.blue(tutorialName)}`);
const dest = await getTutorialDirectory(tutorialName, flags);
const resolvedDest = path6.resolve(process.cwd(), dest);
prompts6.log.info(`Scaffolding tutorial in ${chalk5.blue(resolvedDest)}`);
if (fs6.existsSync(resolvedDest) && !flags.force) {
if (flags.defaults) {
console.error(`
${errorLabel()} Failed to create tutorial. Directory already exists.`);
process.exit(1);
}
let answer;
if (fs6.readdirSync(resolvedDest).length > 0) {
answer = await prompts6.confirm({
message: `Directory is not empty. Continuing may overwrite existing files. Do you want to continue?`,
initialValue: false
});
} else {
answer = await prompts6.confirm({
message: `Directory already exists. Continuing may overwrite existing files. Do you want to continue?`,
initialValue: false
});
}
assertNotCanceled(answer);
if (!answer) {
exitEarly();
}
} else {
if (!flags.dryRun) {
fs6.mkdirSync(resolvedDest, { recursive: true });
}
}
await copyTemplate(resolvedDest, flags);
updatePackageJson(resolvedDest, tutorialName, flags);
const selectedPackageManager = await selectPackageManager(resolvedDest, flags);
updateReadme(resolvedDest, selectedPackageManager, flags);
await setupEnterpriseConfig(resolvedDest, flags);
await initGitRepo(resolvedDest, flags);
const { install, start } = await installAndStart(flags);
prompts6.log.success(chalk5.green("Tutorial successfully created!"));
if (install || start) {
let message = "Please wait while we install the dependencies and start your project...";
if (install && !start) {
message = "Please wait while we install the dependencies...";
printNextSteps(dest, selectedPackageManager, true);
}
prompts6.outro(message);
await startProject(resolvedDest, selectedPackageManager, flags, start);
} else {
printNextSteps(dest, selectedPackageManager, false);
prompts6.outro(`You're all set!`);
console.log("Until next time \u{1F44B}");
}
}
async function startProject(cwd, packageManager, flags, startProject2) {
if (flags.dryRun) {
const message = startProject2 ? "Skipped dependency installation and project start" : "Skipped dependency installation";
console.warn(`${warnLabel("DRY RUN")} ${message}`);
} else {
await execa(packageManager, ["install"], { cwd, stdio: "inherit" });
if (startProject2) {
await execa(packageManager, ["run", "dev"], { cwd, stdio: "inherit" });
}
}
}
async function getTutorialDirectory(tutorialName, flags) {
const dir = flags.dir;
if (dir) {
return dir;
}
if (flags.defaults) {
return `./${tutorialName}`;
}
const promptResult = await prompts6.text({
message: "Where should we create your new tutorial?",
initialValue: `./${tutorialName}`,
placeholder: "./",
validate(value) {
if (!path6.isAbsolute(value) && !value.startsWith("./")) {
return "Please provide an absolute or relative path!";
}
return void 0;
}
});
assertNotCanceled(promptResult);
return promptResult;
}
function printNextSteps(dest, packageManager, dependenciesInstalled) {
let i = 0;
prompts6.log.message(chalk5.bold.underline("Next Steps"));
const steps = [
[`cd ${dest}`, "Navigate to project"],
[`${packageManager} install`, "Install dependencies", !dependenciesInstalled],
[`${packageManager} run dev`, "Start development server"],
[, `Head over to ${chalk5.underline("http://localhost:4321")}`]
];
for (const [command, text2, render] of steps) {
if (render === false) {
continue;
}
i++;
prompts6.log.step(`${i}. ${command ? `${chalk5.blue(command)} - ` : ""}${text2}`);
}
}
function updatePackageJson(dest, projectName, flags) {
if (flags.dryRun) {
return;
}
const pkgPath = path6.resolve(dest, "package.json");
const pkgJson = JSON.parse(fs6.readFileSync(pkgPath, "utf8"));
pkgJson.name = projectName;
updateWorkspaceVersions(pkgJson.dependencies, TUTORIALKIT_VERSION);
updateWorkspaceVersions(pkgJson.devDependencies, TUTORIALKIT_VERSION);
fs6.writeFileSync(pkgPath, JSON.stringify(pkgJson, void 0, 2));
try {
const pkgLockPath = path6.resolve(dest, "package-lock.json");
const pkgLockJson = JSON.parse(fs6.readFileSync(pkgLockPath, "utf8"));
const defaultPackage = pkgLockJson.packages[""];
pkgLockJson.name = projectName;
if (defaultPackage) {
defaultPackage.name = projectName;
}
fs6.writeFileSync(pkgLockPath, JSON.stringify(pkgLockJson, void 0, 2));
} catch {
}
}
function updateReadme(dest, packageManager, flags) {
if (flags.dryRun) {
return;
}
const readmePath = path6.resolve(dest, "README.md");
let readme = fs6.readFileSync(readmePath, "utf8");
readme = readme.replaceAll("<% pkgManager %>", packageManager ?? DEFAULT_VALUES.packageManager);
fs6.writeFileSync(readmePath, readme);
}
function exitEarly(exitCode = 0) {
prompts6.outro("Until next time!");
process.exit(exitCode);
}
function applyAliases(flags) {
if (flags.d) {
flags.dir = flags.d;
}
if (flags.p) {
flags.packageManager = flags.p;
}
if (flags.e) {
flags.enterprise = flags.e;
}
}
function verifyFlags(flags) {
if (flags.install === false && flags.start) {
throw new Error("Cannot start project without installing dependencies.");
}
}
// src/commands/eject/index.ts
import * as prompts7 from "@clack/prompts";
import chalk6 from "chalk";
import detectIndent from "detect-indent";
import { execa as execa2 } from "execa";
import fs7 from "node:fs";
import path7 from "node:path";
import whichpm from "which-pm";
// src/commands/eject/options.ts
var DEFAULT_VALUES2 = {
force: false,
defaults: false
};
// src/commands/eject/index.ts
var TUTORIALKIT_VERSION2 = package_default.version;
var REQUIRED_DEPENDENCIES = ["@tutorialkit/runtime", "@webcontainer/api", "nanostores", "@nanostores/react", "kleur"];
function ejectRoutes(flags) {
if (flags._[1] === "help" || flags.help || flags.h) {
printHelp({
commandName: `${package_default.name} eject`,
usage: "[folder] [...options]",
tables: {
Options: [
[
"--force",
`Overwrite existing files in the target directory without prompting (default ${chalk6.yellow(DEFAULT_VALUES2.force)})`
],
["--defaults", "Skip all the prompts and eject the routes using the defaults"]
]
}
});
return 0;
}
try {
return _eject(flags);
} catch (error) {
console.error(`${errorLabel()} Command failed`);
if (error.stack) {
console.error(`
${error.stack}`);
}
process.exit(1);
}
}
async function _eject(flags) {
let folderPath = flags._[1] !== void 0 ? String(flags._[1]) : void 0;
if (folderPath === void 0) {
folderPath = process.cwd();
} else {
folderPath = path7.resolve(process.cwd(), folderPath);
}
const { astroConfigPath, srcPath, pkgJsonPath, astroIntegrationPath, srcDestPath } = validateDestination(
folderPath,
flags.force
);
const astroConfig = await parseAstroConfig(astroConfigPath);
replaceArgs({ defaultRoutes: false }, astroConfig);
fs7.writeFileSync(astroConfigPath, generateAstroConfig(astroConfig));
fs7.cpSync(srcPath, srcDestPath, { recursive: true });
const pkgJsonContent = fs7.readFileSync(pkgJsonPath, "utf-8");
const indent = detectIndent(pkgJsonContent).indent || " ";
const pkgJson = JSON.parse(pkgJsonContent);
const astroIntegrationPkgJson = JSON.parse(
fs7.readFileSync(path7.join(astroIntegrationPath, "package.json"), "utf-8")
);
const newDependencies = [];
for (const dep of REQUIRED_DEPENDENCIES) {
if (!(dep in pkgJson.dependencies) && !(dep in pkgJson.devDependencies)) {
pkgJson.dependencies[dep] = astroIntegrationPkgJson.dependencies[dep];
newDependencies.push(dep);
}
}
updateWorkspaceVersions(
pkgJson.dependencies,
TUTORIALKIT_VERSION2,
(dependency) => REQUIRED_DEPENDENCIES.includes(dependency)
);
if (newDependencies.length > 0) {
fs7.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, void 0, indent), { encoding: "utf-8" });
console.log(
primaryLabel("INFO"),
`New dependencies added: ${newDependencies.join(", ")}. Install the new dependencies before proceeding.`
);
if (!flags.defaults) {
const packageManager = (await whichpm(path7.dirname(pkgJsonPath))).name;
const answer = await prompts7.confirm({
message: `Do you want to install those dependencies now using ${chalk6.blue(packageManager)}?`
});
if (answer === true) {
await execa2(packageManager, ["install"], { cwd: folderPath, stdio: "inherit" });
}
}
}
}
function validateDestination(folder, force) {
assertExists(folder);
const pkgJsonPath = assertExists(path7.join(folder, "package.json"));
const astroConfigPath = assertExists(path7.join(folder, "astro.config.ts"));
const srcDestPath = assertExists(path7.join(folder, "src"));
const astroIntegrationPath = assertExists(path7.resolve(folder, "node_modules", "@tutorialkit", "astro"));
const srcPath = path7.join(astroIntegrationPath, "dist", "default");
if (!force) {
walk(srcPath, (relativePath) => {
const destination = path7.join(srcDestPath, relativePath);
if (fs7.existsSync(destination)) {
throw new Error(
`Eject aborted because '${destination}' would be overwritten by this command. Use ${chalk6.yellow("--force")} to ignore this error.`
);
}
});
}
return {
astroConfigPath,
astroIntegrationPath,
pkgJsonPath,
srcPath,
srcDestPath
};
}
function assertExists(filePath) {
if (!fs7.existsSync(filePath)) {
throw new Error(`${filePath} does not exists!`);
}
return filePath;
}
function walk(root, visit2) {
function traverse2(folder, pathPrefix) {
for (const filename of fs7.readdirSync(folder)) {
const filePath = path7.join(folder, filename);
const stat = fs7.statSync(filePath);
const relativeFilePath = path7.join(pathPrefix, filename);
if (stat.isDirectory()) {
traverse2(filePath, relativeFilePath);
} else {
visit2(relativeFilePath);
}
}
}
traverse2(root, "");
}
// src/index.ts
var supportedCommands = /* @__PURE__ */ new Set(["version", "help", "create", "eject"]);
cli();
async function cli() {
const flags = yargs2(process.argv.slice(2));
const cmd = resolveCommand(flags);
try {
console.log("");
const exitCode = await runCommand(cmd, flags);
process.exit(exitCode || 0);
} catch (error) {
console.error(`${errorLabel()} ${error.message}`);
process.exit(1);
}
}
async function runCommand(cmd, flags) {
switch (cmd) {
case "version": {
console.log(`${primaryLabel(package_default.name)} ${chalk7.green(`v${package_default.version}`)}`);
return;
}
case "help": {
printHelp({
commandName: package_default.name,
prolog: `${primaryLabel(package_default.name)} ${chalk7.green(`v${package_default.version}`)} Create tutorial apps powered by WebContainer API`,
usage: ["[command] [...options]", "[ -h | --help | -v | --version ]"],
tables: {
Commands: [
["create", "Create new tutorial app"],
["help", "Show this help message"]
]
}
});
return;
}
case "create": {
return createTutorial(flags);
}
case "eject": {
return ejectRoutes(flags);
}
default: {
console.error(`${errorLabel()} Unknown command ${chalk7.red(cmd)}`);
return 1;
}
}
}
function resolveCommand(flags) {
if (flags.version || flags.v) {
return "version";
}
if (flags._[0] == null && (flags.help || flags.h)) {
return "help";
}
const cmd = String(flags._.at(0));
if (!supportedCommands.has(cmd)) {
return "help";
}
return cmd;
}