@mandujs/core 0.54.7 → 0.54.9
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/devtools/client/components/panel/islands-panel.tsx +16 -16
- package/src/devtools/client/components/panel/panel-container.tsx +1 -1
- package/src/generator/templates.test.ts +65 -0
- package/src/generator/templates.ts +210 -101
- package/src/router/client-entry.test.ts +19 -1
- package/src/router/client-entry.ts +47 -4
- package/src/router/fs-scanner.ts +5 -2
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.9",
|
|
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
|
+
}
|
|
@@ -183,28 +183,28 @@ export function IslandsPanel({ islands }: IslandsPanelProps): React.ReactElement
|
|
|
183
183
|
);
|
|
184
184
|
}, [islands]);
|
|
185
185
|
|
|
186
|
-
if (islands.length === 0) {
|
|
187
|
-
return (
|
|
188
|
-
<div style={styles.container}>
|
|
189
|
-
<div style={styles.emptyState}>
|
|
190
|
-
<p>
|
|
191
|
-
아직
|
|
192
|
-
|
|
193
|
-
</p>
|
|
194
|
-
</div>
|
|
195
|
-
</div>
|
|
196
|
-
);
|
|
197
|
-
}
|
|
186
|
+
if (islands.length === 0) {
|
|
187
|
+
return (
|
|
188
|
+
<div style={styles.container}>
|
|
189
|
+
<div style={styles.emptyState}>
|
|
190
|
+
<p>
|
|
191
|
+
아직 hydration 이벤트가 없습니다.<br />
|
|
192
|
+
정적 상태는 mandu.runtime.status에서 확인하세요.
|
|
193
|
+
</p>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
198
|
|
|
199
199
|
return (
|
|
200
200
|
<div style={styles.container}>
|
|
201
201
|
{/* Header Stats */}
|
|
202
202
|
<div style={styles.header}>
|
|
203
203
|
<div style={styles.stats}>
|
|
204
|
-
<div style={styles.stat}>
|
|
205
|
-
<span
|
|
206
|
-
<span style={styles.statValue}>{stats.total}</span>
|
|
207
|
-
</div>
|
|
204
|
+
<div style={styles.stat}>
|
|
205
|
+
<span>Mounts:</span>
|
|
206
|
+
<span style={styles.statValue}>{stats.total}</span>
|
|
207
|
+
</div>
|
|
208
208
|
<div style={styles.stat}>
|
|
209
209
|
<span>Ready</span>
|
|
210
210
|
<span style={styles.statValue}>{stats.hydrated}</span>
|
|
@@ -28,7 +28,7 @@ export interface PanelContainerProps {
|
|
|
28
28
|
|
|
29
29
|
export const TABS: TabDefinition[] = [
|
|
30
30
|
{ id: 'errors', label: 'Issues', icon: 'ERR', testId: testIds.tabErrors },
|
|
31
|
-
{ id: 'islands', label: '
|
|
31
|
+
{ id: 'islands', label: 'Client Mounts', icon: 'HYD', testId: testIds.tabIslands },
|
|
32
32
|
{ id: 'network', label: 'Network', icon: 'NET', testId: testIds.tabNetwork },
|
|
33
33
|
{ id: 'guard', label: 'Guard', icon: 'GRD', testId: testIds.tabGuard },
|
|
34
34
|
{ id: 'preview', label: 'Changes', icon: 'CHG', testId: testIds.tabPreview },
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { RouteSpec } from "../spec/schema";
|
|
3
|
+
import { generatePageComponent } from "./templates";
|
|
4
|
+
|
|
5
|
+
describe("generatePageComponent", () => {
|
|
6
|
+
test("refuses to emit a placeholder for hydrating routes without a clientModule", () => {
|
|
7
|
+
const route: RouteSpec = {
|
|
8
|
+
id: "login",
|
|
9
|
+
kind: "page",
|
|
10
|
+
pattern: "/login",
|
|
11
|
+
module: "app/login/page.tsx",
|
|
12
|
+
componentModule: "app/login/page.tsx",
|
|
13
|
+
hydration: {
|
|
14
|
+
strategy: "full",
|
|
15
|
+
priority: "immediate",
|
|
16
|
+
preload: false,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
expect(() => generatePageComponent(route)).toThrow("no clientModule");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("imports the real page module for static routes instead of emitting a placeholder", () => {
|
|
24
|
+
const route: RouteSpec = {
|
|
25
|
+
id: "about",
|
|
26
|
+
kind: "page",
|
|
27
|
+
pattern: "/about",
|
|
28
|
+
module: "app/about/page.tsx",
|
|
29
|
+
componentModule: "app/about/page.tsx",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const generated = generatePageComponent(route);
|
|
33
|
+
|
|
34
|
+
expect(generated).toContain("Page Module: app/about/page.tsx");
|
|
35
|
+
expect(generated).toContain('import pageModule from "../../../../app/about/page.tsx"');
|
|
36
|
+
expect(generated).toContain("React.createElement(pageModule");
|
|
37
|
+
expect(generated).not.toContain("About Page");
|
|
38
|
+
expect(generated).not.toContain('React.createElement("p", null, "Route ID: about")');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("renders route-level default-imported client components through the page module", () => {
|
|
42
|
+
const route: RouteSpec = {
|
|
43
|
+
id: "login",
|
|
44
|
+
kind: "page",
|
|
45
|
+
pattern: "/login",
|
|
46
|
+
module: "app/login/page.tsx",
|
|
47
|
+
componentModule: "app/login/page.tsx",
|
|
48
|
+
clientModule: "app/login/page.tsx",
|
|
49
|
+
hydration: {
|
|
50
|
+
strategy: "island",
|
|
51
|
+
priority: "immediate",
|
|
52
|
+
preload: false,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const generated = generatePageComponent(route);
|
|
57
|
+
|
|
58
|
+
expect(generated).toContain("Client Module: app/login/page.tsx");
|
|
59
|
+
expect(generated).toContain("Page Module: app/login/page.tsx");
|
|
60
|
+
expect(generated).toContain('import islandModule from "../../../../app/login/page.tsx"');
|
|
61
|
+
expect(generated).toContain("islandModule.definition.render");
|
|
62
|
+
expect(generated).toContain("React.createElement(islandModule");
|
|
63
|
+
expect(generated).not.toContain("Login Page");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { needsHydration, type RouteSpec } from "../spec/schema";
|
|
2
2
|
import { GENERATED_RELATIVE_PATHS } from "../paths";
|
|
3
3
|
|
|
4
4
|
export function generateApiHandler(route: RouteSpec): string {
|
|
@@ -238,23 +238,34 @@ function computeSlotImportPath(slotModule: string, fromDir: string): string {
|
|
|
238
238
|
return result;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
export function generatePageComponent(route: RouteSpec): string {
|
|
242
|
-
// Island-First: clientModule이 있으면 Island render를 SSR에서 직접 사용
|
|
243
|
-
if (route.clientModule) {
|
|
244
|
-
return generatePageComponentWithIsland(route);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
// slotModule이
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
241
|
+
export function generatePageComponent(route: RouteSpec): string {
|
|
242
|
+
// Island-First: clientModule이 있으면 Island render를 SSR에서 직접 사용
|
|
243
|
+
if (route.clientModule) {
|
|
244
|
+
return generatePageComponentWithIsland(route);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (needsHydration(route)) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`[${route.id}] Route has hydration strategy "${route.hydration?.strategy}" but no clientModule. ` +
|
|
250
|
+
"Refusing to generate a placeholder page because it would disagree with runtime hydration state.",
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// slotModule이 있으면 PageHandler 형식으로 생성 (filling 포함)
|
|
255
|
+
if (route.slotModule) {
|
|
256
|
+
return generatePageHandlerWithSlot(route);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (route.kind === "page" && route.componentModule) {
|
|
260
|
+
return generatePageComponentFromModule(route);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const pageName = toPascalCase(route.id);
|
|
264
|
+
|
|
265
|
+
// Legacy fallback for malformed historical manifests that lack componentModule.
|
|
266
|
+
return `// Generated by Mandu - DO NOT EDIT DIRECTLY
|
|
267
|
+
// Route ID: ${route.id}
|
|
268
|
+
// Pattern: ${route.pattern}
|
|
258
269
|
|
|
259
270
|
import React from "react";
|
|
260
271
|
|
|
@@ -270,8 +281,31 @@ export default function ${pageName}Page({ params }: Props): React.ReactElement {
|
|
|
270
281
|
React.createElement("p", null, "Pattern: ${route.pattern}")
|
|
271
282
|
);
|
|
272
283
|
}
|
|
273
|
-
`;
|
|
274
|
-
}
|
|
284
|
+
`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function generatePageComponentFromModule(route: RouteSpec): string {
|
|
288
|
+
const pageName = toPascalCase(route.id);
|
|
289
|
+
const pageImportPath = computeSlotImportPath(route.componentModule!, GENERATED_RELATIVE_PATHS.webRoutes);
|
|
290
|
+
|
|
291
|
+
return `// Generated by Mandu - DO NOT EDIT DIRECTLY
|
|
292
|
+
// Route ID: ${route.id}
|
|
293
|
+
// Pattern: ${route.pattern}
|
|
294
|
+
// Page Module: ${route.componentModule}
|
|
295
|
+
|
|
296
|
+
import React from "react";
|
|
297
|
+
import pageModule from "${pageImportPath}";
|
|
298
|
+
|
|
299
|
+
interface Props {
|
|
300
|
+
params: Record<string, string>;
|
|
301
|
+
loaderData?: unknown;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export default function ${pageName}Page(props: Props): React.ReactElement {
|
|
305
|
+
return React.createElement(pageModule as React.ComponentType<Props>, props);
|
|
306
|
+
}
|
|
307
|
+
`;
|
|
308
|
+
}
|
|
275
309
|
|
|
276
310
|
/**
|
|
277
311
|
* Island-First Rendering: SSR이 island의 render 함수를 직접 사용
|
|
@@ -279,37 +313,51 @@ export default function ${pageName}Page({ params }: Props): React.ReactElement {
|
|
|
279
313
|
* - SSR과 클라이언트가 동일한 렌더링 로직 사용 → 불일치 구조적 방지
|
|
280
314
|
* - slotModule 유무에 따라 두 가지 변형 생성
|
|
281
315
|
*/
|
|
282
|
-
export function generatePageComponentWithIsland(route: RouteSpec): string {
|
|
283
|
-
const pageName = toPascalCase(route.id);
|
|
284
|
-
const clientImportPath = computeSlotImportPath(route.clientModule!, GENERATED_RELATIVE_PATHS.webRoutes);
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
316
|
+
export function generatePageComponentWithIsland(route: RouteSpec): string {
|
|
317
|
+
const pageName = toPascalCase(route.id);
|
|
318
|
+
const clientImportPath = computeSlotImportPath(route.clientModule!, GENERATED_RELATIVE_PATHS.webRoutes);
|
|
319
|
+
const pageImportPath = route.kind === "page" && route.componentModule
|
|
320
|
+
? computeSlotImportPath(route.componentModule, GENERATED_RELATIVE_PATHS.webRoutes)
|
|
321
|
+
: null;
|
|
322
|
+
const pageModuleComment = route.kind === "page" && route.componentModule
|
|
323
|
+
? `// Page Module: ${route.componentModule}\n`
|
|
324
|
+
: "";
|
|
325
|
+
const shouldImportPageModule = !!pageImportPath &&
|
|
326
|
+
normalizeRouteModulePath(route.componentModule) !== normalizeRouteModulePath(route.clientModule);
|
|
327
|
+
const pageImport = shouldImportPageModule ? `import pageModule from "${pageImportPath}";\n` : "";
|
|
328
|
+
const pageRenderTarget = shouldImportPageModule ? "pageModule" : "islandModule";
|
|
329
|
+
const renderHelper = generateClientBackedPageRenderHelper(pageRenderTarget, route.id);
|
|
330
|
+
|
|
331
|
+
// clientModule + slotModule → PageRegistration 형식
|
|
332
|
+
if (route.slotModule) {
|
|
333
|
+
const slotImportPath = computeSlotImportPath(route.slotModule!, GENERATED_RELATIVE_PATHS.webRoutes);
|
|
289
334
|
|
|
290
335
|
return `// Generated by Mandu - DO NOT EDIT DIRECTLY
|
|
291
336
|
// Island-First Rendering + Slot Module
|
|
292
337
|
// Route ID: ${route.id}
|
|
293
|
-
// Pattern: ${route.pattern}
|
|
294
|
-
// Client Module: ${route.clientModule}
|
|
295
|
-
// Slot Module: ${route.slotModule}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
import
|
|
299
|
-
import
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
|
|
338
|
+
// Pattern: ${route.pattern}
|
|
339
|
+
// Client Module: ${route.clientModule}
|
|
340
|
+
// Slot Module: ${route.slotModule}
|
|
341
|
+
${pageModuleComment}
|
|
342
|
+
|
|
343
|
+
import React from "react";
|
|
344
|
+
import filling from "${slotImportPath}";
|
|
345
|
+
import islandModule from "${clientImportPath}";
|
|
346
|
+
${pageImport}
|
|
347
|
+
|
|
348
|
+
interface Props {
|
|
349
|
+
params: Record<string, string>;
|
|
350
|
+
loaderData?: unknown;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
${renderHelper}
|
|
354
|
+
|
|
355
|
+
function ${pageName}Page({ params, loaderData }: Props): React.ReactElement {
|
|
356
|
+
const serverData = (loaderData || {}) as Record<string, unknown>;
|
|
357
|
+
return renderClientBackedPage({ params, loaderData }, serverData);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// PageRegistration 형식으로 export (server.ts의 registerPageHandler용)
|
|
313
361
|
export default {
|
|
314
362
|
component: ${pageName}Page,
|
|
315
363
|
filling: filling,
|
|
@@ -320,67 +368,124 @@ export default {
|
|
|
320
368
|
// clientModule만 (slotModule 없음) → default export 컴포넌트
|
|
321
369
|
return `// Generated by Mandu - DO NOT EDIT DIRECTLY
|
|
322
370
|
// Island-First Rendering: SSR이 island render 직접 사용
|
|
323
|
-
// Route ID: ${route.id}
|
|
324
|
-
// Pattern: ${route.pattern}
|
|
325
|
-
// Client Module: ${route.clientModule}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
import
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
}
|
|
371
|
+
// Route ID: ${route.id}
|
|
372
|
+
// Pattern: ${route.pattern}
|
|
373
|
+
// Client Module: ${route.clientModule}
|
|
374
|
+
${pageModuleComment}
|
|
375
|
+
|
|
376
|
+
import React from "react";
|
|
377
|
+
import islandModule from "${clientImportPath}";
|
|
378
|
+
${pageImport}
|
|
379
|
+
|
|
380
|
+
interface Props {
|
|
381
|
+
params: Record<string, string>;
|
|
382
|
+
loaderData?: unknown;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
${renderHelper}
|
|
386
|
+
|
|
387
|
+
export default function ${pageName}Page({ params, loaderData }: Props): React.ReactElement {
|
|
388
|
+
const serverData = (loaderData || {}) as Record<string, unknown>;
|
|
389
|
+
return renderClientBackedPage({ params, loaderData }, serverData);
|
|
390
|
+
}
|
|
391
|
+
`;
|
|
392
|
+
}
|
|
342
393
|
|
|
343
394
|
/**
|
|
344
395
|
* slotModule이 있는 Page Route용 Handler 생성
|
|
345
396
|
* - component와 filling을 함께 export
|
|
346
397
|
* - server.ts에서 filling.executeLoader() 호출 가능
|
|
347
398
|
*/
|
|
348
|
-
export function generatePageHandlerWithSlot(route: RouteSpec): string {
|
|
349
|
-
const pageName = toPascalCase(route.id);
|
|
350
|
-
const slotImportPath = computeSlotImportPath(route.slotModule!, GENERATED_RELATIVE_PATHS.serverRoutes);
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
399
|
+
export function generatePageHandlerWithSlot(route: RouteSpec): string {
|
|
400
|
+
const pageName = toPascalCase(route.id);
|
|
401
|
+
const slotImportPath = computeSlotImportPath(route.slotModule!, GENERATED_RELATIVE_PATHS.serverRoutes);
|
|
402
|
+
const pageImportPath = route.kind === "page" && route.componentModule
|
|
403
|
+
? computeSlotImportPath(route.componentModule, GENERATED_RELATIVE_PATHS.serverRoutes)
|
|
404
|
+
: null;
|
|
405
|
+
const pageImport = pageImportPath ? `import pageModule from "${pageImportPath}";\n` : "";
|
|
406
|
+
const pageRender = pageImportPath
|
|
407
|
+
? ` return React.createElement(pageModule as React.ComponentType<Props>, { params, loaderData });`
|
|
408
|
+
: ` return React.createElement("div", null,
|
|
409
|
+
React.createElement("h1", null, "${pageName} Page"),
|
|
410
|
+
React.createElement("p", null, "Route ID: ${route.id}"),
|
|
411
|
+
React.createElement("p", null, "Pattern: ${route.pattern}"),
|
|
412
|
+
loaderData ? React.createElement("pre", null, JSON.stringify(loaderData, null, 2)) : null
|
|
413
|
+
);`;
|
|
414
|
+
|
|
415
|
+
return `// Generated by Mandu - DO NOT EDIT DIRECTLY
|
|
416
|
+
// Route ID: ${route.id}
|
|
417
|
+
// Pattern: ${route.pattern}
|
|
418
|
+
// Slot Module: ${route.slotModule}
|
|
419
|
+
|
|
420
|
+
import React from "react";
|
|
421
|
+
import filling from "${slotImportPath}";
|
|
422
|
+
${pageImport}
|
|
423
|
+
|
|
424
|
+
interface Props {
|
|
425
|
+
params: Record<string, string>;
|
|
426
|
+
loaderData?: unknown;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function ${pageName}Page({ params, loaderData }: Props): React.ReactElement {
|
|
430
|
+
${pageRender}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// PageRegistration 형식으로 export (server.ts의 registerPageHandler용)
|
|
375
434
|
export default {
|
|
376
435
|
component: ${pageName}Page,
|
|
377
436
|
filling: filling,
|
|
378
437
|
};
|
|
379
|
-
`;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
438
|
+
`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function generateClientBackedPageRenderHelper(pageRenderTarget: string, routeId: string): string {
|
|
442
|
+
return `type ManduIslandModule = {
|
|
443
|
+
__mandu_island: true;
|
|
444
|
+
definition: {
|
|
445
|
+
setup: (serverData: Record<string, unknown>) => unknown;
|
|
446
|
+
render: (props: unknown) => React.ReactNode;
|
|
447
|
+
};
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
function isManduIslandModule(value: unknown): value is ManduIslandModule {
|
|
451
|
+
const candidate = value as Partial<ManduIslandModule> | null;
|
|
452
|
+
return !!(
|
|
453
|
+
candidate &&
|
|
454
|
+
typeof candidate === "object" &&
|
|
455
|
+
candidate.__mandu_island === true &&
|
|
456
|
+
typeof candidate.definition?.setup === "function" &&
|
|
457
|
+
typeof candidate.definition?.render === "function"
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function renderClientBackedPage(props: Props, serverData: Record<string, unknown>): React.ReactElement {
|
|
462
|
+
if (isManduIslandModule(islandModule)) {
|
|
463
|
+
const setupResult = islandModule.definition.setup(serverData);
|
|
464
|
+
return islandModule.definition.render(setupResult) as React.ReactElement;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (typeof ${pageRenderTarget} === "function") {
|
|
468
|
+
return React.createElement(${pageRenderTarget} as React.ComponentType<Props>, props);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (typeof islandModule === "function") {
|
|
472
|
+
return React.createElement(islandModule as React.ComponentType<Record<string, unknown>>, serverData);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (React.isValidElement(${pageRenderTarget})) {
|
|
476
|
+
return ${pageRenderTarget} as React.ReactElement;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (React.isValidElement(islandModule)) {
|
|
480
|
+
return islandModule as React.ReactElement;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
throw new Error("[Mandu] Route ${routeId} clientModule must export a Mandu island or React component.");
|
|
484
|
+
}`;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Convert string to PascalCase (handles kebab-case, snake_case)
|
|
384
489
|
* "todo-page" → "TodoPage"
|
|
385
490
|
* "user_profile" → "UserProfile"
|
|
386
491
|
*/
|
|
@@ -391,6 +496,10 @@ function toPascalCase(str: string): string {
|
|
|
391
496
|
.join("");
|
|
392
497
|
}
|
|
393
498
|
|
|
394
|
-
function pathDirname(filePath: string): string {
|
|
395
|
-
return filePath.replace(/\\/g, "/").split("/").slice(0, -1).join("/");
|
|
396
|
-
}
|
|
499
|
+
function pathDirname(filePath: string): string {
|
|
500
|
+
return filePath.replace(/\\/g, "/").split("/").slice(0, -1).join("/");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function normalizeRouteModulePath(filePath: string | undefined): string {
|
|
504
|
+
return (filePath ?? "").replace(/\\/g, "/").replace(/^\.\//, "");
|
|
505
|
+
}
|
|
@@ -42,7 +42,7 @@ describe("findClientComponentImports", () => {
|
|
|
42
42
|
});
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
it("
|
|
45
|
+
it("promotes a fragment wrapper with head-only elements and a bare client component", () => {
|
|
46
46
|
const routeClient = findRouteLevelClientComponentImport(`
|
|
47
47
|
import HomeApp from "@/client/pages/home/HomeApp.client";
|
|
48
48
|
|
|
@@ -54,6 +54,24 @@ describe("findClientComponentImports", () => {
|
|
|
54
54
|
}
|
|
55
55
|
`);
|
|
56
56
|
|
|
57
|
+
expect(routeClient).toEqual({
|
|
58
|
+
module: "@/client/pages/home/HomeApp.client",
|
|
59
|
+
localName: "HomeApp",
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("does not promote embedded client imports inside a larger server page", () => {
|
|
64
|
+
const routeClient = findRouteLevelClientComponentImport(`
|
|
65
|
+
import HomeApp from "@/client/pages/home/HomeApp.client";
|
|
66
|
+
|
|
67
|
+
export default function HomePage() {
|
|
68
|
+
return <>
|
|
69
|
+
<header>Server shell</header>
|
|
70
|
+
<HomeApp />
|
|
71
|
+
</>;
|
|
72
|
+
}
|
|
73
|
+
`);
|
|
74
|
+
|
|
57
75
|
expect(routeClient).toBeNull();
|
|
58
76
|
});
|
|
59
77
|
|
|
@@ -141,7 +141,8 @@ export async function shouldPreserveExistingClientModule(
|
|
|
141
141
|
if (source === null) return false;
|
|
142
142
|
if (hasUseServerDirective(source)) return false;
|
|
143
143
|
if (clientModuleIsRouteComponent(route, clientModule)) {
|
|
144
|
-
|
|
144
|
+
if (hasUseClientDirective(source)) return true;
|
|
145
|
+
return await routeComponentHasResolvableClientEntry(rootDir, clientModule, source);
|
|
145
146
|
}
|
|
146
147
|
return true;
|
|
147
148
|
}
|
|
@@ -171,11 +172,11 @@ function defaultExportReturnsOnlyClientComponent(source: string, localName: stri
|
|
|
171
172
|
const functionBody = extractDefaultExportFunctionBody(source);
|
|
172
173
|
if (functionBody !== null) {
|
|
173
174
|
const returned = extractOnlyReturnExpression(functionBody);
|
|
174
|
-
return returned !== null &&
|
|
175
|
+
return returned !== null && jsxExpressionRendersClientComponent(returned, localName);
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
const arrowExpression = extractDefaultExportArrowExpression(source);
|
|
178
|
-
return arrowExpression !== null &&
|
|
179
|
+
return arrowExpression !== null && jsxExpressionRendersClientComponent(arrowExpression, localName);
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
function extractDefaultExportFunctionBody(source: string): string | null {
|
|
@@ -205,12 +206,41 @@ function extractOnlyReturnExpression(body: string): string | null {
|
|
|
205
206
|
return match?.[1]?.trim() ?? null;
|
|
206
207
|
}
|
|
207
208
|
|
|
208
|
-
function
|
|
209
|
+
function jsxExpressionRendersClientComponent(expression: string, localName: string): boolean {
|
|
210
|
+
const expr = stripWrappingParentheses(expression.trim());
|
|
211
|
+
return isBareClientElement(expr, localName) || isHeadOnlyFragmentWrapper(expr, localName);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isBareClientElement(expression: string, localName: string): boolean {
|
|
209
215
|
const expr = stripWrappingParentheses(expression.trim());
|
|
210
216
|
const escaped = escapeRegExp(localName);
|
|
211
217
|
return new RegExp(`^<${escaped}\\s*/>$`).test(expr);
|
|
212
218
|
}
|
|
213
219
|
|
|
220
|
+
function isHeadOnlyFragmentWrapper(expression: string, localName: string): boolean {
|
|
221
|
+
const expr = stripWrappingParentheses(expression.trim());
|
|
222
|
+
const fragmentMatch = /^<>\s*([\s\S]*?)\s*<\/>$/.exec(expr);
|
|
223
|
+
if (!fragmentMatch) return false;
|
|
224
|
+
|
|
225
|
+
const escaped = escapeRegExp(localName);
|
|
226
|
+
const clientElementPattern = new RegExp(`<${escaped}(?:\\s[^>]*)?(?:/>|>)`, "g");
|
|
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;
|
|
242
|
+
}
|
|
243
|
+
|
|
214
244
|
function stripWrappingParentheses(value: string): string {
|
|
215
245
|
let current = value.trim();
|
|
216
246
|
while (current.startsWith("(") && current.endsWith(")")) {
|
|
@@ -301,6 +331,9 @@ export async function validateClientModuleForBrowserBundle(
|
|
|
301
331
|
}
|
|
302
332
|
|
|
303
333
|
if (clientModuleIsRouteComponent(route) && !hasUseClientDirective(source)) {
|
|
334
|
+
if (await routeComponentHasResolvableClientEntry(rootDir, route.clientModule, source)) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
304
337
|
return (
|
|
305
338
|
`[${route.id}] Route component "${route.clientModule}" is configured as clientModule, ` +
|
|
306
339
|
`but it is a server page (missing "use client"). Mandu will not bundle server pages into client islands. ` +
|
|
@@ -311,6 +344,16 @@ export async function validateClientModuleForBrowserBundle(
|
|
|
311
344
|
return null;
|
|
312
345
|
}
|
|
313
346
|
|
|
347
|
+
async function routeComponentHasResolvableClientEntry(
|
|
348
|
+
rootDir: string,
|
|
349
|
+
routeModule: string,
|
|
350
|
+
source: string,
|
|
351
|
+
): Promise<boolean> {
|
|
352
|
+
const routeLevelClientImport = findRouteLevelClientComponentImport(source);
|
|
353
|
+
if (!routeLevelClientImport) return false;
|
|
354
|
+
return (await resolveClientImportModulePath(rootDir, routeModule, routeLevelClientImport.module)) !== null;
|
|
355
|
+
}
|
|
356
|
+
|
|
314
357
|
export async function describeMissingHydrationClientModule(
|
|
315
358
|
route: RouteSpec,
|
|
316
359
|
rootDir: string,
|
package/src/router/fs-scanner.ts
CHANGED
|
@@ -401,11 +401,14 @@ export class FSScanner {
|
|
|
401
401
|
} else {
|
|
402
402
|
const routeLevelClientImport = findRouteLevelClientComponentImport(pageFileContent);
|
|
403
403
|
if (routeLevelClientImport) {
|
|
404
|
-
|
|
404
|
+
const resolvedClientImport = await resolveClientImportModulePath(
|
|
405
405
|
rootDir,
|
|
406
406
|
modulePath,
|
|
407
407
|
routeLevelClientImport.module,
|
|
408
|
-
)
|
|
408
|
+
);
|
|
409
|
+
if (resolvedClientImport) {
|
|
410
|
+
clientModule = modulePath;
|
|
411
|
+
}
|
|
409
412
|
}
|
|
410
413
|
}
|
|
411
414
|
}
|