@zalkera/client 0.23.4 → 0.24.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/bin/validate-storefront.mjs +79 -19
- package/llms.txt +1 -2
- package/package.json +1 -1
|
@@ -594,6 +594,72 @@ let singletonFound = false;
|
|
|
594
594
|
* package.json 을 못 찾거나 파싱 실패하면 none(안전 — S 안 들이댄다).
|
|
595
595
|
* 백엔드는 스택을 모른다 — 선언은 레포 안에 살고 validator 가 현장에서 읽는다.
|
|
596
596
|
*/
|
|
597
|
+
/**
|
|
598
|
+
* **면제 마커 판정을 한 곳에서 만든다.**
|
|
599
|
+
*
|
|
600
|
+
* 이 파일이 판정하는 마커는 **네 계열**이다 — `allow-dynamic`(C1·C1b·C1p·C1pa) ·
|
|
601
|
+
* `allow-inline-style`(S2) · `allow-cross-origin`(X1·X1p) · `allow-custom-theme-inject`(S8).
|
|
602
|
+
* (`allow-preview-write` 라는 다섯째 마커가 있지만 **여기서는 판정하지 않는다** — 판정은
|
|
603
|
+
* `zalkera-storefront-examples` 의 `src/lib/previewGuard.test.ts` 에 살고, 줄머리 앵커에 구 접두를
|
|
604
|
+
* 안 받는 **다른 형태**다. 여기 모았다고 그것까지 모인 것이 아니다.)
|
|
605
|
+
*
|
|
606
|
+
* 그 정규식이 **여덟 자리에 흩어져 있었고 다섯 자리가 사유 없는 맨몸 마커를 면제로 받았다**
|
|
607
|
+
* (S2·X1·X1p·C1p·C1pa). 그중 **셋**(`\s*(.+)` — X1·C1p·C1pa)은 `\s` 가 개행을 넘어 **다음 소스
|
|
608
|
+
* 줄을 사유로 지어내** 사람에게 보여 줬다:
|
|
609
|
+
*
|
|
610
|
+
* // zalkera-allow-cross-origin:
|
|
611
|
+
* import {visitorIp} from "@zalkera/client";
|
|
612
|
+
* → 면제 1건: app/api/checkout/route.ts — import {visitorIp} from "@zalkera/client";
|
|
613
|
+
*
|
|
614
|
+
* 재현: 발행 전 판본을 꺼내(`git show <ref>:bin/validate-storefront.mjs`) **자기 `node_modules` 에
|
|
615
|
+
* 닿는 자리**에 두고, 규칙별 배치에 맞춘 픽스처(X1=변이 라우트 · S2=인라인 style · C1p=`pages/`
|
|
616
|
+
* `getServerSideProps`)에 맨몸 마커를 먹여 `--gate` 출력을 두 판본에서 대조한다.
|
|
617
|
+
*
|
|
618
|
+
* 면제는 곧 그 규칙을 끄는 일이고, 경고문 자체가 「마커를 다세요」라고 가르친다 — 경고를 없애려는
|
|
619
|
+
* 고객·AI 가 마커만 달면 그 라우트의 가드가 사라진다. 그래서 **사유를 요구한다.**
|
|
620
|
+
*
|
|
621
|
+
* ■ 「공백」의 정의를 양쪽에서 같게 쓴다
|
|
622
|
+
* 구분자는 **가로 공백류**다 — NBSP·전각공백을 포함한다. 붙여넣기 경로에서 온 한글 사유가
|
|
623
|
+
* 그 문자로 시작하는 일이 있고, 그것을 거부하면 정당한 마커가 막힌다(과소독도 결함이다).
|
|
624
|
+
* 반대로 사유의 첫 글자는 **보이지 않는 글자여도 안 된다** — `\S` 는 그것들을 통과시켜 ZWSP 한
|
|
625
|
+
* 글자가 「사유」가 됐고, 면제 목록에 **빈 사유**가 찍혔다(실측 9종: ZWSP·ZWNJ·ZWJ·WJ·SHY·
|
|
626
|
+
* CGJ·MVS·ALM·LRM). 그래서 `\p{Cf}` 만이 아니라 `\p{Mn}`·`\p{Me}` 도 배제한다 — **CGJ(U+034F)는
|
|
627
|
+
* `Cf` 가 아니라 `Mn` 이다.** `Cf` 만 막으면 아홉 중 여덟만 막힌다(그렇게 한 판을 냈다가 잡혔다).
|
|
628
|
+
*
|
|
629
|
+
* ⚠ **범주로 완전히 막을 수는 없다.** U+3164 한글 채움 문자(`Lo`)나 U+2800 점자 빈칸(`So`)은
|
|
630
|
+
* 보이지 않으면서 어느 배제 범주에도 안 들어간다. 여기가 막는 것은 「눈에 안 보이는 사유」
|
|
631
|
+
* 전부가 아니라 **결합·서식 문자 계열**이다.
|
|
632
|
+
*
|
|
633
|
+
* ■ `//` 뒤에도 개행을 안 넘는다
|
|
634
|
+
* 종전에는 `//\s*` 라 `//` 와 마커 사이에 개행이 있어도 매치했다 — 템플릿 리터럴이나 블록
|
|
635
|
+
* 주석 안의 `//` 가 다음 줄의 마커를 살렸다. 같은 가로 공백류로 좁힌다.
|
|
636
|
+
*/
|
|
637
|
+
const ALLOW_RULES = new Set(["dynamic", "inline-style", "cross-origin", "custom-theme-inject"]);
|
|
638
|
+
/** 가로 공백류 — 개행을 뺀 공백. NBSP·전각공백을 **받는다**(붙여넣기에서 실제로 나온다). */
|
|
639
|
+
const H_SPACE = "[ \\t\\u00a0\\u1680\\u2000-\\u200a\\u202f\\u205f\\u3000]";
|
|
640
|
+
/** 사유의 첫 글자로 인정하지 않는 보이지 않는 글자 — 서식(`Cf`)·결합(`Mn`·`Me`). */
|
|
641
|
+
const INVISIBLE = "[\\p{Cf}\\p{Mn}\\p{Me}]";
|
|
642
|
+
const ALLOW_MARKERS = new Map();
|
|
643
|
+
function allowMarker(rule) {
|
|
644
|
+
// ⚠ **이름을 검증한다.** 오타는 유효한 정규식을 만들고 **매치만 안 한다** — 면제가 조용히
|
|
645
|
+
// 사라진다. 이 검증이 없던 판에서는 여덟 자리 중 다섯에서 한 글자를 바꿔도 시험이 전건
|
|
646
|
+
// 초록이었다(재현: 자리 하나의 규칙 이름에 글자를 더하고 `npm test` — 그 자리를 덮는 시험이
|
|
647
|
+
// 생기기 전에는 아무것도 안 죽었다). 모르면 던져서 그 자리에서 죽인다.
|
|
648
|
+
if (!ALLOW_RULES.has(rule)) throw new Error(`알 수 없는 면제 규칙: ${rule}`);
|
|
649
|
+
// 한 번만 만든다. 호출 자리 **여덟 중 일곱**이 파일 루프 안이다(S8 만 실행당 1회).
|
|
650
|
+
// V8 은 같은 소스 문자열이면 컴파일을 캐시하므로 이것이 막는 것은 컴파일이 아니라 **파싱·객체
|
|
651
|
+
// 할당**이다. 그 캐시는 GC 로 비워질 수 있어 보험값이 있다.
|
|
652
|
+
let re = ALLOW_MARKERS.get(rule);
|
|
653
|
+
if (re === undefined) {
|
|
654
|
+
re = new RegExp(
|
|
655
|
+
`//${H_SPACE}*(?:zalkera|oneq(?:ue?)?)-allow-${rule}:(?:${H_SPACE}|${INVISIBLE})*(?!\\s|${INVISIBLE})(\\S.*)`,
|
|
656
|
+
"u",
|
|
657
|
+
);
|
|
658
|
+
ALLOW_MARKERS.set(rule, re);
|
|
659
|
+
}
|
|
660
|
+
return re;
|
|
661
|
+
}
|
|
662
|
+
|
|
597
663
|
/**
|
|
598
664
|
* 모르는 `zalkera.styling` 값. 모드는 `none` 이지만 조용히 넘어가지 않는다.
|
|
599
665
|
*
|
|
@@ -2713,9 +2779,7 @@ function checkPageBlastRadius() {
|
|
|
2713
2779
|
reported.add(key);
|
|
2714
2780
|
|
|
2715
2781
|
// 면제는 **범인 파일**에 붙인다(C1b 와 같은 규칙) — page 에 붙이면 그 아래 import 가 한 번에 뚫린다.
|
|
2716
|
-
const allow = (readSafe(file, "C1 면제 마커 판독") ?? "").match(
|
|
2717
|
-
/\/\/\s*(?:zalkera|oneq(?:ue?)?)-allow-dynamic:[ \t]*(\S.*)/,
|
|
2718
|
-
);
|
|
2782
|
+
const allow = (readSafe(file, "C1 면제 마커 판독") ?? "").match(allowMarker("dynamic"));
|
|
2719
2783
|
const own = file === page;
|
|
2720
2784
|
const where = own
|
|
2721
2785
|
? `${relative(process.cwd(), page)}: SEO 라우트가 동적SSR 을 유발한다 → ${why}`
|
|
@@ -2748,9 +2812,7 @@ function checkLayoutBlastRadius() {
|
|
|
2748
2812
|
|
|
2749
2813
|
// 면제는 **범인 파일**에 붙인다 — layout 에 붙이면 그 아래 전부가 한 번에 뚫린다.
|
|
2750
2814
|
// 신 마커 zalkera- + 구 마커(oneq-/oneque-)를 양형 수용한다(리네임 이행기).
|
|
2751
|
-
const allow = (readSafe(file, "C1b 면제 마커 판독") ?? "").match(
|
|
2752
|
-
/\/\/\s*(?:zalkera|oneq(?:ue?)?)-allow-dynamic:[ \t]*(\S.*)/,
|
|
2753
|
-
);
|
|
2815
|
+
const allow = (readSafe(file, "C1b 면제 마커 판독") ?? "").match(allowMarker("dynamic"));
|
|
2754
2816
|
const path = chain.map((f) => relative(process.cwd(), f)).join(" → ");
|
|
2755
2817
|
const detail =
|
|
2756
2818
|
`${relative(process.cwd(), layout)} 이 ${why} 에 도달한다 → ${path}. ` +
|
|
@@ -2829,8 +2891,8 @@ function checkThemeWiring() {
|
|
|
2829
2891
|
// 실효 확인은 스모크 개시(실효층)가 맡는다. 마커는 원문에서 찾는다(주석이 곧 마커다).
|
|
2830
2892
|
// ⚠ **`//` 앵커와 사유를 요구한다.** 앵커가 없으면 문자열 리터럴 안의 같은 글자가 면제로 먹는다 —
|
|
2831
2893
|
// `export const NOTE = "zalkera-allow-custom-theme-inject"` 한 줄로 error 가 warning 이 됐다.
|
|
2832
|
-
//
|
|
2833
|
-
const sink =
|
|
2894
|
+
// 판정은 `allowMarker()` 한 곳에서 만든다 — 자리마다 쓰면 갈린다(맨 위 `allowMarker` 참조).
|
|
2895
|
+
const sink = allowMarker("custom-theme-inject").test(raw) ? warnings : errors;
|
|
2834
2896
|
sink.push(
|
|
2835
2897
|
`[S8] ${relative(process.cwd(), rootLayout)}: 테마 주입 배선이 없습니다 — ` +
|
|
2836
2898
|
`parseThemeColors(...) 호출 + <html style={...}> 가 있어야 콘솔의 색 변경이 화면에 반영됩니다. ` +
|
|
@@ -3250,13 +3312,11 @@ function check(file) {
|
|
|
3250
3312
|
const s2 = styleSink("S2");
|
|
3251
3313
|
// ⚠ 마커는 **원문(`src`)에서 찾는다.** `text` 는 `stripComments(src)` 라 `//` 주석 마커가
|
|
3252
3314
|
// 원리적으로 매치될 수 없었다 — 이 규칙이 안내하는 탈출구가 **한 번도 작동한 적이 없다**
|
|
3253
|
-
// (2026-08-01 첫 사용자가 발견). 형제 규칙 `allow-dynamic`(§S1)은 `readFileSync` 원문을
|
|
3254
|
-
// 처음부터
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
!/\/\/\s*(?:zalkera|oneq(?:ue?)?)-allow-inline-style:/.test(src)
|
|
3259
|
-
) {
|
|
3315
|
+
// (2026-08-01 첫 사용자가 발견). 형제 규칙 `allow-dynamic`(§S1)은 `readFileSync` 원문을 본다 —
|
|
3316
|
+
// **읽는 대상**은 처음부터 옳았다. 다만 그 규칙도 판정 자리가 넷이고 그중 둘(C1p·C1pa)은
|
|
3317
|
+
// **정규식이 느슨했다**(맨몸 마커를 면제로 받고 다음 줄을 사유로 지어냈다). 같은 관례를
|
|
3318
|
+
// 여러 곳이 다르게 구현하고 있었다.
|
|
3319
|
+
if (s2 && /style=\{\{(?!\s*["'`]--)/.test(text) && !allowMarker("inline-style").test(src)) {
|
|
3260
3320
|
s2.push(
|
|
3261
3321
|
`[S2] ${rel}: JSX 인라인 style={{…}} 사용 — 스타일은 Tailwind 유틸리티 클래스로 표현하라. ` +
|
|
3262
3322
|
`정당한 동적 스타일이면 \`// zalkera-allow-inline-style: <이유>\` 마커로 억제하라.`,
|
|
@@ -3815,7 +3875,7 @@ function checkCrossOriginGuards() {
|
|
|
3815
3875
|
// 면제 마커는 **파일 상단**에만 둔다 — 아무 데나 허용하면 주석·문자열 안의 한 줄로
|
|
3816
3876
|
// 조용히 면제되고, 마커를 읽는 사람이 그 사실을 모른다. 첫 `export` 앞까지만 본다.
|
|
3817
3877
|
const head = code.slice(0, code.search(/^export\b/m) + 1 || code.length);
|
|
3818
|
-
const marker = head.match(
|
|
3878
|
+
const marker = head.match(allowMarker("cross-origin"));
|
|
3819
3879
|
if (marker) {
|
|
3820
3880
|
exempted.push(`${rel} — ${marker[1].trim()}`);
|
|
3821
3881
|
continue;
|
|
@@ -4000,7 +4060,7 @@ function checkPagesRouter() {
|
|
|
4000
4060
|
const code = stripLiterals(raw, false); // X1p: 같은 이유
|
|
4001
4061
|
// 파일 상단 마커로 면제. X1 과 같은 마커·같은 "첫 export 앞까지" 규칙.
|
|
4002
4062
|
const head = code.slice(0, code.search(/^export\b/m) + 1 || code.length);
|
|
4003
|
-
if (
|
|
4063
|
+
if (allowMarker("cross-origin").test(raw.slice(0, head.length || raw.length))) continue;
|
|
4004
4064
|
// 비-GET 을 405 로 되돌리는 읽기 전용 라우트는 뺀다. 값 검사라 **원문**에 건다
|
|
4005
4065
|
// (stripLiterals 를 거치면 `"GET"` 이 공백이 된다 — X3 가 그렇게 죽어 있었다).
|
|
4006
4066
|
if (/\breq\.method\s*!==\s*["']GET["']/.test(raw)) continue;
|
|
@@ -4031,7 +4091,7 @@ function checkPagesRouter() {
|
|
|
4031
4091
|
// ── C1pa — `_app`/`_document` 의 getInitialProps: 사이트 전체 정적 최적화 해제.
|
|
4032
4092
|
if (stem === "_app" || stem === "_document") {
|
|
4033
4093
|
if (/\bgetInitialProps\b/.test(text)) {
|
|
4034
|
-
const allow = raw.match(
|
|
4094
|
+
const allow = raw.match(allowMarker("dynamic"));
|
|
4035
4095
|
const detail =
|
|
4036
4096
|
`${rel}: ${stem} 이 getInitialProps 를 씁니다 — **사이트 전 라우트**의 자동 정적 최적화가 ` +
|
|
4037
4097
|
`꺼져 모든 페이지가 매 요청 SSR 이 됩니다(폭발반경이 layout 과 같습니다)`;
|
|
@@ -4053,7 +4113,7 @@ function checkPagesRouter() {
|
|
|
4053
4113
|
/export\s+(?:const|let|var)\s+getServerSideProps\b/.test(text) ||
|
|
4054
4114
|
/export\s*\{[^}]{0,512}\bgetServerSideProps\b/.test(text);
|
|
4055
4115
|
if (!gssp) continue;
|
|
4056
|
-
const allow = raw.match(
|
|
4116
|
+
const allow = raw.match(allowMarker("dynamic"));
|
|
4057
4117
|
const detail = `${rel}: SEO 라우트가 getServerSideProps 로 매 요청 SSR 을 강제합니다`;
|
|
4058
4118
|
if (allow) warnings.push(`[C1p] ${detail} — 예외 허용(zalkera-allow-dynamic: ${allow[1].trim()}).`);
|
|
4059
4119
|
else
|
package/llms.txt
CHANGED
|
@@ -855,8 +855,7 @@ export function merchantReturnPolicyJsonLd(config: SiteConfig, windowDays?: numb
|
|
|
855
855
|
들여왔는지**를 본다 — 같은 이름의 자기 헬퍼는 걸리지 않는다.
|
|
856
856
|
실시간·개인화 데이터(라이브 재고·개인화)는 **클라이언트 컴포넌트(아일랜드)**로 가져오고, 상태 변경
|
|
857
857
|
(장바구니·주문)은 **BFF route handler** 로 한다. 신선도는 **온디맨드 revalidate**(백엔드 데이터 변경 시
|
|
858
|
-
`POST /api/revalidate`)로 지킨다. 동적 SSR 이 꼭 필요하면(예: 검색) **정당화 주석**
|
|
859
|
-
<이유>`)이 필요하다 — 서버 동적 렌더는 **상시 런타임 원가**라 SEO 라우트에서는 기본이 아니다.
|
|
858
|
+
`POST /api/revalidate`)로 지킨다. 동적 SSR 이 꼭 필요하면(예: 검색) **정당화 주석** `// zalkera-allow-dynamic: <이유>` 가 필요하다(마커와 사유는 **같은 줄**이어야 한다) — 서버 동적 렌더는 **상시 런타임 원가**라 SEO 라우트에서는 기본이 아니다.
|
|
860
859
|
|
|
861
860
|
## 5.1 산출물 규범 — 발견되는 사이트 (필수)
|
|
862
861
|
|