create-grist-widget 0.3.6 → 0.3.9
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 +79 -7
- package/bin/create-grist-widget.mjs +485 -90
- package/examples/attachment-gallery/WidgetApp.tsx +202 -0
- package/examples/bulk-mark-done/WidgetApp.tsx +43 -0
- package/examples/declared-columns/WidgetApp.tsx +42 -0
- package/examples/form-edit/WidgetApp.tsx +225 -0
- package/examples/hello-world/WidgetApp.tsx +15 -0
- package/examples/mark-done/WidgetApp.tsx +44 -0
- package/examples/reads-demo/WidgetApp.tsx +69 -0
- package/examples/rest-fetch/WidgetApp.tsx +49 -0
- package/examples/safe-parse/WidgetApp.tsx +61 -0
- package/examples/schema-migration/WidgetApp.tsx +67 -0
- package/examples/schema-preview/WidgetApp.tsx +26 -0
- package/examples/slice-hooks/WidgetApp.tsx +47 -0
- package/examples/spreadsheet-grid/GristDataSheetGrid.tsx +376 -0
- package/examples/spreadsheet-grid/GristRdgGrid.tsx +189 -0
- package/examples/spreadsheet-grid/GristSchemaToolbar.tsx +293 -0
- package/examples/spreadsheet-grid/WidgetApp.tsx +133 -0
- package/examples/spreadsheet-grid/grist-rdg-choice-column.tsx +616 -0
- package/examples/spreadsheet-grid/grist-rdg-columns.ts +397 -0
- package/examples/task-board/WidgetApp.tsx +229 -0
- package/examples/theme-demo/WidgetApp.tsx +26 -0
- package/examples/widget-options/WidgetApp.tsx +34 -0
- package/examples/writes-demo/WidgetApp.tsx +75 -0
- package/manifest.json +304 -0
- package/package.json +14 -2
- package/src/git-gate.mjs +30 -0
- package/template/.github/workflows/deploy.yml +7 -3
- package/template/README.md +3 -2
- package/template/scripts/deploy.mjs +128 -18
- package/ui-source/ShadcnDataSheetGrid.css +117 -0
- package/ui-source/ShadcnDataSheetGrid.tsx +46 -0
- package/ui-source/badge.tsx +49 -0
- package/vendor/compose-example.mjs +252 -0
|
@@ -1,105 +1,500 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
// create-grist-widget — scaffold a Grist custom widget, fully offline.
|
|
3
|
+
//
|
|
4
|
+
// Four verbs (batch H, docs/planning/2026-08-execution-plan.md §98-105):
|
|
5
|
+
// create <name> [--example <id>] scaffold a new widget into ./<name>
|
|
6
|
+
// init [--example <id>] scaffold into the current directory
|
|
7
|
+
// use <example-id> swap the current project's widget
|
|
8
|
+
// list [--json] print the available starting examples
|
|
9
|
+
//
|
|
10
|
+
// `create`/`init`/`use` share one compose engine — see
|
|
11
|
+
// packages/create-grist-widget/vendor/compose-example.mjs (a verbatim,
|
|
12
|
+
// path-free copy of scripts/compose-example.mjs, embedded by
|
|
13
|
+
// scripts/build-template.mjs alongside the template itself, along with
|
|
14
|
+
// examples/, ui-source/, and manifest.json — see that script for why).
|
|
15
|
+
// Nothing here ever hits the network: everything list/use/create --example
|
|
16
|
+
// need already shipped in the tarball.
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
cpSync,
|
|
20
|
+
existsSync,
|
|
21
|
+
mkdirSync,
|
|
22
|
+
readdirSync,
|
|
23
|
+
readFileSync,
|
|
24
|
+
renameSync,
|
|
25
|
+
writeFileSync,
|
|
26
|
+
} from "node:fs"
|
|
3
27
|
import path from "node:path"
|
|
4
28
|
import { fileURLToPath } from "node:url"
|
|
5
29
|
import { execFileSync } from "node:child_process"
|
|
30
|
+
import * as clack from "@clack/prompts"
|
|
6
31
|
|
|
7
|
-
|
|
32
|
+
import { createComposer } from "../vendor/compose-example.mjs"
|
|
33
|
+
import { checkGitGate } from "../src/git-gate.mjs"
|
|
8
34
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
35
|
+
const PKG_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..")
|
|
36
|
+
const TEMPLATE_DIR = path.join(PKG_ROOT, "template")
|
|
37
|
+
|
|
38
|
+
const cliPkg = JSON.parse(readFileSync(path.join(PKG_ROOT, "package.json"), "utf8"))
|
|
39
|
+
|
|
40
|
+
const composer = createComposer({
|
|
41
|
+
templateDir: TEMPLATE_DIR,
|
|
42
|
+
examplesDir: path.join(PKG_ROOT, "examples"),
|
|
43
|
+
uiDir: path.join(PKG_ROOT, "ui-source"),
|
|
44
|
+
manifestPath: path.join(PKG_ROOT, "manifest.json"),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
function loadManifest() {
|
|
48
|
+
return JSON.parse(readFileSync(path.join(PKG_ROOT, "manifest.json"), "utf8"))
|
|
13
49
|
}
|
|
14
50
|
|
|
15
51
|
// Mirrors npm's own unscoped package name rule, kept intentionally loose.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
process.exit(1)
|
|
52
|
+
function isValidName(name) {
|
|
53
|
+
return /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(name || "")
|
|
19
54
|
}
|
|
20
55
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
56
|
+
function titleCase(name) {
|
|
57
|
+
return name
|
|
58
|
+
.split("-")
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.map((w) => w[0].toUpperCase() + w.slice(1))
|
|
61
|
+
.join(" ")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function detectPackageManager() {
|
|
65
|
+
// e.g. "pnpm/9.1.0 npm-node/..." -> "pnpm". Set by every package manager's
|
|
66
|
+
// own `npm create`-equivalent invocation. Falls back to npm, matching
|
|
67
|
+
// what a bare `node bin/create-grist-widget.mjs` (no wrapping pm) should do.
|
|
68
|
+
const ua = process.env.npm_config_user_agent || ""
|
|
69
|
+
return ua.split("/")[0] || "npm"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseArgs(argv) {
|
|
73
|
+
const out = { _: [] }
|
|
74
|
+
for (let i = 0; i < argv.length; i++) {
|
|
75
|
+
const a = argv[i]
|
|
76
|
+
if (a.startsWith("--")) {
|
|
77
|
+
const key = a.slice(2)
|
|
78
|
+
const next = argv[i + 1]
|
|
79
|
+
if (next === undefined || next.startsWith("--")) out[key] = true
|
|
80
|
+
else {
|
|
81
|
+
out[key] = next
|
|
82
|
+
i++
|
|
83
|
+
}
|
|
84
|
+
} else out._.push(a)
|
|
85
|
+
}
|
|
86
|
+
return out
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function printHelp() {
|
|
90
|
+
console.log(`create-grist-widget — scaffold a Grist custom widget, offline
|
|
91
|
+
|
|
92
|
+
Usage:
|
|
93
|
+
npm create grist-widget <name> [options] Scaffold into ./<name>
|
|
94
|
+
create-grist-widget create <name> [options] Same as above, explicit
|
|
95
|
+
create-grist-widget init [options] Scaffold into the current directory
|
|
96
|
+
create-grist-widget use <example-id> Swap the current project's widget
|
|
97
|
+
create-grist-widget list [--json] List available starting examples
|
|
98
|
+
|
|
99
|
+
Options (create / init):
|
|
100
|
+
--example <id> Start from a named example instead of the blank template
|
|
101
|
+
(see "list" for ids)
|
|
102
|
+
--yes Skip interactive prompts, use defaults/flags only
|
|
103
|
+
--no-install Don't run the package manager install automatically
|
|
104
|
+
--pm <name> Package manager for install (default: detected from how
|
|
105
|
+
you invoked this — npm/pnpm/yarn/bun)
|
|
106
|
+
--name <name> (init only) Widget name — default: the current directory's name
|
|
107
|
+
--force (init only) Overwrite files that already exist and differ
|
|
108
|
+
from the template
|
|
109
|
+
|
|
110
|
+
--help, -h Show this help
|
|
111
|
+
--version, -v Show this CLI's version
|
|
112
|
+
|
|
113
|
+
Note: "create", "init", "use", and "list" are themselves valid widget names.
|
|
114
|
+
A bare "create-grist-widget list" runs the list command, not "scaffold a
|
|
115
|
+
widget named list" — use "create-grist-widget create list" for that.`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Same four placeholder-identity edits every scaffold gets, whichever verb produced it. */
|
|
119
|
+
function applyIdentityEdits(target, rawName) {
|
|
120
|
+
const title = titleCase(rawName)
|
|
121
|
+
const edits = [
|
|
122
|
+
["package.json", [["grist-widget-template-vite", rawName]]],
|
|
123
|
+
["README.md", [["grist-widget-template-vite", rawName]]],
|
|
124
|
+
["index.html", [["<title>Grist Widget</title>", `<title>${title}</title>`]]],
|
|
125
|
+
["src/App.tsx", [['title: "Grist Widget Template",', `title: "${title}",`]]],
|
|
126
|
+
]
|
|
127
|
+
for (const [file, subs] of edits) {
|
|
128
|
+
const p = path.join(target, file)
|
|
129
|
+
if (!existsSync(p)) continue
|
|
130
|
+
let content = readFileSync(p, "utf8")
|
|
131
|
+
for (const [search, replace] of subs) content = content.split(search).join(replace)
|
|
132
|
+
writeFileSync(p, content)
|
|
133
|
+
}
|
|
134
|
+
return title
|
|
25
135
|
}
|
|
26
|
-
mkdirSync(target, { recursive: true })
|
|
27
|
-
cpSync(TEMPLATE_DIR, target, { recursive: true })
|
|
28
136
|
|
|
29
137
|
// npm strips a literal `.gitignore` from the published package (see
|
|
30
|
-
// build-template.mjs), so it ships as `_gitignore` — restore the real name.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
138
|
+
// scripts/build-template.mjs), so it ships as `_gitignore` — restore the real name.
|
|
139
|
+
function restoreGitignore(target) {
|
|
140
|
+
const gitignorePath = path.join(target, "_gitignore")
|
|
141
|
+
if (existsSync(gitignorePath)) renameSync(gitignorePath, path.join(target, ".gitignore"))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Purely informational (a small landing-page footer), never read by the
|
|
145
|
+
// deploy pipeline's own version/release logic.
|
|
146
|
+
function stampCreateGristWidgetVersion(target) {
|
|
147
|
+
const pkgJsonPath = path.join(target, "package.json")
|
|
148
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"))
|
|
149
|
+
pkgJson.createGristWidgetVersion = cliPkg.version
|
|
150
|
+
writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n")
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Initialize git on `main` with a first commit and a `dev` branch, if a git identity is configured. */
|
|
154
|
+
function gitInitCommit(target) {
|
|
155
|
+
function git(args) {
|
|
156
|
+
execFileSync("git", args, { cwd: target, stdio: "ignore" })
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
git(["init", "-q", "-b", "main"])
|
|
160
|
+
git(["add", "-A"])
|
|
161
|
+
git(["commit", "-q", "-m", "scaffold: initial widget"])
|
|
162
|
+
git(["branch", "dev"])
|
|
163
|
+
return true
|
|
164
|
+
} catch {
|
|
165
|
+
// No git identity configured (or git unavailable) — leave it to the
|
|
166
|
+
// printed next steps.
|
|
167
|
+
return false
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function maybeInstall(target, { noInstall, pm }) {
|
|
172
|
+
if (noInstall) return false
|
|
173
|
+
const manager = pm || detectPackageManager()
|
|
174
|
+
try {
|
|
175
|
+
execFileSync(manager, ["install"], { cwd: target, stdio: "inherit" })
|
|
176
|
+
return true
|
|
177
|
+
} catch {
|
|
178
|
+
console.warn(`\nCould not run "${manager} install" automatically — run it yourself.`)
|
|
179
|
+
return false
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function listFilesRecursive(dir, base = dir) {
|
|
184
|
+
const out = []
|
|
185
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
186
|
+
const full = path.join(dir, entry.name)
|
|
187
|
+
if (entry.isDirectory()) out.push(...listFilesRecursive(full, base))
|
|
188
|
+
else out.push(path.relative(base, full).split(path.sep).join("/"))
|
|
189
|
+
}
|
|
190
|
+
return out
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Interactive "which example?" picker, shared by `create` and `init` — reads the same manifest.json `list` does. */
|
|
194
|
+
async function pickExampleInteractively(manifest) {
|
|
195
|
+
const choice = await clack.select({
|
|
196
|
+
message: "Start from an example?",
|
|
197
|
+
options: [
|
|
198
|
+
{ value: null, label: "Blank template (two-field editor)" },
|
|
199
|
+
...manifest.map((e) => ({ value: e.id, label: e.title, hint: e.description })),
|
|
200
|
+
],
|
|
201
|
+
})
|
|
202
|
+
if (clack.isCancel(choice)) {
|
|
203
|
+
clack.cancel("Cancelled.")
|
|
204
|
+
process.exit(1)
|
|
205
|
+
}
|
|
206
|
+
return choice
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function resolveExampleFlag(exampleFlag, manifest) {
|
|
210
|
+
if (exampleFlag === undefined || exampleFlag === true) return { ok: true, exampleId: null }
|
|
211
|
+
if (exampleFlag === "blank") return { ok: true, exampleId: null }
|
|
212
|
+
if (!manifest.some((e) => e.id === exampleFlag)) {
|
|
213
|
+
return { ok: false }
|
|
214
|
+
}
|
|
215
|
+
return { ok: true, exampleId: exampleFlag }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// create
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
async function cmdCreate(argv) {
|
|
223
|
+
const args = parseArgs(argv)
|
|
224
|
+
const manifest = loadManifest()
|
|
225
|
+
const interactive = Boolean(process.stdout.isTTY) && !args.yes && !args._[0]
|
|
226
|
+
|
|
227
|
+
let rawName = args._[0]
|
|
228
|
+
let exampleId = null
|
|
229
|
+
|
|
230
|
+
if (args.example !== undefined) {
|
|
231
|
+
const resolved = resolveExampleFlag(args.example, manifest)
|
|
232
|
+
if (!resolved.ok) {
|
|
233
|
+
console.error(`Unknown example "${args.example}". Run "create-grist-widget list" to see available examples.`)
|
|
234
|
+
process.exit(1)
|
|
235
|
+
}
|
|
236
|
+
exampleId = resolved.exampleId
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (interactive) {
|
|
240
|
+
clack.intro("create-grist-widget")
|
|
241
|
+
if (!rawName) {
|
|
242
|
+
const nameAnswer = await clack.text({
|
|
243
|
+
message: "Widget name",
|
|
244
|
+
placeholder: "my-widget",
|
|
245
|
+
validate: (v) => (isValidName(v) ? undefined : "lowercase letters, digits, hyphens only"),
|
|
246
|
+
})
|
|
247
|
+
if (clack.isCancel(nameAnswer)) {
|
|
248
|
+
clack.cancel("Cancelled.")
|
|
249
|
+
process.exit(1)
|
|
250
|
+
}
|
|
251
|
+
rawName = nameAnswer
|
|
252
|
+
}
|
|
253
|
+
if (args.example === undefined) {
|
|
254
|
+
exampleId = await pickExampleInteractively(manifest)
|
|
255
|
+
}
|
|
256
|
+
clack.outro("Scaffolding...")
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!rawName) {
|
|
260
|
+
console.error("Usage: npm create grist-widget <widget-name> [--example <id>] [--yes] [--no-install] [--pm <pm>]")
|
|
261
|
+
process.exit(1)
|
|
262
|
+
}
|
|
263
|
+
if (!isValidName(rawName)) {
|
|
264
|
+
console.error(`"${rawName}" is not a valid name — use lowercase letters, digits, and hyphens.`)
|
|
265
|
+
process.exit(1)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const target = path.resolve(process.cwd(), rawName)
|
|
269
|
+
if (existsSync(target) && readdirSync(target).length > 0) {
|
|
270
|
+
console.error(`"${rawName}" already exists and is not empty.`)
|
|
271
|
+
process.exit(1)
|
|
272
|
+
}
|
|
273
|
+
mkdirSync(target, { recursive: true })
|
|
274
|
+
cpSync(TEMPLATE_DIR, target, { recursive: true })
|
|
275
|
+
restoreGitignore(target)
|
|
276
|
+
applyIdentityEdits(target, rawName)
|
|
277
|
+
|
|
278
|
+
if (exampleId) {
|
|
279
|
+
composer.overlayExampleSrc({ exampleId, destDir: target })
|
|
280
|
+
composer.mergeExtraDependencies({ exampleId, destDir: target })
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
stampCreateGristWidgetVersion(target)
|
|
284
|
+
|
|
285
|
+
const committed = gitInitCommit(target)
|
|
286
|
+
const installed = maybeInstall(target, {
|
|
287
|
+
noInstall: Boolean(args["no-install"]),
|
|
288
|
+
pm: typeof args.pm === "string" ? args.pm : null,
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
console.log(`\nCreated ${rawName}/${exampleId ? ` (starting from "${exampleId}")` : ""}\n`)
|
|
292
|
+
console.log(` cd ${rawName}`)
|
|
293
|
+
if (!installed) console.log(" pnpm install # or npm/yarn")
|
|
294
|
+
console.log(" pnpm dev")
|
|
295
|
+
if (committed) {
|
|
296
|
+
console.log("\nGit is initialized on main (first commit done) with a dev branch ready.")
|
|
297
|
+
console.log("To push to GitHub:")
|
|
298
|
+
console.log(" git remote add origin https://github.com/<owner>/<repo>.git")
|
|
299
|
+
console.log(" git push -u origin main")
|
|
300
|
+
console.log(" git push origin dev # optional, for the live-preview channel")
|
|
301
|
+
} else {
|
|
302
|
+
console.log("\nCould not create the initial git commit automatically (no git identity")
|
|
303
|
+
console.log('configured?) — run `git config user.name`/`user.email`, then:')
|
|
304
|
+
console.log(" git init -b main && git add -A && git commit -m 'scaffold: initial widget'")
|
|
305
|
+
console.log(" git branch dev")
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// init
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
async function cmdInit(argv) {
|
|
314
|
+
const args = parseArgs(argv)
|
|
315
|
+
const manifest = loadManifest()
|
|
316
|
+
const cwd = process.cwd()
|
|
317
|
+
|
|
318
|
+
let exampleId = null
|
|
319
|
+
if (args.example !== undefined) {
|
|
320
|
+
const resolved = resolveExampleFlag(args.example, manifest)
|
|
321
|
+
if (!resolved.ok) {
|
|
322
|
+
console.error(`Unknown example "${args.example}". Run "create-grist-widget list" to see available examples.`)
|
|
323
|
+
process.exit(1)
|
|
324
|
+
}
|
|
325
|
+
exampleId = resolved.exampleId
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const interactive = Boolean(process.stdout.isTTY) && !args.yes && args.example === undefined
|
|
329
|
+
if (interactive) {
|
|
330
|
+
clack.intro("create-grist-widget init")
|
|
331
|
+
exampleId = await pickExampleInteractively(manifest)
|
|
332
|
+
clack.outro("Initializing...")
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const rawName = typeof args.name === "string" ? args.name : path.basename(cwd)
|
|
336
|
+
if (!isValidName(rawName)) {
|
|
337
|
+
console.error(
|
|
338
|
+
`"${rawName}" is not a valid widget name (derived from the current directory) — pass --name.`
|
|
339
|
+
)
|
|
340
|
+
process.exit(1)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// File-collision policy: identity-bearing files always get edited in place
|
|
344
|
+
// (naturally a no-op wherever the placeholder text is already gone — the
|
|
345
|
+
// common case, a pristine "Use this template" clone); anything else that
|
|
346
|
+
// already exists and differs from the template's own pristine copy blocks
|
|
347
|
+
// the run, listed, unless --force.
|
|
348
|
+
const IDENTITY_FILES = new Set(["package.json", "README.md", "index.html", "src/App.tsx"])
|
|
349
|
+
const templateFiles = listFilesRecursive(TEMPLATE_DIR)
|
|
350
|
+
if (!args.force) {
|
|
351
|
+
const conflicts = []
|
|
352
|
+
for (const rel of templateFiles) {
|
|
353
|
+
if (IDENTITY_FILES.has(rel)) continue
|
|
354
|
+
const destPath = path.join(cwd, rel)
|
|
355
|
+
if (!existsSync(destPath)) continue
|
|
356
|
+
const same = readFileSync(path.join(TEMPLATE_DIR, rel)).equals(readFileSync(destPath))
|
|
357
|
+
if (!same) conflicts.push(rel)
|
|
358
|
+
}
|
|
359
|
+
if (conflicts.length > 0) {
|
|
360
|
+
console.error(
|
|
361
|
+
`init: refusing to overwrite the following files (already exist and differ from the template) — remove them or re-run with --force:\n` +
|
|
362
|
+
conflicts.map((f) => ` ${f}`).join("\n")
|
|
363
|
+
)
|
|
364
|
+
process.exit(1)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for (const rel of templateFiles) {
|
|
369
|
+
const destPath = path.join(cwd, rel)
|
|
370
|
+
mkdirSync(path.dirname(destPath), { recursive: true })
|
|
371
|
+
cpSync(path.join(TEMPLATE_DIR, rel), destPath)
|
|
372
|
+
}
|
|
373
|
+
restoreGitignore(cwd)
|
|
374
|
+
applyIdentityEdits(cwd, rawName)
|
|
375
|
+
|
|
376
|
+
if (exampleId) {
|
|
377
|
+
composer.overlayExampleSrc({ exampleId, destDir: cwd })
|
|
378
|
+
composer.mergeExtraDependencies({ exampleId, destDir: cwd })
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
stampCreateGristWidgetVersion(cwd)
|
|
382
|
+
|
|
383
|
+
// Never auto-commit into a pre-existing repo's history — only take over
|
|
384
|
+
// git entirely when cwd had none to begin with (a fresh, non-git directory,
|
|
385
|
+
// or a real git clone where .git already carries whatever history it has).
|
|
386
|
+
const gitAlreadyExists = existsSync(path.join(cwd, ".git"))
|
|
387
|
+
const committed = gitAlreadyExists ? false : gitInitCommit(cwd)
|
|
388
|
+
const installed = maybeInstall(cwd, {
|
|
389
|
+
noInstall: Boolean(args["no-install"]),
|
|
390
|
+
pm: typeof args.pm === "string" ? args.pm : null,
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
console.log(`\nInitialized ${rawName}/${exampleId ? ` (starting from "${exampleId}")` : ""} in the current directory.\n`)
|
|
394
|
+
if (!installed) console.log(" pnpm install # or npm/yarn")
|
|
395
|
+
console.log(" pnpm dev")
|
|
396
|
+
if (gitAlreadyExists) {
|
|
397
|
+
console.log("\nExisting git repository detected — no commit was made automatically.")
|
|
398
|
+
console.log("Review the changes (git status) and commit when ready.")
|
|
399
|
+
} else if (committed) {
|
|
400
|
+
console.log("\nGit is initialized on main (first commit done) with a dev branch ready.")
|
|
401
|
+
} else {
|
|
402
|
+
console.log("\nCould not create the initial git commit automatically (no git identity")
|
|
403
|
+
console.log('configured?) — run `git config user.name`/`user.email`, then commit yourself.')
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ---------------------------------------------------------------------------
|
|
408
|
+
// use
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
|
|
411
|
+
function cmdUse(argv) {
|
|
412
|
+
const args = parseArgs(argv)
|
|
413
|
+
const exampleId = args._[0]
|
|
414
|
+
if (!exampleId) {
|
|
415
|
+
console.error('Usage: create-grist-widget use <example-id>\nRun "create-grist-widget list" to see available examples.')
|
|
416
|
+
process.exit(1)
|
|
417
|
+
}
|
|
418
|
+
const manifest = loadManifest()
|
|
419
|
+
if (!manifest.some((e) => e.id === exampleId)) {
|
|
420
|
+
console.error(`Unknown example "${exampleId}". Run "create-grist-widget list" to see available examples.`)
|
|
421
|
+
process.exit(1)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const cwd = process.cwd()
|
|
425
|
+
const gate = checkGitGate(cwd)
|
|
426
|
+
if (!gate.ok) {
|
|
427
|
+
if (gate.reason === "no-repo") {
|
|
428
|
+
console.error(
|
|
429
|
+
'use: this command overwrites src/App.tsx and must run inside a git repository so the change is easy to review and undo. Run "git init" first.'
|
|
430
|
+
)
|
|
431
|
+
} else {
|
|
432
|
+
console.error(
|
|
433
|
+
"use: working tree has uncommitted changes — commit or stash them first, so this swap is easy to diff and undo."
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
process.exit(1)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
composer.overlayExampleSrc({ exampleId, destDir: cwd })
|
|
440
|
+
const { merged } = composer.mergeExtraDependencies({ exampleId, destDir: cwd })
|
|
441
|
+
|
|
442
|
+
console.log(`Swapped in "${exampleId}".`)
|
|
443
|
+
console.log("\n git diff # review what changed")
|
|
444
|
+
if (merged) console.log(" pnpm install # new dependencies were added")
|
|
445
|
+
console.log(" pnpm dev")
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
// list
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
|
|
452
|
+
function cmdList(argv) {
|
|
453
|
+
const args = parseArgs(argv)
|
|
454
|
+
const manifest = loadManifest()
|
|
455
|
+
|
|
456
|
+
if (args.json) {
|
|
457
|
+
// A projected, public subset — not the raw manifest, which carries
|
|
458
|
+
// monorepo-internal fields (columns, usesUi, sourcePath,
|
|
459
|
+
// hasExampleDocument) that shouldn't become an accidental CLI contract.
|
|
460
|
+
const projected = manifest.map((e) => ({ id: e.id, title: e.title, description: e.description }))
|
|
461
|
+
console.log(JSON.stringify(projected, null, 2))
|
|
462
|
+
return
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const idWidth = Math.max(2, ...manifest.map((e) => e.id.length))
|
|
466
|
+
for (const e of manifest) {
|
|
467
|
+
console.log(`${e.id.padEnd(idWidth)} ${e.title} — ${e.description}`)
|
|
468
|
+
}
|
|
105
469
|
}
|
|
470
|
+
|
|
471
|
+
// ---------------------------------------------------------------------------
|
|
472
|
+
// dispatch
|
|
473
|
+
// ---------------------------------------------------------------------------
|
|
474
|
+
|
|
475
|
+
async function main() {
|
|
476
|
+
const rawArgv = process.argv.slice(2)
|
|
477
|
+
const [first, ...rest] = rawArgv
|
|
478
|
+
|
|
479
|
+
if (first === "--help" || first === "-h") {
|
|
480
|
+
printHelp()
|
|
481
|
+
return
|
|
482
|
+
}
|
|
483
|
+
if (first === "--version" || first === "-v") {
|
|
484
|
+
console.log(cliPkg.version)
|
|
485
|
+
return
|
|
486
|
+
}
|
|
487
|
+
if (first === "list") return cmdList(rest)
|
|
488
|
+
if (first === "use") return cmdUse(rest)
|
|
489
|
+
if (first === "init") return cmdInit(rest)
|
|
490
|
+
if (first === "create") return cmdCreate(rest)
|
|
491
|
+
// Bare invocation — `npm create grist-widget my-widget` (npm's `create`
|
|
492
|
+
// convention passes argv straight through, no `create` keyword), or no
|
|
493
|
+
// args at all (interactive). Treat the whole argv as arguments to `create`.
|
|
494
|
+
return cmdCreate(rawArgv)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
main().catch((err) => {
|
|
498
|
+
console.error(err?.stack || String(err))
|
|
499
|
+
process.exit(1)
|
|
500
|
+
})
|