@zalkera/client 0.11.0 → 0.13.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 +5 -2
- package/bin/check-aeo-surfaces.mjs +706 -0
- package/contracts/aeo-surface-guarantees.json +4 -4
- package/dist/index.cjs +27 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -5
- package/dist/index.d.ts +57 -5
- package/dist/index.js +24 -2
- package/dist/index.js.map +1 -1
- package/lib/site-crawl.mjs +227 -0
- package/llms.txt +27 -5
- package/package.json +57 -50
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 개시된 사이트를 훑는 최소 크롤러 — `snapshot-preview.mjs`(memo116 §3)와
|
|
3
|
+
* `check-aeo-surfaces.mjs`(memo119 T5)가 **같은 사본**을 쓴다.
|
|
4
|
+
*
|
|
5
|
+
* 원래 이 코드는 snapshot-preview 안에 살았다. 보장 검사기가 "산출물을 크롤해 판정한다"(memo119 오너
|
|
6
|
+
* 전제 C)를 구현하려면 같은 훑기가 필요한데, 복사하면 두 크롤러가 조용히 갈라진다 — 한쪽이 라우트 분류를
|
|
7
|
+
* 고치고 다른 쪽이 안 고치면 "미리보기에는 나오는데 검사기는 못 보는 페이지"가 생긴다. 그래서 옮겼다.
|
|
8
|
+
*
|
|
9
|
+
* ── 외부 의존 0 ────────────────────────────────────────────────────────────
|
|
10
|
+
* 이 레포의 팩 스크립트가 zip 을 손으로 쓰는 것과 같은 규율이다. HTML 은 정규식 태그 스캐너로,
|
|
11
|
+
* 다운로드는 Node 내장 `fetch` 로 처리한다. 상대하는 마크업이 **우리가 만든 것**이라(임의 웹이 아니다)
|
|
12
|
+
* 파서를 들일 만큼의 다양성이 없다.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ── HTML 태그 스캐너 ─────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export const TAG_RE = /<([a-zA-Z][-\w:]*)((?:\s+[-\w:@.]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*)\s*(\/?)>/g;
|
|
18
|
+
const ATTR_RE = /([-\w:@.]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
19
|
+
export const DROP = Symbol("drop-element");
|
|
20
|
+
|
|
21
|
+
export function parseAttrs(text) {
|
|
22
|
+
const attrs = [];
|
|
23
|
+
if (!text) return attrs;
|
|
24
|
+
ATTR_RE.lastIndex = 0;
|
|
25
|
+
let m;
|
|
26
|
+
while ((m = ATTR_RE.exec(text)) !== null) {
|
|
27
|
+
attrs.push([m[1], m[2] ?? m[3] ?? m[4] ?? null]);
|
|
28
|
+
}
|
|
29
|
+
return attrs;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const attrOf = (attrs, name) => {
|
|
33
|
+
const hit = attrs.find(([k]) => k.toLowerCase() === name);
|
|
34
|
+
return hit ? hit[1] : undefined;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* `parseAttrs` 는 원문 조각을 **이스케이프된 그대로** 돌려준다. 여기서 `&` 를 다시 인코딩하면
|
|
39
|
+
* `&` → `&amp;` 가 되어 화면에 `&` 가 그대로 보인다(심의 실측 — alt 텍스트가 깨졌다).
|
|
40
|
+
* 그래서 값은 손대지 않고, 따옴표만 원문에 없던 경우에 대비해 감싼다.
|
|
41
|
+
*/
|
|
42
|
+
export function serializeAttrs(attrs) {
|
|
43
|
+
return attrs.map(([k, v]) => (v === null ? ` ${k}` : ` ${k}="${String(v).replace(/"/g, """)}"`)).join("");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 태그 단위 변환. `transform` 이 [DROP] 을 돌려주면 그 **여는 태그만** 지운다 — 대상은 `<meta>`·`<link>`
|
|
48
|
+
* 같은 void 요소뿐이라 내용은 애초에 없다.
|
|
49
|
+
*/
|
|
50
|
+
export function rewriteTags(html, transform) {
|
|
51
|
+
return html.replace(TAG_RE, (full, name, attrText, selfClose) => {
|
|
52
|
+
const attrs = parseAttrs(attrText);
|
|
53
|
+
const out = transform(name.toLowerCase(), attrs);
|
|
54
|
+
if (out === DROP) return "";
|
|
55
|
+
if (!out) return full;
|
|
56
|
+
return `<${name}${serializeAttrs(out)}${selfClose ? " /" : ""}>`;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** HTML 엔티티만 푼다 — 속성값에서 URL 을 꺼낼 때 필요한 최소셋. */
|
|
61
|
+
export const unescapeAttr = (v) =>
|
|
62
|
+
v.replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
63
|
+
|
|
64
|
+
// ── 라우트 정책 ──────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
export const segmentsOf = (path) => path.split("/").filter(Boolean);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 정적으로 성립할 수 없는 기능 라우트. 세션·쓰기·결제다.
|
|
70
|
+
* validator 의 `SEO_EXCLUDE_SEGMENTS`(ISR 게이트 제외 목록)와 같은 어휘를 쓴다 — 그쪽이
|
|
71
|
+
* "정당하게 동적인 인터랙티브 경로"로 이미 판정해 둔 집합이고, 둘이 갈리면 한쪽이 틀린 것이다.
|
|
72
|
+
*/
|
|
73
|
+
export const FUNCTIONAL_SEGMENTS = new Set([
|
|
74
|
+
"api", "cart", "checkout", "mypage", "account", "login", "auth", "orders", "payment",
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 테넌트 **식별정보 표시면**. 미리보기 스냅샷은 이걸 크롤하지 않는다(가릴 것만 많다).
|
|
79
|
+
* 보장 검사기는 다르다 — 아무것도 공개하지 않고 판정만 하므로 여기를 훑어도 유출 표면이 없다.
|
|
80
|
+
* 그래서 이 집합은 상수가 아니라 **호출자가 고르는 값**이다.
|
|
81
|
+
*/
|
|
82
|
+
export const IDENTITY_ROUTES = new Set(["/policies", "/contact"]);
|
|
83
|
+
|
|
84
|
+
/** 페이지가 아닌 기계용 파일. */
|
|
85
|
+
export const MACHINE_FILES = new Set(["/robots.txt", "/sitemap.xml", "/favicon.ico"]);
|
|
86
|
+
|
|
87
|
+
/** 이 확장자는 페이지가 아니라 자산이다(경로만 보고 판정할 때). */
|
|
88
|
+
export const ASSET_EXT = new Set([
|
|
89
|
+
".css", ".js", ".mjs", ".map", ".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".avif",
|
|
90
|
+
".ico", ".woff", ".woff2", ".ttf", ".otf", ".mp4", ".webm", ".json", ".xml", ".txt",
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* URL 하나를 분류하는 함수를 만든다. `origin` 은 크롤 대상 오리진, `skipRoutes` 는 페이지지만
|
|
95
|
+
* 훑지 않을 경로 집합(미리보기=식별정보 라우트, 검사기=빈 집합).
|
|
96
|
+
*/
|
|
97
|
+
export function makeClassifier({origin, skipRoutes = IDENTITY_ROUTES}) {
|
|
98
|
+
return function classify(rawUrl, fromPath) {
|
|
99
|
+
let u;
|
|
100
|
+
try {
|
|
101
|
+
u = new URL(unescapeAttr(rawUrl), origin + fromPath);
|
|
102
|
+
} catch {
|
|
103
|
+
return {kind: "opaque"};
|
|
104
|
+
}
|
|
105
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") return {kind: "opaque"}; // mailto:·tel:·data:
|
|
106
|
+
if (u.origin !== origin) return {kind: "external", url: u};
|
|
107
|
+
const path = u.pathname;
|
|
108
|
+
if (path.startsWith("/_next/") || path.startsWith("/media/")) return {kind: "asset", url: u};
|
|
109
|
+
if (ASSET_EXT.has(extnameOf(path))) return {kind: "asset", url: u};
|
|
110
|
+
if (MACHINE_FILES.has(path)) return {kind: "blocked", url: u};
|
|
111
|
+
const segs = segmentsOf(path);
|
|
112
|
+
if (segs.some((s) => FUNCTIONAL_SEGMENTS.has(s))) return {kind: "blocked", url: u};
|
|
113
|
+
if (skipRoutes.has(path.replace(/\/+$/, "") || "/")) return {kind: "blocked", url: u};
|
|
114
|
+
return {kind: "page", path: path.replace(/\/+$/, "") || "/"};
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** `node:path` 의 extname 과 같되 소문자로. 경로 문자열만 상대하므로 의존을 안 들인다. */
|
|
119
|
+
function extnameOf(path) {
|
|
120
|
+
const base = path.slice(path.lastIndexOf("/") + 1);
|
|
121
|
+
const dot = base.lastIndexOf(".");
|
|
122
|
+
return dot <= 0 ? "" : base.slice(dot).toLowerCase();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── 링크 수집 ────────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
const splitSrcset = (v) => (v ? v.split(",").map((p) => p.trim().split(/\s+/)[0]).filter(Boolean) : []);
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 페이지 HTML 에서 나가는 링크·자산을 수집한다(변형 없음 — 1차 통과).
|
|
131
|
+
* `sink` 는 blocked·external 을 받는 선택 콜백(미리보기 리포트가 쓴다).
|
|
132
|
+
*/
|
|
133
|
+
export function collectLinks(html, fromPath, classify, sink = {}) {
|
|
134
|
+
const links = new Set();
|
|
135
|
+
const assetUrls = new Set();
|
|
136
|
+
const absOrNull = (raw) => {
|
|
137
|
+
if (!raw) return null;
|
|
138
|
+
const c = classify(raw, fromPath);
|
|
139
|
+
return c.kind === "asset" ? c.url.toString() : null;
|
|
140
|
+
};
|
|
141
|
+
rewriteTags(html, (tag, attrs) => {
|
|
142
|
+
const push = (raw) => {
|
|
143
|
+
if (!raw) return;
|
|
144
|
+
const c = classify(raw, fromPath);
|
|
145
|
+
if (c.kind === "page") links.add(c.path);
|
|
146
|
+
else if (c.kind === "asset") assetUrls.add(c.url.toString());
|
|
147
|
+
else if (c.kind === "blocked") sink.blocked?.(c.url.pathname);
|
|
148
|
+
else if (c.kind === "external") sink.external?.(c.url.origin);
|
|
149
|
+
};
|
|
150
|
+
if (tag === "a") push(attrOf(attrs, "href"));
|
|
151
|
+
if (tag === "link") {
|
|
152
|
+
const rel = (attrOf(attrs, "rel") ?? "").toLowerCase();
|
|
153
|
+
if (rel.includes("stylesheet") || rel.includes("icon")) assetUrls.add(absOrNull(attrOf(attrs, "href")));
|
|
154
|
+
}
|
|
155
|
+
if (tag === "img" || tag === "source" || tag === "video" || tag === "audio") {
|
|
156
|
+
const src = absOrNull(attrOf(attrs, "src"));
|
|
157
|
+
if (src) assetUrls.add(src);
|
|
158
|
+
for (const s of splitSrcset(attrOf(attrs, "srcset"))) {
|
|
159
|
+
const abs = absOrNull(s);
|
|
160
|
+
if (abs) assetUrls.add(abs);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
});
|
|
165
|
+
assetUrls.delete(null);
|
|
166
|
+
return {links, assetUrls};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── 크롤 ─────────────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 링크를 따라 페이지 HTML 을 모은다. **JS 를 실행하지 않는다** — 그것이 요점이다: 여기서 보이는
|
|
173
|
+
* 것이 크롤러·답변 엔진이 실제로 받는 것이고, 하이드레이션 뒤에만 나타나는 내용은 보장이 아니다.
|
|
174
|
+
*
|
|
175
|
+
* 반환: `{pages: Map<routePath, html>, assetUrls: Set, fetchFailures: string[], blocked: Set, external: Set}`
|
|
176
|
+
*/
|
|
177
|
+
export async function crawlPages({
|
|
178
|
+
origin,
|
|
179
|
+
entryPath = "/",
|
|
180
|
+
extraRoutes = [],
|
|
181
|
+
maxPages = 60,
|
|
182
|
+
classify,
|
|
183
|
+
userAgent = "zalkera-crawler",
|
|
184
|
+
onPage = null,
|
|
185
|
+
}) {
|
|
186
|
+
const pages = new Map();
|
|
187
|
+
const assetUrls = new Set();
|
|
188
|
+
const fetchFailures = [];
|
|
189
|
+
const blocked = new Set();
|
|
190
|
+
const external = new Set();
|
|
191
|
+
const sink = {blocked: (p) => blocked.add(p), external: (o) => external.add(o)};
|
|
192
|
+
|
|
193
|
+
const queue = [entryPath, ...extraRoutes.map((r) => (r.startsWith("/") ? r : `/${r}`))];
|
|
194
|
+
const seen = new Set();
|
|
195
|
+
|
|
196
|
+
while (queue.length > 0 && pages.size < maxPages) {
|
|
197
|
+
const path = queue.shift();
|
|
198
|
+
if (seen.has(path)) continue;
|
|
199
|
+
seen.add(path);
|
|
200
|
+
|
|
201
|
+
const url = origin + (path === "/" ? "/" : path);
|
|
202
|
+
let res;
|
|
203
|
+
try {
|
|
204
|
+
res = await fetch(url, {headers: {"user-agent": userAgent}});
|
|
205
|
+
} catch (e) {
|
|
206
|
+
fetchFailures.push(`${url} — ${e.message}`);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (!res.ok) {
|
|
210
|
+
fetchFailures.push(`${url} — HTTP ${res.status}`);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (!(res.headers.get("content-type") ?? "").includes("html")) {
|
|
214
|
+
fetchFailures.push(`${url} — HTML 이 아닙니다(${res.headers.get("content-type")})`);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const html = await res.text();
|
|
218
|
+
pages.set(path, html);
|
|
219
|
+
onPage?.(path);
|
|
220
|
+
|
|
221
|
+
const collected = collectLinks(html, path, classify, sink);
|
|
222
|
+
for (const l of collected.links) if (!seen.has(l)) queue.push(l);
|
|
223
|
+
for (const a of collected.assetUrls) assetUrls.add(a);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {pages, assetUrls, fetchFailures, blocked, external};
|
|
227
|
+
}
|
package/llms.txt
CHANGED
|
@@ -137,6 +137,8 @@ await zalkera.submitInquiry(input, { clientIp: ip });
|
|
|
137
137
|
|
|
138
138
|
- `safeLinkUrl(raw)` → `string` — **모든 `href` 는 이걸 태운다.** 콘솔 입력이라 `javascript:` 가 들어올 수 있다(저장형 XSS).
|
|
139
139
|
- `parseConfig<T>(config)` → `T | null` — 섹션 `config` JSON 파싱. **절대 throw 하지 않는다**(실패 시 null).
|
|
140
|
+
- `readConfig<T>(config)` → `T | null` — 같은 계약인데 **문자열이든 객체든** 받는다. 섹션 config 의 거처가 둘이라
|
|
141
|
+
(백엔드 `page_section.config` = JSON 문자열 · 레포 콘텐츠 파일 = 객체) 소비자를 두 벌로 만들지 않으려는 입구다.
|
|
140
142
|
- `asString(v)` / `asId(v)` — 형이 안 맞으면 `undefined`.
|
|
141
143
|
- `asIdArray(v)` / `asObjectArray(v)` — **빈 배열**로 강하한다(undefined 아님).
|
|
142
144
|
빈 배열은 truthy 라 `if (items)` 로 결측을 판별하면 안 된다 — `items.length` 을 봐라.
|
|
@@ -144,6 +146,10 @@ await zalkera.submitInquiry(input, { clientIp: ip });
|
|
|
144
146
|
- `mediaSrc(assetId)` → `/media/{id}` **경로 문자열**. 이미지는 전부 이걸 통한다.
|
|
145
147
|
전제: 프로젝트에 `app/media/[id]/route.ts` 프록시 라우트가 있어야 한다(템플릿에 있다).
|
|
146
148
|
이 헬퍼는 경로만 만든다 — 라우트를 안 만들면 이미지가 404 다.
|
|
149
|
+
- `asHandle(v)` / `asHandleArray(v)` / `assetPath(v)` — **참조 방언**(계약 `contractRev` 4 의 `dialects`)을 읽는다.
|
|
150
|
+
숫자 id 를 못 쓰는 자리에서 상품은 `handle`(= `ProductSummary.slug`), 에셋은 레포 `public/` **루트 절대 경로**로
|
|
151
|
+
가리킨다. `assetPath` 는 루트 절대 경로만 통과시킨다(스킴·`//`·`..`·역슬래시 → `undefined`).
|
|
152
|
+
`asHandleArray` 는 **배열 순서를 보존**한다 — 그 순서가 노출 순서의 원장이다.
|
|
147
153
|
- `SECTION_CONTRACT` / `SECTION_CONTRACT_REV` / `sectionsOfVertical(vertical)` — 아는 섹션 어휘의 코드 표현(§9).
|
|
148
154
|
타입·키를 **지어내지 말고** 여기서 확인한다.
|
|
149
155
|
- `parseThemeColors(raw)` → `ParsedTheme` — 테마 토큰 파서(§8). 화이트리스트라 임의 CSS 가 안 들어온다.
|
|
@@ -191,6 +197,20 @@ export default async function Products() {
|
|
|
191
197
|
}
|
|
192
198
|
```
|
|
193
199
|
|
|
200
|
+
### 4.1-a 카테고리 페이지 (RSC)
|
|
201
|
+
```tsx
|
|
202
|
+
// app/c/[slug]/page.tsx
|
|
203
|
+
const categories = await zalkera.listProductCategories();
|
|
204
|
+
const category = categories.find(c => c.slug === slug);
|
|
205
|
+
if (!category) notFound(); // 카테고리 부재만 404 다
|
|
206
|
+
const page = await zalkera.listProducts({ categoryId: category.id, size: 24 });
|
|
207
|
+
```
|
|
208
|
+
- **좁히는 축은 `categoryId`(숫자)** 다 — slug 가 아니다. slug 로 카테고리를 찾고 그 id 로 상품을 좁힌다.
|
|
209
|
+
- 없는 `categoryId` 는 **빈 목록**이지 404 가 아니다. 목록 API 는 "그 카테고리가 있느냐"를 답하지 않는다 —
|
|
210
|
+
404 판단은 카테고리를 못 찾은 라우트가 한다(위 `notFound()`).
|
|
211
|
+
- 목록 조회 실패는 삼켜 빈 카테고리로 강하해도 되지만, **카테고리 목록 조회 실패를 404 로 바꾸지 마라** —
|
|
212
|
+
백엔드 장애를 "그런 카테고리 없음"으로 말하면 크롤러가 그 오답을 캐시한다.
|
|
213
|
+
|
|
194
214
|
### 4.2 상품 상세 + 장바구니 담기
|
|
195
215
|
```tsx
|
|
196
216
|
// app/products/[slug]/page.tsx (RSC) — 조회는 공개
|
|
@@ -409,13 +429,15 @@ if (booking.orderNo) { // status=PENDING
|
|
|
409
429
|
|
|
410
430
|
**이 절의 규범은 기계로 잴 수 있다.** 이 패키지는 판정 잣대인 **보장표**를
|
|
411
431
|
`@zalkera/client/contracts/aeo-surface-guarantees.json` 으로 함께 배송한다(정본은 잘커라 백엔드에 있고 이것은
|
|
412
|
-
그 발행 산출물이다 — 내용이 갈라지지 않게 발행 전 기계가 대조한다).
|
|
413
|
-
`
|
|
414
|
-
산출물이
|
|
432
|
+
그 발행 산출물이다 — 내용이 갈라지지 않게 발행 전 기계가 대조한다). **검사기 자신도 이 패키지가 싣고
|
|
433
|
+
온다**(bin `zalkera-aeo-check`) — 그 표를 읽어 **개시된 사이트를 크롤해** 판정하므로, 소스가 아니라
|
|
434
|
+
산출물이 잣대다. 어떤 스택으로 짰든 같은 조건이다:
|
|
415
435
|
|
|
416
436
|
```bash
|
|
417
|
-
#
|
|
418
|
-
|
|
437
|
+
npx zalkera-aeo-check https://개시된사이트 --category BOOKING # 0=통과 · 1=미충족 · 2=실행 불가
|
|
438
|
+
npx zalkera-aeo-check https://개시된사이트 --site-wide-only # 보장 주장이 없는 사이트(사이트 축만)
|
|
439
|
+
# 템플릿에서 출발한 프로젝트라면 같은 검사기를 이렇게도 부른다:
|
|
440
|
+
npm run check:aeo -- https://개시된사이트 --category BOOKING
|
|
419
441
|
npm run check:aeo -- --print-guarantees # 잣대 해석만 확인(크롤 없음)
|
|
420
442
|
```
|
|
421
443
|
|
package/package.json
CHANGED
|
@@ -1,53 +1,60 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
"name": "@zalkera/client",
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"description": "zalkera 헤드리스 CMS 공개 API 클라이언트 (테넌트 사이트용)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Credium Co., Ltd.",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"zalkera",
|
|
13
|
+
"headless",
|
|
14
|
+
"commerce",
|
|
15
|
+
"cms",
|
|
16
|
+
"api-client",
|
|
17
|
+
"sdk"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://zalkera.com",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"llms.txt",
|
|
23
|
+
"contracts",
|
|
24
|
+
"bin",
|
|
25
|
+
"lib"
|
|
26
|
+
],
|
|
27
|
+
"main": "./dist/index.cjs",
|
|
28
|
+
"module": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"bin": {
|
|
31
|
+
"zalkera-aeo-check": "./bin/check-aeo-surfaces.mjs"
|
|
32
|
+
},
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"import": "./dist/index.js",
|
|
37
|
+
"require": "./dist/index.cjs"
|
|
10
38
|
},
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"types": "
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"require": "./dist/index.cjs"
|
|
33
|
-
},
|
|
34
|
-
"./contracts/aeo-surface-guarantees.json": "./contracts/aeo-surface-guarantees.json"
|
|
35
|
-
},
|
|
36
|
-
"scripts": {
|
|
37
|
-
"build": "tsup",
|
|
38
|
-
"dev": "tsup --watch",
|
|
39
|
-
"typecheck": "tsc --noEmit",
|
|
40
|
-
"test": "vitest run",
|
|
41
|
-
"test:watch": "vitest",
|
|
42
|
-
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
43
|
-
},
|
|
44
|
-
"engines": {
|
|
45
|
-
"node": ">=18"
|
|
46
|
-
},
|
|
47
|
-
"devDependencies": {
|
|
48
|
-
"@types/node": "^22.10.2",
|
|
49
|
-
"tsup": "^8.3.5",
|
|
50
|
-
"typescript": "^5.7.2",
|
|
51
|
-
"vitest": "^2.1.8"
|
|
52
|
-
}
|
|
39
|
+
"./contracts/aeo-surface-guarantees.json": "./contracts/aeo-surface-guarantees.json",
|
|
40
|
+
"./bin/check-aeo-surfaces.mjs": "./bin/check-aeo-surfaces.mjs",
|
|
41
|
+
"./lib/site-crawl.mjs": "./lib/site-crawl.mjs"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup",
|
|
45
|
+
"dev": "tsup --watch",
|
|
46
|
+
"typecheck": "tsc --noEmit",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"test:watch": "vitest",
|
|
49
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/node": "^22.10.2",
|
|
56
|
+
"tsup": "^8.3.5",
|
|
57
|
+
"typescript": "^5.7.2",
|
|
58
|
+
"vitest": "^2.1.8"
|
|
59
|
+
}
|
|
53
60
|
}
|