@uniweb/build 0.14.30 → 0.14.32
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 +1 -1
- package/src/dev/plugin.js +80 -22
- package/src/generate-entry.js +69 -3
- package/src/hosts/ci-workflow.js +18 -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
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,27 @@
|
|
|
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, for callers that don't supply one.
|
|
27
|
+
*
|
|
28
|
+
* **Must equal the CLI's `versions.js::PNPM_VERSION`.** This is a copy
|
|
29
|
+
* across a package boundary — `@uniweb/build` cannot import from the CLI,
|
|
30
|
+
* since the dependency runs the other way — and copies drift. This one
|
|
31
|
+
* drifted twice in a week: stale at '11' when the CLI moved to '10', then
|
|
32
|
+
* stale at '10' when it moved back to '11'.
|
|
33
|
+
*
|
|
34
|
+
* The CLI always passes an explicit value, so this never applies on the
|
|
35
|
+
* normal path; it exists for direct API and test callers. Drift is caught
|
|
36
|
+
* by a CLI-side test that generates a workflow *without* passing a version
|
|
37
|
+
* and asserts the emitted major matches `PNPM_VERSION` — i.e. the guard
|
|
38
|
+
* lives at the boundary where the two values have to agree.
|
|
39
|
+
*/
|
|
40
|
+
const FALLBACK_PNPM_VERSION = '11'
|
|
41
|
+
|
|
25
42
|
export function setupSteps({
|
|
26
43
|
packageManager = 'pnpm',
|
|
27
44
|
nodeVersion = '20',
|
|
28
|
-
pnpmVersion =
|
|
45
|
+
pnpmVersion = FALLBACK_PNPM_VERSION,
|
|
29
46
|
checkout = true,
|
|
30
47
|
} = {}) {
|
|
31
48
|
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)
|