@uniweb/build 0.30.2 → 0.32.0
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/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -59,15 +59,15 @@
|
|
|
59
59
|
"js-yaml": "^4.1.0",
|
|
60
60
|
"sharp": "^0.35.3",
|
|
61
61
|
"yaml": "^2.5.0",
|
|
62
|
+
"@uniweb/content-writer": "^0.3.4",
|
|
62
63
|
"@uniweb/content-reader": "^1.2.4",
|
|
63
|
-
"@uniweb/
|
|
64
|
-
"@uniweb/schemas": "^0.2.
|
|
65
|
-
"@uniweb/semantic-parser": "^1.3.1",
|
|
64
|
+
"@uniweb/semantic-parser": "^1.4.0",
|
|
65
|
+
"@uniweb/schemas": "^0.2.13",
|
|
66
66
|
"@uniweb/theming": "^0.1.15",
|
|
67
|
-
"@uniweb/
|
|
67
|
+
"@uniweb/projections": "^0.5.4"
|
|
68
68
|
},
|
|
69
69
|
"optionalDependencies": {
|
|
70
|
-
"@uniweb/runtime": "^0.13.
|
|
70
|
+
"@uniweb/runtime": "^0.13.5"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
73
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"@tailwindcss/vite": "^4.0.0",
|
|
77
77
|
"@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
|
|
78
78
|
"vite-plugin-svgr": "^4.0.0",
|
|
79
|
-
"@uniweb/core": "^0.
|
|
79
|
+
"@uniweb/core": "^0.16.0"
|
|
80
80
|
},
|
|
81
81
|
"peerDependenciesMeta": {
|
|
82
82
|
"vite": {
|
package/src/site/config.js
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
import { importMapPlugin } from '../import-map-plugin.js'
|
|
32
32
|
import { resolveModuleUrl, resolveExtensionUrls } from './extension-urls.js'
|
|
33
33
|
import { resolveFoundationSrcPath } from '../utils/foundation-source-root.js'
|
|
34
|
+
import { checkFoundationResolution } from '../utils/foundation-resolution-check.js'
|
|
34
35
|
import { detectFoundationType } from './foundation-ref.js'
|
|
35
36
|
|
|
36
37
|
/**
|
|
@@ -191,7 +192,7 @@ export async function defineSiteConfig(options = {}) {
|
|
|
191
192
|
// Plugin to ensure foundation entry file exists (for bundled mode with local foundation)
|
|
192
193
|
const ensureFoundationEntryPlugin = !isRuntimeMode && foundationInfo.type === 'local' ? {
|
|
193
194
|
name: 'uniweb:ensure-foundation-entry',
|
|
194
|
-
async config() {
|
|
195
|
+
async config(_config, env) {
|
|
195
196
|
const srcDir = resolveFoundationSrcPath(foundationInfo.path)
|
|
196
197
|
const entryPath = join(srcDir, '_entry.generated.js')
|
|
197
198
|
|
|
@@ -202,8 +203,50 @@ export async function defineSiteConfig(options = {}) {
|
|
|
202
203
|
try {
|
|
203
204
|
await generateEntryPoint(srcDir, entryPath)
|
|
204
205
|
} catch (err) {
|
|
205
|
-
|
|
206
|
+
// ⛔ **Swallowing this on a BUILD is how you get vite's opaque
|
|
207
|
+
// `Failed to resolve entry for package "<name>"` 50ms later**: we
|
|
208
|
+
// decline to write the entry, say so in a `console.warn` that scrolls
|
|
209
|
+
// past, and hand vite a package whose `main` points at a file nobody
|
|
210
|
+
// created. The message naming the actual cause is the one we printed
|
|
211
|
+
// and discarded.
|
|
212
|
+
//
|
|
213
|
+
// ⚖️ **But only when there is no entry to fall back on.** If a previous
|
|
214
|
+
// build left one, it is stale rather than absent and the build can
|
|
215
|
+
// still complete — turning that into a hard failure would break a
|
|
216
|
+
// build that works today, which is a worse trade than a loud warning.
|
|
217
|
+
// Dev keeps warning either way: a transient error mid-session must not
|
|
218
|
+
// kill a running server.
|
|
219
|
+
const haveFallback = existsSync(entryPath)
|
|
220
|
+
if (env?.command === 'build' && !haveFallback) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
`[site] Could not generate the foundation entry for "${foundationInfo.name}".\n` +
|
|
223
|
+
` ${err.message}\n\n` +
|
|
224
|
+
` ${entryPath} does not exist, so the build cannot resolve the\n` +
|
|
225
|
+
` foundation. Fixing the error above is the fix; vite's next\n` +
|
|
226
|
+
` message about resolving package "${foundationInfo.name}" is a\n` +
|
|
227
|
+
` symptom of this one.`,
|
|
228
|
+
{ cause: err }
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
console.warn(
|
|
232
|
+
`[site] ⛔ Failed to generate foundation entry: ${err.message}` +
|
|
233
|
+
(haveFallback
|
|
234
|
+
? `\n[site] Continuing with the EXISTING ${entryPath}, which is now STALE.`
|
|
235
|
+
: '')
|
|
236
|
+
)
|
|
206
237
|
}
|
|
238
|
+
|
|
239
|
+
// Do we and vite agree on which directory that was? Two resolutions run
|
|
240
|
+
// here — ours by path, vite's by the bare specifier below
|
|
241
|
+
// (`alias['#foundation'] = foundationInfo.name`) — and when they diverge
|
|
242
|
+
// the quiet outcome is worse than the loud one: a stale entry in vite's
|
|
243
|
+
// directory builds the WRONG foundation and reports success.
|
|
244
|
+
const agreement = checkFoundationResolution({
|
|
245
|
+
name: foundationInfo.name,
|
|
246
|
+
generatedInto: srcDir,
|
|
247
|
+
siteRoot,
|
|
248
|
+
})
|
|
249
|
+
if (!agreement.ok) console.warn(`\n${agreement.message}\n`)
|
|
207
250
|
}
|
|
208
251
|
},
|
|
209
252
|
|
|
@@ -277,7 +320,31 @@ export async function defineSiteConfig(options = {}) {
|
|
|
277
320
|
// Point #foundation at a virtual noop module.
|
|
278
321
|
alias['#foundation'] = '\0__foundation-noop__'
|
|
279
322
|
} else if (foundationInfo.type !== 'url') {
|
|
280
|
-
// Bundled mode: #foundation points to the actual package
|
|
323
|
+
// Bundled mode: #foundation points to the actual package.
|
|
324
|
+
//
|
|
325
|
+
// ⛔ **A BARE SPECIFIER, NOT `foundationInfo.path` — and the obvious
|
|
326
|
+
// "improvement" is measured and wrong.** We hold the resolved path right
|
|
327
|
+
// here, and handing vite the *name* instead is what lets its node_modules
|
|
328
|
+
// lookup disagree with ours (the whole reason
|
|
329
|
+
// `uniweb:ensure-foundation-entry` now runs `checkFoundationResolution`).
|
|
330
|
+
// Collapsing the two by aliasing to the path looks like it deletes that bug
|
|
331
|
+
// class. It does not:
|
|
332
|
+
//
|
|
333
|
+
// A vite alias is PREFIX replacement, so `#foundation/styles` — which the
|
|
334
|
+
// site's own `entry.js` imports — becomes `<path>/styles`, a filesystem
|
|
335
|
+
// path. That bypasses the foundation's `exports` map, and the map is where
|
|
336
|
+
// `./styles` → `./styles.css` lives (along with `./dist` and
|
|
337
|
+
// `./dist/styles`). Measured 2026-09-01 on a stock marketing scaffold:
|
|
338
|
+
// `[vite:load-fallback] Could not load <path>/src/styles (imported by
|
|
339
|
+
// entry.js): ENOENT`. A HEALTHY project fails to build.
|
|
340
|
+
//
|
|
341
|
+
// Splitting it — exact `#foundation` → path, `#foundation/*` → name — is
|
|
342
|
+
// worse still: on a diverged tree the entry would come from one directory
|
|
343
|
+
// and the stylesheet from another, which is a state neither consistent
|
|
344
|
+
// option can reach.
|
|
345
|
+
//
|
|
346
|
+
// ⇒ **The name is load-bearing because the exports map is.** Detecting the
|
|
347
|
+
// disagreement is the correct shape; eliminating it costs the exports map.
|
|
281
348
|
alias['#foundation'] = foundationInfo.name
|
|
282
349
|
}
|
|
283
350
|
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Do WE and VITE agree on where the foundation is?
|
|
3
|
+
*
|
|
4
|
+
* ## The bug this exists for
|
|
5
|
+
*
|
|
6
|
+
* A site build resolves its foundation **twice, by two different mechanisms**,
|
|
7
|
+
* and until 2026-09-01 nothing checked that they agreed:
|
|
8
|
+
*
|
|
9
|
+
* 1. **We resolve a PATH.** `detectFoundationType('src', siteRoot)` returns
|
|
10
|
+
* `{ type: 'local', path: <siteRoot>/../src }`, and the
|
|
11
|
+
* `uniweb:ensure-foundation-entry` plugin generates `_entry.generated.js`
|
|
12
|
+
* into exactly that directory.
|
|
13
|
+
* 2. **Vite resolves a NAME.** `site/config.js` sets
|
|
14
|
+
* `alias['#foundation'] = foundationInfo.name` — the bare specifier — so
|
|
15
|
+
* Vite runs its own node_modules lookup and reads that package's `main`.
|
|
16
|
+
*
|
|
17
|
+
* Those name the same directory only because a package manager linked
|
|
18
|
+
* `node_modules/<name>` to the foundation. Nothing enforces it, and when it does
|
|
19
|
+
* not hold we generate into one directory while Vite reads another.
|
|
20
|
+
*
|
|
21
|
+
* ## ⛔ The failure is worse when it does NOT fail
|
|
22
|
+
*
|
|
23
|
+
* If the directory Vite reaches has no `_entry.generated.js`, the build dies with
|
|
24
|
+
* vite's `[commonjs--resolver] Failed to resolve entry for package "<name>"` —
|
|
25
|
+
* opaque, but loud.
|
|
26
|
+
*
|
|
27
|
+
* **If it has a STALE one, nothing fails at all.** The site builds against a
|
|
28
|
+
* different foundation than the one on disk, and reports success. A developer
|
|
29
|
+
* editing `src/` sees their change generated into `src/_entry.generated.js` and
|
|
30
|
+
* simply not take effect. *(Measured by the `flows` lane 2026-09-01: their
|
|
31
|
+
* harness minted fixtures under a `node_modules` that was itself a symlink to a
|
|
32
|
+
* seed tree, so a relative `../../src` link resolved into the seed. Every run
|
|
33
|
+
* that mutated its foundation and then built a site had been silently building
|
|
34
|
+
* the seed's copy — and presenting as green — for as long as the harness had
|
|
35
|
+
* existed. The red they opened a channel about was the harmless half.)*
|
|
36
|
+
*
|
|
37
|
+
* ⇒ **This check is about the silent case.** An error message can only reach the
|
|
38
|
+
* loud one; comparing the two answers reaches both.
|
|
39
|
+
*
|
|
40
|
+
* ## Why it compares REALPATHS and not link text
|
|
41
|
+
*
|
|
42
|
+
* `readlink node_modules/<name>` returning `../../src` looks healthy and proves
|
|
43
|
+
* nothing: **a relative symlink resolves against the link's target, not its
|
|
44
|
+
* location**, so with `node_modules` itself a link, `../../src` lands in a
|
|
45
|
+
* different tree. Only the resolved physical path answers the question vite
|
|
46
|
+
* actually asks. *(This module's first draft compared link text. It reported
|
|
47
|
+
* healthy on the one tree that was broken.)*
|
|
48
|
+
*
|
|
49
|
+
* ## Why it does its own node_modules walk
|
|
50
|
+
*
|
|
51
|
+
* `createRequire(...).resolve(name)` cannot be used: in the failure case `main`
|
|
52
|
+
* points at the missing entry, so it throws rather than telling us where it
|
|
53
|
+
* looked. `require.resolve(name + '/package.json')` cannot either — a foundation
|
|
54
|
+
* declares `exports: { ".": "./_entry.generated.js" }`, which gates every other
|
|
55
|
+
* subpath, so that lookup fails on a *healthy* package.
|
|
56
|
+
*
|
|
57
|
+
* So we walk `node_modules` upward for the package directory, which is the part
|
|
58
|
+
* of node resolution that locates a package before `exports` or `main` is
|
|
59
|
+
* consulted. That is the step whose answer we need.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
import { existsSync, realpathSync } from 'node:fs'
|
|
63
|
+
import { dirname, join, parse } from 'node:path'
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The directory a bare specifier's package lives in, by node's own
|
|
67
|
+
* node_modules walk — the step that runs before `exports`/`main` are read.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} name - the bare specifier (a foundation's package name)
|
|
70
|
+
* @param {string} fromDir - the directory resolution starts in (the site root)
|
|
71
|
+
* @returns {string|null} the package directory, or null if no node_modules
|
|
72
|
+
* anywhere up the tree holds it
|
|
73
|
+
*/
|
|
74
|
+
export function findPackageDir(name, fromDir) {
|
|
75
|
+
let dir = fromDir
|
|
76
|
+
const { root } = parse(fromDir)
|
|
77
|
+
// eslint-disable-next-line no-constant-condition
|
|
78
|
+
while (true) {
|
|
79
|
+
const candidate = join(dir, 'node_modules', name)
|
|
80
|
+
if (existsSync(join(candidate, 'package.json'))) return candidate
|
|
81
|
+
if (dir === root) return null
|
|
82
|
+
const parent = dirname(dir)
|
|
83
|
+
if (parent === dir) return null
|
|
84
|
+
dir = parent
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Compare where we generated the foundation entry against where vite will look.
|
|
90
|
+
*
|
|
91
|
+
* ⭐ **Silence is deliberate when the package is not found at all.** Not every
|
|
92
|
+
* supported layout puts the foundation in the site's `node_modules` — a
|
|
93
|
+
* `foundations/<name>/` multi-site project may not — and a warning that fires on
|
|
94
|
+
* a healthy project is worse than no warning, because the next person learns to
|
|
95
|
+
* ignore it. A package we cannot locate is also a case vite will fail on by
|
|
96
|
+
* itself, loudly. **We report only a disagreement we can actually prove**, which
|
|
97
|
+
* makes a false positive impossible by construction.
|
|
98
|
+
*
|
|
99
|
+
* @param {Object} args
|
|
100
|
+
* @param {string} args.name - the foundation's declared name (the bare specifier)
|
|
101
|
+
* @param {string} args.generatedInto - the directory we wrote `_entry.generated.js` to
|
|
102
|
+
* @param {string} args.siteRoot - where vite resolves the bare specifier from
|
|
103
|
+
* @returns {{ ok: true } | { ok: false, ours: string, theirs: string, message: string }}
|
|
104
|
+
*/
|
|
105
|
+
export function checkFoundationResolution({ name, generatedInto, siteRoot }) {
|
|
106
|
+
const theirsRaw = findPackageDir(name, siteRoot)
|
|
107
|
+
if (!theirsRaw) return { ok: true }
|
|
108
|
+
|
|
109
|
+
let ours, theirs
|
|
110
|
+
try {
|
|
111
|
+
ours = realpathSync(generatedInto)
|
|
112
|
+
theirs = realpathSync(theirsRaw)
|
|
113
|
+
} catch {
|
|
114
|
+
// A path vanished between the walk and the realpath. Not a disagreement we
|
|
115
|
+
// can prove, so it is not one we report.
|
|
116
|
+
return { ok: true }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (ours === theirs) return { ok: true }
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
ours,
|
|
124
|
+
theirs,
|
|
125
|
+
message: [
|
|
126
|
+
`[site] ⛔ Foundation "${name}" resolves to two different directories.`,
|
|
127
|
+
``,
|
|
128
|
+
` we generated its entry into : ${ours}`,
|
|
129
|
+
` vite will import it from : ${theirs}`,
|
|
130
|
+
``,
|
|
131
|
+
` Your site build will use the SECOND one, so edits to the first do not`,
|
|
132
|
+
` take effect — and if that directory carries a stale _entry.generated.js`,
|
|
133
|
+
` the build SUCCEEDS against the wrong foundation.`,
|
|
134
|
+
``,
|
|
135
|
+
` This means node_modules/${name} under ${siteRoot} is not the foundation`,
|
|
136
|
+
` directory. Re-run your package manager's install. If you copied or moved`,
|
|
137
|
+
` this project, check whether node_modules (or a parent of it) is a symlink`,
|
|
138
|
+
` into another tree — a relative link inside one resolves against the link's`,
|
|
139
|
+
` target, so it can point outside this project while looking correct.`,
|
|
140
|
+
].join('\n'),
|
|
141
|
+
}
|
|
142
|
+
}
|