@format.dev/cli 0.0.0 → 0.1.1
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 +29 -20
- package/bin/format.mjs +20 -0
- package/dist/config-Dq7O4e8N.mjs +63 -0
- package/dist/config.d.mts +21 -0
- package/dist/config.mjs +3 -0
- package/dist/draw-random-name-BzvSSVqO.d.mts +4 -0
- package/dist/draw-random-name-Dpt6YmUk.mjs +13 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1539 -0
- package/dist/install-BiDrmMgJ.mjs +1486 -0
- package/dist/lockstep.d.mts +27 -0
- package/dist/lockstep.mjs +30 -0
- package/dist/project-pX62Di_9.mjs +124 -0
- package/dist/random-name.d.mts +2 -0
- package/dist/random-name.mjs +3 -0
- package/dist/scaffold-shared.d.mts +2 -0
- package/dist/scaffold-shared.mjs +3 -0
- package/dist/scaffold.d.mts +191 -0
- package/dist/scaffold.mjs +6 -0
- package/dist/shared-BD62UD5L.d.mts +32 -0
- package/dist/shared-CM3vyUlL.mjs +143 -0
- package/env.d.ts +99 -0
- package/package.json +86 -34
- package/templates/html/documents/welcome/data/alternative.json +3 -0
- package/templates/html/documents/welcome/data/default.json +3 -0
- package/templates/html/documents/welcome/index.html +32 -0
- package/templates/html/documents/welcome/index.ts +4 -0
- package/templates/html/documents/welcome/pages/Close/close.html +9 -0
- package/templates/html/documents/welcome/pages/Cover/cover.html +13 -0
- package/templates/html/documents/welcome/pages/FeatureShowcase/feature-showcase.css +296 -0
- package/templates/html/documents/welcome/pages/FeatureShowcase/feature-showcase.html +296 -0
- package/templates/html/documents/welcome/pages/NextSteps/next-steps.css +107 -0
- package/templates/html/documents/welcome/pages/NextSteps/next-steps.html +398 -0
- package/templates/html/documents/welcome/styles.css +214 -0
- package/templates/html/format.config.json +4 -0
- package/templates/html/package.json +15 -0
- package/templates/html/tsconfig.json +3 -0
- package/templates/react/documents/welcome/components/ButtonLink.tsx +29 -0
- package/templates/react/documents/welcome/data/alternative.json +3 -0
- package/templates/react/documents/welcome/data/default.json +3 -0
- package/templates/react/documents/welcome/data/schema.ts +7 -0
- package/templates/react/documents/welcome/icons/propeller-asterisk.svg +1 -0
- package/templates/react/documents/welcome/index.tsx +59 -0
- package/templates/react/documents/welcome/logos/_logo-base.tsx +9 -0
- package/templates/react/documents/welcome/logos/aws-lambda.tsx +22 -0
- package/templates/react/documents/welcome/logos/bun.tsx +30 -0
- package/templates/react/documents/welcome/logos/cloudflare-workers.tsx +52 -0
- package/templates/react/documents/welcome/logos/css-modules.tsx +54 -0
- package/templates/react/documents/welcome/logos/css-official.tsx +18 -0
- package/templates/react/documents/welcome/logos/deno.tsx +17 -0
- package/templates/react/documents/welcome/logos/linaria.tsx +18 -0
- package/templates/react/documents/welcome/logos/node.tsx +47 -0
- package/templates/react/documents/welcome/logos/panda-css.tsx +18 -0
- package/templates/react/documents/welcome/logos/sass.tsx +12 -0
- package/templates/react/documents/welcome/logos/tailwind.tsx +45 -0
- package/templates/react/documents/welcome/logos/vanilla-extract.tsx +74 -0
- package/templates/react/documents/welcome/logos/vercel.tsx +17 -0
- package/templates/react/documents/welcome/pages/Close/ClosePage.tsx +13 -0
- package/templates/react/documents/welcome/pages/Cover/CoverPage.tsx +17 -0
- package/templates/react/documents/welcome/pages/FeatureShowcase/FeatureShowcasePage.tsx +221 -0
- package/templates/react/documents/welcome/pages/FeatureShowcase/feature-showcase.css +296 -0
- package/templates/react/documents/welcome/pages/FeatureShowcase/mountains.ts +27 -0
- package/templates/react/documents/welcome/pages/NextSteps/NextStepsPage.tsx +146 -0
- package/templates/react/documents/welcome/pages/NextSteps/next-steps.css +107 -0
- package/templates/react/documents/welcome/styles.css +214 -0
- package/templates/react/documents/welcome/types.ts +4 -0
- package/templates/react/format.config.json +4 -0
- package/templates/react/package.json +22 -0
- package/templates/react/tsconfig.json +6 -0
- package/templates/shared/README.md +18 -0
- package/templates/shared/_env +1 -0
- package/templates/shared/_gitignore +3 -0
- package/templates/shared/documents/welcome/assets/deploy.svg +268 -0
- package/templates/shared/documents/welcome/assets/logo.svg +22 -0
- package/templates/shared/documents/welcome/assets/mona-sans.woff2 +0 -0
- package/templates/shared/documents/welcome/assets/styling.svg +59 -0
- package/templates/vue/documents/welcome/components/ButtonLink.vue +28 -0
- package/templates/vue/documents/welcome/data/alternative.json +3 -0
- package/templates/vue/documents/welcome/data/default.json +3 -0
- package/templates/vue/documents/welcome/data/schema.ts +7 -0
- package/templates/vue/documents/welcome/index.vue +48 -0
- package/templates/vue/documents/welcome/logos/aws-lambda.vue +18 -0
- package/templates/vue/documents/welcome/logos/bun.vue +26 -0
- package/templates/vue/documents/welcome/logos/cloudflare-workers.vue +48 -0
- package/templates/vue/documents/welcome/logos/css-modules.vue +49 -0
- package/templates/vue/documents/welcome/logos/css-official.vue +14 -0
- package/templates/vue/documents/welcome/logos/deno.vue +13 -0
- package/templates/vue/documents/welcome/logos/linaria.vue +14 -0
- package/templates/vue/documents/welcome/logos/node.vue +43 -0
- package/templates/vue/documents/welcome/logos/panda-css.vue +14 -0
- package/templates/vue/documents/welcome/logos/sass.vue +8 -0
- package/templates/vue/documents/welcome/logos/tailwind.vue +41 -0
- package/templates/vue/documents/welcome/logos/vanilla-extract.vue +70 -0
- package/templates/vue/documents/welcome/logos/vercel.vue +13 -0
- package/templates/vue/documents/welcome/pages/Close/ClosePage.vue +11 -0
- package/templates/vue/documents/welcome/pages/Cover/CoverPage.vue +19 -0
- package/templates/vue/documents/welcome/pages/FeatureShowcase/FeatureShowcasePage.vue +217 -0
- package/templates/vue/documents/welcome/pages/FeatureShowcase/feature-showcase.css +296 -0
- package/templates/vue/documents/welcome/pages/FeatureShowcase/mountains.ts +27 -0
- package/templates/vue/documents/welcome/pages/NextSteps/NextStepsPage.vue +146 -0
- package/templates/vue/documents/welcome/pages/NextSteps/next-steps.css +107 -0
- package/templates/vue/documents/welcome/styles.css +214 -0
- package/templates/vue/documents/welcome/types.ts +4 -0
- package/templates/vue/format.config.json +4 -0
- package/templates/vue/package.json +19 -0
- package/templates/vue/tsconfig.json +7 -0
- package/tsconfig.recommended.json +22 -0
- package/index.d.ts +0 -2
- package/index.js +0 -3
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1539 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { a as readConfigFile, i as findConfigPath, o as writeConfigVersion, r as detectIndent, s as CliError } from "./config-Dq7O4e8N.mjs";
|
|
3
|
+
import { C as joinNames, D as commandForPlatform, E as truncateForSpinner, O as detectPackageManager, S as installManifest, T as runInstall$2, c as getDocumentsDir, g as validateDocumentName, i as generateRandomName, k as installCommand, l as readExistingDocumentNames, n as maybeSetupProject, r as writeDocumentDependencies, s as createDocument, w as writeDependenciesToManifest } from "./install-BiDrmMgJ.mjs";
|
|
4
|
+
import { a as rewriteFormatDependencies, i as mayHaveBreakingChanges, n as isExactVersion, r as isNewer, t as loadProjectState } from "./project-pX62Di_9.mjs";
|
|
5
|
+
import { a as DEFAULT_STYLING, f as STYLING_LABELS, h as SUPPORTED_FRAMEWORKS, l as SCHEMA_KINDS, n as DEFAULT_PAGE_SIZE_ID, s as PAGE_SIZES, u as SCHEMA_LABELS, v as isValidDimension, y as methodsForFramework } from "./shared-CM3vyUlL.mjs";
|
|
6
|
+
import "./draw-random-name-Dpt6YmUk.mjs";
|
|
7
|
+
import { Command, Option } from "commander";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import fs, { readFile } from "node:fs/promises";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import pc from "picocolors";
|
|
15
|
+
import { cancel, confirm, intro, isCancel, log, outro, select, spinner, text } from "@clack/prompts";
|
|
16
|
+
|
|
17
|
+
//#region src/cli/cli-spec.json
|
|
18
|
+
var cli_spec_default = { commands: [
|
|
19
|
+
{
|
|
20
|
+
"name": "new",
|
|
21
|
+
"description": "Create a new Format project or document.",
|
|
22
|
+
"options": [],
|
|
23
|
+
"commands": [{
|
|
24
|
+
"name": "project",
|
|
25
|
+
"description": "Scaffold a new Format project. This works best for a completely empty codebase. For existing codebases, use `format init`.",
|
|
26
|
+
"arguments": [{
|
|
27
|
+
"name": "directory",
|
|
28
|
+
"description": "Where to create the project. Prompted if omitted.",
|
|
29
|
+
"required": false
|
|
30
|
+
}],
|
|
31
|
+
"options": [
|
|
32
|
+
{
|
|
33
|
+
"field": "framework",
|
|
34
|
+
"flags": "--framework <framework>",
|
|
35
|
+
"description": "The framework to build your documents with.",
|
|
36
|
+
"type": "string",
|
|
37
|
+
"choices": [
|
|
38
|
+
"react",
|
|
39
|
+
"vue",
|
|
40
|
+
"html"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"field": "install",
|
|
45
|
+
"flags": "--no-install",
|
|
46
|
+
"description": "Don't install dependencies after creating the project.",
|
|
47
|
+
"type": "boolean",
|
|
48
|
+
"defaultValue": true
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"field": "yes",
|
|
52
|
+
"flags": "-y, --yes",
|
|
53
|
+
"description": "Skip all prompts and use the defaults for every option that isn't provided.",
|
|
54
|
+
"type": "boolean",
|
|
55
|
+
"defaultValue": false
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}, {
|
|
59
|
+
"name": "document",
|
|
60
|
+
"description": "Scaffold a new document directory with an entry file, a stylesheet, and a default data variant.",
|
|
61
|
+
"arguments": [{
|
|
62
|
+
"name": "name",
|
|
63
|
+
"description": "The document directory name (kebab-case recommended). Omit it to be prompted with a random suggestion.",
|
|
64
|
+
"required": false
|
|
65
|
+
}],
|
|
66
|
+
"options": [
|
|
67
|
+
{
|
|
68
|
+
"field": "styling",
|
|
69
|
+
"flags": "--styling <method>",
|
|
70
|
+
"description": "Styling method for the document. Choices are filtered to your project framework. See the [styling docs](/docs/studio/styling) for more information.",
|
|
71
|
+
"defaultValue": "css",
|
|
72
|
+
"type": "string",
|
|
73
|
+
"choices": [
|
|
74
|
+
"css",
|
|
75
|
+
"css-modules",
|
|
76
|
+
"vue-sfc-scoped",
|
|
77
|
+
"scss",
|
|
78
|
+
"tailwind",
|
|
79
|
+
"linaria",
|
|
80
|
+
"vanilla-extract",
|
|
81
|
+
"panda-css"
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"field": "schema",
|
|
86
|
+
"flags": "--schema <library>",
|
|
87
|
+
"description": "Scaffold a `data/schema.ts` with this validation library, or \"none\" for no schema. Format validates any Standard Schema compliant library, so you can also wire up others by hand.",
|
|
88
|
+
"type": "string",
|
|
89
|
+
"choices": [
|
|
90
|
+
"none",
|
|
91
|
+
"zod",
|
|
92
|
+
"valibot",
|
|
93
|
+
"joi",
|
|
94
|
+
"yup"
|
|
95
|
+
],
|
|
96
|
+
"defaultValue": "none"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"field": "data",
|
|
100
|
+
"flags": "--data <path>",
|
|
101
|
+
"description": "Path to a JSON file to seed the default data variant. Its shape is used to infer the schema and the entry's data type.",
|
|
102
|
+
"type": "string"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"field": "size",
|
|
106
|
+
"flags": "--size <id>",
|
|
107
|
+
"description": "Page size for the document. A4 is the default; pass \"custom\" with --width and --height for an exact size.",
|
|
108
|
+
"type": "string",
|
|
109
|
+
"defaultValue": "a4",
|
|
110
|
+
"choices": [
|
|
111
|
+
"a3",
|
|
112
|
+
"a4",
|
|
113
|
+
"a5",
|
|
114
|
+
"letter",
|
|
115
|
+
"slide-16-9",
|
|
116
|
+
"slide-4-3",
|
|
117
|
+
"custom"
|
|
118
|
+
]
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"field": "width",
|
|
122
|
+
"flags": "--width <px>",
|
|
123
|
+
"description": "Custom page width in CSS pixels. Requires --height; ignored unless --size is \"custom\" or omitted.",
|
|
124
|
+
"type": "string"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"field": "height",
|
|
128
|
+
"flags": "--height <px>",
|
|
129
|
+
"description": "Custom page height in CSS pixels. Requires --width; ignored unless --size is \"custom\" or omitted.",
|
|
130
|
+
"type": "string"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"field": "install",
|
|
134
|
+
"flags": "--no-install",
|
|
135
|
+
"description": "Don't install missing styling/schema dependencies after creating the document.",
|
|
136
|
+
"type": "boolean",
|
|
137
|
+
"defaultValue": true
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"field": "yes",
|
|
141
|
+
"flags": "-y, --yes",
|
|
142
|
+
"description": "Skip all prompts and use the defaults for every option that isn't provided.",
|
|
143
|
+
"type": "boolean",
|
|
144
|
+
"defaultValue": false
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"field": "empty",
|
|
148
|
+
"flags": "--empty",
|
|
149
|
+
"description": "Scaffold the most minimal document. Can be combined with other flags.",
|
|
150
|
+
"type": "boolean",
|
|
151
|
+
"defaultValue": false
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
}]
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"name": "init",
|
|
158
|
+
"description": "Set up Format in an existing codebase. This script writes a Format config file, adds npm scripts, gitignore entries, the Format packages, and optionally a first document. Each step will confirm before editing anything.",
|
|
159
|
+
"options": [
|
|
160
|
+
{
|
|
161
|
+
"field": "framework",
|
|
162
|
+
"flags": "--framework <framework>",
|
|
163
|
+
"description": "The framework to build your documents with — React, Vue or HTML. Detected from your dependencies when omitted.",
|
|
164
|
+
"type": "string",
|
|
165
|
+
"choices": [
|
|
166
|
+
"react",
|
|
167
|
+
"vue",
|
|
168
|
+
"html"
|
|
169
|
+
]
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"field": "rootDir",
|
|
173
|
+
"flags": "--root-dir <dir>",
|
|
174
|
+
"description": "Where to install the Format project into. The path you give will be set in your Format config's `rootDir`.",
|
|
175
|
+
"type": "string"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"field": "install",
|
|
179
|
+
"flags": "--no-install",
|
|
180
|
+
"description": "Don't automatically add or install any Format packages.",
|
|
181
|
+
"type": "boolean",
|
|
182
|
+
"defaultValue": true
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"field": "scripts",
|
|
186
|
+
"flags": "--no-scripts",
|
|
187
|
+
"description": "Don't add the npm dev and npm compile scripts to your package.json. Existing dev and compile script names are never overwritten regardless, If there's a name clash, they get a `format:` prefix.",
|
|
188
|
+
"type": "boolean",
|
|
189
|
+
"defaultValue": true
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"field": "gitignore",
|
|
193
|
+
"flags": "--no-gitignore",
|
|
194
|
+
"description": "Don't append the `_generated` and `.env` entries to .gitignore. Existing lines are never touched.",
|
|
195
|
+
"type": "boolean",
|
|
196
|
+
"defaultValue": true
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"field": "document",
|
|
200
|
+
"flags": "--no-document",
|
|
201
|
+
"description": "Don't offer to create a first document.",
|
|
202
|
+
"type": "boolean",
|
|
203
|
+
"defaultValue": true
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"field": "yes",
|
|
207
|
+
"flags": "-y, --yes",
|
|
208
|
+
"description": "Skip all prompts and use the defaults for every option that isn't provided.",
|
|
209
|
+
"type": "boolean",
|
|
210
|
+
"defaultValue": false
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"name": "add",
|
|
216
|
+
"description": "Adds and installs Format packages. Packages will be installed at the version defined in your config file.",
|
|
217
|
+
"arguments": [{
|
|
218
|
+
"name": "packages...",
|
|
219
|
+
"description": "One or more Format packages. Shorthands map to the scope: `zip` means `@format.dev/zip`.",
|
|
220
|
+
"required": true
|
|
221
|
+
}],
|
|
222
|
+
"options": []
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"name": "remove",
|
|
226
|
+
"description": "Remove Format packages from your project. The counterpart to `format add`.",
|
|
227
|
+
"arguments": [{
|
|
228
|
+
"name": "packages...",
|
|
229
|
+
"description": "One or more Format packages. Shorthands map to the scope: `zip` means `@format.dev/zip`.",
|
|
230
|
+
"required": true
|
|
231
|
+
}],
|
|
232
|
+
"options": []
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"name": "update",
|
|
236
|
+
"description": "Move your project to a different Format version. With no arguments, the version set in your Format config file will be used. The argument \"latest\" updates to the latest available version and pins it.",
|
|
237
|
+
"arguments": [{
|
|
238
|
+
"name": "version",
|
|
239
|
+
"description": "An exact release like `0.1.2`, or `latest` for the newest. Omit it to apply the release already pinned in your config. The version field is the source of truth, and editing it by hand then running `format update` is a supported flow.",
|
|
240
|
+
"required": false
|
|
241
|
+
}],
|
|
242
|
+
"options": []
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"name": "version",
|
|
246
|
+
"description": "Print the current pinned Format version for your project, check every installed Format package matches it exactly, and mention when a newer version is available. Exits non-zero on any drift, so can be used in CI as a gate.",
|
|
247
|
+
"options": []
|
|
248
|
+
}
|
|
249
|
+
] };
|
|
250
|
+
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region src/versioning/js-version.ts
|
|
253
|
+
const jsVersion = "0.1.1";
|
|
254
|
+
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/versioning/registry.ts
|
|
257
|
+
const REFERENCE_PACKAGE = "@format.dev/studio";
|
|
258
|
+
function registryBaseUrl() {
|
|
259
|
+
return (process.env.npm_config_registry || "https://registry.npmjs.org").replace(/\/$/, "");
|
|
260
|
+
}
|
|
261
|
+
async function fetchLatestVersion() {
|
|
262
|
+
const url = `${registryBaseUrl()}/${encodeURIComponent(REFERENCE_PACKAGE)}/latest`;
|
|
263
|
+
let response;
|
|
264
|
+
try {
|
|
265
|
+
response = await fetch(url, { headers: { accept: "application/json" } });
|
|
266
|
+
} catch {
|
|
267
|
+
throw new CliError("Could not reach the npm registry. Check your connection and try again.");
|
|
268
|
+
}
|
|
269
|
+
if (!response.ok) throw new CliError(`npm registry request failed (${response.status}) while resolving the latest Format version.`);
|
|
270
|
+
const data = await response.json();
|
|
271
|
+
if (!data.version) throw new CliError("Unexpected npm registry response while resolving the latest Format version.");
|
|
272
|
+
return data.version;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
//#endregion
|
|
276
|
+
//#region src/versioning/update-check.ts
|
|
277
|
+
const CACHE_FILE_NAME = "update-check.json";
|
|
278
|
+
const CACHE_MAX_AGE_MS = 1440 * 60 * 1e3;
|
|
279
|
+
const CHANGELOG_URL = "https://format.dev/changelog";
|
|
280
|
+
function cacheDir() {
|
|
281
|
+
const home = os.homedir();
|
|
282
|
+
if (process.env.XDG_CACHE_HOME) return join(process.env.XDG_CACHE_HOME, "format");
|
|
283
|
+
if (process.platform === "darwin") return join(home, "Library", "Caches", "format");
|
|
284
|
+
if (process.platform === "win32") return join(process.env.LOCALAPPDATA ?? join(home, "AppData", "Local"), "format");
|
|
285
|
+
return join(home, ".cache", "format");
|
|
286
|
+
}
|
|
287
|
+
function cacheFilePath() {
|
|
288
|
+
return join(cacheDir(), CACHE_FILE_NAME);
|
|
289
|
+
}
|
|
290
|
+
async function readCache() {
|
|
291
|
+
try {
|
|
292
|
+
const raw = await fs.readFile(cacheFilePath(), "utf8");
|
|
293
|
+
const parsed = JSON.parse(raw);
|
|
294
|
+
if (typeof parsed.checkedAt !== "number" || typeof parsed.latest !== "string") return null;
|
|
295
|
+
return {
|
|
296
|
+
checkedAt: parsed.checkedAt,
|
|
297
|
+
latest: parsed.latest
|
|
298
|
+
};
|
|
299
|
+
} catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async function writeCache(latest) {
|
|
304
|
+
try {
|
|
305
|
+
await fs.mkdir(cacheDir(), { recursive: true });
|
|
306
|
+
const cache = {
|
|
307
|
+
checkedAt: Date.now(),
|
|
308
|
+
latest
|
|
309
|
+
};
|
|
310
|
+
await fs.writeFile(cacheFilePath(), `${JSON.stringify(cache)}\n`, "utf8");
|
|
311
|
+
} catch {}
|
|
312
|
+
}
|
|
313
|
+
async function resolveLatest() {
|
|
314
|
+
const cached = await readCache();
|
|
315
|
+
if (cached && Date.now() - cached.checkedAt < CACHE_MAX_AGE_MS) return cached.latest;
|
|
316
|
+
try {
|
|
317
|
+
const latest = await fetchLatestVersion();
|
|
318
|
+
await writeCache(latest);
|
|
319
|
+
return latest;
|
|
320
|
+
} catch {
|
|
321
|
+
return cached?.latest ?? null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function printNudge(pinnedVersion, latest) {
|
|
325
|
+
console.log("");
|
|
326
|
+
console.log(`A newer Format release is available: ${latest} (you're on ${pinnedVersion}).`);
|
|
327
|
+
if (mayHaveBreakingChanges(pinnedVersion, latest)) console.log(`This release may include breaking changes. See ${CHANGELOG_URL} before updating.`);
|
|
328
|
+
console.log("Run `format update latest` to move to it.");
|
|
329
|
+
}
|
|
330
|
+
async function reportNewerRelease(pinnedVersion) {
|
|
331
|
+
const latest = await resolveLatest();
|
|
332
|
+
if (latest && isNewer(latest, pinnedVersion)) printNudge(pinnedVersion, latest);
|
|
333
|
+
}
|
|
334
|
+
function startUpdateCheck(pinnedVersion) {
|
|
335
|
+
const pending = resolveLatest().catch(() => null);
|
|
336
|
+
return { report: async () => {
|
|
337
|
+
const latest = await pending;
|
|
338
|
+
if (latest && isNewer(latest, pinnedVersion)) printNudge(pinnedVersion, latest);
|
|
339
|
+
} };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
//#endregion
|
|
343
|
+
//#region src/studio.ts
|
|
344
|
+
const STUDIO_PACKAGE = "@format.dev/studio";
|
|
345
|
+
const COMPILE_PACKAGE = "@format.dev/compile";
|
|
346
|
+
const STUDIO_BIN_NAME = "format-studio";
|
|
347
|
+
const COMPILE_BIN_NAME = "format-compile";
|
|
348
|
+
function targetForCommand(commandName) {
|
|
349
|
+
if (commandName === "compile") return {
|
|
350
|
+
packageName: COMPILE_PACKAGE,
|
|
351
|
+
binName: COMPILE_BIN_NAME
|
|
352
|
+
};
|
|
353
|
+
return {
|
|
354
|
+
packageName: STUDIO_PACKAGE,
|
|
355
|
+
binName: STUDIO_BIN_NAME
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function findStudioPackageJson(projectDir, packageName) {
|
|
359
|
+
let dir = resolve(projectDir);
|
|
360
|
+
for (;;) {
|
|
361
|
+
const candidate = join(dir, "node_modules", packageName, "package.json");
|
|
362
|
+
if (existsSync(candidate)) return candidate;
|
|
363
|
+
const parent = dirname(dir);
|
|
364
|
+
if (parent === dir) return null;
|
|
365
|
+
dir = parent;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async function findLocalStudio(projectDir, target) {
|
|
369
|
+
const packageJsonPath = findStudioPackageJson(projectDir, target.packageName);
|
|
370
|
+
if (!packageJsonPath) return null;
|
|
371
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
|
|
372
|
+
const binRelativePath = packageJson.bin?.[target.binName];
|
|
373
|
+
if (!binRelativePath) throw new CliError(`The installed ${target.packageName} (${packageJson.version}) does not expose a "${target.binName}" bin. Run \`format update\` to align your packages.`);
|
|
374
|
+
return {
|
|
375
|
+
binPath: resolve(dirname(packageJsonPath), binRelativePath),
|
|
376
|
+
version: packageJson.version
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
async function runStudioCommand(cwd, argv) {
|
|
380
|
+
const state = await loadProjectState(cwd);
|
|
381
|
+
const updateCheck = argv[0] === "dev" && state.pinnedVersion ? startUpdateCheck(state.pinnedVersion) : null;
|
|
382
|
+
const exitCode = await spawnStudioForProject(state, argv);
|
|
383
|
+
if (updateCheck) await updateCheck.report();
|
|
384
|
+
return exitCode;
|
|
385
|
+
}
|
|
386
|
+
async function spawnStudioForProject(state, argv) {
|
|
387
|
+
const target = targetForCommand(argv[0]);
|
|
388
|
+
const localStudio = await findLocalStudio(state.projectDir, target);
|
|
389
|
+
const keepAliveOnSignal = argv[0] === "dev";
|
|
390
|
+
if (localStudio) {
|
|
391
|
+
if (state.pinnedVersion !== null && localStudio.version !== state.pinnedVersion) throw new CliError(`Installed ${target.packageName} is ${localStudio.version} but format.config.json pins ${state.pinnedVersion}. Run \`format update\` to bring them in line.`);
|
|
392
|
+
return spawnStudio(process.execPath, [localStudio.binPath, ...argv], state.projectDir, keepAliveOnSignal);
|
|
393
|
+
}
|
|
394
|
+
if (!state.pinnedVersion) throw new CliError(`No "version" field in ${state.configFile.filepath} and no local ${target.packageName} install. Run \`format update\` to pin the latest Format release.`);
|
|
395
|
+
return spawnStudio("npm", [
|
|
396
|
+
"exec",
|
|
397
|
+
"-y",
|
|
398
|
+
"--prefer-offline",
|
|
399
|
+
"--",
|
|
400
|
+
`${target.packageName}@${state.pinnedVersion}`,
|
|
401
|
+
...argv
|
|
402
|
+
], state.projectDir, keepAliveOnSignal);
|
|
403
|
+
}
|
|
404
|
+
async function spawnStudio(command, args, cwd, keepAliveOnSignal = false) {
|
|
405
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
406
|
+
const child = spawn(command, args, {
|
|
407
|
+
cwd,
|
|
408
|
+
stdio: "inherit",
|
|
409
|
+
shell: process.platform === "win32"
|
|
410
|
+
});
|
|
411
|
+
let interrupts = 0;
|
|
412
|
+
const onSignal = () => {
|
|
413
|
+
interrupts += 1;
|
|
414
|
+
if (interrupts >= 2) process.exit(130);
|
|
415
|
+
};
|
|
416
|
+
if (keepAliveOnSignal) process.on("SIGINT", onSignal);
|
|
417
|
+
const cleanup = () => {
|
|
418
|
+
if (keepAliveOnSignal) process.off("SIGINT", onSignal);
|
|
419
|
+
};
|
|
420
|
+
child.on("error", (error) => {
|
|
421
|
+
cleanup();
|
|
422
|
+
rejectPromise(new CliError(`Failed to launch Studio: ${error.message}`));
|
|
423
|
+
});
|
|
424
|
+
child.on("exit", (code, signal) => {
|
|
425
|
+
cleanup();
|
|
426
|
+
if (signal) {
|
|
427
|
+
resolvePromise(1);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
resolvePromise(code ?? 0);
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
//#endregion
|
|
436
|
+
//#region src/scaffold/project.ts
|
|
437
|
+
function getBuildMode() {
|
|
438
|
+
return "prod";
|
|
439
|
+
}
|
|
440
|
+
const WORKSPACE_VERSION = "workspace:*";
|
|
441
|
+
function formatDependencyVersion(version) {
|
|
442
|
+
return getBuildMode() === "prod" ? version : WORKSPACE_VERSION;
|
|
443
|
+
}
|
|
444
|
+
function devOnlyWorkspacePackages() {
|
|
445
|
+
if (getBuildMode() === "prod") return [];
|
|
446
|
+
return [STUDIO_PACKAGE, COMPILE_PACKAGE];
|
|
447
|
+
}
|
|
448
|
+
const _dirname = dirname(fileURLToPath(import.meta.url));
|
|
449
|
+
function findCliPackageRoot() {
|
|
450
|
+
let dir = _dirname;
|
|
451
|
+
for (;;) {
|
|
452
|
+
if (existsSync(join(dir, "package.json"))) return dir;
|
|
453
|
+
const parent = dirname(dir);
|
|
454
|
+
if (parent === dir) throw new CliError("Could not locate the @format.dev/cli package root.");
|
|
455
|
+
dir = parent;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const DOTFILE_TEMPLATES = {
|
|
459
|
+
_gitignore: ".gitignore",
|
|
460
|
+
_env: ".env"
|
|
461
|
+
};
|
|
462
|
+
async function restoreDotfiles(dirPath) {
|
|
463
|
+
for (const [templateName, dotfileName] of Object.entries(DOTFILE_TEMPLATES)) {
|
|
464
|
+
const templateFile = join(dirPath, templateName);
|
|
465
|
+
if (existsSync(templateFile)) await fs.rename(templateFile, join(dirPath, dotfileName));
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function resolveTemplatePath(name) {
|
|
469
|
+
const templatePath = join(findCliPackageRoot(), "templates", name);
|
|
470
|
+
if (!existsSync(templatePath)) throw new CliError(`Unknown project template "${name}".`);
|
|
471
|
+
return templatePath;
|
|
472
|
+
}
|
|
473
|
+
function deriveProjectName(dirInput) {
|
|
474
|
+
const name = basename(dirInput.trim().replace(/[\\/]+$/, ""));
|
|
475
|
+
return name === "" || name === "." ? "my-format-project" : name;
|
|
476
|
+
}
|
|
477
|
+
function replaceWorkspaceVersions(deps, version) {
|
|
478
|
+
if (!deps) return deps;
|
|
479
|
+
for (const [depName, depVersion] of Object.entries(deps)) if (depVersion === "workspace:*" && depName.startsWith("@format.dev/")) deps[depName] = version;
|
|
480
|
+
return deps;
|
|
481
|
+
}
|
|
482
|
+
async function updateGeneratedPackageJson(args) {
|
|
483
|
+
const { dirPath, projectName, buildMode, version } = args;
|
|
484
|
+
const packageJsonPath = resolve(dirPath, "package.json");
|
|
485
|
+
const raw = await fs.readFile(packageJsonPath, "utf8");
|
|
486
|
+
const packageJson = JSON.parse(raw);
|
|
487
|
+
packageJson.name = projectName;
|
|
488
|
+
for (const name of devOnlyWorkspacePackages()) packageJson.devDependencies = {
|
|
489
|
+
...packageJson.devDependencies,
|
|
490
|
+
[name]: WORKSPACE_VERSION
|
|
491
|
+
};
|
|
492
|
+
if (buildMode === "prod") {
|
|
493
|
+
packageJson.dependencies = replaceWorkspaceVersions(packageJson.dependencies, version);
|
|
494
|
+
packageJson.devDependencies = replaceWorkspaceVersions(packageJson.devDependencies, version);
|
|
495
|
+
packageJson.optionalDependencies = replaceWorkspaceVersions(packageJson.optionalDependencies, version);
|
|
496
|
+
packageJson.peerDependencies = replaceWorkspaceVersions(packageJson.peerDependencies, version);
|
|
497
|
+
}
|
|
498
|
+
await fs.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, " ")}\n`, "utf8");
|
|
499
|
+
}
|
|
500
|
+
async function updateGeneratedFormatConfig(args) {
|
|
501
|
+
const { dirPath, version } = args;
|
|
502
|
+
const configPath = resolve(dirPath, "format.config.json");
|
|
503
|
+
const raw = await fs.readFile(configPath, "utf8");
|
|
504
|
+
const config = JSON.parse(raw);
|
|
505
|
+
const withVersion = {
|
|
506
|
+
$schema: config.$schema,
|
|
507
|
+
version,
|
|
508
|
+
...config
|
|
509
|
+
};
|
|
510
|
+
await fs.writeFile(configPath, `${JSON.stringify(withVersion, null, " ")}\n`, "utf8");
|
|
511
|
+
}
|
|
512
|
+
function isCommandNotFoundError(error) {
|
|
513
|
+
return !!(error && "code" in error && error.code === "ENOENT");
|
|
514
|
+
}
|
|
515
|
+
async function installProjectDependencies(args) {
|
|
516
|
+
const { dirPath, preferredPackageManager, onProgress } = args;
|
|
517
|
+
const attempt = async (packageManager) => {
|
|
518
|
+
const { command, args: commandArgs } = installCommand(packageManager);
|
|
519
|
+
return runInstall$2({
|
|
520
|
+
command,
|
|
521
|
+
args: commandArgs,
|
|
522
|
+
cwd: dirPath,
|
|
523
|
+
onProgress
|
|
524
|
+
});
|
|
525
|
+
};
|
|
526
|
+
const first = await attempt(preferredPackageManager);
|
|
527
|
+
if (first.ok) return {
|
|
528
|
+
ok: true,
|
|
529
|
+
packageManagerUsed: preferredPackageManager,
|
|
530
|
+
fellBackToNpm: false
|
|
531
|
+
};
|
|
532
|
+
if (!(preferredPackageManager !== "npm" && isCommandNotFoundError(first.error))) return {
|
|
533
|
+
ok: false,
|
|
534
|
+
packageManagerUsed: preferredPackageManager,
|
|
535
|
+
fellBackToNpm: false,
|
|
536
|
+
message: first.error?.message ?? `${preferredPackageManager} exited with code ${first.code}`,
|
|
537
|
+
output: first.output
|
|
538
|
+
};
|
|
539
|
+
const fallback = await attempt("npm");
|
|
540
|
+
if (fallback.ok) return {
|
|
541
|
+
ok: true,
|
|
542
|
+
packageManagerUsed: "npm",
|
|
543
|
+
fellBackToNpm: true
|
|
544
|
+
};
|
|
545
|
+
return {
|
|
546
|
+
ok: false,
|
|
547
|
+
packageManagerUsed: "npm",
|
|
548
|
+
fellBackToNpm: true,
|
|
549
|
+
message: fallback.error?.message ?? `npm exited with code ${fallback.code}`,
|
|
550
|
+
output: fallback.output
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
async function templateUsesWorkspaceDeps(dirPath) {
|
|
554
|
+
try {
|
|
555
|
+
return (await fs.readFile(resolve(dirPath, "package.json"), "utf8")).includes("workspace:");
|
|
556
|
+
} catch {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
async function scaffoldProject(args) {
|
|
561
|
+
const { directory, framework, cwd } = args;
|
|
562
|
+
const dirPath = resolve(cwd, directory);
|
|
563
|
+
const projectName = deriveProjectName(directory);
|
|
564
|
+
const buildMode = getBuildMode();
|
|
565
|
+
const version = jsVersion;
|
|
566
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
567
|
+
try {
|
|
568
|
+
const sharedPath = resolveTemplatePath("shared");
|
|
569
|
+
await fs.cp(sharedPath, dirPath, { recursive: true });
|
|
570
|
+
} catch {}
|
|
571
|
+
const templatePath = resolveTemplatePath(framework);
|
|
572
|
+
await fs.cp(templatePath, dirPath, { recursive: true });
|
|
573
|
+
await restoreDotfiles(dirPath);
|
|
574
|
+
await updateGeneratedPackageJson({
|
|
575
|
+
dirPath,
|
|
576
|
+
projectName,
|
|
577
|
+
buildMode,
|
|
578
|
+
version
|
|
579
|
+
});
|
|
580
|
+
await updateGeneratedFormatConfig({
|
|
581
|
+
dirPath,
|
|
582
|
+
version
|
|
583
|
+
});
|
|
584
|
+
return {
|
|
585
|
+
dirPath,
|
|
586
|
+
projectName
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
//#endregion
|
|
591
|
+
//#region src/cli/commands/add.ts
|
|
592
|
+
const RUNTIME_PACKAGES = ["@format.dev/client", "@format.dev/zip"];
|
|
593
|
+
function normalisePackageName$1(name) {
|
|
594
|
+
if (name.startsWith("@format.dev/")) return name;
|
|
595
|
+
return `@format.dev/${name}`;
|
|
596
|
+
}
|
|
597
|
+
function installArgs(packageManager, packages, dev) {
|
|
598
|
+
const saveFlag = dev ? "-D" : "";
|
|
599
|
+
switch (packageManager) {
|
|
600
|
+
case "npm": return [
|
|
601
|
+
"install",
|
|
602
|
+
...dev ? ["--save-dev"] : [],
|
|
603
|
+
...packages
|
|
604
|
+
];
|
|
605
|
+
default: return [
|
|
606
|
+
"add",
|
|
607
|
+
...saveFlag ? [saveFlag] : [],
|
|
608
|
+
...packages
|
|
609
|
+
];
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
async function addCommand(cwd, packages) {
|
|
613
|
+
if (packages.length === 0) throw new CliError("Usage: format add <package> [...] — e.g. `format add zip` or `format add \"@format.dev/client\"`.");
|
|
614
|
+
const state = await loadProjectState(cwd);
|
|
615
|
+
if (!state.packageJsonPath) throw new CliError("No package.json found next to the config — nothing to install into.");
|
|
616
|
+
const pinnedVersion = state.pinnedVersion;
|
|
617
|
+
if (!pinnedVersion) throw new CliError(`${state.configFile.filepath} is missing its required "version" field. Run \`format init\` to set the project up.`);
|
|
618
|
+
const names = packages.map(normalisePackageName$1);
|
|
619
|
+
const runtime = names.filter((name) => RUNTIME_PACKAGES.includes(name));
|
|
620
|
+
const dev = names.filter((name) => !RUNTIME_PACKAGES.includes(name));
|
|
621
|
+
const packageManager = await detectPackageManager(state.projectDir);
|
|
622
|
+
for (const [group, isDev] of [[runtime, false], [dev, true]]) {
|
|
623
|
+
if (group.length === 0) continue;
|
|
624
|
+
const dependencyVersion = formatDependencyVersion(pinnedVersion);
|
|
625
|
+
const pinned = group.map((name) => `${name}@${dependencyVersion}`);
|
|
626
|
+
console.log(`Installing ${pinned.join(", ")} ${isDev ? "(devDependencies)" : "(dependencies)"} with ${packageManager}...`);
|
|
627
|
+
await runInstall$1(packageManager, installArgs(packageManager, pinned, isDev), state.projectDir);
|
|
628
|
+
}
|
|
629
|
+
console.log("");
|
|
630
|
+
console.log(`Done. Added at ${formatDependencyVersion(pinnedVersion)}.`);
|
|
631
|
+
return 0;
|
|
632
|
+
}
|
|
633
|
+
async function runInstall$1(packageManager, args, cwd) {
|
|
634
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
635
|
+
const child = spawn(commandForPlatform(packageManager), args, {
|
|
636
|
+
cwd,
|
|
637
|
+
stdio: "inherit",
|
|
638
|
+
shell: false
|
|
639
|
+
});
|
|
640
|
+
child.on("error", (error) => {
|
|
641
|
+
rejectPromise(new CliError(`Failed to run ${packageManager}: ${error.message}`));
|
|
642
|
+
});
|
|
643
|
+
child.on("exit", (code) => {
|
|
644
|
+
if (code === 0) {
|
|
645
|
+
resolvePromise();
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
rejectPromise(new CliError(`${packageManager} exited with code ${code}.`));
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
//#endregion
|
|
654
|
+
//#region src/prompts.ts
|
|
655
|
+
var PromptCancelledError = class extends Error {
|
|
656
|
+
constructor() {
|
|
657
|
+
super("Prompt cancelled");
|
|
658
|
+
this.name = "PromptCancelledError";
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
function isPromptCancelledError(error) {
|
|
662
|
+
return error instanceof PromptCancelledError;
|
|
663
|
+
}
|
|
664
|
+
function requirePrompt(value) {
|
|
665
|
+
if (isCancel(value)) {
|
|
666
|
+
cancel("Cancelled.");
|
|
667
|
+
throw new PromptCancelledError();
|
|
668
|
+
}
|
|
669
|
+
return value;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
//#endregion
|
|
673
|
+
//#region src/scaffold/finalize-dependencies.ts
|
|
674
|
+
async function finalizeDependencies(args) {
|
|
675
|
+
const { projectDir, addedDependencies, install, skipPrompts } = args;
|
|
676
|
+
const hasAdded = addedDependencies.length > 0;
|
|
677
|
+
const verb = addedDependencies.length === 1 ? "has" : "have";
|
|
678
|
+
const wantInstall = await decideInstall({
|
|
679
|
+
install,
|
|
680
|
+
skipPrompts,
|
|
681
|
+
hasAdded,
|
|
682
|
+
addedLine: `${joinNames(addedDependencies.map((name) => pc.cyan(name)))} ${verb} been added to your package.json.`
|
|
683
|
+
});
|
|
684
|
+
if (!wantInstall || !hasAdded) {
|
|
685
|
+
if (hasAdded && !wantInstall) {
|
|
686
|
+
const { command, args: args$1 } = installCommand(await detectPackageManager(projectDir));
|
|
687
|
+
log.warn(`Run ${pc.cyan(`${command} ${args$1.join(" ")}`)} when you're ready.`);
|
|
688
|
+
}
|
|
689
|
+
return {
|
|
690
|
+
wantInstall,
|
|
691
|
+
installed: false
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
const installSpinner = spinner();
|
|
695
|
+
installSpinner.start("Installing dependencies");
|
|
696
|
+
const result = await installManifest({
|
|
697
|
+
projectDir,
|
|
698
|
+
onProgress: (line) => installSpinner.message(truncateForSpinner(line))
|
|
699
|
+
});
|
|
700
|
+
if (result.ok) installSpinner.stop(`Dependencies installed with ${pc.cyan(result.packageManager)}`);
|
|
701
|
+
if (!result.ok) installSpinner.error("Could not install dependencies");
|
|
702
|
+
return {
|
|
703
|
+
wantInstall,
|
|
704
|
+
installed: result.ok,
|
|
705
|
+
result
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
async function decideInstall(args) {
|
|
709
|
+
const { install, skipPrompts, hasAdded, addedLine } = args;
|
|
710
|
+
if (install !== void 0) {
|
|
711
|
+
if (hasAdded) log.info(addedLine);
|
|
712
|
+
return install;
|
|
713
|
+
}
|
|
714
|
+
if (!hasAdded) return true;
|
|
715
|
+
if (skipPrompts) {
|
|
716
|
+
log.info(addedLine);
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
return requirePrompt(await confirm({
|
|
720
|
+
message: `${addedLine} Automatically install dependencies?`,
|
|
721
|
+
initialValue: true
|
|
722
|
+
}));
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
//#endregion
|
|
726
|
+
//#region src/cli/commands/new-document.ts
|
|
727
|
+
const STUDIO_URL = "http://localhost:1234";
|
|
728
|
+
async function resolveName(args) {
|
|
729
|
+
const { provided, existingNames, skipPrompts } = args;
|
|
730
|
+
if (provided) return provided;
|
|
731
|
+
const suggestion = generateRandomName({ existingNames });
|
|
732
|
+
if (skipPrompts) return suggestion;
|
|
733
|
+
return requirePrompt(await text({
|
|
734
|
+
message: "Document name",
|
|
735
|
+
defaultValue: suggestion,
|
|
736
|
+
placeholder: suggestion,
|
|
737
|
+
validate: (value) => {
|
|
738
|
+
if (!value) return;
|
|
739
|
+
const result = validateDocumentName({
|
|
740
|
+
name: value,
|
|
741
|
+
existingNames
|
|
742
|
+
});
|
|
743
|
+
return result.ok ? void 0 : result.reason;
|
|
744
|
+
}
|
|
745
|
+
})) || suggestion;
|
|
746
|
+
}
|
|
747
|
+
async function resolveStylingMethod(args) {
|
|
748
|
+
const { provided, framework, skipPrompts } = args;
|
|
749
|
+
if (provided) return provided;
|
|
750
|
+
if (skipPrompts) return DEFAULT_STYLING;
|
|
751
|
+
return requirePrompt(await select({
|
|
752
|
+
message: "Styling method",
|
|
753
|
+
initialValue: DEFAULT_STYLING,
|
|
754
|
+
options: methodsForFramework(framework).map((method) => ({
|
|
755
|
+
value: method,
|
|
756
|
+
label: STYLING_LABELS[method]
|
|
757
|
+
}))
|
|
758
|
+
}));
|
|
759
|
+
}
|
|
760
|
+
function isSchemaChoice(value) {
|
|
761
|
+
return value === "none" || SCHEMA_KINDS.includes(value);
|
|
762
|
+
}
|
|
763
|
+
async function resolveSchema(args) {
|
|
764
|
+
const { provided, skipPrompts } = args;
|
|
765
|
+
if (provided !== void 0) {
|
|
766
|
+
if (!isSchemaChoice(provided)) throw new CliError(`Invalid --schema: "${provided}". Choices: none, ${SCHEMA_KINDS.join(", ")}.`);
|
|
767
|
+
return provided;
|
|
768
|
+
}
|
|
769
|
+
if (skipPrompts) return "none";
|
|
770
|
+
return requirePrompt(await select({
|
|
771
|
+
message: "Schema",
|
|
772
|
+
initialValue: "none",
|
|
773
|
+
options: [{
|
|
774
|
+
value: "none",
|
|
775
|
+
label: "None"
|
|
776
|
+
}, ...SCHEMA_KINDS.map((kind) => ({
|
|
777
|
+
value: kind,
|
|
778
|
+
label: SCHEMA_LABELS[kind]
|
|
779
|
+
}))]
|
|
780
|
+
}));
|
|
781
|
+
}
|
|
782
|
+
function parseDimension(args) {
|
|
783
|
+
const { flag, value } = args;
|
|
784
|
+
const parsed = Number(value);
|
|
785
|
+
if (!isValidDimension(parsed)) throw new CliError(`Invalid ${flag}: "${value}". Enter a number of CSS pixels from 1 to 20000.`);
|
|
786
|
+
return parsed;
|
|
787
|
+
}
|
|
788
|
+
async function promptCustomDimensions() {
|
|
789
|
+
const dimensionValidator = (value) => {
|
|
790
|
+
if (!value) return;
|
|
791
|
+
return isValidDimension(Number(value)) ? void 0 : "Enter a number of CSS pixels from 1 to 20000.";
|
|
792
|
+
};
|
|
793
|
+
const width = requirePrompt(await text({
|
|
794
|
+
message: "Width (px)",
|
|
795
|
+
defaultValue: "794",
|
|
796
|
+
placeholder: "794",
|
|
797
|
+
validate: dimensionValidator
|
|
798
|
+
}));
|
|
799
|
+
const height = requirePrompt(await text({
|
|
800
|
+
message: "Height (px)",
|
|
801
|
+
defaultValue: "1123",
|
|
802
|
+
placeholder: "1123",
|
|
803
|
+
validate: dimensionValidator
|
|
804
|
+
}));
|
|
805
|
+
return {
|
|
806
|
+
width: Number(width || "794"),
|
|
807
|
+
height: Number(height || "1123")
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
async function resolveDimensions(args) {
|
|
811
|
+
const { size, width, height, skipPrompts } = args;
|
|
812
|
+
if (width !== void 0 && height !== void 0) return {
|
|
813
|
+
width: parseDimension({
|
|
814
|
+
flag: "--width",
|
|
815
|
+
value: width
|
|
816
|
+
}),
|
|
817
|
+
height: parseDimension({
|
|
818
|
+
flag: "--height",
|
|
819
|
+
value: height
|
|
820
|
+
})
|
|
821
|
+
};
|
|
822
|
+
if (size && size !== "custom") {
|
|
823
|
+
const match$1 = PAGE_SIZES.find((candidate) => candidate.id === size);
|
|
824
|
+
if (!match$1) throw new CliError(`Invalid --size: "${size}". Choices: ${PAGE_SIZES.map((candidate) => candidate.id).join(", ")}, custom.`);
|
|
825
|
+
return {
|
|
826
|
+
width: match$1.width,
|
|
827
|
+
height: match$1.height
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
if (size === "custom" && (width === void 0 || height === void 0)) {
|
|
831
|
+
if (skipPrompts) throw new CliError("--size custom requires --width and --height.");
|
|
832
|
+
return promptCustomDimensions();
|
|
833
|
+
}
|
|
834
|
+
if (skipPrompts || size !== void 0) return;
|
|
835
|
+
const choice = requirePrompt(await select({
|
|
836
|
+
message: "Page size",
|
|
837
|
+
initialValue: DEFAULT_PAGE_SIZE_ID,
|
|
838
|
+
options: [...PAGE_SIZES.map((candidate) => ({
|
|
839
|
+
value: candidate.id,
|
|
840
|
+
label: `${candidate.name} (${candidate.label})`
|
|
841
|
+
})), {
|
|
842
|
+
value: "custom",
|
|
843
|
+
label: "Custom…"
|
|
844
|
+
}]
|
|
845
|
+
}));
|
|
846
|
+
if (choice === "custom") return promptCustomDimensions();
|
|
847
|
+
const match = PAGE_SIZES.find((candidate) => candidate.id === choice);
|
|
848
|
+
return match ? {
|
|
849
|
+
width: match.width,
|
|
850
|
+
height: match.height
|
|
851
|
+
} : void 0;
|
|
852
|
+
}
|
|
853
|
+
async function resolveData(args) {
|
|
854
|
+
const { provided, skipPrompts } = args;
|
|
855
|
+
const path = provided ?? (skipPrompts ? void 0 : await promptDataPath());
|
|
856
|
+
if (!path) return;
|
|
857
|
+
const resolved = resolve(process.cwd(), path);
|
|
858
|
+
let raw;
|
|
859
|
+
try {
|
|
860
|
+
raw = await readFile(resolved, "utf8");
|
|
861
|
+
} catch {
|
|
862
|
+
throw new CliError(`Couldn't read the data file at "${path}".`);
|
|
863
|
+
}
|
|
864
|
+
try {
|
|
865
|
+
return JSON.parse(raw);
|
|
866
|
+
} catch {
|
|
867
|
+
throw new CliError(`The data file at "${path}" isn't valid JSON.`);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
async function promptDataPath() {
|
|
871
|
+
if (!requirePrompt(await confirm({
|
|
872
|
+
message: "Seed the default variant from a JSON file?",
|
|
873
|
+
initialValue: false
|
|
874
|
+
}))) return;
|
|
875
|
+
return requirePrompt(await text({
|
|
876
|
+
message: "Path to the JSON file",
|
|
877
|
+
placeholder: "./data.json",
|
|
878
|
+
validate: (value) => value && value.trim() !== "" ? void 0 : "Enter a path, or press Ctrl+C to skip."
|
|
879
|
+
})).trim();
|
|
880
|
+
}
|
|
881
|
+
function reportPrerequisites(prerequisites) {
|
|
882
|
+
if (prerequisites.missingConfig.length === 0) return;
|
|
883
|
+
const steps = prerequisites.missingConfig.map((config) => `• ${config.description}`);
|
|
884
|
+
log.warn(`To finish setting up ${STYLING_LABELS[prerequisites.method]}:\n${steps.join("\n")}`);
|
|
885
|
+
}
|
|
886
|
+
function isFramework$2(value) {
|
|
887
|
+
return value === "react" || value === "vue" || value === "html";
|
|
888
|
+
}
|
|
889
|
+
async function loadProject(cwd) {
|
|
890
|
+
const configPath = findConfigPath(cwd, { baseName: process.env.FMT_CONFIG_FILE_NAME });
|
|
891
|
+
if (!configPath) throw new CliError("No Format project found here. Run `npm create format@latest` to start a project, then try again.");
|
|
892
|
+
const configFile = await readConfigFile(configPath);
|
|
893
|
+
const framework = configFile.config.framework;
|
|
894
|
+
if (!isFramework$2(framework)) throw new CliError(`Invalid "framework" in ${configPath}: expected "react", "vue", or "html".`);
|
|
895
|
+
return {
|
|
896
|
+
config: {
|
|
897
|
+
framework,
|
|
898
|
+
rootDir: typeof configFile.config.rootDir === "string" ? configFile.config.rootDir : void 0
|
|
899
|
+
},
|
|
900
|
+
projectRoot: dirname(configPath),
|
|
901
|
+
formatConfigPath: configPath
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
async function newDocumentCommand(cwd, name, options) {
|
|
905
|
+
const { config, projectRoot, formatConfigPath } = await loadProject(cwd);
|
|
906
|
+
const empty = options.empty === true;
|
|
907
|
+
const skipPrompts = options.yes === true;
|
|
908
|
+
const embedded = options.embedded === true;
|
|
909
|
+
const incomingDependencies = options.incomingDependencies ?? [];
|
|
910
|
+
const documentsDir = getDocumentsDir({
|
|
911
|
+
projectRoot,
|
|
912
|
+
config
|
|
913
|
+
});
|
|
914
|
+
const existingNames = await readExistingDocumentNames(documentsDir);
|
|
915
|
+
try {
|
|
916
|
+
if (!embedded) intro(pc.bgCyan(pc.black(" Format ")));
|
|
917
|
+
const resolvedName = await resolveName({
|
|
918
|
+
provided: name,
|
|
919
|
+
existingNames,
|
|
920
|
+
skipPrompts
|
|
921
|
+
});
|
|
922
|
+
const styling = await resolveStylingMethod({
|
|
923
|
+
provided: options.styling,
|
|
924
|
+
framework: config.framework,
|
|
925
|
+
skipPrompts
|
|
926
|
+
});
|
|
927
|
+
const schemaChoice = empty ? "none" : await resolveSchema({
|
|
928
|
+
provided: options.schema,
|
|
929
|
+
skipPrompts
|
|
930
|
+
});
|
|
931
|
+
const emitSchema = schemaChoice !== "none";
|
|
932
|
+
const schemaKind = schemaChoice === "none" ? void 0 : schemaChoice;
|
|
933
|
+
const data = empty ? void 0 : await resolveData({
|
|
934
|
+
provided: options.data,
|
|
935
|
+
skipPrompts
|
|
936
|
+
});
|
|
937
|
+
const dimensions = await resolveDimensions({
|
|
938
|
+
size: options.size,
|
|
939
|
+
width: options.width,
|
|
940
|
+
height: options.height,
|
|
941
|
+
skipPrompts
|
|
942
|
+
});
|
|
943
|
+
const result = await createDocument({
|
|
944
|
+
config,
|
|
945
|
+
projectRoot,
|
|
946
|
+
name: resolvedName,
|
|
947
|
+
styling,
|
|
948
|
+
emitSchema,
|
|
949
|
+
schemaKind,
|
|
950
|
+
data,
|
|
951
|
+
width: dimensions?.width,
|
|
952
|
+
height: dimensions?.height,
|
|
953
|
+
empty
|
|
954
|
+
});
|
|
955
|
+
const documentDependencies = await writeDocumentDependencies({
|
|
956
|
+
projectRoot,
|
|
957
|
+
emitSchema,
|
|
958
|
+
schemaKind,
|
|
959
|
+
missingStylingDependencies: result.prerequisites.missingDependencies.map((dependency) => dependency.name)
|
|
960
|
+
});
|
|
961
|
+
const finalize = await finalizeDependencies({
|
|
962
|
+
projectDir: projectRoot,
|
|
963
|
+
addedDependencies: [...incomingDependencies, ...documentDependencies],
|
|
964
|
+
install: options.install === false ? false : void 0,
|
|
965
|
+
skipPrompts
|
|
966
|
+
});
|
|
967
|
+
if (finalize.result && !finalize.installed) log.error(`Failed to install dependencies automatically: ${finalize.result.message ?? "unknown error"}`);
|
|
968
|
+
const projectSetup = await maybeSetupProject({
|
|
969
|
+
wanted: finalize.wantInstall,
|
|
970
|
+
styling,
|
|
971
|
+
config,
|
|
972
|
+
projectRoot,
|
|
973
|
+
formatConfigPath,
|
|
974
|
+
documentsGlobBase: relative(projectRoot, documentsDir)
|
|
975
|
+
});
|
|
976
|
+
if (projectSetup) log.success(`Set up ${styling}\n${projectSetup.summary.map((line) => pc.dim(line)).join("\n")}`);
|
|
977
|
+
log.success(`Created document ${pc.cyan(result.documentName)}\n${pc.dim(result.documentDir)}`);
|
|
978
|
+
if (!finalize.installed) reportPrerequisites(result.prerequisites);
|
|
979
|
+
const openLine = `Open it in Studio: ${pc.cyan(`${STUDIO_URL}/${result.documentName}`)}`;
|
|
980
|
+
if (embedded) log.info(openLine);
|
|
981
|
+
if (!embedded) outro(openLine);
|
|
982
|
+
return 0;
|
|
983
|
+
} catch (error) {
|
|
984
|
+
if (isPromptCancelledError(error)) return 0;
|
|
985
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
986
|
+
log.error(message);
|
|
987
|
+
return 1;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
//#endregion
|
|
992
|
+
//#region src/cli/commands/init.ts
|
|
993
|
+
const SCHEMA_URL = "https://format.dev/schema/format-config.json";
|
|
994
|
+
const GITIGNORE_ENTRIES = [
|
|
995
|
+
"_generated",
|
|
996
|
+
".env",
|
|
997
|
+
"format-env.d.ts"
|
|
998
|
+
];
|
|
999
|
+
const FORMAT_ENV_TYPES = `/// <reference types="@format.dev/cli/env" />
|
|
1000
|
+
|
|
1001
|
+
// This file is managed by Format and regenerated with your document types when
|
|
1002
|
+
// you compile. Do not edit.
|
|
1003
|
+
`;
|
|
1004
|
+
function isFramework$1(value) {
|
|
1005
|
+
return SUPPORTED_FRAMEWORKS.includes(value);
|
|
1006
|
+
}
|
|
1007
|
+
function detectFramework(packageJson) {
|
|
1008
|
+
const dependencyNames = Object.keys({
|
|
1009
|
+
...packageJson.dependencies,
|
|
1010
|
+
...packageJson.devDependencies
|
|
1011
|
+
});
|
|
1012
|
+
if (dependencyNames.includes("react")) return "react";
|
|
1013
|
+
if (dependencyNames.includes("vue")) return "vue";
|
|
1014
|
+
return "html";
|
|
1015
|
+
}
|
|
1016
|
+
async function resolveFramework$1(args) {
|
|
1017
|
+
const { provided, detected, skipPrompts } = args;
|
|
1018
|
+
if (provided !== void 0) {
|
|
1019
|
+
if (!isFramework$1(provided)) throw new CliError(`Invalid --framework: "${provided}". Choices: ${SUPPORTED_FRAMEWORKS.join(", ")}.`);
|
|
1020
|
+
return provided;
|
|
1021
|
+
}
|
|
1022
|
+
if (skipPrompts) return detected;
|
|
1023
|
+
return requirePrompt(await select({
|
|
1024
|
+
message: "How do you want to build your PDFs?",
|
|
1025
|
+
initialValue: detected,
|
|
1026
|
+
options: [
|
|
1027
|
+
{
|
|
1028
|
+
value: "html",
|
|
1029
|
+
label: "HTML"
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
value: "react",
|
|
1033
|
+
label: "React"
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
value: "vue",
|
|
1037
|
+
label: "Vue"
|
|
1038
|
+
}
|
|
1039
|
+
]
|
|
1040
|
+
}));
|
|
1041
|
+
}
|
|
1042
|
+
async function shouldRun(message, skipPrompts) {
|
|
1043
|
+
if (skipPrompts) return true;
|
|
1044
|
+
return requirePrompt(await confirm({
|
|
1045
|
+
message,
|
|
1046
|
+
initialValue: true
|
|
1047
|
+
}));
|
|
1048
|
+
}
|
|
1049
|
+
function normalizeRootDir(rootDir) {
|
|
1050
|
+
if (rootDir === void 0) return;
|
|
1051
|
+
const trimmed = rootDir.trim();
|
|
1052
|
+
if (trimmed === "" || trimmed === "." || trimmed === "./") return;
|
|
1053
|
+
return trimmed;
|
|
1054
|
+
}
|
|
1055
|
+
async function writeFormatConfig(args) {
|
|
1056
|
+
const { projectDir, framework, version, rootDir } = args;
|
|
1057
|
+
const configPath = join(projectDir, "format.config.json");
|
|
1058
|
+
const config = {
|
|
1059
|
+
$schema: SCHEMA_URL,
|
|
1060
|
+
version,
|
|
1061
|
+
framework,
|
|
1062
|
+
...rootDir ? { rootDir } : {}
|
|
1063
|
+
};
|
|
1064
|
+
await fs.writeFile(configPath, `${JSON.stringify(config, null, " ")}\n`, "utf8");
|
|
1065
|
+
return configPath;
|
|
1066
|
+
}
|
|
1067
|
+
async function addPackageScripts(projectDir) {
|
|
1068
|
+
const packageJsonPath = join(projectDir, "package.json");
|
|
1069
|
+
const raw = await fs.readFile(packageJsonPath, "utf8");
|
|
1070
|
+
const packageJson = JSON.parse(raw);
|
|
1071
|
+
const scripts = { ...packageJson.scripts ?? {} };
|
|
1072
|
+
const wanted = [["dev", "format dev"], ["compile", "format compile"]];
|
|
1073
|
+
const added = [];
|
|
1074
|
+
const renamed = [];
|
|
1075
|
+
const resolvedName = {};
|
|
1076
|
+
for (const [name, command] of wanted) {
|
|
1077
|
+
if (scripts[name] === command) {
|
|
1078
|
+
resolvedName[name] = name;
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
if (scripts[`format:${name}`] === command) {
|
|
1082
|
+
resolvedName[name] = `format:${name}`;
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
if (scripts[name] === void 0) {
|
|
1086
|
+
scripts[name] = command;
|
|
1087
|
+
added.push([name, command]);
|
|
1088
|
+
resolvedName[name] = name;
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
scripts[`format:${name}`] = command;
|
|
1092
|
+
renamed.push([`format:${name}`, command]);
|
|
1093
|
+
resolvedName[name] = `format:${name}`;
|
|
1094
|
+
}
|
|
1095
|
+
if (added.length > 0 || renamed.length > 0) {
|
|
1096
|
+
packageJson.scripts = scripts;
|
|
1097
|
+
const indent = detectIndent(raw);
|
|
1098
|
+
await fs.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, indent)}\n`, "utf8");
|
|
1099
|
+
}
|
|
1100
|
+
return {
|
|
1101
|
+
added,
|
|
1102
|
+
renamed,
|
|
1103
|
+
devScriptName: resolvedName.dev
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
async function updateGitignore(projectDir) {
|
|
1107
|
+
const gitignorePath = join(projectDir, ".gitignore");
|
|
1108
|
+
const existing = existsSync(gitignorePath) ? await fs.readFile(gitignorePath, "utf8") : "";
|
|
1109
|
+
const lines = existing.split("\n").map((line) => line.trim());
|
|
1110
|
+
const missing = GITIGNORE_ENTRIES.filter((entry) => !lines.includes(entry));
|
|
1111
|
+
if (missing.length === 0) return [];
|
|
1112
|
+
const separator = existing === "" || existing.endsWith("\n") ? "" : "\n";
|
|
1113
|
+
await fs.writeFile(gitignorePath, `${existing}${separator}${missing.join("\n")}\n`, "utf8");
|
|
1114
|
+
return missing;
|
|
1115
|
+
}
|
|
1116
|
+
function packagesForFramework(framework) {
|
|
1117
|
+
if (framework === "react") return ["@format.dev/react", "@format.dev/client"];
|
|
1118
|
+
if (framework === "vue") return ["@format.dev/vue", "@format.dev/client"];
|
|
1119
|
+
return ["@format.dev/client"];
|
|
1120
|
+
}
|
|
1121
|
+
function formatDependencySpecs(framework, version) {
|
|
1122
|
+
const dependencyVersion = formatDependencyVersion(version);
|
|
1123
|
+
const packages = packagesForFramework(framework).map((name) => ({
|
|
1124
|
+
name,
|
|
1125
|
+
version: dependencyVersion,
|
|
1126
|
+
dev: !RUNTIME_PACKAGES.includes(name)
|
|
1127
|
+
}));
|
|
1128
|
+
const linked = devOnlyWorkspacePackages().map((name) => ({
|
|
1129
|
+
name,
|
|
1130
|
+
version: WORKSPACE_VERSION,
|
|
1131
|
+
dev: true
|
|
1132
|
+
}));
|
|
1133
|
+
return [...packages, ...linked];
|
|
1134
|
+
}
|
|
1135
|
+
async function initCommand(cwd, options) {
|
|
1136
|
+
const skipPrompts = options.yes === true;
|
|
1137
|
+
const configPath = join(cwd, "format.config.json");
|
|
1138
|
+
const configPathJsonc = join(cwd, "format.config.jsonc");
|
|
1139
|
+
if (existsSync(configPath) || existsSync(configPathJsonc)) throw new CliError("This project is already set up: a format.config file exists here.");
|
|
1140
|
+
const packageJsonPath = join(cwd, "package.json");
|
|
1141
|
+
if (!existsSync(packageJsonPath)) throw new CliError("No package.json found. Run `format init` in the root of an existing project, or start fresh with `npm create format`.");
|
|
1142
|
+
try {
|
|
1143
|
+
intro(pc.bgCyan(pc.black(" Format ")));
|
|
1144
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
|
|
1145
|
+
const framework = await resolveFramework$1({
|
|
1146
|
+
provided: options.framework,
|
|
1147
|
+
detected: detectFramework(packageJson),
|
|
1148
|
+
skipPrompts
|
|
1149
|
+
});
|
|
1150
|
+
const version = jsVersion;
|
|
1151
|
+
const rootDir = normalizeRootDir(options.rootDir);
|
|
1152
|
+
await writeFormatConfig({
|
|
1153
|
+
projectDir: cwd,
|
|
1154
|
+
framework,
|
|
1155
|
+
version,
|
|
1156
|
+
rootDir
|
|
1157
|
+
});
|
|
1158
|
+
const documentsDir = getDocumentsDir({
|
|
1159
|
+
projectRoot: cwd,
|
|
1160
|
+
config: {
|
|
1161
|
+
framework,
|
|
1162
|
+
rootDir
|
|
1163
|
+
}
|
|
1164
|
+
});
|
|
1165
|
+
await fs.mkdir(documentsDir, { recursive: true });
|
|
1166
|
+
const documentsPath = relative(cwd, documentsDir);
|
|
1167
|
+
await fs.writeFile(join(cwd, "format-env.d.ts"), FORMAT_ENV_TYPES, "utf8");
|
|
1168
|
+
const configLine = `Created ${pc.cyan("format.config.json")} ${pc.dim(`(Format ${version}, ${framework})`)}`;
|
|
1169
|
+
log.success(`${configLine}\n${pc.dim(`Documents directory: ${documentsPath}`)}`);
|
|
1170
|
+
let devScriptName;
|
|
1171
|
+
if (options.scripts && await shouldRun("Add format npm scripts to package.json?", skipPrompts)) {
|
|
1172
|
+
const { added, renamed, devScriptName: resolvedDevScript } = await addPackageScripts(cwd);
|
|
1173
|
+
devScriptName = resolvedDevScript;
|
|
1174
|
+
const scriptLines = [...added, ...renamed].map(([name, command]) => `${pc.cyan(name)}: ${pc.dim(command)}`);
|
|
1175
|
+
if (renamed.length > 0) scriptLines.push(pc.dim("Some script names were taken, so those use a format: prefix."));
|
|
1176
|
+
log.success(`Added npm scripts\n${scriptLines.join("\n")}`);
|
|
1177
|
+
}
|
|
1178
|
+
if (options.gitignore && await shouldRun("Update .gitignore?", skipPrompts)) {
|
|
1179
|
+
const appended = await updateGitignore(cwd);
|
|
1180
|
+
if (appended.length > 0) log.success(`Added to .gitignore: ${pc.dim(appended.join(", "))}`);
|
|
1181
|
+
}
|
|
1182
|
+
const addedFormatPackages = await writeDependenciesToManifest({
|
|
1183
|
+
projectDir: cwd,
|
|
1184
|
+
dependencies: formatDependencySpecs(framework, version)
|
|
1185
|
+
});
|
|
1186
|
+
const createDocumentNow = options.document && await shouldRun("Create your first document?", skipPrompts);
|
|
1187
|
+
if (createDocumentNow) {
|
|
1188
|
+
const exitCode = await newDocumentCommand(cwd, void 0, {
|
|
1189
|
+
yes: options.yes,
|
|
1190
|
+
install: options.install,
|
|
1191
|
+
embedded: true,
|
|
1192
|
+
incomingDependencies: addedFormatPackages
|
|
1193
|
+
});
|
|
1194
|
+
if (exitCode !== 0) return exitCode;
|
|
1195
|
+
}
|
|
1196
|
+
if (!createDocumentNow) {
|
|
1197
|
+
await fs.writeFile(join(documentsDir, ".gitkeep"), "", "utf8");
|
|
1198
|
+
await finalizeDependencies({
|
|
1199
|
+
projectDir: cwd,
|
|
1200
|
+
addedDependencies: addedFormatPackages,
|
|
1201
|
+
install: options.install ? void 0 : false,
|
|
1202
|
+
skipPrompts
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
const startCommand = devScriptName ? `npm run ${devScriptName}` : "format dev";
|
|
1206
|
+
outro(`Format is set up. Start Studio with ${pc.cyan(startCommand)}`);
|
|
1207
|
+
return 0;
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
if (isPromptCancelledError(error)) return 0;
|
|
1210
|
+
throw error;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
//#endregion
|
|
1215
|
+
//#region src/cli/commands/remove.ts
|
|
1216
|
+
function normalisePackageName(name) {
|
|
1217
|
+
if (name.startsWith("@format.dev/")) return name;
|
|
1218
|
+
return `@format.dev/${name}`;
|
|
1219
|
+
}
|
|
1220
|
+
function removeArgs(packageManager, packages) {
|
|
1221
|
+
if (packageManager === "npm") return ["uninstall", ...packages];
|
|
1222
|
+
return ["remove", ...packages];
|
|
1223
|
+
}
|
|
1224
|
+
async function removeCommand(cwd, packages) {
|
|
1225
|
+
if (packages.length === 0) throw new CliError("Usage: format remove <package> [...] — e.g. `format remove zip`.");
|
|
1226
|
+
const state = await loadProjectState(cwd);
|
|
1227
|
+
if (!state.packageJsonPath) throw new CliError("No package.json found next to the config — nothing to remove from.");
|
|
1228
|
+
const names = packages.map(normalisePackageName);
|
|
1229
|
+
if (names.includes("@format.dev/cli")) throw new CliError("Refusing to remove @format.dev/cli through itself. Use your package manager directly: `npm uninstall \"@format.dev/cli\"`.");
|
|
1230
|
+
const installedNames = state.dependencies.map((dependency) => dependency.name);
|
|
1231
|
+
const notInstalled = names.filter((name) => !installedNames.includes(name));
|
|
1232
|
+
if (notInstalled.length > 0) throw new CliError(`Not installed in this project: ${notInstalled.join(", ")}.`);
|
|
1233
|
+
const packageManager = await detectPackageManager(state.projectDir);
|
|
1234
|
+
console.log(`Removing ${names.join(", ")} with ${packageManager}...`);
|
|
1235
|
+
await runRemove(packageManager, removeArgs(packageManager, names), state.projectDir);
|
|
1236
|
+
console.log("");
|
|
1237
|
+
console.log("Done.");
|
|
1238
|
+
return 0;
|
|
1239
|
+
}
|
|
1240
|
+
async function runRemove(packageManager, args, cwd) {
|
|
1241
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
1242
|
+
const child = spawn(commandForPlatform(packageManager), args, {
|
|
1243
|
+
cwd,
|
|
1244
|
+
stdio: "inherit",
|
|
1245
|
+
shell: false
|
|
1246
|
+
});
|
|
1247
|
+
child.on("error", (error) => {
|
|
1248
|
+
rejectPromise(new CliError(`Failed to run ${packageManager}: ${error.message}`));
|
|
1249
|
+
});
|
|
1250
|
+
child.on("exit", (code) => {
|
|
1251
|
+
if (code === 0) {
|
|
1252
|
+
resolvePromise();
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
rejectPromise(new CliError(`${packageManager} exited with code ${code}.`));
|
|
1256
|
+
});
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
//#endregion
|
|
1261
|
+
//#region src/cli/commands/new-project.ts
|
|
1262
|
+
function isFramework(value) {
|
|
1263
|
+
return SUPPORTED_FRAMEWORKS.includes(value);
|
|
1264
|
+
}
|
|
1265
|
+
async function resolveDirectory(provided, skipPrompts) {
|
|
1266
|
+
if (provided) return provided;
|
|
1267
|
+
if (skipPrompts) return "./my-format-project";
|
|
1268
|
+
return requirePrompt(await text({
|
|
1269
|
+
message: "Where should we create your project?",
|
|
1270
|
+
placeholder: "./my-format-project",
|
|
1271
|
+
defaultValue: "./my-format-project"
|
|
1272
|
+
}));
|
|
1273
|
+
}
|
|
1274
|
+
async function resolveFramework(provided, skipPrompts) {
|
|
1275
|
+
if (provided !== void 0) {
|
|
1276
|
+
if (!isFramework(provided)) throw new CliError(`Invalid --framework: "${provided}". Choices: ${SUPPORTED_FRAMEWORKS.join(", ")}.`);
|
|
1277
|
+
return provided;
|
|
1278
|
+
}
|
|
1279
|
+
if (skipPrompts) return "react";
|
|
1280
|
+
return requirePrompt(await select({
|
|
1281
|
+
message: "How do you want to build your PDFs?",
|
|
1282
|
+
options: [
|
|
1283
|
+
{
|
|
1284
|
+
value: "html",
|
|
1285
|
+
label: "HTML"
|
|
1286
|
+
},
|
|
1287
|
+
{
|
|
1288
|
+
value: "react",
|
|
1289
|
+
label: "React"
|
|
1290
|
+
},
|
|
1291
|
+
{
|
|
1292
|
+
value: "vue",
|
|
1293
|
+
label: "Vue"
|
|
1294
|
+
}
|
|
1295
|
+
],
|
|
1296
|
+
initialValue: "react"
|
|
1297
|
+
}));
|
|
1298
|
+
}
|
|
1299
|
+
async function resolveInstall(provided, skipPrompts) {
|
|
1300
|
+
if (provided !== void 0) return provided;
|
|
1301
|
+
if (skipPrompts) return true;
|
|
1302
|
+
return requirePrompt(await confirm({
|
|
1303
|
+
message: "Automatically install dependencies?",
|
|
1304
|
+
initialValue: true
|
|
1305
|
+
}));
|
|
1306
|
+
}
|
|
1307
|
+
function runDevLine(packageManager) {
|
|
1308
|
+
return packageManager === "npm" ? "npm run dev" : `${packageManager} dev`;
|
|
1309
|
+
}
|
|
1310
|
+
async function newProjectCommand(cwd, providedDirectory, options) {
|
|
1311
|
+
const skipPrompts = options.yes === true;
|
|
1312
|
+
intro(pc.bgCyan(pc.black(" Format ")));
|
|
1313
|
+
try {
|
|
1314
|
+
const directory = await resolveDirectory(providedDirectory, skipPrompts);
|
|
1315
|
+
const framework = await resolveFramework(options.framework, skipPrompts);
|
|
1316
|
+
const wantInstall = await resolveInstall(options.install, skipPrompts);
|
|
1317
|
+
const scaffold = spinner();
|
|
1318
|
+
scaffold.start("Creating project");
|
|
1319
|
+
const { dirPath } = await scaffoldProject({
|
|
1320
|
+
directory,
|
|
1321
|
+
framework,
|
|
1322
|
+
cwd
|
|
1323
|
+
});
|
|
1324
|
+
scaffold.stop(`Created a ${pc.cyan(framework)} project in ${pc.cyan(directory)}`);
|
|
1325
|
+
let packageManager = await detectPackageManager(cwd);
|
|
1326
|
+
let needsInstall = !wantInstall;
|
|
1327
|
+
if (wantInstall) {
|
|
1328
|
+
const install = spinner();
|
|
1329
|
+
install.start(`Installing dependencies with ${packageManager}`);
|
|
1330
|
+
const result = await installProjectDependencies({
|
|
1331
|
+
dirPath,
|
|
1332
|
+
preferredPackageManager: packageManager,
|
|
1333
|
+
onProgress: (line) => install.message(truncateForSpinner(line))
|
|
1334
|
+
});
|
|
1335
|
+
if (result.ok) {
|
|
1336
|
+
packageManager = result.packageManagerUsed;
|
|
1337
|
+
needsInstall = false;
|
|
1338
|
+
install.stop(`Dependencies installed with ${pc.cyan(packageManager)}`);
|
|
1339
|
+
if (result.fellBackToNpm) log.warn("Could not find your package manager. Fell back to npm.");
|
|
1340
|
+
}
|
|
1341
|
+
if (!result.ok) {
|
|
1342
|
+
needsInstall = true;
|
|
1343
|
+
install.error(`Could not install dependencies with ${packageManager}`);
|
|
1344
|
+
log.error(result.message ?? "unknown error");
|
|
1345
|
+
if (result.output) log.message(pc.dim(result.output));
|
|
1346
|
+
if (await templateUsesWorkspaceDeps(dirPath)) log.warn("This template uses `workspace:*` deps, which only install inside a workspace.");
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
const steps = [`cd ${directory}`];
|
|
1350
|
+
if (needsInstall) {
|
|
1351
|
+
const { command, args } = installCommand(packageManager);
|
|
1352
|
+
steps.push(`${command} ${args.join(" ")}`);
|
|
1353
|
+
}
|
|
1354
|
+
steps.push(runDevLine(packageManager));
|
|
1355
|
+
outro(`Start Studio with ${pc.cyan(steps.join(" && "))}`);
|
|
1356
|
+
return 0;
|
|
1357
|
+
} catch (error) {
|
|
1358
|
+
if (isPromptCancelledError(error)) return 0;
|
|
1359
|
+
throw error;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
//#endregion
|
|
1364
|
+
//#region src/cli/commands/update.ts
|
|
1365
|
+
async function updateCommand(cwd, target) {
|
|
1366
|
+
const state = await loadProjectState(cwd);
|
|
1367
|
+
const targetVersion = await resolveTargetVersion({
|
|
1368
|
+
target,
|
|
1369
|
+
pinnedVersion: state.pinnedVersion
|
|
1370
|
+
});
|
|
1371
|
+
if (targetVersion !== state.pinnedVersion) {
|
|
1372
|
+
await writeConfigVersion({
|
|
1373
|
+
filepath: state.configFile.filepath,
|
|
1374
|
+
version: targetVersion
|
|
1375
|
+
});
|
|
1376
|
+
console.log(`Pinned Format ${targetVersion} in ${state.configFile.filepath}`);
|
|
1377
|
+
} else console.log(`Applying pinned Format ${targetVersion}`);
|
|
1378
|
+
if (!state.packageJsonPath) {
|
|
1379
|
+
console.log("No package.json found next to the config — nothing to install.");
|
|
1380
|
+
return 0;
|
|
1381
|
+
}
|
|
1382
|
+
const rewritten = await rewriteFormatDependencies({
|
|
1383
|
+
packageJsonPath: state.packageJsonPath,
|
|
1384
|
+
version: targetVersion
|
|
1385
|
+
});
|
|
1386
|
+
if (rewritten.length === 0) {
|
|
1387
|
+
console.log("All installed Format packages already match.");
|
|
1388
|
+
return 0;
|
|
1389
|
+
}
|
|
1390
|
+
for (const change of rewritten) console.log(` ${change.name} ${change.from} → ${change.to}`);
|
|
1391
|
+
const packageManager = await detectPackageManager(state.projectDir);
|
|
1392
|
+
const { command, args } = installCommand(packageManager);
|
|
1393
|
+
console.log("");
|
|
1394
|
+
console.log(`Installing with ${packageManager}...`);
|
|
1395
|
+
await runInstall({
|
|
1396
|
+
command,
|
|
1397
|
+
args,
|
|
1398
|
+
cwd: state.projectDir
|
|
1399
|
+
});
|
|
1400
|
+
console.log("");
|
|
1401
|
+
console.log(`Done. Your project is on Format ${targetVersion}.`);
|
|
1402
|
+
return 0;
|
|
1403
|
+
}
|
|
1404
|
+
async function resolveTargetVersion(args) {
|
|
1405
|
+
const { target, pinnedVersion } = args;
|
|
1406
|
+
if (target === "latest") return fetchLatestVersion();
|
|
1407
|
+
if (target) {
|
|
1408
|
+
if (!isExactVersion(target)) throw new CliError(`"${target}" is not an exact version. Use a version like 0.1.2, or \`format update latest\`.`);
|
|
1409
|
+
return target;
|
|
1410
|
+
}
|
|
1411
|
+
if (pinnedVersion) return pinnedVersion;
|
|
1412
|
+
throw new CliError("format.config.json is missing its required \"version\" field. Run `format update latest` to pin the newest release, or `format init` to set the project up.");
|
|
1413
|
+
}
|
|
1414
|
+
async function runInstall(args) {
|
|
1415
|
+
const { command, args: commandArgs, cwd } = args;
|
|
1416
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
1417
|
+
const child = spawn(command, commandArgs, {
|
|
1418
|
+
cwd,
|
|
1419
|
+
stdio: "inherit",
|
|
1420
|
+
shell: process.platform === "win32"
|
|
1421
|
+
});
|
|
1422
|
+
child.on("error", (error) => {
|
|
1423
|
+
rejectPromise(new CliError(`Failed to run ${command} install: ${error.message}`));
|
|
1424
|
+
});
|
|
1425
|
+
child.on("exit", (code) => {
|
|
1426
|
+
if (code === 0) {
|
|
1427
|
+
resolvePromise();
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
rejectPromise(new CliError(`${command} install exited with code ${code}.`));
|
|
1431
|
+
});
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
//#endregion
|
|
1436
|
+
//#region src/cli/commands/version.ts
|
|
1437
|
+
async function versionCommand(cwd) {
|
|
1438
|
+
const state = await loadProjectState(cwd);
|
|
1439
|
+
if (!state.pinnedVersion) {
|
|
1440
|
+
console.log(`No "version" field found in ${state.configFile.filepath}.`);
|
|
1441
|
+
console.log("Run `format update latest` to pin the latest Format release.");
|
|
1442
|
+
return 1;
|
|
1443
|
+
}
|
|
1444
|
+
console.log(`Format ${state.pinnedVersion}`);
|
|
1445
|
+
if (state.mismatches.length > 0) {
|
|
1446
|
+
console.log("");
|
|
1447
|
+
console.log("Some installed Format packages do not match the pinned version:");
|
|
1448
|
+
for (const mismatch of state.mismatches) console.log(` ${mismatch.name} ${mismatch.actual} (expected ${mismatch.expected})`);
|
|
1449
|
+
console.log("");
|
|
1450
|
+
console.log("Run `format update` to bring everything in line.");
|
|
1451
|
+
return 1;
|
|
1452
|
+
}
|
|
1453
|
+
const alignedCount = state.dependencies.length;
|
|
1454
|
+
if (alignedCount > 0) {
|
|
1455
|
+
const names = state.dependencies.map((dependency) => dependency.name).join(", ");
|
|
1456
|
+
console.log(`${alignedCount} installed Format package${alignedCount === 1 ? "" : "s"}: ${names}`);
|
|
1457
|
+
}
|
|
1458
|
+
await reportNewerRelease(state.pinnedVersion);
|
|
1459
|
+
return 0;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
//#endregion
|
|
1463
|
+
//#region src/cli/index.ts
|
|
1464
|
+
const STUDIO_COMMANDS = [
|
|
1465
|
+
{
|
|
1466
|
+
name: "dev",
|
|
1467
|
+
description: "Open Studio for this project."
|
|
1468
|
+
},
|
|
1469
|
+
{
|
|
1470
|
+
name: "compile",
|
|
1471
|
+
description: "Compile this project's documents into a renderer."
|
|
1472
|
+
},
|
|
1473
|
+
{
|
|
1474
|
+
name: "clear-cache",
|
|
1475
|
+
description: "Clear Studio's local cache."
|
|
1476
|
+
}
|
|
1477
|
+
];
|
|
1478
|
+
function createCommanderOption(optionSpec) {
|
|
1479
|
+
const option = new Option(optionSpec.flags, optionSpec.description);
|
|
1480
|
+
if (optionSpec.choices && optionSpec.choices.length > 0) option.choices(optionSpec.choices);
|
|
1481
|
+
return option;
|
|
1482
|
+
}
|
|
1483
|
+
function registerArguments(command, argumentSpecs) {
|
|
1484
|
+
if (!argumentSpecs) return;
|
|
1485
|
+
for (const argumentSpec of argumentSpecs) {
|
|
1486
|
+
const token = argumentSpec.required ? `<${argumentSpec.name}>` : `[${argumentSpec.name}]`;
|
|
1487
|
+
command.argument(token, argumentSpec.description);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
function registerCommand(parent, commandSpec, path, handlers) {
|
|
1491
|
+
const command = parent.command(commandSpec.name).description(commandSpec.description);
|
|
1492
|
+
registerArguments(command, commandSpec.arguments);
|
|
1493
|
+
for (const optionSpec of commandSpec.options) command.addOption(createCommanderOption(optionSpec));
|
|
1494
|
+
const fullPath = [...path, commandSpec.name];
|
|
1495
|
+
if (commandSpec.commands && commandSpec.commands.length > 0) {
|
|
1496
|
+
for (const childSpec of commandSpec.commands) registerCommand(command, childSpec, fullPath, handlers);
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
const key = fullPath.join(" ");
|
|
1500
|
+
const handler = handlers[key];
|
|
1501
|
+
if (!handler) throw new Error(`Missing command handler for "${key}"`);
|
|
1502
|
+
command.action(async (...args) => {
|
|
1503
|
+
process.exitCode = await handler(...args);
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
function createProgram() {
|
|
1507
|
+
const program = new Command();
|
|
1508
|
+
const cliSpec = cli_spec_default;
|
|
1509
|
+
const cwd = process.cwd();
|
|
1510
|
+
program.name("format").description("The Format CLI: create projects and documents, run Studio, compile renderers, and keep every Format package on one release.").enablePositionalOptions();
|
|
1511
|
+
const handlers = {
|
|
1512
|
+
"new project": (directory, options) => newProjectCommand(cwd, directory, options),
|
|
1513
|
+
"new document": (name, options) => newDocumentCommand(cwd, name, options),
|
|
1514
|
+
init: (options) => initCommand(cwd, options),
|
|
1515
|
+
add: (packages) => addCommand(cwd, packages),
|
|
1516
|
+
remove: (packages) => removeCommand(cwd, packages),
|
|
1517
|
+
update: (version) => updateCommand(cwd, version),
|
|
1518
|
+
version: () => versionCommand(cwd)
|
|
1519
|
+
};
|
|
1520
|
+
for (const commandSpec of cliSpec.commands) registerCommand(program, commandSpec, [], handlers);
|
|
1521
|
+
for (const { name, description } of STUDIO_COMMANDS) program.command(name).description(description).helpOption(false).allowUnknownOption().passThroughOptions().argument("[args...]").action(async (_args, _options, command) => {
|
|
1522
|
+
process.exitCode = await runStudioCommand(cwd, [name, ...command.args]);
|
|
1523
|
+
});
|
|
1524
|
+
return program;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
//#endregion
|
|
1528
|
+
//#region src/cli/bin.ts
|
|
1529
|
+
try {
|
|
1530
|
+
await createProgram().parseAsync();
|
|
1531
|
+
} catch (error) {
|
|
1532
|
+
if (error instanceof CliError) {
|
|
1533
|
+
console.error(error.message);
|
|
1534
|
+
process.exitCode = 1;
|
|
1535
|
+
} else throw error;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
//#endregion
|
|
1539
|
+
export { };
|