@tutorialkit-rb/cli 1.5.2-rb.0.1.0 → 1.5.2-rb.0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,21 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import chalk8 from "chalk";
4
+ import chalk9 from "chalk";
5
5
  import yargs2 from "yargs-parser";
6
6
 
7
7
  // src/commands/create/index.ts
8
8
  import fs7 from "node:fs";
9
- import path7 from "node:path";
9
+ import path8 from "node:path";
10
10
  import * as prompts7 from "@clack/prompts";
11
- import chalk6 from "chalk";
12
- import { execa } from "execa";
11
+ import chalk7 from "chalk";
13
12
  import "yargs-parser";
14
13
 
15
14
  // package.json
16
15
  var package_default = {
17
16
  name: "@tutorialkit-rb/cli",
18
- version: "1.5.2-rb.0.1.0",
17
+ version: "1.5.2-rb.0.1.1",
19
18
  description: "Interactive tutorials powered by WebContainer API",
20
19
  author: "StackBlitz Inc.",
21
20
  type: "module",
@@ -439,9 +438,9 @@ function replaceArgs(newTutorialKitArgs, ast) {
439
438
  const integrationImport = "@tutorialkit-rb/astro";
440
439
  let integrationId;
441
440
  visit(ast, {
442
- ImportDeclaration(path9) {
443
- if (path9.node.source.value === integrationImport) {
444
- const defaultImport = path9.node.specifiers.find((specifier) => specifier.type === "ImportDefaultSpecifier");
441
+ ImportDeclaration(path10) {
442
+ if (path10.node.source.value === integrationImport) {
443
+ const defaultImport = path10.node.specifiers.find((specifier) => specifier.type === "ImportDefaultSpecifier");
445
444
  if (defaultImport) {
446
445
  integrationId = defaultImport.local;
447
446
  }
@@ -452,11 +451,11 @@ function replaceArgs(newTutorialKitArgs, ast) {
452
451
  throw new Error(`Could not find import to '${integrationImport}'`);
453
452
  }
454
453
  visit(ast, {
455
- ExportDefaultDeclaration(path9) {
456
- if (!t.isCallExpression(path9.node.declaration)) {
454
+ ExportDefaultDeclaration(path10) {
455
+ if (!t.isCallExpression(path10.node.declaration)) {
457
456
  return;
458
457
  }
459
- const configObject = path9.node.declaration.arguments[0];
458
+ const configObject = path10.node.declaration.arguments[0];
460
459
  if (!t.isObjectExpression(configObject)) {
461
460
  throw new Error("TutorialKit is not part of the exported config");
462
461
  }
@@ -585,11 +584,101 @@ function validateEditorOrigin(value) {
585
584
  }
586
585
  }
587
586
 
588
- // src/commands/create/generate-hosting-config.ts
589
- import fs3 from "node:fs";
587
+ // src/commands/create/gemfile-editing.ts
590
588
  import path3 from "node:path";
591
589
  import * as prompts2 from "@clack/prompts";
592
590
  import chalk3 from "chalk";
591
+ import { execa } from "execa";
592
+
593
+ // src/commands/create/options.ts
594
+ import path2 from "node:path";
595
+ import { fileURLToPath } from "node:url";
596
+ var __dirname = path2.dirname(fileURLToPath(import.meta.url));
597
+ var templatePath = path2.resolve(__dirname, "../template");
598
+ var DEFAULT_VALUES = {
599
+ git: !process.env.CI,
600
+ install: true,
601
+ start: true,
602
+ dryRun: false,
603
+ force: false,
604
+ packageManager: "npm",
605
+ provider: "skip"
606
+ };
607
+ function readFlag(flags, flag) {
608
+ let value = flags[flag];
609
+ if (flags.defaults) {
610
+ value ??= DEFAULT_VALUES[flag];
611
+ }
612
+ return value;
613
+ }
614
+
615
+ // src/commands/create/gemfile-editing.ts
616
+ async function promptGemfileEditing(dest, flags) {
617
+ if (flags.defaults || flags.dryRun) {
618
+ return false;
619
+ }
620
+ prompts2.log.message(chalk3.bold.underline("Ruby Gems Configuration"));
621
+ prompts2.log.info(`Your Ruby application's gems are configured in ${chalk3.blue("ruby-wasm/Gemfile")}`);
622
+ const answer = await prompts2.confirm({
623
+ message: "Would you like to edit the Gemfile to add additional gems?",
624
+ initialValue: true
625
+ });
626
+ assertNotCanceled(answer);
627
+ if (answer) {
628
+ const gemfilePath = path3.resolve(dest, "ruby-wasm/Gemfile");
629
+ const editor = process.env.EDITOR;
630
+ if (editor) {
631
+ prompts2.log.message("");
632
+ prompts2.log.info(`Opening ${chalk3.blue("ruby-wasm/Gemfile")} in your editor...`);
633
+ try {
634
+ await execa(editor, [gemfilePath], { stdio: "inherit" });
635
+ prompts2.log.success("Great! Your Gemfile has been configured.");
636
+ return true;
637
+ } catch (error) {
638
+ prompts2.log.warn(`Failed to open editor: ${error.message}`);
639
+ prompts2.log.info("Falling back to manual editing instructions...");
640
+ }
641
+ }
642
+ prompts2.log.message("");
643
+ prompts2.log.step(`1. Open ${chalk3.blue(gemfilePath)} in your editor`);
644
+ prompts2.log.step(`2. Add any additional gems you need for your tutorial`);
645
+ prompts2.log.step(`3. Save the file and return here`);
646
+ prompts2.log.message("");
647
+ const ready = await prompts2.confirm({
648
+ message: "Have you finished editing the Gemfile?",
649
+ initialValue: true
650
+ });
651
+ assertNotCanceled(ready);
652
+ if (ready) {
653
+ prompts2.log.success("Great! Your Gemfile has been configured.");
654
+ return true;
655
+ }
656
+ }
657
+ return false;
658
+ }
659
+ function printRubyNextSteps(dest) {
660
+ let i = 0;
661
+ prompts2.log.message(chalk3.bold.underline("Next Steps"));
662
+ const steps = [
663
+ [`cd ${dest}`, "Navigate to project"],
664
+ ["npm run build:wasm", "Build Ruby WebAssembly with your gems"],
665
+ ["npm run dev", "Start development server"],
666
+ [, `Head over to ${chalk3.underline("http://localhost:4321")}`]
667
+ ];
668
+ for (const [command, text2] of steps) {
669
+ i++;
670
+ prompts2.log.step(`${i}. ${command ? `${chalk3.blue(command)} - ` : ""}${text2}`);
671
+ }
672
+ prompts2.log.message("");
673
+ prompts2.log.info("\u{1F4A1} The WASM build step may take several minutes the first time.");
674
+ prompts2.log.info("\u{1F4A1} Subsequent builds will be faster thanks to caching.");
675
+ }
676
+
677
+ // src/commands/create/generate-hosting-config.ts
678
+ import fs3 from "node:fs";
679
+ import path4 from "node:path";
680
+ import * as prompts3 from "@clack/prompts";
681
+ import chalk4 from "chalk";
593
682
 
594
683
  // src/commands/create/hosting-config/_headers.txt?raw
595
684
  var headers_default = "/*\n Cross-Origin-Embedder-Policy: require-corp\n Cross-Origin-Opener-Policy: same-origin\n";
@@ -616,33 +705,11 @@ var vercel_default = {
616
705
  ]
617
706
  };
618
707
 
619
- // src/commands/create/options.ts
620
- import path2 from "node:path";
621
- import { fileURLToPath } from "node:url";
622
- var __dirname = path2.dirname(fileURLToPath(import.meta.url));
623
- var templatePath = path2.resolve(__dirname, "../template");
624
- var DEFAULT_VALUES = {
625
- git: !process.env.CI,
626
- install: true,
627
- start: true,
628
- dryRun: false,
629
- force: false,
630
- packageManager: "npm",
631
- provider: "skip"
632
- };
633
- function readFlag(flags, flag) {
634
- let value = flags[flag];
635
- if (flags.defaults) {
636
- value ??= DEFAULT_VALUES[flag];
637
- }
638
- return value;
639
- }
640
-
641
708
  // src/commands/create/generate-hosting-config.ts
642
709
  async function generateHostingConfig(dest, flags) {
643
710
  let provider = readFlag(flags, "provider");
644
711
  if (provider === void 0) {
645
- provider = await prompts2.select({
712
+ provider = await prompts3.select({
646
713
  message: "Select hosting providers for automatic configuration:",
647
714
  options: [
648
715
  { value: "Vercel", label: "Vercel" },
@@ -657,13 +724,13 @@ async function generateHostingConfig(dest, flags) {
657
724
  provider = "skip";
658
725
  }
659
726
  if (!provider || provider === "skip") {
660
- prompts2.log.message(
661
- `${chalk3.blue("hosting provider config [skip]")} You can configure hosting provider settings manually later.`
727
+ prompts3.log.message(
728
+ `${chalk4.blue("hosting provider config [skip]")} You can configure hosting provider settings manually later.`
662
729
  );
663
730
  return provider;
664
731
  }
665
- prompts2.log.info(`${chalk3.blue("Hosting Configuration")} Setting up configuration for ${provider}`);
666
- const resolvedDest = path3.resolve(dest);
732
+ prompts3.log.info(`${chalk4.blue("Hosting Configuration")} Setting up configuration for ${provider}`);
733
+ const resolvedDest = path4.resolve(dest);
667
734
  if (!fs3.existsSync(resolvedDest)) {
668
735
  fs3.mkdirSync(resolvedDest, { recursive: true });
669
736
  }
@@ -692,7 +759,7 @@ async function generateHostingConfig(dest, flags) {
692
759
  dryRun: flags.dryRun,
693
760
  dryRunMessage: `${warnLabel("DRY RUN")} Skipped hosting provider config creation`,
694
761
  task: async () => {
695
- const filepath = path3.join(resolvedDest, filename);
762
+ const filepath = path4.join(resolvedDest, filename);
696
763
  fs3.writeFileSync(filepath, config);
697
764
  return `Added ${filepath}`;
698
765
  }
@@ -703,9 +770,9 @@ async function generateHostingConfig(dest, flags) {
703
770
 
704
771
  // src/commands/create/git.ts
705
772
  import fs4 from "node:fs";
706
- import path4 from "node:path";
707
- import * as prompts3 from "@clack/prompts";
708
- import chalk4 from "chalk";
773
+ import path5 from "node:path";
774
+ import * as prompts4 from "@clack/prompts";
775
+ import chalk5 from "chalk";
709
776
 
710
777
  // src/utils/shell.ts
711
778
  import { spawn } from "node:child_process";
@@ -751,7 +818,7 @@ async function runShellCommand(command, flags, opts = {}) {
751
818
  async function initGitRepo(cwd, flags) {
752
819
  let shouldInitGitRepo = readFlag(flags, "git");
753
820
  if (shouldInitGitRepo === void 0) {
754
- const answer = await prompts3.confirm({
821
+ const answer = await prompts4.confirm({
755
822
  message: "Initialize a new git repository?",
756
823
  initialValue: DEFAULT_VALUES.git
757
824
  });
@@ -769,12 +836,12 @@ async function initGitRepo(cwd, flags) {
769
836
  }
770
837
  });
771
838
  } else {
772
- prompts3.log.message(`${chalk4.blue("git [skip]")} You can always run ${chalk4.yellow("git init")} manually.`);
839
+ prompts4.log.message(`${chalk5.blue("git [skip]")} You can always run ${chalk5.yellow("git init")} manually.`);
773
840
  }
774
841
  }
775
842
  async function _initGitRepo(cwd) {
776
- if (fs4.existsSync(path4.join(cwd, ".git"))) {
777
- return `${chalk4.cyan("Nice!")} Git has already been initialized`;
843
+ if (fs4.existsSync(path5.join(cwd, ".git"))) {
844
+ return `${chalk5.cyan("Nice!")} Git has already been initialized`;
778
845
  }
779
846
  try {
780
847
  await runShellCommand("git", ["init"], { cwd, stdio: "ignore" });
@@ -793,42 +860,11 @@ async function _initGitRepo(cwd) {
793
860
  }
794
861
  }
795
862
 
796
- // src/commands/create/install-start.ts
797
- import * as prompts4 from "@clack/prompts";
798
- async function installAndStart(flags) {
799
- const installDeps = readFlag(flags, "install");
800
- const startProject2 = readFlag(flags, "start");
801
- if (installDeps === false) {
802
- return { install: false, start: false };
803
- }
804
- if (startProject2) {
805
- return { install: true, start: true };
806
- }
807
- if (installDeps) {
808
- if (startProject2 === false) {
809
- return { install: true, start: false };
810
- } else {
811
- const answer2 = await prompts4.confirm({
812
- message: "Start project?",
813
- initialValue: DEFAULT_VALUES.install
814
- });
815
- assertNotCanceled(answer2);
816
- return { install: true, start: answer2 };
817
- }
818
- }
819
- const answer = await prompts4.confirm({
820
- message: "Install dependencies and start project?",
821
- initialValue: DEFAULT_VALUES.install
822
- });
823
- assertNotCanceled(answer);
824
- return { install: answer, start: answer };
825
- }
826
-
827
863
  // src/commands/create/package-manager.ts
828
864
  import fs5 from "node:fs";
829
- import path5 from "node:path";
865
+ import path6 from "node:path";
830
866
  import * as prompts5 from "@clack/prompts";
831
- import chalk5 from "chalk";
867
+ import chalk6 from "chalk";
832
868
  import { lookpath } from "lookpath";
833
869
  var LOCK_FILES = /* @__PURE__ */ new Map([
834
870
  ["npm", "package-lock.json"],
@@ -839,7 +875,7 @@ async function selectPackageManager(cwd, flags) {
839
875
  const packageManager = await resolvePackageManager(flags);
840
876
  for (const [pkgManager, lockFile] of LOCK_FILES) {
841
877
  if (pkgManager !== packageManager) {
842
- fs5.rmSync(path5.join(cwd, lockFile), { force: true });
878
+ fs5.rmSync(path6.join(cwd, lockFile), { force: true });
843
879
  }
844
880
  }
845
881
  return packageManager;
@@ -850,7 +886,7 @@ async function resolvePackageManager(flags) {
850
886
  return flags.packageManager;
851
887
  }
852
888
  prompts5.log.warn(
853
- `The specified package manager '${chalk5.yellow(flags.packageManager)}' doesn't seem to be installed!`
889
+ `The specified package manager '${chalk6.yellow(flags.packageManager)}' doesn't seem to be installed!`
854
890
  );
855
891
  }
856
892
  if (flags.defaults) {
@@ -893,7 +929,7 @@ async function getInstalledPackageManagers() {
893
929
  // src/commands/create/template.ts
894
930
  import fs6 from "node:fs";
895
931
  import fsPromises from "node:fs/promises";
896
- import path6 from "node:path";
932
+ import path7 from "node:path";
897
933
  import * as prompts6 from "@clack/prompts";
898
934
  import ignore from "ignore";
899
935
  async function copyTemplate(dest, flags) {
@@ -911,9 +947,9 @@ async function copyTemplate(dest, flags) {
911
947
  toCopy.push(file);
912
948
  }
913
949
  for (const fileName of toCopy) {
914
- const sourceFilePath = path6.join(templatePath, fileName);
950
+ const sourceFilePath = path7.join(templatePath, fileName);
915
951
  const destFileName = fileName === ".npmignore" ? ".gitignore" : fileName;
916
- const destFilePath = path6.join(dest, destFileName);
952
+ const destFilePath = path7.join(dest, destFileName);
917
953
  const stats = await fsPromises.stat(sourceFilePath);
918
954
  if (stats.isDirectory()) {
919
955
  await fsPromises.cp(sourceFilePath, destFilePath, { recursive: true });
@@ -924,9 +960,9 @@ async function copyTemplate(dest, flags) {
924
960
  }
925
961
  function readIgnoreFile() {
926
962
  try {
927
- return fs6.readFileSync(path6.resolve(templatePath, ".npmignore"), "utf8");
963
+ return fs6.readFileSync(path7.resolve(templatePath, ".npmignore"), "utf8");
928
964
  } catch {
929
- return fs6.readFileSync(path6.resolve(templatePath, ".gitignore"), "utf8");
965
+ return fs6.readFileSync(path7.resolve(templatePath, ".gitignore"), "utf8");
930
966
  }
931
967
  }
932
968
 
@@ -940,25 +976,23 @@ async function createTutorial(flags) {
940
976
  tables: {
941
977
  Options: [
942
978
  ["--dir, -d", "The folder in which the tutorial gets created"],
943
- ["--install, --no-install", `Install dependencies (default ${chalk6.yellow(DEFAULT_VALUES.install)})`],
944
- ["--start, --no-start", `Start project (default ${chalk6.yellow(DEFAULT_VALUES.start)})`],
945
- ["--git, --no-git", `Initialize a local git repository (default ${chalk6.yellow(DEFAULT_VALUES.git)})`],
979
+ ["--git, --no-git", `Initialize a local git repository (default ${chalk7.yellow(DEFAULT_VALUES.git)})`],
946
980
  [
947
981
  "--provider <name>, --no-provider",
948
- `Select a hosting provider (default ${chalk6.yellow(DEFAULT_VALUES.provider)})`
982
+ `Select a hosting provider (default ${chalk7.yellow(DEFAULT_VALUES.provider)})`
949
983
  ],
950
- ["--dry-run", `Walk through steps without executing (default ${chalk6.yellow(DEFAULT_VALUES.dryRun)})`],
984
+ ["--dry-run", `Walk through steps without executing (default ${chalk7.yellow(DEFAULT_VALUES.dryRun)})`],
951
985
  [
952
986
  "--package-manager <name>, -p <name>",
953
- `The package used to install dependencies (default ${chalk6.yellow(DEFAULT_VALUES.packageManager)})`
987
+ `The package used to install dependencies (default ${chalk7.yellow(DEFAULT_VALUES.packageManager)})`
954
988
  ],
955
989
  [
956
990
  "--enterprise <origin>, -e <origin>",
957
- `The origin of your StackBlitz Enterprise instance (if not provided authentication is not turned on and your project will use ${chalk6.yellow("https://stackblitz.com")})`
991
+ `The origin of your StackBlitz Enterprise instance (if not provided authentication is not turned on and your project will use ${chalk7.yellow("https://stackblitz.com")})`
958
992
  ],
959
993
  [
960
994
  "--force",
961
- `Overwrite existing files in the target directory without prompting (default ${chalk6.yellow(DEFAULT_VALUES.force)})`
995
+ `Overwrite existing files in the target directory without prompting (default ${chalk7.yellow(DEFAULT_VALUES.force)})`
962
996
  ],
963
997
  ["--defaults", "Skip all prompts and initialize the tutorial using the defaults"]
964
998
  ]
@@ -967,12 +1001,6 @@ async function createTutorial(flags) {
967
1001
  return 0;
968
1002
  }
969
1003
  applyAliases(flags);
970
- try {
971
- verifyFlags(flags);
972
- } catch (error) {
973
- console.error(`${errorLabel()} ${error.message}`);
974
- process.exit(1);
975
- }
976
1004
  try {
977
1005
  return _createTutorial(flags);
978
1006
  } catch (error) {
@@ -1006,10 +1034,10 @@ async function _createTutorial(flags) {
1006
1034
  tutorialName = answer;
1007
1035
  }
1008
1036
  }
1009
- prompts7.log.info(`We'll call your tutorial ${chalk6.blue(tutorialName)}`);
1037
+ prompts7.log.info(`We'll call your tutorial ${chalk7.blue(tutorialName)}`);
1010
1038
  const dest = await getTutorialDirectory(tutorialName, flags);
1011
- const resolvedDest = path7.resolve(process.cwd(), dest);
1012
- prompts7.log.info(`Scaffolding tutorial in ${chalk6.blue(resolvedDest)}`);
1039
+ const resolvedDest = path8.resolve(process.cwd(), dest);
1040
+ prompts7.log.info(`Scaffolding tutorial in ${chalk7.blue(resolvedDest)}`);
1013
1041
  if (fs7.existsSync(resolvedDest) && !flags.force) {
1014
1042
  if (flags.defaults) {
1015
1043
  console.error(`
@@ -1044,32 +1072,11 @@ ${errorLabel()} Failed to create tutorial. Directory already exists.`);
1044
1072
  updateReadme(resolvedDest, selectedPackageManager, flags);
1045
1073
  await setupEnterpriseConfig(resolvedDest, flags);
1046
1074
  await initGitRepo(resolvedDest, flags);
1047
- const { install, start } = await installAndStart(flags);
1048
- prompts7.log.success(chalk6.green("Tutorial successfully created!"));
1049
- if (install || start) {
1050
- let message = "Please wait while we install the dependencies and start your project...";
1051
- if (install && !start) {
1052
- message = "Please wait while we install the dependencies...";
1053
- printNextSteps(dest, selectedPackageManager, true);
1054
- }
1055
- prompts7.outro(message);
1056
- await startProject(resolvedDest, selectedPackageManager, flags, start);
1057
- } else {
1058
- printNextSteps(dest, selectedPackageManager, false);
1059
- prompts7.outro(`You're all set!`);
1060
- console.log("Until next time \u{1F44B}");
1061
- }
1062
- }
1063
- async function startProject(cwd, packageManager, flags, startProject2) {
1064
- if (flags.dryRun) {
1065
- const message = startProject2 ? "Skipped dependency installation and project start" : "Skipped dependency installation";
1066
- console.warn(`${warnLabel("DRY RUN")} ${message}`);
1067
- } else {
1068
- await execa(packageManager, ["install"], { cwd, stdio: "inherit" });
1069
- if (startProject2) {
1070
- await execa(packageManager, ["run", "dev"], { cwd, stdio: "inherit" });
1071
- }
1072
- }
1075
+ prompts7.log.success(chalk7.green("Tutorial successfully created!"));
1076
+ await promptGemfileEditing(dest, flags);
1077
+ printRubyNextSteps(dest);
1078
+ prompts7.outro(`You're all set!`);
1079
+ console.log("Until next time \u{1F44B}");
1073
1080
  }
1074
1081
  async function getTutorialDirectory(tutorialName, flags) {
1075
1082
  const dir = flags.dir;
@@ -1084,7 +1091,7 @@ async function getTutorialDirectory(tutorialName, flags) {
1084
1091
  initialValue: `./${tutorialName}`,
1085
1092
  placeholder: "./",
1086
1093
  validate(value) {
1087
- if (!path7.isAbsolute(value) && !value.startsWith("./")) {
1094
+ if (!path8.isAbsolute(value) && !value.startsWith("./")) {
1088
1095
  return "Please provide an absolute or relative path!";
1089
1096
  }
1090
1097
  return void 0;
@@ -1093,28 +1100,11 @@ async function getTutorialDirectory(tutorialName, flags) {
1093
1100
  assertNotCanceled(promptResult);
1094
1101
  return promptResult;
1095
1102
  }
1096
- function printNextSteps(dest, packageManager, dependenciesInstalled) {
1097
- let i = 0;
1098
- prompts7.log.message(chalk6.bold.underline("Next Steps"));
1099
- const steps = [
1100
- [`cd ${dest}`, "Navigate to project"],
1101
- [`${packageManager} install`, "Install dependencies", !dependenciesInstalled],
1102
- [`${packageManager} run dev`, "Start development server"],
1103
- [, `Head over to ${chalk6.underline("http://localhost:4321")}`]
1104
- ];
1105
- for (const [command, text2, render] of steps) {
1106
- if (render === false) {
1107
- continue;
1108
- }
1109
- i++;
1110
- prompts7.log.step(`${i}. ${command ? `${chalk6.blue(command)} - ` : ""}${text2}`);
1111
- }
1112
- }
1113
1103
  function updatePackageJson(dest, projectName, flags, provider) {
1114
1104
  if (flags.dryRun) {
1115
1105
  return;
1116
1106
  }
1117
- const pkgPath = path7.resolve(dest, "package.json");
1107
+ const pkgPath = path8.resolve(dest, "package.json");
1118
1108
  const pkgJson = JSON.parse(fs7.readFileSync(pkgPath, "utf8"));
1119
1109
  pkgJson.name = projectName;
1120
1110
  updateWorkspaceVersions(pkgJson.dependencies, TUTORIALKIT_VERSION);
@@ -1125,7 +1115,7 @@ function updatePackageJson(dest, projectName, flags, provider) {
1125
1115
  }
1126
1116
  fs7.writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2));
1127
1117
  try {
1128
- const pkgLockPath = path7.resolve(dest, "package-lock.json");
1118
+ const pkgLockPath = path8.resolve(dest, "package-lock.json");
1129
1119
  const pkgLockJson = JSON.parse(fs7.readFileSync(pkgLockPath, "utf8"));
1130
1120
  const defaultPackage = pkgLockJson.packages[""];
1131
1121
  pkgLockJson.name = projectName;
@@ -1140,7 +1130,7 @@ function updateReadme(dest, packageManager, flags) {
1140
1130
  if (flags.dryRun) {
1141
1131
  return;
1142
1132
  }
1143
- const readmePath = path7.resolve(dest, "README.md");
1133
+ const readmePath = path8.resolve(dest, "README.md");
1144
1134
  let readme = fs7.readFileSync(readmePath, "utf8");
1145
1135
  readme = readme.replaceAll("<% pkgManager %>", packageManager ?? DEFAULT_VALUES.packageManager);
1146
1136
  fs7.writeFileSync(readmePath, readme);
@@ -1160,17 +1150,12 @@ function applyAliases(flags) {
1160
1150
  flags.enterprise = flags.e;
1161
1151
  }
1162
1152
  }
1163
- function verifyFlags(flags) {
1164
- if (flags.install === false && flags.start) {
1165
- throw new Error("Cannot start project without installing dependencies.");
1166
- }
1167
- }
1168
1153
 
1169
1154
  // src/commands/eject/index.ts
1170
1155
  import fs8 from "node:fs";
1171
- import path8 from "node:path";
1156
+ import path9 from "node:path";
1172
1157
  import * as prompts8 from "@clack/prompts";
1173
- import chalk7 from "chalk";
1158
+ import chalk8 from "chalk";
1174
1159
  import detectIndent from "detect-indent";
1175
1160
  import { execa as execa2 } from "execa";
1176
1161
  import whichpm from "which-pm";
@@ -1201,7 +1186,7 @@ function ejectRoutes(flags) {
1201
1186
  Options: [
1202
1187
  [
1203
1188
  "--force",
1204
- `Overwrite existing files in the target directory without prompting (default ${chalk7.yellow(DEFAULT_VALUES2.force)})`
1189
+ `Overwrite existing files in the target directory without prompting (default ${chalk8.yellow(DEFAULT_VALUES2.force)})`
1205
1190
  ],
1206
1191
  ["--defaults", "Skip all the prompts and eject the routes using the defaults"]
1207
1192
  ]
@@ -1225,7 +1210,7 @@ async function _eject(flags) {
1225
1210
  if (folderPath === void 0) {
1226
1211
  folderPath = process.cwd();
1227
1212
  } else {
1228
- folderPath = path8.resolve(process.cwd(), folderPath);
1213
+ folderPath = path9.resolve(process.cwd(), folderPath);
1229
1214
  }
1230
1215
  const { astroConfigPath, srcPath, pkgJsonPath, astroIntegrationPath, srcDestPath } = validateDestination(
1231
1216
  folderPath,
@@ -1239,7 +1224,7 @@ async function _eject(flags) {
1239
1224
  const indent = detectIndent(pkgJsonContent).indent || " ";
1240
1225
  const pkgJson = JSON.parse(pkgJsonContent);
1241
1226
  const astroIntegrationPkgJson = JSON.parse(
1242
- fs8.readFileSync(path8.join(astroIntegrationPath, "package.json"), "utf-8")
1227
+ fs8.readFileSync(path9.join(astroIntegrationPath, "package.json"), "utf-8")
1243
1228
  );
1244
1229
  const newDependencies = [];
1245
1230
  for (const dep of REQUIRED_DEPENDENCIES) {
@@ -1260,9 +1245,9 @@ async function _eject(flags) {
1260
1245
  `New dependencies added: ${newDependencies.join(", ")}. Install the new dependencies before proceeding.`
1261
1246
  );
1262
1247
  if (!flags.defaults) {
1263
- const packageManager = (await whichpm(path8.dirname(pkgJsonPath))).name;
1248
+ const packageManager = (await whichpm(path9.dirname(pkgJsonPath))).name;
1264
1249
  const answer = await prompts8.confirm({
1265
- message: `Do you want to install those dependencies now using ${chalk7.blue(packageManager)}?`
1250
+ message: `Do you want to install those dependencies now using ${chalk8.blue(packageManager)}?`
1266
1251
  });
1267
1252
  if (answer === true) {
1268
1253
  await execa2(packageManager, ["install"], { cwd: folderPath, stdio: "inherit" });
@@ -1273,17 +1258,17 @@ async function _eject(flags) {
1273
1258
  }
1274
1259
  function validateDestination(folder, force) {
1275
1260
  assertExists(folder);
1276
- const pkgJsonPath = assertExists(path8.join(folder, "package.json"));
1277
- const astroConfigPath = assertExists(path8.join(folder, "astro.config.ts"));
1278
- const srcDestPath = assertExists(path8.join(folder, "src"));
1279
- const astroIntegrationPath = assertExists(path8.resolve(folder, "node_modules", "@tutorialkit-rb", "astro"));
1280
- const srcPath = path8.join(astroIntegrationPath, "dist", "default");
1261
+ const pkgJsonPath = assertExists(path9.join(folder, "package.json"));
1262
+ const astroConfigPath = assertExists(path9.join(folder, "astro.config.ts"));
1263
+ const srcDestPath = assertExists(path9.join(folder, "src"));
1264
+ const astroIntegrationPath = assertExists(path9.resolve(folder, "node_modules", "@tutorialkit-rb", "astro"));
1265
+ const srcPath = path9.join(astroIntegrationPath, "dist", "default");
1281
1266
  if (!force) {
1282
1267
  walk(srcPath, (relativePath) => {
1283
- const destination = path8.join(srcDestPath, relativePath);
1268
+ const destination = path9.join(srcDestPath, relativePath);
1284
1269
  if (fs8.existsSync(destination)) {
1285
1270
  throw new Error(
1286
- `Eject aborted because '${destination}' would be overwritten by this command. Use ${chalk7.yellow("--force")} to ignore this error.`
1271
+ `Eject aborted because '${destination}' would be overwritten by this command. Use ${chalk8.yellow("--force")} to ignore this error.`
1287
1272
  );
1288
1273
  }
1289
1274
  });
@@ -1305,9 +1290,9 @@ function assertExists(filePath) {
1305
1290
  function walk(root, visit2) {
1306
1291
  function traverse2(folder, pathPrefix) {
1307
1292
  for (const filename of fs8.readdirSync(folder)) {
1308
- const filePath = path8.join(folder, filename);
1293
+ const filePath = path9.join(folder, filename);
1309
1294
  const stat = fs8.statSync(filePath);
1310
- const relativeFilePath = path8.join(pathPrefix, filename);
1295
+ const relativeFilePath = path9.join(pathPrefix, filename);
1311
1296
  if (stat.isDirectory()) {
1312
1297
  traverse2(filePath, relativeFilePath);
1313
1298
  } else {
@@ -1336,13 +1321,13 @@ async function cli() {
1336
1321
  async function runCommand(cmd, flags) {
1337
1322
  switch (cmd) {
1338
1323
  case "version": {
1339
- console.log(`${primaryLabel(package_default.name)} ${chalk8.green(`v${package_default.version}`)}`);
1324
+ console.log(`${primaryLabel(package_default.name)} ${chalk9.green(`v${package_default.version}`)}`);
1340
1325
  return;
1341
1326
  }
1342
1327
  case "help": {
1343
1328
  printHelp({
1344
1329
  commandName: package_default.name,
1345
- prolog: `${primaryLabel(package_default.name)} ${chalk8.green(`v${package_default.version}`)} Create tutorial apps powered by WebContainer API`,
1330
+ prolog: `${primaryLabel(package_default.name)} ${chalk9.green(`v${package_default.version}`)} Create tutorial apps powered by WebContainer API`,
1346
1331
  usage: ["[command] [...options]", "[ -h | --help | -v | --version ]"],
1347
1332
  tables: {
1348
1333
  Commands: [
@@ -1364,7 +1349,7 @@ async function runCommand(cmd, flags) {
1364
1349
  return ejectRoutes(flags);
1365
1350
  }
1366
1351
  default: {
1367
- console.error(`${errorLabel()} Unknown command ${chalk8.red(cmd)}`);
1352
+ console.error(`${errorLabel()} Unknown command ${chalk9.red(cmd)}`);
1368
1353
  return 1;
1369
1354
  }
1370
1355
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tutorialkit-rb/cli",
3
- "version": "1.5.2-rb.0.1.0",
3
+ "version": "1.5.2-rb.0.1.1",
4
4
  "description": "Interactive tutorials powered by WebContainer API",
5
5
  "author": "StackBlitz Inc.",
6
6
  "type": "module",
@@ -11,3 +11,4 @@ pnpm-debug.log*
11
11
  .idea
12
12
 
13
13
  public/ruby.wasm
14
+ public/ruby.wasm.hash
@@ -24,7 +24,17 @@ echo "Building WASM module..."
24
24
  echo "WASM build completed successfully!"
25
25
  echo "ruby.wasm is now available at ruby-wasm/dist/ruby.wasm"
26
26
 
27
+ # Generate hash of Gemfile.lock for cache versioning
28
+ echo "Generating Gemfile.lock hash..."
29
+ GEMFILE_HASH=$(sha256sum Gemfile.lock | cut -c1-16)
30
+ echo "Gemfile.lock hash: $GEMFILE_HASH"
31
+
27
32
  # Copy the built ruby.wasm to the public directory
28
33
  echo "Copying ruby.wasm to public directory..."
29
34
  cp dist/ruby.wasm ../public/
30
35
  echo "ruby.wasm copied to public directory."
36
+
37
+ # Create ruby.wasm.hash with the hash
38
+ echo "Creating ruby.wasm.hash..."
39
+ echo "$GEMFILE_HASH" > ../public/ruby.wasm.hash
40
+ echo "ruby.wasm.hash created with hash: $GEMFILE_HASH"
@@ -15,7 +15,7 @@
15
15
  "@codemirror/lang-yaml": "^6.1.2",
16
16
  "@codemirror/legacy-modes": "^6.5.1",
17
17
  "@nanostores/react": "0.7.2",
18
- "@tutorialkit-rb/react": "1.5.2-rb.0.1.0",
18
+ "@tutorialkit-rb/react": "1.5.2-rb.0.1.1",
19
19
  "nanostores": "^0.10.3",
20
20
  "react": "^18.3.1",
21
21
  "react-dom": "^18.3.1"
@@ -23,9 +23,9 @@
23
23
  "devDependencies": {
24
24
  "@astrojs/check": "^0.7.0",
25
25
  "@astrojs/react": "^3.6.0",
26
- "@tutorialkit-rb/astro": "1.5.2-rb.0.1.0",
27
- "@tutorialkit-rb/theme": "1.5.2-rb.0.1.0",
28
- "@tutorialkit-rb/types": "1.5.2-rb.0.1.0",
26
+ "@tutorialkit-rb/astro": "1.5.2-rb.0.1.1",
27
+ "@tutorialkit-rb/theme": "1.5.2-rb.0.1.1",
28
+ "@tutorialkit-rb/types": "1.5.2-rb.0.1.1",
29
29
  "@types/mdast": "^4.0.4",
30
30
  "@types/node": "^20.14.6",
31
31
  "@types/react": "^18.3.3",
@@ -5,7 +5,7 @@ import { webcontainer } from 'tutorialkit:core';
5
5
  import tutorialStore from 'tutorialkit:store';
6
6
 
7
7
  const VERSIONED_WASM_URL = `/ruby.wasm`;
8
- const WASM_CACHE_FILE_NAME = `ruby.wasm`;
8
+ const GEMFILE_HASH_URL = `/ruby.wasm.hash`;
9
9
  const WC_WASM_LOG_PATH = `/ruby.wasm.log.txt`;
10
10
  const WC_WASM_PATH = `/ruby.wasm`;
11
11
 
@@ -15,6 +15,31 @@ export function FileManager() {
15
15
  const processedFiles = useRef(new Set<string>());
16
16
  const wasmCached = useRef(false);
17
17
 
18
+ async function fetchGemfileHash(): Promise<string> {
19
+ try {
20
+ console.log(`Fetching Gemfile hash from ${GEMFILE_HASH_URL}...`);
21
+
22
+ const response = await fetch(GEMFILE_HASH_URL);
23
+
24
+ if (!response.ok) {
25
+ console.warn(`Failed to fetch ruby.wasm.hash: ${response.status}`);
26
+ return 'default';
27
+ }
28
+
29
+ const hash = (await response.text()).trim();
30
+ console.log(`Fetched Gemfile hash: ${hash}`);
31
+
32
+ return hash;
33
+ } catch (error) {
34
+ console.warn('Failed to fetch Gemfile hash, using default version:', error);
35
+ return 'default';
36
+ }
37
+ }
38
+
39
+ function getVersionedCacheFileName(gemfileLockHash: string): string {
40
+ return `ruby-${gemfileLockHash}.wasm`;
41
+ }
42
+
18
43
  async function chmodx(wc: WebContainer, path: string) {
19
44
  const process = await wc.spawn('chmod', ['+x', path]);
20
45
 
@@ -27,12 +52,12 @@ export function FileManager() {
27
52
  }
28
53
  }
29
54
 
30
- async function fetchCachedWasmFile(): Promise<Uint8Array | null> {
55
+ async function fetchCachedWasmFile(cacheFileName: string): Promise<Uint8Array | null> {
31
56
  try {
32
57
  const opfsRoot = await navigator.storage.getDirectory();
33
- const fileHandle = await opfsRoot.getFileHandle(WASM_CACHE_FILE_NAME);
58
+ const fileHandle = await opfsRoot.getFileHandle(cacheFileName);
34
59
  const file = await fileHandle.getFile();
35
- console.log(`Found cached Ruby WASM: ${WASM_CACHE_FILE_NAME}`);
60
+ console.log(`Found cached Ruby WASM: ${cacheFileName}`);
36
61
 
37
62
  return new Uint8Array(await file.arrayBuffer());
38
63
  } catch {
@@ -40,20 +65,38 @@ export function FileManager() {
40
65
  }
41
66
  }
42
67
 
43
- async function persistWasmFile(wasmData: Uint8Array): Promise<void> {
68
+ async function persistWasmFile(wasmData: Uint8Array, cacheFileName: string): Promise<void> {
44
69
  try {
45
70
  const opfsRoot = await navigator.storage.getDirectory();
46
- const fileHandle = await opfsRoot.getFileHandle(WASM_CACHE_FILE_NAME, { create: true });
71
+ const fileHandle = await opfsRoot.getFileHandle(cacheFileName, { create: true });
47
72
  const writable = await fileHandle.createWritable();
48
73
  await writable.write(wasmData);
49
74
  await writable.close();
50
- console.log(`Ruby WASM file ${WASM_CACHE_FILE_NAME} cached`);
75
+ console.log(`Ruby WASM file ${cacheFileName} cached`);
51
76
  } catch (error) {
52
77
  console.error('Failed to persist Ruby WASM:', error);
53
78
  }
54
79
  }
55
80
 
56
- async function cacheWasmFile(wc: WebContainer): Promise<void> {
81
+ async function cleanupOldCacheFiles(currentCacheFileName: string): Promise<void> {
82
+ try {
83
+ const opfsRoot = await navigator.storage.getDirectory();
84
+
85
+ for await (const [name] of opfsRoot.entries()) {
86
+ if (
87
+ ((name.startsWith('ruby-') && name.endsWith('.wasm')) || name === 'ruby.wasm') &&
88
+ name !== currentCacheFileName
89
+ ) {
90
+ console.log(`Removing old cached Ruby WASM: ${name}`);
91
+ await opfsRoot.removeEntry(name);
92
+ }
93
+ }
94
+ } catch (error) {
95
+ console.warn('Failed to cleanup old cache files:', error);
96
+ }
97
+ }
98
+
99
+ async function cacheWasmFile(wc: WebContainer, cacheFileName: string): Promise<void> {
57
100
  console.log(`Dowloading WASM file ${VERSIONED_WASM_URL}...`);
58
101
 
59
102
  try {
@@ -61,10 +104,11 @@ export function FileManager() {
61
104
  await wc.fs.writeFile(WC_WASM_LOG_PATH, 'status: downloaded');
62
105
 
63
106
  const wasmData = new Uint8Array(await wasm.arrayBuffer());
64
- await persistWasmFile(wasmData);
107
+ await persistWasmFile(wasmData, cacheFileName);
108
+ await cleanupOldCacheFiles(cacheFileName);
65
109
  await wc.fs.writeFile(WC_WASM_LOG_PATH, 'status: cached');
66
110
  await wc.fs.writeFile(WC_WASM_PATH, wasmData);
67
- } catch (error) {
111
+ } catch {
68
112
  await wc.fs.writeFile(WC_WASM_LOG_PATH, 'status: error');
69
113
  }
70
114
  }
@@ -93,18 +137,23 @@ export function FileManager() {
93
137
  if (!wasmCached.current) {
94
138
  await wc.fs.writeFile(WC_WASM_LOG_PATH, 'status: init');
95
139
 
96
- const cachedWasm = await fetchCachedWasmFile();
140
+ const gemfileLockHash = await fetchGemfileHash();
141
+ const cacheFileName = getVersionedCacheFileName(gemfileLockHash);
142
+ console.log(`Using cache file: ${cacheFileName} (hash: ${gemfileLockHash})`);
143
+
144
+ const cachedWasm = await fetchCachedWasmFile(cacheFileName);
97
145
 
98
146
  if (cachedWasm) {
99
147
  await wc.fs.writeFile(WC_WASM_LOG_PATH, 'status: load from cache');
100
148
  await wc.fs.writeFile(WC_WASM_PATH, cachedWasm);
101
149
  await wc.fs.writeFile(WC_WASM_LOG_PATH, 'status: done');
102
- console.log(`Ruby WASM ${WASM_CACHE_FILE_NAME} loaded from cache`);
150
+ console.log(`Ruby WASM ${cacheFileName} loaded from cache`);
103
151
  wasmCached.current = true;
104
152
  } else {
105
153
  await wc.fs.writeFile(WC_WASM_LOG_PATH, 'status: download');
106
- await cacheWasmFile(wc);
154
+ await cacheWasmFile(wc, cacheFileName);
107
155
  await wc.fs.writeFile(WC_WASM_LOG_PATH, 'status: done');
156
+ wasmCached.current = true;
108
157
  }
109
158
  }
110
159
  })();