@gaonjs/vue 0.1.4 → 0.2.0

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/dist/api.d.ts ADDED
@@ -0,0 +1,52 @@
1
+ import type { Serialized } from './serialize.js';
2
+ /**
3
+ * 라우트 매니페스트 — .gaon/routes.d.ts 가 채우는 GaonRouteMap 옆에 함께
4
+ * 채워지는 런타임/타입 지도. 키는 'controller#action'.
5
+ *
6
+ * pageProps 는 타입만 알면 되지만 api() 는 실제 HTTP 메서드·경로를 알아야
7
+ * 요청을 보낼 수 있다 — 그래서 라우트 지도가 필요하다. 생성기가 declaration
8
+ * merging + 값 export 로 함께 채운다(추가 생성 파일 X · 기존 파일에 병합).
9
+ */
10
+ export interface GaonRouteInfo {
11
+ readonly method: 'get' | 'post' | 'put' | 'patch' | 'delete';
12
+ readonly path: string;
13
+ }
14
+ declare global {
15
+ interface GaonRoutes {
16
+ }
17
+ }
18
+ /** GaonRouteMap[K] (액션 함수) 에서 render 결과 P 를 뽑아 낸다 — pageProps 와 동일. */
19
+ type PropsOf<F> = F extends (...args: never[]) => infer R ? Awaited<R> extends {
20
+ page: string;
21
+ props: infer P;
22
+ } ? P : Awaited<R> : never;
23
+ /**
24
+ * 요청 파라미터 — 라우트에 :id 같은 자리표시자가 있으면 여기서 뽑고, 나머지는
25
+ * GET 이면 쿼리스트링, 그 외 메서드는 JSON 본문으로 붙는다. 값은 문자열
26
+ * 화된다(경로/쿼리 특성).
27
+ */
28
+ export type ApiParams = Record<string, string | number | bigint | boolean | undefined | null>;
29
+ export interface ApiOptions {
30
+ /** 기본 http 헤더에 얹어 보낸다(Authorization 등). */
31
+ readonly headers?: Record<string, string>;
32
+ /** AbortController 지원(취소·타임아웃). */
33
+ readonly signal?: AbortSignal;
34
+ /** URL prefix (앱 프리픽스가 web 이 아니면 지정). 생략 시 window.origin. */
35
+ readonly baseUrl?: string;
36
+ /** fetch 구현 주입(테스트·SSR). 생략 시 globalThis.fetch. */
37
+ readonly fetch?: typeof fetch;
38
+ }
39
+ /**
40
+ * 라우트 키로 서버 액션을 부른다.
41
+ * · 반환 타입: 액션 반환의 Serialized<> (직렬화 경계 재사용).
42
+ * · 요청: 메서드는 라우트 정의에서, 경로는 :param 을 채워 완성.
43
+ * · 실패: 4xx/5xx 는 예외로 던지고, ValidationError(422)는 issues 를 포함한다.
44
+ */
45
+ export declare function api<K extends keyof GaonRouteMap>(key: K, params?: ApiParams, opts?: ApiOptions): Promise<Serialized<PropsOf<GaonRouteMap[K]>>>;
46
+ /**
47
+ * 라우트 매니페스트를 등록한다 — 프론트엔드 부트스트랩(app 엔트리)이 한 번
48
+ * 호출하면 이후 api() 가 method·path 를 안다. 앱 빌드 시 .gaon/routes.d.ts
49
+ * 와 함께 생성되는 매니페스트 모듈이 이 함수를 호출한다.
50
+ */
51
+ export declare function registerRoutes(routes: Record<string, GaonRouteInfo>): void;
52
+ export {};
package/dist/api.js ADDED
@@ -0,0 +1,127 @@
1
+ // @gaonjs/vue · api() — 타입드 클라이언트 (errata E-3 §C)
2
+ //
3
+ // pageProps 와 **같은 .gaon/routes.d.ts 브리지**를 재사용한다 — 추가 생성 파일
4
+ // 없음. 라우트 키 하나로 서버 액션의 반환 타입을 클라이언트에서 그대로 안다.
5
+ //
6
+ // import { api } from 'gaonjs/vue'
7
+ // const { results } = await api('posts#search', { q: keyword.value })
8
+ //
9
+ // 반환 타입은 액션 반환 타입의 **Serialized<>** (M4 render props 직렬화와 같은
10
+ // 규칙 · 같은 매핑 재사용). 별도 직렬화 규칙을 두지 않는다(The One Way).
11
+ //
12
+ // 두 번째 인자는 라우트 파라미터 + 쿼리(HTTP 메서드는 라우트 정의에서 결정).
13
+ // 라우트에 `:id` 처럼 자리표시자가 있으면 params 에서 뽑아 경로에 채우고,
14
+ // 남는 값은 GET 은 쿼리스트링으로, POST/PUT/PATCH/DELETE 는 JSON 본문으로 실어
15
+ // 보낸다. 서버 쪽 this.params 병합 우선순위(라우트>body>query)와 일치한다.
16
+ // ── 런타임 ────────────────────────────────────────────────────
17
+ /**
18
+ * 라우트 키로 서버 액션을 부른다.
19
+ * · 반환 타입: 액션 반환의 Serialized<> (직렬화 경계 재사용).
20
+ * · 요청: 메서드는 라우트 정의에서, 경로는 :param 을 채워 완성.
21
+ * · 실패: 4xx/5xx 는 예외로 던지고, ValidationError(422)는 issues 를 포함한다.
22
+ */
23
+ export async function api(key, params = {}, opts = {}) {
24
+ const routes = (globalThis.__GAON_ROUTES__) ??
25
+ {};
26
+ const info = routes[key];
27
+ if (!info) {
28
+ throw new Error(`[gaonjs/vue] 라우트 '${String(key)}' 를 찾을 수 없습니다.\n` +
29
+ `→ apps/<app>/routes.ts 에 정의돼 있는지, gaon dev/check 가 .gaon/routes.d.ts 를 재생성했는지 확인하세요.`);
30
+ }
31
+ const { url, leftovers } = buildUrl(info, params);
32
+ const method = info.method.toUpperCase();
33
+ const useBody = method !== 'GET' && method !== 'HEAD';
34
+ const headers = {
35
+ Accept: 'application/json',
36
+ ...(opts.headers ?? {}),
37
+ };
38
+ let body;
39
+ let finalUrl = url;
40
+ if (useBody) {
41
+ body = JSON.stringify(leftovers);
42
+ headers['Content-Type'] = 'application/json; charset=utf-8';
43
+ // CSRF: 세션 앱은 쿠키 기반 CSRF 를 쓴다 — 프론트 부트스트랩이 심어 놓은
44
+ // 메타 태그 값을 자동으로 붙여 준다(있으면).
45
+ const csrf = readCsrfToken();
46
+ if (csrf)
47
+ headers['X-CSRF-Token'] = csrf;
48
+ }
49
+ else if (Object.keys(leftovers).length > 0) {
50
+ const q = new URLSearchParams();
51
+ for (const [k, v] of Object.entries(leftovers)) {
52
+ if (v === undefined || v === null)
53
+ continue;
54
+ q.append(k, String(v));
55
+ }
56
+ finalUrl = url + (url.includes('?') ? '&' : '?') + q.toString();
57
+ }
58
+ const base = opts.baseUrl ?? '';
59
+ const doFetch = opts.fetch ?? globalThis.fetch;
60
+ const res = await doFetch(base + finalUrl, {
61
+ method,
62
+ headers,
63
+ body,
64
+ signal: opts.signal,
65
+ credentials: 'same-origin',
66
+ });
67
+ if (!res.ok) {
68
+ // 422 는 서버 검증 실패(§7.5.3). 파싱 시도 후 issues 를 실어 재던진다.
69
+ let payload = undefined;
70
+ try {
71
+ payload = await res.json();
72
+ }
73
+ catch {
74
+ /* 본문 없음 */
75
+ }
76
+ const err = new Error(`[gaonjs/vue] ${String(key)} 요청 실패 (${res.status} ${res.statusText}).`);
77
+ err.status = res.status;
78
+ err.body = payload;
79
+ throw err;
80
+ }
81
+ // 204 No Content 는 빈 값으로.
82
+ if (res.status === 204)
83
+ return undefined;
84
+ const json = (await res.json());
85
+ return json;
86
+ }
87
+ /**
88
+ * 라우트 매니페스트를 등록한다 — 프론트엔드 부트스트랩(app 엔트리)이 한 번
89
+ * 호출하면 이후 api() 가 method·path 를 안다. 앱 빌드 시 .gaon/routes.d.ts
90
+ * 와 함께 생성되는 매니페스트 모듈이 이 함수를 호출한다.
91
+ */
92
+ export function registerRoutes(routes) {
93
+ const g = globalThis;
94
+ g.__GAON_ROUTES__ = { ...(g.__GAON_ROUTES__ ?? {}), ...routes };
95
+ }
96
+ // ── 내부 ──────────────────────────────────────────────────────
97
+ function buildUrl(info, params) {
98
+ const leftovers = {};
99
+ let url = info.path;
100
+ // 사용된 자리표시자는 leftovers 에서 빼고, 남는 값은 쿼리/본문에 실린다.
101
+ const used = new Set();
102
+ url = url.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (_m, name) => {
103
+ used.add(name);
104
+ const v = params[name];
105
+ if (v === undefined || v === null) {
106
+ throw new Error(`[gaonjs/vue] 라우트 파라미터 ':${name}' 값이 없습니다 (path=${info.path}).\n` +
107
+ `→ api('${info.method} ${info.path}', { ${name}: ... }) 처럼 넘기세요.`);
108
+ }
109
+ return encodeURIComponent(String(v));
110
+ });
111
+ for (const [k, v] of Object.entries(params)) {
112
+ if (used.has(k) || v === undefined || v === null)
113
+ continue;
114
+ leftovers[k] = String(v);
115
+ }
116
+ return { url, leftovers };
117
+ }
118
+ function readCsrfToken() {
119
+ const doc = globalThis.document;
120
+ if (!doc)
121
+ return undefined;
122
+ const el = doc.querySelector('meta[name="csrf-token"]');
123
+ if (!el)
124
+ return undefined;
125
+ const c = el.getAttribute('content');
126
+ return c ?? undefined;
127
+ }
package/dist/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export declare const version: string;
2
2
  export declare const status: "planned";
3
3
  export { pageProps } from "./pageProps.js";
4
4
  export { serializeProps, type Serialized } from "./serialize.js";
5
+ export { api, registerRoutes, type ApiParams, type ApiOptions, type GaonRouteInfo, } from "./api.js";
package/dist/index.js CHANGED
@@ -12,3 +12,5 @@ export const status = "planned";
12
12
  export { pageProps } from "./pageProps.js";
13
13
  // 직렬화 경계 (M4) — Rec → JSON-safe 페이지 props (Date/bigint→string, hidden 제외)
14
14
  export { serializeProps } from "./serialize.js";
15
+ // 타입드 클라이언트 (errata E-3) — 라우트 키 → Serialized 반환
16
+ export { api, registerRoutes, } from "./api.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaonjs/vue",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Gaon 프론트엔드 어댑터: Vue 3 · Inertia 브리지 · 타입 전파 · SSR (구현 예정)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,8 +24,8 @@
24
24
  "README.md"
25
25
  ],
26
26
  "dependencies": {
27
- "@gaonjs/core": "0.1.3",
28
- "@gaonjs/web": "0.2.0"
27
+ "@gaonjs/core": "0.1.4",
28
+ "@gaonjs/web": "0.3.0"
29
29
  },
30
30
  "scripts": {
31
31
  "build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json"