@mandujs/core 0.18.22 → 0.19.2
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/README.ko.md +0 -14
- package/package.json +4 -1
- package/src/brain/architecture/analyzer.ts +4 -4
- package/src/brain/doctor/analyzer.ts +18 -14
- package/src/bundler/build.test.ts +127 -0
- package/src/bundler/build.ts +291 -113
- package/src/bundler/css.ts +20 -5
- package/src/bundler/dev.ts +55 -2
- package/src/bundler/prerender.ts +195 -0
- package/src/change/snapshot.ts +4 -23
- package/src/change/types.ts +2 -3
- package/src/client/Form.tsx +105 -0
- package/src/client/__tests__/use-sse.test.ts +153 -0
- package/src/client/hooks.ts +105 -6
- package/src/client/index.ts +35 -6
- package/src/client/router.ts +670 -433
- package/src/client/rpc.ts +140 -0
- package/src/client/runtime.ts +24 -21
- package/src/client/use-fetch.ts +239 -0
- package/src/client/use-head.ts +197 -0
- package/src/client/use-sse.ts +378 -0
- package/src/components/Image.tsx +162 -0
- package/src/config/mandu.ts +5 -0
- package/src/config/validate.ts +34 -0
- package/src/content/index.ts +5 -1
- package/src/devtools/client/catchers/error-catcher.ts +17 -0
- package/src/devtools/client/catchers/network-proxy.ts +390 -367
- package/src/devtools/client/components/kitchen-root.tsx +479 -467
- package/src/devtools/client/components/panel/diff-viewer.tsx +219 -0
- package/src/devtools/client/components/panel/guard-panel.tsx +374 -244
- package/src/devtools/client/components/panel/index.ts +45 -32
- package/src/devtools/client/components/panel/panel-container.tsx +332 -312
- package/src/devtools/client/components/panel/preview-panel.tsx +188 -0
- package/src/devtools/client/state-manager.ts +535 -478
- package/src/devtools/design-tokens.ts +265 -264
- package/src/devtools/types.ts +345 -319
- package/src/filling/context.ts +65 -0
- package/src/filling/filling.ts +336 -14
- package/src/filling/index.ts +5 -1
- package/src/filling/session.ts +216 -0
- package/src/filling/ws.ts +78 -0
- package/src/generator/generate.ts +2 -2
- package/src/guard/auto-correct.ts +0 -29
- package/src/guard/check.ts +14 -31
- package/src/guard/presets/index.ts +296 -294
- package/src/guard/rules.ts +15 -19
- package/src/guard/validator.ts +834 -834
- package/src/index.ts +5 -1
- package/src/island/index.ts +373 -304
- package/src/kitchen/api/contract-api.ts +225 -0
- package/src/kitchen/api/diff-parser.ts +108 -0
- package/src/kitchen/api/file-api.ts +273 -0
- package/src/kitchen/api/guard-api.ts +83 -0
- package/src/kitchen/api/guard-decisions.ts +100 -0
- package/src/kitchen/api/routes-api.ts +50 -0
- package/src/kitchen/index.ts +21 -0
- package/src/kitchen/kitchen-handler.ts +256 -0
- package/src/kitchen/kitchen-ui.ts +1732 -0
- package/src/kitchen/stream/activity-sse.ts +145 -0
- package/src/kitchen/stream/file-tailer.ts +99 -0
- package/src/middleware/compress.ts +62 -0
- package/src/middleware/cors.ts +47 -0
- package/src/middleware/index.ts +10 -0
- package/src/middleware/jwt.ts +134 -0
- package/src/middleware/logger.ts +58 -0
- package/src/middleware/timeout.ts +55 -0
- package/src/paths.ts +0 -4
- package/src/plugins/hooks.ts +64 -0
- package/src/plugins/index.ts +3 -0
- package/src/plugins/types.ts +5 -0
- package/src/report/build.ts +0 -6
- package/src/resource/__tests__/backward-compat.test.ts +0 -1
- package/src/router/fs-patterns.ts +11 -1
- package/src/router/fs-routes.ts +78 -14
- package/src/router/fs-scanner.ts +2 -2
- package/src/router/fs-types.ts +2 -1
- package/src/runtime/adapter-bun.ts +62 -0
- package/src/runtime/adapter.ts +47 -0
- package/src/runtime/cache.ts +310 -0
- package/src/runtime/handler.ts +65 -0
- package/src/runtime/image-handler.ts +195 -0
- package/src/runtime/index.ts +12 -0
- package/src/runtime/middleware.ts +263 -0
- package/src/runtime/server.ts +686 -92
- package/src/runtime/ssr.ts +55 -29
- package/src/runtime/streaming-ssr.ts +106 -82
- package/src/spec/index.ts +0 -1
- package/src/spec/schema.ts +1 -0
- package/src/testing/index.ts +144 -0
- package/src/watcher/watcher.ts +27 -1
- package/src/spec/lock.ts +0 -56
package/src/bundler/build.ts
CHANGED
|
@@ -47,6 +47,78 @@ function getHydratedRoutes(manifest: RoutesManifest): RouteSpec[] {
|
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
const REACT_SHIM_EXPORTS = [
|
|
51
|
+
"Activity",
|
|
52
|
+
"Children",
|
|
53
|
+
"Component",
|
|
54
|
+
"Fragment",
|
|
55
|
+
"Profiler",
|
|
56
|
+
"PureComponent",
|
|
57
|
+
"StrictMode",
|
|
58
|
+
"Suspense",
|
|
59
|
+
"__COMPILER_RUNTIME",
|
|
60
|
+
"act",
|
|
61
|
+
"cache",
|
|
62
|
+
"cacheSignal",
|
|
63
|
+
"captureOwnerStack",
|
|
64
|
+
"cloneElement",
|
|
65
|
+
"createContext",
|
|
66
|
+
"createElement",
|
|
67
|
+
"createRef",
|
|
68
|
+
"forwardRef",
|
|
69
|
+
"isValidElement",
|
|
70
|
+
"lazy",
|
|
71
|
+
"memo",
|
|
72
|
+
"startTransition",
|
|
73
|
+
"unstable_useCacheRefresh",
|
|
74
|
+
"use",
|
|
75
|
+
"useActionState",
|
|
76
|
+
"useCallback",
|
|
77
|
+
"useContext",
|
|
78
|
+
"useDebugValue",
|
|
79
|
+
"useDeferredValue",
|
|
80
|
+
"useEffect",
|
|
81
|
+
"useEffectEvent",
|
|
82
|
+
"useId",
|
|
83
|
+
"useImperativeHandle",
|
|
84
|
+
"useInsertionEffect",
|
|
85
|
+
"useLayoutEffect",
|
|
86
|
+
"useMemo",
|
|
87
|
+
"useOptimistic",
|
|
88
|
+
"useReducer",
|
|
89
|
+
"useRef",
|
|
90
|
+
"useState",
|
|
91
|
+
"useSyncExternalStore",
|
|
92
|
+
"useTransition",
|
|
93
|
+
"version",
|
|
94
|
+
] as const;
|
|
95
|
+
|
|
96
|
+
const REACT_DOM_SHIM_EXPORTS = [
|
|
97
|
+
"createPortal",
|
|
98
|
+
"flushSync",
|
|
99
|
+
"preconnect",
|
|
100
|
+
"prefetchDNS",
|
|
101
|
+
"preinit",
|
|
102
|
+
"preinitModule",
|
|
103
|
+
"preload",
|
|
104
|
+
"preloadModule",
|
|
105
|
+
"requestFormReset",
|
|
106
|
+
"unstable_batchedUpdates",
|
|
107
|
+
"useFormState",
|
|
108
|
+
"useFormStatus",
|
|
109
|
+
"version",
|
|
110
|
+
] as const;
|
|
111
|
+
|
|
112
|
+
const REACT_DOM_CLIENT_SHIM_EXPORTS = [
|
|
113
|
+
"createRoot",
|
|
114
|
+
"hydrateRoot",
|
|
115
|
+
"version",
|
|
116
|
+
] as const;
|
|
117
|
+
|
|
118
|
+
function formatShimBindings(names: readonly string[], indent = " "): string {
|
|
119
|
+
return names.map((name) => `${indent}${name},`).join("\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
50
122
|
/**
|
|
51
123
|
* Runtime 번들 소스 생성 (v0.8.0 재설계)
|
|
52
124
|
*
|
|
@@ -66,7 +138,7 @@ function generateRuntimeSource(): string {
|
|
|
66
138
|
|
|
67
139
|
// React 정적 import (Island와 같은 인스턴스 공유)
|
|
68
140
|
import React, { useState, useEffect, Component } from 'react';
|
|
69
|
-
import { hydrateRoot } from 'react-dom/client';
|
|
141
|
+
import { hydrateRoot, createRoot } from 'react-dom/client';
|
|
70
142
|
|
|
71
143
|
// Hydrated roots 추적 (unmount용) - 전역 초기화
|
|
72
144
|
window.__MANDU_ROOTS__ = window.__MANDU_ROOTS__ || new Map();
|
|
@@ -147,6 +219,67 @@ function IslandLoadingWrapper({ children, loading, isReady }) {
|
|
|
147
219
|
return children;
|
|
148
220
|
}
|
|
149
221
|
|
|
222
|
+
function resolveHydrationTarget(element) {
|
|
223
|
+
if (!(element instanceof HTMLElement)) {
|
|
224
|
+
return element;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (getComputedStyle(element).display !== 'contents') {
|
|
228
|
+
return element;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const queue = Array.from(element.children);
|
|
232
|
+
while (queue.length > 0) {
|
|
233
|
+
const candidate = queue.shift();
|
|
234
|
+
if (candidate instanceof HTMLElement) {
|
|
235
|
+
return candidate;
|
|
236
|
+
}
|
|
237
|
+
if (candidate) {
|
|
238
|
+
queue.push(...candidate.children);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return element.parentElement || element;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function hasHydratableMarkup(element) {
|
|
246
|
+
for (const node of element.childNodes) {
|
|
247
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim() !== '') {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function shouldHydrateCompiledIsland(element) {
|
|
260
|
+
return (
|
|
261
|
+
element.getAttribute('data-mandu-loading') !== 'true' &&
|
|
262
|
+
hasHydratableMarkup(element)
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function createHydrationOptions(element, id, mode) {
|
|
267
|
+
return {
|
|
268
|
+
onRecoverableError(error) {
|
|
269
|
+
element.setAttribute('data-mandu-recoverable-error', 'true');
|
|
270
|
+
console.warn('[Mandu] Recoverable hydration error:', id, mode, error);
|
|
271
|
+
element.dispatchEvent(new CustomEvent('mandu:recoverable-hydration-error', {
|
|
272
|
+
bubbles: true,
|
|
273
|
+
detail: {
|
|
274
|
+
id,
|
|
275
|
+
mode,
|
|
276
|
+
error: error instanceof Error ? error.message : String(error),
|
|
277
|
+
},
|
|
278
|
+
}));
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
150
283
|
/**
|
|
151
284
|
* Hydration 스케줄러
|
|
152
285
|
*/
|
|
@@ -164,7 +297,8 @@ function scheduleHydration(element, src, priority) {
|
|
|
164
297
|
loadAndHydrate(element, src);
|
|
165
298
|
}
|
|
166
299
|
}, { rootMargin: '50px' });
|
|
167
|
-
|
|
300
|
+
const target = resolveHydrationTarget(element);
|
|
301
|
+
observer.observe(target);
|
|
168
302
|
} else {
|
|
169
303
|
loadAndHydrate(element, src);
|
|
170
304
|
}
|
|
@@ -179,15 +313,20 @@ function scheduleHydration(element, src, priority) {
|
|
|
179
313
|
break;
|
|
180
314
|
|
|
181
315
|
case 'interaction': {
|
|
316
|
+
const target = resolveHydrationTarget(element);
|
|
182
317
|
const hydrate = () => {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
318
|
+
target.removeEventListener('mouseenter', hydrate);
|
|
319
|
+
target.removeEventListener('focusin', hydrate);
|
|
320
|
+
target.removeEventListener('touchstart', hydrate);
|
|
321
|
+
target.removeEventListener('pointerdown', hydrate);
|
|
322
|
+
target.removeEventListener('keydown', hydrate);
|
|
186
323
|
loadAndHydrate(element, src);
|
|
187
324
|
};
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
325
|
+
target.addEventListener('mouseenter', hydrate, { once: true, passive: true });
|
|
326
|
+
target.addEventListener('focusin', hydrate, { once: true });
|
|
327
|
+
target.addEventListener('touchstart', hydrate, { once: true, passive: true });
|
|
328
|
+
target.addEventListener('pointerdown', hydrate, { once: true, passive: true });
|
|
329
|
+
target.addEventListener('keydown', hydrate, { once: true });
|
|
191
330
|
break;
|
|
192
331
|
}
|
|
193
332
|
}
|
|
@@ -200,20 +339,47 @@ function scheduleHydration(element, src, priority) {
|
|
|
200
339
|
*/
|
|
201
340
|
async function loadAndHydrate(element, src) {
|
|
202
341
|
const id = element.getAttribute('data-mandu-island');
|
|
342
|
+
if (!id) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (
|
|
347
|
+
hydratedRoots.has(id) ||
|
|
348
|
+
element.hasAttribute('data-mandu-hydrated') ||
|
|
349
|
+
element.getAttribute('data-mandu-hydrating') === 'true'
|
|
350
|
+
) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
element.setAttribute('data-mandu-hydrating', 'true');
|
|
203
355
|
|
|
204
356
|
try {
|
|
205
357
|
// Dynamic import - 이 시점에 Island 모듈 로드
|
|
206
358
|
const module = await import(src);
|
|
207
359
|
const island = module.default;
|
|
208
|
-
|
|
360
|
+
let data = getServerData(id);
|
|
361
|
+
|
|
362
|
+
// Fallback: read data-props from child element if __MANDU_DATA__ is empty
|
|
363
|
+
if (!data || Object.keys(data).length === 0) {
|
|
364
|
+
const propsEl = element.querySelector('[data-props]');
|
|
365
|
+
if (propsEl) {
|
|
366
|
+
try {
|
|
367
|
+
data = JSON.parse(propsEl.getAttribute('data-props'));
|
|
368
|
+
} catch (e) {
|
|
369
|
+
console.warn('[Mandu] Failed to parse data-props fallback:', e);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
209
373
|
|
|
210
374
|
// Mandu Island (preferred)
|
|
211
375
|
if (island && island.__mandu_island === true) {
|
|
212
376
|
const { definition } = island;
|
|
377
|
+
const shouldHydrate = shouldHydrateCompiledIsland(element);
|
|
378
|
+
const renderMode = shouldHydrate ? 'hydrate' : 'mount';
|
|
213
379
|
|
|
214
380
|
// Island 컴포넌트 (Error Boundary + Loading 지원)
|
|
215
|
-
function IslandComponent() {
|
|
216
|
-
const [isReady, setIsReady] = useState(
|
|
381
|
+
function IslandComponent({ initialReady }) {
|
|
382
|
+
const [isReady, setIsReady] = useState(initialReady);
|
|
217
383
|
|
|
218
384
|
useEffect(() => {
|
|
219
385
|
setIsReady(true);
|
|
@@ -238,11 +404,22 @@ async function loadAndHydrate(element, src) {
|
|
|
238
404
|
}, wrappedContent);
|
|
239
405
|
}
|
|
240
406
|
|
|
241
|
-
|
|
242
|
-
|
|
407
|
+
const root = shouldHydrate
|
|
408
|
+
? hydrateRoot(
|
|
409
|
+
element,
|
|
410
|
+
React.createElement(IslandComponent, { initialReady: true }),
|
|
411
|
+
createHydrationOptions(element, id, renderMode)
|
|
412
|
+
)
|
|
413
|
+
: createRoot(element);
|
|
414
|
+
|
|
415
|
+
if (!shouldHydrate) {
|
|
416
|
+
root.render(React.createElement(IslandComponent, { initialReady: false }));
|
|
417
|
+
}
|
|
418
|
+
|
|
243
419
|
hydratedRoots.set(id, root);
|
|
244
420
|
|
|
245
421
|
// 완료 표시
|
|
422
|
+
element.setAttribute('data-mandu-render-mode', renderMode);
|
|
246
423
|
element.setAttribute('data-mandu-hydrated', 'true');
|
|
247
424
|
|
|
248
425
|
// 성능 마커
|
|
@@ -253,22 +430,47 @@ async function loadAndHydrate(element, src) {
|
|
|
253
430
|
// 이벤트 발송
|
|
254
431
|
element.dispatchEvent(new CustomEvent('mandu:hydrated', {
|
|
255
432
|
bubbles: true,
|
|
256
|
-
detail: { id, data }
|
|
433
|
+
detail: { id, data, mode: renderMode }
|
|
257
434
|
}));
|
|
258
435
|
|
|
259
|
-
|
|
436
|
+
// Kitchen DevTools에 island 등록
|
|
437
|
+
if (window.__MANDU_DEVTOOLS_HOOK__) {
|
|
438
|
+
const hydrateTime = performance.now ? performance.now() : Date.now();
|
|
439
|
+
window.__MANDU_DEVTOOLS_HOOK__.emit({
|
|
440
|
+
type: 'island:register',
|
|
441
|
+
timestamp: Date.now(),
|
|
442
|
+
data: {
|
|
443
|
+
id,
|
|
444
|
+
name: id,
|
|
445
|
+
strategy: element.getAttribute('data-mandu-priority') || 'visible',
|
|
446
|
+
status: 'hydrated',
|
|
447
|
+
renderMode,
|
|
448
|
+
hydrateStartTime: hydrateTime - 10,
|
|
449
|
+
hydrateEndTime: hydrateTime,
|
|
450
|
+
propsSize: JSON.stringify(data).length,
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
console.log('[Mandu] Hydrated:', id, '(' + renderMode + ')');
|
|
260
456
|
}
|
|
261
457
|
// Plain React component fallback (e.g. "use client" pages)
|
|
262
458
|
else if (typeof island === 'function' || React.isValidElement(island)) {
|
|
263
459
|
console.warn('[Mandu] Plain component hydration:', id);
|
|
460
|
+
const renderMode = 'hydrate';
|
|
264
461
|
|
|
265
462
|
const root = typeof island === 'function'
|
|
266
|
-
? hydrateRoot(
|
|
267
|
-
|
|
463
|
+
? hydrateRoot(
|
|
464
|
+
element,
|
|
465
|
+
React.createElement(island, data),
|
|
466
|
+
createHydrationOptions(element, id, renderMode)
|
|
467
|
+
)
|
|
468
|
+
: hydrateRoot(element, island, createHydrationOptions(element, id, renderMode));
|
|
268
469
|
|
|
269
470
|
hydratedRoots.set(id, root);
|
|
270
471
|
|
|
271
472
|
// 완료 표시
|
|
473
|
+
element.setAttribute('data-mandu-render-mode', renderMode);
|
|
272
474
|
element.setAttribute('data-mandu-hydrated', 'true');
|
|
273
475
|
|
|
274
476
|
// 성능 마커
|
|
@@ -279,10 +481,10 @@ async function loadAndHydrate(element, src) {
|
|
|
279
481
|
// 이벤트 발송
|
|
280
482
|
element.dispatchEvent(new CustomEvent('mandu:hydrated', {
|
|
281
483
|
bubbles: true,
|
|
282
|
-
detail: { id, data }
|
|
484
|
+
detail: { id, data, mode: renderMode }
|
|
283
485
|
}));
|
|
284
486
|
|
|
285
|
-
console.log('[Mandu] Plain component hydrated:', id);
|
|
487
|
+
console.log('[Mandu] Plain component hydrated:', id, '(' + renderMode + ')');
|
|
286
488
|
}
|
|
287
489
|
else {
|
|
288
490
|
throw new Error('[Mandu] Invalid module: expected Mandu island or React component: ' + id);
|
|
@@ -296,6 +498,8 @@ async function loadAndHydrate(element, src) {
|
|
|
296
498
|
bubbles: true,
|
|
297
499
|
detail: { id, error: error.message }
|
|
298
500
|
}));
|
|
501
|
+
} finally {
|
|
502
|
+
element.removeAttribute('data-mandu-hydrating');
|
|
299
503
|
}
|
|
300
504
|
}
|
|
301
505
|
|
|
@@ -363,42 +567,7 @@ function generateReactShimSource(): string {
|
|
|
363
567
|
* import map을 통해 bare specifier 해결
|
|
364
568
|
*/
|
|
365
569
|
import React, {
|
|
366
|
-
|
|
367
|
-
createElement,
|
|
368
|
-
cloneElement,
|
|
369
|
-
createContext,
|
|
370
|
-
createRef,
|
|
371
|
-
forwardRef,
|
|
372
|
-
isValidElement,
|
|
373
|
-
memo,
|
|
374
|
-
lazy,
|
|
375
|
-
// Hooks
|
|
376
|
-
useState,
|
|
377
|
-
useEffect,
|
|
378
|
-
useContext,
|
|
379
|
-
useReducer,
|
|
380
|
-
useCallback,
|
|
381
|
-
useMemo,
|
|
382
|
-
useRef,
|
|
383
|
-
useLayoutEffect,
|
|
384
|
-
useImperativeHandle,
|
|
385
|
-
useDebugValue,
|
|
386
|
-
useDeferredValue,
|
|
387
|
-
useTransition,
|
|
388
|
-
useId,
|
|
389
|
-
useSyncExternalStore,
|
|
390
|
-
useInsertionEffect,
|
|
391
|
-
// Components
|
|
392
|
-
Fragment,
|
|
393
|
-
Suspense,
|
|
394
|
-
StrictMode,
|
|
395
|
-
Profiler,
|
|
396
|
-
// Misc
|
|
397
|
-
version,
|
|
398
|
-
// Types
|
|
399
|
-
Component,
|
|
400
|
-
PureComponent,
|
|
401
|
-
Children,
|
|
570
|
+
${formatShimBindings(REACT_SHIM_EXPORTS)}
|
|
402
571
|
} from 'react';
|
|
403
572
|
|
|
404
573
|
// JSX Runtime functions (JSX 변환에 필요)
|
|
@@ -426,37 +595,7 @@ if (typeof window !== 'undefined') {
|
|
|
426
595
|
|
|
427
596
|
// Named exports
|
|
428
597
|
export {
|
|
429
|
-
|
|
430
|
-
cloneElement,
|
|
431
|
-
createContext,
|
|
432
|
-
createRef,
|
|
433
|
-
forwardRef,
|
|
434
|
-
isValidElement,
|
|
435
|
-
memo,
|
|
436
|
-
lazy,
|
|
437
|
-
useState,
|
|
438
|
-
useEffect,
|
|
439
|
-
useContext,
|
|
440
|
-
useReducer,
|
|
441
|
-
useCallback,
|
|
442
|
-
useMemo,
|
|
443
|
-
useRef,
|
|
444
|
-
useLayoutEffect,
|
|
445
|
-
useImperativeHandle,
|
|
446
|
-
useDebugValue,
|
|
447
|
-
useDeferredValue,
|
|
448
|
-
useTransition,
|
|
449
|
-
useId,
|
|
450
|
-
useSyncExternalStore,
|
|
451
|
-
useInsertionEffect,
|
|
452
|
-
Fragment,
|
|
453
|
-
Suspense,
|
|
454
|
-
StrictMode,
|
|
455
|
-
Profiler,
|
|
456
|
-
version,
|
|
457
|
-
Component,
|
|
458
|
-
PureComponent,
|
|
459
|
-
Children,
|
|
598
|
+
${formatShimBindings(REACT_SHIM_EXPORTS)}
|
|
460
599
|
__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
|
|
461
600
|
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
|
|
462
601
|
// JSX Runtime exports
|
|
@@ -480,16 +619,12 @@ function generateReactDOMShimSource(): string {
|
|
|
480
619
|
* Mandu React DOM Shim (Generated)
|
|
481
620
|
*/
|
|
482
621
|
import ReactDOM, {
|
|
483
|
-
|
|
484
|
-
flushSync,
|
|
485
|
-
version,
|
|
622
|
+
${formatShimBindings(REACT_DOM_SHIM_EXPORTS)}
|
|
486
623
|
} from 'react-dom';
|
|
487
624
|
|
|
488
625
|
// Named exports
|
|
489
626
|
export {
|
|
490
|
-
|
|
491
|
-
flushSync,
|
|
492
|
-
version,
|
|
627
|
+
${formatShimBindings(REACT_DOM_SHIM_EXPORTS)}
|
|
493
628
|
};
|
|
494
629
|
|
|
495
630
|
// Default export
|
|
@@ -506,13 +641,19 @@ function generateReactDOMClientShimSource(): string {
|
|
|
506
641
|
/**
|
|
507
642
|
* Mandu React DOM Client Shim (Generated)
|
|
508
643
|
*/
|
|
509
|
-
import {
|
|
644
|
+
import {
|
|
645
|
+
${formatShimBindings(REACT_DOM_CLIENT_SHIM_EXPORTS)}
|
|
646
|
+
} from 'react-dom/client';
|
|
510
647
|
|
|
511
648
|
// Named exports (명시적으로 re-export)
|
|
512
|
-
export {
|
|
649
|
+
export {
|
|
650
|
+
${formatShimBindings(REACT_DOM_CLIENT_SHIM_EXPORTS)}
|
|
651
|
+
};
|
|
513
652
|
|
|
514
653
|
// Default export
|
|
515
|
-
export default {
|
|
654
|
+
export default {
|
|
655
|
+
${formatShimBindings(REACT_DOM_CLIENT_SHIM_EXPORTS)}
|
|
656
|
+
};
|
|
516
657
|
`;
|
|
517
658
|
}
|
|
518
659
|
|
|
@@ -1087,7 +1228,7 @@ async function buildIsland(
|
|
|
1087
1228
|
minify: options.minify ?? process.env.NODE_ENV === "production",
|
|
1088
1229
|
sourcemap: options.sourcemap ? "external" : "none",
|
|
1089
1230
|
target: "browser",
|
|
1090
|
-
splitting: options.splitting ??
|
|
1231
|
+
splitting: options.splitting ?? (process.env.NODE_ENV === "production"),
|
|
1091
1232
|
external: ["react", "react-dom", "react-dom/client", ...(options.external || [])],
|
|
1092
1233
|
define: {
|
|
1093
1234
|
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"),
|
|
@@ -1305,24 +1446,28 @@ export async function buildClientBundles(
|
|
|
1305
1446
|
return buildClientBundles(manifest, rootDir, { ...options, targetRouteIds: undefined });
|
|
1306
1447
|
}
|
|
1307
1448
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1449
|
+
// Only update manifest with successfully built outputs (#10: preserve previous good manifest on failure)
|
|
1450
|
+
if (outputs.length > 0) {
|
|
1451
|
+
for (const output of outputs) {
|
|
1452
|
+
if (existingManifest.bundles[output.routeId]) {
|
|
1453
|
+
existingManifest.bundles[output.routeId].js = output.outputPath;
|
|
1454
|
+
} else {
|
|
1455
|
+
const route = targetRoutes.find((r) => r.id === output.routeId);
|
|
1456
|
+
const hydration = route ? getRouteHydration(route) : null;
|
|
1457
|
+
existingManifest.bundles[output.routeId] = {
|
|
1458
|
+
js: output.outputPath,
|
|
1459
|
+
dependencies: ["_runtime", "_react"],
|
|
1460
|
+
priority: hydration?.priority || HYDRATION.DEFAULT_PRIORITY,
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1319
1463
|
}
|
|
1320
|
-
}
|
|
1321
1464
|
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1465
|
+
await fs.writeFile(
|
|
1466
|
+
path.join(rootDir, ".mandu/manifest.json"),
|
|
1467
|
+
JSON.stringify(existingManifest, null, 2)
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
// When all builds failed, do NOT overwrite manifest — keep previous good state
|
|
1326
1471
|
|
|
1327
1472
|
const stats = calculateStats(outputs, startTime);
|
|
1328
1473
|
return { success: errors.length === 0, outputs, errors, manifest: existingManifest, stats };
|
|
@@ -1356,13 +1501,46 @@ export async function buildClientBundles(
|
|
|
1356
1501
|
console.warn("[Mandu] DevTools bundle build failed:", devtoolsResult.errors.join(", "));
|
|
1357
1502
|
}
|
|
1358
1503
|
|
|
1504
|
+
// 4.5. Pre-build validation: detect wrong import paths in island files
|
|
1505
|
+
for (const route of hydratedRoutes) {
|
|
1506
|
+
if (route.clientModule) {
|
|
1507
|
+
const clientModulePath = path.join(rootDir, route.clientModule);
|
|
1508
|
+
try {
|
|
1509
|
+
const source = await fs.readFile(clientModulePath, "utf-8");
|
|
1510
|
+
// Match imports from "@mandujs/core" but NOT "@mandujs/core/client" or other subpaths
|
|
1511
|
+
const wrongImportPattern = /(?:import|from)\s+['"]@mandujs\/core['"]|require\s*\(\s*['"]@mandujs\/core['"]\s*\)/;
|
|
1512
|
+
if (wrongImportPattern.test(source)) {
|
|
1513
|
+
const errMsg =
|
|
1514
|
+
`[${route.id}] Island file "${route.clientModule}" imports from "@mandujs/core" which is a server-side module.\n` +
|
|
1515
|
+
` Fix: Change the import to "@mandujs/core/client".\n` +
|
|
1516
|
+
` Client islands cannot use server-side modules.`;
|
|
1517
|
+
console.error(`\n\x1b[31mERROR: ${errMsg}\x1b[0m\n`);
|
|
1518
|
+
errors.push(errMsg);
|
|
1519
|
+
}
|
|
1520
|
+
} catch {
|
|
1521
|
+
// File read failure will be caught later during build
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1359
1526
|
// 5. 각 Island 번들 빌드
|
|
1360
1527
|
for (const route of hydratedRoutes) {
|
|
1361
1528
|
try {
|
|
1362
1529
|
const result = await buildIsland(route, rootDir, outDir, options);
|
|
1363
1530
|
outputs.push(result);
|
|
1364
1531
|
} catch (error) {
|
|
1365
|
-
|
|
1532
|
+
const errorStr = String(error);
|
|
1533
|
+
// Detect common mistake: importing @mandujs/core (server module) in client island
|
|
1534
|
+
if (errorStr.includes("AggregateError") || errorStr.includes("Could not resolve")) {
|
|
1535
|
+
const clientModule = route.clientModule || "";
|
|
1536
|
+
errors.push(
|
|
1537
|
+
`[${route.id}] ${errorStr}\n` +
|
|
1538
|
+
` 💡 Hint: If your island imports from "@mandujs/core", change it to "@mandujs/core/client".\n` +
|
|
1539
|
+
` Client islands cannot use server-side modules. File: ${clientModule}`
|
|
1540
|
+
);
|
|
1541
|
+
} else {
|
|
1542
|
+
errors.push(`[${route.id}] ${errorStr}`);
|
|
1543
|
+
}
|
|
1366
1544
|
}
|
|
1367
1545
|
}
|
|
1368
1546
|
|
package/src/bundler/css.ts
CHANGED
|
@@ -8,11 +8,24 @@
|
|
|
8
8
|
* - 출력: .mandu/client/globals.css
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { spawn, type Subprocess } from "bun";
|
|
11
|
+
import { spawn, which, type Subprocess } from "bun";
|
|
12
12
|
import path from "path";
|
|
13
13
|
import fs from "fs/promises";
|
|
14
14
|
import { watch as fsWatch, type FSWatcher } from "fs";
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Tailwind CLI 실행 명령어를 결정한다.
|
|
18
|
+
* bunx가 PATH에 없는 환경(일부 Windows/CI)에서도 동작하도록
|
|
19
|
+
* `bun x`로 fallback한다.
|
|
20
|
+
*/
|
|
21
|
+
function getTailwindCommand(args: string[]): string[] {
|
|
22
|
+
if (which("bunx")) {
|
|
23
|
+
return ["bunx", ...args];
|
|
24
|
+
}
|
|
25
|
+
// bunx shim이 없어도 `bun x`는 동작함
|
|
26
|
+
return ["bun", "x", ...args];
|
|
27
|
+
}
|
|
28
|
+
|
|
16
29
|
// ========== Types ==========
|
|
17
30
|
|
|
18
31
|
export interface CSSBuildOptions {
|
|
@@ -124,7 +137,7 @@ export async function buildCSS(options: CSSBuildOptions): Promise<CSSBuildResult
|
|
|
124
137
|
}
|
|
125
138
|
|
|
126
139
|
try {
|
|
127
|
-
const proc = spawn(
|
|
140
|
+
const proc = spawn(getTailwindCommand(args), {
|
|
128
141
|
cwd: rootDir,
|
|
129
142
|
stdout: "pipe",
|
|
130
143
|
stderr: "pipe",
|
|
@@ -206,7 +219,7 @@ export async function startCSSWatch(options: CSSBuildOptions): Promise<CSSWatche
|
|
|
206
219
|
// Bun subprocess로 Tailwind CLI 실행
|
|
207
220
|
let proc;
|
|
208
221
|
try {
|
|
209
|
-
proc = spawn(
|
|
222
|
+
proc = spawn(getTailwindCommand(args), {
|
|
210
223
|
cwd: rootDir,
|
|
211
224
|
stdout: "pipe",
|
|
212
225
|
stderr: "pipe",
|
|
@@ -275,7 +288,9 @@ export async function startCSSWatch(options: CSSBuildOptions): Promise<CSSWatche
|
|
|
275
288
|
const { done, value } = await reader.read();
|
|
276
289
|
if (done) break;
|
|
277
290
|
|
|
278
|
-
const
|
|
291
|
+
const rawText = decoder.decode(value).trim();
|
|
292
|
+
// ANSI 이스케이프 코드 제거 후 비교 (Tailwind CLI가 컬러 출력)
|
|
293
|
+
const text = rawText.replace(/\u001b\[[0-9;]*m/g, "").trim();
|
|
279
294
|
if (text) {
|
|
280
295
|
// 환경 경고 무시
|
|
281
296
|
if (text.includes(".bash_profile") || text.includes("$'\\377")) {
|
|
@@ -287,7 +302,7 @@ export async function startCSSWatch(options: CSSBuildOptions): Promise<CSSWatche
|
|
|
287
302
|
text.includes("Resolving dependencies") ||
|
|
288
303
|
text.includes("Resolved, downloaded") ||
|
|
289
304
|
text.includes("Saved lockfile") ||
|
|
290
|
-
text.includes("
|
|
305
|
+
text.includes("tailwindcss") ||
|
|
291
306
|
text.match(/^v?\d+\.\d+\.\d+/) // 버전 출력
|
|
292
307
|
) {
|
|
293
308
|
if (text) console.log(` ℹ️ CSS: ${text}`);
|