uniweb 1.0.0 → 1.0.2
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/LICENSE +201 -0
- package/README.md +532 -3
- package/package.json +60 -11
- package/partials/adding-pages.hbs +77 -0
- package/partials/agents.md +3000 -0
- package/partials/ai-assistance.hbs +28 -0
- package/partials/components-docs.hbs +13 -0
- package/partials/config-reference.hbs +66 -0
- package/partials/deployment.hbs +13 -0
- package/partials/learn-more.hbs +6 -0
- package/partials/quick-start.hbs +8 -0
- package/partials/search-docs.hbs +110 -0
- package/src/backend/asset-download.js +222 -0
- package/src/backend/client.js +986 -0
- package/src/backend/foundation-bring-along.js +692 -0
- package/src/backend/payment-handoff.js +362 -0
- package/src/backend/service-request.js +210 -0
- package/src/backend/site-media.js +255 -0
- package/src/backend/site-preview.js +77 -0
- package/src/backend/site-sync.js +2515 -0
- package/src/backend/workspace.js +234 -0
- package/src/commands/add.js +1900 -0
- package/src/commands/build.js +1214 -0
- package/src/commands/clone.js +466 -0
- package/src/commands/content.js +202 -0
- package/src/commands/deploy.js +537 -0
- package/src/commands/dev.js +202 -0
- package/src/commands/docs.js +530 -0
- package/src/commands/doctor.js +1982 -0
- package/src/commands/export.js +116 -0
- package/src/commands/families.js +82 -0
- package/src/commands/forget.js +197 -0
- package/src/commands/handoff.js +21 -0
- package/src/commands/i18n.js +1707 -0
- package/src/commands/inspect.js +375 -0
- package/src/commands/invite.js +24 -0
- package/src/commands/org.js +96 -0
- package/src/commands/publish.js +1087 -0
- package/src/commands/pull.js +1257 -0
- package/src/commands/push.js +663 -0
- package/src/commands/refresh.js +212 -0
- package/src/commands/register.js +1092 -0
- package/src/commands/rename.js +647 -0
- package/src/commands/site.js +358 -0
- package/src/commands/snapshot.js +461 -0
- package/src/commands/starter.js +206 -0
- package/src/commands/status.js +302 -0
- package/src/commands/sync.js +93 -0
- package/src/commands/template.js +107 -0
- package/src/commands/update.js +887 -0
- package/src/commands/validate.js +376 -0
- package/src/framework-index.json +248 -0
- package/src/index.js +2241 -0
- package/src/templates/fetchers/github.js +113 -0
- package/src/templates/fetchers/npm.js +85 -0
- package/src/templates/fetchers/release.js +92 -0
- package/src/templates/index.js +190 -0
- package/src/templates/processor.js +333 -0
- package/src/templates/resolver.js +232 -0
- package/src/templates/validator.js +155 -0
- package/src/utils/agents-stamp.js +110 -0
- package/src/utils/args.js +126 -0
- package/src/utils/asset-upload.js +270 -0
- package/src/utils/bytes.js +30 -0
- package/src/utils/code-upload.js +394 -0
- package/src/utils/config.js +417 -0
- package/src/utils/conformance.js +274 -0
- package/src/utils/dep-survey.js +176 -0
- package/src/utils/destination-prompt.js +146 -0
- package/src/utils/discover.js +75 -0
- package/src/utils/env.js +22 -0
- package/src/utils/exit.js +30 -0
- package/src/utils/fetch-retry.js +109 -0
- package/src/utils/flag-guard.js +246 -0
- package/src/utils/foundation-name.js +179 -0
- package/src/utils/git.js +342 -0
- package/src/utils/host-prompt.js +51 -0
- package/src/utils/install-integrity.js +210 -0
- package/src/utils/interactive.js +118 -0
- package/src/utils/json-file.js +72 -0
- package/src/utils/names.js +91 -0
- package/src/utils/placement.js +151 -0
- package/src/utils/pm.js +165 -0
- package/src/utils/preview.js +20 -0
- package/src/utils/pull-written.js +132 -0
- package/src/utils/records-guard.js +88 -0
- package/src/utils/registry-auth.js +994 -0
- package/src/utils/registry-orgs.js +290 -0
- package/src/utils/scaffold.js +483 -0
- package/src/utils/schemaless-report.js +30 -0
- package/src/utils/semver-precedence.js +80 -0
- package/src/utils/session-file.js +95 -0
- package/src/utils/site-content-refs.js +23 -0
- package/src/utils/site-copies.js +123 -0
- package/src/utils/site-data-upload.js +302 -0
- package/src/utils/site-identity.js +137 -0
- package/src/utils/snapshot-settings.js +112 -0
- package/src/utils/update-check.js +302 -0
- package/src/utils/uwx-read.js +157 -0
- package/src/utils/workspace.js +223 -0
- package/src/versions.js +467 -0
- package/starter/foundation/main.js +50 -0
- package/starter/foundation/sections/Section/index.jsx +95 -0
- package/starter/foundation/sections/Section/meta.js +44 -0
- package/starter/site/pages/about/1-about.md.hbs +13 -0
- package/starter/site/pages/about/page.yml +3 -0
- package/starter/site/pages/home/1-welcome.md.hbs +14 -0
- package/starter/site/pages/home/page.yml +3 -0
- package/templates/foundation/_gitignore +5 -0
- package/templates/foundation/components/.gitkeep +0 -0
- package/templates/foundation/main.js.hbs +33 -0
- package/templates/foundation/package.json.hbs +51 -0
- package/templates/foundation/sections/.gitkeep +0 -0
- package/templates/foundation/styles.css +6 -0
- package/templates/foundation/vite.config.js +3 -0
- package/templates/site/_gitignore +12 -0
- package/templates/site/entry.js +7 -0
- package/templates/site/index.html.hbs +13 -0
- package/templates/site/package.json.hbs +27 -0
- package/templates/site/public/favicon.svg +7 -0
- package/templates/site/site.yml.hbs +91 -0
- package/templates/site/theme.yml +99 -0
- package/templates/site/vite.config.js +7 -0
- package/templates/workspace/AGENTS.md.hbs +2 -0
- package/templates/workspace/README.md.hbs +87 -0
- package/templates/workspace/_gitignore +29 -0
- package/templates/workspace/_vscode/settings.json +6 -0
- package/templates/workspace/package.json.hbs +20 -0
- package/templates/workspace/pnpm-workspace.yaml.hbs +26 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reject unrecognized flags on the backend verbs.
|
|
3
|
+
*
|
|
4
|
+
* Every one of these commands can send data to, or authenticate against, a remote
|
|
5
|
+
* host — and the CLI resolves flags by scanning argv for a literal, so a flag it
|
|
6
|
+
* does not recognize does not fail: it *disappears*, and the thing it was meant to
|
|
7
|
+
* change silently keeps its default.
|
|
8
|
+
*
|
|
9
|
+
* Tolerable for a cosmetic flag; dangerous for any flag that aims the command or picks
|
|
10
|
+
* its identity. This turns that class into one sentence.
|
|
11
|
+
*
|
|
12
|
+
* ⛔ **A backend flag and `--token` are NOT flags of these verbs** *(2026-09-21)*. Every
|
|
13
|
+
* backend verb goes to the backend you are logged in to, with that login's session:
|
|
14
|
+
* switching and signing in are `uniweb login` (`--server`, `--token`), and a script
|
|
15
|
+
* aims one process with UNIWEB_SERVER + UNIWEB_TOKEN. Passed to a verb, each is an
|
|
16
|
+
* unknown flag — and this guard is what makes that a loud error, with a pointer to the
|
|
17
|
+
* login, instead of a command that quietly runs against wherever and as whoever you
|
|
18
|
+
* happen to be. `--server` stays on `forget`, where it SELECTS which backend's records
|
|
19
|
+
* to remove. ⛔ *The flag was `--backend` until 2026-10-07, renamed so it is not read as
|
|
20
|
+
* a site's `backend` service; `--backend` is refused everywhere, naming `--server`.*
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ A wrong rejection is worse than a missed one — it breaks an invocation that
|
|
23
|
+
* works — so the per-command lists must be complete, INCLUDING flags read by
|
|
24
|
+
* helpers rather than by the command file itself. Two live examples: `--no-validate`
|
|
25
|
+
* is consumed inside `utils/conformance.js`, and `--yes` inside
|
|
26
|
+
* `backend/foundation-bring-along.js`. Grepping only the command's own source
|
|
27
|
+
* misses both. When you add a flag anywhere on one of these paths, add it here.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { findUnknownFlags, didYouMean } from './args.js'
|
|
31
|
+
|
|
32
|
+
/** Accepted by every command, wherever they are actually consumed. */
|
|
33
|
+
const GLOBAL = ['--non-interactive', '--help', '-h']
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Login-method flags. Any verb that can hit an unauthenticated backend may fall
|
|
37
|
+
* into `ensureRegistryAuth`, which hands `args` to the login picker — so these are
|
|
38
|
+
* genuinely reachable from all of them, not just from `uniweb login`.
|
|
39
|
+
*/
|
|
40
|
+
const AUTH = ['--browser', '--password', '--token-paste']
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Flags a verb inherits by importing `commands/deploy.js` for `resolveSiteDir`.
|
|
44
|
+
* Inert on these verbs, and listed rather than filtered:
|
|
45
|
+
* accepting a flag that does nothing is exactly the pre-guard behaviour, while
|
|
46
|
+
* rejecting one that works is a broken command. The guard's job is catching
|
|
47
|
+
* `--backed`, not policing inert-but-valid spellings.
|
|
48
|
+
*/
|
|
49
|
+
const VIA_DEPLOY = ['--target', '--host', '--no-save']
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Per-verb flag sets. Derived by scanning each command for dash-literals AND the
|
|
53
|
+
* helpers it calls — not from the help text, which has drifted from the parser in
|
|
54
|
+
* both directions (`--yes` is documented on `publish` and consumed two files away).
|
|
55
|
+
*/
|
|
56
|
+
const VERBS = {
|
|
57
|
+
push: [
|
|
58
|
+
'--all', '--org', '--dry-run', '--force',
|
|
59
|
+
'--foundation', '--output', '-o', '--personal',
|
|
60
|
+
// read in utils/conformance.js and backend/site-sync.js respectively —
|
|
61
|
+
// neither appears in push.js
|
|
62
|
+
'--no-validate', '--yes',
|
|
63
|
+
// via backend/foundation-bring-along.js, which push runs since 2026-08-19 —
|
|
64
|
+
// one of the three flags that skip its prompts. Found by
|
|
65
|
+
// flag-guard-coverage.test.js the moment push gained the import, which is
|
|
66
|
+
// exactly the hand-enumeration failure that test exists to catch.
|
|
67
|
+
'--no-verify',
|
|
68
|
+
// ship content against the already-released code, releasing nothing — or
|
|
69
|
+
// release above a newer registered version instead of stopping (both bring-along)
|
|
70
|
+
'--no-release', '--bump', ...VIA_DEPLOY
|
|
71
|
+
],
|
|
72
|
+
publish: [
|
|
73
|
+
'--org', '--dry-run', '--force', '--foundation',
|
|
74
|
+
'--personal',
|
|
75
|
+
// read in utils/conformance.js, backend/site-sync.js, and
|
|
76
|
+
// backend/foundation-bring-along.js — none appear in publish.js
|
|
77
|
+
'--no-validate', '--yes', '--no-verify', '--no-release', '--bump', ...VIA_DEPLOY
|
|
78
|
+
],
|
|
79
|
+
pull: [
|
|
80
|
+
'--content-only', '--dry-run', '--force', '--merge',
|
|
81
|
+
'--no-assets',
|
|
82
|
+
'--no-records', '--no-delete', '--no-prune',
|
|
83
|
+
// via backend/site-sync.js (the owner resolver) and utils/conformance.js
|
|
84
|
+
'--yes', '--org', '--personal', '--no-validate', ...VIA_DEPLOY
|
|
85
|
+
],
|
|
86
|
+
clone: [
|
|
87
|
+
'--content-only', '--no-assets', '--no-records', '--path',
|
|
88
|
+
'--project', '--org', '--personal'
|
|
89
|
+
],
|
|
90
|
+
register: [
|
|
91
|
+
'--dry-run', '--json', '--output', '-o',
|
|
92
|
+
'--schema-only', '--scope', '--org',
|
|
93
|
+
// reach the login a register may trigger, which chooses the workspace it works in
|
|
94
|
+
'--personal'
|
|
95
|
+
],
|
|
96
|
+
/**
|
|
97
|
+
* `forget` = remove one backend's records (`--server <url>`), or everything a
|
|
98
|
+
* copied project inherited (`--all`). One of the two is required — the verb refuses
|
|
99
|
+
* without it — and they exclude each other. `--non-interactive` reaches it through
|
|
100
|
+
* resolveSiteDir in a workspace of several sites.
|
|
101
|
+
*/
|
|
102
|
+
forget: ['--server', '--all'],
|
|
103
|
+
/**
|
|
104
|
+
* `site list | unpublish | delete` — a workspace's sites. `--yes` confirms a write;
|
|
105
|
+
* `--json` is `list`'s porcelain; the workspace is `--org` / `--personal`.
|
|
106
|
+
*/
|
|
107
|
+
site: ['--json', '--yes', '--org', '--personal'],
|
|
108
|
+
status: [
|
|
109
|
+
'--json', '--remote', '--dry-run',
|
|
110
|
+
'--force', '--no-verify', '--no-validate', '--yes', '--org', '--personal',
|
|
111
|
+
// inert here, reachable through the bring-along module status imports for
|
|
112
|
+
// `resolveLocalFoundation` — listed per the over-approximation note above
|
|
113
|
+
'--no-release', '--bump', ...VIA_DEPLOY
|
|
114
|
+
],
|
|
115
|
+
/**
|
|
116
|
+
* `refresh` = `git pull`, then a DELEGATED `pull --merge`.
|
|
117
|
+
*
|
|
118
|
+
* It forwards nothing of its own argv to that pull — it builds `['--merge']`
|
|
119
|
+
* itself. So pull's own flags (`--merge`, `--force`, `--no-delete`, `--no-prune`,
|
|
120
|
+
* `--content-only`, …) are NOT reachable from a `refresh` argv and
|
|
121
|
+
* are deliberately absent here. ⚠️ `--force` especially: `refresh` is read-only by
|
|
122
|
+
* design, and forwarding it would ask pull to DISCARD local work.
|
|
123
|
+
*
|
|
124
|
+
* The trailing group is inert on this verb but reachable through the
|
|
125
|
+
* `resolveSiteDir` / `probeUnpushed` imports — listed rather than filtered, per
|
|
126
|
+
* the VIA_DEPLOY note above, and required by `flag-guard-coverage.test.js`.
|
|
127
|
+
*/
|
|
128
|
+
refresh: [
|
|
129
|
+
'--no-backend', '--no-git',
|
|
130
|
+
'--org', '--personal', '--dry-run', '--no-validate', '--yes', ...VIA_DEPLOY
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* `sync` is `refresh` + `push`, forwarding RAW argv to both halves — so its
|
|
136
|
+
* accepted set is exactly the UNION of theirs. Derived, never hand-written.
|
|
137
|
+
*
|
|
138
|
+
* Hand-writing it would rot the moment either half gained a flag, and it would rot
|
|
139
|
+
* in the expensive direction: `uniweb sync --no-git` and `uniweb sync --force` are
|
|
140
|
+
* both documented in `sync.js`, and a union that forgot either would reject a
|
|
141
|
+
* working command.
|
|
142
|
+
*
|
|
143
|
+
* ⭐ The union is also WHY `sync` checks and the two halves skip (`skipFlagCheck`,
|
|
144
|
+
* see `sync.js`). Each half's set is a strict subset of this one, so letting them
|
|
145
|
+
* re-check would have `push` reject `--no-git` and `refresh` reject `--force` —
|
|
146
|
+
* both legal on `sync`. Validating once, against the union, at the only layer that
|
|
147
|
+
* knows both halves are in play, is what makes the composite safe.
|
|
148
|
+
*/
|
|
149
|
+
VERBS.sync = [...new Set([...VERBS.refresh, ...VERBS.push])]
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The accepted set per verb: its own flags, plus the login-method flags every
|
|
153
|
+
* backend verb can reach, plus the globals.
|
|
154
|
+
*
|
|
155
|
+
* Derived from each verb's IMPORT GRAPH, not from its own source and not from the
|
|
156
|
+
* help text — `test/flag-guard-coverage.test.js` walks that graph and fails if a
|
|
157
|
+
* verb can honour a flag this list omits. Deliberately an over-approximation: a
|
|
158
|
+
* flag accepted here but inert costs nothing (it was ignored before the guard
|
|
159
|
+
* existed), while one rejected here breaks a working command.
|
|
160
|
+
*/
|
|
161
|
+
/**
|
|
162
|
+
* Read in the verbs' import graphs, and deliberately NOT accepted by them.
|
|
163
|
+
*
|
|
164
|
+
* `--token` is read by `runRegistryLogin` (utils/registry-auth.js) for
|
|
165
|
+
* `uniweb login --token <bearer>`, which seeds and STORES a session. The verbs reach
|
|
166
|
+
* that function through `ensureRegistryAuth`, which strips `--token` from their argv
|
|
167
|
+
* first — so a static walk sees the read, and no verb can honour it.
|
|
168
|
+
* `flag-guard-coverage.test.js` subtracts this list; add to it only with the same
|
|
169
|
+
* proof: a read no verb's argv can reach.
|
|
170
|
+
*/
|
|
171
|
+
export const LOGIN_ONLY = ['--token']
|
|
172
|
+
|
|
173
|
+
export const VERB_FLAGS = Object.fromEntries(
|
|
174
|
+
Object.entries(VERBS).map(([verb, flags]) => [
|
|
175
|
+
verb,
|
|
176
|
+
[...new Set([...flags, ...AUTH])]
|
|
177
|
+
])
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Check `args` against the verb's accepted set. Returns null when everything is
|
|
182
|
+
* recognized, or a ready-to-print message naming the first offender (plus a
|
|
183
|
+
* suggestion when one is close).
|
|
184
|
+
*
|
|
185
|
+
* Reports ONE flag rather than all of them: the first is usually the cause, and a
|
|
186
|
+
* list invites skimming past the suggestion, which is the actionable half.
|
|
187
|
+
*
|
|
188
|
+
* @param {string} verb - a key of VERB_FLAGS
|
|
189
|
+
* @param {string[]} args - the argv slice for this command
|
|
190
|
+
* @returns {{ flag: string, message: string, suggestion: string|null }|null}
|
|
191
|
+
*/
|
|
192
|
+
export function checkFlags(verb, args = []) {
|
|
193
|
+
const known = VERB_FLAGS[verb]
|
|
194
|
+
if (!known) return null
|
|
195
|
+
const all = [...known, ...GLOBAL]
|
|
196
|
+
const unknown = findUnknownFlags(args, all)
|
|
197
|
+
if (!unknown.length) return null
|
|
198
|
+
|
|
199
|
+
const flag = unknown[0]
|
|
200
|
+
// ⭐ `--backend`, `--server` and `--token` are not typos on these verbs — they aim a
|
|
201
|
+
// command or pick its identity, which only the login does (2026-09-21), and `--backend`
|
|
202
|
+
// is `--server` since 2026-10-07. The useful answer is where each went, not "run --help".
|
|
203
|
+
if (flag === '--backend' && verb === 'forget') {
|
|
204
|
+
return {
|
|
205
|
+
flag,
|
|
206
|
+
suggestion: '--server',
|
|
207
|
+
message: '`--backend` is now `--server`: uniweb forget --server <url>'
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (flag === '--backend' || flag === '--server') {
|
|
211
|
+
return {
|
|
212
|
+
flag,
|
|
213
|
+
suggestion: null,
|
|
214
|
+
message: [
|
|
215
|
+
`\`uniweb ${verb}\` has no \`${flag}\`: it goes to the backend you are logged in to.`,
|
|
216
|
+
' Switch with: uniweb login --server <url>',
|
|
217
|
+
' (A script can aim one process with UNIWEB_SERVER instead.)'
|
|
218
|
+
].join('\n')
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (flag === '--token') {
|
|
222
|
+
return {
|
|
223
|
+
flag,
|
|
224
|
+
suggestion: null,
|
|
225
|
+
message: [
|
|
226
|
+
`\`uniweb ${verb}\` has no \`--token\`: it uses the session of the backend you are logged in to.`,
|
|
227
|
+
' Sign in with a token: uniweb login --server <url> --token <bearer>',
|
|
228
|
+
' (A script can authenticate one process with UNIWEB_TOKEN instead.)'
|
|
229
|
+
].join('\n')
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// ⭐ `--as-org` is RETIRED (2026-09-23) with the `?as_org=` it mirrored — the request
|
|
233
|
+
// names its workspace in a header now, and `--org` is the one spelling.
|
|
234
|
+
if (flag === '--as-org' && all.includes('--org')) {
|
|
235
|
+
return {
|
|
236
|
+
flag,
|
|
237
|
+
suggestion: '--org',
|
|
238
|
+
message: `\`--as-org\` is retired — use \`--org @org\` (e.g. \`uniweb ${verb} --org @acme\`).`
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const suggestion = didYouMean(flag, all)
|
|
242
|
+
const lines = [`Unknown flag \`${flag}\` for \`uniweb ${verb}\`.`]
|
|
243
|
+
if (suggestion) lines.push(` Did you mean \`${suggestion}\`?`)
|
|
244
|
+
lines.push(` Run \`uniweb ${verb} --help\` for the accepted flags.`)
|
|
245
|
+
return { flag, suggestion, message: lines.join('\n') }
|
|
246
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Giving a foundation its name — `name` in `main.js`'s default export.
|
|
3
|
+
*
|
|
4
|
+
* The rule itself (main.js `name`, else package.json `name`; `src` and `foundation`
|
|
5
|
+
* refused) is `@uniweb/build`'s `foundation-name.js`, shared by the build, `register`
|
|
6
|
+
* and `push`. This module only WRITES the name: the scaffold at `create` / `add`, and
|
|
7
|
+
* `register` when it asks for one.
|
|
8
|
+
*
|
|
9
|
+
* ⛔ NO `@uniweb/build` IMPORT, AND NO IMPORT OF main.js. `uniweb create` runs before
|
|
10
|
+
* any project exists: `@uniweb/build` is an optional peer that may not be installed,
|
|
11
|
+
* and main.js's own imports may not be either. Hence text scans rather than a load,
|
|
12
|
+
* and the one duplicated fact below, pinned to the build's list by
|
|
13
|
+
* `test/foundation-name.test.js`. What a foundation IS named is always read by the
|
|
14
|
+
* build (`readFoundationName`); these only decide where to write.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
18
|
+
import { join, resolve, basename, dirname } from 'node:path'
|
|
19
|
+
|
|
20
|
+
// The folders a foundation's code lives in — never a foundation's name
|
|
21
|
+
// (`@uniweb/build`'s FORBIDDEN_FOUNDATION_NAMES; the test keeps them equal).
|
|
22
|
+
export const SCAFFOLD_FOLDER_NAMES = new Set(['src', 'foundation'])
|
|
23
|
+
|
|
24
|
+
// Written above the name wherever the CLI adds one — the scaffold's main.js
|
|
25
|
+
// template says the same.
|
|
26
|
+
const NAME_COMMENT = '// What this foundation registers as — @<org>/<name> — and what sites pin.'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A name made from free text — a project's name, a folder's — in the form a
|
|
30
|
+
* foundation name takes: lowercase letters, digits and inner hyphens.
|
|
31
|
+
* `My Site` → `my-site`. Null when nothing usable is left, or when what is left
|
|
32
|
+
* names a folder rather than a foundation.
|
|
33
|
+
*
|
|
34
|
+
* @param {unknown} raw
|
|
35
|
+
* @returns {string|null}
|
|
36
|
+
*/
|
|
37
|
+
export function normalizeFoundationName(raw) {
|
|
38
|
+
const name = String(raw ?? '')
|
|
39
|
+
.trim()
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/^@[^/]*\//, '')
|
|
42
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
43
|
+
.replace(/^-+|-+$/g, '')
|
|
44
|
+
return name && !SCAFFOLD_FOLDER_NAMES.has(name) ? name : null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A name to offer a foundation that has none of its own: its folder's — or, for
|
|
49
|
+
* the `src/` and `foundation/` a scaffold makes, its project's folder. Null when
|
|
50
|
+
* neither makes one.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} foundationDir
|
|
53
|
+
* @returns {string|null}
|
|
54
|
+
*/
|
|
55
|
+
export function suggestFoundationName(foundationDir) {
|
|
56
|
+
const dir = resolve(foundationDir)
|
|
57
|
+
return (
|
|
58
|
+
normalizeFoundationName(basename(dir)) ||
|
|
59
|
+
normalizeFoundationName(basename(dirname(dir)))
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether `main.js`'s default export names the foundation — a `name` key at the
|
|
65
|
+
* export's own level, not one nested in `props` or `vars`.
|
|
66
|
+
*
|
|
67
|
+
* A scan, not a parse (see the module note): it skips strings and comments and
|
|
68
|
+
* counts brackets, which is all an object literal of declarations needs. A name
|
|
69
|
+
* that arrives by a spread is invisible to it — the build's read is the truth.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} text - main.js source
|
|
72
|
+
* @returns {boolean}
|
|
73
|
+
*/
|
|
74
|
+
export function mainNamesFoundation(text) {
|
|
75
|
+
const opening = /export\s+default\s*\{/.exec(text)
|
|
76
|
+
if (!opening) return false
|
|
77
|
+
let depth = 1
|
|
78
|
+
let atKey = true // just after the opening brace, or a comma at the top level
|
|
79
|
+
for (let i = opening.index + opening[0].length; i < text.length && depth > 0; i++) {
|
|
80
|
+
const c = text[i]
|
|
81
|
+
if (c === '/' && text[i + 1] === '/') {
|
|
82
|
+
i = text.indexOf('\n', i)
|
|
83
|
+
if (i < 0) break
|
|
84
|
+
} else if (c === '/' && text[i + 1] === '*') {
|
|
85
|
+
i = text.indexOf('*/', i + 2) + 1
|
|
86
|
+
if (i <= 0) break
|
|
87
|
+
} else if (c === "'" || c === '"' || c === '`') {
|
|
88
|
+
let j = i + 1
|
|
89
|
+
while (j < text.length && text[j] !== c) j += text[j] === '\\' ? 2 : 1
|
|
90
|
+
if (depth === 1 && atKey && text.slice(i + 1, j) === 'name' && /^\s*:/.test(text.slice(j + 1))) {
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
i = j
|
|
94
|
+
atKey = false
|
|
95
|
+
} else if ('{[('.includes(c)) {
|
|
96
|
+
depth++
|
|
97
|
+
atKey = false
|
|
98
|
+
} else if ('}])'.includes(c)) {
|
|
99
|
+
depth--
|
|
100
|
+
} else if (depth === 1 && c === ',') {
|
|
101
|
+
atKey = true
|
|
102
|
+
} else if (depth === 1 && atKey && /[A-Za-z_$]/.test(c)) {
|
|
103
|
+
const word = /^[A-Za-z_$][\w$]*/.exec(text.slice(i))[0]
|
|
104
|
+
// `name: 'x'`, or the shorthand `{ name }`
|
|
105
|
+
if (word === 'name' && /^\s*[:,}]/.test(text.slice(i + word.length))) return true
|
|
106
|
+
i += word.length - 1
|
|
107
|
+
atKey = false
|
|
108
|
+
} else if (!/\s/.test(c)) {
|
|
109
|
+
atKey = false
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Write `name` into a foundation's `main.js`.
|
|
117
|
+
*
|
|
118
|
+
* With `replace`, the one `name: '<replace>'` literal is rewritten — the case where
|
|
119
|
+
* main.js already names the foundation, with a name that cannot register. Without
|
|
120
|
+
* it, `name` becomes the default export's first key. A missing main.js is created.
|
|
121
|
+
*
|
|
122
|
+
* ⚠️ A text edit, not a parse: it handles `export default { … }` — every scaffold
|
|
123
|
+
* and every template — and says so otherwise rather than guessing.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} mainFile
|
|
126
|
+
* @param {string} name - already a valid foundation name
|
|
127
|
+
* @param {{ replace?: string|null }} [opts]
|
|
128
|
+
* @returns {{ ok: true } | { ok: false, reason: string }}
|
|
129
|
+
*/
|
|
130
|
+
export function writeFoundationName(mainFile, name, { replace = null } = {}) {
|
|
131
|
+
const line = `${NAME_COMMENT}\n name: '${name}',`
|
|
132
|
+
if (!existsSync(mainFile)) {
|
|
133
|
+
writeFileSync(mainFile, `export default {\n ${line}\n}\n`)
|
|
134
|
+
return { ok: true }
|
|
135
|
+
}
|
|
136
|
+
const text = readFileSync(mainFile, 'utf8')
|
|
137
|
+
|
|
138
|
+
if (replace !== null) {
|
|
139
|
+
const literal = new RegExp(
|
|
140
|
+
`(\\bname\\s*:\\s*)(['"\`])${replace.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\2`,
|
|
141
|
+
'g'
|
|
142
|
+
)
|
|
143
|
+
const hits = text.match(literal) || []
|
|
144
|
+
if (hits.length !== 1) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
reason: `main.js does not name the foundation "${replace}" in exactly one place`
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
writeFileSync(mainFile, text.replace(literal, `$1'${name}'`))
|
|
151
|
+
return { ok: true }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const opening = /export\s+default\s*\{/.exec(text)
|
|
155
|
+
if (!opening) {
|
|
156
|
+
return { ok: false, reason: 'main.js has no `export default { … }` to add a name to' }
|
|
157
|
+
}
|
|
158
|
+
// The name leads the export — on lines of its own, so a multi-line export keeps
|
|
159
|
+
// its first line and gains a blank one between.
|
|
160
|
+
const at = opening.index + opening[0].length
|
|
161
|
+
writeFileSync(mainFile, `${text.slice(0, at)}\n ${line}\n${text.slice(at)}`)
|
|
162
|
+
return { ok: true }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Give a freshly scaffolded foundation its name, unless its main.js already has one.
|
|
167
|
+
*
|
|
168
|
+
* Called after template content is applied, because a template's own main.js
|
|
169
|
+
* replaces the scaffolded one — and with it the name the scaffold wrote.
|
|
170
|
+
*
|
|
171
|
+
* @param {string} foundationDir - the scaffolded package (flat layout: main.js at its root)
|
|
172
|
+
* @param {string|null} name
|
|
173
|
+
*/
|
|
174
|
+
export function ensureFoundationName(foundationDir, name) {
|
|
175
|
+
if (!name) return
|
|
176
|
+
const mainFile = join(foundationDir, 'main.js')
|
|
177
|
+
if (existsSync(mainFile) && mainNamesFoundation(readFileSync(mainFile, 'utf8'))) return
|
|
178
|
+
writeFoundationName(mainFile, name)
|
|
179
|
+
}
|