create-inferative 0.2.0-next.20261023154401 → 0.2.0

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/README.md CHANGED
@@ -41,7 +41,7 @@ Add the token line to your `~/.npmrc`. If the token is missing, step 2 stops and
41
41
 
42
42
  | Flag | What it does |
43
43
  | :-- | :-- |
44
- | `--starter <name>` | The starter to use: `nuxt-cloudflare` (a client site, the default) or `dashboard-cloudflare` (the dashboard). |
44
+ | `--starter <name>` | The starter to use: `nuxt-cloudflare` (a client site, the default), `dashboard-cloudflare` (the dashboard) or `nuxt-sdk` (a website with its own server that reads a deployment with the SDK). |
45
45
  | `--source <giget source>` | Download this source instead of the default starters. |
46
46
  | `--local <folder>` | Copy the starter from this starters folder. Nothing is downloaded. |
47
47
  | `--no-install` | Do not run `pnpm install`. |
package/dist/bin.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as CreateFrontDoor } from "./front-door-CGKOn3bI.mjs";
2
+ import { t as CreateFrontDoor } from "./front-door-C_ckNreK.mjs";
3
3
  import { runMain } from "citty";
4
4
  //#region src/bin.ts
5
5
  const argv = CreateFrontDoor.rawArgs(process.argv.slice(2));
@@ -3,7 +3,8 @@ import { defineCommand } from "citty";
3
3
  import { basename, dirname, join, relative, resolve } from "node:path";
4
4
  import { installDependencies } from "nypm";
5
5
  import { x } from "tinyexec";
6
- import { cp, mkdir, readFile, readdir, rm, rmdir, stat, writeFile } from "node:fs/promises";
6
+ import { cp, mkdir, mkdtemp, readFile, readdir, rm, rmdir, stat, writeFile } from "node:fs/promises";
7
+ import { tmpdir } from "node:os";
7
8
  //#region src/package-version.ts
8
9
  /**
9
10
  * The version in this package's own `package.json`.
@@ -113,10 +114,13 @@ var InitMessages = class InitMessages {
113
114
  static devCommand = "pnpm dev";
114
115
  /** The command that writes the first migration of a starter. A starter commits no migration. */
115
116
  static generateCommand = "pnpm db:generate";
117
+ /** The starters with no database of their own, so no migration step. */
118
+ static startersWithoutDatabase = /* @__PURE__ */ new Set(["nuxt-sdk"]);
116
119
  /** The last line of the next steps for each starter: what its `README.md` explains first. */
117
120
  static firstRunLines = {
118
121
  "nuxt-cloudflare": "Fill in the values of .env first. README.md in the project explains the first run, with its first manifest sync (pnpm sync:manifest --first-sync).",
119
- "dashboard-cloudflare": "Fill in the values of .env first. README.md in the project explains the secrets, the Cloudflare resources and the first run."
122
+ "dashboard-cloudflare": "Fill in the values of .env first. README.md in the project explains the secrets, the Cloudflare resources and the first run.",
123
+ "nuxt-sdk": "Fill in the values of .env first. README.md in the project explains the channel, its two keys and the manifest sync (pnpm sync:manifest)."
120
124
  };
121
125
  /** The last line of the next steps for a starter that {@link firstRunLines} does not name. */
122
126
  static firstRunLine = "Fill in the values of .env first. README.md in the project explains the first run.";
@@ -136,7 +140,7 @@ var InitMessages = class InitMessages {
136
140
  ...changeDirectory === void 0 ? [] : [`cd ${InitMessages.quote(changeDirectory)}`],
137
141
  ...isInstalled ? [] : [`${DependencyInstaller.command} install`],
138
142
  "cp .env.example .env",
139
- InitMessages.generateCommand,
143
+ ...InitMessages.startersWithoutDatabase.has(starter) ? [] : [InitMessages.generateCommand],
140
144
  InitMessages.devCommand
141
145
  ].map((step) => ` ${step}`),
142
146
  InitMessages.firstRunLines[starter] ?? InitMessages.firstRunLine
@@ -610,7 +614,7 @@ var NpmrcScopes = class NpmrcScopes {
610
614
  };
611
615
  //#endregion
612
616
  //#region src/init/starter-source.ts
613
- /** A download that failed. Only this failure makes {@link StarterSource.fetch} try the next source. */
617
+ /** A download that failed or wrote no file. Only this failure makes {@link StarterSource.fetch} try the next source. */
614
618
  var StarterDownloadError = class StarterDownloadError extends InitError {
615
619
  /** The giget sources that failed. */
616
620
  sources;
@@ -632,6 +636,31 @@ var StarterDownloadError = class StarterDownloadError extends InitError {
632
636
  return new StarterDownloadError([source], cause instanceof Error ? cause.message : String(cause), { cause });
633
637
  }
634
638
  };
639
+ /** No source holds the starter: each download worked and wrote no file. */
640
+ var StarterMissingError = class StarterMissingError extends InitError {
641
+ /** The giget sources that were tried. */
642
+ sources;
643
+ /** The starters that the repository holds, or `undefined` when they could not be read. */
644
+ listing;
645
+ constructor(sources, listing) {
646
+ super(StarterMissingError.describe(sources, listing));
647
+ this.name = "StarterMissingError";
648
+ this.sources = sources;
649
+ this.listing = listing;
650
+ }
651
+ /** The message: the starter, the repository and the refs tried, then the starters that exist. */
652
+ static describe(sources, listing) {
653
+ const locations = sources.map((source) => StarterSource.locate(source));
654
+ const [first] = locations;
655
+ const missing = first !== void 0 && locations.every((location) => location !== void 0 && location.repository === first.repository && location.folder === first.folder) ? `The starter "${first.folder}" does not exist in ${first.repository} (tried ${locations.map((location) => StarterMissingError.refName(location?.ref)).join(", ")}).` : `No source holds the starter: ${sources.join(", ")}.`;
656
+ if (listing === void 0) return missing;
657
+ const at = StarterMissingError.refName(listing.ref);
658
+ return listing.starters.length === 0 ? `${missing} The repository holds no starter at ${at}.` : `${missing} The starters at ${at}: ${listing.starters.join(", ")}.`;
659
+ }
660
+ static refName(ref) {
661
+ return ref === void 0 ? "the default branch" : `#${ref}`;
662
+ }
663
+ };
635
664
  /** Fetches the files of a starter. */
636
665
  var StarterSource = class StarterSource {
637
666
  /** The public repository that holds the starters, one folder for each. */
@@ -641,11 +670,19 @@ var StarterSource = class StarterSource {
641
670
  /** The starters of the starters repository, by folder name, with what each one creates. */
642
671
  static starters = {
643
672
  "nuxt-cloudflare": "a client site: the public site and its dashboard on one Worker",
644
- "dashboard-cloudflare": "the Inferative dashboard on one Worker"
673
+ "dashboard-cloudflare": "the Inferative dashboard on one Worker",
674
+ "nuxt-sdk": "a website with its own server that reads a deployment with the SDK, on Node"
645
675
  };
646
676
  /** The branch that the default sources fall back to. */
647
677
  static fallbackBranch = "main";
648
678
  static starterNamePattern = /^[a-z0-9][a-z0-9-]*$/u;
679
+ /** Why a source that wrote no file failed. */
680
+ static missingReason = "the starter folder does not exist there";
681
+ static locationPattern = /^(?<provider>[\w+.-]+):(?<path>[^#]+)(?:#(?<ref>.+))?$/u;
682
+ /** The giget sources that are a URL, not a repository path: the error names them whole. */
683
+ static urlProviders = /* @__PURE__ */ new Set(["http", "https"]);
684
+ /** The giget providers whose repository path may have more than two segments (GitLab subgroups). */
685
+ static nestedRepositoryProviders = /* @__PURE__ */ new Set(["gitlab"]);
649
686
  download;
650
687
  /** @param download - The download function. It calls giget by default. */
651
688
  constructor(download = (source, directory) => StarterSource.downloadWithGiget(source, directory)) {
@@ -668,6 +705,37 @@ var StarterSource = class StarterSource {
668
705
  return Object.entries(StarterSource.starters).map(([name, creates]) => `${name} (${creates})`).join(", ");
669
706
  }
670
707
  /**
708
+ * Splits a giget source of a starter folder, such as `gh:inferative/starters/nuxt-cloudflare#main`.
709
+ *
710
+ * @returns The parts, or `undefined` when the source names no folder in a repository.
711
+ */
712
+ static locate(source) {
713
+ const groups = StarterSource.locationPattern.exec(source)?.groups;
714
+ const provider = groups?.["provider"];
715
+ const path = groups?.["path"];
716
+ if (provider === void 0 || path === void 0) return;
717
+ const split = StarterSource.splitPath(provider, path);
718
+ return split === void 0 ? void 0 : {
719
+ repository: `${provider}:${split.repository}`,
720
+ folder: split.folder,
721
+ ref: groups?.["ref"]
722
+ };
723
+ }
724
+ /**
725
+ * Splits the path of a giget source as giget does: `::` ends the repository path; without it, a GitLab path is
726
+ * the repository only, and any other path has a repository of two segments.
727
+ */
728
+ static splitPath(provider, path) {
729
+ const separator = path.indexOf("::");
730
+ const [repository, folder] = separator >= 0 ? [path.slice(0, separator), path.slice(separator + 2)] : StarterSource.nestedRepositoryProviders.has(provider) ? [path, ""] : [path.split("/").slice(0, 2).join("/"), path.split("/").slice(2).join("/")];
731
+ const trimmed = folder.replace(/^\/+|\/+$/gu, "");
732
+ const segments = repository.split("/");
733
+ return StarterSource.urlProviders.has(provider) || segments.length < 2 || segments.includes("") || trimmed === "" ? void 0 : {
734
+ repository,
735
+ folder: trimmed
736
+ };
737
+ }
738
+ /**
671
739
  * Checks a starter name, which becomes a path segment.
672
740
  *
673
741
  * @throws {InitError} When the name is not a plain lowercase folder name.
@@ -678,24 +746,35 @@ var StarterSource = class StarterSource {
678
746
  /**
679
747
  * Downloads the starter from the first source that works.
680
748
  *
681
- * Only a failed download moves on to the next source. Any other error stops the run, because the next
682
- * source would fail the same way.
749
+ * Only a failed download, or one that wrote no file, moves on to the next source. Any other error stops the
750
+ * run, because the next source would fail the same way. Giget does not fail for a folder that the repository
751
+ * does not hold: it writes nothing.
683
752
  *
684
753
  * @param sources - The giget sources, in order.
685
754
  * @param directory - The folder to fill.
686
755
  * @param resetDirectory - Called before each later source, to remove the half-written files of the last one.
687
756
  * @returns The source that worked.
757
+ * @throws {StarterMissingError} When no source holds the starter. The message names the starters that exist.
688
758
  * @throws {StarterDownloadError} When every source failed. The message holds each failure.
689
759
  */
690
760
  async fetch(sources, directory, resetDirectory) {
691
761
  const failures = [];
692
- for (const source of sources) try {
693
- await this.download(source, directory);
694
- return source;
695
- } catch (error) {
696
- failures.push(StarterDownloadError.of(source, error));
697
- await resetDirectory();
762
+ const missing = [];
763
+ for (const source of sources) {
764
+ const before = new Set(await StarterSource.entryNames(directory));
765
+ try {
766
+ await this.download(source, directory);
767
+ } catch (error) {
768
+ failures.push(StarterDownloadError.of(source, error));
769
+ await resetDirectory();
770
+ continue;
771
+ }
772
+ if ((await StarterSource.entryNames(directory)).some((entry) => !before.has(entry))) return source;
773
+ missing.push(source);
774
+ failures.push(new StarterDownloadError([source], StarterSource.missingReason));
698
775
  }
776
+ const lastMissing = missing.at(-1);
777
+ if (lastMissing !== void 0 && missing.length === sources.length) throw new StarterMissingError(sources, await this.listStarters(lastMissing));
699
778
  const [first] = failures;
700
779
  if (failures.length === 1 && first !== void 0) throw first;
701
780
  throw new StarterDownloadError(sources, `Tried ${failures.map((failure) => `${failure.sources.join("")} (${failure.reason})`).join(", then ")}.`);
@@ -722,6 +801,43 @@ var StarterSource = class StarterSource {
722
801
  });
723
802
  return source;
724
803
  }
804
+ /**
805
+ * Reads the starter folders of the repository at the ref of a source, from a download of the repository root
806
+ * into a temporary folder. The starters repository is small, so the second download is cheap.
807
+ *
808
+ * @returns The listing, or `undefined` when the source names no repository folder or the download fails.
809
+ */
810
+ async listStarters(source) {
811
+ const location = StarterSource.locate(source);
812
+ if (location === void 0) return;
813
+ const root = location.ref === void 0 ? location.repository : `${location.repository}#${location.ref}`;
814
+ let directory;
815
+ try {
816
+ directory = await mkdtemp(join(tmpdir(), "inferative-starters-"));
817
+ await this.download(root, directory);
818
+ const starters = (await readdir(directory, { withFileTypes: true })).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name).sort();
819
+ return {
820
+ ref: location.ref,
821
+ starters
822
+ };
823
+ } catch {
824
+ return;
825
+ } finally {
826
+ if (directory !== void 0) await rm(directory, {
827
+ recursive: true,
828
+ force: true
829
+ }).catch(() => void 0);
830
+ }
831
+ }
832
+ /** The entries of a folder, or none when it does not exist. */
833
+ static async entryNames(directory) {
834
+ try {
835
+ return await readdir(directory);
836
+ } catch (error) {
837
+ if (ErrorCode.is(error, "ENOENT")) return [];
838
+ throw error;
839
+ }
840
+ }
725
841
  /** The default {@link StarterDownload}. It loads giget on first use, so a local copy needs no giget. */
726
842
  static async downloadWithGiget(source, directory) {
727
843
  const { downloadTemplate } = await import("giget");
@@ -6,7 +6,7 @@
6
6
  "../../tools/vitest/build-inputs.ts": "HO104hfJGDCrhwXLsy7I55ONi1PnjtDA6_gpYQ1gYlI",
7
7
  "../../tools/vitest/measure-lane.ts": "VKf7uKWSoz3vYvCbLFw76YSksMT_E41akYmYdWIeFbE",
8
8
  "../../tsconfig.base.json": "w6D6cbSUqUPq2R3SoBAQReeRQ-kQPVfQbddoBcybh-0",
9
- "package.json": "7Ta-mRHwaztqLYBbu3Ez2zG3lxWpM0fDIgP9-yXLt3U",
9
+ "package.json": "klm0tpBWEWBj7nJ7Y5DUAuFdn0x5AqSvgwo3quoyJ40",
10
10
  "src/bin.ts": "IjsGZUVikS80RJn9Bo_1czPmrIh-J2cH4vVLZVmfggQ",
11
11
  "src/error-code.ts": "5ollpvBpNVAGGvd0Jfic7xhNc0B_nGN9au8G7OqMIq4",
12
12
  "src/front-door.ts": "AvZQP1lKw9zodvbNmg9HIdB8d1pfJx7MsrbBoO_bzlc",
@@ -14,12 +14,12 @@
14
14
  "src/init/dependency-installer.ts": "jVTa8spFjggsWLUnSn9Vzvl4D-B2EYwRmpsVaBTCJaI",
15
15
  "src/init/init-command.ts": "CWR-yuf-vglYxt2RiNqr5GCoEIiEO0ekvK3nCS20inU",
16
16
  "src/init/init-error.ts": "Bdd0MQ9JvVwNHnzFH6aQnA1HiM_mVOsO0utGervvUsQ",
17
- "src/init/init-messages.ts": "a5mcbK_R0bKyH2ohyOaMQzJH8WNEFfHz9t-zy_hrQso",
17
+ "src/init/init-messages.ts": "r9kO19rAH-xgu-U2ty5vVMOPzywWe7z5TzsETbvzvZk",
18
18
  "src/init/init-target.ts": "9UaRdl0vf6nVsgHJ5BHFcIIcbScWsocDol9zFf5_Kqc",
19
19
  "src/init/manifest-stamper.ts": "0ANMdqEvOUbAYwiihoyIExim9fc0mIcjAkCfw840bfE",
20
20
  "src/init/npmrc-scopes.ts": "aRIFEP9-WVq67QPsVjVP8HPPZFltMsKu-xXMFgJbLK4",
21
21
  "src/init/project-initializer.ts": "NW9IsMY0Rx1vaDSGNkWUyCxDQU0V6UksrCQdfLK-RSk",
22
- "src/init/starter-source.ts": "u8OzsosNZSDZq4KpogMFlA5K-HDxWkGmORUnI1ovzWQ",
22
+ "src/init/starter-source.ts": "AhAW-qXEBb4c5-beeIS8qh-DqZBkjY0X0IyvlPeWkkQ",
23
23
  "src/package-version.ts": "BkSZq0oCN8fB5tbamfWcRonwCyWs2Rw0-qUXq-83zu4",
24
24
  "src/process/npm-runner.ts": "KpIcT-JZAnaCXZ6v4AYo5WlEiWfb3jfel9El1R2jqjo",
25
25
  "src/registry/registry-access.ts": "DVVkaI0o0MTVfyWZ1d9S8KT8h0WMS16xTzV2mjgf98M",
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { i as RegistryAccessStatus, n as InitCommand, r as RegistryAccess, t as CreateFrontDoor } from "./front-door-CGKOn3bI.mjs";
1
+ import { i as RegistryAccessStatus, n as InitCommand, r as RegistryAccess, t as CreateFrontDoor } from "./front-door-C_ckNreK.mjs";
2
2
  export { CreateFrontDoor, InitCommand, RegistryAccess, RegistryAccessStatus };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-inferative",
3
- "version": "0.2.0-next.20261023154401",
3
+ "version": "0.2.0",
4
4
  "description": "Create a new Inferative project: the `npm create inferative` command, which installs from public npm only.",
5
5
  "keywords": [
6
6
  "create",