create-codebase-snapshot 4.0.6 → 4.1.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.
Files changed (3) hide show
  1. package/README.md +23 -2
  2. package/index.mjs +132 -1
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -8,8 +8,10 @@ npm create codebase-snapshot
8
8
  ```
9
9
 
10
10
  It detects the project type, asks a handful of questions with sensible defaults,
11
- and writes a `snapshot.config.mjs` that lists **every setting at its default** —
12
- so the config doubles as the documentation.
11
+ and writes the config file this project can actually load — `snapshot.config.ts`
12
+ for an ES-module project on Node ≥ 22.6, `snapshot.config.mjs` everywhere else —
13
+ listing **every setting at its default**, so the config doubles as the
14
+ documentation.
13
15
 
14
16
  ```bash
15
17
  npm create codebase-snapshot -- --preset react-vite --yes
@@ -22,5 +24,24 @@ npm create codebase-snapshot -- --preset react-vite --yes
22
24
  | `--minimal` | Write only the settings that differ from defaults |
23
25
  | `--force` | Overwrite an existing config |
24
26
 
27
+ ## What it writes
28
+
29
+ | Path | Notes |
30
+ | :--- | :--- |
31
+ | `snapshot.config.ts` / `snapshot.config.mjs` | Every setting at its default. `.ts` needs an ES-module `package.json` and Node ≥ 22.6; `.mjs` is used otherwise, because a `.ts` config is loaded with the enclosing package's module type. |
32
+ | `package.json` → `scripts.snapshot`, `scripts.snapshot:validate` | Skipped when there is no `package.json` to hold them. |
33
+ | `package.json` → `devDependencies` | `@ankurhalder/codebase-snapshot` at the range this scaffolder shipped with. |
34
+ | `.gitignore` | `snapshot/`, `snapshot.tmp/`. |
35
+
36
+ The generated scripts run `npx @ankurhalder/codebase-snapshot …`, and `npx` uses the
37
+ local `devDependencies` copy when there is one — so the version is pinned by your
38
+ lockfile instead of drifting to `@latest`. Until you run `npm install`, `npx` would
39
+ fall back to downloading the newest release, which is why the printed next step is
40
+ `npm install && npm run snapshot`. Nothing is installed, and no network is used,
41
+ while scaffolding.
42
+
43
+ Without a `package.json` the scripts cannot be added, and the printed next step is
44
+ `npx @ankurhalder/codebase-snapshot generate` instead.
45
+
25
46
  This package is a shim: the wizard itself lives in the main package, so
26
47
  `npx @ankurhalder/codebase-snapshot init` does exactly the same thing.
package/index.mjs CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import * as fs from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import * as path from "node:path";
2
5
  import { PromptCancelled, runInit } from "@ankurhalder/codebase-snapshot";
3
6
 
4
7
  /**
@@ -10,6 +13,126 @@ import { PromptCancelled, runInit } from "@ankurhalder/codebase-snapshot";
10
13
  * `npm create <x>` is the convention developers reach for first.
11
14
  */
12
15
 
16
+ const PACKAGE_NAME = "@ankurhalder/codebase-snapshot";
17
+
18
+ const require = createRequire(import.meta.url);
19
+
20
+ /** The range this shim was published against, read from its own manifest. */
21
+ function dependencyRange() {
22
+ try {
23
+ const manifest = require("./package.json");
24
+ const range = manifest?.dependencies?.[PACKAGE_NAME];
25
+ return typeof range === "string" ? range : null;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ /** Keeps the file's own indentation and trailing newline when rewriting JSON. */
32
+ function serializePackageJson(raw, value) {
33
+ const indent = /^[{[]\r?\n([ \t]+)/.exec(raw)?.[1];
34
+ const spacing = raw.includes("\n") ? (indent ?? " ") : undefined;
35
+ const body = JSON.stringify(value, null, spacing);
36
+ return `${body}${raw.endsWith("\n") ? "\n" : ""}`;
37
+ }
38
+
39
+ /**
40
+ * Records the runtime dependency the generated scripts need.
41
+ *
42
+ * The wizard writes `npx @ankurhalder/codebase-snapshot generate`, which resolves
43
+ * the *latest* release when nothing is installed locally: the first run silently
44
+ * downloaded @latest (observed pulling an older minor), failed behind a proxy and
45
+ * could cross a major version. Naming the package in `devDependencies` makes
46
+ * `npx` prefer that local copy, so the scripts become version-locked once the
47
+ * project is installed — without this shim ever touching the network itself.
48
+ */
49
+ function recordDependency(written) {
50
+ if (!written) return { added: false };
51
+
52
+ const range = dependencyRange();
53
+ if (!range) {
54
+ return {
55
+ added: false,
56
+ note: `Could not read ${PACKAGE_NAME}'s version from this package, so it was not added to devDependencies.`,
57
+ };
58
+ }
59
+
60
+ const pkgPath = path.join(process.cwd(), "package.json");
61
+ if (!fs.existsSync(pkgPath)) return { added: false };
62
+
63
+ let raw;
64
+ try {
65
+ raw = fs.readFileSync(pkgPath, "utf8");
66
+ } catch {
67
+ return {
68
+ added: false,
69
+ note: `package.json could not be read, so ${PACKAGE_NAME} was not added to devDependencies.`,
70
+ };
71
+ }
72
+
73
+ let pkg;
74
+ try {
75
+ pkg = JSON.parse(raw);
76
+ } catch {
77
+ return {
78
+ added: false,
79
+ note: `package.json could not be parsed, so ${PACKAGE_NAME} was not added to devDependencies.`,
80
+ };
81
+ }
82
+
83
+ const declared = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"].some(
84
+ (field) => pkg?.[field] && typeof pkg[field] === "object" && PACKAGE_NAME in pkg[field]
85
+ );
86
+ if (declared) return { added: false, range };
87
+
88
+ pkg.devDependencies = { ...(pkg.devDependencies ?? {}), [PACKAGE_NAME]: range };
89
+ try {
90
+ fs.writeFileSync(pkgPath, serializePackageJson(raw, pkg));
91
+ } catch {
92
+ return {
93
+ added: false,
94
+ note: `package.json could not be written, so ${PACKAGE_NAME} was not added to devDependencies.`,
95
+ };
96
+ }
97
+ return { added: true, range };
98
+ }
99
+
100
+ /** True when the package's own binary is already installed in the project. */
101
+ function hasLocalBinary() {
102
+ const names =
103
+ process.platform === "win32"
104
+ ? ["codebase-snapshot.cmd", "codebase-snapshot"]
105
+ : ["codebase-snapshot"];
106
+ return names.some((name) =>
107
+ fs.existsSync(path.join(process.cwd(), "node_modules", ".bin", name))
108
+ );
109
+ }
110
+
111
+ /** True when the project's package.json already carries the `snapshot` script. */
112
+ function hasSnapshotScript() {
113
+ try {
114
+ const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), "package.json"), "utf8"));
115
+ return typeof pkg?.scripts?.snapshot === "string";
116
+ } catch {
117
+ return false;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * The command the developer should run next.
123
+ *
124
+ * `npm run snapshot` is only meaningful when the project has that script, which
125
+ * needs a `package.json`: the old text promised it unconditionally, so the
126
+ * documented next step failed with ENOENT in exactly the case that needs help
127
+ * most. A pre-existing script counts even when this run added nothing, hence the
128
+ * check is against `package.json` rather than against what the wizard wrote. The
129
+ * remaining forms are executable as printed.
130
+ */
131
+ function nextStep() {
132
+ if (!hasSnapshotScript()) return `npx ${PACKAGE_NAME} generate`;
133
+ return hasLocalBinary() ? "npm run snapshot" : "npm install && npm run snapshot";
134
+ }
135
+
13
136
  const argv = process.argv.slice(2);
14
137
 
15
138
  if (argv.includes("--help") || argv.includes("-h")) {
@@ -60,8 +183,16 @@ try {
60
183
  if (result.gitignoreEntries.length > 0) {
61
184
  process.stderr.write(` Added to .gitignore: ${result.gitignoreEntries.join(", ")}\n`);
62
185
  }
186
+
187
+ const dependency = recordDependency(result.written);
188
+ if (dependency.added) {
189
+ process.stderr.write(` Added devDependency: ${PACKAGE_NAME} ${dependency.range}\n`);
190
+ } else if (dependency.note) {
191
+ process.stderr.write(` ⚠ ${dependency.note}\n`);
192
+ }
193
+
63
194
  for (const note of result.notes) process.stderr.write(` ⚠ ${note}\n`);
64
- process.stderr.write("\n Next: npm run snapshot\n\n");
195
+ process.stderr.write(`\n Next: ${nextStep()}\n\n`);
65
196
  } catch (err) {
66
197
  if (err instanceof PromptCancelled) {
67
198
  process.stderr.write("\n❌ Cancelled.\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-codebase-snapshot",
3
- "version": "4.0.6",
3
+ "version": "4.1.0",
4
4
  "description": "Set up @ankurhalder/codebase-snapshot in a repository",
5
5
  "license": "MIT",
6
6
  "author": "Ankur Halder <ankur.halder12345@gmail.com>",
@@ -22,7 +22,7 @@
22
22
  "README.md"
23
23
  ],
24
24
  "dependencies": {
25
- "@ankurhalder/codebase-snapshot": "^4.0.6"
25
+ "@ankurhalder/codebase-snapshot": "^4.1.0"
26
26
  },
27
27
  "engines": {
28
28
  "node": ">=18.0.0"