@mandujs/core 0.54.9 → 0.54.10
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 +200 -200
- package/src/agent/__tests__/context.test.ts +65 -17
- package/src/agent/types.ts +18 -10
- package/src/agent/verify.ts +115 -17
- package/src/bundler/build.test.ts +1 -1
- package/src/error/formatter.ts +31 -22
- package/src/router/client-entry.test.ts +56 -5
- package/src/router/client-entry.ts +136 -50
- package/src/router/fs-scanner.ts +4 -3
package/package.json
CHANGED
|
@@ -1,200 +1,200 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@mandujs/core",
|
|
3
|
-
"version": "0.54.
|
|
4
|
-
"description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./src/index.ts",
|
|
7
|
-
"types": "./src/index.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": "./src/index.ts",
|
|
10
|
-
"./a11y": "./src/a11y/index.ts",
|
|
11
|
-
"./agent": "./src/agent/index.ts",
|
|
12
|
-
"./auth": "./src/auth/index.ts",
|
|
13
|
-
"./auth/login": "./src/auth/login.ts",
|
|
14
|
-
"./auth/password": "./src/auth/password.ts",
|
|
15
|
-
"./auth/reset": "./src/auth/reset.ts",
|
|
16
|
-
"./auth/verification": "./src/auth/verification.ts",
|
|
17
|
-
"./client": "./src/client/index.ts",
|
|
18
|
-
"./client/rpc": "./src/client/rpc.ts",
|
|
19
|
-
"./contract": "./src/contract/index.ts",
|
|
20
|
-
"./contract/rpc": "./src/contract/rpc.ts",
|
|
21
|
-
"./content": "./src/content/index.ts",
|
|
22
|
-
"./content/prebuild": "./src/content/prebuild.ts",
|
|
23
|
-
"./content/collection": "./src/content/collection.ts",
|
|
24
|
-
"./content/sidebar": "./src/content/sidebar.ts",
|
|
25
|
-
"./content/slug": "./src/content/slug.ts",
|
|
26
|
-
"./content/llms-txt": "./src/content/llms-txt.ts",
|
|
27
|
-
"./content/schema": "./src/content/schema.ts",
|
|
28
|
-
"./config": "./src/config/index.ts",
|
|
29
|
-
"./config/mandu": "./src/config/mandu.ts",
|
|
30
|
-
"./config/validate": "./src/config/validate.ts",
|
|
31
|
-
"./db": "./src/db/index.ts",
|
|
32
|
-
"./db/migrations": "./src/db/migrations/index.ts",
|
|
33
|
-
"./db/migrations/history-table": "./src/db/migrations/history-table.ts",
|
|
34
|
-
"./db/migrations/runner": "./src/db/migrations/runner.ts",
|
|
35
|
-
"./deploy": "./src/deploy/index.ts",
|
|
36
|
-
"./design": "./src/design/index.ts",
|
|
37
|
-
"./desktop": "./src/desktop/index.ts",
|
|
38
|
-
"./desktop/worker": "./src/desktop/worker.ts",
|
|
39
|
-
"./email": "./src/email/index.ts",
|
|
40
|
-
"./filling": "./src/filling/index.ts",
|
|
41
|
-
"./filling/cookie-codec": "./src/filling/cookie-codec.ts",
|
|
42
|
-
"./filling/session-sqlite": "./src/filling/session-sqlite.ts",
|
|
43
|
-
"./kitchen": "./src/kitchen/index.ts",
|
|
44
|
-
"./logging": "./src/logging/index.ts",
|
|
45
|
-
"./middleware": "./src/middleware/index.ts",
|
|
46
|
-
"./middleware/csrf": "./src/middleware/csrf.ts",
|
|
47
|
-
"./middleware/oauth": "./src/middleware/oauth/index.ts",
|
|
48
|
-
"./middleware/secure": "./src/middleware/secure/index.ts",
|
|
49
|
-
"./middleware/rate-limit": "./src/middleware/rate-limit/index.ts",
|
|
50
|
-
"./middleware/session": "./src/middleware/session.ts",
|
|
51
|
-
"./testing": "./src/testing/index.ts",
|
|
52
|
-
"./testing/db": "./src/testing/db.ts",
|
|
53
|
-
"./testing/mocks": "./src/testing/mocks.ts",
|
|
54
|
-
"./testing/reporter": "./src/testing/reporter.ts",
|
|
55
|
-
"./testing/server": "./src/testing/server.ts",
|
|
56
|
-
"./testing/session": "./src/testing/session.ts",
|
|
57
|
-
"./testing/snapshot": "./src/testing/snapshot.ts",
|
|
58
|
-
"./plugins": "./src/plugins/index.ts",
|
|
59
|
-
"./plugins/define": "./src/plugins/define.ts",
|
|
60
|
-
"./plugins/runner": "./src/plugins/runner.ts",
|
|
61
|
-
"./error": "./src/error/index.ts",
|
|
62
|
-
"./i18n": "./src/i18n/index.ts",
|
|
63
|
-
"./id": "./src/id/index.ts",
|
|
64
|
-
"./observability": "./src/observability/index.ts",
|
|
65
|
-
"./openapi/generator": "./src/openapi/generator.ts",
|
|
66
|
-
"./perf": "./src/perf/index.ts",
|
|
67
|
-
"./perf/hmr-markers": "./src/perf/hmr-markers.ts",
|
|
68
|
-
"./perf/user-marks": "./src/perf/user-marks.ts",
|
|
69
|
-
"./resource": "./src/resource/index.ts",
|
|
70
|
-
"./resource/ddl/diff": "./src/resource/ddl/diff.ts",
|
|
71
|
-
"./resource/ddl/emit": "./src/resource/ddl/emit.ts",
|
|
72
|
-
"./resource/ddl/snapshot": "./src/resource/ddl/snapshot.ts",
|
|
73
|
-
"./resource/ddl/type-map": "./src/resource/ddl/type-map.ts",
|
|
74
|
-
"./resource/ddl/types": "./src/resource/ddl/types.ts",
|
|
75
|
-
"./resource/generator-repo": "./src/resource/generator-repo.ts",
|
|
76
|
-
"./resource/parser": "./src/resource/parser.ts",
|
|
77
|
-
"./resource/schema": "./src/resource/schema.ts",
|
|
78
|
-
"./router": "./src/router/index.ts",
|
|
79
|
-
"./routes": "./src/routes/index.ts",
|
|
80
|
-
"./runtime": "./src/runtime/index.ts",
|
|
81
|
-
"./runtime/cache": "./src/runtime/cache.ts",
|
|
82
|
-
"./runtime/router": "./src/runtime/router.ts",
|
|
83
|
-
"./runtime/server": "./src/runtime/server.ts",
|
|
84
|
-
"./scheduler": "./src/scheduler/index.ts",
|
|
85
|
-
"./storage/s3": "./src/storage/s3/index.ts",
|
|
86
|
-
"./guard": "./src/guard/index.ts",
|
|
87
|
-
"./guard/define-rule": "./src/guard/define-rule.ts",
|
|
88
|
-
"./guard/design-inline-class": "./src/guard/design-inline-class.ts",
|
|
89
|
-
"./guard/rule-presets": "./src/guard/rule-presets.ts",
|
|
90
|
-
"./guard/tsgolint-bridge": "./src/guard/tsgolint-bridge.ts",
|
|
91
|
-
"./bundler": "./src/bundler/index.ts",
|
|
92
|
-
"./bundler/prerender": "./src/bundler/prerender.ts",
|
|
93
|
-
"./bundler/safe-build": "./src/bundler/safe-build.ts",
|
|
94
|
-
"./bundler/types": "./src/bundler/types.ts",
|
|
95
|
-
"./bundler/hmr-types": "./src/bundler/hmr-types.ts",
|
|
96
|
-
"./bundler/scenario-matrix": "./src/bundler/scenario-matrix.ts",
|
|
97
|
-
"./bundler/vendor-cache-types": "./src/bundler/vendor-cache-types.ts",
|
|
98
|
-
"./bundler/manifest-schema": "./src/bundler/manifest-schema.ts",
|
|
99
|
-
"./bundler/analyzer": "./src/bundler/analyzer.ts",
|
|
100
|
-
"./bundler/budget": "./src/bundler/budget.ts",
|
|
101
|
-
"./bundler/plugins": "./src/bundler/plugins/index.ts",
|
|
102
|
-
"./bundler/plugins/block-generated-imports": "./src/bundler/plugins/block-generated-imports.ts",
|
|
103
|
-
"./bundler/generate-static-params": "./src/bundler/generate-static-params.ts",
|
|
104
|
-
"./diagnose": "./src/diagnose/index.ts",
|
|
105
|
-
"./dev-error-overlay": "./src/dev-error-overlay/index.ts",
|
|
106
|
-
"./middleware/compose": "./src/middleware/compose.ts",
|
|
107
|
-
"./middleware/define": "./src/middleware/define.ts",
|
|
108
|
-
"./middleware/bridge": "./src/middleware/bridge.ts",
|
|
109
|
-
"./client/hydrate": "./src/client/hydrate.ts",
|
|
110
|
-
"./runtime/fast-refresh-types": "./src/runtime/fast-refresh-types.ts",
|
|
111
|
-
"./components/Image": "./src/components/Image.tsx"
|
|
112
|
-
},
|
|
113
|
-
"files": [
|
|
114
|
-
"src/**/*",
|
|
115
|
-
"scripts/postinstall-lock.ts"
|
|
116
|
-
],
|
|
117
|
-
"scripts": {
|
|
118
|
-
"postinstall": "bun ./scripts/postinstall-lock.ts",
|
|
119
|
-
"test": "bun test tests/streaming-ssr && bun test tests/hydration tests/typing src",
|
|
120
|
-
"test:hydration": "bun test tests/hydration",
|
|
121
|
-
"test:streaming": "bun test tests/streaming-ssr",
|
|
122
|
-
"test:watch": "bun test --watch"
|
|
123
|
-
},
|
|
124
|
-
"devDependencies": {
|
|
125
|
-
"@happy-dom/global-registrator": "^15.0.0"
|
|
126
|
-
},
|
|
127
|
-
"keywords": [
|
|
128
|
-
"mandu",
|
|
129
|
-
"framework",
|
|
130
|
-
"agent",
|
|
131
|
-
"ai",
|
|
132
|
-
"code-generation"
|
|
133
|
-
],
|
|
134
|
-
"repository": {
|
|
135
|
-
"type": "git",
|
|
136
|
-
"url": "git+https://github.com/konamgil/mandu.git",
|
|
137
|
-
"directory": "packages/core"
|
|
138
|
-
},
|
|
139
|
-
"author": "konamgil",
|
|
140
|
-
"license": "MPL-2.0",
|
|
141
|
-
"publishConfig": {
|
|
142
|
-
"access": "public"
|
|
143
|
-
},
|
|
144
|
-
"engines": {
|
|
145
|
-
"bun": ">=1.3.12"
|
|
146
|
-
},
|
|
147
|
-
"peerDependencies": {
|
|
148
|
-
"react": "^19.2.0",
|
|
149
|
-
"react-dom": "^19.2.0",
|
|
150
|
-
"react-refresh": ">=0.18.0",
|
|
151
|
-
"@tailwindcss/cli": ">=4.0.0",
|
|
152
|
-
"webview-bun": "^2.4.0",
|
|
153
|
-
"axe-core": ">=4.8.0",
|
|
154
|
-
"jsdom": ">=24.0.0",
|
|
155
|
-
"happy-dom": ">=15.0.0",
|
|
156
|
-
"@babel/core": "^7.29.0",
|
|
157
|
-
"babel-plugin-react-compiler": "^1.0.0",
|
|
158
|
-
"eslint": "^9.0.0",
|
|
159
|
-
"eslint-plugin-react-compiler": "^19.0.0 || ^1.0.0"
|
|
160
|
-
},
|
|
161
|
-
"peerDependenciesMeta": {
|
|
162
|
-
"@tailwindcss/cli": {
|
|
163
|
-
"optional": true
|
|
164
|
-
},
|
|
165
|
-
"react-refresh": {
|
|
166
|
-
"optional": true
|
|
167
|
-
},
|
|
168
|
-
"webview-bun": {
|
|
169
|
-
"optional": true
|
|
170
|
-
},
|
|
171
|
-
"axe-core": {
|
|
172
|
-
"optional": true
|
|
173
|
-
},
|
|
174
|
-
"jsdom": {
|
|
175
|
-
"optional": true
|
|
176
|
-
},
|
|
177
|
-
"happy-dom": {
|
|
178
|
-
"optional": true
|
|
179
|
-
},
|
|
180
|
-
"@babel/core": {
|
|
181
|
-
"optional": true
|
|
182
|
-
},
|
|
183
|
-
"babel-plugin-react-compiler": {
|
|
184
|
-
"optional": true
|
|
185
|
-
},
|
|
186
|
-
"eslint": {
|
|
187
|
-
"optional": true
|
|
188
|
-
},
|
|
189
|
-
"eslint-plugin-react-compiler": {
|
|
190
|
-
"optional": true
|
|
191
|
-
}
|
|
192
|
-
},
|
|
193
|
-
"dependencies": {
|
|
194
|
-
"chokidar": "^5.0.0",
|
|
195
|
-
"fast-glob": "^3.3.2",
|
|
196
|
-
"glob": "^13.0.0",
|
|
197
|
-
"minimatch": "^10.1.1",
|
|
198
|
-
"zod": "^3.23.8"
|
|
199
|
-
}
|
|
200
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@mandujs/core",
|
|
3
|
+
"version": "0.54.10",
|
|
4
|
+
"description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./a11y": "./src/a11y/index.ts",
|
|
11
|
+
"./agent": "./src/agent/index.ts",
|
|
12
|
+
"./auth": "./src/auth/index.ts",
|
|
13
|
+
"./auth/login": "./src/auth/login.ts",
|
|
14
|
+
"./auth/password": "./src/auth/password.ts",
|
|
15
|
+
"./auth/reset": "./src/auth/reset.ts",
|
|
16
|
+
"./auth/verification": "./src/auth/verification.ts",
|
|
17
|
+
"./client": "./src/client/index.ts",
|
|
18
|
+
"./client/rpc": "./src/client/rpc.ts",
|
|
19
|
+
"./contract": "./src/contract/index.ts",
|
|
20
|
+
"./contract/rpc": "./src/contract/rpc.ts",
|
|
21
|
+
"./content": "./src/content/index.ts",
|
|
22
|
+
"./content/prebuild": "./src/content/prebuild.ts",
|
|
23
|
+
"./content/collection": "./src/content/collection.ts",
|
|
24
|
+
"./content/sidebar": "./src/content/sidebar.ts",
|
|
25
|
+
"./content/slug": "./src/content/slug.ts",
|
|
26
|
+
"./content/llms-txt": "./src/content/llms-txt.ts",
|
|
27
|
+
"./content/schema": "./src/content/schema.ts",
|
|
28
|
+
"./config": "./src/config/index.ts",
|
|
29
|
+
"./config/mandu": "./src/config/mandu.ts",
|
|
30
|
+
"./config/validate": "./src/config/validate.ts",
|
|
31
|
+
"./db": "./src/db/index.ts",
|
|
32
|
+
"./db/migrations": "./src/db/migrations/index.ts",
|
|
33
|
+
"./db/migrations/history-table": "./src/db/migrations/history-table.ts",
|
|
34
|
+
"./db/migrations/runner": "./src/db/migrations/runner.ts",
|
|
35
|
+
"./deploy": "./src/deploy/index.ts",
|
|
36
|
+
"./design": "./src/design/index.ts",
|
|
37
|
+
"./desktop": "./src/desktop/index.ts",
|
|
38
|
+
"./desktop/worker": "./src/desktop/worker.ts",
|
|
39
|
+
"./email": "./src/email/index.ts",
|
|
40
|
+
"./filling": "./src/filling/index.ts",
|
|
41
|
+
"./filling/cookie-codec": "./src/filling/cookie-codec.ts",
|
|
42
|
+
"./filling/session-sqlite": "./src/filling/session-sqlite.ts",
|
|
43
|
+
"./kitchen": "./src/kitchen/index.ts",
|
|
44
|
+
"./logging": "./src/logging/index.ts",
|
|
45
|
+
"./middleware": "./src/middleware/index.ts",
|
|
46
|
+
"./middleware/csrf": "./src/middleware/csrf.ts",
|
|
47
|
+
"./middleware/oauth": "./src/middleware/oauth/index.ts",
|
|
48
|
+
"./middleware/secure": "./src/middleware/secure/index.ts",
|
|
49
|
+
"./middleware/rate-limit": "./src/middleware/rate-limit/index.ts",
|
|
50
|
+
"./middleware/session": "./src/middleware/session.ts",
|
|
51
|
+
"./testing": "./src/testing/index.ts",
|
|
52
|
+
"./testing/db": "./src/testing/db.ts",
|
|
53
|
+
"./testing/mocks": "./src/testing/mocks.ts",
|
|
54
|
+
"./testing/reporter": "./src/testing/reporter.ts",
|
|
55
|
+
"./testing/server": "./src/testing/server.ts",
|
|
56
|
+
"./testing/session": "./src/testing/session.ts",
|
|
57
|
+
"./testing/snapshot": "./src/testing/snapshot.ts",
|
|
58
|
+
"./plugins": "./src/plugins/index.ts",
|
|
59
|
+
"./plugins/define": "./src/plugins/define.ts",
|
|
60
|
+
"./plugins/runner": "./src/plugins/runner.ts",
|
|
61
|
+
"./error": "./src/error/index.ts",
|
|
62
|
+
"./i18n": "./src/i18n/index.ts",
|
|
63
|
+
"./id": "./src/id/index.ts",
|
|
64
|
+
"./observability": "./src/observability/index.ts",
|
|
65
|
+
"./openapi/generator": "./src/openapi/generator.ts",
|
|
66
|
+
"./perf": "./src/perf/index.ts",
|
|
67
|
+
"./perf/hmr-markers": "./src/perf/hmr-markers.ts",
|
|
68
|
+
"./perf/user-marks": "./src/perf/user-marks.ts",
|
|
69
|
+
"./resource": "./src/resource/index.ts",
|
|
70
|
+
"./resource/ddl/diff": "./src/resource/ddl/diff.ts",
|
|
71
|
+
"./resource/ddl/emit": "./src/resource/ddl/emit.ts",
|
|
72
|
+
"./resource/ddl/snapshot": "./src/resource/ddl/snapshot.ts",
|
|
73
|
+
"./resource/ddl/type-map": "./src/resource/ddl/type-map.ts",
|
|
74
|
+
"./resource/ddl/types": "./src/resource/ddl/types.ts",
|
|
75
|
+
"./resource/generator-repo": "./src/resource/generator-repo.ts",
|
|
76
|
+
"./resource/parser": "./src/resource/parser.ts",
|
|
77
|
+
"./resource/schema": "./src/resource/schema.ts",
|
|
78
|
+
"./router": "./src/router/index.ts",
|
|
79
|
+
"./routes": "./src/routes/index.ts",
|
|
80
|
+
"./runtime": "./src/runtime/index.ts",
|
|
81
|
+
"./runtime/cache": "./src/runtime/cache.ts",
|
|
82
|
+
"./runtime/router": "./src/runtime/router.ts",
|
|
83
|
+
"./runtime/server": "./src/runtime/server.ts",
|
|
84
|
+
"./scheduler": "./src/scheduler/index.ts",
|
|
85
|
+
"./storage/s3": "./src/storage/s3/index.ts",
|
|
86
|
+
"./guard": "./src/guard/index.ts",
|
|
87
|
+
"./guard/define-rule": "./src/guard/define-rule.ts",
|
|
88
|
+
"./guard/design-inline-class": "./src/guard/design-inline-class.ts",
|
|
89
|
+
"./guard/rule-presets": "./src/guard/rule-presets.ts",
|
|
90
|
+
"./guard/tsgolint-bridge": "./src/guard/tsgolint-bridge.ts",
|
|
91
|
+
"./bundler": "./src/bundler/index.ts",
|
|
92
|
+
"./bundler/prerender": "./src/bundler/prerender.ts",
|
|
93
|
+
"./bundler/safe-build": "./src/bundler/safe-build.ts",
|
|
94
|
+
"./bundler/types": "./src/bundler/types.ts",
|
|
95
|
+
"./bundler/hmr-types": "./src/bundler/hmr-types.ts",
|
|
96
|
+
"./bundler/scenario-matrix": "./src/bundler/scenario-matrix.ts",
|
|
97
|
+
"./bundler/vendor-cache-types": "./src/bundler/vendor-cache-types.ts",
|
|
98
|
+
"./bundler/manifest-schema": "./src/bundler/manifest-schema.ts",
|
|
99
|
+
"./bundler/analyzer": "./src/bundler/analyzer.ts",
|
|
100
|
+
"./bundler/budget": "./src/bundler/budget.ts",
|
|
101
|
+
"./bundler/plugins": "./src/bundler/plugins/index.ts",
|
|
102
|
+
"./bundler/plugins/block-generated-imports": "./src/bundler/plugins/block-generated-imports.ts",
|
|
103
|
+
"./bundler/generate-static-params": "./src/bundler/generate-static-params.ts",
|
|
104
|
+
"./diagnose": "./src/diagnose/index.ts",
|
|
105
|
+
"./dev-error-overlay": "./src/dev-error-overlay/index.ts",
|
|
106
|
+
"./middleware/compose": "./src/middleware/compose.ts",
|
|
107
|
+
"./middleware/define": "./src/middleware/define.ts",
|
|
108
|
+
"./middleware/bridge": "./src/middleware/bridge.ts",
|
|
109
|
+
"./client/hydrate": "./src/client/hydrate.ts",
|
|
110
|
+
"./runtime/fast-refresh-types": "./src/runtime/fast-refresh-types.ts",
|
|
111
|
+
"./components/Image": "./src/components/Image.tsx"
|
|
112
|
+
},
|
|
113
|
+
"files": [
|
|
114
|
+
"src/**/*",
|
|
115
|
+
"scripts/postinstall-lock.ts"
|
|
116
|
+
],
|
|
117
|
+
"scripts": {
|
|
118
|
+
"postinstall": "bun ./scripts/postinstall-lock.ts",
|
|
119
|
+
"test": "bun test tests/streaming-ssr && bun test tests/hydration tests/typing src",
|
|
120
|
+
"test:hydration": "bun test tests/hydration",
|
|
121
|
+
"test:streaming": "bun test tests/streaming-ssr",
|
|
122
|
+
"test:watch": "bun test --watch"
|
|
123
|
+
},
|
|
124
|
+
"devDependencies": {
|
|
125
|
+
"@happy-dom/global-registrator": "^15.0.0"
|
|
126
|
+
},
|
|
127
|
+
"keywords": [
|
|
128
|
+
"mandu",
|
|
129
|
+
"framework",
|
|
130
|
+
"agent",
|
|
131
|
+
"ai",
|
|
132
|
+
"code-generation"
|
|
133
|
+
],
|
|
134
|
+
"repository": {
|
|
135
|
+
"type": "git",
|
|
136
|
+
"url": "git+https://github.com/konamgil/mandu.git",
|
|
137
|
+
"directory": "packages/core"
|
|
138
|
+
},
|
|
139
|
+
"author": "konamgil",
|
|
140
|
+
"license": "MPL-2.0",
|
|
141
|
+
"publishConfig": {
|
|
142
|
+
"access": "public"
|
|
143
|
+
},
|
|
144
|
+
"engines": {
|
|
145
|
+
"bun": ">=1.3.12"
|
|
146
|
+
},
|
|
147
|
+
"peerDependencies": {
|
|
148
|
+
"react": "^19.2.0",
|
|
149
|
+
"react-dom": "^19.2.0",
|
|
150
|
+
"react-refresh": ">=0.18.0",
|
|
151
|
+
"@tailwindcss/cli": ">=4.0.0",
|
|
152
|
+
"webview-bun": "^2.4.0",
|
|
153
|
+
"axe-core": ">=4.8.0",
|
|
154
|
+
"jsdom": ">=24.0.0",
|
|
155
|
+
"happy-dom": ">=15.0.0",
|
|
156
|
+
"@babel/core": "^7.29.0",
|
|
157
|
+
"babel-plugin-react-compiler": "^1.0.0",
|
|
158
|
+
"eslint": "^9.0.0",
|
|
159
|
+
"eslint-plugin-react-compiler": "^19.0.0 || ^1.0.0"
|
|
160
|
+
},
|
|
161
|
+
"peerDependenciesMeta": {
|
|
162
|
+
"@tailwindcss/cli": {
|
|
163
|
+
"optional": true
|
|
164
|
+
},
|
|
165
|
+
"react-refresh": {
|
|
166
|
+
"optional": true
|
|
167
|
+
},
|
|
168
|
+
"webview-bun": {
|
|
169
|
+
"optional": true
|
|
170
|
+
},
|
|
171
|
+
"axe-core": {
|
|
172
|
+
"optional": true
|
|
173
|
+
},
|
|
174
|
+
"jsdom": {
|
|
175
|
+
"optional": true
|
|
176
|
+
},
|
|
177
|
+
"happy-dom": {
|
|
178
|
+
"optional": true
|
|
179
|
+
},
|
|
180
|
+
"@babel/core": {
|
|
181
|
+
"optional": true
|
|
182
|
+
},
|
|
183
|
+
"babel-plugin-react-compiler": {
|
|
184
|
+
"optional": true
|
|
185
|
+
},
|
|
186
|
+
"eslint": {
|
|
187
|
+
"optional": true
|
|
188
|
+
},
|
|
189
|
+
"eslint-plugin-react-compiler": {
|
|
190
|
+
"optional": true
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
"dependencies": {
|
|
194
|
+
"chokidar": "^5.0.0",
|
|
195
|
+
"fast-glob": "^3.3.2",
|
|
196
|
+
"glob": "^13.0.0",
|
|
197
|
+
"minimatch": "^10.1.1",
|
|
198
|
+
"zod": "^3.23.8"
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -27,13 +27,31 @@ import {
|
|
|
27
27
|
writeAgentVerifyReport,
|
|
28
28
|
} from "../verify";
|
|
29
29
|
|
|
30
|
-
async function writeFile(root: string, rel: string, content: string): Promise<void> {
|
|
31
|
-
const abs = path.join(root, rel);
|
|
32
|
-
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
33
|
-
await fs.writeFile(abs, content, "utf8");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
async function writeFile(root: string, rel: string, content: string): Promise<void> {
|
|
31
|
+
const abs = path.join(root, rel);
|
|
32
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
33
|
+
await fs.writeFile(abs, content, "utf8");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function runGit(root: string, args: string[]): Promise<{ ok: boolean; stdout: string; stderr: string }> {
|
|
37
|
+
try {
|
|
38
|
+
const proc = Bun.spawn(["git", ...args], {
|
|
39
|
+
cwd: root,
|
|
40
|
+
stdout: "pipe",
|
|
41
|
+
stderr: "pipe",
|
|
42
|
+
});
|
|
43
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
44
|
+
new Response(proc.stdout).text(),
|
|
45
|
+
new Response(proc.stderr).text(),
|
|
46
|
+
proc.exited,
|
|
47
|
+
]);
|
|
48
|
+
return { ok: exitCode === 0, stdout, stderr };
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return { ok: false, stdout: "", stderr: error instanceof Error ? error.message : String(error) };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe("agent context", () => {
|
|
37
55
|
let root: string;
|
|
38
56
|
|
|
39
57
|
beforeEach(async () => {
|
|
@@ -172,19 +190,48 @@ describe("agent context", () => {
|
|
|
172
190
|
includeContract: false,
|
|
173
191
|
});
|
|
174
192
|
|
|
175
|
-
expect(report.framework).toBe("mandu");
|
|
176
|
-
expect(report.ok).toBe(true);
|
|
177
|
-
expect(report.checks.map((check) => check.id)).toEqual(["manifest"]);
|
|
193
|
+
expect(report.framework).toBe("mandu");
|
|
194
|
+
expect(report.ok).toBe(true);
|
|
195
|
+
expect(report.checks.map((check) => check.id)).toEqual(["internal-api", "manifest"]);
|
|
178
196
|
expect(report.suggestedCommands.map((cmd) => cmd.command)).toContain("bun run typecheck");
|
|
179
197
|
expect(report.nextRepairInput).toBe(".mandu/agent-verify.json");
|
|
180
198
|
|
|
181
199
|
const result = await writeAgentVerifyReport(root, report);
|
|
182
200
|
expect(result.path).toBe(agentVerifyReportPath(root));
|
|
183
201
|
const parsed = JSON.parse(await fs.readFile(result.path, "utf8"));
|
|
184
|
-
expect(parsed.project.name).toBe("agent-app");
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("
|
|
202
|
+
expect(parsed.project.name).toBe("agent-app");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("records changed file reasons and warns on internal API edits", async () => {
|
|
206
|
+
const version = await runGit(root, ["--version"]);
|
|
207
|
+
if (!version.ok) return;
|
|
208
|
+
|
|
209
|
+
expect((await runGit(root, ["init"])).ok).toBe(true);
|
|
210
|
+
await runGit(root, ["config", "user.email", "agent@example.com"]);
|
|
211
|
+
await runGit(root, ["config", "user.name", "Agent"]);
|
|
212
|
+
expect((await runGit(root, ["add", "."])).ok).toBe(true);
|
|
213
|
+
expect((await runGit(root, ["commit", "-m", "initial"])).ok).toBe(true);
|
|
214
|
+
|
|
215
|
+
await writeFile(root, "packages/core/src/runtime/internal-change.ts", "export const value = 1;\n");
|
|
216
|
+
|
|
217
|
+
const report = await buildAgentVerifyReport(root, {
|
|
218
|
+
includeDiagnose: false,
|
|
219
|
+
includeGuard: false,
|
|
220
|
+
includeContract: false,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const reason = report.changedFileReasons.find(
|
|
224
|
+
(entry) => entry.file === "packages/core/src/runtime/internal-change.ts",
|
|
225
|
+
);
|
|
226
|
+
expect(reason?.internalApi).toBe(true);
|
|
227
|
+
expect(reason?.recommendedChecks).toContain("bun run check:public-api && bun run check:target-boundaries");
|
|
228
|
+
expect(report.diagnostics.some((diag) => diag.code === "MANDU_VERIFY_INTERNAL_API_EDIT")).toBe(true);
|
|
229
|
+
expect(report.suggestedCommands.map((cmd) => cmd.command)).toContain(
|
|
230
|
+
"bun run check:public-api && bun run check:target-boundaries",
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("turns a verify report into repair actions", async () => {
|
|
188
235
|
await writeAgentVerifyReport(root, {
|
|
189
236
|
schemaVersion: 1,
|
|
190
237
|
framework: "mandu",
|
|
@@ -195,9 +242,10 @@ describe("agent context", () => {
|
|
|
195
242
|
root,
|
|
196
243
|
packageManager: "bun",
|
|
197
244
|
configFile: null,
|
|
198
|
-
},
|
|
199
|
-
changedFiles: [],
|
|
200
|
-
|
|
245
|
+
},
|
|
246
|
+
changedFiles: [],
|
|
247
|
+
changedFileReasons: [],
|
|
248
|
+
gitAvailable: false,
|
|
201
249
|
notes: [],
|
|
202
250
|
ok: false,
|
|
203
251
|
checks: [],
|
package/src/agent/types.ts
CHANGED
|
@@ -153,19 +153,27 @@ export interface AgentVerifyCheck {
|
|
|
153
153
|
details?: Record<string, unknown>;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
export interface AgentSuggestedCommand {
|
|
157
|
-
command: string;
|
|
158
|
-
reason: string;
|
|
159
|
-
required: boolean;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export interface
|
|
156
|
+
export interface AgentSuggestedCommand {
|
|
157
|
+
command: string;
|
|
158
|
+
reason: string;
|
|
159
|
+
required: boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface AgentChangedFileReason {
|
|
163
|
+
file: string;
|
|
164
|
+
reasons: string[];
|
|
165
|
+
recommendedChecks: string[];
|
|
166
|
+
internalApi: boolean;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface AgentVerifyReport {
|
|
163
170
|
schemaVersion: 1;
|
|
164
171
|
framework: "mandu";
|
|
165
172
|
generatedAt: string;
|
|
166
|
-
project: AgentProjectSummary;
|
|
167
|
-
changedFiles: string[];
|
|
168
|
-
|
|
173
|
+
project: AgentProjectSummary;
|
|
174
|
+
changedFiles: string[];
|
|
175
|
+
changedFileReasons: AgentChangedFileReason[];
|
|
176
|
+
gitAvailable: boolean;
|
|
169
177
|
notes: string[];
|
|
170
178
|
ok: boolean;
|
|
171
179
|
checks: AgentVerifyCheck[];
|
package/src/agent/verify.ts
CHANGED
|
@@ -7,8 +7,9 @@ import type { ContractViolation } from "../guard/contract-guard";
|
|
|
7
7
|
import {
|
|
8
8
|
buildAgentContext,
|
|
9
9
|
} from "./context";
|
|
10
|
-
import type {
|
|
11
|
-
|
|
10
|
+
import type {
|
|
11
|
+
AgentChangedFileReason,
|
|
12
|
+
AgentDiagnostic,
|
|
12
13
|
AgentDiagnosticSeverity,
|
|
13
14
|
AgentSuggestedCommand,
|
|
14
15
|
AgentVerifyCheck,
|
|
@@ -208,7 +209,11 @@ function matchesChanged(
|
|
|
208
209
|
return candidates.some((file) => changed.has(file));
|
|
209
210
|
}
|
|
210
211
|
|
|
211
|
-
function commandSuggestions(
|
|
212
|
+
function commandSuggestions(
|
|
213
|
+
changedFiles: string[],
|
|
214
|
+
diagnostics: AgentDiagnostic[],
|
|
215
|
+
changedFileReasons: AgentChangedFileReason[] = [],
|
|
216
|
+
): AgentSuggestedCommand[] {
|
|
212
217
|
const files = changedFiles.map(normalizePath).filter((value): value is string => Boolean(value));
|
|
213
218
|
const out: AgentSuggestedCommand[] = [];
|
|
214
219
|
const add = (command: string, reason: string, required: boolean) => {
|
|
@@ -240,15 +245,101 @@ function commandSuggestions(changedFiles: string[], diagnostics: AgentDiagnostic
|
|
|
240
245
|
if (files.some((file) => file.startsWith("packages/mcp/"))) {
|
|
241
246
|
add("bun test packages/mcp/tests", "MCP package files changed.", true);
|
|
242
247
|
}
|
|
243
|
-
if (files.some((file) => file.includes("package.json") || file === "bun.lock")) {
|
|
244
|
-
add("bun run check:publish", "Package metadata or lockfile changed.", true);
|
|
245
|
-
}
|
|
246
|
-
if (
|
|
248
|
+
if (files.some((file) => file.includes("package.json") || file === "bun.lock")) {
|
|
249
|
+
add("bun run check:publish", "Package metadata or lockfile changed.", true);
|
|
250
|
+
}
|
|
251
|
+
if (changedFileReasons.some((entry) => entry.internalApi)) {
|
|
252
|
+
add("bun run check:public-api && bun run check:target-boundaries", "Internal framework boundaries changed.", true);
|
|
253
|
+
}
|
|
254
|
+
if (diagnostics.some((d) => d.severity === "error" || d.severity === "fatal")) {
|
|
247
255
|
add("mandu agent repair --from .mandu/agent-verify.json", "Verification produced repairable diagnostics.", false);
|
|
248
256
|
}
|
|
249
257
|
|
|
250
258
|
return out;
|
|
251
|
-
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function isInternalApiEdit(file: string): boolean {
|
|
262
|
+
return [
|
|
263
|
+
"packages/core/src/runtime/",
|
|
264
|
+
"packages/core/src/bundler/",
|
|
265
|
+
"packages/core/src/server/",
|
|
266
|
+
"packages/core/src/guard/",
|
|
267
|
+
"packages/core/src/spec/",
|
|
268
|
+
"packages/core/src/router/",
|
|
269
|
+
"packages/core/src/internal/",
|
|
270
|
+
].some((prefix) => file.startsWith(prefix));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function changedFileReason(file: string): AgentChangedFileReason {
|
|
274
|
+
const normalized = normalizePath(file) ?? file;
|
|
275
|
+
const reasons: string[] = [];
|
|
276
|
+
const recommendedChecks: string[] = [];
|
|
277
|
+
|
|
278
|
+
if (/\.(ts|tsx|js|jsx)$/.test(normalized)) {
|
|
279
|
+
reasons.push("Source code changed.");
|
|
280
|
+
recommendedChecks.push("bun run typecheck");
|
|
281
|
+
}
|
|
282
|
+
if (/\.test\.(ts|tsx|js|jsx)$/.test(normalized)) {
|
|
283
|
+
reasons.push("Test code changed.");
|
|
284
|
+
recommendedChecks.push(`bun test ${normalized}`);
|
|
285
|
+
}
|
|
286
|
+
if (normalized.startsWith("packages/cli/")) {
|
|
287
|
+
reasons.push("CLI behavior or documentation changed.");
|
|
288
|
+
recommendedChecks.push("bun test packages/cli/src");
|
|
289
|
+
}
|
|
290
|
+
if (normalized.startsWith("packages/mcp/")) {
|
|
291
|
+
reasons.push("MCP tool surface changed.");
|
|
292
|
+
recommendedChecks.push("bun test packages/mcp/tests");
|
|
293
|
+
}
|
|
294
|
+
if (normalized.startsWith("docs/") || normalized.endsWith("README.md") || normalized.endsWith("README.ko.md")) {
|
|
295
|
+
reasons.push("User-facing documentation changed.");
|
|
296
|
+
recommendedChecks.push("bun run check:docs-drift");
|
|
297
|
+
}
|
|
298
|
+
if (normalized === "package.json" || normalized === "bun.lock" || normalized.endsWith("/package.json")) {
|
|
299
|
+
reasons.push("Package metadata or dependency graph changed.");
|
|
300
|
+
recommendedChecks.push("bun run check:publish");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const internalApi = isInternalApiEdit(normalized);
|
|
304
|
+
if (internalApi) {
|
|
305
|
+
reasons.push("Framework internal API changed.");
|
|
306
|
+
recommendedChecks.push("bun run check:public-api && bun run check:target-boundaries");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
file: normalized,
|
|
311
|
+
reasons: reasons.length > 0 ? reasons : ["Changed file requires standard verification."],
|
|
312
|
+
recommendedChecks: [...new Set(recommendedChecks)],
|
|
313
|
+
internalApi,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function buildChangedFileReasons(changedFiles: string[]): AgentChangedFileReason[] {
|
|
318
|
+
return changedFiles
|
|
319
|
+
.map(normalizePath)
|
|
320
|
+
.filter((file): file is string => Boolean(file))
|
|
321
|
+
.map(changedFileReason);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function internalApiDiagnostics(changedFileReasons: AgentChangedFileReason[]): AgentDiagnostic[] {
|
|
325
|
+
return changedFileReasons
|
|
326
|
+
.filter((entry) => entry.internalApi)
|
|
327
|
+
.map((entry) => ({
|
|
328
|
+
code: "MANDU_VERIFY_INTERNAL_API_EDIT",
|
|
329
|
+
severity: "warning" as const,
|
|
330
|
+
title: "Internal framework API changed",
|
|
331
|
+
file: entry.file,
|
|
332
|
+
cause: "This file is part of Mandu's internal runtime/bundler/guard surface and can affect public behavior indirectly.",
|
|
333
|
+
suggestedFix: {
|
|
334
|
+
type: "run_command" as const,
|
|
335
|
+
command: "bun run check:public-api && bun run check:target-boundaries",
|
|
336
|
+
description: "Verify public API classification and target-safe import boundaries.",
|
|
337
|
+
},
|
|
338
|
+
docs: "docs/architect/public-api-boundary.md",
|
|
339
|
+
repairable: false,
|
|
340
|
+
source: "agent.verify",
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
252
343
|
|
|
253
344
|
function check(
|
|
254
345
|
id: string,
|
|
@@ -281,16 +372,22 @@ export async function buildAgentVerifyReport(
|
|
|
281
372
|
rootDir: string = process.cwd(),
|
|
282
373
|
options: BuildAgentVerifyOptions = {},
|
|
283
374
|
): Promise<AgentVerifyReport> {
|
|
284
|
-
const root = path.resolve(rootDir);
|
|
285
|
-
const changed = await collectChangedFiles(root, options);
|
|
286
|
-
const changedSet = new Set(changed.files.map(normalizePath).filter((value): value is string => Boolean(value)));
|
|
375
|
+
const root = path.resolve(rootDir);
|
|
376
|
+
const changed = await collectChangedFiles(root, options);
|
|
377
|
+
const changedSet = new Set(changed.files.map(normalizePath).filter((value): value is string => Boolean(value)));
|
|
378
|
+
const changedFileReasons = buildChangedFileReasons(changed.files);
|
|
287
379
|
const context = await buildAgentContext(root, {
|
|
288
380
|
includeDiagnose: false,
|
|
289
381
|
includeGit: false,
|
|
290
382
|
});
|
|
291
383
|
const notes = [...changed.notes];
|
|
292
|
-
const checks: AgentVerifyCheck[] = [];
|
|
293
|
-
const diagnostics: AgentDiagnostic[] = [];
|
|
384
|
+
const checks: AgentVerifyCheck[] = [];
|
|
385
|
+
const diagnostics: AgentDiagnostic[] = [];
|
|
386
|
+
const internalDiagnostics = internalApiDiagnostics(changedFileReasons);
|
|
387
|
+
diagnostics.push(...internalDiagnostics);
|
|
388
|
+
checks.push(check("internal-api", "Internal API boundary", internalDiagnostics, {
|
|
389
|
+
changedFiles: changedFileReasons.filter((entry) => entry.internalApi).length,
|
|
390
|
+
}));
|
|
294
391
|
|
|
295
392
|
if (options.includeDiagnose !== false) {
|
|
296
393
|
try {
|
|
@@ -382,14 +479,15 @@ export async function buildAgentVerifyReport(
|
|
|
382
479
|
schemaVersion: 1,
|
|
383
480
|
framework: "mandu",
|
|
384
481
|
generatedAt: new Date().toISOString(),
|
|
385
|
-
project: context.project,
|
|
386
|
-
changedFiles: changed.files,
|
|
387
|
-
|
|
482
|
+
project: context.project,
|
|
483
|
+
changedFiles: changed.files,
|
|
484
|
+
changedFileReasons,
|
|
485
|
+
gitAvailable: changed.gitAvailable,
|
|
388
486
|
notes,
|
|
389
487
|
ok,
|
|
390
488
|
checks,
|
|
391
489
|
diagnostics,
|
|
392
|
-
suggestedCommands: commandSuggestions(changed.files, diagnostics),
|
|
490
|
+
suggestedCommands: commandSuggestions(changed.files, diagnostics, changedFileReasons),
|
|
393
491
|
nextRepairInput: AGENT_VERIFY_RELATIVE_PATH,
|
|
394
492
|
};
|
|
395
493
|
}
|
|
@@ -248,7 +248,7 @@ describe("buildClientBundles vendor shims", () => {
|
|
|
248
248
|
expect(noClientResult.success).toBe(false);
|
|
249
249
|
expect(errors).toContain("no clientModule could be resolved");
|
|
250
250
|
expect(errors).toContain("LoginForm.client");
|
|
251
|
-
expect(errors).toContain("
|
|
251
|
+
expect(errors).toContain("run mandu generate");
|
|
252
252
|
} finally {
|
|
253
253
|
await rm(missingRoot, { recursive: true, force: true });
|
|
254
254
|
}
|
package/src/error/formatter.ts
CHANGED
|
@@ -21,15 +21,20 @@ export function formatErrorResponse(error: ManduError, options: FormatOptions =
|
|
|
21
21
|
|
|
22
22
|
const response: Record<string, unknown> = {
|
|
23
23
|
errorType: error.errorType,
|
|
24
|
-
code: error.code,
|
|
25
|
-
message: error.message,
|
|
26
|
-
summary: error.summary,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
code: error.code,
|
|
25
|
+
message: error.message,
|
|
26
|
+
summary: error.summary,
|
|
27
|
+
cause: error.summary,
|
|
28
|
+
fix: error.fix,
|
|
29
|
+
filePath: error.fix.file,
|
|
30
|
+
solution: error.fix.suggestion,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (error.route) {
|
|
34
|
+
response.route = error.route;
|
|
35
|
+
response.routeId = error.route.id;
|
|
36
|
+
response.routePattern = error.route.pattern;
|
|
37
|
+
}
|
|
33
38
|
|
|
34
39
|
// 개발 모드에서만 디버그 정보 포함
|
|
35
40
|
if (isDev && error.debug) {
|
|
@@ -60,27 +65,31 @@ export function formatErrorForConsole(error: ManduError, options: FormatOptions
|
|
|
60
65
|
lines.push(` ${error.message}`);
|
|
61
66
|
|
|
62
67
|
// 요약
|
|
63
|
-
if (useColors) {
|
|
64
|
-
lines.push(` ${CYAN}→ ${error.summary}${RESET}`);
|
|
65
|
-
} else {
|
|
66
|
-
lines.push(` → ${error.summary}`);
|
|
67
|
-
}
|
|
68
|
+
if (useColors) {
|
|
69
|
+
lines.push(` ${CYAN}→ ${error.summary}${RESET}`);
|
|
70
|
+
} else {
|
|
71
|
+
lines.push(` → ${error.summary}`);
|
|
72
|
+
}
|
|
73
|
+
lines.push(` Cause: ${error.summary}`);
|
|
68
74
|
|
|
69
75
|
// 수정 안내
|
|
70
76
|
lines.push("");
|
|
71
77
|
if (useColors) {
|
|
72
78
|
lines.push(` ${YELLOW}Fix:${RESET} ${error.fix.file}${error.fix.line ? `:${error.fix.line}` : ""}`);
|
|
73
79
|
lines.push(` ${error.fix.suggestion}`);
|
|
74
|
-
} else {
|
|
75
|
-
lines.push(` Fix: ${error.fix.file}${error.fix.line ? `:${error.fix.line}` : ""}`);
|
|
76
|
-
lines.push(` ${error.fix.suggestion}`);
|
|
77
|
-
}
|
|
80
|
+
} else {
|
|
81
|
+
lines.push(` Fix: ${error.fix.file}${error.fix.line ? `:${error.fix.line}` : ""}`);
|
|
82
|
+
lines.push(` ${error.fix.suggestion}`);
|
|
83
|
+
}
|
|
84
|
+
lines.push(` File: ${error.fix.file}${error.fix.line ? `:${error.fix.line}` : ""}`);
|
|
85
|
+
lines.push(` Solution: ${error.fix.suggestion}`);
|
|
78
86
|
|
|
79
87
|
// 라우트 컨텍스트
|
|
80
|
-
if (error.route) {
|
|
81
|
-
lines.push("");
|
|
82
|
-
lines.push(` Route: ${error.route.id}
|
|
83
|
-
}
|
|
88
|
+
if (error.route) {
|
|
89
|
+
lines.push("");
|
|
90
|
+
lines.push(` Route ID: ${error.route.id}`);
|
|
91
|
+
lines.push(` Route: ${error.route.pattern}`);
|
|
92
|
+
}
|
|
84
93
|
|
|
85
94
|
// 디버그 정보 (개발 모드)
|
|
86
95
|
if (isDev && includeStack && error.debug?.stack) {
|
|
@@ -2,6 +2,7 @@ import { describe, expect, it } from "bun:test";
|
|
|
2
2
|
import {
|
|
3
3
|
findClientComponentImports,
|
|
4
4
|
findRouteLevelClientComponentImport,
|
|
5
|
+
findRouteLevelClientComponentImports,
|
|
5
6
|
} from "./client-entry";
|
|
6
7
|
|
|
7
8
|
describe("findClientComponentImports", () => {
|
|
@@ -15,7 +16,7 @@ describe("findClientComponentImports", () => {
|
|
|
15
16
|
{
|
|
16
17
|
module: "@/client/widgets/login-form/LoginForm.client",
|
|
17
18
|
kind: "named",
|
|
18
|
-
names: ["LoginForm", "
|
|
19
|
+
names: ["LoginForm", "Button"],
|
|
19
20
|
},
|
|
20
21
|
{
|
|
21
22
|
module: "./Header.client.tsx",
|
|
@@ -60,7 +61,22 @@ describe("findClientComponentImports", () => {
|
|
|
60
61
|
});
|
|
61
62
|
});
|
|
62
63
|
|
|
63
|
-
it("
|
|
64
|
+
it("detects a named-imported client component when the page returns only that component", () => {
|
|
65
|
+
const routeClient = findRouteLevelClientComponentImport(`
|
|
66
|
+
import { NotificationsPage } from "@/client/pages/notifications/NotificationsPage.client";
|
|
67
|
+
|
|
68
|
+
export default function Page() {
|
|
69
|
+
return <NotificationsPage />;
|
|
70
|
+
}
|
|
71
|
+
`);
|
|
72
|
+
|
|
73
|
+
expect(routeClient).toEqual({
|
|
74
|
+
module: "@/client/pages/notifications/NotificationsPage.client",
|
|
75
|
+
localName: "NotificationsPage",
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("promotes embedded client imports inside a larger server page", () => {
|
|
64
80
|
const routeClient = findRouteLevelClientComponentImport(`
|
|
65
81
|
import HomeApp from "@/client/pages/home/HomeApp.client";
|
|
66
82
|
|
|
@@ -72,10 +88,13 @@ describe("findClientComponentImports", () => {
|
|
|
72
88
|
}
|
|
73
89
|
`);
|
|
74
90
|
|
|
75
|
-
expect(routeClient).
|
|
91
|
+
expect(routeClient).toEqual({
|
|
92
|
+
module: "@/client/pages/home/HomeApp.client",
|
|
93
|
+
localName: "HomeApp",
|
|
94
|
+
});
|
|
76
95
|
});
|
|
77
96
|
|
|
78
|
-
it("
|
|
97
|
+
it("promotes client imports when the page wrapper passes props", () => {
|
|
79
98
|
const routeClient = findRouteLevelClientComponentImport(`
|
|
80
99
|
import PledgePage from "@/client/pages/pledges/PledgePage.client";
|
|
81
100
|
|
|
@@ -84,6 +103,38 @@ describe("findClientComponentImports", () => {
|
|
|
84
103
|
}
|
|
85
104
|
`);
|
|
86
105
|
|
|
87
|
-
expect(routeClient).
|
|
106
|
+
expect(routeClient).toEqual({
|
|
107
|
+
module: "@/client/pages/pledges/PledgePage.client",
|
|
108
|
+
localName: "PledgePage",
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("detects multiple rendered client imports in a server shell", () => {
|
|
113
|
+
const routeClients = findRouteLevelClientComponentImports(`
|
|
114
|
+
import { CommentsSection } from "@/client/widgets/comments-section/CommentsSection.client";
|
|
115
|
+
import { PledgeActions } from "@/client/widgets/pledge-actions/PledgeActions.client";
|
|
116
|
+
|
|
117
|
+
export default async function PledgePage({ params }) {
|
|
118
|
+
const pledge = await Promise.resolve(params.id);
|
|
119
|
+
return (
|
|
120
|
+
<main>
|
|
121
|
+
<article>{pledge}</article>
|
|
122
|
+
<PledgeActions pledgeId={params.id} />
|
|
123
|
+
<CommentsSection pledgeId={params.id} />
|
|
124
|
+
</main>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
`);
|
|
128
|
+
|
|
129
|
+
expect(routeClients).toEqual([
|
|
130
|
+
{
|
|
131
|
+
module: "@/client/widgets/comments-section/CommentsSection.client",
|
|
132
|
+
localName: "CommentsSection",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
module: "@/client/widgets/pledge-actions/PledgeActions.client",
|
|
136
|
+
localName: "PledgeActions",
|
|
137
|
+
},
|
|
138
|
+
]);
|
|
88
139
|
});
|
|
89
140
|
});
|
|
@@ -62,7 +62,8 @@ export function findClientComponentImports(source: string): ClientComponentImpor
|
|
|
62
62
|
for (const rawName of namedMatch[1].split(",")) {
|
|
63
63
|
const name = rawName.trim();
|
|
64
64
|
if (!name) continue;
|
|
65
|
-
|
|
65
|
+
const parts = name.split(/\s+as\s+/i).map((part) => part.trim()).filter(Boolean);
|
|
66
|
+
names.push(parts[1] ?? parts[0]);
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
|
|
@@ -100,19 +101,18 @@ export function findClientComponentImports(source: string): ClientComponentImpor
|
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
export function findRouteLevelClientComponentImport(source: string): RouteLevelClientComponentImport | null {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return entry.kind === "default" && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(localName);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
if (defaultImports.length !== 1) return null;
|
|
104
|
+
return findRouteLevelClientComponentImports(source)[0] ?? null;
|
|
105
|
+
}
|
|
109
106
|
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
107
|
+
export function findRouteLevelClientComponentImports(source: string): RouteLevelClientComponentImport[] {
|
|
108
|
+
const candidates = findClientComponentImports(source).flatMap((entry) =>
|
|
109
|
+
entry.names
|
|
110
|
+
.filter((localName) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(localName))
|
|
111
|
+
.map((localName) => ({ module: entry.module, localName }))
|
|
112
|
+
);
|
|
114
113
|
|
|
115
|
-
|
|
114
|
+
if (candidates.length === 0) return [];
|
|
115
|
+
return defaultExportRendersClientComponents(source, candidates);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
export async function resolveClientImportModulePath(
|
|
@@ -168,15 +168,28 @@ function expandClientModuleCandidates(basePath: string): string[] {
|
|
|
168
168
|
];
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
function
|
|
171
|
+
function defaultExportRendersClientComponents(
|
|
172
|
+
source: string,
|
|
173
|
+
candidates: RouteLevelClientComponentImport[],
|
|
174
|
+
): RouteLevelClientComponentImport[] {
|
|
172
175
|
const functionBody = extractDefaultExportFunctionBody(source);
|
|
173
176
|
if (functionBody !== null) {
|
|
174
|
-
const returned =
|
|
175
|
-
return returned !== null
|
|
177
|
+
const returned = extractTopLevelReturnExpression(functionBody);
|
|
178
|
+
return returned !== null ? jsxExpressionRendersClientComponents(returned, candidates) : [];
|
|
176
179
|
}
|
|
177
180
|
|
|
178
181
|
const arrowExpression = extractDefaultExportArrowExpression(source);
|
|
179
|
-
|
|
182
|
+
if (arrowExpression !== null) {
|
|
183
|
+
return jsxExpressionRendersClientComponents(arrowExpression, candidates);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const arrowBody = extractDefaultExportArrowFunctionBody(source);
|
|
187
|
+
if (arrowBody !== null) {
|
|
188
|
+
const returned = extractTopLevelReturnExpression(arrowBody);
|
|
189
|
+
return returned !== null ? jsxExpressionRendersClientComponents(returned, candidates) : [];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return [];
|
|
180
193
|
}
|
|
181
194
|
|
|
182
195
|
function extractDefaultExportFunctionBody(source: string): string | null {
|
|
@@ -201,44 +214,109 @@ function extractDefaultExportArrowExpression(source: string): string | null {
|
|
|
201
214
|
return semicolon === -1 ? rest : rest.slice(0, semicolon);
|
|
202
215
|
}
|
|
203
216
|
|
|
204
|
-
function
|
|
205
|
-
const match =
|
|
206
|
-
|
|
217
|
+
function extractDefaultExportArrowFunctionBody(source: string): string | null {
|
|
218
|
+
const match = /export\s+default\s+(?:async\s+)?(?:\([^)]*\)|[A-Za-z_$][A-Za-z0-9_$]*)\s*=>\s*\{/m.exec(source);
|
|
219
|
+
if (!match) return null;
|
|
220
|
+
|
|
221
|
+
const openBrace = match.index + match[0].lastIndexOf("{");
|
|
222
|
+
const closeBrace = findMatchingBrace(source, openBrace);
|
|
223
|
+
if (closeBrace === -1) return null;
|
|
224
|
+
return source.slice(openBrace + 1, closeBrace);
|
|
207
225
|
}
|
|
208
226
|
|
|
209
|
-
function
|
|
210
|
-
|
|
211
|
-
|
|
227
|
+
function extractTopLevelReturnExpression(body: string): string | null {
|
|
228
|
+
let quote: '"' | "'" | "`" | null = null;
|
|
229
|
+
let lineComment = false;
|
|
230
|
+
let blockComment = false;
|
|
231
|
+
let braceDepth = 0;
|
|
232
|
+
let parenDepth = 0;
|
|
233
|
+
let bracketDepth = 0;
|
|
234
|
+
|
|
235
|
+
for (let i = 0; i < body.length; i++) {
|
|
236
|
+
const char = body[i];
|
|
237
|
+
const next = body[i + 1];
|
|
238
|
+
const prev = body[i - 1];
|
|
239
|
+
|
|
240
|
+
if (lineComment) {
|
|
241
|
+
if (char === "\n" || char === "\r") lineComment = false;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (blockComment) {
|
|
246
|
+
if (char === "*" && next === "/") {
|
|
247
|
+
blockComment = false;
|
|
248
|
+
i++;
|
|
249
|
+
}
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (quote) {
|
|
254
|
+
if (char === quote && prev !== "\\") quote = null;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (char === "/" && next === "/") {
|
|
259
|
+
lineComment = true;
|
|
260
|
+
i++;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (char === "/" && next === "*") {
|
|
264
|
+
blockComment = true;
|
|
265
|
+
i++;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
269
|
+
quote = char;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (braceDepth === 0 && parenDepth === 0 && bracketDepth === 0 && body.startsWith("return", i)) {
|
|
274
|
+
const before = body[i - 1] ?? "";
|
|
275
|
+
const after = body[i + "return".length] ?? "";
|
|
276
|
+
if (!isIdentifierChar(before) && !isIdentifierChar(after)) {
|
|
277
|
+
const expr = body.slice(i + "return".length).trim();
|
|
278
|
+
return trimTrailingSemicolon(expr);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (char === "{") braceDepth++;
|
|
283
|
+
if (char === "}") braceDepth = Math.max(0, braceDepth - 1);
|
|
284
|
+
if (char === "(") parenDepth++;
|
|
285
|
+
if (char === ")") parenDepth = Math.max(0, parenDepth - 1);
|
|
286
|
+
if (char === "[") bracketDepth++;
|
|
287
|
+
if (char === "]") bracketDepth = Math.max(0, bracketDepth - 1);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return null;
|
|
212
291
|
}
|
|
213
292
|
|
|
214
|
-
function
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
return new RegExp(`^<${escaped}\\s*/>$`).test(expr);
|
|
293
|
+
function trimTrailingSemicolon(value: string): string {
|
|
294
|
+
const trimmed = value.trim();
|
|
295
|
+
return trimmed.endsWith(";") ? trimmed.slice(0, -1).trim() : trimmed;
|
|
218
296
|
}
|
|
219
297
|
|
|
220
|
-
function
|
|
298
|
+
function jsxExpressionRendersClientComponents(
|
|
299
|
+
expression: string,
|
|
300
|
+
candidates: RouteLevelClientComponentImport[],
|
|
301
|
+
): RouteLevelClientComponentImport[] {
|
|
221
302
|
const expr = stripWrappingParentheses(expression.trim());
|
|
222
|
-
const
|
|
223
|
-
|
|
303
|
+
const seen = new Set<string>();
|
|
304
|
+
const rendered: RouteLevelClientComponentImport[] = [];
|
|
305
|
+
|
|
306
|
+
for (const candidate of candidates) {
|
|
307
|
+
const key = `${candidate.module}\0${candidate.localName}`;
|
|
308
|
+
if (seen.has(key)) continue;
|
|
309
|
+
if (!jsxExpressionContainsClientElement(expr, candidate.localName)) continue;
|
|
310
|
+
seen.add(key);
|
|
311
|
+
rendered.push(candidate);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return rendered;
|
|
315
|
+
}
|
|
224
316
|
|
|
317
|
+
function jsxExpressionContainsClientElement(expression: string, localName: string): boolean {
|
|
225
318
|
const escaped = escapeRegExp(localName);
|
|
226
|
-
|
|
227
|
-
const matches = [...(fragmentMatch[1] ?? "").matchAll(clientElementPattern)];
|
|
228
|
-
if (matches.length !== 1) return false;
|
|
229
|
-
const clientElement = matches[0][0];
|
|
230
|
-
if (!isBareClientElement(clientElement, localName)) return false;
|
|
231
|
-
|
|
232
|
-
const rest = (fragmentMatch[1] ?? "")
|
|
233
|
-
.replace(clientElement, "")
|
|
234
|
-
.replace(/<meta\b[^>]*\/>/gi, "")
|
|
235
|
-
.replace(/<link\b[^>]*\/>/gi, "")
|
|
236
|
-
.replace(/<base\b[^>]*\/>/gi, "")
|
|
237
|
-
.replace(/<title\b[^>]*>[\s\S]*?<\/title>/gi, "")
|
|
238
|
-
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "")
|
|
239
|
-
.trim();
|
|
240
|
-
|
|
241
|
-
return rest.length === 0;
|
|
319
|
+
return new RegExp(`<${escaped}(?:\\s|/|>)`).test(expression);
|
|
242
320
|
}
|
|
243
321
|
|
|
244
322
|
function stripWrappingParentheses(value: string): string {
|
|
@@ -317,6 +395,10 @@ function escapeRegExp(value: string): string {
|
|
|
317
395
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
318
396
|
}
|
|
319
397
|
|
|
398
|
+
function isIdentifierChar(value: string): boolean {
|
|
399
|
+
return /[A-Za-z0-9_$]/.test(value);
|
|
400
|
+
}
|
|
401
|
+
|
|
320
402
|
export async function validateClientModuleForBrowserBundle(
|
|
321
403
|
route: RouteSpec,
|
|
322
404
|
rootDir: string,
|
|
@@ -349,9 +431,13 @@ async function routeComponentHasResolvableClientEntry(
|
|
|
349
431
|
routeModule: string,
|
|
350
432
|
source: string,
|
|
351
433
|
): Promise<boolean> {
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
434
|
+
const routeLevelClientImports = findRouteLevelClientComponentImports(source);
|
|
435
|
+
for (const routeLevelClientImport of routeLevelClientImports) {
|
|
436
|
+
if ((await resolveClientImportModulePath(rootDir, routeModule, routeLevelClientImport.module)) !== null) {
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return false;
|
|
355
441
|
}
|
|
356
442
|
|
|
357
443
|
export async function describeMissingHydrationClientModule(
|
|
@@ -393,8 +479,8 @@ export async function describeMissingHydrationClientModule(
|
|
|
393
479
|
|
|
394
480
|
return (
|
|
395
481
|
`${base}\n` +
|
|
396
|
-
` The page imports client-looking modules, but
|
|
482
|
+
` The page imports client-looking modules, but none was linked into the route manifest:\n` +
|
|
397
483
|
`${importList}\n` +
|
|
398
|
-
` Fix:
|
|
484
|
+
` Fix: run mandu generate with the current source, or set an explicit route-level clientModule.`
|
|
399
485
|
);
|
|
400
486
|
}
|
package/src/router/fs-scanner.ts
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
import { mark, measure } from "../perf";
|
|
32
32
|
import { METADATA_ROUTES } from "../routes/types";
|
|
33
33
|
import {
|
|
34
|
-
|
|
34
|
+
findRouteLevelClientComponentImports,
|
|
35
35
|
hasUseClientDirective,
|
|
36
36
|
resolveClientImportModulePath,
|
|
37
37
|
} from "./client-entry";
|
|
@@ -399,8 +399,8 @@ export class FSScanner {
|
|
|
399
399
|
if (hasUseClient) {
|
|
400
400
|
clientModule = modulePath;
|
|
401
401
|
} else {
|
|
402
|
-
const
|
|
403
|
-
|
|
402
|
+
const routeLevelClientImports = findRouteLevelClientComponentImports(pageFileContent);
|
|
403
|
+
for (const routeLevelClientImport of routeLevelClientImports) {
|
|
404
404
|
const resolvedClientImport = await resolveClientImportModulePath(
|
|
405
405
|
rootDir,
|
|
406
406
|
modulePath,
|
|
@@ -408,6 +408,7 @@ export class FSScanner {
|
|
|
408
408
|
);
|
|
409
409
|
if (resolvedClientImport) {
|
|
410
410
|
clientModule = modulePath;
|
|
411
|
+
break;
|
|
411
412
|
}
|
|
412
413
|
}
|
|
413
414
|
}
|