UNPKG

tutorialkit

Version:

Interactive tutorials powered by WebContainer API

1,263 lines (1,234 loc) 35.7 kB
#!/usr/bin/env node // 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; }