uniweb 0.12.6 → 0.12.8

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/src/index.js CHANGED
@@ -440,20 +440,9 @@ async function main() {
440
440
  return
441
441
  }
442
442
 
443
- // Global install launcher: delegate project-bound commands to local CLI.
444
- //
445
- // Escape hatch: `UNIWEB_DISABLE_LOCAL_DELEGATION=1` forces the in-process
446
- // CLI to handle the command itself, even when a project-local copy of
447
- // `uniweb` is installed. This exists for the workspace-ergonomics eval
448
- // harness — when it points the eval at `node $WORKSPACE_ROOT/.../index.js`
449
- // it expects to exercise the workspace source, not whatever `uniweb`
450
- // version is symlinked under the test fixture's `node_modules`. Without
451
- // the escape, evals silently test the published npm version and any
452
- // unpublished workspace fixes are invisible. See
453
- // `kb/framework/build/workspace-ergonomics-runbook.md` (`--cli=workspace`).
443
+ // Global install launcher: delegate project-bound commands to local CLI
454
444
  const global = isGlobalInstall()
455
- const skipDelegation = process.env.UNIWEB_DISABLE_LOCAL_DELEGATION === '1'
456
- if (global && !skipDelegation && command && !STANDALONE_COMMANDS.has(command)) {
445
+ if (global && command && !STANDALONE_COMMANDS.has(command)) {
457
446
  const localCli = findLocalCli()
458
447
  if (localCli) {
459
448
  await delegateToLocal(localCli)
@@ -550,6 +539,13 @@ async function main() {
550
539
  return
551
540
  }
552
541
 
542
+ // Handle export command (dynamic import — depends on @uniweb/build)
543
+ if (command === 'export') {
544
+ const { exportSite } = await importProjectCommand('./commands/export.js')
545
+ await exportSite(args.slice(1))
546
+ return
547
+ }
548
+
553
549
  // Handle login command
554
550
  if (command === 'login') {
555
551
  await login(args.slice(1))
@@ -836,7 +832,8 @@ ${colors.bright}Commands:${colors.reset}
836
832
  rename <type> Rename a workspace package (foundation today)
837
833
  build Build the current project
838
834
  deploy Deploy a site to Uniweb hosting
839
- publish Publish a foundation to the Uniweb Registry
835
+ export Export a self-contained site for third-party hosting
836
+ publish Publish a foundation to the Uniweb catalog (deliberate; for site-bound foundations, use deploy)
840
837
  invite <email> Create a foundation invite for a client
841
838
  handoff <email> Hand off a site to a client
842
839
  inspect <path> Inspect parsed content shape of a markdown file or folder
@@ -866,10 +863,20 @@ ${colors.bright}Global Options:${colors.reset}
866
863
  Auto-detected when CI=true or no TTY (pipes, agents)
867
864
 
868
865
  ${colors.bright}Publish Options:${colors.reset}
866
+ --catalog Confirm publish to the public catalog (required in CI)
867
+ --propagate Walk trusting sites' policy waves (default: silent)
868
+ --name <id> Foundation id (overrides package.json::uniweb.id)
869
+ --namespace <ns> Force org-scope namespace (overrides package.json)
869
870
  --local Publish to the local registry (.unicloud/) instead of Uniweb Registry
871
+ --registry <url> Use a specific registry URL
870
872
  --edit-access <p> Set edit access policy: "open" or "restricted" (default: restricted)
871
873
  --dry-run Show what would be published without uploading
872
874
 
875
+ uniweb publish is for cataloging a foundation as a product. For
876
+ site-bound foundations (one foundation, one site), use uniweb deploy
877
+ instead — it auto-publishes under a site-scoped slot, no naming
878
+ ceremony.
879
+
873
880
  ${colors.bright}Invite Options:${colors.reset}
874
881
  --uses <n> Max sites per invite (default: 1)
875
882
  --expires <days> Days until expiry (default: 30)
@@ -889,16 +896,16 @@ ${colors.bright}Template Options:${colors.reset}
889
896
  --registry <url> Registry URL (default: http://localhost:4001)
890
897
 
891
898
  ${colors.bright}Deploy Options:${colors.reset}
892
- --dry-run Resolve site.yml + foundation/runtime but skip the Worker POST
893
- --skip-build Don't rebuild, use existing dist/ as-is
894
- --skip-assets Skip binary asset upload (content-only deploy)
899
+ --dry-run Resolve site.yml + foundation/runtime; print summary; no writes
900
+ --no-auto-publish Don't auto-publish workspace-local foundation as part of deploy
901
+
902
+ ${colors.bright}Export Options:${colors.reset}
903
+ --no-prerender Skip per-page prerendered HTML
895
904
 
896
905
  ${colors.bright}Build Options:${colors.reset}
897
906
  --target <type> Build target (foundation, site) - auto-detected if not specified
898
- --link Site: data-only pipeline (Uniweb-edge hosting)
899
- --bundle Site: vite-built static-host artifact (default)
900
- --prerender Force pre-rendering (bundle mode only; overrides site.yml)
901
- --no-prerender Skip pre-rendering (bundle mode only; overrides site.yml)
907
+ --prerender Force pre-rendering (overrides site.yml)
908
+ --no-prerender Skip pre-rendering (overrides site.yml)
902
909
  --foundation-dir Path to foundation directory (for prerendering)
903
910
  --platform <name> Deployment platform (e.g., vercel) for platform-specific output
904
911
 
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Internal env-var helpers.
3
+ *
4
+ * UNIWEB_* env vars are escape hatches for developers, operators, and the
5
+ * platform test team — not user-facing settings. Documented in
6
+ * `framework/cli/docs/env-vars.md`. Anything truly user-facing should be a
7
+ * flag, not an env var.
8
+ */
9
+
10
+ /**
11
+ * Parse a boolean-shaped env var. Returns true for "1", "true", "yes" (any
12
+ * case); false otherwise (including unset, empty, or any other string).
13
+ *
14
+ * @param {string} name - Env var name (e.g., "UNIWEB_SKIP_BUILD").
15
+ * @returns {boolean}
16
+ */
17
+ export function parseBoolEnv(name) {
18
+ const raw = process.env[name]
19
+ if (!raw) return false
20
+ const v = String(raw).trim().toLowerCase()
21
+ return v === '1' || v === 'true' || v === 'yes'
22
+ }
@@ -144,9 +144,8 @@ export class LocalRegistry {
144
144
 
145
145
  /**
146
146
  * Get the full version entry for `name@version`, or null if absent.
147
- * Used by `publish` and `deploy` to refill `dist/publish.json` from
148
- * server-of-record state when the local cache is missing — see
149
- * `kb/framework/build/workspace-ergonomics.md` (receipt-as-cache).
147
+ * Used by `publish` (pre-flight duplicate check) and `deploy`
148
+ * (staleness check via git provenance comparison).
150
149
  *
151
150
  * @param {string} name
152
151
  * @param {string} version
@@ -270,8 +269,8 @@ export class RemoteRegistry {
270
269
 
271
270
  /**
272
271
  * Get the full version entry for `name@version`, or null if absent.
273
- * Used by `publish` and `deploy` to refill `dist/publish.json` from
274
- * server-of-record state when the local cache is missing.
272
+ * Used by `publish` (pre-flight duplicate check) and `deploy`
273
+ * (staleness check via git provenance comparison).
275
274
  *
276
275
  * @param {string} name
277
276
  * @param {string} version
@@ -1,91 +0,0 @@
1
- /**
2
- * `dist/publish.json` receipt — shared shape used by `publish` and `deploy`.
3
- *
4
- * The receipt is a per-checkout cache of the last publish; it lets the
5
- * deploy verb decide whether a workspace-local foundation needs republishing
6
- * without a network round-trip on the happy path. It's gitignored, so it
7
- * never travels with the source — fresh clones, CI runs, and teammates all
8
- * start with no cache. Both verbs refill the cache lazily by reading the
9
- * registry's index when the local file is missing.
10
- *
11
- * See `kb/framework/build/workspace-ergonomics.md` for the full rationale.
12
- */
13
-
14
- /**
15
- * Compose the canonical six-field receipt body. All callers MUST go through
16
- * this helper so the runbook's pp-10 schema check stays meaningful.
17
- *
18
- * @param {Object} params
19
- * @param {string|null} params.gitSha
20
- * @param {boolean} params.gitDirty
21
- * @param {string} params.url
22
- * @param {string} params.publishedAt
23
- * @param {string} params.classification 'propagate' | 'silent'
24
- * @returns {Object}
25
- */
26
- export function composeReceipt({ gitSha, gitDirty, url, publishedAt, classification }) {
27
- return {
28
- schemaVersion: 1,
29
- publishedFromGitSha: gitSha,
30
- publishedFromGitDirty: gitDirty,
31
- url,
32
- publishedAt,
33
- classification,
34
- }
35
- }
36
-
37
- /**
38
- * Resolve the canonical receipt URL given (in priority order):
39
- * 1. A `publishResult.url` from a fresh upload — server-rendered, handles
40
- * empty-scope rewrites the CLI can't synthesize.
41
- * 2. An `existingEntry.url` recorded by a previous publish (refill path).
42
- * 3. A synthesized canonical form — `file://` for local registries,
43
- * `<apiUrl>/foundations/<name>@<version>/foundation.js` for remote.
44
- * The remote form mirrors the path the worker returns in `publishResult.url`,
45
- * which keeps the receipt's URL parseable by the regex in
46
- * `deploy.js::deriveLocalFoundationRef` even when the registry's index
47
- * entry doesn't carry an explicit `url` field. (Unicloud's index entries
48
- * don't; uniweb-edge's index entries don't either — both rely on the
49
- * response shape, not the index shape.)
50
- *
51
- * Path-shaped candidates (e.g. `/foundations/...`) are joined with the
52
- * registry's `apiUrl` so the receipt always carries an absolute URL.
53
- */
54
- export function deriveReceiptUrl({ publishResult, existingEntry, registry, name, version, isLocal }) {
55
- const candidate = publishResult?.url || existingEntry?.url
56
- if (candidate) {
57
- if (candidate.startsWith('http') || candidate.startsWith('file://')) return candidate
58
- if (registry?.apiUrl) return new URL(candidate, registry.apiUrl).toString()
59
- }
60
- if (isLocal) return `file://${registry.getPackagePath(name, version)}/`
61
- return `${registry.apiUrl.replace(/\/$/, '')}/foundations/${name}@${version}/foundation.js`
62
- }
63
-
64
- /**
65
- * Build a receipt from an existing registry version entry, used to refill a
66
- * missing `dist/publish.json` from server-of-record state. Returns null if
67
- * the entry doesn't carry enough provenance to make the receipt useful for
68
- * staleness checks (the only field that strictly must be present is
69
- * `publishedFromGitSha` — without it, the deploy verb can't compare against
70
- * HEAD, and refilling would just re-trigger the auto-publish next run).
71
- */
72
- export function receiptFromRegistryEntry({ existingEntry, registry, name, version, isLocal, isPropagateDefault }) {
73
- if (!existingEntry || !existingEntry.publishedFromGitSha) return null
74
- return composeReceipt({
75
- gitSha: existingEntry.publishedFromGitSha,
76
- gitDirty: existingEntry.publishedFromGitDirty ?? false,
77
- url: deriveReceiptUrl({ existingEntry, registry, name, version, isLocal }),
78
- publishedAt: existingEntry.publishedAt || new Date().toISOString(),
79
- classification: existingEntry.classification || (isPropagateDefault ? 'propagate' : 'silent'),
80
- })
81
- }
82
-
83
- /**
84
- * Split `@ns/name@ver`, `~user/name@ver`, or `name@ver` into name + version.
85
- * Returns null on any shape we don't recognize.
86
- */
87
- export function splitRegistryRef(ref) {
88
- if (typeof ref !== 'string') return null
89
- const m = /^(@[^/]+\/[^@]+|~[^/]+\/[^@]+|[^@]+)@(.+)$/.exec(ref)
90
- return m ? { name: m[1], version: m[2] } : null
91
- }