@uniweb/build 0.14.30 → 0.14.31
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 +5 -5
- package/src/dev/plugin.js +80 -22
- package/src/generate-entry.js +69 -3
- package/src/hosts/ci-workflow.js +19 -1
- package/src/hosts/cloudflare-pages.js +1 -1
- package/src/hosts/github-pages.js +1 -1
- package/src/hosts/netlify.js +1 -1
- package/src/hosts/vercel.js +1 -1
- package/src/schema.js +5 -3
- package/src/site/config.js +12 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.31",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -59,13 +59,13 @@
|
|
|
59
59
|
"js-yaml": "^4.1.0",
|
|
60
60
|
"sharp": "^0.33.2",
|
|
61
61
|
"yaml": "^2.5.0",
|
|
62
|
-
"@uniweb/
|
|
63
|
-
"@uniweb/
|
|
62
|
+
"@uniweb/content-writer": "0.2.6",
|
|
63
|
+
"@uniweb/theming": "0.1.8"
|
|
64
64
|
},
|
|
65
65
|
"optionalDependencies": {
|
|
66
66
|
"@uniweb/runtime": "0.8.26",
|
|
67
|
-
"@uniweb/
|
|
68
|
-
"@uniweb/
|
|
67
|
+
"@uniweb/schemas": "0.2.4",
|
|
68
|
+
"@uniweb/content-reader": "1.1.12"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
package/src/dev/plugin.js
CHANGED
|
@@ -23,10 +23,59 @@
|
|
|
23
23
|
import { resolve, join } from 'node:path'
|
|
24
24
|
import { watch } from 'node:fs'
|
|
25
25
|
import { readFile } from 'node:fs/promises'
|
|
26
|
-
import { existsSync } from 'node:fs'
|
|
26
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
27
27
|
import { build } from 'vite'
|
|
28
28
|
import { resolveFoundationSrcPath } from '../utils/foundation-source-root.js'
|
|
29
29
|
|
|
30
|
+
/** Directories that never hold foundation source and must never be walked. */
|
|
31
|
+
const UNWATCHABLE_DIRS = new Set(['node_modules', 'dist', 'build', 'coverage'])
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve what to watch for a foundation's source changes.
|
|
35
|
+
*
|
|
36
|
+
* Returns the source root as a NON-recursive target (root-level main.js,
|
|
37
|
+
* styles.css, …) plus each source subdirectory as a recursive target.
|
|
38
|
+
*
|
|
39
|
+
* Enumerating subdirectories rather than naming them keeps this correct across
|
|
40
|
+
* every foundation layout — `sections/`, `components/`, `utils/`, `layouts/`,
|
|
41
|
+
* whatever extra section paths `defineFoundationConfig({ sections })` declares,
|
|
42
|
+
* and whatever a given project happens to call its folders.
|
|
43
|
+
*
|
|
44
|
+
* Known limitation: a top-level directory created after the server starts is
|
|
45
|
+
* not watched recursively until restart. The non-recursive root watch still
|
|
46
|
+
* sees it appear, so a rebuild fires; only per-file events inside it are
|
|
47
|
+
* missed. This matches the existing directory-rename limitation.
|
|
48
|
+
*
|
|
49
|
+
* Watching the source root recursively is not an option: under the flat layout
|
|
50
|
+
* (`main: "./_entry.generated.js"`) that root is the foundation *package* root,
|
|
51
|
+
* so a recursive watch descends into `node_modules/`. On Linux — including
|
|
52
|
+
* WSL2 — Node implements recursive fs.watch as one inotify watch per
|
|
53
|
+
* subdirectory, which exhausts `fs.inotify.max_user_watches` on startup and
|
|
54
|
+
* fails with ENOSPC. macOS hides the bug entirely: FSEvents makes a recursive
|
|
55
|
+
* watch a single handle regardless of tree size.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} srcPath - Foundation source directory (absolute)
|
|
58
|
+
* @returns {Array<{path: string, recursive: boolean}>}
|
|
59
|
+
*/
|
|
60
|
+
function resolveWatchTargets(srcPath) {
|
|
61
|
+
const targets = [{ path: srcPath, recursive: false }]
|
|
62
|
+
|
|
63
|
+
let entries = []
|
|
64
|
+
try {
|
|
65
|
+
entries = readdirSync(srcPath, { withFileTypes: true })
|
|
66
|
+
} catch {
|
|
67
|
+
return targets
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
if (!entry.isDirectory()) continue
|
|
72
|
+
if (entry.name.startsWith('.') || UNWATCHABLE_DIRS.has(entry.name)) continue
|
|
73
|
+
targets.push({ path: join(srcPath, entry.name), recursive: true })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return targets
|
|
77
|
+
}
|
|
78
|
+
|
|
30
79
|
/**
|
|
31
80
|
* Create the foundation dev plugin
|
|
32
81
|
*
|
|
@@ -180,28 +229,37 @@ export function foundationDevPlugin(options = {}) {
|
|
|
180
229
|
}, 200)
|
|
181
230
|
}
|
|
182
231
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
232
|
+
const onChange = (eventType, filename) => {
|
|
233
|
+
// Ignore generated files (build output triggers entry regeneration)
|
|
234
|
+
if (filename && filename.includes('_entry.generated')) return
|
|
235
|
+
|
|
236
|
+
// Only rebuild for source file changes
|
|
237
|
+
if (
|
|
238
|
+
filename &&
|
|
239
|
+
(filename.endsWith('.js') ||
|
|
240
|
+
filename.endsWith('.jsx') ||
|
|
241
|
+
filename.endsWith('.ts') ||
|
|
242
|
+
filename.endsWith('.tsx') ||
|
|
243
|
+
filename.endsWith('.css') ||
|
|
244
|
+
filename.endsWith('.svg'))
|
|
245
|
+
) {
|
|
246
|
+
console.log(`[foundation] ${filename} changed`)
|
|
247
|
+
scheduleRebuild()
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const watchers = []
|
|
252
|
+
for (const target of resolveWatchTargets(srcPath)) {
|
|
253
|
+
try {
|
|
254
|
+
watchers.push(watch(target.path, { recursive: target.recursive }, onChange))
|
|
255
|
+
} catch (err) {
|
|
256
|
+
console.warn(`[foundation] Could not watch ${target.path}:`, err.message)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (watchers.length > 0) {
|
|
261
|
+
watcher = { close: () => watchers.forEach(w => w.close()) }
|
|
202
262
|
console.log(`[foundation] Watching ${srcPath}`)
|
|
203
|
-
} catch (err) {
|
|
204
|
-
console.warn(`[foundation] Could not watch source:`, err.message)
|
|
205
263
|
}
|
|
206
264
|
}
|
|
207
265
|
},
|
package/src/generate-entry.js
CHANGED
|
@@ -22,7 +22,12 @@
|
|
|
22
22
|
import { writeFile, readFile, mkdir } from 'node:fs/promises'
|
|
23
23
|
import { existsSync } from 'node:fs'
|
|
24
24
|
import { join, dirname } from 'node:path'
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
discoverComponents,
|
|
27
|
+
discoverLayoutsInPath,
|
|
28
|
+
DEFAULT_SECTION_PATHS,
|
|
29
|
+
LAYOUTS_PATH
|
|
30
|
+
} from './schema.js'
|
|
26
31
|
import { extractAllRuntimeSchemas, extractAllLayoutRuntimeSchemas } from './runtime-schema.js'
|
|
27
32
|
import { collectSchemaRefs, buildDataSchemaMap } from './resolve-data-schema.js'
|
|
28
33
|
|
|
@@ -361,6 +366,53 @@ export async function generateEntryPoint(srcDir, outputPath = null, options = {}
|
|
|
361
366
|
}
|
|
362
367
|
}
|
|
363
368
|
|
|
369
|
+
/** Normalize native path separators so matching is platform-independent. */
|
|
370
|
+
const toPosix = path => path.replace(/\\/g, '/')
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Structural paths worth watching, given a foundation source root.
|
|
374
|
+
*
|
|
375
|
+
* Lives next to `shouldRegenerateForFile` so the watch surface and the match
|
|
376
|
+
* predicate stay in sync — anything this returns is a path that predicate can
|
|
377
|
+
* match, and nothing it matches lies outside these paths.
|
|
378
|
+
*
|
|
379
|
+
* Derived from the same discovery constants `generateEntryPoint` scans, and
|
|
380
|
+
* takes the same `sectionPaths` override, so a foundation that declares extra
|
|
381
|
+
* search paths via `defineFoundationConfig({ sections })` is watched wherever
|
|
382
|
+
* its sections actually live. Foundation folder layout is free-form — `src/`,
|
|
383
|
+
* `foundations/blog/`, `marketing/src/` — but every path here is relative to
|
|
384
|
+
* the resolved source root, so layout does not matter.
|
|
385
|
+
*
|
|
386
|
+
* Deliberately does NOT include the source root itself. Under the flat layout
|
|
387
|
+
* (`main: "./_entry.generated.js"`) the source root IS the foundation package
|
|
388
|
+
* root, so handing it to a watcher pulls in `node_modules/`, `dist/` and
|
|
389
|
+
* `.git/` — thousands of directories, and on Linux one inotify watch each.
|
|
390
|
+
*
|
|
391
|
+
* @param {string} srcDir - Foundation source directory (absolute)
|
|
392
|
+
* @param {Object} [options]
|
|
393
|
+
* @param {string[]} [options.sectionPaths] - Section search paths (relative to srcDir)
|
|
394
|
+
* @returns {string[]} Absolute paths to hand to a watcher
|
|
395
|
+
*/
|
|
396
|
+
export function getStructuralWatchPaths(srcDir, options = {}) {
|
|
397
|
+
const { sectionPaths = DEFAULT_SECTION_PATHS } = options
|
|
398
|
+
|
|
399
|
+
const rootFiles = [
|
|
400
|
+
'meta.js',
|
|
401
|
+
'main.js',
|
|
402
|
+
'main.jsx',
|
|
403
|
+
'foundation.js',
|
|
404
|
+
'foundation.jsx',
|
|
405
|
+
'styles.css',
|
|
406
|
+
'index.css'
|
|
407
|
+
]
|
|
408
|
+
|
|
409
|
+
return [
|
|
410
|
+
...sectionPaths.map(path => join(srcDir, path)),
|
|
411
|
+
join(srcDir, LAYOUTS_PATH),
|
|
412
|
+
...rootFiles.map(file => join(srcDir, file))
|
|
413
|
+
]
|
|
414
|
+
}
|
|
415
|
+
|
|
364
416
|
/**
|
|
365
417
|
* Check if a file change should trigger entry point regeneration.
|
|
366
418
|
*
|
|
@@ -370,14 +422,28 @@ export async function generateEntryPoint(srcDir, outputPath = null, options = {}
|
|
|
370
422
|
* The content-comparison guard in generateEntryPoint() makes false positives
|
|
371
423
|
* cheap (discovery runs but no write), so we err on the side of regenerating.
|
|
372
424
|
*
|
|
425
|
+
* The bare-file and entry-file rules below are scoped to the primary `sections`
|
|
426
|
+
* path on purpose: that is the only path where relaxed discovery applies.
|
|
427
|
+
* Additional paths declared via `defineFoundationConfig({ sections })` use
|
|
428
|
+
* strict discovery, where a section is only registered once it has a `meta.js`
|
|
429
|
+
* — and the "meta.js anywhere" rule already covers those. Widening the bare-file
|
|
430
|
+
* rule to every configured path would fire on files discovery ignores.
|
|
431
|
+
*
|
|
373
432
|
* @param {string} file - Absolute path of the changed file
|
|
374
433
|
* @param {string} srcDir - Foundation source directory (absolute)
|
|
375
434
|
* @returns {string|null} Reason string if regeneration needed, null otherwise
|
|
376
435
|
*/
|
|
377
436
|
export function shouldRegenerateForFile(file, srcDir) {
|
|
378
|
-
|
|
437
|
+
// Both sides arrive with native separators — chokidar normalizes the paths it
|
|
438
|
+
// emits with path.normalize() on Windows, and srcDir comes from path.resolve().
|
|
439
|
+
// Comparing against a hardcoded '/' silently matched nothing on Windows, so
|
|
440
|
+
// entry regeneration never fired there and new sections needed a dev restart.
|
|
441
|
+
const normalized = toPosix(file)
|
|
442
|
+
const base = toPosix(srcDir).replace(/\/+$/, '')
|
|
443
|
+
|
|
444
|
+
if (!normalized.startsWith(base + '/')) return null
|
|
379
445
|
|
|
380
|
-
const rel =
|
|
446
|
+
const rel = normalized.slice(base.length + 1)
|
|
381
447
|
|
|
382
448
|
// meta.js anywhere — affects runtime metadata
|
|
383
449
|
if (rel.endsWith('/meta.js') || rel === 'meta.js') {
|
package/src/hosts/ci-workflow.js
CHANGED
|
@@ -22,10 +22,28 @@
|
|
|
22
22
|
* @param {boolean} [opts.checkout] — Include the checkout step (default true).
|
|
23
23
|
* @returns {string} YAML fragment, no trailing newline.
|
|
24
24
|
*/
|
|
25
|
+
/**
|
|
26
|
+
* Fallback pnpm major, used only when a caller omits `pnpmVersion`.
|
|
27
|
+
*
|
|
28
|
+
* The CLI is the authority — it resolves the major from the project's
|
|
29
|
+
* `packageManager` field (see `versions.js::resolveCiPnpmVersion`) and
|
|
30
|
+
* always passes it, so this value does not apply on the normal path. It
|
|
31
|
+
* exists for direct API and test callers.
|
|
32
|
+
*
|
|
33
|
+
* `@uniweb/build` cannot import the CLI's constant (the CLI depends on
|
|
34
|
+
* this package, not the reverse), so this is a deliberate duplicate.
|
|
35
|
+
* Keeping ONE copy here — rather than one per adapter — is the point:
|
|
36
|
+
* the adapters previously each defaulted to '11' and drifted out of sync
|
|
37
|
+
* with the CLI when it moved to '10', leaving five stale fallbacks that
|
|
38
|
+
* would have silently generated an uninstallable workflow for any caller
|
|
39
|
+
* that omitted the argument.
|
|
40
|
+
*/
|
|
41
|
+
const FALLBACK_PNPM_VERSION = '10'
|
|
42
|
+
|
|
25
43
|
export function setupSteps({
|
|
26
44
|
packageManager = 'pnpm',
|
|
27
45
|
nodeVersion = '20',
|
|
28
|
-
pnpmVersion =
|
|
46
|
+
pnpmVersion = FALLBACK_PNPM_VERSION,
|
|
29
47
|
checkout = true,
|
|
30
48
|
} = {}) {
|
|
31
49
|
const lines = []
|
package/src/hosts/netlify.js
CHANGED
package/src/hosts/vercel.js
CHANGED
package/src/schema.js
CHANGED
|
@@ -26,8 +26,10 @@ const META_FILE_NAME = 'meta.js'
|
|
|
26
26
|
// Whichever exists at the source root is loaded.
|
|
27
27
|
const FOUNDATION_FILE_NAMES = ['main.js', 'foundation.js']
|
|
28
28
|
|
|
29
|
-
// Default paths to scan for section types (relative to srcDir)
|
|
30
|
-
|
|
29
|
+
// Default paths to scan for section types (relative to srcDir).
|
|
30
|
+
// Exported so dev-server watchers can derive their watch surface from the same
|
|
31
|
+
// value discovery uses, instead of re-typing the literal and drifting from it.
|
|
32
|
+
export const DEFAULT_SECTION_PATHS = ['sections']
|
|
31
33
|
|
|
32
34
|
// Extensions recognized as component entry files
|
|
33
35
|
const COMPONENT_EXTENSIONS = new Set(['.jsx', '.tsx', '.js', '.ts'])
|
|
@@ -36,7 +38,7 @@ const COMPONENT_EXTENSIONS = new Set(['.jsx', '.tsx', '.js', '.ts'])
|
|
|
36
38
|
const SECTIONS_PATH = 'sections'
|
|
37
39
|
|
|
38
40
|
// The layouts path where layout components are discovered
|
|
39
|
-
const LAYOUTS_PATH = 'layouts'
|
|
41
|
+
export const LAYOUTS_PATH = 'layouts'
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
44
|
* Load a meta.js file via dynamic import
|
package/src/site/config.js
CHANGED
|
@@ -23,7 +23,11 @@
|
|
|
23
23
|
import { existsSync, readFileSync } from 'node:fs'
|
|
24
24
|
import { resolve, dirname, join } from 'node:path'
|
|
25
25
|
import yaml from 'js-yaml'
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
generateEntryPoint,
|
|
28
|
+
shouldRegenerateForFile,
|
|
29
|
+
getStructuralWatchPaths
|
|
30
|
+
} from '../generate-entry.js'
|
|
27
31
|
import { importMapPlugin } from '../import-map-plugin.js'
|
|
28
32
|
import { resolveFoundationSrcPath } from '../utils/foundation-source-root.js'
|
|
29
33
|
|
|
@@ -360,11 +364,16 @@ export async function defineSiteConfig(options = {}) {
|
|
|
360
364
|
},
|
|
361
365
|
|
|
362
366
|
configureServer(server) {
|
|
363
|
-
// Watch foundation src for structural changes that affect the entry
|
|
367
|
+
// Watch foundation src for structural changes that affect the entry.
|
|
368
|
+
// Add the structural paths individually rather than the source root:
|
|
369
|
+
// under the flat layout that root is the foundation *package* root, so
|
|
370
|
+
// adding it walks node_modules/, dist/ and .git/. Vite's default ignore
|
|
371
|
+
// list filters those out today, but there is no reason to hand a package
|
|
372
|
+
// root to a watcher and depend on the filter to undo it.
|
|
364
373
|
const srcDir = resolveFoundationSrcPath(foundationInfo.path)
|
|
365
374
|
const entryPath = join(srcDir, '_entry.generated.js')
|
|
366
375
|
|
|
367
|
-
server.watcher.add(srcDir)
|
|
376
|
+
server.watcher.add(getStructuralWatchPaths(srcDir))
|
|
368
377
|
|
|
369
378
|
server.watcher.on('all', async (event, path) => {
|
|
370
379
|
const reason = shouldRegenerateForFile(path, srcDir)
|