|
|
@@ -58,6 +58,40 @@ interface PkgMeta {
|
|
|
immediately: boolean
|
|
|
}
|
|
|
|
|
|
+/** Missing built client export, retained as structured data for activation-error grouping. */
|
|
|
+class MissingClientBundleError extends Error {
|
|
|
+ constructor(
|
|
|
+ readonly packageName: string,
|
|
|
+ readonly clientPath: string,
|
|
|
+ cause: unknown,
|
|
|
+ ) {
|
|
|
+ super(
|
|
|
+ `client-modules: ${packageName} needs to be built before source launch; client bundle not found at ${clientPath}`,
|
|
|
+ { cause },
|
|
|
+ )
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** Activation failures grouped by actionable package-build errors and unrelated failures. */
|
|
|
+class ClientPackageCompositionError extends AggregateError {
|
|
|
+ constructor(failures: Error[]) {
|
|
|
+ const missingBundles = failures.filter((error): error is MissingClientBundleError => error instanceof MissingClientBundleError)
|
|
|
+ const otherFailures = failures.filter(error => !(error instanceof MissingClientBundleError))
|
|
|
+ const packageNoun = failures.length === 1 ? 'package' : 'packages'
|
|
|
+ const lines = [`client-modules: ${String(failures.length)} client ${packageNoun} failed to compose:`]
|
|
|
+ if (missingBundles.length > 0) {
|
|
|
+ lines.push(' client packages requiring a build before source launch:')
|
|
|
+ for (const error of missingBundles) {
|
|
|
+ lines.push(` - package: ${error.packageName}`, ` path: ${error.clientPath}`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (otherFailures.length > 0) {
|
|
|
+ lines.push(' other failures:', ...otherFailures.map(error => ` - ${error.message}`))
|
|
|
+ }
|
|
|
+ super(failures, lines.join('\n'))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
/** One composed table row: the wire entry plus its bundle path. */
|
|
|
interface WebPluginRecord {
|
|
|
entry: WebBootEntry
|
|
|
@@ -138,7 +172,7 @@ export function injectBootManifest(html: string, graph: WebBootGraph): string {
|
|
|
* + bundle route + index tap. Construction runs the activation scan
|
|
|
* synchronously — a malformed declaration or missing bundle among the
|
|
|
* already-loaded entries aggregates into one loud throw (FAILED fiber; the
|
|
|
- * boot sweep reports it).
|
|
|
+ * boot activation audit reports it).
|
|
|
*/
|
|
|
export class ClientModuleHostService extends Service {
|
|
|
static inject = ['httpServer', 'loader']
|
|
|
@@ -194,10 +228,7 @@ export class ClientModuleHostService extends Service {
|
|
|
const failures: Error[] = []
|
|
|
this.flush(err => failures.push(err))
|
|
|
if (failures.length > 0) {
|
|
|
- throw new AggregateError(
|
|
|
- failures,
|
|
|
- `client-modules: ${String(failures.length)} client package(s) failed to compose:\n${failures.map(e => ` - ${e.message}`).join('\n')}`,
|
|
|
- )
|
|
|
+ throw new ClientPackageCompositionError(failures)
|
|
|
}
|
|
|
|
|
|
ctx.effect(
|
|
|
@@ -322,6 +353,16 @@ export class ClientModuleHostService extends Service {
|
|
|
return meta
|
|
|
}
|
|
|
|
|
|
+ /** Read the activation-time bundle revision, translating only a missing build artifact into source-launch guidance. */
|
|
|
+ private initialBundleRevision(pkgName: string, clientPath: string): string {
|
|
|
+ try {
|
|
|
+ return shortHash(readFileSync(clientPath))
|
|
|
+ } catch (error) {
|
|
|
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
|
|
+ throw new MissingClientBundleError(pkgName, clientPath, error)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/** Reconcile one entry name against the live loader entries. @returns whether the table changed. */
|
|
|
private processOne(entryName: string): boolean {
|
|
|
let qualifies = false
|
|
|
@@ -337,7 +378,7 @@ export class ClientModuleHostService extends Service {
|
|
|
if (meta === null) return false
|
|
|
// The rev rides the row from here on: a fiber restart reuses the row (and
|
|
|
// its rev) untouched; only rebuilt() re-reads the bundle.
|
|
|
- const rev = shortHash(readFileSync(meta.clientPath))
|
|
|
+ const rev = this.initialBundleRevision(entryName, meta.clientPath)
|
|
|
this.table.set(entryName, { entry: graphRow(entryName, rev, meta.inject, meta.immediately), clientPath: meta.clientPath })
|
|
|
return true
|
|
|
}
|