@ultimat3/cli 24.0.0 → 25.0.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/CLAUDE.md +15 -8
- package/README.md +30 -5
- package/package.json +29 -29
- package/src/admin-catalog-keys.ts +2 -2
- package/src/affected.ts +3 -1
- package/src/app-boundaries.ts +17 -4
- package/src/app-config-load.ts +3 -3
- package/src/app-env.ts +21 -14
- package/src/app-load.ts +38 -14
- package/src/app-manifest.ts +2 -2
- package/src/app-mcp.ts +25 -27
- package/src/app-name.ts +52 -0
- package/src/app-permissions-borrowed.ts +2 -8
- package/src/app-permissions-site.ts +31 -7
- package/src/app-root.ts +9 -5
- package/src/bin.ts +3 -7
- package/src/browser-launcher.ts +4 -2
- package/src/budgets-charged.ts +60 -0
- package/src/budgets-scan.ts +76 -0
- package/src/budgets-unmeasured.ts +91 -0
- package/src/budgets.ts +115 -121
- package/src/cdp-shot-driver.ts +17 -0
- package/src/cdp-shot-watch.ts +35 -31
- package/src/ci-log.ts +0 -0
- package/src/cmd-auth.ts +2 -2
- package/src/cmd-build-spec.ts +7 -1
- package/src/cmd-build.ts +89 -9
- package/src/cmd-db-backfill.ts +1 -1
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db.ts +5 -2
- package/src/cmd-deploy-compose.ts +94 -0
- package/src/cmd-deploy-drain.ts +48 -0
- package/src/cmd-deploy-start-first.ts +178 -0
- package/src/cmd-deploy.ts +158 -36
- package/src/cmd-dev.ts +18 -357
- package/src/cmd-doctor.ts +16 -66
- package/src/cmd-env.ts +29 -14
- package/src/cmd-generate.ts +12 -2
- package/src/cmd-i18n.ts +11 -8
- package/src/cmd-jobs-spec.ts +7 -22
- package/src/cmd-jobs.ts +18 -137
- package/src/cmd-mcp.ts +25 -7
- package/src/cmd-new.ts +36 -8
- package/src/cmd-planned.ts +9 -0
- package/src/cmd-routes.ts +3 -3
- package/src/cmd-shot-matrix.ts +21 -4
- package/src/cmd-shot-spec.ts +10 -2
- package/src/cmd-shot.ts +35 -39
- package/src/cmd-test.ts +1 -1
- package/src/coverage-lcov.ts +4 -2
- package/src/db-backfill.ts +2 -2
- package/src/db-replay-engine.ts +4 -10
- package/src/db-subscribes.ts +19 -16
- package/src/dev-boot.ts +290 -0
- package/src/dev-child-watch.ts +63 -12
- package/src/dev-dashboard-guard.ts +1 -1
- package/src/dev-dashboard.ts +5 -1
- package/src/dev-lock.ts +96 -24
- package/src/dev-rebuild.ts +80 -0
- package/src/dev-restart-retire.ts +46 -0
- package/src/dev-route-table.ts +30 -8
- package/src/dev-supervisor.ts +114 -22
- package/src/dev-traces.ts +53 -20
- package/src/dispatch.ts +14 -7
- package/src/doctor-auth.ts +4 -4
- package/src/doctor-offline.ts +6 -5
- package/src/document-options.ts +7 -0
- package/src/drift-replica-identity.ts +2 -2
- package/src/drift.ts +29 -6
- package/src/error-codes.ts +6 -0
- package/src/error-page-csp.ts +14 -15
- package/src/exec.ts +8 -0
- package/src/favicon.ts +2 -2
- package/src/fix-command.ts +9 -1
- package/src/framework-env.ts +148 -0
- package/src/framework-schema.ts +13 -2
- package/src/generate-catalog-locales.ts +79 -0
- package/src/generate-kinds.ts +3 -17
- package/src/generate-plural.ts +50 -0
- package/src/generate-shadow.ts +50 -1
- package/src/hold.ts +56 -1
- package/src/i18n-audit.ts +21 -8
- package/src/i18n-index.ts +5 -6
- package/src/i18n-registration.ts +7 -9
- package/src/import-scan.ts +28 -3
- package/src/index.ts +1 -3
- package/src/island-bundle.ts +54 -64
- package/src/island-capture.ts +1 -1
- package/src/island-duplicate-refusal.ts +57 -0
- package/src/island-duplicates.ts +221 -0
- package/src/island-entry-missing.ts +64 -0
- package/src/island-harness-route.ts +20 -4
- package/src/island-harness-script.ts +23 -2
- package/src/island-identity.ts +17 -14
- package/src/island-package-dedupe.ts +150 -0
- package/src/island-realtime.ts +36 -16
- package/src/island-routes.ts +2 -2
- package/src/island-runtime.ts +241 -0
- package/src/island-sources.ts +105 -0
- package/src/island-states-load.ts +6 -0
- package/src/island-store.ts +90 -6
- package/src/jobs-json.ts +0 -26
- package/src/jobs-report.ts +32 -16
- package/src/local-cli-handoff.ts +90 -0
- package/src/mcp-errors.ts +8 -1
- package/src/mcp-host.ts +6 -5
- package/src/mcp-ui-inspect.ts +3 -2
- package/src/mcp-ui-interact.ts +3 -2
- package/src/mcp-ui.ts +8 -7
- package/src/measure-scope.ts +15 -10
- package/src/messages.ts +27 -16
- package/src/metrics-endpoint.ts +3 -2
- package/src/migration-extensions.ts +1 -1
- package/src/otlp-export.ts +2 -2
- package/src/output.ts +18 -0
- package/src/page-speculation.ts +14 -11
- package/src/parse.ts +0 -8
- package/src/permission-grants.ts +3 -3
- package/src/posix-path.ts +30 -0
- package/src/prerender-locales.ts +6 -4
- package/src/prerender-report.ts +58 -0
- package/src/prerender-served.ts +52 -0
- package/src/prerender.ts +63 -98
- package/src/pwa-artifacts.ts +8 -1
- package/src/pwa-manifest-locales.ts +9 -9
- package/src/retry-memo.ts +1 -1
- package/src/role-realtime.ts +7 -1
- package/src/role-replicator.ts +110 -31
- package/src/role-start.ts +64 -85
- package/src/role-sync.ts +65 -6
- package/src/root-env.ts +157 -28
- package/src/route-islands.ts +69 -0
- package/src/runtime-admin.ts +4 -1
- package/src/runtime-assets.ts +15 -2
- package/src/runtime-bindings.ts +23 -3
- package/src/runtime-cache.ts +8 -8
- package/src/runtime-hooks.ts +2 -1
- package/src/runtime-isr-outcome.ts +100 -0
- package/src/runtime-isr.ts +2 -2
- package/src/runtime-jobs.ts +78 -18
- package/src/runtime-live-feed.ts +1 -1
- package/src/runtime-mail.ts +26 -0
- package/src/runtime-mfa-warning.ts +2 -2
- package/src/runtime-overrides.ts +3 -3
- package/src/runtime-purge.ts +1 -1
- package/src/runtime-queue.ts +12 -12
- package/src/runtime-realtime.ts +5 -3
- package/src/runtime-render.ts +77 -82
- package/src/runtime-replica.ts +3 -8
- package/src/runtime-services.ts +33 -13
- package/src/runtime-storage-upload.ts +2 -2
- package/src/runtime-storage.ts +3 -3
- package/src/schema-diff.ts +1 -1
- package/src/secrets-rotation.ts +8 -9
- package/src/seo-routes.ts +2 -2
- package/src/serve-boot.ts +22 -11
- package/src/serve-drain.ts +3 -1
- package/src/serve-retire.ts +95 -0
- package/src/serve-web.ts +19 -8
- package/src/serve.ts +27 -10
- package/src/server-barrels.ts +6 -3
- package/src/shot-browser.ts +1 -1
- package/src/shot-locale.ts +6 -15
- package/src/shot-settle.ts +3 -3
- package/src/shot-theme.ts +1 -1
- package/src/shot-viewport.ts +152 -0
- package/src/signal-shred.ts +26 -12
- package/src/site-asset-routes.ts +2 -2
- package/src/site-seo.ts +6 -4
- package/src/source-files.ts +13 -3
- package/src/static-report.ts +5 -0
- package/src/style-routes.ts +2 -2
- package/src/sw-artifacts.ts +8 -8
- package/src/sw-precache-plan.ts +34 -4
- package/src/sync-authenticator.ts +3 -3
- package/src/sync-url.ts +1 -1
- package/src/templates/action.ts +4 -4
- package/src/templates/backfill.ts +4 -4
- package/src/templates/entity.ts +2 -2
- package/src/templates/github/ci.yml.ts +28 -18
- package/src/templates/github/image.yml.ts +92 -0
- package/src/templates/github/runner.ts +18 -0
- package/src/templates/guard-bare-error.ts +16 -2
- package/src/templates/guard-unzoned-date.ts +56 -6
- package/src/templates/index.ts +1 -1
- package/src/templates/job-body-test.ts +2 -2
- package/src/templates/job.ts +6 -6
- package/src/templates/query.ts +9 -8
- package/src/templates/resource-page.ts +9 -7
- package/src/templates/resource-service.ts +2 -2
- package/src/templates/route.ts +23 -8
- package/src/templates/scaffold-api.ts +2 -3
- package/src/templates/scaffold-app.ts +2 -1
- package/src/templates/scaffold-auth.ts +205 -70
- package/src/templates/scaffold-bin.ts +122 -0
- package/src/templates/scaffold-claude-commands.ts +11 -11
- package/src/templates/scaffold-container.ts +12 -6
- package/src/templates/scaffold-dashboard-bare.ts +8 -4
- package/src/templates/scaffold-dashboard-example-tests.ts +208 -0
- package/src/templates/scaffold-dashboard-example.ts +112 -172
- package/src/templates/scaffold-dashboard-shared.ts +62 -1
- package/src/templates/scaffold-dashboard.ts +9 -5
- package/src/templates/scaffold-db-package.ts +2 -2
- package/src/templates/scaffold-docs.ts +20 -77
- package/src/templates/scaffold-domain-package.ts +0 -4
- package/src/templates/scaffold-entries.ts +4 -3
- package/src/templates/scaffold-env.ts +6 -3
- package/src/templates/scaffold-gitattributes.ts +59 -0
- package/src/templates/scaffold-helm-templates.ts +74 -18
- package/src/templates/scaffold-helm.ts +31 -4
- package/src/templates/scaffold-http.ts +1 -1
- package/src/templates/scaffold-i18n.ts +15 -2
- package/src/templates/scaffold-icon.ts +2 -2
- package/src/templates/scaffold-repo.ts +18 -15
- package/src/templates/scaffold-shell.ts +17 -2
- package/src/test-only-shipped.ts +193 -0
- package/src/test-select.ts +2 -1
- package/src/test-shards.ts +1 -4
- package/src/theme-boot.ts +2 -2
- package/src/theme-brand.ts +66 -0
- package/src/trusted-hops.ts +49 -0
- package/src/unscanned-admin.ts +2 -2
- package/src/verify-checks.ts +12 -2
- package/src/verify-coverage-run.ts +3 -2
- package/src/verify-deadline.ts +87 -10
- package/src/verify-merge.ts +48 -16
- package/src/verify-part-step.ts +10 -2
- package/src/verify-role-load-probe.ts +12 -2
- package/src/verify-role-load.ts +9 -4
- package/src/verify-run.ts +3 -0
- package/src/verify-step.ts +2 -0
- package/src/verify-test-run.ts +3 -2
- package/src/verify-tests.ts +0 -1
- package/src/worker-bundle.ts +2 -2
- package/src/workspace-checks.ts +53 -7
- package/src/workspace-graph.ts +3 -2
- package/src/write-line.ts +82 -7
- package/src/browser-launcher-fake-html.ts +0 -80
- package/src/browser-launcher-fake.ts +0 -168
- package/src/jobs-drain.ts +0 -230
- package/src/scaffold-typecheck.ts +0 -227
- package/src/thrown-by.ts +0 -24
package/src/i18n-registration.ts
CHANGED
|
@@ -18,12 +18,13 @@ import {
|
|
|
18
18
|
pluralVariantsOf,
|
|
19
19
|
registeredLocales,
|
|
20
20
|
} from '@ultimat3/i18n';
|
|
21
|
+
import { APP_CATALOGS_PATH } from '@ultimat3/i18n/app-catalogs';
|
|
21
22
|
import { mountedAdminKeys } from './admin-catalog-keys';
|
|
23
|
+
import type { LoadedApp } from './app-load';
|
|
22
24
|
import { loadApp } from './app-load';
|
|
23
25
|
import type { DuplicateProbe } from './duplicate-packages';
|
|
24
26
|
import { duplicateCause, duplicateFinding, findDuplicateInstalls } from './duplicate-packages';
|
|
25
27
|
import { auditApp } from './i18n-audit';
|
|
26
|
-
import { I18N_INDEX_PATH } from './i18n-index';
|
|
27
28
|
import type { Finding } from './output';
|
|
28
29
|
import { findingFrom } from './output';
|
|
29
30
|
import { CATALOG_ROOT, catalogPath } from './templates/locales';
|
|
@@ -37,10 +38,7 @@ const I18N_PKG = '@ultimat3/i18n';
|
|
|
37
38
|
* can resolve `@ultimat3/*`. `loadApp` is the production value, and it is the same call
|
|
38
39
|
* `serveApp` makes: asking a different loader than the server uses would prove nothing.
|
|
39
40
|
*/
|
|
40
|
-
export type AppLoader = (root: string) => Promise<
|
|
41
|
-
readonly findings: readonly Finding[];
|
|
42
|
-
readonly defaultLocale: string;
|
|
43
|
-
}>;
|
|
41
|
+
export type AppLoader = (root: string) => Promise<Pick<LoadedApp, 'findings' | 'locales'>>;
|
|
44
42
|
|
|
45
43
|
export interface RegistrationInput {
|
|
46
44
|
readonly root: string;
|
|
@@ -121,9 +119,9 @@ export async function checkRegistration(input: RegistrationInput): Promise<Regis
|
|
|
121
119
|
let locales = gaps.length;
|
|
122
120
|
|
|
123
121
|
if (Object.keys(input.catalogs).length === 0) {
|
|
124
|
-
const unresolved = unresolvedUsedKeys(input, app.defaultLocale);
|
|
122
|
+
const unresolved = unresolvedUsedKeys(input, app.locales.defaultLocale);
|
|
125
123
|
if (unresolved.length > 0) {
|
|
126
|
-
findings.push(findingFrom(catalogsNeverRegistered(app.defaultLocale, unresolved)));
|
|
124
|
+
findings.push(findingFrom(catalogsNeverRegistered(app.locales.defaultLocale, unresolved)));
|
|
127
125
|
unregistered += unresolved.length;
|
|
128
126
|
locales += 1;
|
|
129
127
|
}
|
|
@@ -182,7 +180,7 @@ function adminKeysFinding(locale: Locale, missing: readonly string[]): Finding {
|
|
|
182
180
|
* the half that knows what an app's directories are.
|
|
183
181
|
*/
|
|
184
182
|
async function indexSource(root: string): Promise<string | undefined> {
|
|
185
|
-
const file = Bun.file(join(root,
|
|
183
|
+
const file = Bun.file(join(root, APP_CATALOGS_PATH));
|
|
186
184
|
return (await file.exists()) ? file.text() : undefined;
|
|
187
185
|
}
|
|
188
186
|
|
|
@@ -208,7 +206,7 @@ export function unregisteredFix(
|
|
|
208
206
|
// report a registered locale as unregistered, which is the direction that costs trust.
|
|
209
207
|
if (index.includes(`${CATALOG_ROOT.split('/').pop() ?? 'catalogs'}/${locale}.json`)) return {};
|
|
210
208
|
return {
|
|
211
|
-
fix: `x i18n sync ${locale} # re-derives ${
|
|
209
|
+
fix: `x i18n sync ${locale} # re-derives ${APP_CATALOGS_PATH} from the catalogs on disk`,
|
|
212
210
|
};
|
|
213
211
|
}
|
|
214
212
|
|
package/src/import-scan.ts
CHANGED
|
@@ -42,12 +42,36 @@ export function scanRuntimeImports(file: ScannedFile): readonly string[] {
|
|
|
42
42
|
.map((entry) => entry.path);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* A type-position `import('…')` — `typeof import('@ultimat3/cli')`, `let x: import('a').B` — is no
|
|
47
|
+
* import statement, so the transpiler erases it whole and neither pass above sees it (K16: the tier
|
|
48
|
+
* rule passed one). Read off `masked`, the source with comments and string CONTENTS blanked and
|
|
49
|
+
* delimiters kept (`@ultimat3/core`'s `maskLiterals`): a match there is code, and the specifier is
|
|
50
|
+
* read back from `source` at the same offsets. Taken as an argument because this file is a leaf.
|
|
51
|
+
* A dynamic `import(x)` is reported too, which the runtime pass already holds — callers dedupe.
|
|
52
|
+
*/
|
|
53
|
+
const IMPORT_CALL = /(?<![\w$.])import\s*\(\s*(['"])/g;
|
|
54
|
+
|
|
55
|
+
export function typePositionImports(source: string, masked: string): readonly string[] {
|
|
56
|
+
const found: string[] = [];
|
|
57
|
+
for (const match of masked.matchAll(IMPORT_CALL)) {
|
|
58
|
+
const quote = match[1] as string;
|
|
59
|
+
const open = match.index + match[0].length;
|
|
60
|
+
const close = masked.indexOf(quote, open);
|
|
61
|
+
if (close === -1) continue;
|
|
62
|
+
const specifier = source.slice(open, close);
|
|
63
|
+
if (specifier !== '' && !specifier.includes('\n')) found.push(specifier);
|
|
64
|
+
}
|
|
65
|
+
return found;
|
|
66
|
+
}
|
|
67
|
+
|
|
45
68
|
/**
|
|
46
69
|
* Every specifier the file names, type-only ones included. Throws when the file does not parse —
|
|
47
70
|
* a scan that answered `[]` there would read as a file importing nothing. The rewrite pass alone
|
|
48
|
-
* is forgiving: a rewrite the parser refuses falls back to the runtime scan.
|
|
71
|
+
* is forgiving: a rewrite the parser refuses falls back to the runtime scan. Hand it `masked`
|
|
72
|
+
* (core's `maskLiterals(file.source)`) and a type-position `import('…')` is read as well.
|
|
49
73
|
*/
|
|
50
|
-
export function scanAllImports(file: ScannedFile): readonly string[] {
|
|
74
|
+
export function scanAllImports(file: ScannedFile, masked?: string): readonly string[] {
|
|
51
75
|
const runtime = scanRuntimeImports(file);
|
|
52
76
|
let typed: readonly string[] = [];
|
|
53
77
|
try {
|
|
@@ -58,6 +82,7 @@ export function scanAllImports(file: ScannedFile): readonly string[] {
|
|
|
58
82
|
} catch {
|
|
59
83
|
typed = [];
|
|
60
84
|
}
|
|
85
|
+
const inTypes = masked === undefined ? [] : typePositionImports(file.source, masked);
|
|
61
86
|
// The rewritten pass first: it holds every import in SOURCE order, which callers report in.
|
|
62
|
-
return [...new Set([...typed, ...runtime])];
|
|
87
|
+
return [...new Set([...typed, ...inTypes, ...runtime])];
|
|
63
88
|
}
|
package/src/index.ts
CHANGED
|
@@ -115,7 +115,6 @@ export { ICON_BASE_PATH, ICON_SOURCE } from './icon-assets';
|
|
|
115
115
|
// `x build`'s and `x dev`'s wiring, and every name here is a semver promise forever.
|
|
116
116
|
export type { IslandChunk, SharedChunk } from './island-bundle';
|
|
117
117
|
export { buildIslands } from './island-bundle';
|
|
118
|
-
export { JOB_STATES } from './jobs-report';
|
|
119
118
|
export { renderJobTable } from './jobs-table';
|
|
120
119
|
export { msg } from './messages';
|
|
121
120
|
export { DEFAULT_METRICS_PORT, startMetricsEndpoint } from './metrics-endpoint';
|
|
@@ -123,7 +122,7 @@ export { parseMigrationSql, readMigrations } from './migrations';
|
|
|
123
122
|
export type { CommandResult, Finding, JsonValue } from './output';
|
|
124
123
|
export { exitCodeFor, findingFrom, render, renderFinding, renderHuman } from './output';
|
|
125
124
|
export type { CommandSpec, FlagSpec, ParsedArgs } from './parse';
|
|
126
|
-
export { flagBool, flagList, flagString, GLOBAL_FLAGS,
|
|
125
|
+
export { flagBool, flagList, flagString, GLOBAL_FLAGS, parseArgs } from './parse';
|
|
127
126
|
export type { PrerenderedPage, PrerenderReport } from './prerender';
|
|
128
127
|
export { DEFAULT_ORIGIN, isPrerenderable, prerenderSite } from './prerender';
|
|
129
128
|
export type { PwaArtifacts } from './pwa-artifacts';
|
|
@@ -169,7 +168,6 @@ export { readVerifyFloor, skippedSuiteFinding, VERIFY_FLOOR_FILE } from './verif
|
|
|
169
168
|
export type { HostCheck, StepOutcome, VerifyStep, VerifyStepName } from './verify-step';
|
|
170
169
|
export { VERIFY_STEP_NAMES } from './verify-step';
|
|
171
170
|
export type { TestType } from './verify-tests';
|
|
172
|
-
export { TEST_TYPES } from './verify-tests';
|
|
173
171
|
export {
|
|
174
172
|
checkFileSizes,
|
|
175
173
|
checkLockstep,
|
package/src/island-bundle.ts
CHANGED
|
@@ -15,25 +15,32 @@
|
|
|
15
15
|
// `globalThis` object created lazily by the first transport call or realtime hook. Measured before
|
|
16
16
|
// that decision on `examples/dummy`, a core-importing wrapper cost contact-sales 875 → 8,827 B.
|
|
17
17
|
// What IS prepended is the realtime install, and only where the island's graph reaches
|
|
18
|
-
// `@ultimat3/realtime` (`island-realtime.ts`)
|
|
19
|
-
//
|
|
18
|
+
// `@ultimat3/realtime` (`island-realtime.ts`); the page runtime it awaits is ONE more output,
|
|
19
|
+
// `page-runtime.<identity>.js` beside the entries (`island-runtime.ts`), never inlined into one (#505).
|
|
20
20
|
|
|
21
21
|
// Bun ships no path API. `posix` does the specifier arithmetic (an app-relative route file is
|
|
22
22
|
// POSIX by construction), `join`/`basename` the filesystem side.
|
|
23
23
|
import { basename, join, posix, relative, sep } from 'node:path';
|
|
24
|
-
import { ISLAND_EXTENSION,
|
|
24
|
+
import { ISLAND_EXTENSION, islandModuleId } from '@ultimat3/render';
|
|
25
25
|
import { loadAppConfig } from './app-config-load';
|
|
26
26
|
import { IslandBuildFailedError } from './errors';
|
|
27
|
+
import { refuseDuplicatedModules } from './island-duplicate-refusal';
|
|
28
|
+
import { entryMissing, onlyMissing } from './island-entry-missing';
|
|
27
29
|
import { describeBuildError, sourcesContentOf, stableChunk, stripDebugId } from './island-identity';
|
|
28
30
|
import type { BuiltOutput, LinkedFile } from './island-link';
|
|
29
31
|
import { linkOutputs, sharedChunkName } from './island-link';
|
|
32
|
+
import { frameworkDedupePlugins } from './island-package-dedupe';
|
|
30
33
|
import {
|
|
31
34
|
islandRealtimePlugin,
|
|
32
35
|
REALTIME_ISLAND_ENTRY,
|
|
33
36
|
reachesRealtime,
|
|
34
37
|
realtimeIslandEntry,
|
|
35
38
|
} from './island-realtime';
|
|
39
|
+
import type { Runtimes } from './island-runtime';
|
|
40
|
+
import { loadingRuntime, mergeBuilds, runtimeBuilder, runtimeShared } from './island-runtime';
|
|
36
41
|
import { solidDedupePlugin } from './island-solid-dedupe';
|
|
42
|
+
import type { SourcePaths } from './island-sources';
|
|
43
|
+
import { sourcesOnDisk } from './island-sources';
|
|
37
44
|
import { islandStylesPlugin } from './island-styles';
|
|
38
45
|
import { hasPathSegment } from './path-segments';
|
|
39
46
|
import { solidJsxPlugin } from './solid-loader';
|
|
@@ -70,6 +77,8 @@ export interface IslandChunk {
|
|
|
70
77
|
* beyond `bytes`, what a precached page must carry with it. Empty for an island sharing nothing.
|
|
71
78
|
*/
|
|
72
79
|
readonly imports: readonly string[];
|
|
80
|
+
/** The files on disk it was built from (`island-sources.ts`). Absent on a chunk not built here. */
|
|
81
|
+
readonly sources?: SourcePaths;
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
/** A module two or more islands import, served once beside them. */
|
|
@@ -80,6 +89,7 @@ export interface SharedChunk {
|
|
|
80
89
|
readonly bytes: number;
|
|
81
90
|
/** The islands (app-root-relative) that load it — for a finding that names what an author edits. */
|
|
82
91
|
readonly importers: readonly string[];
|
|
92
|
+
readonly sources?: SourcePaths;
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
export interface IslandBundle {
|
|
@@ -119,6 +129,7 @@ async function buildAll(
|
|
|
119
129
|
root: string,
|
|
120
130
|
files: readonly string[],
|
|
121
131
|
splitting: boolean,
|
|
132
|
+
runtimes: Runtimes,
|
|
122
133
|
): Promise<{ readonly chunks: readonly IslandChunk[]; readonly shared: readonly SharedChunk[] }> {
|
|
123
134
|
if (files.length === 0) return { chunks: [], shared: [] };
|
|
124
135
|
// Inside the refusal: the realtime probe PARSES the island's graph, and a file that will not
|
|
@@ -134,6 +145,7 @@ async function buildAll(
|
|
|
134
145
|
// Only an island whose own graph reaches `@ultimat3/realtime` is wrapped (`island-realtime.ts`);
|
|
135
146
|
// every other one is built from its own file, byte for byte what it was.
|
|
136
147
|
const live = files.filter((_, index) => realtime[index] === true);
|
|
148
|
+
const runtime = live[0] === undefined ? undefined : await runtimes(live[0]);
|
|
137
149
|
const entrypoints = files.map((file, index) =>
|
|
138
150
|
realtime[index] === true ? realtimeIslandEntry(file) : join(root, file),
|
|
139
151
|
);
|
|
@@ -161,10 +173,13 @@ async function buildAll(
|
|
|
161
173
|
//
|
|
162
174
|
// The dedupe goes FIRST: it answers `solid-js` specifiers before either plugin loads a file,
|
|
163
175
|
// so the `solid-js/web` helpers the JSX transform writes into a symlinked package resolve
|
|
164
|
-
// to the app's one copy. See `island-solid-dedupe.ts` for the measurement.
|
|
176
|
+
// to the app's one copy. See `island-solid-dedupe.ts` for the measurement. `@ultimat3/*`
|
|
177
|
+
// gets the same rule (`island-package-dedupe.ts`): a nested copy is a second module where no
|
|
178
|
+
// symlink folds it, which is every `file:` install on Windows.
|
|
165
179
|
plugins: [
|
|
166
|
-
...(
|
|
180
|
+
...(runtime === undefined ? [] : [islandRealtimePlugin(root, runtime)]),
|
|
167
181
|
solidDedupePlugin(root),
|
|
182
|
+
...frameworkDedupePlugins(root),
|
|
168
183
|
solidJsxPlugin,
|
|
169
184
|
islandStylesPlugin,
|
|
170
185
|
],
|
|
@@ -187,9 +202,13 @@ async function buildAll(
|
|
|
187
202
|
// output has. See `graphHash`. Measured on 1.4.0 against a 131 kB island: 277ms with it and
|
|
188
203
|
// 276ms without, so the map costs nothing worth naming.
|
|
189
204
|
sourcemap: 'external',
|
|
205
|
+
// The fifth: which inputs each output carries, so one module bundled twice is refused below
|
|
206
|
+
// rather than shipped (`island-duplicates.ts`). Measured on 1.4.2 against examples/dummy's
|
|
207
|
+
// feed island, five interleaved builds each way: 44 ms without, 48 ms with, noise-level.
|
|
208
|
+
metafile: true,
|
|
190
209
|
});
|
|
191
210
|
} catch (error) {
|
|
192
|
-
throw await blame(root, files, error, splitting);
|
|
211
|
+
throw await blame(root, files, error, splitting, runtimes);
|
|
193
212
|
}
|
|
194
213
|
if (!built.success) {
|
|
195
214
|
throw new IslandBuildFailedError({
|
|
@@ -197,9 +216,18 @@ async function buildAll(
|
|
|
197
216
|
logs: built.logs.map((log) => String(log)).join('; '),
|
|
198
217
|
});
|
|
199
218
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
219
|
+
if (built.metafile === undefined) {
|
|
220
|
+
throw new IslandBuildFailedError({
|
|
221
|
+
file: files.join(', '),
|
|
222
|
+
logs: 'the bundler returned no metafile',
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
await refuseDuplicatedModules(files.join(', '), built.metafile);
|
|
226
|
+
const sources = new Map<string, SourcePaths>();
|
|
227
|
+
const linked = linkOutputs(await builtOutputs(root, files, built.outputs, sources));
|
|
228
|
+
const chunks = loadingRuntime(entryChunks(files, linked, sources), live, runtime);
|
|
229
|
+
const shared = sharedChunks(chunks, linked, sources);
|
|
230
|
+
return { chunks, shared: [...shared, ...runtimeShared(runtime, live)] };
|
|
203
231
|
}
|
|
204
232
|
|
|
205
233
|
/**
|
|
@@ -212,11 +240,12 @@ async function blame(
|
|
|
212
240
|
files: readonly string[],
|
|
213
241
|
error: unknown,
|
|
214
242
|
splitting: boolean,
|
|
243
|
+
runtimes: Runtimes,
|
|
215
244
|
): Promise<IslandBuildFailedError> {
|
|
216
245
|
if (files.length > 1) {
|
|
217
246
|
for (const file of files) {
|
|
218
247
|
try {
|
|
219
|
-
await buildAll(root, [file], splitting);
|
|
248
|
+
await buildAll(root, [file], splitting, runtimes);
|
|
220
249
|
} catch (alone) {
|
|
221
250
|
if (alone instanceof IslandBuildFailedError) return alone;
|
|
222
251
|
}
|
|
@@ -241,8 +270,10 @@ const withoutExtension = (file: string): string =>
|
|
|
241
270
|
|
|
242
271
|
/** Every code output paired with its source map's inputs and, for an entry, its island. */
|
|
243
272
|
async function builtOutputs(
|
|
273
|
+
root: string,
|
|
244
274
|
files: readonly string[],
|
|
245
275
|
artifacts: readonly Bun.BuildArtifact[],
|
|
276
|
+
sourcePaths: Map<string, SourcePaths>,
|
|
246
277
|
): Promise<readonly BuiltOutput[]> {
|
|
247
278
|
const byStem = new Map(files.map((file) => [withoutExtension(file), file]));
|
|
248
279
|
const maps = new Map<string, string>();
|
|
@@ -255,7 +286,9 @@ async function builtOutputs(
|
|
|
255
286
|
const key = outputKey(artifact.path);
|
|
256
287
|
const file = artifact.kind === 'entry-point' ? byStem.get(withoutExtension(key)) : undefined;
|
|
257
288
|
const map = maps.get(`${artifact.path}.map`);
|
|
258
|
-
const
|
|
289
|
+
const parsed: unknown = map === undefined ? undefined : JSON.parse(map);
|
|
290
|
+
const sources = parsed === undefined ? undefined : sourcesContentOf(parsed, true);
|
|
291
|
+
sourcePaths.set(key, await sourcesOnDisk(root, parsed));
|
|
259
292
|
if (sources === undefined || (artifact.kind === 'entry-point' && file === undefined)) {
|
|
260
293
|
throw new IslandBuildFailedError({
|
|
261
294
|
file: file ?? key,
|
|
@@ -276,6 +309,7 @@ async function builtOutputs(
|
|
|
276
309
|
function entryChunks(
|
|
277
310
|
files: readonly string[],
|
|
278
311
|
linked: readonly LinkedFile[],
|
|
312
|
+
sources: ReadonlyMap<string, SourcePaths>,
|
|
279
313
|
): readonly IslandChunk[] {
|
|
280
314
|
const shared = new Map(
|
|
281
315
|
linked
|
|
@@ -311,6 +345,7 @@ function entryChunks(
|
|
|
311
345
|
// different files at one address. `bytes` is measured on THAT code, never on this build's.
|
|
312
346
|
...stableChunk(file, entry.identity, entry.code),
|
|
313
347
|
imports: closure(entry.imports),
|
|
348
|
+
sources: sources.get(entry.path) ?? [],
|
|
314
349
|
});
|
|
315
350
|
}
|
|
316
351
|
return chunks;
|
|
@@ -320,6 +355,7 @@ function entryChunks(
|
|
|
320
355
|
function sharedChunks(
|
|
321
356
|
chunks: readonly IslandChunk[],
|
|
322
357
|
linked: readonly LinkedFile[],
|
|
358
|
+
sources: ReadonlyMap<string, SourcePaths>,
|
|
323
359
|
): readonly SharedChunk[] {
|
|
324
360
|
const out: SharedChunk[] = [];
|
|
325
361
|
for (const one of linked) {
|
|
@@ -332,6 +368,7 @@ function sharedChunks(
|
|
|
332
368
|
.filter((chunk) => chunk.imports.includes(url))
|
|
333
369
|
.map((chunk) => chunk.file)
|
|
334
370
|
.sort(),
|
|
371
|
+
sources: sources.get(one.path) ?? [],
|
|
335
372
|
});
|
|
336
373
|
}
|
|
337
374
|
return out.sort((a, b) => (a.url < b.url ? -1 : a.url > b.url ? 1 : 0));
|
|
@@ -373,43 +410,15 @@ export async function buildIslands(
|
|
|
373
410
|
// an empty bundle here would surface two steps later, as a chunk table with no entry for a file
|
|
374
411
|
// the caller can see on disk.
|
|
375
412
|
if (only !== undefined && files.length === 0) throw onlyMissing(only, discovered);
|
|
413
|
+
const runtimes = runtimeBuilder(root, ISLAND_BASE_PATH);
|
|
376
414
|
if (options.sharedChunks ?? (await loadSharedChunks(root))) {
|
|
377
|
-
const built = await buildAll(root, files, true);
|
|
415
|
+
const built = await buildAll(root, files, true, runtimes);
|
|
378
416
|
return islandBundle(built.chunks, built.shared);
|
|
379
417
|
}
|
|
380
|
-
// One build per island, splitting off: each chunk is self-contained
|
|
381
|
-
//
|
|
382
|
-
const solo = await Promise.all(files.map((file) => buildAll(root, [file], false)));
|
|
383
|
-
return islandBundle(solo
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Same code as an unbuildable `src`: "this path cannot become a client entry" is one condition.
|
|
388
|
-
*
|
|
389
|
-
* Two fixes, because there are two causes and only one of them can be repaired by naming a path.
|
|
390
|
-
* The line was `pass only: '<app-root-relative path>.island.tsx'` — a placeholder nobody can run,
|
|
391
|
-
* which no gate could see: `fixProblem` fails a fix only for ADVICE with no command token, and a
|
|
392
|
-
* sentence with neither is not advice. Both forms below are constructed from what the caller
|
|
393
|
-
* already handed in, so neither can name a path this app does not have.
|
|
394
|
-
*/
|
|
395
|
-
function onlyMissing(only: string, discovered: readonly string[]): IslandInvalidError {
|
|
396
|
-
const cause =
|
|
397
|
-
`buildIslands was asked for ${JSON.stringify(only)} alone, which is not one of the ` +
|
|
398
|
-
`${discovered.length} islands this app has (${discovered.length === 0 ? 'none' : discovered.join(', ')})`;
|
|
399
|
-
// The basename match first: a filter that misses normally missed on the PREFIX — a route-relative
|
|
400
|
-
// specifier where `discoverIslands`' app-root-relative path was wanted — and the filename
|
|
401
|
-
// survives that. Falling back to the first keeps the fix a real path rather than a shape.
|
|
402
|
-
const nearest =
|
|
403
|
-
discovered.find((file) => posix.basename(file) === posix.basename(only)) ?? discovered[0];
|
|
404
|
-
// An app with no islands cannot be pointed at one, so the fix WRITES the file that was asked
|
|
405
|
-
// for — the same command `entryMissing` hands back, split off the same path.
|
|
406
|
-
if (nearest === undefined) {
|
|
407
|
-
return new IslandInvalidError(
|
|
408
|
-
cause,
|
|
409
|
-
`x g island ${posix.basename(only, ISLAND_EXTENSION)} --at ${posix.dirname(only)}`,
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
return new IslandInvalidError(cause, `buildIslands(root, { only: '${nearest}' })`);
|
|
418
|
+
// One build per island, splitting off: each chunk is self-contained but for the page runtime,
|
|
419
|
+
// the ONE file beside it every realtime island loads, counted once however many load it.
|
|
420
|
+
const solo = await Promise.all(files.map((file) => buildAll(root, [file], false, runtimes)));
|
|
421
|
+
return islandBundle(...mergeBuilds(solo));
|
|
413
422
|
}
|
|
414
423
|
|
|
415
424
|
export function islandBundle(
|
|
@@ -437,25 +446,6 @@ export function islandBundle(
|
|
|
437
446
|
};
|
|
438
447
|
}
|
|
439
448
|
|
|
440
|
-
/**
|
|
441
|
-
* The specifier named no file the build could bundle. `X_ISLAND_INVALID` is render's and is
|
|
442
|
-
* borrowed rather than renamed here: "this src cannot become a client entry" is the condition that
|
|
443
|
-
* code already means, and a second name for it is a second thing to look up.
|
|
444
|
-
*/
|
|
445
|
-
function entryMissing(
|
|
446
|
-
routeFile: string,
|
|
447
|
-
src: string,
|
|
448
|
-
target: string,
|
|
449
|
-
chunks: readonly IslandChunk[],
|
|
450
|
-
): IslandInvalidError {
|
|
451
|
-
const known = chunks.map((chunk) => chunk.file);
|
|
452
|
-
return new IslandInvalidError(
|
|
453
|
-
`${routeFile} declares island src ${JSON.stringify(src)}, which resolves to ${target} — a ` +
|
|
454
|
-
`file this build did not bundle (${known.length === 0 ? 'it found no islands at all' : `it found ${known.join(', ')}`})`,
|
|
455
|
-
`x g island ${posix.basename(target, ISLAND_EXTENSION)} --at ${posix.dirname(target)}`,
|
|
456
|
-
);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
449
|
/** Write every chunk under the static export, at the same URL the documents already carry. */
|
|
460
450
|
export async function writeIslands(bundle: IslandBundle, out: string): Promise<void> {
|
|
461
451
|
for (const chunk of [...bundle.chunks, ...bundle.shared]) {
|
package/src/island-capture.ts
CHANGED
|
@@ -265,7 +265,7 @@ export async function captureIslandState(
|
|
|
265
265
|
state: target.state,
|
|
266
266
|
theme: target.theme,
|
|
267
267
|
reason: `produced ${bytes.byteLength} bytes, under the ${floor}-byte floor — that is not an image`,
|
|
268
|
-
fix: `x shot --island ${target.name} --browser /usr/bin/chromium --json`,
|
|
268
|
+
fix: `x shot --island ${target.name} --browser /usr/bin/chromium --json # macOS: --browser "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; Windows: --browser "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"`,
|
|
269
269
|
});
|
|
270
270
|
}
|
|
271
271
|
await Bun.write(join(options.outDir, target.file), bytes);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// What a module bundled twice costs the build. A framework bug refuses it: one file under two
|
|
2
|
+
// spellings, or a second `@ultimat3/*` copy, is the framework's resolver or dedupe failing. A
|
|
3
|
+
// third-party package installed twice with identical bytes is the app's lockfile, which the app
|
|
4
|
+
// did not write by hand — so it is a warning naming the bytes, never a broken build.
|
|
5
|
+
|
|
6
|
+
import { logger, UltimateError } from '@ultimat3/core';
|
|
7
|
+
import type { DuplicatedModule, IslandMetafile, ModuleIdentityIo } from './island-duplicates';
|
|
8
|
+
import { duplicatedModules, hostIo } from './island-duplicates';
|
|
9
|
+
import { quoteArg } from './shell-quote';
|
|
10
|
+
|
|
11
|
+
/** The framework's own scope: a second copy of one of these is ours to fold (`island-package-dedupe.ts`). */
|
|
12
|
+
const FRAMEWORK_SCOPE = '@ultimat3/';
|
|
13
|
+
|
|
14
|
+
/** A duplicate only the framework can have caused. */
|
|
15
|
+
export const isFrameworkDuplicate = (one: DuplicatedModule): boolean =>
|
|
16
|
+
one.sameFile || (one.packageName?.startsWith(FRAMEWORK_SCOPE) ?? false);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Its own code, not `X_BUILD_FAILED`: that one's fix re-runs `bun build`, which SUCCEEDS here — the
|
|
20
|
+
* build did not fail, it shipped one module twice and said `success: true`. Every refusal is a
|
|
21
|
+
* framework defect, so the fix is the report that gets it fixed.
|
|
22
|
+
*/
|
|
23
|
+
export class IslandModuleDuplicatedError extends UltimateError {
|
|
24
|
+
constructor(island: string, found: readonly DuplicatedModule[]) {
|
|
25
|
+
const wasted = found.reduce((sum, one) => sum + one.wastedBytes, 0);
|
|
26
|
+
const listed = found
|
|
27
|
+
.slice(0, 3)
|
|
28
|
+
.map((one) => `${one.package ?? 'one file'} as ${one.spellings.join(' and ')}`)
|
|
29
|
+
.join('; ');
|
|
30
|
+
super({
|
|
31
|
+
code: 'X_ISLAND_MODULE_DUPLICATED',
|
|
32
|
+
cause:
|
|
33
|
+
`${island} bundles ${found.length} module(s) more than once in ${found[0]?.output ?? 'its chunk'} ` +
|
|
34
|
+
`(${wasted} B shipped twice): ${listed}${found.length > 3 ? `; and ${found.length - 3} more` : ''}`,
|
|
35
|
+
fix: `gh issue create --repo developerz-ai/ultimate --title ${quoteArg(`X_ISLAND_MODULE_DUPLICATED in ${island}`)} — paste this error's --json as the body: the framework bundled one of its own modules twice`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The one line a third-party copy costs: the island, the package, the bytes, and what to run. */
|
|
41
|
+
export const thirdPartyWarning = (island: string, one: DuplicatedModule): string =>
|
|
42
|
+
`X_ISLAND_MODULE_DUPLICATED (warning): ${island} bundles ${one.package ?? 'a package'} twice ` +
|
|
43
|
+
`(${one.wastedBytes} B shipped twice, as ${one.spellings.join(' and ')}) — fix: bun why ` +
|
|
44
|
+
`${quoteArg(one.packageName ?? '<package>')}, then bun install --force to fold the copies`;
|
|
45
|
+
|
|
46
|
+
/** Refuse a framework duplicate; warn about each third-party one. */
|
|
47
|
+
export async function refuseDuplicatedModules(
|
|
48
|
+
island: string,
|
|
49
|
+
metafile: IslandMetafile,
|
|
50
|
+
io: ModuleIdentityIo = hostIo(),
|
|
51
|
+
warn: (line: string) => void = (line) => logger.warn(line),
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
const found = await duplicatedModules(metafile, io);
|
|
54
|
+
const fatal = found.filter(isFrameworkDuplicate);
|
|
55
|
+
if (fatal.length > 0) throw new IslandModuleDuplicatedError(island, fatal);
|
|
56
|
+
for (const one of found) warn(thirdPartyWarning(island, one));
|
|
57
|
+
}
|