create-ovok 0.1.0-dev.22 → 0.1.0-dev.33

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
@@ -24,6 +24,9 @@ Flags answer their questions, and `--yes` takes the recommended setup for the re
24
24
  Outside a terminal, such as in CI, nothing is asked: unanswered questions take the recommended
25
25
  setup.
26
26
 
27
+ In a terminal it also asks npm for the latest create-ovok and, when there is a newer one, ends
28
+ with the version to run next time.
29
+
27
30
  Inside the workspace:
28
31
 
29
32
  ```bash
@@ -0,0 +1,15 @@
1
+ const REGISTRY = "https://registry.npmjs.org";
2
+ /** The version npm serves under `distTag`, or null when the registry does not answer in time. */
3
+ export async function distTagVersion(packageName, distTag, signal) {
4
+ const url = `${REGISTRY}/-/package/${encodeURIComponent(packageName)}/dist-tags`;
5
+ try {
6
+ const response = await fetch(url, { signal });
7
+ if (!response.ok)
8
+ return null;
9
+ const version = (await response.json())[distTag];
10
+ return typeof version === "string" ? version : null;
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
@@ -0,0 +1,28 @@
1
+ const NUMERIC_ORDER = new Intl.Collator("en", { numeric: true });
2
+ const DIST_TAG_BY_CHANNEL = { dev: "dev", rc: "next" };
3
+ const STABLE_DIST_TAG = "latest";
4
+ /** Semver order: numbers compare as numbers, and a release outranks its own prereleases. */
5
+ export function isNewer(candidate, current) {
6
+ const [candidateCore, candidatePrerelease] = splitPrerelease(candidate);
7
+ const [currentCore, currentPrerelease] = splitPrerelease(current);
8
+ const coreOrder = NUMERIC_ORDER.compare(candidateCore, currentCore);
9
+ if (coreOrder !== 0)
10
+ return coreOrder > 0;
11
+ if (candidatePrerelease === "" || currentPrerelease === "") {
12
+ return candidatePrerelease === "" && currentPrerelease !== "";
13
+ }
14
+ return NUMERIC_ORDER.compare(candidatePrerelease, currentPrerelease) > 0;
15
+ }
16
+ /** The npm dist-tag that builds like this one are published under. */
17
+ export function distTagOf(version) {
18
+ const [, prerelease] = splitPrerelease(version);
19
+ const [channel = ""] = prerelease.split(".");
20
+ return DIST_TAG_BY_CHANNEL[channel] ?? STABLE_DIST_TAG;
21
+ }
22
+ function splitPrerelease(version) {
23
+ const [withoutBuild = ""] = version.split("+");
24
+ const dash = withoutBuild.indexOf("-");
25
+ return dash === -1
26
+ ? [withoutBuild, ""]
27
+ : [withoutBuild.slice(0, dash), withoutBuild.slice(dash + 1)];
28
+ }
package/dist/main.js CHANGED
@@ -1,17 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFile } from "node:fs/promises";
3
- import { parseArgs } from "node:util";
3
+ import { parseArgs, styleText } from "node:util";
4
4
  import { cancel } from "@clack/prompts";
5
5
  import { ClackPrompt } from "./adapters/clack-prompt.js";
6
6
  import { FsProjectFiles } from "./adapters/fs-project-files.js";
7
+ import { distTagVersion } from "./adapters/npm-registry.js";
7
8
  import { dependenciesAt } from "./adapters/scaffold-dependencies.js";
8
9
  import { addApp, addPackage } from "./application/add-app.js";
9
10
  import { refreshAgentRules } from "./application/refresh-agent-rules.js";
10
11
  import { CancelledError, DEFAULT_ANSWERS, RECOMMENDED, } from "./application/workspace-answers.js";
11
12
  import { initialiseGit, install, scaffold, showNextSteps } from "./commands/create.js";
12
13
  import { MOBILE_OSES, osFor, PLATFORMS, parseChoice, parsePackageName, STARTING_PLATFORMS, } from "./domain/app-spec.js";
14
+ import { distTagOf, isNewer } from "./domain/version.js";
13
15
  const EXIT_FAILURE = 1;
14
16
  const EXIT_CANCELLED = 130;
17
+ const UPDATE_CHECK_TIMEOUT_MS = 1_500;
15
18
  const OPTIONS = {
16
19
  platform: { type: "string" },
17
20
  type: { type: "string" },
@@ -69,7 +72,7 @@ async function main(argv) {
69
72
  if (values.help)
70
73
  return console.log(USAGE);
71
74
  if (values.version)
72
- return console.log(await ownVersion());
75
+ return console.log((await ownManifest()).version);
73
76
  const [first, ...rest] = positionals;
74
77
  const subcommand = first === undefined ? undefined : SUBCOMMANDS[first];
75
78
  if (subcommand === undefined)
@@ -77,14 +80,31 @@ async function main(argv) {
77
80
  return subcommand(rest, values);
78
81
  }
79
82
  async function create(name, values) {
80
- const workspace = await scaffold(setupFlagsOf(name, values), promptFor(values), await ownVersion());
83
+ const manifest = await ownManifest();
84
+ const pendingNotice = updateNotice(manifest);
85
+ const workspace = await scaffold(setupFlagsOf(name, values), promptFor(values), manifest.version);
81
86
  const installation = values["skip-install"] ? "skipped" : await install(workspace);
82
87
  if (!values["disable-git"])
83
88
  await initialiseGit(workspace);
84
89
  showNextSteps(workspace, installation);
90
+ const notice = await pendingNotice;
91
+ if (notice !== null)
92
+ console.error(notice);
85
93
  if (installation === "failed")
86
94
  process.exitCode = EXIT_FAILURE;
87
95
  }
96
+ /** Asks npm while the workspace is made, and only when someone is at a terminal to read the answer. */
97
+ async function updateNotice({ name, version }) {
98
+ if (!process.stderr.isTTY)
99
+ return null;
100
+ const signal = AbortSignal.timeout(UPDATE_CHECK_TIMEOUT_MS);
101
+ const newest = await distTagVersion(name, distTagOf(version), signal);
102
+ return newest !== null && isNewer(newest, version) ? updateMessage(version, newest) : null;
103
+ }
104
+ function updateMessage(current, newest) {
105
+ const update = styleText("cyan", `pnpm create ovok@${newest}`);
106
+ return `${styleText("yellow", "Update available:")} ${current} → ${newest}. Next time run ${update}`;
107
+ }
88
108
  function promptFor(values) {
89
109
  const interactive = process.stdin.isTTY && process.stdout.isTTY && !values.yes;
90
110
  return interactive ? new ClackPrompt() : DEFAULT_ANSWERS;
@@ -103,9 +123,9 @@ function setupFlagsOf(name, values) {
103
123
  function typeOf(values) {
104
124
  return values.type === undefined ? null : parseChoice(values.type, MOBILE_OSES, "--type");
105
125
  }
106
- async function ownVersion() {
126
+ async function ownManifest() {
107
127
  const manifest = await readFile(new URL("../package.json", import.meta.url), "utf8");
108
- return JSON.parse(manifest).version;
128
+ return JSON.parse(manifest);
109
129
  }
110
130
  try {
111
131
  await main(process.argv.slice(2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ovok",
3
- "version": "0.1.0-dev.22",
3
+ "version": "0.1.0-dev.33+5f9fdd8",
4
4
  "description": "Create a Turborepo with Ovok web and mobile apps, and add more apps through turbo gen.",
5
5
  "license": "MIT",
6
6
  "type": "module",