@uniweb/build 0.64.1 → 0.64.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/package.json +4 -4
- package/src/import-map-plugin.js +13 -0
- package/src/site/config.js +59 -5
- package/src/site/foundation-ref.js +21 -2
- package/src/site/index.js +2 -0
- package/src/site/preview-media.js +90 -0
- package/src/site/preview.js +84 -0
- package/src/site/query-processor.js +6 -4
- package/src/site/registered-foundation.js +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.64.
|
|
3
|
+
"version": "0.64.2",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -57,11 +57,11 @@
|
|
|
57
57
|
"js-yaml": "^4.1.0",
|
|
58
58
|
"sharp": "^0.35.3",
|
|
59
59
|
"yaml": "^2.5.0",
|
|
60
|
+
"@uniweb/schemas": "^0.7.2",
|
|
60
61
|
"@uniweb/content-reader": "^1.2.5",
|
|
61
62
|
"@uniweb/content-writer": "^0.3.4",
|
|
62
63
|
"@uniweb/projections": "^0.7.4",
|
|
63
64
|
"@uniweb/semantic-parser": "^1.4.1",
|
|
64
|
-
"@uniweb/schemas": "^0.7.1",
|
|
65
65
|
"@uniweb/theming": "^0.1.15"
|
|
66
66
|
},
|
|
67
67
|
"optionalDependencies": {
|
|
@@ -74,8 +74,8 @@
|
|
|
74
74
|
"@tailwindcss/vite": "^4.0.0",
|
|
75
75
|
"@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
|
|
76
76
|
"vite-plugin-svgr": "^4.0.0",
|
|
77
|
-
"@uniweb/
|
|
78
|
-
"@uniweb/
|
|
77
|
+
"@uniweb/runtime": "^0.27.6",
|
|
78
|
+
"@uniweb/core": "^0.30.1"
|
|
79
79
|
},
|
|
80
80
|
"peerDependenciesMeta": {
|
|
81
81
|
"vite": {
|
package/src/import-map-plugin.js
CHANGED
|
@@ -85,6 +85,9 @@ export const bridgeFileName = (spec) => `${spec.replace(/\//g, '-')}.js`
|
|
|
85
85
|
* @param {Object} [options.devBridges] - Map of specifier → dev-mode URL for import map injection in dev.
|
|
86
86
|
* When provided, the import map is injected in both dev and prod (with different URLs).
|
|
87
87
|
* When omitted, the import map is only injected in prod (dev uses other mechanisms like transformRequest).
|
|
88
|
+
* @param {boolean} [options.serveInDev] - In dev, point each external at a bridge the dev server serves
|
|
89
|
+
* itself — the module a build would emit, through Vite's pipeline, so its bare imports become the very
|
|
90
|
+
* URLs the host app imports. For a module loaded from a URL, which no dev plugin rewrites.
|
|
88
91
|
* @returns {import('vite').Plugin}
|
|
89
92
|
*/
|
|
90
93
|
export function importMapPlugin({
|
|
@@ -93,14 +96,17 @@ export function importMapPlugin({
|
|
|
93
96
|
basePath = '/',
|
|
94
97
|
resolveFrom,
|
|
95
98
|
devBridges,
|
|
99
|
+
serveInDev = false,
|
|
96
100
|
} = {}) {
|
|
97
101
|
let isBuild = false
|
|
102
|
+
let devBase = '/'
|
|
98
103
|
|
|
99
104
|
return {
|
|
100
105
|
name,
|
|
101
106
|
|
|
102
107
|
configResolved(config) {
|
|
103
108
|
isBuild = config.command === 'build'
|
|
109
|
+
devBase = config.base || '/'
|
|
104
110
|
},
|
|
105
111
|
|
|
106
112
|
resolveId(id, importer) {
|
|
@@ -199,6 +205,13 @@ export function importMapPlugin({
|
|
|
199
205
|
}
|
|
200
206
|
} else if (devBridges) {
|
|
201
207
|
Object.assign(imports, devBridges)
|
|
208
|
+
} else if (serveInDev) {
|
|
209
|
+
// Vite serves a virtual module at `/@id/`, its NUL written `__x00__` — the id `load`
|
|
210
|
+
// answers below, so dev and build bridge one module the same way.
|
|
211
|
+
for (const ext of externals) {
|
|
212
|
+
const spec = specOf(ext)
|
|
213
|
+
imports[spec] = `${devBase}@id/__x00__${IMPORT_MAP_PREFIX.slice(1)}${spec}`
|
|
214
|
+
}
|
|
202
215
|
} else {
|
|
203
216
|
// No dev injection — consumer handles dev mode separately
|
|
204
217
|
return html
|
package/src/site/config.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* })
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
23
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
24
24
|
import { resolve, dirname, join } from 'node:path'
|
|
25
25
|
import yaml from 'js-yaml'
|
|
26
26
|
import { YAML_OPTIONS } from '../utils/yaml-schema.js'
|
|
@@ -34,6 +34,8 @@ import { resolveModuleUrl, resolveExtensionUrls } from './extension-urls.js'
|
|
|
34
34
|
import { resolveFoundationSrcPath } from '../utils/foundation-source-root.js'
|
|
35
35
|
import { checkFoundationResolution } from '../utils/foundation-resolution-check.js'
|
|
36
36
|
import { detectFoundationType } from './foundation-ref.js'
|
|
37
|
+
import { readPreview, PREVIEW_ENV } from './preview.js'
|
|
38
|
+
import { previewMediaPlugin } from './preview-media.js'
|
|
37
39
|
|
|
38
40
|
/**
|
|
39
41
|
* Normalize a base path for Vite compatibility
|
|
@@ -112,6 +114,25 @@ function unique(list) {
|
|
|
112
114
|
return [...new Set(list)]
|
|
113
115
|
}
|
|
114
116
|
|
|
117
|
+
/**
|
|
118
|
+
* The site's `@uniweb/runtime`, as a path to resolve its dependencies from — its REAL
|
|
119
|
+
* directory, since under pnpm a package's dependencies sit beside its real location and
|
|
120
|
+
* not beside the link to it. Null when the site has none installed.
|
|
121
|
+
*/
|
|
122
|
+
function runtimeAnchor(siteRoot) {
|
|
123
|
+
for (let dir = siteRoot; ; dir = dirname(dir)) {
|
|
124
|
+
const candidate = join(dir, 'node_modules', '@uniweb', 'runtime')
|
|
125
|
+
if (existsSync(join(candidate, 'package.json'))) {
|
|
126
|
+
try {
|
|
127
|
+
return join(realpathSync(candidate), 'package.json')
|
|
128
|
+
} catch {
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (dirname(dir) === dir) return null
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
115
136
|
export async function defineSiteConfig(options = {}) {
|
|
116
137
|
const {
|
|
117
138
|
plugins: extraPlugins = [],
|
|
@@ -147,8 +168,15 @@ export async function defineSiteConfig(options = {}) {
|
|
|
147
168
|
const rawBase = baseOption || process.env.UNIWEB_BASE || siteConfig.base
|
|
148
169
|
const base = rawBase ? normalizeBasePath(String(rawBase)) : undefined
|
|
149
170
|
|
|
150
|
-
//
|
|
151
|
-
|
|
171
|
+
// What `uniweb dev` read from the backend the site is on, for this dev server alone
|
|
172
|
+
// (`./preview.js`) — null unless it was handed one.
|
|
173
|
+
const preview = readPreview(process.env)
|
|
174
|
+
|
|
175
|
+
// Detect foundation type. A catalog ref resolves only for a preview, which is handed where
|
|
176
|
+
// the backend serves it.
|
|
177
|
+
const foundationInfo = detectFoundationType(siteConfig.foundation, siteRoot, {
|
|
178
|
+
served: preview?.foundation
|
|
179
|
+
})
|
|
152
180
|
|
|
153
181
|
// Check for runtime mode (env variable or URL-based foundation).
|
|
154
182
|
// Runtime mode means the foundation is loaded by URL at runtime; the
|
|
@@ -376,13 +404,39 @@ export async function defineSiteConfig(options = {}) {
|
|
|
376
404
|
plugins.push(importMapPlugin({
|
|
377
405
|
basePath: base || '/',
|
|
378
406
|
// Under pnpm strict mode, the site may not have @uniweb/core in its own
|
|
379
|
-
// node_modules. Resolve from the foundation directory where it's a direct dep
|
|
407
|
+
// node_modules. Resolve from the foundation directory where it's a direct dep —
|
|
408
|
+
// or, with no local foundation, as the site's runtime resolves it: the runtime is
|
|
409
|
+
// the host app whose instances a foundation must share.
|
|
380
410
|
resolveFrom: foundationInfo.path
|
|
381
411
|
? resolve(foundationInfo.path, 'package.json')
|
|
382
|
-
: resolve(siteRoot, 'main.js'),
|
|
412
|
+
: runtimeAnchor(siteRoot) || resolve(siteRoot, 'main.js'),
|
|
413
|
+
// A foundation served from a URL imports its externals by bare name, and in dev
|
|
414
|
+
// nothing else resolves them: the foundation dev plugin rewrites a LOCAL
|
|
415
|
+
// foundation's imports, and a served one is not ours to rewrite. So the dev server
|
|
416
|
+
// answers the import map a build writes, bridging to the site's own copies.
|
|
417
|
+
serveInDev: foundationInfo.type === 'url'
|
|
383
418
|
}))
|
|
384
419
|
}
|
|
385
420
|
|
|
421
|
+
if (preview) {
|
|
422
|
+
// ⛔ A PREVIEW'S ANSWER NEVER REACHES A BUILT ARTIFACT. Where the backend serves a
|
|
423
|
+
// version is decided at publish and read for one preview (`./preview.js`); written into
|
|
424
|
+
// `dist/` it would outlive the answer.
|
|
425
|
+
plugins.push({
|
|
426
|
+
name: 'uniweb:preview-only',
|
|
427
|
+
config(_config, env) {
|
|
428
|
+
if (env.command === 'build') {
|
|
429
|
+
throw new Error(
|
|
430
|
+
`[site] ${PREVIEW_ENV} is what \`uniweb dev\` read from ${preview.backend} for one ` +
|
|
431
|
+
`preview, and a build does not take it. Unset it to build.`
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
})
|
|
436
|
+
// A media URL the site keeps from its backend is fetched from that backend.
|
|
437
|
+
plugins.push(previewMediaPlugin({ backend: preview.backend, siteRoot }))
|
|
438
|
+
}
|
|
439
|
+
|
|
386
440
|
// Preload hints for runtime-loaded foundations and extensions.
|
|
387
441
|
// In runtime mode, foundation JS is loaded via import() and CSS is injected
|
|
388
442
|
// dynamically in JavaScript — the browser doesn't discover them until JS executes.
|
|
@@ -66,11 +66,15 @@ export function parseCatalogRef(value) {
|
|
|
66
66
|
*
|
|
67
67
|
* @param {string|Object} foundation - Foundation config from site.yml
|
|
68
68
|
* @param {string} siteRoot - Path to site directory
|
|
69
|
-
* @
|
|
69
|
+
* @param {Object} [options]
|
|
70
|
+
* @param {{ ref: string, url: string, cssUrl?: string|null }|null} [options.served] - where a
|
|
71
|
+
* catalog ref is served, for a preview: the answer `uniweb dev` read from the backend the site
|
|
72
|
+
* is on (`./preview.js`). Honored for the ref it names and no other.
|
|
73
|
+
* @returns {{ type: 'local'|'url', name?: string, url?: string, cssUrl?: string, path?: string, served?: string }}
|
|
70
74
|
* @throws {Error} when the declaration shape is invalid (versionless registry
|
|
71
75
|
* ref, unknown name with no local match, etc.)
|
|
72
76
|
*/
|
|
73
|
-
export function detectFoundationType(foundation, siteRoot) {
|
|
77
|
+
export function detectFoundationType(foundation, siteRoot, { served = null } = {}) {
|
|
74
78
|
// Object form with explicit URL
|
|
75
79
|
if (foundation && typeof foundation === 'object') {
|
|
76
80
|
if (foundation.url) {
|
|
@@ -119,11 +123,26 @@ export function detectFoundationType(foundation, siteRoot) {
|
|
|
119
123
|
// --backend` and the documented env var all left it pinned — and the artifact
|
|
120
124
|
// names were the pre-`entry.js` ones the build stopped emitting.
|
|
121
125
|
if (parseCatalogRef(name)) {
|
|
126
|
+
// ⭐ A PREVIEW is handed the answer instead of guessing it: `uniweb dev` asks the backend
|
|
127
|
+
// the site is on where this version is served, for its one dev server. The answer names
|
|
128
|
+
// the ref it is for, so it resolves the DECLARED foundation and can substitute no other.
|
|
129
|
+
if (served?.url && served.ref === name) {
|
|
130
|
+
return { type: 'url', url: served.url, cssUrl: served.cssUrl || null, served: name }
|
|
131
|
+
}
|
|
132
|
+
if (served?.ref) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`This dev server was handed where "${served.ref}" is served, and site.yml names "${name}".\n` +
|
|
135
|
+
`Restart \`uniweb dev\`, which asks for the foundation site.yml names.`
|
|
136
|
+
)
|
|
137
|
+
}
|
|
122
138
|
throw new Error(
|
|
123
139
|
[
|
|
124
140
|
`Foundation "${name}" is a catalog ref, and a build cannot resolve it to a URL.`,
|
|
125
141
|
`Where a foundation is served is the host's to declare, so the build does not guess it.`,
|
|
126
142
|
``,
|
|
143
|
+
` • Previewing the site? Run \`uniweb dev\`, which asks the backend the site is on`,
|
|
144
|
+
` where this version is served — and says so when that backend does not tell it.`,
|
|
145
|
+
``,
|
|
127
146
|
` • Deploying to another host (\`uniweb deploy --host=<adapter>\`), or taking`,
|
|
128
147
|
` the build anywhere (\`uniweb export\`)? Declare the served URL in site.yml —`,
|
|
129
148
|
` the runtime accepts any URL, from any host:`,
|
package/src/site/index.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
export { siteContentPlugin } from './plugin.js'
|
|
10
10
|
export { defineSiteConfig, readSiteConfig, default } from './config.js'
|
|
11
|
+
// What `uniweb dev` hands the dev server of a site a backend holds (`./preview.js`).
|
|
12
|
+
export { PREVIEW_ENV, encodePreview, readPreview } from './preview.js'
|
|
11
13
|
export { collectSiteContent } from './content-collector.js'
|
|
12
14
|
// The `agents:` vocabulary lives in @uniweb/projections (which owns the block);
|
|
13
15
|
// re-exported so the CLI can validate an author's spelling without taking a
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A PREVIEW FETCHES THE MEDIA ITS BACKEND SERVES — from that backend.
|
|
3
|
+
*
|
|
4
|
+
* A clone keeps a serve URL wherever the backend held one the pull cannot map back to an author's
|
|
5
|
+
* file — a record's image, a param (`uwx/asset-map.js::restoreAssetRefs`: *"the URL that works"*).
|
|
6
|
+
* A root-relative one names a file on the origin that served the document holding it. On the dev
|
|
7
|
+
* server's origin it names nothing, and a dev server answers a path it lacks with `index.html` —
|
|
8
|
+
* so the image broke with a 200.
|
|
9
|
+
*
|
|
10
|
+
* So in a preview (`./preview.js`), a request for media the site does not hold is answered by the
|
|
11
|
+
* backend the site is on. ⭐ Nothing is composed: the path is the URL the content holds, resolved
|
|
12
|
+
* against the origin it came from. ⛔ Only media — a request whose destination is an image, a video,
|
|
13
|
+
* audio, a track or a font — and only what the site does not hold, in `public/` or under its root:
|
|
14
|
+
* the site's own files, its routes and Vite's own paths are never sent anywhere. The request carries
|
|
15
|
+
* no credential; a served asset is public.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, statSync } from 'node:fs'
|
|
18
|
+
import { join, normalize, sep } from 'node:path'
|
|
19
|
+
import { Readable } from 'node:stream'
|
|
20
|
+
|
|
21
|
+
const MEDIA_DESTINATIONS = new Set(['image', 'video', 'audio', 'track', 'font'])
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {{ backend: string, siteRoot: string }} options - the backend's origin (`readPreview`)
|
|
25
|
+
* @returns {import('vite').Plugin}
|
|
26
|
+
*/
|
|
27
|
+
export function previewMediaPlugin({ backend, siteRoot }) {
|
|
28
|
+
return {
|
|
29
|
+
name: 'uniweb:preview-media',
|
|
30
|
+
apply: 'serve',
|
|
31
|
+
configureServer(server) {
|
|
32
|
+
// Registered before Vite's own middlewares, whose SPA fallback would answer first.
|
|
33
|
+
server.middlewares.use(async (req, res, next) => {
|
|
34
|
+
const pathname = mediaPath(req)
|
|
35
|
+
if (!pathname) return next()
|
|
36
|
+
const base = server.config.base || '/'
|
|
37
|
+
const local = [pathname]
|
|
38
|
+
if (base !== '/' && pathname.startsWith(base)) local.push('/' + pathname.slice(base.length))
|
|
39
|
+
const roots = [server.config.publicDir, siteRoot].filter(Boolean)
|
|
40
|
+
if (local.some((p) => roots.some((root) => holds(root, p)))) return next()
|
|
41
|
+
|
|
42
|
+
let upstream
|
|
43
|
+
try {
|
|
44
|
+
upstream = await fetch(new URL(req.url, backend), { method: req.method, redirect: 'follow' })
|
|
45
|
+
} catch (err) {
|
|
46
|
+
res.statusCode = 502
|
|
47
|
+
return res.end(`The site's backend (${backend}) did not answer: ${err.message}`)
|
|
48
|
+
}
|
|
49
|
+
// Its answer, whatever it is — a 404 here is the backend not having it either, which
|
|
50
|
+
// is the truth; falling through would answer `index.html`.
|
|
51
|
+
res.statusCode = upstream.status
|
|
52
|
+
for (const header of ['content-type', 'content-length', 'cache-control', 'etag', 'last-modified']) {
|
|
53
|
+
const value = upstream.headers.get(header)
|
|
54
|
+
if (value) res.setHeader(header, value)
|
|
55
|
+
}
|
|
56
|
+
if (req.method === 'HEAD' || !upstream.body) return res.end()
|
|
57
|
+
Readable.fromWeb(upstream.body).pipe(res)
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The root-relative path a media request names, or null for any other request. */
|
|
64
|
+
export function mediaPath(req) {
|
|
65
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') return null
|
|
66
|
+
const dest = req.headers['sec-fetch-dest']
|
|
67
|
+
const accept = String(req.headers.accept || '')
|
|
68
|
+
const media = dest ? MEDIA_DESTINATIONS.has(dest) : /^(image|video|audio)\//.test(accept)
|
|
69
|
+
if (!media) return null
|
|
70
|
+
let pathname
|
|
71
|
+
try {
|
|
72
|
+
pathname = decodeURIComponent(new URL(req.url, 'http://preview.invalid').pathname)
|
|
73
|
+
} catch {
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
// Vite's own paths are never the backend's.
|
|
77
|
+
if (/^\/(@|node_modules\/|__)/.test(pathname)) return null
|
|
78
|
+
return pathname
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Whether a file lies at `pathname` under `root`, and inside it. */
|
|
82
|
+
function holds(root, pathname) {
|
|
83
|
+
const file = normalize(join(root, pathname))
|
|
84
|
+
if (file !== root && !file.startsWith(root.endsWith(sep) ? root : root + sep)) return false
|
|
85
|
+
try {
|
|
86
|
+
return existsSync(file) && statSync(file).isFile()
|
|
87
|
+
} catch {
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A PREVIEW OF A SITE A BACKEND HOLDS — what `uniweb dev` read from that backend, handed to the one
|
|
3
|
+
* dev server it starts.
|
|
4
|
+
*
|
|
5
|
+
* A clone names its foundation by catalog ref (`@acme/fnd@1.2.0`) and keeps the media URLs the
|
|
6
|
+
* backend serves, so two things its dev server needs are the backend's to say. `uniweb dev` asks the
|
|
7
|
+
* backend the site is on, through the site, and passes what it learned in `UNIWEB_PREVIEW`, as JSON:
|
|
8
|
+
*
|
|
9
|
+
* { backend: 'https://…', the backend the site is on
|
|
10
|
+
* foundation: { ref, url, cssUrl } } where it serves the version site.yml names
|
|
11
|
+
*
|
|
12
|
+
* ⭐ It resolves the declared foundation; it never substitutes one. The answer names the ref it is
|
|
13
|
+
* for, and `detectFoundationType` honors it for that ref alone — which is what separates it from the
|
|
14
|
+
* `UNIWEB_FOUNDATION_REF` override `config.js` retired, a way for a site to be rendered by code its
|
|
15
|
+
* own `site.yml` did not name.
|
|
16
|
+
*
|
|
17
|
+
* ⭐ And a media URL the clone kept is the backend's own. A pull keeps any serve URL it cannot map
|
|
18
|
+
* back to an author's file (`uwx/asset-map.js::restoreAssetRefs`, *"the URL that works"*), and a
|
|
19
|
+
* root-relative one works on the origin that served the document holding it. So the dev server
|
|
20
|
+
* fetches from `backend` what the content names and the site does not have (`config.js`) — it
|
|
21
|
+
* resolves the URL it was given against the origin it came from, and composes no route.
|
|
22
|
+
*
|
|
23
|
+
* ⛔ Nothing keeps it. A serve location is decided at publish and read from the response that
|
|
24
|
+
* carries it, so it lives as long as the process that asked: `site.yml` keeps the ref, and no push,
|
|
25
|
+
* pull or build sees a URL from here. A build handed one refuses it (`config.js`), so a preview's
|
|
26
|
+
* answer never reaches a built artifact.
|
|
27
|
+
*
|
|
28
|
+
* ⚠️ And it may not be known. Whether a backend tells the CLI where a version is served is the
|
|
29
|
+
* backend's to decide; `uniweb dev` says so and starts nothing when it is not told.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** The variable `uniweb dev` sets for the dev server it starts. */
|
|
33
|
+
export const PREVIEW_ENV = 'UNIWEB_PREVIEW'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The variable's value.
|
|
37
|
+
*
|
|
38
|
+
* @param {{ backend: string, foundation: { ref: string, url: string, cssUrl?: string|null } }} preview
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
export function encodePreview({ backend, foundation }) {
|
|
42
|
+
const { ref, url, cssUrl = null } = foundation
|
|
43
|
+
return JSON.stringify({ backend, foundation: { ref, url, cssUrl: cssUrl || null } })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* What a dev server was handed, or null when it was handed nothing.
|
|
48
|
+
*
|
|
49
|
+
* @param {Record<string, string|undefined>} [env]
|
|
50
|
+
* @returns {{ backend: string, foundation: { ref: string, url: string, cssUrl: string|null } }|null}
|
|
51
|
+
* @throws {Error} when the variable is set and says nothing usable — a preview that cannot read its
|
|
52
|
+
* answer must not start as if it had none
|
|
53
|
+
*/
|
|
54
|
+
export function readPreview(env = process.env) {
|
|
55
|
+
const raw = env[PREVIEW_ENV]
|
|
56
|
+
if (!raw) return null
|
|
57
|
+
let given = null
|
|
58
|
+
try {
|
|
59
|
+
given = JSON.parse(raw)
|
|
60
|
+
} catch {
|
|
61
|
+
// reported below
|
|
62
|
+
}
|
|
63
|
+
const f = given?.foundation
|
|
64
|
+
if (!isHttpOrigin(given?.backend) || typeof f?.ref !== 'string' || typeof f.url !== 'string' || !f.url) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`${PREVIEW_ENV} holds no preview (${JSON.stringify(raw).slice(0, 120)}). ` +
|
|
67
|
+
'It is set by `uniweb dev`; run that rather than setting it.'
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
backend: new URL(given.backend).origin,
|
|
72
|
+
foundation: { ref: f.ref, url: f.url, cssUrl: typeof f.cssUrl === 'string' ? f.cssUrl : null }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ⛔ `new URL('localhost:8080')` parses — as the scheme `localhost:` — and its origin is the string
|
|
77
|
+
// "null". An origin is http(s), or it is nothing.
|
|
78
|
+
function isHttpOrigin(value) {
|
|
79
|
+
try {
|
|
80
|
+
return /^https?:$/.test(new URL(value).protocol)
|
|
81
|
+
} catch {
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -606,10 +606,12 @@ async function processContentItem(dir, filename, config, siteRoot, basePath, rec
|
|
|
606
606
|
* (`@uniweb/schemas/conform`'s `toDeliveredRecord`), with a markdown body in the
|
|
607
607
|
* schema's content body field — the ProseMirror document for a `format: prosemirror`
|
|
608
608
|
* field (`article_body.content`, for `@std/article`), the markdown source for a markup
|
|
609
|
-
* `text` one. A component is written against one shape, so
|
|
610
|
-
* one
|
|
611
|
-
*
|
|
612
|
-
*
|
|
609
|
+
* `text` one. A component is written against one shape, so both lanes deliver the same
|
|
610
|
+
* one — ruled 2026-09-24 [Diego] as a requirement that the shape be one AND a good one,
|
|
611
|
+
* not as an instruction to adopt the host's. ⚠️ It was built by adopting the host's as it
|
|
612
|
+
* was (read until 2026-09-26 as "the same as hosted"), and that shape puts two namespaces
|
|
613
|
+
* in one object (`@uniweb/schemas/conform`, above `toDeliveredRecord`). ⛔ Until 2026-09-24
|
|
614
|
+
* a record reached a component as its file held it, its body in `content` at the top.
|
|
613
615
|
*
|
|
614
616
|
* `$name` is the record's handle, taken BEFORE the lift: a brief may declare a `slug`
|
|
615
617
|
* field of its own (`@std/article`'s does), and the lift puts that one over the file's.
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* trip, never a wrong answer: the next push or pull reads it again. That is what makes `.uniweb/`
|
|
12
12
|
* (gitignored, per copy) the right home for it.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Only its `schema` is kept — the foundation schema a build would emit, less its data schemas, which
|
|
15
|
+
* travel as Models. ⛔ The reply carries more than a project reads, a serve location among it; a serve
|
|
16
|
+
* location is decided at publish and read from the response that carries it, never kept here.
|
|
16
17
|
*/
|
|
17
18
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
18
19
|
import { dirname, join } from 'node:path'
|