create-grist-widget 0.3.23 → 0.3.25

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
@@ -31,8 +31,10 @@ prompt that asks for both.
31
31
  | `list [--json]` | Print the available starting examples |
32
32
 
33
33
  Common flags for `create`/`init`: `--yes` skips interactive prompts and uses
34
- flags/defaults only; `--no-install` skips the automatic dependency install;
35
- `--pm <name>` picks a specific package manager instead of auto-detecting one.
34
+ flags/defaults only; `--no-install` skips the automatic dependency install
35
+ (which uses pnpm, whichever package manager runs the CLI: the widget's
36
+ workflows only read `pnpm-lock.yaml`); `--pm <name>` installs with another
37
+ package manager instead.
36
38
  `init` additionally takes `--name <name>` (default: the current directory's
37
39
  name) and `--force` (overwrite files that already exist and differ from the
38
40
  template — see below).
@@ -26,12 +26,13 @@ import {
26
26
  } from "node:fs"
27
27
  import path from "node:path"
28
28
  import { fileURLToPath } from "node:url"
29
- import { execFileSync } from "node:child_process"
29
+ import { execFileSync, execSync } from "node:child_process"
30
30
  import * as clack from "@clack/prompts"
31
31
 
32
32
  import { createComposer } from "../vendor/compose-example.mjs"
33
33
  import { checkGitGate } from "../src/git-gate.mjs"
34
34
  import { addedAgeExceptions } from "../src/age-exceptions.mjs"
35
+ import { installPlan } from "../src/package-manager.mjs"
35
36
 
36
37
  const PKG_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..")
37
38
  const TEMPLATE_DIR = path.join(PKG_ROOT, "template")
@@ -62,14 +63,6 @@ function titleCase(name) {
62
63
  .join(" ")
63
64
  }
64
65
 
65
- function detectPackageManager() {
66
- // e.g. "pnpm/9.1.0 npm-node/..." -> "pnpm". Set by every package manager's
67
- // own `npm create`-equivalent invocation. Falls back to npm, matching
68
- // what a bare `node bin/create-grist-widget.mjs` (no wrapping pm) should do.
69
- const ua = process.env.npm_config_user_agent || ""
70
- return ua.split("/")[0] || "npm"
71
- }
72
-
73
66
  function parseArgs(argv) {
74
67
  const out = { _: [] }
75
68
  for (let i = 0; i < argv.length; i++) {
@@ -102,8 +95,8 @@ Options (create / init):
102
95
  (see "list" for ids)
103
96
  --yes Skip interactive prompts, use defaults/flags only
104
97
  --no-install Don't run the package manager install automatically
105
- --pm <name> Package manager for install (default: detected from how
106
- you invoked this — npm/pnpm/yarn/bun)
98
+ --pm <name> Install with this package manager instead of pnpm (the
99
+ widget's workflows use pnpm and only read its lockfile)
107
100
  --name <name> (init only) Widget name — default: the current directory's name
108
101
  --force (init only) Overwrite files that already exist and differ
109
102
  from the template
@@ -169,33 +162,59 @@ function gitInitCommit(target) {
169
162
  }
170
163
  }
171
164
 
172
- function maybeInstall(target, { noInstall, pm }) {
173
- if (noInstall) return false
174
- const manager = pm || detectPackageManager()
165
+ // Package managers are .cmd shims on Windows, which only a shell starts.
166
+ function runCommand(cmd, args, options) {
167
+ if (process.platform === "win32") return execSync([cmd, ...args].join(" "), options)
168
+ return execFileSync(cmd, args, options)
169
+ }
170
+
171
+ function hasCommand(cmd) {
175
172
  try {
176
- execFileSync(manager, ["install"], { cwd: target, stdio: "inherit" })
173
+ runCommand(cmd, ["--version"], { stdio: "ignore" })
177
174
  return true
178
175
  } catch {
179
- console.warn(`\nCould not run "${manager} install" automatically — run it yourself.`)
180
176
  return false
181
177
  }
182
178
  }
183
179
 
180
+ /** Returns whether the install ran, and why not when it was skipped. */
181
+ function maybeInstall(target, { noInstall, pm }) {
182
+ const plan = installPlan({ noInstall, pm, hasPnpm: () => hasCommand("pnpm") })
183
+ if (plan.skip) return { installed: false, skipped: plan.skip }
184
+ try {
185
+ runCommand(plan.manager, ["install"], { cwd: target, stdio: "inherit" })
186
+ return { installed: true }
187
+ } catch {
188
+ console.warn(`\nCould not run "${plan.manager} install" automatically — run it yourself.`)
189
+ return { installed: false }
190
+ }
191
+ }
192
+
184
193
  /**
185
- * Install (unless --no-install), returning which `minimumReleaseAgeExclude`
186
- * entries pnpm added to pnpm-workspace.yaml along the way. Runs before the
187
- * first commit, so the lockfile -- and those entries, without which every
188
- * later install rejects that lockfile -- are part of it.
194
+ * Install (unless --no-install, or pnpm is missing), returning which
195
+ * `minimumReleaseAgeExclude` entries pnpm added to pnpm-workspace.yaml along
196
+ * the way. Runs before the first commit, so the lockfile -- and those
197
+ * entries, without which every later install rejects that lockfile -- are
198
+ * part of it.
189
199
  */
190
200
  function installBeforeFirstCommit(target, args) {
191
201
  const workspaceFile = path.join(target, "pnpm-workspace.yaml")
192
202
  const read = () => (existsSync(workspaceFile) ? readFileSync(workspaceFile, "utf8") : "")
193
203
  const before = read()
194
- const installed = maybeInstall(target, {
204
+ const { installed, skipped } = maybeInstall(target, {
195
205
  noInstall: Boolean(args["no-install"]),
196
206
  pm: typeof args.pm === "string" ? args.pm : null,
197
207
  })
198
- return { installed, ageExceptions: installed ? addedAgeExceptions(before, read()) : [] }
208
+ return { installed, skipped, ageExceptions: installed ? addedAgeExceptions(before, read()) : [] }
209
+ }
210
+
211
+ function printNoPnpmNote(skipped) {
212
+ if (skipped !== "no-pnpm") return
213
+ console.log(
214
+ "\nNote: dependencies were not installed. This widget uses pnpm (its workflows install with it," +
215
+ "\nand only read pnpm-lock.yaml), and pnpm isn't installed. Install it (https://pnpm.io/installation)," +
216
+ "\nthen run `pnpm install` and commit pnpm-lock.yaml."
217
+ )
199
218
  }
200
219
 
201
220
  function printAgeExceptionsNote(ageExceptions) {
@@ -312,13 +331,14 @@ async function cmdCreate(argv) {
312
331
 
313
332
  // Install first, so the first commit includes the lockfile (pinned, with
314
333
  // integrity hashes; pnpm then freezes installs in CI by itself).
315
- const { installed, ageExceptions } = installBeforeFirstCommit(target, args)
334
+ const { installed, skipped, ageExceptions } = installBeforeFirstCommit(target, args)
316
335
  const committed = gitInitCommit(target)
317
336
 
318
337
  printAgeExceptionsNote(ageExceptions)
338
+ printNoPnpmNote(skipped)
319
339
  console.log(`\nCreated ${rawName}/${exampleId ? ` (starting from "${exampleId}")` : ""}\n`)
320
340
  console.log(` cd ${rawName}`)
321
- if (!installed) console.log(" pnpm install # or npm/yarn")
341
+ if (!installed) console.log(" pnpm install")
322
342
  console.log(" pnpm dev")
323
343
  if (committed) {
324
344
  console.log("\nGit is initialized on main (first commit done) with a dev branch ready.")
@@ -413,12 +433,13 @@ async function cmdInit(argv) {
413
433
  // or a real git clone where .git already carries whatever history it has).
414
434
  const gitAlreadyExists = existsSync(path.join(cwd, ".git"))
415
435
  // Install first, so a first commit includes the lockfile (see cmdCreate).
416
- const { installed, ageExceptions } = installBeforeFirstCommit(cwd, args)
436
+ const { installed, skipped, ageExceptions } = installBeforeFirstCommit(cwd, args)
417
437
  const committed = gitAlreadyExists ? false : gitInitCommit(cwd)
418
438
 
419
439
  printAgeExceptionsNote(ageExceptions)
440
+ printNoPnpmNote(skipped)
420
441
  console.log(`\nInitialized ${rawName}/${exampleId ? ` (starting from "${exampleId}")` : ""} in the current directory.\n`)
421
- if (!installed) console.log(" pnpm install # or npm/yarn")
442
+ if (!installed) console.log(" pnpm install")
422
443
  console.log(" pnpm dev")
423
444
  if (gitAlreadyExists) {
424
445
  console.log("\nExisting git repository detected — no commit was made automatically.")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-grist-widget",
3
- "version": "0.3.23",
3
+ "version": "0.3.25",
4
4
  "description": "Scaffold a new Grist custom widget from the grist-widget-sdk Vite template",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,19 @@
1
+ // Which package manager installs a new widget's dependencies.
2
+ //
3
+ // The scaffold is a pnpm project: its workflows install with pnpm, and
4
+ // pnpm-workspace.yaml holds settings only pnpm reads (build-script
5
+ // approvals, release-age exceptions). So the CLI installs with pnpm
6
+ // whichever package manager runs it -- `npm create grist-widget` runs it
7
+ // under npm. Installing with npm would commit a package-lock.json that the
8
+ // workflows never read, and skip pnpm's 24 h release-age check.
9
+
10
+ /**
11
+ * @param {{ noInstall?: boolean, pm?: string | null, hasPnpm: () => boolean }} options
12
+ * `pm` is an explicit `--pm`; `hasPnpm` is only called when pnpm is the pick.
13
+ * @returns {{ manager: string } | { skip: "no-install" | "no-pnpm" }}
14
+ */
15
+ export function installPlan({ noInstall, pm, hasPnpm }) {
16
+ if (noInstall) return { skip: "no-install" }
17
+ if (pm) return { manager: pm }
18
+ return hasPnpm() ? { manager: "pnpm" } : { skip: "no-pnpm" }
19
+ }
@@ -38,9 +38,11 @@ This is the **source** template. It is consumed two ways:
38
38
  standalone `pnpm install` (pnpm 11) exits cleanly; inside the monorepo it's
39
39
  ignored (the root workspace governs).
40
40
 
41
- A scaffold's first commit includes `pnpm-lock.yaml` (unless it was made with
42
- `--no-install`): versions are pinned with their integrity hashes, CI installs
43
- exactly that, and Dependabot updates it. pnpm 11 won't install a version
41
+ A scaffold's first commit includes `pnpm-lock.yaml`: the CLI installs with
42
+ pnpm, even under `npm create`, unless it was run with `--no-install` or
43
+ without pnpm installed. Versions are pinned with their integrity hashes, CI
44
+ installs exactly that, and Dependabot updates it. A `package-lock.json` isn't
45
+ used: the workflows install with pnpm. pnpm 11 won't install a version
44
46
  published less than 24 hours ago. So a widget scaffolded on the day of an SDK
45
47
  release also gets one `minimumReleaseAgeExclude` entry in
46
48
  `pnpm-workspace.yaml`, for that SDK version only, written by pnpm itself.
@@ -17,7 +17,7 @@
17
17
  "@tailwindcss/vite": "^4.3.3",
18
18
  "class-variance-authority": "^0.7.1",
19
19
  "clsx": "^2.1.1",
20
- "grist-widget-sdk": "^0.3.20",
20
+ "grist-widget-sdk": "^0.3.25",
21
21
  "lucide-react": "^1.61.0",
22
22
  "radix-ui": "^1.8.1",
23
23
  "react": "^19.3.1",
@@ -1,28 +1,13 @@
1
1
  {
2
+ // Compiler settings come from grist-widget-sdk and update with it. This
3
+ // file keeps what is specific to this widget: its files, paths and types.
4
+ "extends": "grist-widget-sdk/tsconfig/app.json",
2
5
  "compilerOptions": {
6
+ // Also set in the base. Repeated so that a missing base (a grist-widget-sdk
7
+ // too old to ship it) fails the typecheck without writing .js files into src/.
8
+ "noEmit": true,
3
9
  "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
- "target": "ES2022",
5
- "useDefineForClassFields": true,
6
- "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
- "module": "ESNext",
8
10
  "types": ["vite/client"],
9
- "skipLibCheck": true,
10
-
11
- /* Bundler mode */
12
- "moduleResolution": "bundler",
13
- "allowImportingTsExtensions": true,
14
- "verbatimModuleSyntax": true,
15
- "moduleDetection": "force",
16
- "noEmit": true,
17
- "jsx": "react-jsx",
18
-
19
- /* Linting */
20
- "strict": true,
21
- "noUnusedLocals": true,
22
- "noUnusedParameters": true,
23
- "erasableSyntaxOnly": true,
24
- "noFallthroughCasesInSwitch": true,
25
- "noUncheckedSideEffectImports": true,
26
11
  "paths": {
27
12
  "@/*": ["./src/*"]
28
13
  }
@@ -1,26 +1,13 @@
1
1
  {
2
+ // Compiler settings come from grist-widget-sdk and update with it. This
3
+ // file keeps what is specific to this widget: its files and types.
4
+ "extends": "grist-widget-sdk/tsconfig/node.json",
2
5
  "compilerOptions": {
3
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
- "target": "ES2023",
5
- "lib": ["ES2023"],
6
- "module": "ESNext",
7
- "types": ["node"],
8
- "skipLibCheck": true,
9
-
10
- /* Bundler mode */
11
- "moduleResolution": "bundler",
12
- "allowImportingTsExtensions": true,
13
- "verbatimModuleSyntax": true,
14
- "moduleDetection": "force",
6
+ // Also set in the base. Repeated so that a missing base (a grist-widget-sdk
7
+ // too old to ship it) fails the typecheck without writing a .js file.
15
8
  "noEmit": true,
16
-
17
- /* Linting */
18
- "strict": true,
19
- "noUnusedLocals": true,
20
- "noUnusedParameters": true,
21
- "erasableSyntaxOnly": true,
22
- "noFallthroughCasesInSwitch": true,
23
- "noUncheckedSideEffectImports": true
9
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
10
+ "types": ["node"]
24
11
  },
25
12
  "include": ["vite.config.ts"]
26
13
  }