uniweb 0.12.8 → 0.12.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.9",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,14 +41,14 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
-
"@uniweb/core": "0.7.10",
|
|
45
44
|
"@uniweb/kit": "0.9.10",
|
|
46
|
-
"@uniweb/runtime": "0.8.11"
|
|
45
|
+
"@uniweb/runtime": "0.8.11",
|
|
46
|
+
"@uniweb/core": "0.7.10"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@uniweb/build": "0.
|
|
50
|
-
"@uniweb/
|
|
51
|
-
"@uniweb/
|
|
49
|
+
"@uniweb/build": "0.14.1",
|
|
50
|
+
"@uniweb/semantic-parser": "1.1.17",
|
|
51
|
+
"@uniweb/content-reader": "1.1.10"
|
|
52
52
|
},
|
|
53
53
|
"peerDependenciesMeta": {
|
|
54
54
|
"@uniweb/build": {
|
package/src/commands/build.js
CHANGED
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
* uniweb build --target site # Explicitly build as site
|
|
36
36
|
* uniweb build --prerender # Force pre-rendering
|
|
37
37
|
* uniweb build --no-prerender # Skip pre-rendering
|
|
38
|
+
* uniweb build --host <name> # Override site.yml deploy.host (e.g.
|
|
39
|
+
* netlify, s3-cloudfront, generic-static)
|
|
38
40
|
*
|
|
39
41
|
* Internal flags (called by `uniweb deploy` / `uniweb export`):
|
|
40
42
|
* --link # Data-only pipeline (Uniweb-edge)
|
|
@@ -540,7 +542,7 @@ async function resolveFoundationDirForSite(siteDir, siteConfig) {
|
|
|
540
542
|
* Build a site
|
|
541
543
|
*/
|
|
542
544
|
async function buildSite(projectDir, options = {}) {
|
|
543
|
-
const { prerender = false, foundationDir, siteConfig = null } = options
|
|
545
|
+
const { prerender = false, foundationDir, siteConfig = null, host = null } = options
|
|
544
546
|
|
|
545
547
|
info('Building site...')
|
|
546
548
|
|
|
@@ -613,6 +615,7 @@ async function buildSite(projectDir, options = {}) {
|
|
|
613
615
|
|
|
614
616
|
const result = await prerenderSite(projectDir, {
|
|
615
617
|
foundationDir: foundationDir || resolveFoundationDir(projectDir, siteConfig),
|
|
618
|
+
host,
|
|
616
619
|
onProgress: (msg) => log(` ${colors.dim}${msg}${colors.reset}`)
|
|
617
620
|
})
|
|
618
621
|
|
|
@@ -710,7 +713,7 @@ async function discoverWorkspacePackages(workspaceDir) {
|
|
|
710
713
|
* Build all packages in a workspace
|
|
711
714
|
*/
|
|
712
715
|
async function buildWorkspace(workspaceDir, options = {}) {
|
|
713
|
-
const { prerenderFlag, noPrerenderFlag } = options
|
|
716
|
+
const { prerenderFlag, noPrerenderFlag, host = null } = options
|
|
714
717
|
|
|
715
718
|
log(`${colors.cyan}${colors.bright}Building workspace...${colors.reset}`)
|
|
716
719
|
log('')
|
|
@@ -757,7 +760,7 @@ async function buildWorkspace(workspaceDir, options = {}) {
|
|
|
757
760
|
// Resolve foundation directory for this site
|
|
758
761
|
const foundationDir = resolveFoundationDir(site.path, siteConfig)
|
|
759
762
|
|
|
760
|
-
await buildSite(site.path, { prerender, foundationDir, siteConfig })
|
|
763
|
+
await buildSite(site.path, { prerender, foundationDir, siteConfig, host })
|
|
761
764
|
log('')
|
|
762
765
|
}
|
|
763
766
|
|
|
@@ -835,6 +838,14 @@ export async function build(args = []) {
|
|
|
835
838
|
foundationDir = resolve(args[foundationDirIndex + 1])
|
|
836
839
|
}
|
|
837
840
|
|
|
841
|
+
// --host overrides site.yml's deploy.host for this build's prerender
|
|
842
|
+
// step. Validated lazily (inside prerender.js, via the registry).
|
|
843
|
+
let host = null
|
|
844
|
+
const hostIndex = args.indexOf('--host')
|
|
845
|
+
if (hostIndex !== -1 && args[hostIndex + 1]) {
|
|
846
|
+
host = args[hostIndex + 1]
|
|
847
|
+
}
|
|
848
|
+
|
|
838
849
|
// Auto-detect project type if not specified
|
|
839
850
|
if (!targetType) {
|
|
840
851
|
targetType = detectProjectType(projectDir)
|
|
@@ -868,7 +879,7 @@ export async function build(args = []) {
|
|
|
868
879
|
// Run appropriate build
|
|
869
880
|
try {
|
|
870
881
|
if (targetType === 'workspace') {
|
|
871
|
-
await buildWorkspace(projectDir, { prerenderFlag, noPrerenderFlag })
|
|
882
|
+
await buildWorkspace(projectDir, { prerenderFlag, noPrerenderFlag, host })
|
|
872
883
|
} else if (targetType === 'foundation') {
|
|
873
884
|
await buildFoundation(projectDir)
|
|
874
885
|
} else {
|
|
@@ -898,7 +909,7 @@ export async function build(args = []) {
|
|
|
898
909
|
if (prerenderFlag) prerender = true
|
|
899
910
|
if (noPrerenderFlag) prerender = false
|
|
900
911
|
|
|
901
|
-
await buildSite(projectDir, { prerender, foundationDir, siteConfig })
|
|
912
|
+
await buildSite(projectDir, { prerender, foundationDir, siteConfig, host })
|
|
902
913
|
}
|
|
903
914
|
} catch (err) {
|
|
904
915
|
error(err.message)
|
package/src/commands/deploy.js
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Deploy Command
|
|
3
3
|
*
|
|
4
|
-
* Deploys a site
|
|
5
|
-
*
|
|
6
|
-
* For static-host artifacts (no upload), see `uniweb export`.
|
|
4
|
+
* Deploys a site. Host is determined by `deploy.host` in site.yml (or
|
|
5
|
+
* `--host <name>` flag). The default is `uniweb`:
|
|
7
6
|
*
|
|
8
|
-
*
|
|
7
|
+
* - `uniweb` (default): Uniweb hosting — link-mode + edge JIT prerender.
|
|
8
|
+
* Foundation loaded by URL from the registry. Requires `uniweb login`
|
|
9
|
+
* and a `foundation:` declaration in site.yml.
|
|
10
|
+
*
|
|
11
|
+
* - Static-host adapters (`s3-cloudfront`, `cloudflare-pages`,
|
|
12
|
+
* `github-pages`, `generic-static`, …): build dist/ in bundle-mode
|
|
13
|
+
* and hand it to a host adapter for upload + invalidation. No login,
|
|
14
|
+
* no edge. See kb/framework/plans/static-host-deploy-adapters.md.
|
|
15
|
+
*
|
|
16
|
+
* For static-host artifacts WITHOUT upload, see `uniweb export`.
|
|
17
|
+
*
|
|
18
|
+
* Default-flow steps:
|
|
9
19
|
* 1. Read site.yml → { site.id?, site.handle?, foundation, runtime? }.
|
|
10
|
-
* 2. Resolve runtime (default: GET /
|
|
20
|
+
* 2. Resolve runtime (default: GET /runtime/latest from the Worker).
|
|
11
21
|
* 3. ensureAuth() → bearer CLI JWT from ~/.uniweb/auth.json.
|
|
12
22
|
* 4. Build `dist/` if missing.
|
|
13
23
|
* 5. Load dist/site-content.json → extract `languages` for the capability
|
|
@@ -19,9 +29,9 @@
|
|
|
19
29
|
* - publishToken returned → fast path.
|
|
20
30
|
* - needsReview:true + reviewUrl → open browser, wait for callback,
|
|
21
31
|
* consume { publishToken, siteId, handle }.
|
|
22
|
-
* 9. POST Worker /
|
|
32
|
+
* 9. POST Worker /publish/check to confirm foundation + runtime
|
|
23
33
|
* exist and the token's namespace claim matches.
|
|
24
|
-
* 10. POST Worker /
|
|
34
|
+
* 10. POST Worker /publish with the full payload.
|
|
25
35
|
* 11. On first-deploy create flow: write site.id + site.handle back into
|
|
26
36
|
* site.yml so subsequent deploys fast-path.
|
|
27
37
|
*
|
|
@@ -29,6 +39,8 @@
|
|
|
29
39
|
* uniweb deploy Normal deploy (browser may open on first deploy)
|
|
30
40
|
* uniweb deploy --dry-run Resolve everything but skip the Worker POST
|
|
31
41
|
* uniweb deploy --no-auto-publish Don't auto-publish workspace-local foundation
|
|
42
|
+
* uniweb deploy --host <name> Static-host flow (e.g., s3-cloudfront,
|
|
43
|
+
* generic-static). Overrides site.yml deploy.host.
|
|
32
44
|
*
|
|
33
45
|
* Internal escape hatches (UNIWEB_* env vars — see framework/cli/docs/env-vars.md):
|
|
34
46
|
* UNIWEB_SKIP_BUILD=1 Reuse existing dist/ instead of rebuilding
|
|
@@ -404,6 +416,22 @@ export async function deploy(args = []) {
|
|
|
404
416
|
// site.id / site.handle from prior deploys.
|
|
405
417
|
const siteYmlPath = join(siteDir, 'site.yml')
|
|
406
418
|
const siteYml = await readSiteYml(siteYmlPath)
|
|
419
|
+
|
|
420
|
+
// Host dispatch. The default host is `uniweb` — Uniweb hosting
|
|
421
|
+
// (link-mode + edge JIT prerender), which is the rest of this
|
|
422
|
+
// function. Any other named host is a static-host adapter; hand off
|
|
423
|
+
// to it and return. The default flow requires a `foundation:`
|
|
424
|
+
// declaration; static-host deploys don't, so this branch comes BEFORE
|
|
425
|
+
// the foundation check.
|
|
426
|
+
// See kb/framework/plans/static-host-deploy-adapters.md.
|
|
427
|
+
const hostFlagIndex = args.indexOf('--host')
|
|
428
|
+
const hostFromFlag = hostFlagIndex !== -1 ? args[hostFlagIndex + 1] : null
|
|
429
|
+
const host = hostFromFlag || siteYml.deploy?.host || 'uniweb'
|
|
430
|
+
if (host !== 'uniweb') {
|
|
431
|
+
await deployStaticHost(siteDir, siteYml, host, { dryRun })
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
|
|
407
435
|
if (!siteYml.foundation) {
|
|
408
436
|
say.err('site.yml is missing `foundation`.')
|
|
409
437
|
say.dim('Add a line like: foundation: \'@uniweb/docs-foundation@0.1.20\'')
|
|
@@ -512,7 +540,7 @@ export async function deploy(args = []) {
|
|
|
512
540
|
if (!runtimeVersion) {
|
|
513
541
|
runtimeVersion = await fetchLatestRuntime(workerUrl)
|
|
514
542
|
if (!runtimeVersion) {
|
|
515
|
-
say.err('Could not resolve a runtime version (no runtime: in site.yml, /
|
|
543
|
+
say.err('Could not resolve a runtime version (no runtime: in site.yml, /runtime/latest failed).')
|
|
516
544
|
process.exit(1)
|
|
517
545
|
}
|
|
518
546
|
say.dim(`Runtime: ${runtimeVersion} (latest; pin via \`runtime:\` in site.yml)`)
|
|
@@ -542,11 +570,12 @@ export async function deploy(args = []) {
|
|
|
542
570
|
// serving edge, not here). Always --link: the edge serves a runtime
|
|
543
571
|
// template + per-site base.html, never a self-contained vite bundle.
|
|
544
572
|
//
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
//
|
|
573
|
+
// Phase 4d: workspace-local foundations carry the `~self/{name}@{ver}`
|
|
574
|
+
// placeholder at this point; the canonical `~{siteId}/...` ref isn't
|
|
575
|
+
// known until authorize returns. Link mode doesn't run vite or fetch
|
|
576
|
+
// the foundation, so site-content.json's foundation field reflects
|
|
577
|
+
// whatever's in site.yml — that's fine because the publish payload
|
|
578
|
+
// overrides it with the canonical form post-authorize.
|
|
550
579
|
//
|
|
551
580
|
// Spawn the SAME CLI binary that's currently running rather than
|
|
552
581
|
// `npx uniweb build` — npx walks node_modules and would resolve to
|
|
@@ -556,9 +585,7 @@ export async function deploy(args = []) {
|
|
|
556
585
|
execSync(`node ${JSON.stringify(process.argv[1])} build --link`, {
|
|
557
586
|
cwd: siteDir,
|
|
558
587
|
stdio: 'inherit',
|
|
559
|
-
env:
|
|
560
|
-
? { ...process.env, UNIWEB_FOUNDATION_REF: foundationBuildOverride }
|
|
561
|
-
: process.env,
|
|
588
|
+
env: process.env,
|
|
562
589
|
})
|
|
563
590
|
console.log('')
|
|
564
591
|
} else if (!existsSync(contentPath)) {
|
|
@@ -693,9 +720,16 @@ export async function deploy(args = []) {
|
|
|
693
720
|
// CLI can write `features:` back into site.yml accurately. Older
|
|
694
721
|
// PHP that doesn't include this field is a no-op.
|
|
695
722
|
mintedFeatures = Array.isArray(cb.features) ? cb.features : null
|
|
723
|
+
// Phase 4d: workspace-local foundation deploys on the create flow
|
|
724
|
+
// need the rewritten `~{siteId}/{name}@{ver}` ref + upload endpoint.
|
|
725
|
+
// PHP/unicloud put them in the finalize response; the web app
|
|
726
|
+
// forwards them to the loopback. Catalog-ref deploys leave them
|
|
727
|
+
// undefined and we fall back to the placeholder/derived URL below.
|
|
728
|
+
if (cb.foundationRef) foundation = cb.foundationRef
|
|
729
|
+
if (cb.foundationUploadUrl) foundationUploadUrl = cb.foundationUploadUrl
|
|
696
730
|
// Review path: Worker URLs are implicit (we derive them from config).
|
|
697
|
-
publishUrl = `${workerUrl}/
|
|
698
|
-
validateUrl = `${workerUrl}/
|
|
731
|
+
publishUrl = `${workerUrl}/publish`
|
|
732
|
+
validateUrl = `${workerUrl}/publish/check`
|
|
699
733
|
} else {
|
|
700
734
|
publishToken = authRes.publishToken
|
|
701
735
|
siteIdResolved = authRes.siteId
|
|
@@ -731,7 +765,7 @@ export async function deploy(args = []) {
|
|
|
731
765
|
// Phase 4d: upload site-bound foundation files directly. Replaces the
|
|
732
766
|
// pre-Phase-4d `execSync('uniweb publish')` flow — we now know the
|
|
733
767
|
// canonical `~{siteId}/{name}@{ver}` ref from authorize, and the worker's
|
|
734
|
-
// /
|
|
768
|
+
// /foundations endpoint accepts the publish token's siteId claim
|
|
735
769
|
// for this scope.
|
|
736
770
|
if (localFoundation) {
|
|
737
771
|
say.info(`Building foundation at ${localFoundation.relPath}…`)
|
|
@@ -749,7 +783,7 @@ export async function deploy(args = []) {
|
|
|
749
783
|
|
|
750
784
|
say.info(`Uploading foundation as ${foundation}…`)
|
|
751
785
|
const foundationFiles = await collectFoundationDistFiles(join(localFoundation.path, 'dist'))
|
|
752
|
-
const foundationPublishUrl = foundationUploadUrl || `${workerUrl}/
|
|
786
|
+
const foundationPublishUrl = foundationUploadUrl || `${workerUrl}/foundations`
|
|
753
787
|
const { gitSha: fGitSha, gitDirty: fGitDirty } = readGitState(localFoundation.path)
|
|
754
788
|
await callFoundationUpload({
|
|
755
789
|
url: foundationPublishUrl,
|
|
@@ -784,36 +818,60 @@ export async function deploy(args = []) {
|
|
|
784
818
|
process.exit(1)
|
|
785
819
|
}
|
|
786
820
|
|
|
787
|
-
//
|
|
788
|
-
//
|
|
789
|
-
//
|
|
790
|
-
//
|
|
791
|
-
//
|
|
821
|
+
// Collect compiled collection JSON files from dist/data/. The framework
|
|
822
|
+
// emits these for `collection:` data sources — `<name>.json` cascade
|
|
823
|
+
// payloads plus per-record `<name>/<slug>.json` files when `deferred:` is
|
|
824
|
+
// declared. Editor publish has no equivalent (collections live in the DB);
|
|
825
|
+
// CLI sites need them shipped as static R2 objects.
|
|
826
|
+
//
|
|
827
|
+
// Read BEFORE the asset pipeline so the asset scan can pick up image
|
|
828
|
+
// refs in collection JSON (e.g. `article.image: "/covers/foo.svg"`)
|
|
829
|
+
// and the rewrite can swap them for CDN URLs alongside locale content.
|
|
830
|
+
const dataFiles = await collectDataFiles(distDir)
|
|
831
|
+
// Decode each data file as JSON so the asset scan can walk the tree;
|
|
832
|
+
// mutated in place by the rewrite step. Re-stringified before publish.
|
|
833
|
+
const dataFileObjects = {}
|
|
834
|
+
for (const [k, raw] of Object.entries(dataFiles)) {
|
|
835
|
+
try {
|
|
836
|
+
dataFileObjects[k] = JSON.parse(raw)
|
|
837
|
+
} catch {
|
|
838
|
+
dataFileObjects[k] = null // unparseable — skip rewrite, ship as-is
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (Object.keys(dataFiles).length > 0) {
|
|
842
|
+
say.dim(`Data files : ${Object.keys(dataFiles).length} (collection JSON)`)
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Asset pipeline — upload dist/assets/* + favicon + fonts + content-scan
|
|
846
|
+
// hits (public/, data file refs) to S3, then rewrite each locale's
|
|
847
|
+
// siteContent + each parsed data file so the runtime resolves CDN URLs at
|
|
848
|
+
// render time. Assets are locale-shared (they live in dist/assets/ +
|
|
849
|
+
// public/ regardless of language); diff/upload runs once and the rewrite
|
|
850
|
+
// walks every locale's content tree + every data-file JSON tree.
|
|
792
851
|
// Skipped with --skip-assets.
|
|
793
852
|
if (!skipAssets) {
|
|
794
853
|
await uploadAssetsAndRewriteContent({
|
|
795
854
|
siteDir,
|
|
796
855
|
localeContents,
|
|
856
|
+
dataFileObjects,
|
|
797
857
|
siteYml,
|
|
798
858
|
theme,
|
|
799
859
|
backendUrl,
|
|
800
860
|
cliToken,
|
|
801
861
|
siteId: siteIdResolved,
|
|
802
862
|
})
|
|
863
|
+
// Re-stringify any data-file JSON that the rewrite step mutated, so the
|
|
864
|
+
// publish payload below sees the rewritten URLs. Untouched files round-
|
|
865
|
+
// trip identically.
|
|
866
|
+
for (const k of Object.keys(dataFiles)) {
|
|
867
|
+
if (dataFileObjects[k] !== null) {
|
|
868
|
+
dataFiles[k] = JSON.stringify(dataFileObjects[k])
|
|
869
|
+
}
|
|
870
|
+
}
|
|
803
871
|
} else {
|
|
804
872
|
say.dim('Skipping asset upload (--skip-assets).')
|
|
805
873
|
}
|
|
806
874
|
|
|
807
|
-
// Collect compiled collection JSON files from dist/data/. The framework
|
|
808
|
-
// emits these for `collection:` data sources — `<name>.json` cascade
|
|
809
|
-
// payloads plus per-record `<name>/<slug>.json` files when `deferred:` is
|
|
810
|
-
// declared. Editor publish has no equivalent (collections live in the DB);
|
|
811
|
-
// CLI sites need them shipped as static R2 objects.
|
|
812
|
-
const dataFiles = await collectDataFiles(distDir)
|
|
813
|
-
if (Object.keys(dataFiles).length > 0) {
|
|
814
|
-
say.dim(`Data files : ${Object.keys(dataFiles).length} (collection JSON)`)
|
|
815
|
-
}
|
|
816
|
-
|
|
817
875
|
say.info('Publishing…')
|
|
818
876
|
const publishPayload = {
|
|
819
877
|
foundation,
|
|
@@ -888,6 +946,107 @@ export async function deploy(args = []) {
|
|
|
888
946
|
}
|
|
889
947
|
}
|
|
890
948
|
|
|
949
|
+
// ─── Static-host deploy (S3+CloudFront, etc.) ─────────────────
|
|
950
|
+
//
|
|
951
|
+
// Distinct from the uniweb-edge flow above. Picked when site.yml's
|
|
952
|
+
// `deploy.host` (or --host flag) names a static-host adapter
|
|
953
|
+
// registered in @uniweb/build/hosts. Always runs `uniweb build`
|
|
954
|
+
// (bundle mode + prerender) first, then hands dist/ to the adapter's
|
|
955
|
+
// deploy hook for upload + invalidation.
|
|
956
|
+
//
|
|
957
|
+
// See kb/framework/plans/static-host-deploy-adapters.md.
|
|
958
|
+
|
|
959
|
+
async function deployStaticHost(siteDir, siteYml, hostName, { dryRun }) {
|
|
960
|
+
let getAdapter
|
|
961
|
+
try {
|
|
962
|
+
({ getAdapter } = await import('@uniweb/build/hosts'))
|
|
963
|
+
} catch (err) {
|
|
964
|
+
say.err('Failed to load host adapter registry from @uniweb/build/hosts.')
|
|
965
|
+
say.dim(err.message)
|
|
966
|
+
process.exit(1)
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
let adapter
|
|
970
|
+
try {
|
|
971
|
+
adapter = getAdapter(hostName)
|
|
972
|
+
} catch (err) {
|
|
973
|
+
say.err(err.message)
|
|
974
|
+
say.dim(`Set deploy.host in site.yml or pass --host=<name>. See \`uniweb deploy --help\`.`)
|
|
975
|
+
process.exit(1)
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (typeof adapter.deploy !== 'function') {
|
|
979
|
+
say.err(`Host adapter '${hostName}' does not implement a deploy step.`)
|
|
980
|
+
say.dim(`Build with \`uniweb build --host=${hostName}\` and upload \`dist/\` manually,`)
|
|
981
|
+
say.dim(`or use a host whose adapter ships a deploy hook (e.g., s3-cloudfront).`)
|
|
982
|
+
process.exit(1)
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const deployConfig = siteYml.deploy || {}
|
|
986
|
+
const distDir = join(siteDir, 'dist')
|
|
987
|
+
|
|
988
|
+
if (dryRun) {
|
|
989
|
+
say.info(`Dry run — would deploy via host adapter: ${c.bold}${adapter.name}${c.reset}`)
|
|
990
|
+
say.dim(`Site dir : ${siteDir}`)
|
|
991
|
+
say.dim(`dist/ : ${existsSync(distDir) ? 'exists (would not rebuild)' : 'missing (would build)'}`)
|
|
992
|
+
say.dim(`deploy.bucket : ${deployConfig.bucket || '(unset)'}`)
|
|
993
|
+
say.dim(`deploy.distId : ${deployConfig.distributionId || '(unset)'}`)
|
|
994
|
+
say.dim(`deploy.region : ${deployConfig.region || '(unset)'}`)
|
|
995
|
+
say.dim(`deploy.profile : ${deployConfig.profile || '(default AWS chain)'}`)
|
|
996
|
+
return
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Always rebuild — the static-host flow expects fresh dist/ on every
|
|
1000
|
+
// deploy. UNIWEB_SKIP_BUILD env var lets CI / dev loops reuse an
|
|
1001
|
+
// existing build (mirrors the uniweb-edge flow's escape hatch).
|
|
1002
|
+
const skipBuild = parseBoolEnv('UNIWEB_SKIP_BUILD')
|
|
1003
|
+
if (skipBuild) {
|
|
1004
|
+
if (!existsSync(distDir)) {
|
|
1005
|
+
say.err('UNIWEB_SKIP_BUILD is set but dist/ does not exist.')
|
|
1006
|
+
process.exit(1)
|
|
1007
|
+
}
|
|
1008
|
+
say.info('UNIWEB_SKIP_BUILD set — reusing existing dist/.')
|
|
1009
|
+
} else {
|
|
1010
|
+
say.info(`Building site (host: ${adapter.name})…`)
|
|
1011
|
+
console.log('')
|
|
1012
|
+
try {
|
|
1013
|
+
execSync(
|
|
1014
|
+
`node ${JSON.stringify(process.argv[1])} build --bundle --host ${JSON.stringify(adapter.name)}`,
|
|
1015
|
+
{ cwd: siteDir, stdio: 'inherit' }
|
|
1016
|
+
)
|
|
1017
|
+
} catch {
|
|
1018
|
+
say.err('Build failed. See output above.')
|
|
1019
|
+
process.exit(1)
|
|
1020
|
+
}
|
|
1021
|
+
if (!existsSync(distDir)) {
|
|
1022
|
+
say.err('Build did not produce dist/.')
|
|
1023
|
+
process.exit(1)
|
|
1024
|
+
}
|
|
1025
|
+
console.log('')
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Hand off to the adapter. DeployError is the structured shape from
|
|
1029
|
+
// @uniweb/build/hosts/s3-cloudfront — translate to user-facing output.
|
|
1030
|
+
try {
|
|
1031
|
+
await adapter.deploy({
|
|
1032
|
+
distDir,
|
|
1033
|
+
deployConfig,
|
|
1034
|
+
env: process.env,
|
|
1035
|
+
log: (m) => console.log(m),
|
|
1036
|
+
})
|
|
1037
|
+
} catch (err) {
|
|
1038
|
+
if (err && err.name === 'DeployError') {
|
|
1039
|
+
say.err(err.message)
|
|
1040
|
+
if (err.hint) {
|
|
1041
|
+
console.log('')
|
|
1042
|
+
console.log(err.hint)
|
|
1043
|
+
}
|
|
1044
|
+
process.exit(1)
|
|
1045
|
+
}
|
|
1046
|
+
throw err
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
891
1050
|
// ─── site.yml ──────────────────────────────────────────────
|
|
892
1051
|
|
|
893
1052
|
async function readSiteYml(path) {
|
|
@@ -1008,7 +1167,7 @@ export async function resolveSiteDir(args, verb = 'deploy') {
|
|
|
1008
1167
|
|
|
1009
1168
|
async function fetchLatestRuntime(workerUrl) {
|
|
1010
1169
|
try {
|
|
1011
|
-
const res = await fetch(`${workerUrl}/
|
|
1170
|
+
const res = await fetch(`${workerUrl}/runtime/latest`)
|
|
1012
1171
|
if (!res.ok) return null
|
|
1013
1172
|
const body = await res.json()
|
|
1014
1173
|
return body.version || null
|
|
@@ -1168,7 +1327,7 @@ async function callPublish({ url, token, body }) {
|
|
|
1168
1327
|
|
|
1169
1328
|
/**
|
|
1170
1329
|
* Walk a built foundation's `dist/` directory and return `{ relPath: base64Bytes }`
|
|
1171
|
-
* — the shape `POST /
|
|
1330
|
+
* — the shape `POST /foundations` expects in its `files` field.
|
|
1172
1331
|
*/
|
|
1173
1332
|
async function collectFoundationDistFiles(distDir) {
|
|
1174
1333
|
if (!existsSync(distDir)) {
|
|
@@ -1219,13 +1378,37 @@ async function callFoundationUpload({ url, token, body }) {
|
|
|
1219
1378
|
* siteContent is mutated in place so the caller's publish payload picks up
|
|
1220
1379
|
* the rewritten nodes without passing anything back.
|
|
1221
1380
|
*/
|
|
1222
|
-
async function uploadAssetsAndRewriteContent({ siteDir, localeContents, siteYml, theme, backendUrl, cliToken, siteId }) {
|
|
1381
|
+
async function uploadAssetsAndRewriteContent({ siteDir, localeContents, dataFileObjects = {}, siteYml, theme, backendUrl, cliToken, siteId }) {
|
|
1223
1382
|
const distAssetsDir = join(siteDir, 'dist', 'assets')
|
|
1224
1383
|
const hasDistAssets = existsSync(distAssetsDir)
|
|
1225
1384
|
|
|
1226
1385
|
// 1. Enumerate local files + read size.
|
|
1227
1386
|
const localFiles = hasDistAssets ? await walkAssetDir(distAssetsDir) : []
|
|
1228
1387
|
|
|
1388
|
+
// 1a. Content-scan: walk site-content.json (and locale variants) for any
|
|
1389
|
+
// asset references (image/document src/href) and resolve absolute
|
|
1390
|
+
// paths to local files under `dist/` or `public/`. This catches static
|
|
1391
|
+
// assets the author placed in `public/covers/`, `public/images/`, etc.
|
|
1392
|
+
// that the dist/assets walk above misses (vite's image-pipeline only
|
|
1393
|
+
// produces files for refs that go through it). Each resolved file
|
|
1394
|
+
// joins the upload pipeline; the rewrite step at the end maps every
|
|
1395
|
+
// such reference to its CDN identifier so content stays portable
|
|
1396
|
+
// across site delete / template extraction.
|
|
1397
|
+
const contentRefMap = await scanContentForAssetRefs(localeContents, dataFileObjects, siteDir)
|
|
1398
|
+
const seenPaths = new Set(localFiles.map((f) => f.fullPath))
|
|
1399
|
+
for (const [, info] of contentRefMap) {
|
|
1400
|
+
if (seenPaths.has(info.resolvedPath)) continue
|
|
1401
|
+
const ext = (info.filename.split('.').pop() || '').toLowerCase()
|
|
1402
|
+
const st = await stat(info.resolvedPath)
|
|
1403
|
+
localFiles.push({
|
|
1404
|
+
filename: info.filename,
|
|
1405
|
+
fullPath: info.resolvedPath,
|
|
1406
|
+
size: st.size,
|
|
1407
|
+
mime: MIME_BY_EXT[ext] || 'application/octet-stream',
|
|
1408
|
+
})
|
|
1409
|
+
seenPaths.add(info.resolvedPath)
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1229
1412
|
// 1a. Favicon — sits at site root, not in dist/assets. Ship it through
|
|
1230
1413
|
// the same pipeline so it ends up at assets.uniweb.app with an
|
|
1231
1414
|
// identifier; config.favicon gets set further down.
|
|
@@ -1345,14 +1528,39 @@ async function uploadAssetsAndRewriteContent({ siteDir, localeContents, siteYml,
|
|
|
1345
1528
|
}
|
|
1346
1529
|
|
|
1347
1530
|
// 6. Rewrite each locale's content in place. Image/document nodes whose
|
|
1348
|
-
// src/href references
|
|
1349
|
-
//
|
|
1350
|
-
//
|
|
1351
|
-
//
|
|
1531
|
+
// src/href references an uploaded asset get an info.identifier pointing
|
|
1532
|
+
// to the CDN. Walking every locale means translated content (which
|
|
1533
|
+
// still references the same image files via the source ProseMirror
|
|
1534
|
+
// tree) gets the same rewrite.
|
|
1535
|
+
//
|
|
1536
|
+
// Two lookup paths:
|
|
1537
|
+
// - byOriginalRef: full src/href string → identifier (covers static
|
|
1538
|
+
// public/ assets like `/covers/foo.svg` and dist/-resolved refs)
|
|
1539
|
+
// - byFilename: legacy match for `assets/{filename}` shape — kept
|
|
1540
|
+
// for back-compat with content authored against the old vite-
|
|
1541
|
+
// produced `/assets/...` URLs.
|
|
1352
1542
|
const byFilenameAll = new Map([...reused, ...fresh])
|
|
1543
|
+
const byOriginalRef = new Map()
|
|
1544
|
+
for (const [ref, info] of contentRefMap) {
|
|
1545
|
+
const id = byFilenameAll.get(info.filename)
|
|
1546
|
+
if (id) byOriginalRef.set(ref, id)
|
|
1547
|
+
}
|
|
1353
1548
|
let rewritten = 0
|
|
1354
1549
|
for (const lang of Object.keys(localeContents)) {
|
|
1355
|
-
rewritten += rewriteAssetReferences(localeContents[lang], byFilenameAll)
|
|
1550
|
+
rewritten += rewriteAssetReferences(localeContents[lang], byFilenameAll, byOriginalRef)
|
|
1551
|
+
}
|
|
1552
|
+
// Data files: walk the JSON tree. Two patterns coexist in collection
|
|
1553
|
+
// payloads:
|
|
1554
|
+
// - Flat fields (e.g. `article.image: "/covers/foo.svg"`) → replace
|
|
1555
|
+
// the string with a resolveAssetCdnUrl(identifier). The runtime
|
|
1556
|
+
// reads these as plain URLs, so rewriting at deploy time is the
|
|
1557
|
+
// simplest path to portability.
|
|
1558
|
+
// - Nested ProseMirror sub-trees (e.g. `article.content`) → use the
|
|
1559
|
+
// existing image/document node rewrite (sets `attrs.info.identifier`).
|
|
1560
|
+
for (const k of Object.keys(dataFileObjects)) {
|
|
1561
|
+
if (dataFileObjects[k] === null) continue
|
|
1562
|
+
rewritten += rewriteFlatAssetUrls(dataFileObjects[k], byOriginalRef)
|
|
1563
|
+
rewritten += rewriteAssetReferences(dataFileObjects[k], byFilenameAll, byOriginalRef)
|
|
1356
1564
|
}
|
|
1357
1565
|
if (rewritten > 0) {
|
|
1358
1566
|
say.dim(`Rewrote ${rewritten} asset reference(s) across ${Object.keys(localeContents).length} locale(s).`)
|
|
@@ -1626,44 +1834,68 @@ async function runInPool(items, concurrency, worker) {
|
|
|
1626
1834
|
|
|
1627
1835
|
/**
|
|
1628
1836
|
* Walk siteContent (ProseMirror-ish JSON tree) and rewrite any node whose
|
|
1629
|
-
* `attrs.src` or `attrs.href` references
|
|
1630
|
-
*
|
|
1631
|
-
*
|
|
1837
|
+
* `attrs.src` or `attrs.href` references an uploaded/reused asset. Sets
|
|
1838
|
+
* `attrs.info.identifier` so semantic-parser resolves the real CDN URL
|
|
1839
|
+
* (and optimized variants) at render time.
|
|
1840
|
+
*
|
|
1841
|
+
* Two lookup paths, in order:
|
|
1842
|
+
* 1. `byOriginalRef` — full src/href string → identifier. Covers static
|
|
1843
|
+
* public/ assets (`/covers/foo.svg`, `/images/foo.png`) and any
|
|
1844
|
+
* content-scan-resolved file. Decouples assets from site lifecycle
|
|
1845
|
+
* (templates can extract content + identifier; assets stay on CDN).
|
|
1846
|
+
* 2. `byFilename` (legacy) — only fires when the path matches the old
|
|
1847
|
+
* `/assets/{filename}` shape. Kept so re-deploys of content authored
|
|
1848
|
+
* against pre-content-scan CLIs still work.
|
|
1632
1849
|
*
|
|
1633
1850
|
* Returns the number of rewrites performed — useful for reporting, and to
|
|
1634
1851
|
* detect "nothing matched" (likely a content-shape mismatch worth flagging).
|
|
1635
1852
|
*/
|
|
1636
|
-
function rewriteAssetReferences(node, byFilename) {
|
|
1853
|
+
function rewriteAssetReferences(node, byFilename, byOriginalRef = new Map()) {
|
|
1637
1854
|
let count = 0
|
|
1638
1855
|
const walk = (n) => {
|
|
1639
1856
|
if (!n || typeof n !== 'object') return
|
|
1640
1857
|
if (Array.isArray(n)) { for (const child of n) walk(child); return }
|
|
1641
1858
|
if (n.attrs && typeof n.attrs === 'object') {
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1859
|
+
// Prefer full-ref lookup (covers static + dist refs uniformly);
|
|
1860
|
+
// fall back to legacy `assets/{filename}` extraction.
|
|
1861
|
+
let identifier = null
|
|
1862
|
+
let srcMatched = false
|
|
1863
|
+
let hrefMatched = false
|
|
1864
|
+
if (typeof n.attrs.src === 'string' && byOriginalRef.has(n.attrs.src)) {
|
|
1865
|
+
identifier = byOriginalRef.get(n.attrs.src)
|
|
1866
|
+
srcMatched = true
|
|
1867
|
+
} else if (typeof n.attrs.href === 'string' && byOriginalRef.has(n.attrs.href)) {
|
|
1868
|
+
identifier = byOriginalRef.get(n.attrs.href)
|
|
1869
|
+
hrefMatched = true
|
|
1870
|
+
} else {
|
|
1871
|
+
const srcRef = pickAssetRef(n.attrs.src)
|
|
1872
|
+
const hrefRef = pickAssetRef(n.attrs.href)
|
|
1873
|
+
const ref = srcRef || hrefRef
|
|
1874
|
+
if (ref) {
|
|
1875
|
+
identifier = byFilename.get(ref) || null
|
|
1876
|
+
srcMatched = !!srcRef
|
|
1877
|
+
hrefMatched = !srcRef && !!hrefRef
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
if (identifier) {
|
|
1881
|
+
n.attrs.info = {
|
|
1882
|
+
...(n.attrs.info || {}),
|
|
1883
|
+
identifier,
|
|
1884
|
+
contentType: 'website',
|
|
1885
|
+
viewType: 'profile',
|
|
1886
|
+
}
|
|
1887
|
+
// Clear the local path so the runtime resolves via info.identifier
|
|
1888
|
+
// (→ assets.uniweb.app CDN) instead of requesting a non-existent
|
|
1889
|
+
// file from the site host.
|
|
1890
|
+
if (srcMatched) n.attrs.src = null
|
|
1891
|
+
if (hrefMatched) n.attrs.href = null
|
|
1892
|
+
// Match the Editor shape: plain `image` nodes skip identifier
|
|
1893
|
+
// resolution in older runtimes; `ImageBlock` routes through
|
|
1894
|
+
// parseImgBlock which reads info.identifier and fills url.
|
|
1895
|
+
if (n.type === 'image' && n.attrs.role !== 'icon') {
|
|
1896
|
+
n.type = 'ImageBlock'
|
|
1666
1897
|
}
|
|
1898
|
+
count++
|
|
1667
1899
|
}
|
|
1668
1900
|
}
|
|
1669
1901
|
for (const v of Object.values(n)) if (typeof v === 'object') walk(v)
|
|
@@ -1679,6 +1911,153 @@ function pickAssetRef(v) {
|
|
|
1679
1911
|
return m ? m[1] : null
|
|
1680
1912
|
}
|
|
1681
1913
|
|
|
1914
|
+
/**
|
|
1915
|
+
* Walk every locale's content for `attrs.src` and `attrs.href` strings, and
|
|
1916
|
+
* resolve absolute-path refs (e.g. `/covers/foo.svg`) to local files under
|
|
1917
|
+
* the site root.
|
|
1918
|
+
*
|
|
1919
|
+
* Resolution order per ref:
|
|
1920
|
+
* 1. `dist/{path}` — vite outputs, link-mode collection JSON, etc.
|
|
1921
|
+
* 2. `public/{path}` — static author-placed assets (covers, images).
|
|
1922
|
+
*
|
|
1923
|
+
* Returns Map<originalRef, { resolvedPath, filename }> where:
|
|
1924
|
+
* - `originalRef` — the exact src/href string from content (used as the
|
|
1925
|
+
* lookup key during rewrite).
|
|
1926
|
+
* - `resolvedPath` — absolute path on disk (used for upload).
|
|
1927
|
+
* - `filename` — basename, used as the assets-server upload filename.
|
|
1928
|
+
* Server keys by (siteId, filename); collisions across
|
|
1929
|
+
* paths with the same basename are flagged as warnings.
|
|
1930
|
+
*
|
|
1931
|
+
* Skips:
|
|
1932
|
+
* - Non-string values, refs that don't start with `/`, protocol-relative
|
|
1933
|
+
* refs (`//cdn.example.com/...`), and external URLs.
|
|
1934
|
+
* - Refs starting with `/api/` or `/_` (worker-internal paths, never
|
|
1935
|
+
* local files).
|
|
1936
|
+
* - Nodes already rewritten with `attrs.info.identifier` set (re-deploy).
|
|
1937
|
+
*/
|
|
1938
|
+
async function scanContentForAssetRefs(localeContents, dataFileObjects, siteDir) {
|
|
1939
|
+
const candidates = new Set()
|
|
1940
|
+
for (const lang of Object.keys(localeContents)) {
|
|
1941
|
+
walkContentForAssetRefs(localeContents[lang], candidates)
|
|
1942
|
+
}
|
|
1943
|
+
// Also walk parsed collection JSON files. These contain BOTH ProseMirror-
|
|
1944
|
+
// shaped sub-trees (article.content) AND flat string fields (article.image,
|
|
1945
|
+
// article.cover, etc.). The walker captures both: any string-valued src/
|
|
1946
|
+
// href/image/cover/thumbnail/icon/poster field, plus any string anywhere
|
|
1947
|
+
// that looks like an absolute path with a known media extension.
|
|
1948
|
+
for (const k of Object.keys(dataFileObjects || {})) {
|
|
1949
|
+
if (dataFileObjects[k] !== null) {
|
|
1950
|
+
walkContentForAssetRefs(dataFileObjects[k], candidates)
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
const results = new Map()
|
|
1955
|
+
const filenameToRef = new Map() // detect collisions (same basename, different path)
|
|
1956
|
+
for (const ref of candidates) {
|
|
1957
|
+
if (!isResolvableContentRef(ref)) continue
|
|
1958
|
+
const cleanPath = ref.split('?')[0].split('#')[0].slice(1) // drop leading '/'
|
|
1959
|
+
const distCandidate = join(siteDir, 'dist', cleanPath)
|
|
1960
|
+
const publicCandidate = join(siteDir, 'public', cleanPath)
|
|
1961
|
+
let resolvedPath = null
|
|
1962
|
+
if (existsSync(distCandidate)) {
|
|
1963
|
+
try { if ((await stat(distCandidate)).isFile()) resolvedPath = distCandidate } catch {}
|
|
1964
|
+
}
|
|
1965
|
+
if (!resolvedPath && existsSync(publicCandidate)) {
|
|
1966
|
+
try { if ((await stat(publicCandidate)).isFile()) resolvedPath = publicCandidate } catch {}
|
|
1967
|
+
}
|
|
1968
|
+
if (!resolvedPath) continue
|
|
1969
|
+
const filename = resolvedPath.split(sep).pop()
|
|
1970
|
+
const prior = filenameToRef.get(filename)
|
|
1971
|
+
if (prior && prior !== resolvedPath) {
|
|
1972
|
+
// Two different files want the same upload filename — server keys by
|
|
1973
|
+
// filename so the second would clobber the first. Skip + warn rather
|
|
1974
|
+
// than silently overwrite. Caller can rename the file or move one
|
|
1975
|
+
// into a vite-processed path to disambiguate via content hashing.
|
|
1976
|
+
say.warn(
|
|
1977
|
+
`Asset filename collision: "${filename}" exists at multiple paths ` +
|
|
1978
|
+
`(${prior}, ${resolvedPath}). Skipping the second; rename to disambiguate.`
|
|
1979
|
+
)
|
|
1980
|
+
continue
|
|
1981
|
+
}
|
|
1982
|
+
filenameToRef.set(filename, resolvedPath)
|
|
1983
|
+
results.set(ref, { resolvedPath, filename })
|
|
1984
|
+
}
|
|
1985
|
+
return results
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// Field names commonly used for media in collection JSON. The walker
|
|
1989
|
+
// collects any absolute-path string under these keys as a potential asset
|
|
1990
|
+
// reference. ProseMirror image/link nodes are caught separately via attrs.
|
|
1991
|
+
const FLAT_ASSET_FIELDS = new Set([
|
|
1992
|
+
'src', 'href', 'image', 'cover', 'thumbnail', 'icon', 'poster', 'logo',
|
|
1993
|
+
'avatar', 'photo', 'banner', 'background',
|
|
1994
|
+
])
|
|
1995
|
+
|
|
1996
|
+
function walkContentForAssetRefs(node, refs) {
|
|
1997
|
+
if (!node || typeof node !== 'object') return
|
|
1998
|
+
if (Array.isArray(node)) { for (const child of node) walkContentForAssetRefs(child, refs); return }
|
|
1999
|
+
if (node.attrs && typeof node.attrs === 'object') {
|
|
2000
|
+
// Skip nodes already rewritten in a prior deploy — those have an
|
|
2001
|
+
// identifier and the runtime resolves them through the CDN already.
|
|
2002
|
+
if (!node.attrs.info?.identifier) {
|
|
2003
|
+
if (typeof node.attrs.src === 'string') refs.add(node.attrs.src)
|
|
2004
|
+
if (typeof node.attrs.href === 'string') refs.add(node.attrs.href)
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
// Flat fields: collection-shaped objects (e.g. an article record) often
|
|
2008
|
+
// carry media URLs as plain string fields rather than ProseMirror nodes.
|
|
2009
|
+
// Capture absolute-path values under known keys.
|
|
2010
|
+
for (const [k, v] of Object.entries(node)) {
|
|
2011
|
+
if (typeof v === 'string' && FLAT_ASSET_FIELDS.has(k) && isResolvableContentRef(v)) {
|
|
2012
|
+
refs.add(v)
|
|
2013
|
+
} else if (typeof v === 'object') {
|
|
2014
|
+
walkContentForAssetRefs(v, refs)
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
/**
|
|
2020
|
+
* Walk an arbitrary JSON tree and replace any string equal to a key in
|
|
2021
|
+
* `byOriginalRef` (and not already a CDN URL) with the asset's CDN URL.
|
|
2022
|
+
* Used for collection JSON files where image refs are flat string fields
|
|
2023
|
+
* (e.g. `article.image: "/covers/foo.svg"`) rather than ProseMirror nodes.
|
|
2024
|
+
*
|
|
2025
|
+
* Returns the number of replacements performed.
|
|
2026
|
+
*/
|
|
2027
|
+
function rewriteFlatAssetUrls(node, byOriginalRef) {
|
|
2028
|
+
let count = 0
|
|
2029
|
+
const walk = (n, parent, key) => {
|
|
2030
|
+
if (n == null) return
|
|
2031
|
+
if (typeof n === 'string') {
|
|
2032
|
+
const id = byOriginalRef.get(n)
|
|
2033
|
+
if (id && parent != null && key != null) {
|
|
2034
|
+
parent[key] = resolveAssetCdnUrl(id)
|
|
2035
|
+
count++
|
|
2036
|
+
}
|
|
2037
|
+
return
|
|
2038
|
+
}
|
|
2039
|
+
if (typeof n !== 'object') return
|
|
2040
|
+
if (Array.isArray(n)) {
|
|
2041
|
+
for (let i = 0; i < n.length; i++) walk(n[i], n, i)
|
|
2042
|
+
return
|
|
2043
|
+
}
|
|
2044
|
+
for (const [k, v] of Object.entries(n)) walk(v, n, k)
|
|
2045
|
+
}
|
|
2046
|
+
walk(node, null, null)
|
|
2047
|
+
return count
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
function isResolvableContentRef(ref) {
|
|
2051
|
+
if (typeof ref !== 'string' || !ref) return false
|
|
2052
|
+
// Absolute-path only — relative paths (`./foo`, `foo`) are content-author
|
|
2053
|
+
// shorthand handled elsewhere; URLs (`http://`, `//cdn`) never resolve to
|
|
2054
|
+
// local files; worker-internal paths (`/api/`, `/_`) aren't asset content.
|
|
2055
|
+
if (!ref.startsWith('/')) return false
|
|
2056
|
+
if (ref.startsWith('//')) return false
|
|
2057
|
+
if (ref.startsWith('/api/') || ref.startsWith('/_')) return false
|
|
2058
|
+
return true
|
|
2059
|
+
}
|
|
2060
|
+
|
|
1682
2061
|
// ─── Loopback listener (review path) ───────────────────────
|
|
1683
2062
|
|
|
1684
2063
|
/**
|
package/src/commands/export.js
CHANGED
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
* Usage:
|
|
18
18
|
* uniweb export Produce dist/ for static hosting
|
|
19
19
|
* uniweb export --no-prerender Skip per-page prerendered HTML
|
|
20
|
+
* uniweb export --host <name> Select a host adapter (e.g.
|
|
21
|
+
* netlify, s3-cloudfront, generic-static).
|
|
22
|
+
* Overrides site.yml's deploy.host.
|
|
20
23
|
*/
|
|
21
24
|
|
|
22
25
|
import { execSync } from 'node:child_process'
|
|
@@ -42,14 +45,19 @@ const say = {
|
|
|
42
45
|
export async function exportSite(args = []) {
|
|
43
46
|
const siteDir = await resolveSiteDir(args, 'export')
|
|
44
47
|
|
|
45
|
-
// Pass through --no-prerender
|
|
46
|
-
// export`
|
|
47
|
-
//
|
|
48
|
-
//
|
|
48
|
+
// Pass through --no-prerender and --host. Everything else is ignored.
|
|
49
|
+
// `uniweb export` stays low-flag: the user picks the destination host
|
|
50
|
+
// themselves outside the CLI, so there's nothing to configure beyond
|
|
51
|
+
// what `uniweb build --bundle` already exposes.
|
|
49
52
|
const noPrerender = args.includes('--no-prerender')
|
|
50
53
|
const buildArgs = ['build', '--bundle']
|
|
51
54
|
if (noPrerender) buildArgs.push('--no-prerender')
|
|
52
55
|
|
|
56
|
+
const hostIndex = args.indexOf('--host')
|
|
57
|
+
if (hostIndex !== -1 && args[hostIndex + 1]) {
|
|
58
|
+
buildArgs.push('--host', args[hostIndex + 1])
|
|
59
|
+
}
|
|
60
|
+
|
|
53
61
|
say.info('Exporting site (vite build → dist/)…')
|
|
54
62
|
console.log('')
|
|
55
63
|
|
package/src/framework-index.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-05-
|
|
3
|
+
"generatedAt": "2026-05-04T22:31:28.093Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/build": {
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.14.1",
|
|
7
7
|
"path": "framework/build",
|
|
8
8
|
"deps": [
|
|
9
9
|
"@uniweb/content-reader",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"deps": []
|
|
93
93
|
},
|
|
94
94
|
"@uniweb/unipress": {
|
|
95
|
-
"version": "0.4.
|
|
95
|
+
"version": "0.4.5",
|
|
96
96
|
"path": "framework/unipress",
|
|
97
97
|
"deps": [
|
|
98
98
|
"@uniweb/build",
|
package/src/utils/config.js
CHANGED
|
@@ -14,9 +14,15 @@ import { filterCmd } from './pm.js'
|
|
|
14
14
|
|
|
15
15
|
// ── Platform URLs ──────────────────────────────────────────────
|
|
16
16
|
|
|
17
|
-
// Production defaults — regular users get these out of the box
|
|
17
|
+
// Production defaults — regular users get these out of the box.
|
|
18
|
+
// REGISTRY hosts platform operations (publish, foundations, runtime, admin):
|
|
19
|
+
// moved to hosting.uniweb.app in the CDN migration (Phase 4c, 2026-05-04).
|
|
20
|
+
// BACKEND hosts the PHP user-facing surface (login, account, orgs, billing,
|
|
21
|
+
// publish-authorize): owned by the v4 single-domain plan
|
|
22
|
+
// (kb/platform/plans/uniweb-domain-plan-v4.md), which will move it to
|
|
23
|
+
// uniweb.app/api/* when v4 ships. Until then, it stays at hub.uniweb.app.
|
|
18
24
|
const PRODUCTION_BACKEND_URL = 'https://hub.uniweb.app'
|
|
19
|
-
const PRODUCTION_REGISTRY_URL = 'https://
|
|
25
|
+
const PRODUCTION_REGISTRY_URL = 'https://hosting.uniweb.app'
|
|
20
26
|
|
|
21
27
|
/**
|
|
22
28
|
* Read ~/.uniweb/config.json for persistent URL overrides.
|