uniweb 0.12.3 → 0.12.4
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 +32 -22
- package/package.json +2 -2
- package/src/commands/add.js +88 -12
- package/src/commands/build.js +190 -23
- package/src/commands/deploy.js +318 -16
- package/src/commands/doctor.js +172 -130
- package/src/commands/handoff.js +1 -1
- package/src/commands/invite.js +2 -2
- package/src/commands/publish.js +297 -54
- package/src/commands/rename.js +310 -0
- package/src/framework-index.json +4 -4
- package/src/index.js +14 -5
- package/src/utils/receipt.js +91 -0
- package/src/utils/registry.js +33 -0
- package/templates/workspace/package.json.hbs +2 -4
package/README.md
CHANGED
|
@@ -14,8 +14,8 @@ The interactive prompt asks for a project name and template. Pick one, then:
|
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
cd my-project
|
|
17
|
-
npm install
|
|
18
|
-
npm run dev
|
|
17
|
+
pnpm install # or npm install
|
|
18
|
+
pnpm dev # or npm run dev
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
Edit files in `site/pages/` and `src/sections/` to see changes instantly.
|
|
@@ -46,17 +46,17 @@ Or skip the interactive prompt:
|
|
|
46
46
|
pnpm create uniweb my-site --template docs
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
###
|
|
49
|
+
### Local Scripts
|
|
50
50
|
|
|
51
51
|
Run these from the **project root**:
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
pnpm dev # Start development server
|
|
55
|
+
pnpm build # Build foundation + site for production
|
|
56
|
+
pnpm preview # Preview the production build
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
The `build` command outputs to `site/dist/`. With pre-rendering enabled (the default for official templates), you get static HTML files ready to deploy anywhere.
|
|
59
|
+
The `build` command outputs to `site/dist/`. With pre-rendering enabled (the default for official templates), you get static HTML files ready to deploy anywhere. For the actual deploy step (and the `uniweb publish` / `uniweb deploy` commands), see [Deployment](#deployment) below.
|
|
60
60
|
|
|
61
61
|
## What You Get
|
|
62
62
|
|
|
@@ -243,14 +243,13 @@ The parser extracts semantic elements from markdown—`title` from the first hea
|
|
|
243
243
|
|
|
244
244
|
## Foundations Are Portable
|
|
245
245
|
|
|
246
|
-
The `src/` folder (your project's foundation) ships with your project as a convenience, but a foundation is a
|
|
246
|
+
The `src/` folder (your project's foundation) ships with your project as a convenience, but a foundation is a dynamically linked module (DML) with no dependency on any specific site. Sites reference foundations by configuration, not by folder proximity.
|
|
247
247
|
|
|
248
|
-
**
|
|
248
|
+
**Two ways to use a foundation:**
|
|
249
249
|
|
|
250
250
|
| Mode | How it works | Best for |
|
|
251
251
|
| ---------------- | ---------------------------------- | -------------------------------------------------- |
|
|
252
252
|
| **Local folder** | Foundation lives in your workspace | Developing site and components together |
|
|
253
|
-
| **npm package** | `npm add @acme/foundation` | Distributing via standard package tooling |
|
|
254
253
|
| **Runtime link** | Foundation loads from a URL | Independent release cycles, platform-managed sites |
|
|
255
254
|
|
|
256
255
|
You can delete the `src/` folder entirely and point your site at a published foundation. Or develop a foundation locally, then publish it for other sites to consume. The site doesn't care where its components come from.
|
|
@@ -259,7 +258,7 @@ You can delete the `src/` folder entirely and point your site at a published fou
|
|
|
259
258
|
|
|
260
259
|
_Site-first_ — You're building a website. The foundation is your component library, co-developed with the site. This is the common case.
|
|
261
260
|
|
|
262
|
-
_Foundation-first_ — You're building a component system. The site is a test harness with sample content. The real sites live elsewhere—other repositories, other teams, or managed on [
|
|
261
|
+
_Foundation-first_ — You're building a component system. The site is a test harness with sample content. The real sites live elsewhere—other repositories, other teams, or managed on [uniweb.app](https://uniweb.app). Use `uniweb add site` to add multiple test sites exercising a shared foundation.
|
|
263
262
|
|
|
264
263
|
## Growing Your Project
|
|
265
264
|
|
|
@@ -301,14 +300,25 @@ The structure you start with scales without rewrites:
|
|
|
301
300
|
|
|
302
301
|
1. **Single project** — One site, one foundation. Develop and deploy together. Most projects stay here.
|
|
303
302
|
|
|
304
|
-
2. **Published foundation** — Release your foundation as
|
|
303
|
+
2. **Published foundation** — Release your foundation as a dynamically linked module to [uniweb.app](https://uniweb.app). Other sites can use it without copying code.
|
|
305
304
|
|
|
306
305
|
3. **Multiple sites** — Several sites share one foundation. Update components once, every site benefits.
|
|
307
306
|
|
|
308
|
-
4. **Platform-managed sites** — Sites built on [
|
|
307
|
+
4. **Platform-managed sites** — Sites built on [uniweb.app](https://uniweb.app) with visual editing tools can use your foundation. You develop components locally; content teams work in the browser.
|
|
309
308
|
|
|
310
309
|
Start with local files deployed anywhere. The same foundation works across all these scenarios.
|
|
311
310
|
|
|
311
|
+
## Deployment
|
|
312
|
+
|
|
313
|
+
A Uniweb project produces two artifacts — a **site** (content) and a **foundation** (code) — and they don't have to ship together. That opens up deployment options other frameworks can't express:
|
|
314
|
+
|
|
315
|
+
- **Bundled mode** — site and foundation built into one self-contained `dist/`, deployed to any static host.
|
|
316
|
+
- **Linked mode** — the foundation lives in any host and the site in any other host; different sites can dynamically link with the same foundation. Update the foundation, every site picks it up — no site rebuilds.
|
|
317
|
+
|
|
318
|
+
Two verbs handle it: `uniweb publish` sends a foundation to a registry, `uniweb deploy` sends a site to a host. Most projects start bundled (one command, one destination) and grow into linked mode by changing one line in `site.yml`. Mix providers freely — foundation on GitHub Pages, site on Vercel; or use Uniweb's registry + hosting for propagation, gated rollouts, and edge SSR.
|
|
319
|
+
|
|
320
|
+
→ **[Deploying](https://github.com/uniweb/docs/blob/main/development/deploying.md)** — the full menu: bundled vs linked, the two-verb model, one-foundation-many-sites, optimized hosting on the Uniweb platform, and recipes for other hosting services.
|
|
321
|
+
|
|
312
322
|
---
|
|
313
323
|
|
|
314
324
|
## Documentation
|
|
@@ -331,7 +341,7 @@ Full documentation is available at **[github.com/uniweb/docs](https://github.com
|
|
|
331
341
|
| Site Configuration | [site.yml reference](https://github.com/uniweb/docs/blob/main/reference/site-configuration.md) |
|
|
332
342
|
| CLI Commands | [create, add, build, docs, doctor, i18n](https://github.com/uniweb/docs/blob/main/reference/cli-commands.md) |
|
|
333
343
|
| Templates | [Built-in, official, and external templates](https://github.com/uniweb/docs/blob/main/getting-started/templates.md) |
|
|
334
|
-
| Deployment | [
|
|
344
|
+
| Deployment | [Two artifacts, two verbs — bundled, linked, and per-host recipes](https://github.com/uniweb/docs/blob/main/development/deploying.md) |
|
|
335
345
|
|
|
336
346
|
---
|
|
337
347
|
|
|
@@ -387,7 +397,7 @@ A default project has two packages, listed in both `pnpm-workspace.yaml` and `pa
|
|
|
387
397
|
```yaml
|
|
388
398
|
# pnpm-workspace.yaml
|
|
389
399
|
packages:
|
|
390
|
-
-
|
|
400
|
+
- src
|
|
391
401
|
- site
|
|
392
402
|
```
|
|
393
403
|
|
|
@@ -395,20 +405,20 @@ In `package.json` (for npm compatibility):
|
|
|
395
405
|
|
|
396
406
|
```json
|
|
397
407
|
{
|
|
398
|
-
"workspaces": ["
|
|
408
|
+
"workspaces": ["src", "site"]
|
|
399
409
|
}
|
|
400
410
|
```
|
|
401
411
|
|
|
402
|
-
When you `add` more packages, the CLI adds the appropriate
|
|
412
|
+
When you `add` more packages, the CLI adds the appropriate workspaces automatically:
|
|
403
413
|
|
|
404
414
|
```yaml
|
|
405
|
-
# After: uniweb add
|
|
406
|
-
# After: uniweb add
|
|
415
|
+
# After: uniweb add site marketing/blog
|
|
416
|
+
# After: uniweb add foundation marketing/blogger
|
|
407
417
|
packages:
|
|
408
|
-
-
|
|
418
|
+
- src
|
|
409
419
|
- site
|
|
410
|
-
-
|
|
411
|
-
-
|
|
420
|
+
- marketing/blog
|
|
421
|
+
- marketing/blogger
|
|
412
422
|
```
|
|
413
423
|
|
|
414
424
|
## FAQ
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.4",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@uniweb/runtime": "0.8.10"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@uniweb/build": "0.13.
|
|
49
|
+
"@uniweb/build": "0.13.3",
|
|
50
50
|
"@uniweb/content-reader": "1.1.9",
|
|
51
51
|
"@uniweb/semantic-parser": "1.1.16"
|
|
52
52
|
},
|
package/src/commands/add.js
CHANGED
|
@@ -196,7 +196,9 @@ async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
196
196
|
// both. Format validation runs on the derived package name below, not
|
|
197
197
|
// on the raw input — slashes in the input are intentional path syntax.
|
|
198
198
|
const FOUNDATION_KIND = { defaultDir: 'src', defaultPkg: 'src', projectSub: 'src' }
|
|
199
|
-
const
|
|
199
|
+
const placement = resolvePlacement(rootDir, name, opts, FOUNDATION_KIND)
|
|
200
|
+
const { relativePath } = placement
|
|
201
|
+
let { packageName } = placement
|
|
200
202
|
const fullPath = join(rootDir, relativePath)
|
|
201
203
|
|
|
202
204
|
// Validate the derived package name (format + reserved-name check). The
|
|
@@ -222,11 +224,33 @@ async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
222
224
|
|
|
223
225
|
// Collision check 2: a package with the same name already exists somewhere
|
|
224
226
|
// in the workspace.
|
|
227
|
+
//
|
|
228
|
+
// Cross-role collisions auto-resolve. If a *site* already owns this name,
|
|
229
|
+
// suffix the foundation with `-src` (matching the `add project` and
|
|
230
|
+
// `add extension` precedents). The site keeps its name; the foundation
|
|
231
|
+
// gets a self-documenting suffix that says "this is the source code for
|
|
232
|
+
// the site that owns this name." Same-role collisions stay an error —
|
|
233
|
+
// two foundations with the same name is a real "be more specific"
|
|
234
|
+
// situation, not a disambiguation case.
|
|
225
235
|
if (existingNames.has(packageName)) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
236
|
+
const sites = await discoverSites(rootDir)
|
|
237
|
+
const isSiteCollision = sites.some(s => s.name === packageName)
|
|
238
|
+
if (isSiteCollision) {
|
|
239
|
+
const suffixed = `${packageName}-src`
|
|
240
|
+
if (existingNames.has(suffixed)) {
|
|
241
|
+
error(`Cannot create foundation: both ${colors.bright}${packageName}${colors.reset} and ${colors.bright}${suffixed}${colors.reset} are taken in this workspace.`)
|
|
242
|
+
log(`Pick a different name:`)
|
|
243
|
+
log(` ${colors.cyan}${getCliPrefix()} add foundation <other-name>${colors.reset}`)
|
|
244
|
+
process.exit(1)
|
|
245
|
+
}
|
|
246
|
+
info(`Package "${packageName}" is taken by a site; using "${suffixed}" for this foundation.`)
|
|
247
|
+
packageName = suffixed
|
|
248
|
+
} else {
|
|
249
|
+
error(`Cannot create foundation: a foundation named ${colors.bright}${packageName}${colors.reset} already exists in this workspace.`)
|
|
250
|
+
log(`Pick a different name:`)
|
|
251
|
+
log(` ${colors.cyan}${getCliPrefix()} add foundation <other-name>${colors.reset}`)
|
|
252
|
+
process.exit(1)
|
|
253
|
+
}
|
|
230
254
|
}
|
|
231
255
|
|
|
232
256
|
// Scaffold
|
|
@@ -266,7 +290,9 @@ async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
266
290
|
|
|
267
291
|
// Resolve placement first (path + package name); see notes in addFoundation.
|
|
268
292
|
const SITE_KIND = { defaultDir: 'site', defaultPkg: 'site', projectSub: 'site' }
|
|
269
|
-
const
|
|
293
|
+
const placement = resolvePlacement(rootDir, name, opts, SITE_KIND)
|
|
294
|
+
const { relativePath } = placement
|
|
295
|
+
let siteName = placement.packageName
|
|
270
296
|
const fullPath = join(rootDir, relativePath)
|
|
271
297
|
|
|
272
298
|
// Validate the package name (skip for the auto-derived 'site' default).
|
|
@@ -288,12 +314,28 @@ async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
288
314
|
process.exit(1)
|
|
289
315
|
}
|
|
290
316
|
|
|
291
|
-
// Collision check 2:
|
|
317
|
+
// Collision check 2: cross-role collisions auto-resolve with `-site`
|
|
318
|
+
// suffix; same-role collisions error. See the symmetric logic in
|
|
319
|
+
// addFoundation for the rationale.
|
|
292
320
|
if (existingNames.has(siteName)) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
321
|
+
const foundations = await discoverFoundations(rootDir)
|
|
322
|
+
const isFoundationCollision = foundations.some(f => f.name === siteName)
|
|
323
|
+
if (isFoundationCollision) {
|
|
324
|
+
const suffixed = `${siteName}-site`
|
|
325
|
+
if (existingNames.has(suffixed)) {
|
|
326
|
+
error(`Cannot create site: both ${colors.bright}${siteName}${colors.reset} and ${colors.bright}${suffixed}${colors.reset} are taken in this workspace.`)
|
|
327
|
+
log(`Pick a different name:`)
|
|
328
|
+
log(` ${colors.cyan}${getCliPrefix()} add site <other-name>${colors.reset}`)
|
|
329
|
+
process.exit(1)
|
|
330
|
+
}
|
|
331
|
+
info(`Package "${siteName}" is taken by a foundation; using "${suffixed}" for this site.`)
|
|
332
|
+
siteName = suffixed
|
|
333
|
+
} else {
|
|
334
|
+
error(`Cannot create site: a site named ${colors.bright}${siteName}${colors.reset} already exists in this workspace.`)
|
|
335
|
+
log(`Pick a different name:`)
|
|
336
|
+
log(` ${colors.cyan}${getCliPrefix()} add site <other-name>${colors.reset}`)
|
|
337
|
+
process.exit(1)
|
|
338
|
+
}
|
|
297
339
|
}
|
|
298
340
|
|
|
299
341
|
// Resolve foundation
|
|
@@ -430,14 +472,45 @@ async function addExtension(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
430
472
|
// Update workspace globs
|
|
431
473
|
await addWorkspaceGlob(rootDir, 'extensions/*')
|
|
432
474
|
|
|
433
|
-
// Wire extension to site
|
|
475
|
+
// Wire extension to site:
|
|
476
|
+
// - --site <name>: explicit, wire it.
|
|
477
|
+
// - exactly one site: silent auto-wire (intent is unambiguous).
|
|
478
|
+
// - multiple sites, interactive: single-select prompt (extensions are
|
|
479
|
+
// typically per-site specialization — pick which site).
|
|
480
|
+
// - multiple sites, non-interactive: don't wire silently. Print a
|
|
481
|
+
// warning so the user/agent knows wiring is pending, and exit 0
|
|
482
|
+
// (the extension itself is fine).
|
|
483
|
+
// - no sites: print a note and exit 0.
|
|
434
484
|
let wiredSite = null
|
|
485
|
+
let unwiredReason = null
|
|
435
486
|
if (opts.site) {
|
|
436
487
|
wiredSite = await wireExtensionToSite(rootDir, opts.site, name, target)
|
|
437
488
|
} else {
|
|
438
489
|
const sites = await discoverSites(rootDir)
|
|
439
490
|
if (sites.length === 1) {
|
|
440
491
|
wiredSite = await wireExtensionToSite(rootDir, sites[0].name, name, target)
|
|
492
|
+
} else if (sites.length > 1) {
|
|
493
|
+
if (isNonInteractive(process.argv)) {
|
|
494
|
+
unwiredReason = `Multiple sites in workspace; extension not wired. Re-run with --site <name>, or edit <site>/site.yml::extensions: manually.`
|
|
495
|
+
} else {
|
|
496
|
+
const sortedSites = [...sites].sort((a, b) => a.name.localeCompare(b.name))
|
|
497
|
+
const response = await prompts({
|
|
498
|
+
type: 'select',
|
|
499
|
+
name: 'site',
|
|
500
|
+
message: 'Which site is this extension for?',
|
|
501
|
+
choices: sortedSites.map(s => ({ title: s.name, description: s.path, value: s.name })),
|
|
502
|
+
}, {
|
|
503
|
+
onCancel: () => {
|
|
504
|
+
log('\nCancelled.')
|
|
505
|
+
process.exit(0)
|
|
506
|
+
},
|
|
507
|
+
})
|
|
508
|
+
if (response.site) {
|
|
509
|
+
wiredSite = await wireExtensionToSite(rootDir, response.site, name, target)
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
} else {
|
|
513
|
+
unwiredReason = `No site in this workspace yet. Wire this extension into a site's site.yml::extensions: once you create one.`
|
|
441
514
|
}
|
|
442
515
|
}
|
|
443
516
|
|
|
@@ -450,6 +523,9 @@ async function addExtension(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
450
523
|
msg += ` → wired to site '${wiredSite}'`
|
|
451
524
|
}
|
|
452
525
|
success(msg)
|
|
526
|
+
if (unwiredReason) {
|
|
527
|
+
log(` ${colors.yellow}⚠ ${unwiredReason}${colors.reset}`)
|
|
528
|
+
}
|
|
453
529
|
log('')
|
|
454
530
|
log(`Next: ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
455
531
|
}
|
package/src/commands/build.js
CHANGED
|
@@ -1,13 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Build Command
|
|
3
3
|
*
|
|
4
|
-
* Builds foundations with schema generation
|
|
4
|
+
* Builds foundations with schema generation, or sites in either link or
|
|
5
|
+
* bundle mode.
|
|
6
|
+
*
|
|
7
|
+
* Site build modes:
|
|
8
|
+
* --bundle (default for sites)
|
|
9
|
+
* Full vite + post-vite pipeline. Produces a static-host JS bundle
|
|
10
|
+
* (`dist/index.html`, `dist/entry.js`, `_importmap/*`, `_pages/*` for
|
|
11
|
+
* split mode, sitemap/robots/search-index, prerendered HTML when
|
|
12
|
+
* configured). Targets non-Uniweb hosts (Netlify, Vercel, GitHub
|
|
13
|
+
* Pages) and the future Uniweb bundled-mode hosting.
|
|
14
|
+
*
|
|
15
|
+
* --link
|
|
16
|
+
* Data-only pipeline. No vite. Emits ONLY what the Uniweb-edge
|
|
17
|
+
* deploy needs: `dist/site-content.json` (with full sections),
|
|
18
|
+
* `dist/<lang>/site-content.json` per non-default locale,
|
|
19
|
+
* `dist/data/*.json` (collections), and `dist/assets/<media>` (images,
|
|
20
|
+
* fonts, video posters). Worker generates HTML at request time using
|
|
21
|
+
* its own runtime + the foundation served from the registry — the
|
|
22
|
+
* site's JS bundle is dead weight on this path.
|
|
5
23
|
*
|
|
6
24
|
* Usage:
|
|
7
|
-
* uniweb build # Build current directory
|
|
25
|
+
* uniweb build # Build current directory (sites default to --bundle)
|
|
8
26
|
* uniweb build --target foundation # Explicitly build as foundation
|
|
9
27
|
* uniweb build --target site # Explicitly build as site
|
|
10
|
-
* uniweb build --
|
|
28
|
+
* uniweb build --link # Site: link-mode (data only, no vite)
|
|
29
|
+
* uniweb build --bundle # Site: bundle-mode (full pipeline, today's behavior)
|
|
30
|
+
* uniweb build --prerender # Bundle-mode site + SSG (static HTML)
|
|
11
31
|
*/
|
|
12
32
|
|
|
13
33
|
import { existsSync, readFileSync } from 'node:fs'
|
|
@@ -375,6 +395,136 @@ function resolveFoundationDir(projectDir, siteConfig) {
|
|
|
375
395
|
return singleFoundationDir
|
|
376
396
|
}
|
|
377
397
|
|
|
398
|
+
/**
|
|
399
|
+
* Build a site in link mode — data only, no vite.
|
|
400
|
+
*
|
|
401
|
+
* Emits exactly what `uniweb deploy` ships to Uniweb-edge:
|
|
402
|
+
* dist/site-content.json (full sections inlined)
|
|
403
|
+
* dist/<lang>/site-content.json per non-default locale
|
|
404
|
+
* dist/data/<collection>.json (+ per-record files for `deferred:`)
|
|
405
|
+
* dist/assets/<media> (processed images, video posters, PDF thumbnails)
|
|
406
|
+
*
|
|
407
|
+
* Does NOT emit HTML, JS, CSS, source maps, _importmap chunks, or
|
|
408
|
+
* static-host extras (sitemap, robots, search-index, _pages/*) — none
|
|
409
|
+
* of these are consumed on the link-mode deploy path. The worker
|
|
410
|
+
* generates HTML at request time and re-derives split-content per-page
|
|
411
|
+
* files from the full payload it receives.
|
|
412
|
+
*
|
|
413
|
+
* The `buildLocalizedContent` step is the same call bundle mode makes
|
|
414
|
+
* post-vite, so multi-locale sites get identical per-locale outputs in
|
|
415
|
+
* either mode. Collection translation (`buildLocalizedCollections`)
|
|
416
|
+
* also runs here so deploy ships translated collection JSONs.
|
|
417
|
+
*
|
|
418
|
+
* Bug surfaced + fixed by routing deploy through this path: the bundle
|
|
419
|
+
* pipeline's prerender step rewrites `dist/site-content.json` into a
|
|
420
|
+
* lightweight manifest (sections stripped) when split-content is active,
|
|
421
|
+
* and deploy was reading that stripped version, causing the worker to
|
|
422
|
+
* mis-detect split and serve blank pages. Link mode skips prerender
|
|
423
|
+
* entirely; `dist/site-content.json` keeps full sections; the worker
|
|
424
|
+
* splits correctly. See `kb/framework/build/workspace-ergonomics.md`.
|
|
425
|
+
*/
|
|
426
|
+
async function buildSiteLink(projectDir, options = {}) {
|
|
427
|
+
const { siteConfig = null } = options
|
|
428
|
+
|
|
429
|
+
info('Building site (link mode)...')
|
|
430
|
+
|
|
431
|
+
const { buildSiteData } = await import('@uniweb/build/site')
|
|
432
|
+
const distDir = join(projectDir, 'dist')
|
|
433
|
+
|
|
434
|
+
// Resolve the local foundation path so collectSiteContent can pick up
|
|
435
|
+
// theme variable defaults from `foundation.js::theme.vars`. When the
|
|
436
|
+
// foundation is purely a registry ref (no local sibling), this stays
|
|
437
|
+
// null and theme defaults come from theme.yml only.
|
|
438
|
+
const foundationDir = await resolveFoundationDirForSite(projectDir, siteConfig).catch(() => null)
|
|
439
|
+
|
|
440
|
+
await buildSiteData({
|
|
441
|
+
siteRoot: projectDir,
|
|
442
|
+
distDir,
|
|
443
|
+
foundationPath: foundationDir,
|
|
444
|
+
assets: siteConfig?.build?.assets || {},
|
|
445
|
+
})
|
|
446
|
+
success(`Wrote ${join('dist', 'site-content.json')}`)
|
|
447
|
+
|
|
448
|
+
// Per-locale variants — same call bundle mode makes post-vite. Both
|
|
449
|
+
// modes produce identical `dist/<lang>/site-content.json` outputs so
|
|
450
|
+
// the deploy CLI walks the same path shape regardless of mode.
|
|
451
|
+
const i18nConfig = await loadI18nConfig(projectDir, siteConfig)
|
|
452
|
+
if (i18nConfig && i18nConfig.locales.length > 0) {
|
|
453
|
+
log('')
|
|
454
|
+
info(`Building localized content for: ${i18nConfig.locales.join(', ')}`)
|
|
455
|
+
try {
|
|
456
|
+
const outputs = await buildLocalizedContent(projectDir, i18nConfig)
|
|
457
|
+
success(`Generated ${Object.keys(outputs).length} locale(s)`)
|
|
458
|
+
for (const [locale] of Object.entries(outputs)) {
|
|
459
|
+
log(` ${colors.dim}dist/${locale}/site-content.json${colors.reset}`)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Collection translations — optional; don't fail the build if
|
|
463
|
+
// missing. Bundle mode does the same.
|
|
464
|
+
try {
|
|
465
|
+
const { buildLocalizedCollections } = await import('@uniweb/build/i18n')
|
|
466
|
+
const collectionOutputs = await buildLocalizedCollections(projectDir, {
|
|
467
|
+
locales: i18nConfig.locales,
|
|
468
|
+
outputDir: distDir,
|
|
469
|
+
collectionsLocalesDir: join(projectDir, i18nConfig.localesDir, 'collections'),
|
|
470
|
+
})
|
|
471
|
+
const collectionCount = Object.values(collectionOutputs).reduce(
|
|
472
|
+
(sum, localeOutputs) => sum + Object.keys(localeOutputs).length,
|
|
473
|
+
0
|
|
474
|
+
)
|
|
475
|
+
if (collectionCount > 0) {
|
|
476
|
+
success(`Translated collections for ${Object.keys(collectionOutputs).length} locale(s)`)
|
|
477
|
+
}
|
|
478
|
+
} catch (err) {
|
|
479
|
+
if (process.env.DEBUG) console.error('Collection translation:', err.message)
|
|
480
|
+
}
|
|
481
|
+
} catch (err) {
|
|
482
|
+
error(`i18n build failed: ${err.message}`)
|
|
483
|
+
if (process.env.DEBUG) console.error(err.stack)
|
|
484
|
+
log(`${colors.yellow}Continuing without localized content${colors.reset}`)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
log('')
|
|
489
|
+
log(`${colors.green}${colors.bright}Build complete (link mode)${colors.reset}`)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Best-effort resolution of the local foundation directory for a site,
|
|
494
|
+
* used by `buildSiteLink` to pass `foundationPath` to the data pipeline.
|
|
495
|
+
*
|
|
496
|
+
* Mirrors a subset of `@uniweb/build`'s `detectFoundationType` semantics:
|
|
497
|
+
* when the site declares `foundation: <name>` and a sibling/file: dep
|
|
498
|
+
* resolves to a local foundation, return its path. When the foundation
|
|
499
|
+
* is a registry ref or URL, return null (data pipeline still works
|
|
500
|
+
* without a local foundation; theme defaults just come from theme.yml).
|
|
501
|
+
*/
|
|
502
|
+
async function resolveFoundationDirForSite(siteDir, siteConfig) {
|
|
503
|
+
const cfg = siteConfig || readSiteConfig(siteDir)
|
|
504
|
+
const foundation = cfg?.foundation
|
|
505
|
+
if (!foundation || typeof foundation !== 'string') return null
|
|
506
|
+
// Registry ref or URL — no local foundation.
|
|
507
|
+
if (/^@[a-z0-9_-]+\/[a-z0-9_-]+@/.test(foundation)) return null
|
|
508
|
+
if (/^~[A-Za-z0-9_-]+\/[a-z0-9_-]+@/.test(foundation)) return null
|
|
509
|
+
if (foundation.startsWith('http://') || foundation.startsWith('https://')) return null
|
|
510
|
+
|
|
511
|
+
// Workspace sibling.
|
|
512
|
+
const sibling = resolve(siteDir, '..', foundation)
|
|
513
|
+
if (existsSync(sibling)) return sibling
|
|
514
|
+
|
|
515
|
+
// file: dep declared in package.json.
|
|
516
|
+
try {
|
|
517
|
+
const pkg = JSON.parse(readFileSync(join(siteDir, 'package.json'), 'utf-8'))
|
|
518
|
+
const dep = pkg.dependencies?.[foundation]
|
|
519
|
+
if (typeof dep === 'string' && dep.startsWith('file:')) {
|
|
520
|
+
const filePath = resolve(siteDir, dep.slice(5))
|
|
521
|
+
if (existsSync(filePath)) return filePath
|
|
522
|
+
}
|
|
523
|
+
} catch { /* no package.json or malformed — fall through */ }
|
|
524
|
+
|
|
525
|
+
return null
|
|
526
|
+
}
|
|
527
|
+
|
|
378
528
|
/**
|
|
379
529
|
* Build a site
|
|
380
530
|
*/
|
|
@@ -650,13 +800,24 @@ export async function build(args = []) {
|
|
|
650
800
|
}
|
|
651
801
|
}
|
|
652
802
|
|
|
653
|
-
// Check for --shell flag (shell mode: no embedded content, for dynamic backend)
|
|
654
|
-
const shellFlag = args.includes('--shell')
|
|
655
|
-
|
|
656
803
|
// Check for --prerender / --no-prerender flags
|
|
657
804
|
const prerenderFlag = args.includes('--prerender')
|
|
658
805
|
const noPrerenderFlag = args.includes('--no-prerender')
|
|
659
806
|
|
|
807
|
+
// Check for --link / --bundle flags. These select between two
|
|
808
|
+
// mutually exclusive site build pipelines:
|
|
809
|
+
// --link: data only, no vite (for Uniweb-edge hosting)
|
|
810
|
+
// --bundle: full vite pipeline (for static hosts; today's behavior)
|
|
811
|
+
// Bare `uniweb build` for a site defaults to --bundle for back-compat
|
|
812
|
+
// with users targeting static hosts. `uniweb deploy` always passes one
|
|
813
|
+
// of the two explicitly based on the resolved deploy mode.
|
|
814
|
+
const linkFlag = args.includes('--link')
|
|
815
|
+
const bundleFlag = args.includes('--bundle')
|
|
816
|
+
if (linkFlag && bundleFlag) {
|
|
817
|
+
error('Cannot pass both --link and --bundle (they select different build pipelines)')
|
|
818
|
+
process.exit(1)
|
|
819
|
+
}
|
|
820
|
+
|
|
660
821
|
// Check for --foundation-dir flag (for prerendering)
|
|
661
822
|
let foundationDir = null
|
|
662
823
|
const foundationDirIndex = args.indexOf('--foundation-dir')
|
|
@@ -685,9 +846,12 @@ export async function build(args = []) {
|
|
|
685
846
|
process.exit(1)
|
|
686
847
|
}
|
|
687
848
|
|
|
688
|
-
// Validate --
|
|
689
|
-
|
|
690
|
-
|
|
849
|
+
// Validate --link / --bundle are only used with site target.
|
|
850
|
+
// (Foundation builds have no equivalent split — they always produce
|
|
851
|
+
// dist/foundation.js + schema.json regardless of how a downstream
|
|
852
|
+
// site consumes the result.)
|
|
853
|
+
if ((linkFlag || bundleFlag) && targetType === 'foundation') {
|
|
854
|
+
error('--link and --bundle apply to site builds only')
|
|
691
855
|
process.exit(1)
|
|
692
856
|
}
|
|
693
857
|
|
|
@@ -700,6 +864,23 @@ export async function build(args = []) {
|
|
|
700
864
|
} else {
|
|
701
865
|
// For sites, read config to determine prerender default
|
|
702
866
|
const siteConfig = readSiteConfig(projectDir)
|
|
867
|
+
|
|
868
|
+
// Link mode: data-only pipeline, no vite. The deployed Uniweb-edge
|
|
869
|
+
// site never consumes the JS bundle, so we skip producing it.
|
|
870
|
+
// Worker generates HTML at request time using its own runtime +
|
|
871
|
+
// the foundation served from the registry. See
|
|
872
|
+
// `framework/build/src/site/build-site-data.js` for what gets
|
|
873
|
+
// emitted (and what doesn't).
|
|
874
|
+
if (linkFlag) {
|
|
875
|
+
if (prerenderFlag) {
|
|
876
|
+
error('--prerender does not apply to link mode (no static HTML is produced)')
|
|
877
|
+
process.exit(1)
|
|
878
|
+
}
|
|
879
|
+
await buildSiteLink(projectDir, { siteConfig })
|
|
880
|
+
return
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Bundle mode (default for sites, or explicit --bundle).
|
|
703
884
|
const configPrerender = siteConfig.build?.prerender === true
|
|
704
885
|
|
|
705
886
|
// CLI flags override config: --prerender forces on, --no-prerender forces off
|
|
@@ -707,21 +888,7 @@ export async function build(args = []) {
|
|
|
707
888
|
if (prerenderFlag) prerender = true
|
|
708
889
|
if (noPrerenderFlag) prerender = false
|
|
709
890
|
|
|
710
|
-
// Shell mode: set env var for Vite config, force no prerender
|
|
711
|
-
if (shellFlag) {
|
|
712
|
-
process.env.UNIWEB_SHELL = 'true'
|
|
713
|
-
prerender = false
|
|
714
|
-
info('Building in shell mode (no embedded content)')
|
|
715
|
-
}
|
|
716
|
-
|
|
717
891
|
await buildSite(projectDir, { prerender, foundationDir, siteConfig })
|
|
718
|
-
|
|
719
|
-
if (shellFlag) {
|
|
720
|
-
log('')
|
|
721
|
-
log(`${colors.green}${colors.bright}Shell build complete!${colors.reset}`)
|
|
722
|
-
log(` The shell contains no embedded content.`)
|
|
723
|
-
log(` Use ${colors.cyan}node scripts/platform/serve.js${colors.reset} to serve with dynamic content.`)
|
|
724
|
-
}
|
|
725
892
|
}
|
|
726
893
|
} catch (err) {
|
|
727
894
|
error(err.message)
|