sh-ui-cli 0.25.0 → 0.32.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.
Files changed (43) hide show
  1. package/README.md +8 -9
  2. package/data/changelog/versions.json +117 -1
  3. package/data/registry/react/components/code-editor/index.tsx +232 -0
  4. package/data/registry/react/components/code-editor/styles.css +76 -0
  5. package/data/registry/react/components/code-tabs/index.tsx +49 -0
  6. package/data/registry/react/components/header/index.tsx +632 -82
  7. package/data/registry/react/components/header/styles.css +169 -9
  8. package/data/registry/react/components/markdown-editor/index.tsx +121 -0
  9. package/data/registry/react/components/markdown-editor/styles.css +160 -0
  10. package/data/registry/react/components/page-toc/index.tsx +175 -0
  11. package/data/registry/react/components/page-toc/styles.css +82 -0
  12. package/data/registry/react/components/rich-text-editor/index.tsx +350 -0
  13. package/data/registry/react/components/rich-text-editor/styles.css +196 -0
  14. package/data/registry/react/registry.json +100 -0
  15. package/data/summaries/react.json +6 -1
  16. package/package.json +1 -1
  17. package/src/create/cli-args.js +1 -1
  18. package/src/create/index.mjs +1 -1
  19. package/src/create/plugins/authJwt.js +340 -0
  20. package/src/create/plugins/index.js +2 -1
  21. package/src/create/plugins/sentry.js +32 -280
  22. package/src/mcp.mjs +1 -2
  23. package/templates/flutter-standalone/README.md +2 -2
  24. package/templates/monorepo/README.md +1 -1
  25. package/templates/nextjs-app/app/api/proxy/[...path]/route.ts +31 -4
  26. package/templates/nextjs-app/package.json +0 -1
  27. package/templates/nextjs-app/src/app/providers/tanstack/QueryClientProvider.tsx +5 -18
  28. package/templates/nextjs-app/src/shared/api/clientFetch.ts +40 -0
  29. package/templates/nextjs-app/src/shared/api/http.ts +13 -56
  30. package/templates/nextjs-app/src/shared/api/observability.ts +20 -0
  31. package/templates/nextjs-app/src/shared/api/queryClient.ts +30 -0
  32. package/templates/nextjs-app/src/shared/api/serverFetch.ts +59 -0
  33. package/templates/nextjs-app/src/shared/hooks/useAppMutation.ts +52 -0
  34. package/templates/nextjs-standalone/README.md +3 -3
  35. package/templates/nextjs-standalone/app/api/proxy/[...path]/route.ts +31 -4
  36. package/templates/nextjs-standalone/package.json +0 -1
  37. package/templates/nextjs-standalone/src/app/providers/tanstack/QueryClientProvider.tsx +5 -18
  38. package/templates/nextjs-standalone/src/shared/api/clientFetch.ts +40 -0
  39. package/templates/nextjs-standalone/src/shared/api/http.ts +13 -56
  40. package/templates/nextjs-standalone/src/shared/api/observability.ts +20 -0
  41. package/templates/nextjs-standalone/src/shared/api/queryClient.ts +30 -0
  42. package/templates/nextjs-standalone/src/shared/api/serverFetch.ts +59 -0
  43. package/templates/nextjs-standalone/src/shared/hooks/useAppMutation.ts +52 -0
@@ -157,6 +157,106 @@
157
157
  ],
158
158
  "registryDependencies": []
159
159
  },
160
+ "code-tabs": {
161
+ "name": "code-tabs",
162
+ "type": "component",
163
+ "files": [
164
+ {
165
+ "src": "components/code-tabs/index.tsx",
166
+ "dest": "{components}/code-tabs/index.tsx"
167
+ }
168
+ ],
169
+ "dependencies": [],
170
+ "registryDependencies": [
171
+ "tabs",
172
+ "code-panel"
173
+ ]
174
+ },
175
+ "page-toc": {
176
+ "name": "page-toc",
177
+ "type": "component",
178
+ "files": [
179
+ {
180
+ "src": "components/page-toc/index.tsx",
181
+ "dest": "{components}/page-toc/index.tsx"
182
+ },
183
+ {
184
+ "src": "components/page-toc/styles.css",
185
+ "dest": "{components}/page-toc/styles.css"
186
+ }
187
+ ],
188
+ "dependencies": [],
189
+ "registryDependencies": []
190
+ },
191
+ "code-editor": {
192
+ "name": "code-editor",
193
+ "type": "component",
194
+ "files": [
195
+ {
196
+ "src": "components/code-editor/index.tsx",
197
+ "dest": "{components}/code-editor/index.tsx"
198
+ },
199
+ {
200
+ "src": "components/code-editor/styles.css",
201
+ "dest": "{components}/code-editor/styles.css"
202
+ }
203
+ ],
204
+ "dependencies": [
205
+ "codemirror",
206
+ "@codemirror/state",
207
+ "@codemirror/view",
208
+ "@codemirror/lang-javascript",
209
+ "@codemirror/lang-json",
210
+ "@codemirror/lang-css",
211
+ "@codemirror/lang-html",
212
+ "@codemirror/lang-markdown"
213
+ ],
214
+ "registryDependencies": []
215
+ },
216
+ "markdown-editor": {
217
+ "name": "markdown-editor",
218
+ "type": "component",
219
+ "files": [
220
+ {
221
+ "src": "components/markdown-editor/index.tsx",
222
+ "dest": "{components}/markdown-editor/index.tsx"
223
+ },
224
+ {
225
+ "src": "components/markdown-editor/styles.css",
226
+ "dest": "{components}/markdown-editor/styles.css"
227
+ }
228
+ ],
229
+ "dependencies": [
230
+ "react-markdown",
231
+ "remark-gfm"
232
+ ],
233
+ "registryDependencies": [
234
+ "code-editor"
235
+ ]
236
+ },
237
+ "rich-text-editor": {
238
+ "name": "rich-text-editor",
239
+ "type": "component",
240
+ "files": [
241
+ {
242
+ "src": "components/rich-text-editor/index.tsx",
243
+ "dest": "{components}/rich-text-editor/index.tsx"
244
+ },
245
+ {
246
+ "src": "components/rich-text-editor/styles.css",
247
+ "dest": "{components}/rich-text-editor/styles.css"
248
+ }
249
+ ],
250
+ "dependencies": [
251
+ "@tiptap/react",
252
+ "@tiptap/pm",
253
+ "@tiptap/starter-kit",
254
+ "@tiptap/extension-placeholder",
255
+ "@tiptap/extension-link",
256
+ "lucide-react"
257
+ ],
258
+ "registryDependencies": []
259
+ },
160
260
  "select": {
161
261
  "name": "select",
162
262
  "type": "component",
@@ -27,7 +27,7 @@
27
27
  "accordion": "펼침/접힘 아코디언 — single/multiple (Base UI).",
28
28
  "carousel": "슬라이드 캐러셀 — Embla 기반, autoplay/autoscroll.",
29
29
  "sidebar": "앱 사이드바 — collapsible, SidebarMenu/SidebarGroup 조합.",
30
- "header": "앱 헤더 — 로고/네비/액션 compound.",
30
+ "header": "앱 헤더 — 로고/네비/액션 compound, 데스크탑 inline / 모바일 drawer 자동 전환. drawer focus trap·ESC·focus restore. HeaderNav value(controlled) / defaultValue+onValueChange(uncontrolled) 로 자식 HeaderItem active 자동 매칭 (aria-current 자동, match 커스터마이즈). HeaderMenu(서브메뉴, 데스크탑 portal dropdown / drawer collapsible) · HeaderNavGroup(섹션 라벨) · HeaderDesktopOnly/HeaderMobileOnly(가시성 토글, drawer 이동 없음). variant(solid/transparent/blur) · stickyHide(prefers-reduced-motion 존중, 컨테이너 스크롤 자동 감지) 정식 지원. backdrop-filter @supports 폴백.",
31
31
  "breadcrumb": "경로 내비게이션 — compound (Breadcrumb.List/Item/Link/Page/Separator/Ellipsis). aria-current 자동.",
32
32
  "pagination": "페이지 단위 내비게이션 — compound (PaginationContent/Item/Link/Previous/Next/Ellipsis). getPaginationRange 유틸 동봉. aria-current 자동.",
33
33
  "avatar": "프로필 아바타 — 이미지 fallback → initials (Base UI).",
@@ -38,6 +38,11 @@
38
38
  "skeleton": "스켈레톤 로딩 플레이스홀더.",
39
39
  "theme": "테마 프로바이더 + useTheme 훅 — light/dark/system.",
40
40
  "code-panel": "Shiki 기반 코드 하이라이트 패널 — 복사 버튼 포함.",
41
+ "code-tabs": "여러 코드 뷰(예: React/Flutter, 강조/전체)를 탭으로 전환 — Tabs + CodePanel 합성, 각 탭 내용은 CodePanelProps 그대로.",
42
+ "page-toc": "페이지 자동 목차 — 헤딩 스캔 · slugify · IntersectionObserver active 추적 · smooth scroll. routeKey 로 라우트 변경 자동 재스캔, levels/excludeSelector 커스터마이즈.",
43
+ "code-editor": "CodeMirror 6 기반 코드 에디터 — js/ts/jsx/tsx/json/css/html/markdown, sh-ui 토큰 테마, readOnly·placeholder·minHeight/maxHeight. controlled(value/onChange) · uncontrolled(defaultValue) 모두 지원.",
44
+ "markdown-editor": "마크다운 에디터 — CodeEditor + react-markdown 라이브 프리뷰 합성, GFM 지원, raw HTML 차단으로 XSS 방어. controlled(value/onChange) · uncontrolled(defaultValue) 모두 지원.",
45
+ "rich-text-editor": "Tiptap 3 기반 WYSIWYG 에디터 — HTML 입출력, 기본 toolbar(헤딩·리스트·인용·코드·링크·강조), readOnly·placeholder. controlled(value/onChange) · uncontrolled(defaultValue) 모두 지원.",
41
46
  "base": "CSS 리셋 — base.css.",
42
47
  "breakpoints": "반응형 미디어 쿼리 토큰 — breakpoints.css.",
43
48
  "focus-ring": "공용 포커스 링 스타일 — focus-ring.css.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh-ui-cli",
3
- "version": "0.25.0",
3
+ "version": "0.32.0",
4
4
  "description": "sh-ui CLI — 프로젝트 스캐폴드(create) + 컴포넌트 추가(add/list/remove) + IDE-내 AI용 MCP 서버",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  const VALID_PLATFORMS = ['next', 'flutter'];
2
2
  const VALID_STRUCTURES = ['standalone', 'monorepo'];
3
- const VALID_PLUGINS = ['sentry', 'next-intl'];
3
+ const VALID_PLUGINS = ['sentry', 'next-intl', 'auth-jwt'];
4
4
 
5
5
  const VALUE_FLAGS = ['platform', 'structure', 'plugins', 'theme', 'app'];
6
6
  const BOOL_FLAGS = ['yes', 'help'];
@@ -13,7 +13,7 @@ export const HELP_TEXT = `sh-ui create — sh-ui 프로젝트 스캐폴드 (Next
13
13
  옵션:
14
14
  --platform <next|flutter> 타겟 플랫폼
15
15
  --structure <standalone|monorepo> Next.js 프로젝트 구조 (next 일 때)
16
- --plugins <a,b> 플러그인 (sentry, next-intl). 미지정/"" → 없음
16
+ --plugins <a,b> 플러그인 (sentry, next-intl, auth-jwt). 미지정/"" → 없음
17
17
  --theme <base64> 테마 JSON (base64). 선택
18
18
  --yes 디렉토리 덮어쓰기 + 모노레포 기본값 자동 채택
19
19
  -h, --help 이 도움말
@@ -0,0 +1,340 @@
1
+ export const authJwtPlugin = {
2
+ name: 'auth-jwt',
3
+ label: '쿠키 기반 JWT 인증 (refresh 자리표시자 포함)',
4
+ priority: 2,
5
+
6
+ // 의존성 추가 없음 — 베이스의 fetch + cookies + react-query 만 사용
7
+
8
+ envVars: [
9
+ '# Auth (auth-jwt)',
10
+ 'COOKIE_SECURE=false',
11
+ ],
12
+
13
+ turboEnvVars: [
14
+ 'COOKIE_SECURE',
15
+ ],
16
+
17
+ providerImports: [],
18
+ providerWrappers: [],
19
+
20
+ // ─── 독립 파일 ───
21
+ //
22
+ // 베이스의 BFF (app/api/proxy/[...path]/route.ts) 를 refresh-aware 버전으로
23
+ // 덮어쓰고, 미들웨어와 인증 헬퍼를 추가한다.
24
+ // refreshSession.ts 는 v1 placeholder — 백엔드 명세 확정 후 본문만 채우면
25
+ // BFF 와 withAuthRetry 가 자동 활용한다.
26
+
27
+ files: {
28
+ 'src/proxy.ts': `import { NextRequest, NextResponse } from 'next/server';
29
+
30
+ const AUTH_ROUTES = ['/sign-in', '/sign-up'];
31
+
32
+ /**
33
+ * Next 16+ 의 proxy.ts (구 middleware.ts).
34
+ * 토큰 존재 여부만 검사한다 — 만료 검사나 refresh 는 하지 않는다.
35
+ *
36
+ * - AT 쿠키 없음 + 인증 라우트 아님 → /sign-in 으로 리다이렉트
37
+ * - AT 쿠키 있음 또는 인증 라우트 → 통과
38
+ *
39
+ * AT 가 만료된 채 통과한 요청은 BFF (/api/proxy) 가 401 을 받아
40
+ * refreshSession 으로 갱신을 시도한다.
41
+ */
42
+ export default function proxy(req: NextRequest) {
43
+ const { pathname } = req.nextUrl;
44
+ const hasToken = !!req.cookies.get('accessToken')?.value;
45
+ const isAuthRoute = AUTH_ROUTES.some((r) => pathname.startsWith(r));
46
+
47
+ if (isAuthRoute) return NextResponse.next();
48
+ if (!hasToken) return NextResponse.redirect(new URL('/sign-in', req.url));
49
+
50
+ return NextResponse.next();
51
+ }
52
+
53
+ export const config = {
54
+ matcher: '/((?!api|_next|.*\\\\..*).*)',
55
+ };
56
+ `,
57
+
58
+ 'src/shared/api/refreshSession.ts': `type RefreshResult =
59
+ | { ok: true; accessToken: string; refreshToken: string }
60
+ | { ok: false };
61
+
62
+ let inflight: Promise<RefreshResult> | null = null;
63
+
64
+ /**
65
+ * refreshToken 으로 새 accessToken / refreshToken 을 발급받는다.
66
+ *
67
+ * v1 placeholder — 백엔드 refresh API 명세가 확정되면 아래 TODO 부분을 채운다.
68
+ * 본문이 채워지면 BFF (/api/proxy) 와 withAuthRetry 가 자동으로 활용한다.
69
+ *
70
+ * 동시에 여러 요청이 401 을 만나도 inflight 모듈 변수로 코얼레싱돼서
71
+ * refresh 는 한 번만 발사된다.
72
+ *
73
+ * 참고 구현 예시:
74
+ *
75
+ * const res = await fetch(\`\${process.env.API_URL}/v1/auth/token/refresh\`, {
76
+ * method: 'POST',
77
+ * headers: { 'Content-Type': 'application/json' },
78
+ * body: JSON.stringify({ refreshToken }),
79
+ * });
80
+ * const body = await res.json();
81
+ * if (body.result === 'SUCCESS') {
82
+ * return {
83
+ * ok: true,
84
+ * accessToken: body.data.accessToken,
85
+ * refreshToken: body.data.refreshToken,
86
+ * };
87
+ * }
88
+ * return { ok: false };
89
+ */
90
+ export async function refreshSession(
91
+ refreshToken: string,
92
+ ): Promise<RefreshResult> {
93
+ if (inflight) return inflight;
94
+
95
+ inflight = (async (): Promise<RefreshResult> => {
96
+ try {
97
+ // TODO: 백엔드 refresh API 명세 확정 후 여기에 fetch 호출을 작성.
98
+ // 지금은 placeholder 라 항상 실패 → BFF 가 쿠키 삭제 + 401 응답.
99
+ void refreshToken;
100
+ return { ok: false };
101
+ } finally {
102
+ inflight = null;
103
+ }
104
+ })();
105
+
106
+ return inflight;
107
+ }
108
+ `,
109
+
110
+ 'src/shared/api/withAuthRetry.ts': `import { cookies } from 'next/headers';
111
+
112
+ import { ApiError } from './error';
113
+ import { refreshSession } from './refreshSession';
114
+
115
+ const ACCESS_TOKEN_COOKIE = 'accessToken';
116
+ const REFRESH_TOKEN_COOKIE = 'refreshToken';
117
+
118
+ const COOKIE = {
119
+ httpOnly: true,
120
+ secure: process.env.COOKIE_SECURE === 'true',
121
+ sameSite: 'lax' as const,
122
+ path: '/',
123
+ };
124
+
125
+ /**
126
+ * Route Handler 또는 Server Action 안에서 인증된 요청을 보낼 때 사용한다.
127
+ * 401 을 만나면 refreshSession 으로 토큰을 갱신하고 fn 을 한 번 더 실행한다.
128
+ *
129
+ * RSC (Server Component) 에서는 cookies().set() 이 막혀 있으므로 여기서 호출하면 안 된다.
130
+ * RSC 는 serverFetch 를 직접 호출하고, 401 은 prefetchQuery 가 swallow 한 뒤
131
+ * 클라이언트 refetch 가 BFF 경유로 자동 복구한다.
132
+ *
133
+ * 사용 예 (Server Action):
134
+ *
135
+ * 'use server';
136
+ * import { serverFetch } from '@/src/shared/api/serverFetch';
137
+ * import { withAuthRetry } from '@/src/shared/api/withAuthRetry';
138
+ *
139
+ * export async function toggleFavoriteAction(id: number) {
140
+ * return withAuthRetry(() =>
141
+ * serverFetch(\`/v1/products/\${id}/favorite\`, { method: 'POST' }),
142
+ * );
143
+ * }
144
+ */
145
+ export async function withAuthRetry<T>(fn: () => Promise<T>): Promise<T> {
146
+ try {
147
+ return await fn();
148
+ } catch (e) {
149
+ if (!(e instanceof ApiError) || e.status !== 401) throw e;
150
+
151
+ const jar = await cookies();
152
+ const refreshToken = jar.get(REFRESH_TOKEN_COOKIE)?.value;
153
+ if (!refreshToken) throw e;
154
+
155
+ const r = await refreshSession(refreshToken);
156
+ if (!r.ok) throw new ApiError(401, 'UNAUTHORIZED', null);
157
+
158
+ jar.set(ACCESS_TOKEN_COOKIE, r.accessToken, COOKIE);
159
+ jar.set(REFRESH_TOKEN_COOKIE, r.refreshToken, COOKIE);
160
+
161
+ return await fn();
162
+ }
163
+ }
164
+ `,
165
+
166
+ 'app/api/proxy/[...path]/route.ts': `import { cookies } from 'next/headers';
167
+ import { NextResponse, type NextRequest } from 'next/server';
168
+
169
+ import {
170
+ captureApiError,
171
+ logApiError,
172
+ } from '@/src/shared/api/observability';
173
+ import { refreshSession } from '@/src/shared/api/refreshSession';
174
+
175
+ const API_URL = process.env.API_URL ?? 'http://localhost:8080/api';
176
+ const ACCESS_TOKEN_COOKIE = 'accessToken';
177
+ const REFRESH_TOKEN_COOKIE = 'refreshToken';
178
+ const LOCALE_COOKIE = 'NEXT_LOCALE';
179
+
180
+ const COOKIE = {
181
+ httpOnly: true,
182
+ secure: process.env.COOKIE_SECURE === 'true',
183
+ sameSite: 'lax' as const,
184
+ path: '/',
185
+ };
186
+
187
+ const clearAuthCookies = (res: NextResponse) => {
188
+ res.cookies.set(ACCESS_TOKEN_COOKIE, '', { ...COOKIE, maxAge: 0 });
189
+ res.cookies.set(REFRESH_TOKEN_COOKIE, '', { ...COOKIE, maxAge: 0 });
190
+ return res;
191
+ };
192
+
193
+ const proxyRequest = async (
194
+ request: NextRequest,
195
+ ctx: { params: Promise<{ path: string[] }> },
196
+ method: string,
197
+ ) => {
198
+ const { path } = await ctx.params;
199
+ const apiPath = path.join('/');
200
+ const url = new URL(\`\${API_URL}/\${apiPath}\`);
201
+
202
+ request.nextUrl.searchParams.forEach((value, key) => {
203
+ url.searchParams.set(key, value);
204
+ });
205
+
206
+ const cookieStore = await cookies();
207
+ const accessToken = cookieStore.get(ACCESS_TOKEN_COOKIE)?.value;
208
+ const refreshToken = cookieStore.get(REFRESH_TOKEN_COOKIE)?.value;
209
+ const locale =
210
+ cookieStore.get(LOCALE_COOKIE)?.value ??
211
+ request.headers.get('Accept-Language') ??
212
+ undefined;
213
+
214
+ const headers: Record<string, string> = {};
215
+ if (accessToken) headers.Authorization = \`Bearer \${accessToken}\`;
216
+ if (locale) headers['Accept-Language'] = locale;
217
+
218
+ let body: BodyInit | undefined;
219
+ if (method !== 'GET' && method !== 'HEAD') {
220
+ const contentType = request.headers.get('Content-Type');
221
+ if (contentType?.includes('multipart/form-data')) {
222
+ body = await request.formData();
223
+ } else {
224
+ headers['Content-Type'] = 'application/json';
225
+ body = await request.text();
226
+ }
227
+ }
228
+
229
+ let response: Response;
230
+ try {
231
+ response = await fetch(url.toString(), { method, headers, body });
232
+ } catch (error) {
233
+ console.error(\`[PROXY] \${method} \${url.toString()} —\`, error);
234
+ return NextResponse.json(
235
+ {
236
+ result: 'ERROR',
237
+ data: null,
238
+ error: {
239
+ code: 'NETWORK_ERROR',
240
+ message: '서버에 연결할 수 없습니다.',
241
+ },
242
+ },
243
+ { status: 502 },
244
+ );
245
+ }
246
+
247
+ let data = await response.json();
248
+
249
+ // 401 → refresh 시도 (RT 가 있을 때만)
250
+ if (response.status === 401) {
251
+ if (!refreshToken) {
252
+ return clearAuthCookies(NextResponse.json(data, { status: 401 }));
253
+ }
254
+
255
+ const r = await refreshSession(refreshToken);
256
+
257
+ // refresh placeholder 가 본문 미구현이면 항상 ok:false
258
+ // → 쿠키 삭제 + 401 그대로 (clientFetch 가 /sign-in 으로 이동)
259
+ if (!r.ok) {
260
+ return clearAuthCookies(NextResponse.json(data, { status: 401 }));
261
+ }
262
+
263
+ // 새 AT 로 재시도
264
+ headers.Authorization = \`Bearer \${r.accessToken}\`;
265
+ const retry = await fetch(url.toString(), { method, headers, body });
266
+ data = await retry.json();
267
+
268
+ const res = NextResponse.json(data, { status: retry.status });
269
+ res.cookies.set(ACCESS_TOKEN_COOKIE, r.accessToken, COOKIE);
270
+ res.cookies.set(REFRESH_TOKEN_COOKIE, r.refreshToken, COOKIE);
271
+
272
+ if (!retry.ok) {
273
+ logApiError('PROXY', {
274
+ url: url.toString(),
275
+ method,
276
+ status: retry.status,
277
+ requestBody: typeof body === 'string' ? body : undefined,
278
+ responseBody: data,
279
+ });
280
+
281
+ captureApiError({
282
+ url: url.toString(),
283
+ apiPath,
284
+ method,
285
+ status: retry.status,
286
+ responseBody: data,
287
+ });
288
+ }
289
+
290
+ return res;
291
+ }
292
+
293
+ if (!response.ok) {
294
+ logApiError('PROXY', {
295
+ url: url.toString(),
296
+ method,
297
+ status: response.status,
298
+ requestBody: typeof body === 'string' ? body : undefined,
299
+ responseBody: data,
300
+ });
301
+
302
+ captureApiError({
303
+ url: url.toString(),
304
+ apiPath,
305
+ method,
306
+ status: response.status,
307
+ responseBody: data,
308
+ });
309
+ }
310
+
311
+ return NextResponse.json(data, { status: response.status });
312
+ };
313
+
314
+ export const GET = (
315
+ req: NextRequest,
316
+ ctx: { params: Promise<{ path: string[] }> },
317
+ ) => proxyRequest(req, ctx, 'GET');
318
+
319
+ export const POST = (
320
+ req: NextRequest,
321
+ ctx: { params: Promise<{ path: string[] }> },
322
+ ) => proxyRequest(req, ctx, 'POST');
323
+
324
+ export const PUT = (
325
+ req: NextRequest,
326
+ ctx: { params: Promise<{ path: string[] }> },
327
+ ) => proxyRequest(req, ctx, 'PUT');
328
+
329
+ export const PATCH = (
330
+ req: NextRequest,
331
+ ctx: { params: Promise<{ path: string[] }> },
332
+ ) => proxyRequest(req, ctx, 'PATCH');
333
+
334
+ export const DELETE = (
335
+ req: NextRequest,
336
+ ctx: { params: Promise<{ path: string[] }> },
337
+ ) => proxyRequest(req, ctx, 'DELETE');
338
+ `,
339
+ },
340
+ };
@@ -1,7 +1,8 @@
1
1
  import { sentryPlugin } from './sentry.js';
2
2
  import { nextIntlPlugin } from './nextIntl.js';
3
+ import { authJwtPlugin } from './authJwt.js';
3
4
 
4
- export const allPlugins = [sentryPlugin, nextIntlPlugin];
5
+ export const allPlugins = [sentryPlugin, nextIntlPlugin, authJwtPlugin];
5
6
 
6
7
  export function getPluginChoices() {
7
8
  return allPlugins.map((p) => ({