sh-ui-cli 0.97.0 → 0.98.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/README.md +1 -1
- package/data/changelog/versions.json +13 -0
- package/package.json +1 -1
- package/src/api.d.ts +0 -2
- package/src/constants.js +0 -5
- package/src/create/cli-args.js +1 -5
- package/src/create/describeTemplate.js +2 -31
- package/src/create/generator.js +9 -665
- package/src/create/index.mjs +2 -5
- package/src/create/plugins/index.js +1 -3
- package/src/create/plugins/nextIntl.js +0 -4
- package/src/create/plugins/pluginSchema.js +1 -1
- package/src/mcp.mjs +0 -34
- package/src/create/plugins/authJwt.js +0 -420
- package/src/create/plugins/sentry.js +0 -467
package/src/create/generator.js
CHANGED
|
@@ -220,13 +220,6 @@ export async function createProject(options = {}) {
|
|
|
220
220
|
);
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
// observability 옵션도 vite preset 전용. v0.93.0+.
|
|
224
|
-
if (options.observability && options.observability !== 'none' && platform !== 'vite') {
|
|
225
|
-
throw new Error(
|
|
226
|
-
`observability='${options.observability}' 은 platform=vite 일 때만 지원합니다 (현재 platform=${platform}). --observability none 또는 --platform vite 사용.`,
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
223
|
// arch 결정 — platform 확정 후. 사용자가 --arch 미지정 시:
|
|
231
224
|
// - next → DEFAULT_ARCH ('fsd')
|
|
232
225
|
// - flutter → 현재 Flutter arch 디스크립터 없음 → null. 미래에 flutter arch 추가되면
|
|
@@ -350,7 +343,6 @@ export async function createProject(options = {}) {
|
|
|
350
343
|
await generateViteStandalone(targetDir, projectName, theme, cssFramework, arch, themeBase, {
|
|
351
344
|
i18n: options.i18n ?? 'none',
|
|
352
345
|
locales: options.locales ?? 'ko,en',
|
|
353
|
-
observability: options.observability ?? 'none',
|
|
354
346
|
});
|
|
355
347
|
} else {
|
|
356
348
|
await generateMonorepo(targetDir, projectName, [], {
|
|
@@ -358,7 +350,6 @@ export async function createProject(options = {}) {
|
|
|
358
350
|
platform: 'vite',
|
|
359
351
|
i18n: options.i18n ?? 'none',
|
|
360
352
|
locales: options.locales ?? 'ko,en',
|
|
361
|
-
observability: options.observability ?? 'none',
|
|
362
353
|
appName: options.appName ?? null,
|
|
363
354
|
port: options.port ?? null,
|
|
364
355
|
});
|
|
@@ -394,7 +385,7 @@ export async function createProject(options = {}) {
|
|
|
394
385
|
});
|
|
395
386
|
|
|
396
387
|
// plugins 는 미지정시 기본 빈 배열 — prompt 띄우지 않는다.
|
|
397
|
-
// (플러그인을 쓰려면 명시적으로 --plugins
|
|
388
|
+
// (플러그인을 쓰려면 명시적으로 --plugins next-intl 사용)
|
|
398
389
|
const selectedPluginNames = options.plugins ?? [];
|
|
399
390
|
|
|
400
391
|
const plugins = getPluginsByNames(selectedPluginNames);
|
|
@@ -518,11 +509,6 @@ export async function addApp(options = {}) {
|
|
|
518
509
|
`i18n='${options.i18n}' 은 platform=vite 일 때만 지원합니다 (현재 platform=${platform}).`,
|
|
519
510
|
);
|
|
520
511
|
}
|
|
521
|
-
if (options.observability && options.observability !== 'none' && platform !== 'vite') {
|
|
522
|
-
throw new Error(
|
|
523
|
-
`observability='${options.observability}' 은 platform=vite 일 때만 지원합니다 (현재 platform=${platform}). --observability none 또는 --platform vite 사용.`,
|
|
524
|
-
);
|
|
525
|
-
}
|
|
526
512
|
|
|
527
513
|
const appName = validateProjectName(
|
|
528
514
|
options.name ?? await input({
|
|
@@ -576,7 +562,6 @@ export async function addApp(options = {}) {
|
|
|
576
562
|
await generateViteApp(appsDir, appName, port, arch, css, {
|
|
577
563
|
i18n: options.i18n ?? 'none',
|
|
578
564
|
locales: options.locales ?? 'ko,en',
|
|
579
|
-
observability: options.observability ?? 'none',
|
|
580
565
|
});
|
|
581
566
|
} else {
|
|
582
567
|
await generateApp(appsDir, appName, port, plugins, arch, css);
|
|
@@ -833,7 +818,7 @@ async function generateStandalone(targetDir, projectName, plugins, theme, css, a
|
|
|
833
818
|
await patchShUiConfig(path.join(targetDir, 'sh-ui.config.json'), css, themeBase);
|
|
834
819
|
}
|
|
835
820
|
|
|
836
|
-
async function generateViteStandalone(targetDir, projectName, theme, css, arch, themeBase, { i18n = 'none', locales = 'ko,en'
|
|
821
|
+
async function generateViteStandalone(targetDir, projectName, theme, css, arch, themeBase, { i18n = 'none', locales = 'ko,en' } = {}) {
|
|
837
822
|
// 베이스 (arch-neutral) + arch 오버레이 — generateStandalone 과 같은 패턴.
|
|
838
823
|
await fs.copy(path.join(TEMPLATES_DIR, 'vite-standalone'), targetDir, {
|
|
839
824
|
filter: (src) => !src.includes(`${path.sep}_arch${path.sep}`) && !src.endsWith(`${path.sep}_arch`),
|
|
@@ -873,10 +858,6 @@ async function generateViteStandalone(targetDir, projectName, theme, css, arch,
|
|
|
873
858
|
const localesArr = parseLocales(locales);
|
|
874
859
|
await emitI18n(targetDir, { arch, locales: localesArr });
|
|
875
860
|
}
|
|
876
|
-
|
|
877
|
-
if (observability === 'sentry') {
|
|
878
|
-
await emitSentry(targetDir, { arch, i18nActive: i18n === 'react-i18next' });
|
|
879
|
-
}
|
|
880
861
|
}
|
|
881
862
|
|
|
882
863
|
/**
|
|
@@ -1083,8 +1064,8 @@ async function patchLandingForI18n(targetDir, { arch }) {
|
|
|
1083
1064
|
*
|
|
1084
1065
|
* - balanced bracket 으로 outer `]` 를 찾으므로 `viteStaticCopy({ targets: [...] })`
|
|
1085
1066
|
* 처럼 안쪽에 `[]` 가 있어도 안전.
|
|
1086
|
-
* -
|
|
1087
|
-
*
|
|
1067
|
+
* - i18n 의 viteStaticCopy 처럼 nested `[]` 를 가진 plugin 을 append 할 때
|
|
1068
|
+
* anchor 경합으로 entry 가 안쪽 배열에 inject 되던 v0.92~0.95 회귀의 원인을 제거.
|
|
1088
1069
|
*
|
|
1089
1070
|
* 입력 src 가 예상 형태가 아니면 (`plugins: [` 못 찾음 / unbalanced bracket)
|
|
1090
1071
|
* `null` 을 반환 → 호출부에서 patch 포기.
|
|
@@ -1221,182 +1202,7 @@ function parseLocales(input) {
|
|
|
1221
1202
|
return cleaned.length > 0 ? cleaned : ['ko', 'en'];
|
|
1222
1203
|
}
|
|
1223
1204
|
|
|
1224
|
-
|
|
1225
|
-
* Sentry observability 셋업 emit (v0.93.0+). vite preset 전용 opt-in.
|
|
1226
|
-
*
|
|
1227
|
-
* - shared/observability/sentry.ts — Sentry.init (DSN 있을 때만)
|
|
1228
|
-
* - shared/observability/index.ts — Sentry + ErrorBoundary re-export
|
|
1229
|
-
* - app/providers/SentryProvider.tsx — ErrorBoundary wrapper
|
|
1230
|
-
* - GlobalProvider 재작성 — Sentry > [I18n?] > Theme > Query 순서
|
|
1231
|
-
* - package.json — @sentry/react + @sentry/vite-plugin 추가
|
|
1232
|
-
* - vite.config.ts — sentryVitePlugin 삽입 + sourcemap: true
|
|
1233
|
-
* - .env.example — Sentry 변수 안내 블록 추가
|
|
1234
|
-
*
|
|
1235
|
-
* @param {string} targetDir — 앱 디렉토리
|
|
1236
|
-
* @param {object} opts
|
|
1237
|
-
* @param {object} opts.arch — arch descriptor
|
|
1238
|
-
* @param {boolean} [opts.i18nActive] — i18n 도 같이 켜져 있는지 (GlobalProvider wrapping 순서)
|
|
1239
|
-
*/
|
|
1240
|
-
async function emitSentry(targetDir, { arch, i18nActive = false }) {
|
|
1241
|
-
const isFsd = arch.name === 'fsd';
|
|
1242
|
-
const obsDirRel = isFsd ? 'src/shared/observability' : 'src/lib/observability';
|
|
1243
|
-
const obsAlias = isFsd ? '@/shared/observability' : '@/lib/observability';
|
|
1244
|
-
const providersDirRel = isFsd ? 'src/app/providers' : 'src/components/providers';
|
|
1245
|
-
const apiAlias = isFsd ? '@/shared/api/queryClient' : '@/lib/api/queryClient';
|
|
1246
|
-
|
|
1247
|
-
const obsDir = path.join(targetDir, obsDirRel);
|
|
1248
|
-
await fs.ensureDir(obsDir);
|
|
1249
|
-
|
|
1250
|
-
const sentryTs = `/// <reference types="vite/client" />
|
|
1251
|
-
import * as Sentry from '@sentry/react';
|
|
1252
|
-
|
|
1253
|
-
// VITE_SENTRY_DSN 이 있을 때만 init — 로컬 dev 에선 자동 skip.
|
|
1254
|
-
// GlitchTip self-hosted 도 같은 SDK — DSN 만 변경.
|
|
1255
|
-
if (import.meta.env.VITE_SENTRY_DSN) {
|
|
1256
|
-
Sentry.init({
|
|
1257
|
-
dsn: import.meta.env.VITE_SENTRY_DSN,
|
|
1258
|
-
release: import.meta.env.VITE_APP_VERSION,
|
|
1259
|
-
environment: import.meta.env.MODE,
|
|
1260
|
-
tracesSampleRate: 0.1,
|
|
1261
|
-
replaysOnErrorSampleRate: 1.0,
|
|
1262
|
-
replaysSessionSampleRate: 0,
|
|
1263
|
-
integrations: [
|
|
1264
|
-
Sentry.browserTracingIntegration(),
|
|
1265
|
-
Sentry.replayIntegration({ maskAllText: true, blockAllMedia: true }),
|
|
1266
|
-
],
|
|
1267
|
-
});
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
export { Sentry };
|
|
1271
|
-
`;
|
|
1272
|
-
await fs.writeFile(path.join(obsDir, 'sentry.ts'), sentryTs);
|
|
1273
|
-
|
|
1274
|
-
await fs.writeFile(
|
|
1275
|
-
path.join(obsDir, 'index.ts'),
|
|
1276
|
-
`export { Sentry } from './sentry';\nexport { ErrorBoundary } from '@sentry/react';\n`,
|
|
1277
|
-
);
|
|
1278
|
-
|
|
1279
|
-
const providersDir = path.join(targetDir, providersDirRel);
|
|
1280
|
-
await fs.ensureDir(providersDir);
|
|
1281
|
-
const sentryProvider = `import { type ReactNode } from 'react';
|
|
1282
|
-
import { ErrorBoundary } from '${obsAlias}';
|
|
1283
|
-
|
|
1284
|
-
function Fallback({ error }: { error: unknown }) {
|
|
1285
|
-
return (
|
|
1286
|
-
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
|
1287
|
-
<h1 style={{ fontSize: '1.5rem', fontWeight: 600 }}>오류가 발생했습니다</h1>
|
|
1288
|
-
<p style={{ marginTop: '0.5rem', color: 'var(--foreground-muted)' }}>
|
|
1289
|
-
{error instanceof Error ? error.message : '알 수 없는 오류'}
|
|
1290
|
-
</p>
|
|
1291
|
-
</div>
|
|
1292
|
-
);
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
export function SentryProvider({ children }: { children: ReactNode }) {
|
|
1296
|
-
return (
|
|
1297
|
-
<ErrorBoundary fallback={({ error }) => <Fallback error={error} />}>
|
|
1298
|
-
{children}
|
|
1299
|
-
</ErrorBoundary>
|
|
1300
|
-
);
|
|
1301
|
-
}
|
|
1302
|
-
`;
|
|
1303
|
-
await fs.writeFile(path.join(providersDir, 'SentryProvider.tsx'), sentryProvider);
|
|
1304
|
-
|
|
1305
|
-
// GlobalProvider rewrite — Sentry outermost, then optional I18n, then Theme + Query.
|
|
1306
|
-
const globalProviderPath = path.join(targetDir, providersDirRel, 'GlobalProvider', 'index.tsx');
|
|
1307
|
-
const i18nImport = i18nActive ? `import { I18nProvider } from '../I18nProvider';\n` : '';
|
|
1308
|
-
const innerOpen = i18nActive ? ' <I18nProvider>\n ' : ' ';
|
|
1309
|
-
const innerClose = i18nActive ? '\n </I18nProvider>' : '';
|
|
1310
|
-
const globalProvider = `import { QueryClientProvider } from '@tanstack/react-query';
|
|
1311
|
-
import { type ReactNode, useState } from 'react';
|
|
1312
|
-
import { createQueryClient } from '${apiAlias}';
|
|
1313
|
-
import { ThemeProvider } from '../theme/ThemeProvider';
|
|
1314
|
-
${i18nImport}import { SentryProvider } from '../SentryProvider';
|
|
1315
|
-
|
|
1316
|
-
export function GlobalProvider({ children }: { children: ReactNode }) {
|
|
1317
|
-
const [queryClient] = useState(() => createQueryClient());
|
|
1318
|
-
return (
|
|
1319
|
-
<SentryProvider>
|
|
1320
|
-
${innerOpen}<ThemeProvider>
|
|
1321
|
-
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
1322
|
-
</ThemeProvider>${innerClose}
|
|
1323
|
-
</SentryProvider>
|
|
1324
|
-
);
|
|
1325
|
-
}
|
|
1326
|
-
`;
|
|
1327
|
-
await fs.writeFile(globalProviderPath, globalProvider);
|
|
1328
|
-
|
|
1329
|
-
// package.json deps
|
|
1330
|
-
const pkgPath = path.join(targetDir, 'package.json');
|
|
1331
|
-
const pkg = await fs.readJson(pkgPath);
|
|
1332
|
-
pkg.dependencies = pkg.dependencies ?? {};
|
|
1333
|
-
pkg.devDependencies = pkg.devDependencies ?? {};
|
|
1334
|
-
pkg.dependencies['@sentry/react'] = '^8.45.0';
|
|
1335
|
-
pkg.devDependencies['@sentry/vite-plugin'] = '^2.22.7';
|
|
1336
|
-
pkg.dependencies = sortObjectKeys(pkg.dependencies);
|
|
1337
|
-
pkg.devDependencies = sortObjectKeys(pkg.devDependencies);
|
|
1338
|
-
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
1339
|
-
|
|
1340
|
-
await patchViteConfigForSentry(targetDir);
|
|
1341
|
-
|
|
1342
|
-
// .env.example
|
|
1343
|
-
const envExamplePath = path.join(targetDir, '.env.example');
|
|
1344
|
-
const envBlock = `# Sentry observability (v0.93.0+) — 비워두면 init 자동 skip.
|
|
1345
|
-
VITE_SENTRY_DSN=
|
|
1346
|
-
VITE_APP_VERSION=
|
|
1347
|
-
# Source map upload (vite build 시).
|
|
1348
|
-
SENTRY_AUTH_TOKEN=
|
|
1349
|
-
SENTRY_ORG=
|
|
1350
|
-
SENTRY_PROJECT=
|
|
1351
|
-
`;
|
|
1352
|
-
if (await fs.pathExists(envExamplePath)) {
|
|
1353
|
-
const existing = await fs.readFile(envExamplePath, 'utf-8');
|
|
1354
|
-
if (!existing.includes('VITE_SENTRY_DSN')) {
|
|
1355
|
-
await fs.writeFile(envExamplePath, existing + '\n' + envBlock);
|
|
1356
|
-
}
|
|
1357
|
-
} else {
|
|
1358
|
-
await fs.writeFile(envExamplePath, envBlock);
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
async function patchViteConfigForSentry(targetDir) {
|
|
1363
|
-
const viteCfgPath = path.join(targetDir, 'vite.config.ts');
|
|
1364
|
-
if (!(await fs.pathExists(viteCfgPath))) {
|
|
1365
|
-
throw new Error(`vite.config.ts 가 ${targetDir} 에 없습니다.`);
|
|
1366
|
-
}
|
|
1367
|
-
let cfg = await fs.readFile(viteCfgPath, 'utf-8');
|
|
1368
|
-
|
|
1369
|
-
if (!cfg.includes("@sentry/vite-plugin")) {
|
|
1370
|
-
cfg = cfg.replace(
|
|
1371
|
-
/import { defineConfig } from 'vite';/,
|
|
1372
|
-
`import { defineConfig } from 'vite';\nimport { sentryVitePlugin } from '@sentry/vite-plugin';`,
|
|
1373
|
-
);
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
if (!cfg.includes("sentryVitePlugin(")) {
|
|
1377
|
-
// i18n 의 viteStaticCopy 처럼 plugins 배열 안에 nested `[]` 가 있을 수 있으므로
|
|
1378
|
-
// balanced-bracket helper 로 outer entry append.
|
|
1379
|
-
const sentryCall = `sentryVitePlugin({
|
|
1380
|
-
org: process.env.SENTRY_ORG,
|
|
1381
|
-
project: process.env.SENTRY_PROJECT,
|
|
1382
|
-
authToken: process.env.SENTRY_AUTH_TOKEN,
|
|
1383
|
-
disable: !process.env.SENTRY_AUTH_TOKEN,
|
|
1384
|
-
})`;
|
|
1385
|
-
const patched = appendVitePluginEntry(cfg, sentryCall);
|
|
1386
|
-
if (patched) cfg = patched;
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
if (!cfg.includes("sourcemap:")) {
|
|
1390
|
-
if (cfg.includes("server: {")) {
|
|
1391
|
-
cfg = cfg.replace(/(\n\s*server:\s*\{)/, `\n build: { sourcemap: true },$1`);
|
|
1392
|
-
}
|
|
1393
|
-
// server 가 없는 변종은 build 삽입 위치가 모호 → 보수적으로 skip (사용자가 수동 추가).
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
await fs.writeFile(viteCfgPath, cfg);
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
async function generateMonorepo(targetDir, projectName, plugins, { yes = false, theme, css, arch, themeBase, platform = 'next', i18n = 'none', locales = 'ko,en', observability = 'none', appName: appNameOpt = null, port: portOpt = null } = {}) {
|
|
1205
|
+
async function generateMonorepo(targetDir, projectName, plugins, { yes = false, theme, css, arch, themeBase, platform = 'next', i18n = 'none', locales = 'ko,en', appName: appNameOpt = null, port: portOpt = null } = {}) {
|
|
1400
1206
|
await fs.copy(path.join(TEMPLATES_DIR, 'monorepo'), targetDir);
|
|
1401
1207
|
|
|
1402
1208
|
// Update root package.json
|
|
@@ -1424,10 +1230,6 @@ async function generateMonorepo(targetDir, projectName, plugins, { yes = false,
|
|
|
1424
1230
|
turbo.tasks.build.outputs = ['dist/**'];
|
|
1425
1231
|
// vite 는 클라이언트 노출 env 가 VITE_ 접두사 관례.
|
|
1426
1232
|
turbo.globalEnv = turbo.globalEnv.map((e) => (e === 'API_URL' ? 'VITE_API_URL' : e));
|
|
1427
|
-
// sentry observability 는 플러그인 turboEnvVars 훅을 안 타므로 직접 선언.
|
|
1428
|
-
if (observability === 'sentry') {
|
|
1429
|
-
turbo.globalEnv.push('MODE', 'SENTRY_ORG', 'SENTRY_PROJECT', 'SENTRY_AUTH_TOKEN');
|
|
1430
|
-
}
|
|
1431
1233
|
}
|
|
1432
1234
|
turbo.globalEnv = [...new Set(turbo.globalEnv)];
|
|
1433
1235
|
await fs.writeJson(turboPath, turbo, { spaces: 2 });
|
|
@@ -1448,7 +1250,7 @@ async function generateMonorepo(targetDir, projectName, plugins, { yes = false,
|
|
|
1448
1250
|
|
|
1449
1251
|
const appsDir = path.join(targetDir, 'apps', appName);
|
|
1450
1252
|
if (platform === 'vite') {
|
|
1451
|
-
await generateViteApp(appsDir, appName, port, arch, css, { i18n, locales
|
|
1253
|
+
await generateViteApp(appsDir, appName, port, arch, css, { i18n, locales });
|
|
1452
1254
|
} else {
|
|
1453
1255
|
await generateApp(appsDir, appName, port, plugins, arch, css);
|
|
1454
1256
|
}
|
|
@@ -1549,7 +1351,7 @@ async function generateApp(targetDir, appName, port, plugins, arch, css = 'tailw
|
|
|
1549
1351
|
}
|
|
1550
1352
|
}
|
|
1551
1353
|
|
|
1552
|
-
async function generateViteApp(targetDir, appName, port, arch, css = 'tailwind', { i18n = 'none', locales = 'ko,en'
|
|
1354
|
+
async function generateViteApp(targetDir, appName, port, arch, css = 'tailwind', { i18n = 'none', locales = 'ko,en' } = {}) {
|
|
1553
1355
|
// 베이스 (arch-neutral) + arch 오버레이 — generateApp 과 동일 패턴.
|
|
1554
1356
|
await fs.copy(path.join(TEMPLATES_DIR, 'vite-app'), targetDir, {
|
|
1555
1357
|
filter: (src) => !src.includes(`${path.sep}_arch${path.sep}`) && !src.endsWith(`${path.sep}_arch`),
|
|
@@ -1613,10 +1415,6 @@ async function generateViteApp(targetDir, appName, port, arch, css = 'tailwind',
|
|
|
1613
1415
|
const localesArr = parseLocales(locales);
|
|
1614
1416
|
await emitI18n(targetDir, { arch, locales: localesArr });
|
|
1615
1417
|
}
|
|
1616
|
-
|
|
1617
|
-
if (observability === 'sentry') {
|
|
1618
|
-
await emitSentry(targetDir, { arch, i18nActive: i18n === 'react-i18next' });
|
|
1619
|
-
}
|
|
1620
1418
|
}
|
|
1621
1419
|
|
|
1622
1420
|
/**
|
|
@@ -1812,30 +1610,10 @@ export default function Home() {
|
|
|
1812
1610
|
}
|
|
1813
1611
|
}
|
|
1814
1612
|
|
|
1815
|
-
// 6)
|
|
1816
|
-
// intl + sentry 면 nextIntl 이 [locale]/error.tsx 를 i18n-aware 로 replace 하므로 그것도 변환.
|
|
1817
|
-
const sentryActive = plugins?.some((p) => p.name === 'sentry');
|
|
1818
|
-
if (sentryActive) {
|
|
1819
|
-
const errorCandidates = intlActive
|
|
1820
|
-
? [path.join(targetDir, 'app/[locale]/error.tsx')]
|
|
1821
|
-
: [path.join(targetDir, 'app/error.tsx')];
|
|
1822
|
-
for (const errPath of errorCandidates) {
|
|
1823
|
-
if (!(await fs.pathExists(errPath))) continue;
|
|
1824
|
-
const useI18n = intlActive;
|
|
1825
|
-
await fs.writeFile(errPath, buildErrorTsxNonTailwind({ useI18n, cssFramework, arch }));
|
|
1826
|
-
if (cssFramework === 'css-modules') {
|
|
1827
|
-
await fs.writeFile(
|
|
1828
|
-
path.join(path.dirname(errPath), 'error.module.css'),
|
|
1829
|
-
buildErrorModuleCss(),
|
|
1830
|
-
);
|
|
1831
|
-
}
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
// 7) .prettierrc — tailwind plugin 제거.
|
|
1613
|
+
// 6) .prettierrc — tailwind plugin 제거.
|
|
1836
1614
|
await stripTailwindFromPrettier(path.join(targetDir, '.prettierrc'));
|
|
1837
1615
|
|
|
1838
|
-
//
|
|
1616
|
+
// 7) monorepo 인 경우 root .prettierrc 와 root package.json 도 정리 (root 의 prettier-plugin-tailwindcss).
|
|
1839
1617
|
// applyCssFrameworkVariant 는 apps/web 마다 호출되지만 root 정리는 1회면 충분 — idempotent 라 OK.
|
|
1840
1618
|
if (isMonorepo && !isUiPackage) {
|
|
1841
1619
|
const monorepoRoot = path.resolve(targetDir, '..', '..');
|
|
@@ -1865,365 +1643,6 @@ async function stripTailwindFromPrettier(prettierPath) {
|
|
|
1865
1643
|
await fs.writeJson(prettierPath, c, { spaces: 2 });
|
|
1866
1644
|
}
|
|
1867
1645
|
|
|
1868
|
-
/**
|
|
1869
|
-
* sentry 의 error.tsx 를 plain/cssmodules 로 변환한 콘텐츠 생성.
|
|
1870
|
-
* useI18n=true 면 next-intl 의 useTranslations + Link 사용.
|
|
1871
|
-
*/
|
|
1872
|
-
function buildErrorTsxNonTailwind({ useI18n, cssFramework, arch }) {
|
|
1873
|
-
const i18nImports = useI18n
|
|
1874
|
-
? `import { useTranslations } from 'next-intl';\n`
|
|
1875
|
-
: '';
|
|
1876
|
-
const configAlias = arch ? arch.aliases.config : '@/src/shared/config';
|
|
1877
|
-
const linkImport = useI18n
|
|
1878
|
-
? `import { Link } from '${configAlias}/i18n/navigation';\n`
|
|
1879
|
-
: `import Link from 'next/link';\n`;
|
|
1880
|
-
const tHook = useI18n ? ` const t = useTranslations('error');\n` : '';
|
|
1881
|
-
const titleText = useI18n ? `{t('title')}` : `오류가 발생했습니다`;
|
|
1882
|
-
const descText = useI18n
|
|
1883
|
-
? `{t('description')}`
|
|
1884
|
-
: `예상치 못한 오류가 발생했습니다. 다시 시도해주세요.`;
|
|
1885
|
-
const fallback = useI18n ? `t('unexpectedError')` : `'알 수 없는 오류'`;
|
|
1886
|
-
const tryAgain = useI18n ? `{t('button.tryAgain')}` : `다시 시도`;
|
|
1887
|
-
const goHome = useI18n ? `{t('button.goHome')}` : `홈으로 이동`;
|
|
1888
|
-
|
|
1889
|
-
if (cssFramework === 'css-modules') {
|
|
1890
|
-
return `'use client';
|
|
1891
|
-
|
|
1892
|
-
import * as Sentry from '@sentry/nextjs';
|
|
1893
|
-
import { AlertTriangle, Home, RefreshCw } from 'lucide-react';
|
|
1894
|
-
${i18nImports}${linkImport}import { useEffect } from 'react';
|
|
1895
|
-
|
|
1896
|
-
import styles from './error.module.css';
|
|
1897
|
-
|
|
1898
|
-
export default function Error({
|
|
1899
|
-
error,
|
|
1900
|
-
reset,
|
|
1901
|
-
}: {
|
|
1902
|
-
error: Error & { digest?: string };
|
|
1903
|
-
reset: () => void;
|
|
1904
|
-
}) {
|
|
1905
|
-
${tHook} useEffect(() => {
|
|
1906
|
-
Sentry.captureException(error);
|
|
1907
|
-
}, [error]);
|
|
1908
|
-
|
|
1909
|
-
return (
|
|
1910
|
-
<div className={styles.wrapper}>
|
|
1911
|
-
<div className={styles.card}>
|
|
1912
|
-
<div className={styles.iconRow}>
|
|
1913
|
-
<div className={styles.iconCircle}>
|
|
1914
|
-
<AlertTriangle className={styles.icon} />
|
|
1915
|
-
</div>
|
|
1916
|
-
</div>
|
|
1917
|
-
|
|
1918
|
-
<h2 className={styles.title}>${titleText}</h2>
|
|
1919
|
-
<p className={styles.description}>${descText}</p>
|
|
1920
|
-
|
|
1921
|
-
<div className={styles.errorBox}>
|
|
1922
|
-
<p className={styles.errorText}>{error.message || ${fallback}}</p>
|
|
1923
|
-
</div>
|
|
1924
|
-
|
|
1925
|
-
<div className={styles.actions}>
|
|
1926
|
-
<button onClick={reset} className={styles.primaryButton}>
|
|
1927
|
-
<RefreshCw className={styles.buttonIcon} />
|
|
1928
|
-
${tryAgain}
|
|
1929
|
-
</button>
|
|
1930
|
-
|
|
1931
|
-
<Link href='/' className={styles.secondaryButton}>
|
|
1932
|
-
<Home className={styles.buttonIcon} />
|
|
1933
|
-
${goHome}
|
|
1934
|
-
</Link>
|
|
1935
|
-
</div>
|
|
1936
|
-
|
|
1937
|
-
{process.env.NODE_ENV === 'development' && error.digest && (
|
|
1938
|
-
<div className={styles.digest}>
|
|
1939
|
-
<p className={styles.digestText}>Error ID: {error.digest}</p>
|
|
1940
|
-
</div>
|
|
1941
|
-
)}
|
|
1942
|
-
</div>
|
|
1943
|
-
</div>
|
|
1944
|
-
);
|
|
1945
|
-
}
|
|
1946
|
-
`;
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
// plain — inline style (토큰 var 활용)
|
|
1950
|
-
return `'use client';
|
|
1951
|
-
|
|
1952
|
-
import * as Sentry from '@sentry/nextjs';
|
|
1953
|
-
import { AlertTriangle, Home, RefreshCw } from 'lucide-react';
|
|
1954
|
-
${i18nImports}${linkImport}import { useEffect } from 'react';
|
|
1955
|
-
|
|
1956
|
-
const wrapper: React.CSSProperties = {
|
|
1957
|
-
display: 'flex',
|
|
1958
|
-
minHeight: '100vh',
|
|
1959
|
-
alignItems: 'center',
|
|
1960
|
-
justifyContent: 'center',
|
|
1961
|
-
padding: '0 16px',
|
|
1962
|
-
};
|
|
1963
|
-
const card: React.CSSProperties = {
|
|
1964
|
-
width: '100%',
|
|
1965
|
-
maxWidth: 448,
|
|
1966
|
-
borderRadius: 8,
|
|
1967
|
-
border: '1px solid var(--border)',
|
|
1968
|
-
background: 'var(--background)',
|
|
1969
|
-
padding: 24,
|
|
1970
|
-
boxShadow: 'var(--shadow-lg, 0 8px 24px rgba(0,0,0,0.15))',
|
|
1971
|
-
};
|
|
1972
|
-
|
|
1973
|
-
export default function Error({
|
|
1974
|
-
error,
|
|
1975
|
-
reset,
|
|
1976
|
-
}: {
|
|
1977
|
-
error: Error & { digest?: string };
|
|
1978
|
-
reset: () => void;
|
|
1979
|
-
}) {
|
|
1980
|
-
${tHook} useEffect(() => {
|
|
1981
|
-
Sentry.captureException(error);
|
|
1982
|
-
}, [error]);
|
|
1983
|
-
|
|
1984
|
-
return (
|
|
1985
|
-
<div style={wrapper}>
|
|
1986
|
-
<div style={card}>
|
|
1987
|
-
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 16 }}>
|
|
1988
|
-
<div
|
|
1989
|
-
style={{
|
|
1990
|
-
width: 64,
|
|
1991
|
-
height: 64,
|
|
1992
|
-
borderRadius: '50%',
|
|
1993
|
-
background: 'color-mix(in srgb, var(--danger) 10%, transparent)',
|
|
1994
|
-
display: 'flex',
|
|
1995
|
-
alignItems: 'center',
|
|
1996
|
-
justifyContent: 'center',
|
|
1997
|
-
}}
|
|
1998
|
-
>
|
|
1999
|
-
<AlertTriangle style={{ width: 32, height: 32, color: 'var(--danger)' }} />
|
|
2000
|
-
</div>
|
|
2001
|
-
</div>
|
|
2002
|
-
|
|
2003
|
-
<h2
|
|
2004
|
-
style={{
|
|
2005
|
-
fontSize: 24,
|
|
2006
|
-
fontWeight: 700,
|
|
2007
|
-
textAlign: 'center',
|
|
2008
|
-
color: 'var(--foreground)',
|
|
2009
|
-
margin: '0 0 8px',
|
|
2010
|
-
}}
|
|
2011
|
-
>
|
|
2012
|
-
${titleText}
|
|
2013
|
-
</h2>
|
|
2014
|
-
<p
|
|
2015
|
-
style={{
|
|
2016
|
-
fontSize: 14,
|
|
2017
|
-
color: 'var(--foreground-muted)',
|
|
2018
|
-
textAlign: 'center',
|
|
2019
|
-
margin: '0 0 24px',
|
|
2020
|
-
}}
|
|
2021
|
-
>
|
|
2022
|
-
${descText}
|
|
2023
|
-
</p>
|
|
2024
|
-
|
|
2025
|
-
<div
|
|
2026
|
-
style={{
|
|
2027
|
-
borderRadius: 6,
|
|
2028
|
-
border: '1px solid color-mix(in srgb, var(--danger) 30%, transparent)',
|
|
2029
|
-
background: 'color-mix(in srgb, var(--danger) 5%, transparent)',
|
|
2030
|
-
padding: 12,
|
|
2031
|
-
}}
|
|
2032
|
-
>
|
|
2033
|
-
<p style={{ fontSize: 14, color: 'var(--danger)', margin: 0 }}>
|
|
2034
|
-
{error.message || ${fallback}}
|
|
2035
|
-
</p>
|
|
2036
|
-
</div>
|
|
2037
|
-
|
|
2038
|
-
<div style={{ marginTop: 24, display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
2039
|
-
<button
|
|
2040
|
-
onClick={reset}
|
|
2041
|
-
style={{
|
|
2042
|
-
width: '100%',
|
|
2043
|
-
display: 'flex',
|
|
2044
|
-
alignItems: 'center',
|
|
2045
|
-
justifyContent: 'center',
|
|
2046
|
-
gap: 8,
|
|
2047
|
-
padding: '8px 16px',
|
|
2048
|
-
borderRadius: 6,
|
|
2049
|
-
border: 'none',
|
|
2050
|
-
background: 'var(--primary)',
|
|
2051
|
-
color: 'var(--primary-foreground)',
|
|
2052
|
-
fontSize: 14,
|
|
2053
|
-
fontWeight: 500,
|
|
2054
|
-
cursor: 'pointer',
|
|
2055
|
-
}}
|
|
2056
|
-
>
|
|
2057
|
-
<RefreshCw style={{ width: 16, height: 16 }} />
|
|
2058
|
-
${tryAgain}
|
|
2059
|
-
</button>
|
|
2060
|
-
|
|
2061
|
-
<Link
|
|
2062
|
-
href='/'
|
|
2063
|
-
style={{
|
|
2064
|
-
width: '100%',
|
|
2065
|
-
display: 'flex',
|
|
2066
|
-
alignItems: 'center',
|
|
2067
|
-
justifyContent: 'center',
|
|
2068
|
-
gap: 8,
|
|
2069
|
-
padding: '8px 16px',
|
|
2070
|
-
borderRadius: 6,
|
|
2071
|
-
border: '1px solid var(--border)',
|
|
2072
|
-
color: 'var(--foreground)',
|
|
2073
|
-
fontSize: 14,
|
|
2074
|
-
fontWeight: 500,
|
|
2075
|
-
textDecoration: 'none',
|
|
2076
|
-
}}
|
|
2077
|
-
>
|
|
2078
|
-
<Home style={{ width: 16, height: 16 }} />
|
|
2079
|
-
${goHome}
|
|
2080
|
-
</Link>
|
|
2081
|
-
</div>
|
|
2082
|
-
|
|
2083
|
-
{process.env.NODE_ENV === 'development' && error.digest && (
|
|
2084
|
-
<div
|
|
2085
|
-
style={{
|
|
2086
|
-
marginTop: 16,
|
|
2087
|
-
borderRadius: 6,
|
|
2088
|
-
background: 'var(--background-subtle)',
|
|
2089
|
-
padding: 12,
|
|
2090
|
-
}}
|
|
2091
|
-
>
|
|
2092
|
-
<p style={{ fontSize: 12, color: 'var(--foreground-subtle)', margin: 0 }}>
|
|
2093
|
-
Error ID: {error.digest}
|
|
2094
|
-
</p>
|
|
2095
|
-
</div>
|
|
2096
|
-
)}
|
|
2097
|
-
</div>
|
|
2098
|
-
</div>
|
|
2099
|
-
);
|
|
2100
|
-
}
|
|
2101
|
-
`;
|
|
2102
|
-
}
|
|
2103
|
-
|
|
2104
|
-
function buildErrorModuleCss() {
|
|
2105
|
-
return `.wrapper {
|
|
2106
|
-
display: flex;
|
|
2107
|
-
min-height: 100vh;
|
|
2108
|
-
align-items: center;
|
|
2109
|
-
justify-content: center;
|
|
2110
|
-
padding: 0 16px;
|
|
2111
|
-
}
|
|
2112
|
-
|
|
2113
|
-
.card {
|
|
2114
|
-
width: 100%;
|
|
2115
|
-
max-width: 448px;
|
|
2116
|
-
border-radius: 8px;
|
|
2117
|
-
border: 1px solid var(--border);
|
|
2118
|
-
background: var(--background);
|
|
2119
|
-
padding: 24px;
|
|
2120
|
-
box-shadow: var(--shadow-lg, 0 8px 24px rgba(0, 0, 0, 0.15));
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
|
-
.iconRow {
|
|
2124
|
-
display: flex;
|
|
2125
|
-
justify-content: center;
|
|
2126
|
-
margin-bottom: 16px;
|
|
2127
|
-
}
|
|
2128
|
-
|
|
2129
|
-
.iconCircle {
|
|
2130
|
-
width: 64px;
|
|
2131
|
-
height: 64px;
|
|
2132
|
-
border-radius: 50%;
|
|
2133
|
-
background: color-mix(in srgb, var(--danger) 10%, transparent);
|
|
2134
|
-
display: flex;
|
|
2135
|
-
align-items: center;
|
|
2136
|
-
justify-content: center;
|
|
2137
|
-
}
|
|
2138
|
-
|
|
2139
|
-
.icon {
|
|
2140
|
-
width: 32px;
|
|
2141
|
-
height: 32px;
|
|
2142
|
-
color: var(--danger);
|
|
2143
|
-
}
|
|
2144
|
-
|
|
2145
|
-
.title {
|
|
2146
|
-
font-size: 24px;
|
|
2147
|
-
font-weight: 700;
|
|
2148
|
-
text-align: center;
|
|
2149
|
-
color: var(--foreground);
|
|
2150
|
-
margin: 0 0 8px;
|
|
2151
|
-
}
|
|
2152
|
-
|
|
2153
|
-
.description {
|
|
2154
|
-
font-size: 14px;
|
|
2155
|
-
color: var(--foreground-muted);
|
|
2156
|
-
text-align: center;
|
|
2157
|
-
margin: 0 0 24px;
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
.errorBox {
|
|
2161
|
-
border-radius: 6px;
|
|
2162
|
-
border: 1px solid color-mix(in srgb, var(--danger) 30%, transparent);
|
|
2163
|
-
background: color-mix(in srgb, var(--danger) 5%, transparent);
|
|
2164
|
-
padding: 12px;
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
.errorText {
|
|
2168
|
-
font-size: 14px;
|
|
2169
|
-
color: var(--danger);
|
|
2170
|
-
margin: 0;
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
.actions {
|
|
2174
|
-
margin-top: 24px;
|
|
2175
|
-
display: flex;
|
|
2176
|
-
flex-direction: column;
|
|
2177
|
-
gap: 12px;
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
.primaryButton,
|
|
2181
|
-
.secondaryButton {
|
|
2182
|
-
width: 100%;
|
|
2183
|
-
display: flex;
|
|
2184
|
-
align-items: center;
|
|
2185
|
-
justify-content: center;
|
|
2186
|
-
gap: 8px;
|
|
2187
|
-
padding: 8px 16px;
|
|
2188
|
-
border-radius: 6px;
|
|
2189
|
-
font-size: 14px;
|
|
2190
|
-
font-weight: 500;
|
|
2191
|
-
cursor: pointer;
|
|
2192
|
-
text-decoration: none;
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
|
-
.primaryButton {
|
|
2196
|
-
border: none;
|
|
2197
|
-
background: var(--primary);
|
|
2198
|
-
color: var(--primary-foreground);
|
|
2199
|
-
}
|
|
2200
|
-
|
|
2201
|
-
.secondaryButton {
|
|
2202
|
-
border: 1px solid var(--border);
|
|
2203
|
-
color: var(--foreground);
|
|
2204
|
-
background: transparent;
|
|
2205
|
-
}
|
|
2206
|
-
|
|
2207
|
-
.buttonIcon {
|
|
2208
|
-
width: 16px;
|
|
2209
|
-
height: 16px;
|
|
2210
|
-
}
|
|
2211
|
-
|
|
2212
|
-
.digest {
|
|
2213
|
-
margin-top: 16px;
|
|
2214
|
-
border-radius: 6px;
|
|
2215
|
-
background: var(--background-subtle);
|
|
2216
|
-
padding: 12px;
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
.digestText {
|
|
2220
|
-
font-size: 12px;
|
|
2221
|
-
color: var(--foreground-subtle);
|
|
2222
|
-
margin: 0;
|
|
2223
|
-
}
|
|
2224
|
-
`;
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
1646
|
/**
|
|
2228
1647
|
* 스캐폴드 마무리 — `gitignore` 파일을 `.gitignore` 로 되돌리고 `git init` 실행.
|
|
2229
1648
|
*
|
|
@@ -2380,81 +1799,6 @@ async function writePluginFiles(targetDir, plugins, arch) {
|
|
|
2380
1799
|
}
|
|
2381
1800
|
}
|
|
2382
1801
|
}
|
|
2383
|
-
|
|
2384
|
-
// auth-jwt + next-intl 동시 활성화 시 proxy.ts 병합
|
|
2385
|
-
// (각 플러그인이 단독으로 깐 proxy.ts 를 합친 버전으로 덮어쓴다)
|
|
2386
|
-
// i18n routing import 는 arch.aliases.config 기준 — FSD 면 @/src/shared/config,
|
|
2387
|
-
// flat 이면 @/lib/config 로 해석.
|
|
2388
|
-
const names = new Set(plugins.map((p) => p.name));
|
|
2389
|
-
if (names.has('auth-jwt') && names.has('next-intl')) {
|
|
2390
|
-
const configAlias = arch ? arch.aliases.config : '@/src/shared/config';
|
|
2391
|
-
const mergedProxy = `import createIntlMiddleware from 'next-intl/middleware';
|
|
2392
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
2393
|
-
|
|
2394
|
-
import { routing } from '${configAlias}/i18n/routing';
|
|
2395
|
-
|
|
2396
|
-
const AUTH_ROUTES = ['/sign-in', '/sign-up'];
|
|
2397
|
-
|
|
2398
|
-
/**
|
|
2399
|
-
* 홈(\`/\`, \`/{locale}\`) 진입 시 redirect 할 path. 빈 문자열이면
|
|
2400
|
-
* \`app/[locale]/page.tsx\` 가 그대로 노출. 예: '/dashboard', '/projects'.
|
|
2401
|
-
* 인증 가드 위에서 동작하므로 미인증이면 그대로 \`/sign-in\` 으로 빠진다.
|
|
2402
|
-
*/
|
|
2403
|
-
const HOME_REDIRECT = '';
|
|
2404
|
-
|
|
2405
|
-
const intl = createIntlMiddleware(routing);
|
|
2406
|
-
|
|
2407
|
-
/**
|
|
2408
|
-
* 로케일 prefix (/ko, /en) 를 벗겨 인증 라우트 매칭에 사용한다.
|
|
2409
|
-
* 예: /ko/sign-in → /sign-in
|
|
2410
|
-
*/
|
|
2411
|
-
const stripLocalePrefix = (pathname: string): string => {
|
|
2412
|
-
const locales = routing.locales as readonly string[];
|
|
2413
|
-
const segments = pathname.split('/').filter(Boolean);
|
|
2414
|
-
if (segments[0] && locales.includes(segments[0])) {
|
|
2415
|
-
const rest = segments.slice(1).join('/');
|
|
2416
|
-
return \`/\${rest}\`.replace(/\\/$/, '') || '/';
|
|
2417
|
-
}
|
|
2418
|
-
return pathname;
|
|
2419
|
-
};
|
|
2420
|
-
|
|
2421
|
-
/**
|
|
2422
|
-
* Next 16+ proxy.ts (구 middleware.ts).
|
|
2423
|
-
* next-intl 라우팅 + auth-jwt 토큰 존재 체크 합성 버전.
|
|
2424
|
-
*
|
|
2425
|
-
* - \`/\` + HOME_REDIRECT 설정 → 해당 경로로 리다이렉트 (인증 가드보다 먼저)
|
|
2426
|
-
* - intl 이 로케일 prefix 처리 + NEXT_LOCALE 쿠키 set
|
|
2427
|
-
* - 그 위에 인증 가드 — 토큰 없고 인증 라우트도 아니면 /sign-in 으로 redirect
|
|
2428
|
-
* - dev + \`NEXT_PUBLIC_DEV_AUTH_BYPASS=true\` → 가드 전체 우회 (개발용)
|
|
2429
|
-
* - AT 만료 검사나 refresh 는 하지 않는다 (BFF 가 처리)
|
|
2430
|
-
*/
|
|
2431
|
-
const DEV_BYPASS =
|
|
2432
|
-
process.env.NODE_ENV !== 'production' &&
|
|
2433
|
-
process.env.NEXT_PUBLIC_DEV_AUTH_BYPASS === 'true';
|
|
2434
|
-
|
|
2435
|
-
export default function proxy(req: NextRequest) {
|
|
2436
|
-
const intlRes = intl(req);
|
|
2437
|
-
const pathname = stripLocalePrefix(req.nextUrl.pathname);
|
|
2438
|
-
const hasToken = !!req.cookies.get('accessToken')?.value;
|
|
2439
|
-
const isAuthRoute = AUTH_ROUTES.some((r) => pathname.startsWith(r));
|
|
2440
|
-
|
|
2441
|
-
if (pathname === '/' && HOME_REDIRECT) {
|
|
2442
|
-
return NextResponse.redirect(new URL(HOME_REDIRECT, req.url));
|
|
2443
|
-
}
|
|
2444
|
-
|
|
2445
|
-
if (DEV_BYPASS) return intlRes;
|
|
2446
|
-
if (isAuthRoute) return intlRes;
|
|
2447
|
-
if (!hasToken) return NextResponse.redirect(new URL('/sign-in', req.url));
|
|
2448
|
-
|
|
2449
|
-
return intlRes;
|
|
2450
|
-
}
|
|
2451
|
-
|
|
2452
|
-
export const config = {
|
|
2453
|
-
matcher: '/((?!api|_next|_vercel|monitoring|.*\\\\..*).*)',
|
|
2454
|
-
};
|
|
2455
|
-
`;
|
|
2456
|
-
await fs.writeFile(path.join(targetDir, 'proxy.ts'), mergedProxy);
|
|
2457
|
-
}
|
|
2458
1802
|
}
|
|
2459
1803
|
|
|
2460
1804
|
async function composeProviders(targetDir, plugins, arch) {
|