@zalkera/client 0.23.0 → 0.23.4
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/bin/validate-storefront.mjs +54 -19
- package/package.json +1 -1
|
@@ -2075,19 +2075,6 @@ function isSeoPageFile(file) {
|
|
|
2075
2075
|
return !dirSegments.some((s) => SEO_EXCLUDE_SEGMENTS.has(s));
|
|
2076
2076
|
}
|
|
2077
2077
|
|
|
2078
|
-
// SEO page 에서 금지되는 per-page SSR 유발 패턴들.
|
|
2079
|
-
const SSR_FORBIDDEN = [
|
|
2080
|
-
{
|
|
2081
|
-
re: /export\s+const\s+dynamic\s*=\s*["']force-dynamic["']/,
|
|
2082
|
-
why: `export const dynamic = "force-dynamic" (전 요청 SSR 강제)`,
|
|
2083
|
-
},
|
|
2084
|
-
{re: /export\s+const\s+revalidate\s*=\s*0\b/, why: `export const revalidate = 0 (ISR 무력화·매 요청 재생성)`},
|
|
2085
|
-
{re: /\bcookies\s*\(/, why: `page 레벨 cookies() 호출 (동적 렌더 opt-in — 세션값은 클라이언트 컴포넌트로)`},
|
|
2086
|
-
{re: /\bheaders\s*\(/, why: `page 레벨 headers() 호출 (동적 렌더 opt-in)`},
|
|
2087
|
-
{re: /cache\s*:\s*["']no-store["']/, why: `fetch(..., { cache: "no-store" }) (캐시 불가·매 요청 fetch)`},
|
|
2088
|
-
{re: /next\s*:\s*\{[^}]*\brevalidate\s*:\s*0\b/, why: `fetch(..., { next: { revalidate: 0 } }) (ISR 무력화)`},
|
|
2089
|
-
];
|
|
2090
|
-
|
|
2091
2078
|
const WALK_MAX_DEPTH = 64;
|
|
2092
2079
|
|
|
2093
2080
|
/**
|
|
@@ -2501,7 +2488,22 @@ function nextDynamicCalls(raw, text, code, where) {
|
|
|
2501
2488
|
/** 이름 축을 안 태우는 나머지 — 형태가 곧 Next 계약이라 출처를 물을 것이 없다. */
|
|
2502
2489
|
const DYNAMIC_API = [
|
|
2503
2490
|
{re: /export\s+const\s+revalidate\s*=\s*0\b/, why: "export const revalidate = 0", corpus: "code"},
|
|
2504
|
-
|
|
2491
|
+
// ⚠ **상한을 건다.** `[^}]*` 가 닫는 `}` 를 못 찾으면 시작 오프셋마다 끝까지 훑고 되돌아온다 —
|
|
2492
|
+
// `page.tsx` 에 `next:{` 를 반복한 입력에서 관문 관통 400KB **13,863ms → 250ms**
|
|
2493
|
+
// (빈 트리 기준선 190ms · 3회 · `repo()`/`timeGate()` 하네스).
|
|
2494
|
+
// ⚠ **이 형상은 어느 크기에서도 검사를 마치지 않는다**(이 자리와 무관한 선재 결함이고 네
|
|
2495
|
+
// 판본 전부에서 같다). `next:{` 1,222회(≈7.2KB)부터 TypeScript 파서가 스택을 넘겨 rc=2 이고,
|
|
2496
|
+
// 그 아래는 rc=0 이 아니라 **rc=7**(파스 실패 — 「읽지 못한 자리」)이다. 위 수치는 「검사를
|
|
2497
|
+
// 마친 시간」이 아니라 거기까지 간 시간이다. 상한이 지운 것은 그중 정규식 몫이고, 파스
|
|
2498
|
+
// 예산은 아직 없다.
|
|
2499
|
+
// 상한은 **문법이 낼 수 있는 현실적 길이**에서 골랐다 — 검사 대상(스토어프론트 트리)에는 이
|
|
2500
|
+
// 블록이 **0건**이다(재현: `grep -rE "next[[:space:]]*:[[:space:]]*\{" <스토어프론트 src>`).
|
|
2501
|
+
// `stripCommentsAndStrings` 가 문자열을 지우므로 `tags: ["a","b",…]` 는 길어지지 않는다 —
|
|
2502
|
+
// 붕괴 후 길이라 문자열 태그는 수십 개가 들어가고, 예산을 먹는 것은 **식별자 태그**다.
|
|
2503
|
+
// 몇 개부터 놓치는지는 이름 길이와 **구분자 표기**에 달렸다(`, ` 기준 9자면 23개·10자면
|
|
2504
|
+
// 21개부터, `,` 만 쓰면 25개·23개부터) — 숫자 하나로
|
|
2505
|
+
// 적을 수 없어 조건을 적는다.
|
|
2506
|
+
{re: /next\s*:\s*\{[^}]{0,256}\brevalidate\s*:\s*0\b/, why: "fetch(..., {next: {revalidate: 0}})", corpus: "code"},
|
|
2505
2507
|
{re: /cache\s*:\s*["']no-store["']/, why: `fetch(..., {cache: "no-store"})`, corpus: "text"},
|
|
2506
2508
|
{
|
|
2507
2509
|
re: /export\s+const\s+dynamic\s*=\s*["']force-dynamic["']/,
|
|
@@ -3640,7 +3642,14 @@ function findMutationHandlers(code) {
|
|
|
3640
3642
|
}
|
|
3641
3643
|
|
|
3642
3644
|
// ⒞ `export {POST}` · `export {handler as POST}` — 본문을 못 따라간다.
|
|
3643
|
-
|
|
3645
|
+
// ⚠ **상한을 건다.** 닫는 `}` 가 없으면 2차다 — `route.ts` 에 `export{` 를 반복한 입력에서
|
|
3646
|
+
// 관문 관통 400KB **14,044ms → 375ms**(빈 트리 기준선 190ms · 3회 · `repo()`/`timeGate()`).
|
|
3647
|
+
//
|
|
3648
|
+
// 상한은 **이 규칙이 보는 자리**에서 골랐다 — 라우트 핸들러의 export 목록이고, 검사 대상
|
|
3649
|
+
// 트리에서 최대 **2자**다(`export {cn};` 꼴. 재현:
|
|
3650
|
+
// `grep -rE "export[[:space:]]*\{" <스토어프론트 src>`). 512 는 배럴 파일(수십 개 재export)
|
|
3651
|
+
// 까지 덮는 값이고, 라우트 핸들러가 그만큼 길어질 일은 없다.
|
|
3652
|
+
for (const m of code.matchAll(/export\s*\{([^}]{0,512})\}/g)) {
|
|
3644
3653
|
for (const part of m[1].split(",")) {
|
|
3645
3654
|
const name = part
|
|
3646
3655
|
.trim()
|
|
@@ -3706,7 +3715,15 @@ function judgeGuardPlacement(rawBody) {
|
|
|
3706
3715
|
}
|
|
3707
3716
|
|
|
3708
3717
|
// 가드가 어디에도 없다 — 다른 갈래들과 사유가 다르다(고치는 방법이 다르다).
|
|
3709
|
-
|
|
3718
|
+
// ⚠ **여기서는 `CALL` 을 쓰지 않는다.** 이 줄은 「가드가 본문 어딘가에 있나」를 보는 존재
|
|
3719
|
+
// 확인이고, `(?:X)?Y` 의 부울값은 `Y` 의 부울값과 같다 — 그런데 `CALL` 앞의 `\w+` 가 긴 낱말
|
|
3720
|
+
// 런을 먹고 `.` 를 못 찾아 되돌아온다. 관문 관통 실측(`POST` 본문이 낱말 런 200KB):
|
|
3721
|
+
// **32,295ms → 247ms**(빈 트리 기준선 190ms · 3회 · `repo()`/`timeGate()`).
|
|
3722
|
+
// 오탐 통제군 — 같은 크기 평범한 본문은 양쪽 다 237ms 다(방아쇠가 크기가 아니라 낱말 런이다).
|
|
3723
|
+
//
|
|
3724
|
+
// `CALL` 자체는 아래 `^` 앵커 자리들에서 **네임스페이스 접두를 실제로 소비해야** 하므로
|
|
3725
|
+
// 상수를 바꾸지 않는다. 존재만 보는 이 한 줄만 접두 없는 형태로 본다.
|
|
3726
|
+
if (!/assertSameOrigin\s*\(/.test(body)) {
|
|
3710
3727
|
return (
|
|
3711
3728
|
`가 변이 메서드인데 assertSameOrigin 호출이 없습니다 — 교차사이트 위조가 열립니다.` +
|
|
3712
3729
|
` 정당한 예외면 파일 상단에 \`// zalkera-allow-cross-origin: 이유\` 를 다세요.`
|
|
@@ -3987,7 +4004,14 @@ function checkPagesRouter() {
|
|
|
3987
4004
|
// 비-GET 을 405 로 되돌리는 읽기 전용 라우트는 뺀다. 값 검사라 **원문**에 건다
|
|
3988
4005
|
// (stripLiterals 를 거치면 `"GET"` 이 공백이 된다 — X3 가 그렇게 죽어 있었다).
|
|
3989
4006
|
if (/\breq\.method\s*!==\s*["']GET["']/.test(raw)) continue;
|
|
3990
|
-
|
|
4007
|
+
// ⚠ **선택 그룹을 두지 않는다.** 여기는 존재만 보는 `.test()` 이고 `(?:X)?Y` 의 부울값은
|
|
4008
|
+
// `Y` 의 부울값과 같다 — 그런데 `\w+` 가 긴 낱말 런을 먹고 `.` 를 못 찾아 되돌아온다.
|
|
4009
|
+
// 관문 관통 실측(`pages/api` 200KB): `assertSameOrigin` 반복 **26,254ms → 251ms**
|
|
4010
|
+
// (빈 트리 기준선 190ms · 3회 · `repo()`/`timeGate()`).
|
|
4011
|
+
// ⚠ **방아쇠는 그 이름이 아니라 긴 낱말 런이다** — 가드 이름이 0건인 무해한 식별자
|
|
4012
|
+
// 반복도 26,383ms → 254ms 다(번들·미니파이 산출물이 `pages/api` 에 들어가기만 해도 터진다).
|
|
4013
|
+
// 오탐 통제군(평범한 줄이 반복되는 같은 크기 파일)은 양쪽 다 236ms.
|
|
4014
|
+
if (!/assertSameOrigin\s*\(/.test(code)) {
|
|
3991
4015
|
crossOriginSink().push(
|
|
3992
4016
|
`[X1p] ${rel}: pages/api 핸들러에 assertSameOrigin 호출이 없습니다 — 교차사이트 위조가 ` +
|
|
3993
4017
|
`열립니다. 이 형태(default export + req.method 분기)에서는 검사기가 **가드의 ` +
|
|
@@ -4027,7 +4051,7 @@ function checkPagesRouter() {
|
|
|
4027
4051
|
const gssp =
|
|
4028
4052
|
/export\s+(?:async\s+)?function\s+getServerSideProps\b/.test(text) ||
|
|
4029
4053
|
/export\s+(?:const|let|var)\s+getServerSideProps\b/.test(text) ||
|
|
4030
|
-
/export\s*\{[^}]
|
|
4054
|
+
/export\s*\{[^}]{0,512}\bgetServerSideProps\b/.test(text);
|
|
4031
4055
|
if (!gssp) continue;
|
|
4032
4056
|
const allow = raw.match(/\/\/\s*(?:zalkera|oneq(?:ue?)?)-allow-dynamic:\s*(.+)/);
|
|
4033
4057
|
const detail = `${rel}: SEO 라우트가 getServerSideProps 로 매 요청 SSR 을 강제합니다`;
|
|
@@ -4106,7 +4130,18 @@ function isStaticallyReadableConfig(text) {
|
|
|
4106
4130
|
if (/export\s+default\s+[A-Za-z_$][\w$]*\s*\(/.test(text)) return false;
|
|
4107
4131
|
if (/module\.exports\s*=\s*[A-Za-z_$][\w$]*\s*\(/.test(text)) return false;
|
|
4108
4132
|
// `import type {NextConfig} from "next"` 은 값을 안 나른다 — 그것만 허용한다.
|
|
4109
|
-
|
|
4133
|
+
// ⚠ **줄머리 공백에서 줄바꿈류만 뺀다.** `^\s*` 의 `\s` 는 개행을 포함하고 `m` 플래그가 줄마다
|
|
4134
|
+
// `^` 를 세우므로, 개행만 있는 입력에서 줄머리마다 파일 끝까지 훑고 되돌아온다. 관문 관통 실측
|
|
4135
|
+
// (`next.config.mjs` 를 개행으로 채움 200KB): **26,184ms → 190ms**
|
|
4136
|
+
// (빈 트리 기준선 190ms · 3회 · `repo()`/`timeGate()` — 즉 기준선으로 되돌아왔다).
|
|
4137
|
+
// 오탐 통제군: 같은 크기 공백만 192→190ms · 정상 config 190→188ms(둘 다 이 형상에 안 걸린다).
|
|
4138
|
+
//
|
|
4139
|
+
// ⚠ **`[ \t]` 로 좁히면 안 된다.** JS `\s` 는 25자이고 그중 19자(`\v`·`\f`·NBSP·전각공백·
|
|
4140
|
+
// **BOM** 등)가 함께 빠진다. 그 하나가 첫 import 줄머리에 있으면 값 import 를 못 보고
|
|
4141
|
+
// `true` 를 돌려주어 **O1 거짓 경고**가 난다(실측: BOM·NBSP·전각공백·수직탭 각 1건, 그 문자만
|
|
4142
|
+
// 빼면 0건). UTF-8 BOM 은 Windows 도구가 그대로 내고 zip 업로드가 이 제품의 정본 입구다.
|
|
4143
|
+
// 그래서 빼는 것은 **줄바꿈류 넷**뿐이다 — 그것이 이 자리에서 말한 「가로 공백」이다.
|
|
4144
|
+
for (const m of text.matchAll(/^[^\S\n\r\u2028\u2029]*import\s+([^;\n]*)from\s/gm)) {
|
|
4110
4145
|
if (!/^\s*type\b/.test(m[1])) return false;
|
|
4111
4146
|
}
|
|
4112
4147
|
return true;
|