UNPKG

tutorialkit

Version:

Interactive tutorials powered by WebContainer API

8 lines (7 loc) 19.2 kB
#!/usr/bin/env node import K from"chalk";import st from"yargs-parser";import*as d from"@clack/prompts";import g from"chalk";import{execa as me}from"execa";import x from"node:fs";import I from"node:path";import"yargs-parser";var u={name:"tutorialkit",version:"0.0.1-alpha.25",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"}};import y from"chalk";import ve from"chalk";var q=ve.bgHex("#0d6fe8");function O(e){return q(` ${y.whiteBright(e)} `)}function h(e){return y.bgRed(` ${y.white(e??"ERROR")} `)}function S(e){return y.bgYellow(` ${y.black(e??"WARN")} `)}function $({commandName:e,usage:t,tables:r,prolog:n,epilog:o}){let i=[],s=!1;if(n&&(i.push(n),s=!0),t){s&&i.push(""),s=!0;let a=Array.isArray(t)?t:[t],l="Usage:",p=" ".repeat(l.length+1);i.push(`${y.bold.underline(l)} ${y.green(e)} ${y.bold(a[0])}`);for(let k of a.slice(1))i.push(`${p}${y.green(e)} ${y.bold(k)}`)}if(r){let a=0,l=Object.entries(r);l.length>0&&s&&(i.push(""),s=!0);for(let[p,k]of l){let z=Object.values(k).reduce((C,f)=>{let j=f[0];return j.length>C?j.length:C},0);i.push(y.bold.underline(`${p}:`));for(let C of k){let[f,j]=C;i.push(` ${f.padEnd(z," ")} ${y.dim(j)}`)}a++<l.length-1&&i.push("")}}o&&(s&&i.push(""),i.push(o)),console.log(i.join(` `))}function W(e){return e[Math.floor(e.length*Math.random())]}var Q=["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"],X=["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"];function Z(){let e=W(Q),t=W(X);return`${e}-${t}`}import*as P from"@clack/prompts";function w(e,t=0){P.isCancel(e)&&(P.cancel("Command aborted"),console.log("Until next time!"),process.exit(t))}async function ee(e){if(e.disabled===!0)return;if(e.dryRun){P.log.warn(e.dryRunMessage??`Skipped '${e.title}'`);return}let t=P.spinner();t.start(e.title);try{let r=await e.task(t.message);t.stop(r||e.title)}catch(r){t.stop(`${h()} ${r.message??"Task failed"}`,1)}}function E(e,t,r=Ce){for(let n in e)if(e[n]==="workspace:*"&&r(n))if(process.env.TK_DIRECTORY){let i=n.split("/")[1];e[n]=`file:${process.env.TK_DIRECTORY}/packages/${i.replace("-","/")}`}else e[n]=t}function Ce(){return!0}import je from"node:fs";import Ee from"node:path";import $e from"node:fs/promises";import Pe from"@babel/generator";import Oe from"@babel/parser";import Se from"@babel/traverse";import*as c from"@babel/types";var H=Se.default;function te(e){let t=Pe.default,{code:r}=t(e);return r}var re=e=>Oe.parse(e,{sourceType:"unambiguous",plugins:["typescript"]});async function T(e){let t=await $e.readFile(e,{encoding:"utf-8"}),r=re(t);if(!r)throw new Error("Unknown error parsing astro config");if(r.errors.length>0)throw new Error("Error parsing astro config: "+JSON.stringify(r.errors));return r}function M(e){let t="export default defineConfig",r=te(e);return r=r.replace(t,` ${t}`),r}function _(e,t){let r="@tutorialkit/astro",n;if(H(t,{ImportDeclaration(o){if(o.node.source.value===r){let i=o.node.specifiers.find(s=>s.type==="ImportDefaultSpecifier");i&&(n=i.local)}}}),!n)throw new Error(`Could not find import to '${r}'`);H(t,{ExportDefaultDeclaration(o){if(!c.isCallExpression(o.node.declaration))return;let i=o.node.declaration.arguments[0];if(!c.isObjectExpression(i))throw new Error("TutorialKit is not part of the exported config");let s=i.properties.find(p=>p.type!=="ObjectProperty"?!1:p.key.type==="Identifier"&&p.key.name==="integrations"||p.key.type==="StringLiteral"&&p.key.value==="integrations");if(s.value.type!=="ArrayExpression")throw new Error("Unable to parse integrations in Astro config");let a=s.value.elements.find(p=>c.isCallExpression(p)&&c.isIdentifier(p.callee)&&p.callee.name===n.name);a||(a=c.callExpression(n,[]),s.value.elements.push(a));let l=a.arguments;if(l.length===0){let p=R(e);p.properties.length>0&&l.push(p);return}if(!c.isObjectExpression(l[0]))throw new Error("Only updating an existing object literal as the config is supported");ne(e,l[0])}})}function ne(e,t){if(typeof e=="object"){t??=c.objectExpression([]);for(let r in e){let n=t.properties.find(o=>o.type==="ObjectProperty"&&o.key.type==="Identifier"&&o.key.name===r);n?typeof e[r]=="object"&&c.isObjectExpression(n.value)?ne(e[r],n.value):n.value=R(e[r]):t.properties.push(c.objectProperty(c.identifier(r),R(e[r])))}}}function R(e){return e==null?c.nullLiteral():typeof e=="string"?c.stringLiteral(e):typeof e=="number"?c.numericLiteral(e):typeof e=="boolean"?c.booleanLiteral(e):Array.isArray(e)?c.arrayExpression(e.map(R)):c.objectExpression(Object.keys(e).map(t=>c.objectProperty(c.identifier(t),R(e[t]))))}async function oe(e,t){if(!t.defaults&&t.enterprise===void 0)return;let r=t.enterprise;if(r){let o=Re(r);if(o)throw o;r=new URL(r).origin}let n=Ee.resolve(e,"astro.config.ts");if(!t.dryRun&&r){let o=await T(n);_({enterprise:{clientId:"wc_api",editorOrigin:r,scope:"turbo"}},o),je.writeFileSync(n,M(o))}}function Re(e){if(!e)return"Please provide an origin!";try{let t=new URL(e);if(t.protocol!=="http:"&&t.protocol!=="https:")return"Please provide an origin starting with http:// or https://"}catch{return"Please provide a valid origin URL!"}}import*as N from"@clack/prompts";import Y from"chalk";import Le from"node:fs";import Te from"node:path";import{spawn as Ae}from"node:child_process";async function D(e,t,r={}){let n,o="",i="";try{n=Ae(e,t,{cwd:r.cwd,shell:!0,stdio:r.stdio,timeout:r.timeout});let s=new Promise((a,l)=>{n.on("close",p=>{if(p!==0){l(p);return}a(p)}),n.on("error",p=>{l(p)})});n.stdout?.setEncoding("utf8"),n.stderr?.setEncoding("utf8"),n.stdout?.on("data",a=>{o+=a}),n.stderr?.on("data",a=>{i+=a}),await s}catch(s){throw{stdout:o,stderr:i,exitCode:s}}return{stdout:o,stderr:i,exitCode:n.exitCode}}import ie from"node:path";import{fileURLToPath as Fe}from"node:url";var Ie=ie.dirname(Fe(import.meta.url)),A=ie.resolve(Ie,"../template"),m={git:!process.env.CI,install:!0,start:!0,dryRun:!1,force:!1,packageManager:"npm"};function F(e,t){let r=e[t];return e.defaults&&(r??=m[t]),r}async function ae(e,t){let r=F(t,"git");if(r===void 0){let n=await N.confirm({message:"Initialize a new git repository?",initialValue:m.git});w(n),r=n}r?await ee({title:"Initializing git repository",dryRun:t.dryRun,dryRunMessage:`${S("DRY RUN")} Skipped initializing git repository`,task:async()=>await Me(e)??"Git repository initialized"}):N.log.message(`${Y.blue("git [skip]")} You can always run ${Y.yellow("git init")} manually.`)}async function Me(e){if(Le.existsSync(Te.join(e,".git")))return`${Y.cyan("Nice!")} Git has already been initialized`;try{await D("git",["init"],{cwd:e,stdio:"ignore"}),await D("git",["add","-A"],{cwd:e,stdio:"ignore"}),await D("git",["commit","-m",`"feat: initial commit from ${u.name}"`,'--author="StackBlitz <hello@stackblitz.com>"'],{cwd:e,stdio:"ignore"});return}catch{throw new Error("Failed to initialize local git repository")}}import*as B from"@clack/prompts";async function se(e){let t=F(e,"install"),r=F(e,"start");if(t===!1)return{install:!1,start:!1};if(r)return{install:!0,start:!0};if(t){if(r===!1)return{install:!0,start:!1};{let o=await B.confirm({message:"Start project?",initialValue:m.install});return w(o),{install:!0,start:o}}}let n=await B.confirm({message:"Install dependencies and start project?",initialValue:m.install});return w(n),{install:n,start:n}}import _e from"node:fs";import De from"node:path";import*as U from"@clack/prompts";import Ne from"chalk";import{lookpath as le}from"lookpath";var Ue=new Map([["npm","package-lock.json"],["pnpm","pnpm-lock.yaml"],["yarn","yarn.lock"]]);async function pe(e,t){let r=await Ve(t);for(let[n,o]of Ue)n!==r&&_e.rmSync(De.join(e,o),{force:!0});return r}async function Ve(e){if(e.packageManager){if(await le(String(e.packageManager)))return e.packageManager;U.log.warn(`The specified package manager '${Ne.yellow(e.packageManager)}' doesn't seem to be installed!`)}return e.defaults?m.packageManager:await Je()}async function Je(){let e=await ze(),t=process.env.npm_config_user_agent?.split("/")[0];e.includes(t)||(t="npm");let r=await U.select({message:"What package manager should we use?",initialValue:t,options:[{label:"npm",value:"npm"},...e.map(n=>({label:n,value:n}))]});return w(r),r}async function ze(){let e=[];for(let t of["yarn","pnpm","bun"])try{await le(t)&&e.push(t)}catch{}return e}import*as de from"@clack/prompts";import We from"ignore";import ce from"node:fs";import V from"node:fs/promises";import J from"node:path";async function ue(e,t){if(t.dryRun){de.log.warn(`${S("DRY RUN")} Skipped copying template`);return}let r=We.default().add(He()),n=[],o=await V.readdir(A);for(let i of o)r.ignores(i)||n.push(i);for(let i of n){let s=J.join(A,i),a=i===".npmignore"?".gitignore":i,l=J.join(e,a),p=await V.stat(s);p.isDirectory()?await V.cp(s,l,{recursive:!0}):p.isFile()&&await V.copyFile(s,l)}}function He(){try{return ce.readFileSync(J.resolve(A,".npmignore"),"utf8")}catch{return ce.readFileSync(J.resolve(A,".gitignore"),"utf8")}}var fe=u.version;async function ye(e){if(e._[1]==="help"||e.help||e.h)return $({commandName:`${u.name} create`,usage:"[name] [...options]",tables:{Options:[["--dir, -d","The folder in which the tutorial gets created"],["--install, --no-install",`Install dependencies (default ${g.yellow(m.install)})`],["--start, --no-start",`Start project (default ${g.yellow(m.start)})`],["--git, --no-git",`Initialize a local git repository (default ${g.yellow(m.git)})`],["--dry-run",`Walk through steps without executing (default ${g.yellow(m.dryRun)})`],["--package-manager <name>, -p <name>",`The package used to install dependencies (default ${g.yellow(m.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 ${g.yellow("https://stackblitz.com")})`],["--force",`Overwrite existing files in the target directory without prompting (default ${g.yellow(m.force)})`],["--defaults","Skip all prompts and initialize the tutorial using the defaults"]]}}),0;Xe(e);try{Ze(e)}catch(t){console.error(`${h()} ${t.message}`),process.exit(1)}try{return Ye(e)}catch(t){console.error(`${h()} Command failed`),t.stack&&console.error(` ${t.stack}`),process.exit(1)}}async function Ye(e){d.intro(O(u.name));let t=e._[1]!==void 0?String(e._[1]):void 0;if(t===void 0){let a=Z();if(e.defaults)t=a;else{let l=await d.text({message:"What's the name of your tutorial?",placeholder:a,validate:p=>{if(!p)return"Please provide a name!"}});w(l),t=l}}d.log.info(`We'll call your tutorial ${g.blue(t)}`);let r=await Ge(t,e),n=I.resolve(process.cwd(),r);if(d.log.info(`Scaffolding tutorial in ${g.blue(n)}`),x.existsSync(n)&&!e.force){e.defaults&&(console.error(` ${h()} Failed to create tutorial. Directory already exists.`),process.exit(1));let a;x.readdirSync(n).length>0?a=await d.confirm({message:"Directory is not empty. Continuing may overwrite existing files. Do you want to continue?",initialValue:!1}):a=await d.confirm({message:"Directory already exists. Continuing may overwrite existing files. Do you want to continue?",initialValue:!1}),w(a),a||Qe()}else e.dryRun||x.mkdirSync(n,{recursive:!0});await ue(n,e),Ke(n,t,e);let o=await pe(n,e);qe(n,o,e),await oe(n,e),await ae(n,e);let{install:i,start:s}=await se(e);if(d.log.success(g.green("Tutorial successfully created!")),i||s){let a="Please wait while we install the dependencies and start your project...";i&&!s&&(a="Please wait while we install the dependencies...",ge(r,o,!0)),d.outro(a),await Be(n,o,e,s)}else ge(r,o,!1),d.outro("You're all set!"),console.log("Until next time \u{1F44B}")}async function Be(e,t,r,n){if(r.dryRun){let o=n?"Skipped dependency installation and project start":"Skipped dependency installation";console.warn(`${S("DRY RUN")} ${o}`)}else await me(t,["install"],{cwd:e,stdio:"inherit"}),n&&await me(t,["run","dev"],{cwd:e,stdio:"inherit"})}async function Ge(e,t){let r=t.dir;if(r)return r;if(t.defaults)return`./${e}`;let n=await d.text({message:"Where should we create your new tutorial?",initialValue:`./${e}`,placeholder:"./",validate(o){if(!I.isAbsolute(o)&&!o.startsWith("./"))return"Please provide an absolute or relative path!"}});return w(n),n}function ge(e,t,r){let n=0;d.log.message(g.bold.underline("Next Steps"));let o=[[`cd ${e}`,"Navigate to project"],[`${t} install`,"Install dependencies",!r],[`${t} run dev`,"Start development server"],[,`Head over to ${g.underline("http://localhost:4321")}`]];for(let[i,s,a]of o)a!==!1&&(n++,d.log.step(`${n}. ${i?`${g.blue(i)} - `:""}${s}`))}function Ke(e,t,r){if(r.dryRun)return;let n=I.resolve(e,"package.json"),o=JSON.parse(x.readFileSync(n,"utf8"));o.name=t,E(o.dependencies,fe),E(o.devDependencies,fe),x.writeFileSync(n,JSON.stringify(o,void 0,2));try{let i=I.resolve(e,"package-lock.json"),s=JSON.parse(x.readFileSync(i,"utf8")),a=s.packages[""];s.name=t,a&&(a.name=t),x.writeFileSync(i,JSON.stringify(s,void 0,2))}catch{}}function qe(e,t,r){if(r.dryRun)return;let n=I.resolve(e,"README.md"),o=x.readFileSync(n,"utf8");o=o.replaceAll("<% pkgManager %>",t??m.packageManager),x.writeFileSync(n,o)}function Qe(e=0){d.outro("Until next time!"),process.exit(e)}function Xe(e){e.d&&(e.dir=e.d),e.p&&(e.packageManager=e.p),e.e&&(e.enterprise=e.e)}function Ze(e){if(e.install===!1&&e.start)throw new Error("Cannot start project without installing dependencies.")}import*as be from"@clack/prompts";import G from"chalk";import et from"detect-indent";import{execa as tt}from"execa";import v from"node:fs";import b from"node:path";import rt from"which-pm";var he={force:!1,defaults:!1};var nt=u.version,we=["@tutorialkit/runtime","@webcontainer/api","nanostores","@nanostores/react"];function ke(e){if(e._[1]==="help"||e.help||e.h)return $({commandName:`${u.name} eject`,usage:"[folder] [...options]",tables:{Options:[["--force",`Overwrite existing files in the target directory without prompting (default ${G.yellow(he.force)})`],["--defaults","Skip all the prompts and eject the routes using the defaults"]]}}),0;try{return ot(e)}catch(t){console.error(`${h()} Command failed`),t.stack&&console.error(` ${t.stack}`),process.exit(1)}}async function ot(e){let t=e._[1]!==void 0?String(e._[1]):void 0;t===void 0?t=process.cwd():t=b.resolve(process.cwd(),t);let{astroConfigPath:r,srcPath:n,pkgJsonPath:o,astroIntegrationPath:i,srcDestPath:s}=it(t,e.force),a=await T(r);_({defaultRoutes:!1},a),v.writeFileSync(r,M(a)),v.cpSync(n,s,{recursive:!0});let l=v.readFileSync(o,"utf-8"),p=et(l).indent||" ",k=JSON.parse(l),z=JSON.parse(v.readFileSync(b.join(i,"package.json"),"utf-8")),C=[];for(let f of we)!(f in k.dependencies)&&!(f in k.devDependencies)&&(k.dependencies[f]=z.dependencies[f],C.push(f));if(E(k.dependencies,nt,f=>we.includes(f)),C.length>0&&(v.writeFileSync(o,JSON.stringify(k,void 0,p),{encoding:"utf-8"}),console.log(O("INFO"),`New dependencies added: ${C.join(", ")}. Install the new dependencies before proceeding.`),!e.defaults)){let f=(await rt(b.dirname(o))).name;await be.confirm({message:`Do you want to install those dependencies now using ${G.blue(f)}?`})===!0&&await tt(f,["install"],{cwd:t,stdio:"inherit"})}}function it(e,t){L(e);let r=L(b.join(e,"package.json")),n=L(b.join(e,"astro.config.ts")),o=L(b.join(e,"src")),i=L(b.resolve(e,"node_modules","@tutorialkit","astro")),s=b.join(i,"dist","default");return t||at(s,a=>{let l=b.join(o,a);if(v.existsSync(l))throw new Error(`Eject aborted because '${l}' would be overwritten by this command. Use ${G.yellow("--force")} to ignore this error.`)}),{astroConfigPath:n,astroIntegrationPath:i,pkgJsonPath:r,srcPath:s,srcDestPath:o}}function L(e){if(!v.existsSync(e))throw new Error(`${e} does not exists!`);return e}function at(e,t){function r(n,o){for(let i of v.readdirSync(n)){let s=b.join(n,i),a=v.statSync(s),l=b.join(o,i);a.isDirectory()?r(s,l):t(l)}}r(e,"")}var lt=new Set(["version","help","create","eject"]);pt();async function pt(){let e=st(process.argv.slice(2)),t=dt(e);try{console.log("");let r=await ct(t,e);process.exit(r||0)}catch(r){console.error(`${h()} ${r.message}`),process.exit(1)}}async function ct(e,t){switch(e){case"version":{console.log(`${O(u.name)} ${K.green(`v${u.version}`)}`);return}case"help":{$({commandName:u.name,prolog:`${O(u.name)} ${K.green(`v${u.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 ye(t);case"eject":return ke(t);default:return console.error(`${h()} Unknown command ${K.red(e)}`),1}}function dt(e){if(e.version||e.v)return"version";if(e._[0]==null&&(e.help||e.h))return"help";let t=String(e._.at(0));return lt.has(t)?t:"help"}