@mandujs/core 0.54.1 → 0.54.3
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 +4 -2
- package/scripts/postinstall-lock.ts +153 -0
- package/src/a11y/run-audit.ts +15 -15
- package/src/brain/doctor/analyzer.ts +7 -7
- package/src/bundler/__tests__/cold-start.test.ts +35 -7
- package/src/bundler/analyzer.ts +15 -7
- package/src/bundler/build.test.ts +13 -6
- package/src/bundler/build.ts +429 -182
- package/src/bundler/manifest-schema.ts +21 -14
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +13 -9
- package/src/bundler/plugins/block-generated-imports.ts +13 -12
- package/src/bundler/types.ts +31 -14
- package/src/client/island.ts +79 -29
- package/src/config/validate.ts +1 -1
- package/src/deploy/inference/context.ts +82 -15
- package/src/filling/context.ts +17 -4
- package/src/guard/check.ts +9 -9
- package/src/guard/config-guard.ts +13 -7
- package/src/guard/fs-routes-policy.ts +51 -0
- package/src/guard/index.ts +11 -6
- package/src/kitchen/api/file-api.ts +11 -8
- package/src/resource/__tests__/schema.test.ts +14 -9
- package/src/resource/generators/slot.ts +72 -71
- package/src/resource/schema.ts +21 -13
- package/src/runtime/__tests__/devtools-adapter.test.ts +68 -0
- package/src/runtime/__tests__/observability-lifecycle.test.ts +103 -0
- package/src/runtime/__tests__/page-render-response.test.ts +103 -0
- package/src/runtime/__tests__/request-middleware.test.ts +70 -0
- package/src/runtime/devtools-adapter.ts +68 -0
- package/src/runtime/escape.ts +34 -6
- package/src/runtime/observability-lifecycle.ts +290 -0
- package/src/runtime/page-render-response.ts +106 -0
- package/src/runtime/request-middleware.ts +31 -0
- package/src/runtime/server.ts +228 -944
- package/src/runtime/ssr.ts +59 -37
- package/src/runtime/static-files.ts +289 -0
- package/src/runtime/streaming-ssr.ts +22 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/core",
|
|
3
|
-
"version": "0.54.
|
|
3
|
+
"version": "0.54.3",
|
|
4
4
|
"description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -110,9 +110,11 @@
|
|
|
110
110
|
"./components/Image": "./src/components/Image.tsx"
|
|
111
111
|
},
|
|
112
112
|
"files": [
|
|
113
|
-
"src/**/*"
|
|
113
|
+
"src/**/*",
|
|
114
|
+
"scripts/postinstall-lock.ts"
|
|
114
115
|
],
|
|
115
116
|
"scripts": {
|
|
117
|
+
"postinstall": "bun ./scripts/postinstall-lock.ts",
|
|
116
118
|
"test": "bun test tests/streaming-ssr && bun test tests/hydration tests/typing src",
|
|
117
119
|
"test:hydration": "bun test tests/hydration",
|
|
118
120
|
"test:streaming": "bun test tests/streaming-ssr",
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refresh an existing Guard lock after package-manager updates.
|
|
3
|
+
*
|
|
4
|
+
* Lifecycle scripts run from inside the installed package, so projectRoot
|
|
5
|
+
* defaults to INIT_CWD when Bun provides it. This helper is intentionally
|
|
6
|
+
* best-effort: installs must not fail because a project has a stale, invalid,
|
|
7
|
+
* or temporarily unreadable Mandu config.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import {
|
|
12
|
+
validateConfig,
|
|
13
|
+
type ValidatedManduConfig,
|
|
14
|
+
} from "../src/config/validate.js";
|
|
15
|
+
import { CONFIG_FILES } from "../src/config/mandu.js";
|
|
16
|
+
import {
|
|
17
|
+
generateLockfile,
|
|
18
|
+
readLockfile,
|
|
19
|
+
readMcpConfig,
|
|
20
|
+
writeLockfile,
|
|
21
|
+
LOCKFILE_PATH,
|
|
22
|
+
} from "../src/lockfile/index.js";
|
|
23
|
+
|
|
24
|
+
export type PostinstallLockAction =
|
|
25
|
+
| "updated"
|
|
26
|
+
| "skipped-disabled"
|
|
27
|
+
| "skipped-no-project-config"
|
|
28
|
+
| "skipped-no-lockfile"
|
|
29
|
+
| "skipped-invalid-lockfile"
|
|
30
|
+
| "skipped-invalid-config"
|
|
31
|
+
| "skipped-invalid-mcp-config"
|
|
32
|
+
| "skipped-write-failed";
|
|
33
|
+
|
|
34
|
+
export interface PostinstallLockResult {
|
|
35
|
+
action: PostinstallLockAction;
|
|
36
|
+
projectRoot: string;
|
|
37
|
+
hash?: string;
|
|
38
|
+
error?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PostinstallLockOptions {
|
|
42
|
+
projectRoot?: string;
|
|
43
|
+
env?: NodeJS.ProcessEnv;
|
|
44
|
+
verbose?: boolean;
|
|
45
|
+
log?: (message: string) => void;
|
|
46
|
+
warn?: (message: string) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function refreshGuardLockAfterInstall(
|
|
50
|
+
options: PostinstallLockOptions = {},
|
|
51
|
+
): Promise<PostinstallLockResult> {
|
|
52
|
+
const env = options.env ?? process.env;
|
|
53
|
+
const projectRoot = path.resolve(
|
|
54
|
+
options.projectRoot ?? env.INIT_CWD ?? process.cwd(),
|
|
55
|
+
);
|
|
56
|
+
const verbose = options.verbose ?? env.MANDU_POSTINSTALL_VERBOSE === "1";
|
|
57
|
+
const log = options.log ?? console.log;
|
|
58
|
+
const warn = options.warn ?? console.warn;
|
|
59
|
+
|
|
60
|
+
const report = (result: PostinstallLockResult): PostinstallLockResult => {
|
|
61
|
+
if (verbose) {
|
|
62
|
+
if (result.action === "updated") {
|
|
63
|
+
log(`[Mandu] refreshed ${LOCKFILE_PATH} (${result.hash})`);
|
|
64
|
+
} else if (result.error) {
|
|
65
|
+
warn(`[Mandu] ${result.action}: ${result.error}`);
|
|
66
|
+
} else {
|
|
67
|
+
log(`[Mandu] ${result.action}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (env.MANDU_POSTINSTALL_LOCK === "0") {
|
|
74
|
+
return report({ action: "skipped-disabled", projectRoot });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!(await hasProjectConfig(projectRoot))) {
|
|
78
|
+
return report({ action: "skipped-no-project-config", projectRoot });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let existingLockfile: Awaited<ReturnType<typeof readLockfile>>;
|
|
82
|
+
try {
|
|
83
|
+
existingLockfile = await readLockfile(projectRoot);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return report({
|
|
86
|
+
action: "skipped-invalid-lockfile",
|
|
87
|
+
projectRoot,
|
|
88
|
+
error: stringifyError(error),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!existingLockfile) {
|
|
93
|
+
return report({ action: "skipped-no-lockfile", projectRoot });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const validation = await validateConfig(projectRoot);
|
|
97
|
+
if (!validation.valid || !validation.config) {
|
|
98
|
+
return report({
|
|
99
|
+
action: "skipped-invalid-config",
|
|
100
|
+
projectRoot,
|
|
101
|
+
error:
|
|
102
|
+
validation.errors?.map((entry) => entry.message).join("; ") ??
|
|
103
|
+
"Mandu config validation failed",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let mcpConfig: Record<string, unknown> | null;
|
|
108
|
+
try {
|
|
109
|
+
mcpConfig = await readMcpConfig(projectRoot);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return report({
|
|
112
|
+
action: "skipped-invalid-mcp-config",
|
|
113
|
+
projectRoot,
|
|
114
|
+
error: stringifyError(error),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const lockfile = generateLockfile(
|
|
120
|
+
validation.config as ValidatedManduConfig,
|
|
121
|
+
{
|
|
122
|
+
includeSnapshot: existingLockfile.snapshot !== undefined,
|
|
123
|
+
includeMcpServerHashes: true,
|
|
124
|
+
},
|
|
125
|
+
mcpConfig,
|
|
126
|
+
);
|
|
127
|
+
await writeLockfile(projectRoot, lockfile);
|
|
128
|
+
return report({ action: "updated", projectRoot, hash: lockfile.configHash });
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return report({
|
|
131
|
+
action: "skipped-write-failed",
|
|
132
|
+
projectRoot,
|
|
133
|
+
error: stringifyError(error),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function hasProjectConfig(projectRoot: string): Promise<boolean> {
|
|
139
|
+
for (const fileName of CONFIG_FILES) {
|
|
140
|
+
if (await Bun.file(path.join(projectRoot, fileName)).exists()) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function stringifyError(error: unknown): string {
|
|
148
|
+
return error instanceof Error ? error.message : String(error);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (import.meta.main) {
|
|
152
|
+
await refreshGuardLockAfterInstall();
|
|
153
|
+
}
|
package/src/a11y/run-audit.ts
CHANGED
|
@@ -69,8 +69,11 @@ interface DomProvider {
|
|
|
69
69
|
fromHtml(html: string, url: string): Promise<{ window: unknown; dispose: () => Promise<void> }>;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
const DEFAULT_MAX_FILES = 500;
|
|
73
|
-
const DEFAULT_MIN_IMPACT: AuditImpact = "minor";
|
|
72
|
+
const DEFAULT_MAX_FILES = 500;
|
|
73
|
+
const DEFAULT_MIN_IMPACT: AuditImpact = "minor";
|
|
74
|
+
const AXE_CORE_MODULE = "axe-core";
|
|
75
|
+
const JSDOM_MODULE = "jsdom";
|
|
76
|
+
const HAPPY_DOM_MODULE = "happy-dom";
|
|
74
77
|
|
|
75
78
|
/**
|
|
76
79
|
* Zero every entry in an impact-count record. Returned by value so
|
|
@@ -100,11 +103,10 @@ async function resolveAxe(options: RunAuditOptions): Promise<AxeLike | null> {
|
|
|
100
103
|
return null;
|
|
101
104
|
}
|
|
102
105
|
};
|
|
103
|
-
|
|
104
|
-
if (options.axeLoader) return tryLoad(options.axeLoader);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
106
|
+
|
|
107
|
+
if (options.axeLoader) return tryLoad(options.axeLoader);
|
|
108
|
+
return tryLoad(() => import(AXE_CORE_MODULE));
|
|
109
|
+
}
|
|
108
110
|
|
|
109
111
|
/**
|
|
110
112
|
* Resolve a DOM provider. Prefers jsdom; falls back to HappyDOM via
|
|
@@ -126,10 +128,9 @@ async function resolveDomProvider(options: RunAuditOptions): Promise<DomProvider
|
|
|
126
128
|
return null;
|
|
127
129
|
}
|
|
128
130
|
|
|
129
|
-
// Preferred path — jsdom.
|
|
130
|
-
try {
|
|
131
|
-
|
|
132
|
-
const jsdom = await import("jsdom");
|
|
131
|
+
// Preferred path — jsdom.
|
|
132
|
+
try {
|
|
133
|
+
const jsdom = await import(JSDOM_MODULE);
|
|
133
134
|
const JSDOMCtor = (jsdom as { JSDOM?: new (html: string, opts?: unknown) => unknown }).JSDOM;
|
|
134
135
|
if (JSDOMCtor) {
|
|
135
136
|
return {
|
|
@@ -153,10 +154,9 @@ async function resolveDomProvider(options: RunAuditOptions): Promise<DomProvider
|
|
|
153
154
|
// jsdom not installed — fall through to HappyDOM.
|
|
154
155
|
}
|
|
155
156
|
|
|
156
|
-
// Fallback path — HappyDOM.
|
|
157
|
-
try {
|
|
158
|
-
|
|
159
|
-
const happy = await import("happy-dom");
|
|
157
|
+
// Fallback path — HappyDOM.
|
|
158
|
+
try {
|
|
159
|
+
const happy = await import(HAPPY_DOM_MODULE);
|
|
160
160
|
const WindowCtor = (happy as { Window?: new (opts?: { url?: string; innerWidth?: number }) => unknown }).Window;
|
|
161
161
|
if (WindowCtor) {
|
|
162
162
|
return {
|
|
@@ -206,13 +206,13 @@ export function generateTemplatePatches(
|
|
|
206
206
|
"Do NOT import or re-export the island in page.tsx — island() returns " +
|
|
207
207
|
"a config object, not a React component. Use data-island attributes instead.",
|
|
208
208
|
type: "modify",
|
|
209
|
-
content:
|
|
210
|
-
`// Example: app/my-feature.island.tsx\n` +
|
|
211
|
-
`import {
|
|
212
|
-
`export default
|
|
213
|
-
`// In page.tsx, reference via: <div data-island="my-feature">...</div>`,
|
|
214
|
-
confidence: 0.9,
|
|
215
|
-
});
|
|
209
|
+
content:
|
|
210
|
+
`// Example: app/my-feature.island.tsx\n` +
|
|
211
|
+
`import { wrapComponent } from "@mandujs/core/client";\n\n` +
|
|
212
|
+
`export default wrapComponent(MyComponent);\n\n` +
|
|
213
|
+
`// In page.tsx, reference via: <div data-island="my-feature">...</div>`,
|
|
214
|
+
confidence: 0.9,
|
|
215
|
+
});
|
|
216
216
|
break;
|
|
217
217
|
|
|
218
218
|
default:
|
|
@@ -29,10 +29,11 @@ import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "fs";
|
|
|
29
29
|
import { tmpdir } from "os";
|
|
30
30
|
import path from "path";
|
|
31
31
|
import type { RouteSpec } from "../../spec/schema";
|
|
32
|
-
import {
|
|
33
|
-
_testOnly_scanIslandFiles,
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
import {
|
|
33
|
+
_testOnly_scanIslandFiles,
|
|
34
|
+
_testOnly_scanPartialFiles,
|
|
35
|
+
_testOnly_getHydratedRoutes,
|
|
36
|
+
} from "../build";
|
|
36
37
|
import { HMR_PERF } from "../../perf/hmr-markers";
|
|
37
38
|
import {
|
|
38
39
|
_resetCacheForTesting as _resetPerfCache,
|
|
@@ -372,7 +373,7 @@ describe("Phase 7.1 R1 Agent C — getHydratedRoutes filter", () => {
|
|
|
372
373
|
});
|
|
373
374
|
});
|
|
374
375
|
|
|
375
|
-
describe("Phase 7.1 R1 Agent C — per-island scan skips non-hydrated routes", () => {
|
|
376
|
+
describe("Phase 7.1 R1 Agent C — per-island scan skips non-hydrated routes", () => {
|
|
376
377
|
let project: ReturnType<typeof createProject>;
|
|
377
378
|
|
|
378
379
|
beforeEach(() => {
|
|
@@ -500,5 +501,32 @@ describe("Phase 7.1 R1 Agent C — per-island scan skips non-hydrated routes", (
|
|
|
500
501
|
];
|
|
501
502
|
const result = await _testOnly_scanIslandFiles(routes, project.rootDir);
|
|
502
503
|
expect(result).toEqual([]);
|
|
503
|
-
});
|
|
504
|
-
});
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
describe("partial bundle scan", () => {
|
|
508
|
+
let project: ReturnType<typeof createProject>;
|
|
509
|
+
|
|
510
|
+
beforeEach(() => {
|
|
511
|
+
project = createProject();
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
afterEach(() => {
|
|
515
|
+
try {
|
|
516
|
+
rmSync(project.rootDir, { recursive: true, force: true });
|
|
517
|
+
} catch {
|
|
518
|
+
/* Windows lock tolerance */
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("discovers *.partial.tsx files outside .mandu and node_modules", async () => {
|
|
523
|
+
project.writeIslandFile("app", "Home.partial.tsx");
|
|
524
|
+
project.writeIslandFile(".mandu/client", "Stale.partial.tsx");
|
|
525
|
+
project.writeIslandFile("node_modules/pkg", "Ignored.partial.tsx");
|
|
526
|
+
|
|
527
|
+
const result = await _testOnly_scanPartialFiles(project.rootDir);
|
|
528
|
+
|
|
529
|
+
expect(result.map((entry) => entry.name)).toEqual(["Home"]);
|
|
530
|
+
expect(result[0].priority).toBe("visible");
|
|
531
|
+
});
|
|
532
|
+
});
|
package/src/bundler/analyzer.ts
CHANGED
|
@@ -341,18 +341,26 @@ export async function analyzeBundle(
|
|
|
341
341
|
deps: entry.dependencies ?? [],
|
|
342
342
|
});
|
|
343
343
|
}
|
|
344
|
-
for (const [islandName, entry] of Object.entries(manifest.islands ?? {})) {
|
|
345
|
-
// Avoid double-counting: if a per-island chunk shares its route id with
|
|
346
|
-
// a route-level bundle, we prefer the island entry (finer granularity).
|
|
347
|
-
const existing = islandSources.findIndex((s) => s.name === islandName);
|
|
344
|
+
for (const [islandName, entry] of Object.entries(manifest.islands ?? {})) {
|
|
345
|
+
// Avoid double-counting: if a per-island chunk shares its route id with
|
|
346
|
+
// a route-level bundle, we prefer the island entry (finer granularity).
|
|
347
|
+
const existing = islandSources.findIndex((s) => s.name === islandName);
|
|
348
348
|
if (existing !== -1) islandSources.splice(existing, 1);
|
|
349
349
|
islandSources.push({
|
|
350
350
|
name: islandName,
|
|
351
351
|
url: entry.js,
|
|
352
352
|
priority: entry.priority,
|
|
353
|
-
deps: [],
|
|
354
|
-
});
|
|
355
|
-
}
|
|
353
|
+
deps: [],
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
for (const [partialName, entry] of Object.entries(manifest.partials ?? {})) {
|
|
357
|
+
islandSources.push({
|
|
358
|
+
name: `partial:${partialName}`,
|
|
359
|
+
url: entry.js,
|
|
360
|
+
priority: entry.priority,
|
|
361
|
+
deps: [],
|
|
362
|
+
});
|
|
363
|
+
}
|
|
356
364
|
|
|
357
365
|
const islands: AnalyzeIsland[] = [];
|
|
358
366
|
for (const src of islandSources) {
|
|
@@ -171,9 +171,16 @@ describe("buildClientBundles vendor shims", () => {
|
|
|
171
171
|
expect(runtimeSource).toContain("function hasHydratableMarkup");
|
|
172
172
|
expect(runtimeSource).toContain("function shouldHydrateCompiledIsland");
|
|
173
173
|
expect(runtimeSource).toContain("onRecoverableError");
|
|
174
|
-
expect(runtimeSource).toContain("data-mandu-hydrating");
|
|
175
|
-
expect(runtimeSource).toContain("data-mandu-render-mode");
|
|
176
|
-
expect(runtimeSource).toContain("data-mandu-recoverable-error");
|
|
177
|
-
expect(runtimeSource).toContain("pointerdown");
|
|
178
|
-
});
|
|
179
|
-
|
|
174
|
+
expect(runtimeSource).toContain("data-mandu-hydrating");
|
|
175
|
+
expect(runtimeSource).toContain("data-mandu-render-mode");
|
|
176
|
+
expect(runtimeSource).toContain("data-mandu-recoverable-error");
|
|
177
|
+
expect(runtimeSource).toContain("pointerdown");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("runtime parses SSR data script before island setup", async () => {
|
|
181
|
+
const runtimeSource = await readFile(path.join(rootDir, ".mandu", "client", "_runtime.js"), "utf-8");
|
|
182
|
+
expect(runtimeSource).toContain("function readManduData");
|
|
183
|
+
expect(runtimeSource).toContain("document.getElementById(\"__MANDU_DATA__\")");
|
|
184
|
+
expect(runtimeSource).toContain("JSON.parse");
|
|
185
|
+
});
|
|
186
|
+
});
|