@mandujs/core 0.53.3 → 0.54.1
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.md +654 -654
- package/package.json +1 -1
- package/src/a11y/__tests__/run-audit.test.ts +333 -333
- package/src/a11y/fix-hints.ts +76 -76
- package/src/a11y/index.ts +18 -18
- package/src/a11y/run-audit.ts +394 -394
- package/src/a11y/types.ts +125 -125
- package/src/auth/__tests__/login.test.ts +1 -1
- package/src/auth/__tests__/password.test.ts +122 -122
- package/src/auth/__tests__/tokens.test.ts +274 -274
- package/src/auth/__tests__/verification.test.ts +274 -274
- package/src/auth/index.ts +76 -76
- package/src/auth/login.ts +225 -225
- package/src/auth/password.ts +120 -120
- package/src/auth/reset.ts +243 -243
- package/src/auth/tokens.ts +612 -612
- package/src/auth/verification.ts +253 -253
- package/src/brain/__tests__/redactor.test.ts +94 -94
- package/src/brain/adapters/__tests__/_helpers.ts +83 -83
- package/src/brain/adapters/__tests__/anthropic-oauth.test.ts +196 -196
- package/src/brain/adapters/__tests__/chatgpt-auth.test.ts +193 -193
- package/src/brain/adapters/__tests__/openai-oauth.test.ts +209 -209
- package/src/brain/adapters/__tests__/resolver.test.ts +143 -143
- package/src/brain/adapters/anthropic-oauth.ts +1 -1
- package/src/brain/adapters/chatgpt-auth.ts +300 -300
- package/src/brain/adapters/index.ts +319 -319
- package/src/brain/adapters/oauth-flow.ts +439 -439
- package/src/brain/consent.ts +240 -240
- package/src/brain/credentials.ts +396 -396
- package/src/bundler/__tests__/build-runner.ts +113 -113
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -619
- package/src/bundler/__tests__/extended-watch.test.ts +711 -711
- package/src/bundler/__tests__/fast-refresh.test.ts +10 -10
- package/src/bundler/__tests__/hdr.test.ts +353 -353
- package/src/bundler/__tests__/hmr-client.test.ts +532 -532
- package/src/bundler/__tests__/manifest-schema.test.ts +266 -266
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -138
- package/src/bundler/__tests__/reverse-import-graph.test.ts +519 -519
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -573
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -286
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -455
- package/src/bundler/budget.ts +404 -404
- package/src/bundler/build.test.ts +179 -179
- package/src/bundler/build.ts +55 -55
- package/src/bundler/dev.ts +42 -42
- package/src/bundler/manifest-schema.ts +301 -301
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +263 -263
- package/src/bundler/plugins/__tests__/react-compiler-config.test.ts +83 -83
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -110
- package/src/bundler/plugins/block-generated-imports.ts +155 -155
- package/src/bundler/plugins/index.ts +83 -83
- package/src/bundler/plugins/react-compiler-config.ts +108 -108
- package/src/bundler/plugins/react-compiler-lint.ts +253 -253
- package/src/bundler/plugins/react-compiler.ts +162 -162
- package/src/bundler/reverse-import-graph.ts +339 -339
- package/src/bundler/safe-build.test.ts +201 -201
- package/src/bundler/safe-build.ts +103 -103
- package/src/bundler/scenario-matrix.ts +229 -229
- package/src/bundler/types.ts +10 -10
- package/src/bundler/vendor-cache-types.ts +130 -130
- package/src/bundler/vendor-cache.ts +526 -526
- package/src/change/snapshot.ts +18 -18
- package/src/client/Form.tsx +105 -105
- package/src/client/Link.tsx +9 -9
- package/src/client/__tests__/use-sse.test.ts +153 -153
- package/src/client/globals.ts +1 -1
- package/src/client/hooks.ts +362 -362
- package/src/client/hydrate.ts +340 -340
- package/src/client/prefetch-helper.ts +55 -55
- package/src/client/router.ts +11 -11
- package/src/client/runtime.ts +47 -47
- package/src/client/serialize.ts +404 -404
- package/src/client/use-fetch.ts +6 -6
- package/src/client/use-head.ts +197 -197
- package/src/client/use-sse.ts +378 -378
- package/src/client/window-state.ts +101 -101
- package/src/components/Image.tsx +162 -162
- package/src/config/validate.ts +3 -3
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/collection.ts +8 -8
- package/src/content/content-layer.ts +7 -7
- package/src/content/data-store.ts +245 -245
- package/src/content/frontmatter.ts +189 -189
- package/src/content/loader-context.ts +171 -171
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +172 -172
- package/src/content/loaders/glob.ts +253 -253
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/prebuild.test.ts +571 -571
- package/src/content/prebuild.ts +636 -636
- package/src/content/schema.ts +20 -20
- package/src/content/sidebar.ts +630 -630
- package/src/content/slug.ts +110 -110
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/define.ts +11 -11
- package/src/contract/index.ts +1 -1
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +410 -410
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/route-helpers.ts +1 -1
- package/src/contract/rpc.ts +443 -443
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/db/__tests__/db.test.ts +482 -482
- package/src/db/index.ts +138 -138
- package/src/db/migrations/history-table.ts +345 -345
- package/src/db/migrations/index.ts +3 -3
- package/src/db/migrations/lock.ts +324 -324
- package/src/db/migrations/runner.ts +650 -650
- package/src/deploy/cache.ts +140 -140
- package/src/deploy/compile/vercel.ts +344 -344
- package/src/deploy/index.ts +87 -87
- package/src/deploy/inference/brain.ts +268 -268
- package/src/deploy/inference/context.ts +182 -182
- package/src/deploy/inference/filling-extract.ts +245 -245
- package/src/deploy/inference/heuristic.ts +182 -182
- package/src/deploy/intent.ts +173 -173
- package/src/deploy/plan.ts +178 -178
- package/src/design/__tests__/agents-link.test.ts +109 -109
- package/src/design/__tests__/extract-patch-diff.test.ts +265 -265
- package/src/design/__tests__/lint.test.ts +110 -110
- package/src/design/__tests__/parser.test.ts +195 -195
- package/src/design/__tests__/tailwind-theme.test.ts +229 -229
- package/src/design/agents-link.ts +165 -165
- package/src/design/diff.ts +138 -138
- package/src/design/extract.ts +285 -285
- package/src/design/index.ts +102 -102
- package/src/design/lint.ts +209 -209
- package/src/design/parser.ts +555 -555
- package/src/design/patch.ts +241 -241
- package/src/design/scaffold.ts +147 -147
- package/src/design/tailwind-theme.ts +441 -441
- package/src/design/types.ts +210 -210
- package/src/desktop/__tests__/webview-fallback.test.ts +254 -254
- package/src/desktop/__tests__/window.test.ts +248 -248
- package/src/desktop/__tests__/worker.test.ts +266 -266
- package/src/desktop/index.ts +43 -43
- package/src/desktop/types.ts +158 -158
- package/src/desktop/worker.ts +180 -180
- package/src/dev-error-overlay/__tests__/overlay-injector.test.ts +241 -241
- package/src/dev-error-overlay/index.ts +30 -30
- package/src/dev-error-overlay/overlay-injector.ts +243 -243
- package/src/dev-error-overlay/overlay-styles.ts +52 -52
- package/src/dev-error-overlay/types.ts +66 -66
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +25 -25
- package/src/devtools/client/catchers/error-catcher.ts +344 -344
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/mandu-character.tsx +331 -331
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +30 -30
- package/src/devtools/client/components/panel/islands-panel.tsx +320 -320
- package/src/devtools/client/components/panel/network-panel.tsx +291 -291
- package/src/devtools/client/components/panel/panel-container.tsx +500 -500
- package/src/devtools/client/components/panel/preview-panel.tsx +46 -46
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +265 -265
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +450 -450
- package/src/devtools/types.ts +35 -35
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +233 -233
- package/src/devtools/worker/worker-manager.ts +410 -410
- package/src/diagnose/__tests__/checks.test.ts +451 -451
- package/src/diagnose/checks.ts +832 -832
- package/src/diagnose/index.ts +17 -17
- package/src/diagnose/run.ts +93 -93
- package/src/diagnose/types.ts +53 -53
- package/src/email/__tests__/email.test.ts +355 -355
- package/src/email/index.ts +282 -282
- package/src/email/smtp.ts +64 -64
- package/src/error/domains.ts +265 -265
- package/src/error/result.ts +60 -60
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/__tests__/session-sqlite.test.ts +454 -454
- package/src/filling/auth.ts +308 -308
- package/src/filling/cookie-codec.ts +299 -299
- package/src/filling/deps.ts +265 -265
- package/src/filling/session-sqlite.ts +617 -617
- package/src/filling/sse.ts +5 -5
- package/src/filling/ws.ts +78 -78
- package/src/generator/index.ts +3 -3
- package/src/guard/__tests__/design-inline-class.test.ts +219 -219
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -347
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/define-rule.ts +243 -243
- package/src/guard/design-inline-class.ts +393 -393
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -36
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/reporter.ts +442 -442
- package/src/guard/rule-presets.ts +379 -379
- package/src/guard/semantic-slots.ts +1 -1
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/tsgolint-bridge.ts +512 -512
- package/src/guard/types.ts +348 -348
- package/src/guard/watcher.ts +405 -405
- package/src/i18n/define.ts +126 -126
- package/src/i18n/index.ts +52 -52
- package/src/i18n/message-registry.ts +173 -173
- package/src/i18n/types.ts +112 -112
- package/src/id/index.ts +105 -105
- package/src/index.ts +2 -2
- package/src/kitchen/api/agent-devtools-api.ts +779 -544
- package/src/kitchen/api/errors-grouping.ts +126 -0
- package/src/kitchen/kitchen-handler.ts +192 -63
- package/src/kitchen/kitchen-ui.ts +842 -464
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/middleware/bridge.ts +147 -147
- package/src/middleware/compose.ts +134 -134
- package/src/middleware/compress.ts +62 -62
- package/src/middleware/cors.ts +47 -47
- package/src/middleware/csrf.ts +328 -328
- package/src/middleware/define.ts +132 -132
- package/src/middleware/jwt.ts +134 -134
- package/src/middleware/logger.ts +58 -58
- package/src/middleware/oauth/__tests__/oauth.test.ts +1 -1
- package/src/middleware/oauth/index.ts +505 -505
- package/src/middleware/oauth/providers.ts +115 -115
- package/src/middleware/rate-limit/index.ts +522 -522
- package/src/middleware/rate-limit/sqlite-store.ts +382 -382
- package/src/middleware/scheduler-cron.ts +96 -96
- package/src/middleware/secure/__tests__/secure.test.ts +360 -360
- package/src/middleware/secure/csp.ts +193 -193
- package/src/middleware/session.ts +174 -174
- package/src/middleware/timeout.ts +55 -55
- package/src/observability/logger-adapter.ts +36 -36
- package/src/observability/sqlite-store.ts +254 -254
- package/src/openapi/generator.ts +1 -1
- package/src/openapi/openapi.test.ts +43 -43
- package/src/perf/__tests__/user-marks.test.ts +354 -354
- package/src/perf/index.ts +133 -133
- package/src/plugins/__tests__/lifecycle-integration.test.ts +272 -272
- package/src/plugins/__tests__/runner.test.ts +409 -409
- package/src/plugins/define.ts +124 -124
- package/src/plugins/examples/dep-check-plugin.ts +80 -80
- package/src/plugins/examples/prerender-cache-plugin.ts +111 -111
- package/src/plugins/examples/sitemap-plugin.ts +65 -65
- package/src/plugins/runner.ts +361 -361
- package/src/plugins/types.ts +368 -368
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +32 -32
- package/src/resource/ddl/__tests__/diff.test.ts +639 -639
- package/src/resource/ddl/__tests__/emit.test.ts +823 -823
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -499
- package/src/resource/ddl/emit.ts +559 -559
- package/src/resource/ddl/persistence-types.ts +218 -218
- package/src/resource/ddl/type-map.ts +223 -223
- package/src/resource/ddl/types.ts +232 -232
- package/src/resource/generator-repo.ts +630 -630
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-patterns.test.ts +96 -96
- package/src/router/fs-routes.ts +6 -6
- package/src/routes/index.ts +74 -74
- package/src/routes/metadata-routes.ts +427 -427
- package/src/routes/types.ts +341 -341
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -141
- package/src/runtime/__tests__/hdr-client.test.ts +223 -223
- package/src/runtime/__tests__/http-errors.test.ts +117 -117
- package/src/runtime/__tests__/not-found.test.ts +152 -152
- package/src/runtime/adapter.ts +47 -47
- package/src/runtime/boundary.tsx +252 -252
- package/src/runtime/cache.ts +494 -494
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/fast-refresh-runtime.ts +322 -322
- package/src/runtime/handler.ts +65 -65
- package/src/runtime/handlers.ts +300 -300
- package/src/runtime/http-errors.ts +113 -113
- package/src/runtime/image-handler.ts +1 -1
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/middleware.ts +264 -264
- package/src/runtime/not-found.ts +93 -93
- package/src/runtime/openapi-endpoint.ts +236 -236
- package/src/runtime/ppr.ts +74 -74
- package/src/runtime/registry.ts +171 -171
- package/src/runtime/router.ts +105 -105
- package/src/runtime/server.ts +67 -67
- package/src/runtime/shims.ts +48 -48
- package/src/runtime/trace.ts +144 -144
- package/src/scheduler/validate.ts +169 -169
- package/src/seo/index.ts +219 -219
- package/src/seo/integration/ssr.ts +306 -306
- package/src/seo/render/basic.ts +435 -435
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +197 -197
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +127 -127
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +104 -104
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +588 -588
- package/src/slot/validator.ts +39 -39
- package/src/storage/s3/__tests__/s3.test.ts +479 -479
- package/src/storage/s3/index.ts +412 -412
- package/src/testing/__tests__/assertions.test.ts +632 -632
- package/src/testing/__tests__/reporter.test.ts +454 -454
- package/src/testing/assertions.ts +986 -986
- package/src/testing/db.ts +157 -157
- package/src/testing/mocks.ts +203 -203
- package/src/testing/session.ts +190 -190
- package/src/types/branded.ts +56 -56
- package/src/types/index.ts +1 -1
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/watcher.ts +18 -18
package/src/change/snapshot.ts
CHANGED
|
@@ -13,19 +13,19 @@ import { validateAndReport } from "../config";
|
|
|
13
13
|
const MANDU_DIR = ".mandu";
|
|
14
14
|
const SPEC_DIR = "spec";
|
|
15
15
|
const MANIFEST_FILE = "routes.manifest.json";
|
|
16
|
-
const SLOTS_DIR = "slots";
|
|
17
|
-
const HISTORY_DIR = "history";
|
|
18
|
-
const SNAPSHOTS_DIR = "snapshots";
|
|
19
|
-
|
|
20
|
-
function reverseCopy<T>(items: readonly T[]): T[] {
|
|
21
|
-
const reversed: T[] = [];
|
|
22
|
-
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
23
|
-
reversed.push(items[index] as T);
|
|
24
|
-
}
|
|
25
|
-
return reversed;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
16
|
+
const SLOTS_DIR = "slots";
|
|
17
|
+
const HISTORY_DIR = "history";
|
|
18
|
+
const SNAPSHOTS_DIR = "snapshots";
|
|
19
|
+
|
|
20
|
+
function reverseCopy<T>(items: readonly T[]): T[] {
|
|
21
|
+
const reversed: T[] = [];
|
|
22
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
23
|
+
reversed.push(items[index] as T);
|
|
24
|
+
}
|
|
25
|
+
return reversed;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
29
|
* 스냅샷 ID 생성 (YYYYMMDD-HHmmss-xxx)
|
|
30
30
|
*/
|
|
31
31
|
function generateSnapshotId(): string {
|
|
@@ -258,11 +258,11 @@ export async function listSnapshotIds(rootDir: string): Promise<string[]> {
|
|
|
258
258
|
})
|
|
259
259
|
);
|
|
260
260
|
|
|
261
|
-
return reverseCopy(
|
|
262
|
-
entries
|
|
263
|
-
.map((entry) => entry.replace(".snapshot.json", ""))
|
|
264
|
-
.sort()
|
|
265
|
-
); // 최신 순
|
|
261
|
+
return reverseCopy(
|
|
262
|
+
entries
|
|
263
|
+
.map((entry) => entry.replace(".snapshot.json", ""))
|
|
264
|
+
.sort()
|
|
265
|
+
); // 최신 순
|
|
266
266
|
} catch {
|
|
267
267
|
return [];
|
|
268
268
|
}
|
package/src/client/Form.tsx
CHANGED
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu <Form> Component
|
|
3
|
-
* Progressive Enhancement: JS 없으면 일반 HTML form, JS 있으면 fetch 기반 제출
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { useState, useCallback, useRef, type FormEvent, type ReactNode } from "react";
|
|
7
|
-
import { submitAction, type ActionResult } from "./router";
|
|
8
|
-
|
|
9
|
-
export interface FormState {
|
|
10
|
-
submitting: boolean;
|
|
11
|
-
error: string | null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface FormProps extends Omit<React.FormHTMLAttributes<HTMLFormElement>, "action" | "children"> {
|
|
15
|
-
/** 제출 대상 URL */
|
|
16
|
-
action: string;
|
|
17
|
-
/** Action 이름 (서버의 filling.action(name)과 매칭) */
|
|
18
|
-
actionName?: string;
|
|
19
|
-
/** HTTP 메서드 */
|
|
20
|
-
method?: "post" | "put" | "patch" | "delete";
|
|
21
|
-
/** JS에서 fetch 방식으로 전환 (기본: true, false면 일반 HTML form 제출) */
|
|
22
|
-
enhance?: boolean;
|
|
23
|
-
/** Action 성공 후 콜백 */
|
|
24
|
-
onActionSuccess?: (result: ActionResult) => void;
|
|
25
|
-
/** Action 실패 후 콜백 */
|
|
26
|
-
onActionError?: (error: Error) => void;
|
|
27
|
-
/** render props 또는 일반 children */
|
|
28
|
-
children: ReactNode | ((state: FormState) => ReactNode);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Progressive Enhancement Form 컴포넌트
|
|
33
|
-
*
|
|
34
|
-
* @example
|
|
35
|
-
* ```tsx
|
|
36
|
-
* <Form action="/api/todos" actionName="create">
|
|
37
|
-
* <input name="title" required />
|
|
38
|
-
* <button type="submit">추가</button>
|
|
39
|
-
* </Form>
|
|
40
|
-
*
|
|
41
|
-
* // render props로 제출 상태 접근
|
|
42
|
-
* <Form action="/api/todos" actionName="create">
|
|
43
|
-
* {({ submitting, error }) => (
|
|
44
|
-
* <>
|
|
45
|
-
* <input name="title" required />
|
|
46
|
-
* <button type="submit" disabled={submitting}>
|
|
47
|
-
* {submitting ? "처리 중..." : "추가"}
|
|
48
|
-
* </button>
|
|
49
|
-
* {error && <p>{error}</p>}
|
|
50
|
-
* </>
|
|
51
|
-
* )}
|
|
52
|
-
* </Form>
|
|
53
|
-
* ```
|
|
54
|
-
*/
|
|
55
|
-
export function Form({
|
|
56
|
-
action,
|
|
57
|
-
actionName = "default",
|
|
58
|
-
method = "post",
|
|
59
|
-
enhance = true,
|
|
60
|
-
onActionSuccess,
|
|
61
|
-
onActionError,
|
|
62
|
-
children,
|
|
63
|
-
...rest
|
|
64
|
-
}: FormProps) {
|
|
65
|
-
const [state, setState] = useState<FormState>({ submitting: false, error: null });
|
|
66
|
-
const submittingRef = useRef(false);
|
|
67
|
-
const formMethod = method === "post" ? "post" : "post";
|
|
68
|
-
|
|
69
|
-
const handleSubmit = useCallback(async (e: FormEvent<HTMLFormElement>) => {
|
|
70
|
-
if (!enhance) return;
|
|
71
|
-
if (submittingRef.current) return; // 이중 제출 방지
|
|
72
|
-
|
|
73
|
-
e.preventDefault();
|
|
74
|
-
submittingRef.current = true;
|
|
75
|
-
setState({ submitting: true, error: null });
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
const formData = new FormData(e.currentTarget);
|
|
79
|
-
const result = await submitAction(action, formData, actionName, method);
|
|
80
|
-
|
|
81
|
-
if (result.ok) {
|
|
82
|
-
setState({ submitting: false, error: null });
|
|
83
|
-
onActionSuccess?.(result);
|
|
84
|
-
} else {
|
|
85
|
-
const message = "요청이 실패했습니다.";
|
|
86
|
-
setState({ submitting: false, error: message });
|
|
87
|
-
onActionError?.(new Error(message));
|
|
88
|
-
}
|
|
89
|
-
} catch (error) {
|
|
90
|
-
const message = error instanceof Error ? error.message : "요청 실패";
|
|
91
|
-
setState({ submitting: false, error: message });
|
|
92
|
-
onActionError?.(error instanceof Error ? error : new Error(message));
|
|
93
|
-
} finally {
|
|
94
|
-
submittingRef.current = false;
|
|
95
|
-
}
|
|
96
|
-
}, [action, actionName, enhance, method, onActionSuccess, onActionError]);
|
|
97
|
-
|
|
98
|
-
return (
|
|
99
|
-
<form action={action} method={formMethod} onSubmit={handleSubmit} {...rest}>
|
|
100
|
-
<input type="hidden" name="_action" value={actionName} />
|
|
101
|
-
{method !== "post" && <input type="hidden" name="_method" value={method.toUpperCase()} />}
|
|
102
|
-
{typeof children === "function" ? children(state) : children}
|
|
103
|
-
</form>
|
|
104
|
-
);
|
|
105
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Mandu <Form> Component
|
|
3
|
+
* Progressive Enhancement: JS 없으면 일반 HTML form, JS 있으면 fetch 기반 제출
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback, useRef, type FormEvent, type ReactNode } from "react";
|
|
7
|
+
import { submitAction, type ActionResult } from "./router";
|
|
8
|
+
|
|
9
|
+
export interface FormState {
|
|
10
|
+
submitting: boolean;
|
|
11
|
+
error: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface FormProps extends Omit<React.FormHTMLAttributes<HTMLFormElement>, "action" | "children"> {
|
|
15
|
+
/** 제출 대상 URL */
|
|
16
|
+
action: string;
|
|
17
|
+
/** Action 이름 (서버의 filling.action(name)과 매칭) */
|
|
18
|
+
actionName?: string;
|
|
19
|
+
/** HTTP 메서드 */
|
|
20
|
+
method?: "post" | "put" | "patch" | "delete";
|
|
21
|
+
/** JS에서 fetch 방식으로 전환 (기본: true, false면 일반 HTML form 제출) */
|
|
22
|
+
enhance?: boolean;
|
|
23
|
+
/** Action 성공 후 콜백 */
|
|
24
|
+
onActionSuccess?: (result: ActionResult) => void;
|
|
25
|
+
/** Action 실패 후 콜백 */
|
|
26
|
+
onActionError?: (error: Error) => void;
|
|
27
|
+
/** render props 또는 일반 children */
|
|
28
|
+
children: ReactNode | ((state: FormState) => ReactNode);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Progressive Enhancement Form 컴포넌트
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```tsx
|
|
36
|
+
* <Form action="/api/todos" actionName="create">
|
|
37
|
+
* <input name="title" required />
|
|
38
|
+
* <button type="submit">추가</button>
|
|
39
|
+
* </Form>
|
|
40
|
+
*
|
|
41
|
+
* // render props로 제출 상태 접근
|
|
42
|
+
* <Form action="/api/todos" actionName="create">
|
|
43
|
+
* {({ submitting, error }) => (
|
|
44
|
+
* <>
|
|
45
|
+
* <input name="title" required />
|
|
46
|
+
* <button type="submit" disabled={submitting}>
|
|
47
|
+
* {submitting ? "처리 중..." : "추가"}
|
|
48
|
+
* </button>
|
|
49
|
+
* {error && <p>{error}</p>}
|
|
50
|
+
* </>
|
|
51
|
+
* )}
|
|
52
|
+
* </Form>
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function Form({
|
|
56
|
+
action,
|
|
57
|
+
actionName = "default",
|
|
58
|
+
method = "post",
|
|
59
|
+
enhance = true,
|
|
60
|
+
onActionSuccess,
|
|
61
|
+
onActionError,
|
|
62
|
+
children,
|
|
63
|
+
...rest
|
|
64
|
+
}: FormProps) {
|
|
65
|
+
const [state, setState] = useState<FormState>({ submitting: false, error: null });
|
|
66
|
+
const submittingRef = useRef(false);
|
|
67
|
+
const formMethod = method === "post" ? "post" : "post";
|
|
68
|
+
|
|
69
|
+
const handleSubmit = useCallback(async (e: FormEvent<HTMLFormElement>) => {
|
|
70
|
+
if (!enhance) return;
|
|
71
|
+
if (submittingRef.current) return; // 이중 제출 방지
|
|
72
|
+
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
submittingRef.current = true;
|
|
75
|
+
setState({ submitting: true, error: null });
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const formData = new FormData(e.currentTarget);
|
|
79
|
+
const result = await submitAction(action, formData, actionName, method);
|
|
80
|
+
|
|
81
|
+
if (result.ok) {
|
|
82
|
+
setState({ submitting: false, error: null });
|
|
83
|
+
onActionSuccess?.(result);
|
|
84
|
+
} else {
|
|
85
|
+
const message = "요청이 실패했습니다.";
|
|
86
|
+
setState({ submitting: false, error: message });
|
|
87
|
+
onActionError?.(new Error(message));
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const message = error instanceof Error ? error.message : "요청 실패";
|
|
91
|
+
setState({ submitting: false, error: message });
|
|
92
|
+
onActionError?.(error instanceof Error ? error : new Error(message));
|
|
93
|
+
} finally {
|
|
94
|
+
submittingRef.current = false;
|
|
95
|
+
}
|
|
96
|
+
}, [action, actionName, enhance, method, onActionSuccess, onActionError]);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<form action={action} method={formMethod} onSubmit={handleSubmit} {...rest}>
|
|
100
|
+
<input type="hidden" name="_action" value={actionName} />
|
|
101
|
+
{method !== "post" && <input type="hidden" name="_method" value={method.toUpperCase()} />}
|
|
102
|
+
{typeof children === "function" ? children(state) : children}
|
|
103
|
+
</form>
|
|
104
|
+
);
|
|
105
|
+
}
|
package/src/client/Link.tsx
CHANGED
|
@@ -88,10 +88,10 @@ export function Link({
|
|
|
88
88
|
} catch {
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
|
-
|
|
92
|
-
// Client-side 네비게이션
|
|
93
|
-
event.preventDefault();
|
|
94
|
-
void navigate(href, { replace, scroll });
|
|
91
|
+
|
|
92
|
+
// Client-side 네비게이션
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
void navigate(href, { replace, scroll });
|
|
95
95
|
},
|
|
96
96
|
[href, replace, scroll, onClick]
|
|
97
97
|
);
|
|
@@ -101,11 +101,11 @@ export function Link({
|
|
|
101
101
|
if (!shouldPrefetch || prefetchedRef.current) return;
|
|
102
102
|
|
|
103
103
|
try {
|
|
104
|
-
const url = new URL(href, window.location.origin);
|
|
105
|
-
if (url.origin === window.location.origin) {
|
|
106
|
-
void prefetch(href);
|
|
107
|
-
prefetchedRef.current = true;
|
|
108
|
-
}
|
|
104
|
+
const url = new URL(href, window.location.origin);
|
|
105
|
+
if (url.origin === window.location.origin) {
|
|
106
|
+
void prefetch(href);
|
|
107
|
+
prefetchedRef.current = true;
|
|
108
|
+
}
|
|
109
109
|
} catch {
|
|
110
110
|
// 무시
|
|
111
111
|
}
|
|
@@ -1,153 +1,153 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for useSSE hook and readStreamWithYield utility
|
|
3
|
-
*
|
|
4
|
-
* These tests verify the microtask-starvation-safe ReadableStream reading
|
|
5
|
-
* utilities work correctly.
|
|
6
|
-
*/
|
|
7
|
-
import { describe, test, expect, beforeEach, mock } from "bun:test";
|
|
8
|
-
import { readStreamWithYield } from "../use-sse";
|
|
9
|
-
|
|
10
|
-
// Note: useSSE is a React hook and would need a React testing environment.
|
|
11
|
-
// We test the lower-level readStreamWithYield utility here since it contains
|
|
12
|
-
// the core macrotask-yielding logic that prevents main thread blocking.
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Helper: create a ReadableStream from an array of strings
|
|
16
|
-
*/
|
|
17
|
-
function createMockStream(chunks: string[]): ReadableStream<Uint8Array> {
|
|
18
|
-
const encoder = new TextEncoder();
|
|
19
|
-
let index = 0;
|
|
20
|
-
|
|
21
|
-
return new ReadableStream({
|
|
22
|
-
pull(controller) {
|
|
23
|
-
if (index < chunks.length) {
|
|
24
|
-
controller.enqueue(encoder.encode(chunks[index++]));
|
|
25
|
-
} else {
|
|
26
|
-
controller.close();
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe("readStreamWithYield", () => {
|
|
33
|
-
test("should read all chunks from a stream", async () => {
|
|
34
|
-
const chunks = ["Hello", " ", "World"];
|
|
35
|
-
const stream = createMockStream(chunks);
|
|
36
|
-
const received: string[] = [];
|
|
37
|
-
|
|
38
|
-
await readStreamWithYield(stream, {
|
|
39
|
-
onChunk: (text) => received.push(text),
|
|
40
|
-
onDone: () => {},
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
expect(received).toEqual(chunks);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("should call onDone when stream completes", async () => {
|
|
47
|
-
const stream = createMockStream(["a", "b"]);
|
|
48
|
-
let doneCalled = false;
|
|
49
|
-
|
|
50
|
-
await readStreamWithYield(stream, {
|
|
51
|
-
onChunk: () => {},
|
|
52
|
-
onDone: () => {
|
|
53
|
-
doneCalled = true;
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
expect(doneCalled).toBe(true);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test("should call onError on stream failure", async () => {
|
|
61
|
-
const stream = new ReadableStream({
|
|
62
|
-
start(controller) {
|
|
63
|
-
controller.error(new Error("test error"));
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
let errorCaught: Error | null = null;
|
|
68
|
-
|
|
69
|
-
await readStreamWithYield(stream, {
|
|
70
|
-
onChunk: () => {},
|
|
71
|
-
onError: (err) => {
|
|
72
|
-
errorCaught = err;
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
expect(errorCaught).not.toBeNull();
|
|
77
|
-
expect(errorCaught!.message).toBe("test error");
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("should respect abort signal", async () => {
|
|
81
|
-
const controller = new AbortController();
|
|
82
|
-
const chunks = ["a", "b", "c", "d", "e"];
|
|
83
|
-
const stream = createMockStream(chunks);
|
|
84
|
-
const received: string[] = [];
|
|
85
|
-
|
|
86
|
-
// Abort after first chunk
|
|
87
|
-
let chunkCount = 0;
|
|
88
|
-
|
|
89
|
-
await readStreamWithYield(stream, {
|
|
90
|
-
onChunk: (text) => {
|
|
91
|
-
received.push(text);
|
|
92
|
-
chunkCount++;
|
|
93
|
-
if (chunkCount >= 2) {
|
|
94
|
-
controller.abort();
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
signal: controller.signal,
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// Should have received at most 2-3 chunks (abort may not be immediate)
|
|
101
|
-
expect(received.length).toBeLessThanOrEqual(3);
|
|
102
|
-
expect(received.length).toBeGreaterThanOrEqual(2);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("should handle empty stream", async () => {
|
|
106
|
-
const stream = createMockStream([]);
|
|
107
|
-
const received: string[] = [];
|
|
108
|
-
let doneCalled = false;
|
|
109
|
-
|
|
110
|
-
await readStreamWithYield(stream, {
|
|
111
|
-
onChunk: (text) => received.push(text),
|
|
112
|
-
onDone: () => {
|
|
113
|
-
doneCalled = true;
|
|
114
|
-
},
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
expect(received).toEqual([]);
|
|
118
|
-
expect(doneCalled).toBe(true);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("should yield between chunks (non-blocking)", async () => {
|
|
122
|
-
// Create a stream with many rapid chunks
|
|
123
|
-
const chunkCount = 50;
|
|
124
|
-
const chunks = Array.from({ length: chunkCount }, (_, i) => `chunk-${i}`);
|
|
125
|
-
const stream = createMockStream(chunks);
|
|
126
|
-
const received: string[] = [];
|
|
127
|
-
|
|
128
|
-
// Track that setTimeout(0) yields are happening by checking
|
|
129
|
-
// that the promise resolves in multiple event loop ticks
|
|
130
|
-
let macrotaskCount = 0;
|
|
131
|
-
const originalSetTimeout = globalThis.setTimeout;
|
|
132
|
-
|
|
133
|
-
// Count macrotask yields (setTimeout(0) calls from yieldToMacrotask)
|
|
134
|
-
const timeoutSpy = mock((fn: Function, ms: number) => {
|
|
135
|
-
if (ms === 0) macrotaskCount++;
|
|
136
|
-
return originalSetTimeout(fn, ms);
|
|
137
|
-
});
|
|
138
|
-
// @ts-expect-error - mock override
|
|
139
|
-
globalThis.setTimeout = timeoutSpy;
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
await readStreamWithYield(stream, {
|
|
143
|
-
onChunk: (text) => received.push(text),
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// Every chunk should cause a macrotask yield
|
|
147
|
-
expect(received.length).toBe(chunkCount);
|
|
148
|
-
expect(macrotaskCount).toBeGreaterThanOrEqual(chunkCount);
|
|
149
|
-
} finally {
|
|
150
|
-
globalThis.setTimeout = originalSetTimeout;
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Tests for useSSE hook and readStreamWithYield utility
|
|
3
|
+
*
|
|
4
|
+
* These tests verify the microtask-starvation-safe ReadableStream reading
|
|
5
|
+
* utilities work correctly.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, test, expect, beforeEach, mock } from "bun:test";
|
|
8
|
+
import { readStreamWithYield } from "../use-sse";
|
|
9
|
+
|
|
10
|
+
// Note: useSSE is a React hook and would need a React testing environment.
|
|
11
|
+
// We test the lower-level readStreamWithYield utility here since it contains
|
|
12
|
+
// the core macrotask-yielding logic that prevents main thread blocking.
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Helper: create a ReadableStream from an array of strings
|
|
16
|
+
*/
|
|
17
|
+
function createMockStream(chunks: string[]): ReadableStream<Uint8Array> {
|
|
18
|
+
const encoder = new TextEncoder();
|
|
19
|
+
let index = 0;
|
|
20
|
+
|
|
21
|
+
return new ReadableStream({
|
|
22
|
+
pull(controller) {
|
|
23
|
+
if (index < chunks.length) {
|
|
24
|
+
controller.enqueue(encoder.encode(chunks[index++]));
|
|
25
|
+
} else {
|
|
26
|
+
controller.close();
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("readStreamWithYield", () => {
|
|
33
|
+
test("should read all chunks from a stream", async () => {
|
|
34
|
+
const chunks = ["Hello", " ", "World"];
|
|
35
|
+
const stream = createMockStream(chunks);
|
|
36
|
+
const received: string[] = [];
|
|
37
|
+
|
|
38
|
+
await readStreamWithYield(stream, {
|
|
39
|
+
onChunk: (text) => received.push(text),
|
|
40
|
+
onDone: () => {},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(received).toEqual(chunks);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("should call onDone when stream completes", async () => {
|
|
47
|
+
const stream = createMockStream(["a", "b"]);
|
|
48
|
+
let doneCalled = false;
|
|
49
|
+
|
|
50
|
+
await readStreamWithYield(stream, {
|
|
51
|
+
onChunk: () => {},
|
|
52
|
+
onDone: () => {
|
|
53
|
+
doneCalled = true;
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(doneCalled).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("should call onError on stream failure", async () => {
|
|
61
|
+
const stream = new ReadableStream({
|
|
62
|
+
start(controller) {
|
|
63
|
+
controller.error(new Error("test error"));
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
let errorCaught: Error | null = null;
|
|
68
|
+
|
|
69
|
+
await readStreamWithYield(stream, {
|
|
70
|
+
onChunk: () => {},
|
|
71
|
+
onError: (err) => {
|
|
72
|
+
errorCaught = err;
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(errorCaught).not.toBeNull();
|
|
77
|
+
expect(errorCaught!.message).toBe("test error");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("should respect abort signal", async () => {
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
const chunks = ["a", "b", "c", "d", "e"];
|
|
83
|
+
const stream = createMockStream(chunks);
|
|
84
|
+
const received: string[] = [];
|
|
85
|
+
|
|
86
|
+
// Abort after first chunk
|
|
87
|
+
let chunkCount = 0;
|
|
88
|
+
|
|
89
|
+
await readStreamWithYield(stream, {
|
|
90
|
+
onChunk: (text) => {
|
|
91
|
+
received.push(text);
|
|
92
|
+
chunkCount++;
|
|
93
|
+
if (chunkCount >= 2) {
|
|
94
|
+
controller.abort();
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
signal: controller.signal,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Should have received at most 2-3 chunks (abort may not be immediate)
|
|
101
|
+
expect(received.length).toBeLessThanOrEqual(3);
|
|
102
|
+
expect(received.length).toBeGreaterThanOrEqual(2);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("should handle empty stream", async () => {
|
|
106
|
+
const stream = createMockStream([]);
|
|
107
|
+
const received: string[] = [];
|
|
108
|
+
let doneCalled = false;
|
|
109
|
+
|
|
110
|
+
await readStreamWithYield(stream, {
|
|
111
|
+
onChunk: (text) => received.push(text),
|
|
112
|
+
onDone: () => {
|
|
113
|
+
doneCalled = true;
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(received).toEqual([]);
|
|
118
|
+
expect(doneCalled).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("should yield between chunks (non-blocking)", async () => {
|
|
122
|
+
// Create a stream with many rapid chunks
|
|
123
|
+
const chunkCount = 50;
|
|
124
|
+
const chunks = Array.from({ length: chunkCount }, (_, i) => `chunk-${i}`);
|
|
125
|
+
const stream = createMockStream(chunks);
|
|
126
|
+
const received: string[] = [];
|
|
127
|
+
|
|
128
|
+
// Track that setTimeout(0) yields are happening by checking
|
|
129
|
+
// that the promise resolves in multiple event loop ticks
|
|
130
|
+
let macrotaskCount = 0;
|
|
131
|
+
const originalSetTimeout = globalThis.setTimeout;
|
|
132
|
+
|
|
133
|
+
// Count macrotask yields (setTimeout(0) calls from yieldToMacrotask)
|
|
134
|
+
const timeoutSpy = mock((fn: Function, ms: number) => {
|
|
135
|
+
if (ms === 0) macrotaskCount++;
|
|
136
|
+
return originalSetTimeout(fn, ms);
|
|
137
|
+
});
|
|
138
|
+
// @ts-expect-error - mock override
|
|
139
|
+
globalThis.setTimeout = timeoutSpy;
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
await readStreamWithYield(stream, {
|
|
143
|
+
onChunk: (text) => received.push(text),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Every chunk should cause a macrotask yield
|
|
147
|
+
expect(received.length).toBe(chunkCount);
|
|
148
|
+
expect(macrotaskCount).toBeGreaterThanOrEqual(chunkCount);
|
|
149
|
+
} finally {
|
|
150
|
+
globalThis.setTimeout = originalSetTimeout;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
package/src/client/globals.ts
CHANGED