pubuilder 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 taehoon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # pubuilder
2
+
3
+ **In-app IA minimap & block inspector for React** — press a floating button in your dev build and see every page of your app laid out as live iframe thumbnails on a zoomable canvas. Click a page to zoom in, then hover-select sections and blocks devtools-style.
4
+
5
+ Dev-only by design: `<PageMap>` renders nothing in production builds automatically.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add -D pubuilder # or npm i -D / yarn add -D
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```tsx
16
+ // App.tsx
17
+ import { PageMap } from 'pubuilder'
18
+ import ia from './ia.config'
19
+
20
+ <PageMap config={ia} />
21
+ ```
22
+
23
+ That's it — in production bundles (`NODE_ENV === 'production'`) the component bails out on its own, so no manual guard is required. Wrapping it in your own dev flag still works and saves the bundle bytes:
24
+
25
+ ```tsx
26
+ {import.meta.env.DEV && <PageMap config={ia} />}
27
+ ```
28
+
29
+ ## Generate `ia.config.ts` automatically (Next.js)
30
+
31
+ ```bash
32
+ npx pubuilder scan # scan app/ or pages/ routes and draft ia.config.ts
33
+ npx pubuilder scan --dry-run # preview without writing
34
+ ```
35
+
36
+ - First run drafts the config — you only polish titles and replace `[param]` segments with sample paths (e.g. `/artflo/1`).
37
+ - Re-running **merges**: your hand-tuned titles, hierarchy, and variants are preserved; only new routes are added. Routes that disappeared are reported, never deleted.
38
+ - Keep `ia.config.ts` a pure data file (it is actually loaded during scan).
39
+ - Non-Next.js projects aren't scannable yet (v1) — the CLI generates an annotated template to start from instead.
40
+
41
+ ## Writing the config by hand
42
+
43
+ ```ts
44
+ // ia.config.ts
45
+ import { defineIA } from 'pubuilder'
46
+
47
+ export default defineIA({
48
+ pages: [
49
+ { path: '/login', title: 'Login' },
50
+ {
51
+ path: '/',
52
+ title: 'Home',
53
+ children: [
54
+ {
55
+ path: '/ai-agent',
56
+ title: 'AI Agent',
57
+ // screens that differ by query string become child nodes
58
+ variants: [
59
+ { title: 'All', query: { category: 'all' } },
60
+ { title: 'Art', query: { category: 'art', tab: 1 } },
61
+ ],
62
+ },
63
+ { path: 'https://example.com/brand', title: 'Brand', external: true },
64
+ ],
65
+ },
66
+ ],
67
+ })
68
+ ```
69
+
70
+ - `path` — route path; use a representative sample for dynamic routes (`/notice/1`)
71
+ - `variants` — query-string variations, rendered as separate thumbnails
72
+ - `external` — link-out node, no thumbnail/inspection (auto-detected for `http(s)://`)
73
+ - `viewport` — virtual viewport for thumbnails, default `1440x900`
74
+
75
+ ## Requirements
76
+
77
+ - React ≥ 18, same-origin pages (live thumbnails are iframes)
78
+ - Node ≥ 18 for the CLI
79
+
80
+ ## License
81
+
82
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,330 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync as I, existsSync as w, readdirSync as N, realpathSync as q, writeFileSync as v } from "node:fs";
3
+ import { join as g } from "node:path";
4
+ import { fileURLToPath as G } from "node:url";
5
+ import { createJiti as J } from "jiti";
6
+ const M = { width: 1440, height: 900 };
7
+ function E(t, n, e) {
8
+ e != null && t.append(n, String(e));
9
+ }
10
+ function W(t, n) {
11
+ const e = t.indexOf("#"), r = e >= 0 ? t.slice(e) : "", s = e >= 0 ? t.slice(0, e) : t, i = s.indexOf("?"), a = i >= 0 ? s.slice(0, i) : s, o = new URLSearchParams(i >= 0 ? s.slice(i + 1) : "");
12
+ for (const [c, h] of Object.entries(n.query))
13
+ if (o.delete(c), Array.isArray(h))
14
+ for (const m of h) E(o, c, m);
15
+ else
16
+ E(o, c, h);
17
+ const l = o.toString();
18
+ return `${a}${l ? `?${l}` : ""}${r}`;
19
+ }
20
+ function b(t) {
21
+ const n = [], e = /* @__PURE__ */ new Set(), r = (s, i, a) => {
22
+ for (const o of s) {
23
+ if (!o.path) throw new Error("[pubuilder] page.path는 필수입니다");
24
+ if (!o.title) throw new Error(`[pubuilder] "${o.path}"의 title이 없습니다`);
25
+ if (e.has(o.path)) throw new Error(`[pubuilder] 중복 path: ${o.path}`);
26
+ if (e.add(o.path), n.push({
27
+ id: o.path,
28
+ path: o.path,
29
+ title: o.title,
30
+ external: o.external ?? /^https?:\/\//.test(o.path),
31
+ parentId: i,
32
+ depth: a
33
+ }), o.variants?.length) {
34
+ if (o.external ?? /^https?:\/\//.test(o.path))
35
+ throw new Error(`[pubuilder] 외부 페이지에는 variants를 사용할 수 없습니다: ${o.path}`);
36
+ for (const l of o.variants) {
37
+ if (!l.title) throw new Error(`[pubuilder] "${o.path}" variant의 title이 없습니다`);
38
+ const c = W(o.path, l);
39
+ if (e.has(c)) throw new Error(`[pubuilder] 중복 path: ${c}`);
40
+ e.add(c), n.push({
41
+ id: c,
42
+ path: c,
43
+ title: l.title,
44
+ external: !1,
45
+ parentId: o.path,
46
+ depth: a + 1
47
+ });
48
+ }
49
+ }
50
+ o.children?.length && r(o.children, o.path, a + 1);
51
+ }
52
+ };
53
+ if (r(t.pages, null, 0), n.length === 0) throw new Error("[pubuilder] pages가 비어 있습니다");
54
+ return { nodes: n, viewport: t.viewport ?? M };
55
+ }
56
+ const p = " ";
57
+ function V(t) {
58
+ return t.split("/").map((n) => /^\[{1,2}\.{0,3}.*\]{1,2}$/.test(n) && n.startsWith("[") ? "1" : n).join("/");
59
+ }
60
+ function $(t) {
61
+ return JSON.stringify(t);
62
+ }
63
+ function k(t, n, e) {
64
+ const r = p.repeat(n), s = [];
65
+ t.path.includes("[") && s.push(`${r}// TODO: 샘플 경로로 교체 (예: ${V(t.path)})`);
66
+ const i = e.has(t.path) ? " // TODO: title 확인" : "";
67
+ if (!t.children?.length && !t.variants?.length && !t.external)
68
+ return s.push(`${r}{ path: ${$(t.path)}, title: ${$(t.title)} },${i}`), s;
69
+ if (s.push(`${r}{`), s.push(`${r}${p}path: ${$(t.path)},`), s.push(`${r}${p}title: ${$(t.title)},${i}`), t.external && s.push(`${r}${p}external: true,`), t.variants?.length) {
70
+ s.push(`${r}${p}variants: [`);
71
+ for (const o of t.variants)
72
+ s.push(
73
+ `${r}${p}${p}{ title: ${$(o.title)}, query: ${$(o.query)} },`
74
+ );
75
+ s.push(`${r}${p}],`);
76
+ }
77
+ if (t.children?.length) {
78
+ s.push(`${r}${p}children: [`);
79
+ for (const o of t.children) s.push(...k(o, n + 2, e));
80
+ s.push(`${r}${p}],`);
81
+ }
82
+ return s.push(`${r}},`), s;
83
+ }
84
+ function A(t, n = {}) {
85
+ const e = n.newPaths ?? /* @__PURE__ */ new Set(), r = [
86
+ 'import { defineIA } from "pubuilder"',
87
+ "",
88
+ "export default defineIA({"
89
+ ];
90
+ t.viewport && r.push(`${p}viewport: ${$(t.viewport)},`), r.push(`${p}pages: [`);
91
+ for (const s of t.pages) r.push(...k(s, 2, e));
92
+ return r.push(`${p}],`), r.push("})"), r.push(""), r.join(`
93
+ `);
94
+ }
95
+ function d(t) {
96
+ return t === "/" ? [] : t.replace(/^\//, "").split("/");
97
+ }
98
+ function z(t, n) {
99
+ return t.length >= n.length ? !1 : t.every((e, r) => e === n[r]);
100
+ }
101
+ function B(t) {
102
+ const n = [...t].sort((s, i) => {
103
+ const a = d(s.routePath).length - d(i.routePath).length;
104
+ return a !== 0 ? a : s.routePath.localeCompare(i.routePath);
105
+ }), e = [], r = [];
106
+ for (const s of n) {
107
+ const i = d(s.routePath), a = {
108
+ path: s.routePath,
109
+ title: s.title ?? i.at(-1) ?? "/"
110
+ };
111
+ let o;
112
+ for (const l of r)
113
+ z(l.segments, i) && (!o || l.segments.length > o.segments.length) && (o = l);
114
+ o ? (o.page.children ??= []).push(a) : e.push(a), r.push({ segments: i, page: a });
115
+ }
116
+ return e;
117
+ }
118
+ function R(t, n) {
119
+ const e = d(t), r = d(n);
120
+ for (let s = 0; s < r.length; s++) {
121
+ const i = r[s];
122
+ if (/^\[\[\.\.\..+\]\]$/.test(i))
123
+ return s <= e.length;
124
+ if (/^\[\.\.\..+\]$/.test(i))
125
+ return s < e.length;
126
+ if (s >= e.length) return !1;
127
+ if (!/^\[.+\]$/.test(i) && i !== e[s])
128
+ return !1;
129
+ }
130
+ return r.length === e.length;
131
+ }
132
+ function F(t, n = []) {
133
+ for (const e of t)
134
+ n.push({ page: e, segments: d(e.path) }), e.children && F(e.children, n);
135
+ return n;
136
+ }
137
+ function D(t) {
138
+ return t.external ?? /^https?:\/\//.test(t.path);
139
+ }
140
+ function X(t, n) {
141
+ const e = structuredClone(t), r = F(e.pages), s = r.filter((u) => !D(u.page)), i = (u) => s.some(
142
+ (f) => f.page.path === u.routePath || R(f.page.path, u.routePath)
143
+ ), a = n.filter((u) => !i(u)), o = (u) => n.some(
144
+ (f) => f.routePath === u.page.path || R(u.page.path, f.routePath)
145
+ ), l = s.filter((u) => !o(u)).map((u) => u.page.path), c = [], h = [...r], m = [...a].sort(
146
+ (u, f) => d(u.routePath).length - d(f.routePath).length
147
+ );
148
+ for (const u of m) {
149
+ const f = d(u.routePath), y = { path: u.routePath, title: u.title ?? f.at(-1) ?? "/" };
150
+ let P;
151
+ for (const x of h) {
152
+ if (D(x.page)) continue;
153
+ const S = x.segments;
154
+ S.length >= f.length || S.every((L, U) => L === f[U]) && (!P || S.length > P.segments.length) && (P = x);
155
+ }
156
+ P ? (P.page.children ??= []).push(y) : e.pages.push(y), h.push({ page: y, segments: f }), c.push(u.routePath);
157
+ }
158
+ return { config: e, added: c, removed: l };
159
+ }
160
+ const Q = /^page\.(tsx|jsx|ts|js)$/, Z = /^\(.+\)$/, H = /^\(\.{1,3}\)/;
161
+ function O(t) {
162
+ for (const n of ["app", "src/app"])
163
+ if (w(g(t, n))) return g(t, n);
164
+ return null;
165
+ }
166
+ function K(t) {
167
+ const n = [];
168
+ for (const e of t) {
169
+ if (e.startsWith("@") || e.startsWith("_") || H.test(e)) return null;
170
+ Z.test(e) || n.push(e);
171
+ }
172
+ return `/${n.join("/")}`;
173
+ }
174
+ function Y(t) {
175
+ const n = t.match(
176
+ /export\s+const\s+metadata\s*(?::\s*[A-Za-z_$][\w$.]*\s*)?=\s*\{(?:[^{}]*?[,\s])?title\s*:\s*(['"])((?:\\.|(?!\1)[^\\])*)\1/
177
+ );
178
+ return n ? n[2] : void 0;
179
+ }
180
+ function _(t, n, e) {
181
+ for (const r of N(t, { withFileTypes: !0 }))
182
+ r.isDirectory() ? _(g(t, r.name), [...n, r.name], e) : Q.test(r.name) && e.push({ file: g(t, r.name), segments: n });
183
+ }
184
+ const tt = {
185
+ name: "next-app",
186
+ detect(t) {
187
+ return O(t) !== null;
188
+ },
189
+ scan(t) {
190
+ const n = O(t);
191
+ if (!n) return [];
192
+ const e = [];
193
+ _(n, [], e);
194
+ const r = [];
195
+ for (const { file: s, segments: i } of e) {
196
+ const a = K(i);
197
+ a !== null && r.push({
198
+ routePath: a,
199
+ isDynamic: /\[.+\]/.test(a),
200
+ title: Y(I(s, "utf8"))
201
+ });
202
+ }
203
+ return r;
204
+ }
205
+ }, T = /\.(tsx|jsx|ts|js)$/, et = /* @__PURE__ */ new Set(["_app", "_document", "_error", "404", "500"]);
206
+ function j(t) {
207
+ for (const n of ["pages", "src/pages"])
208
+ if (w(g(t, n))) return g(t, n);
209
+ return null;
210
+ }
211
+ function C(t, n, e) {
212
+ for (const r of N(t, { withFileTypes: !0 })) {
213
+ if (r.isDirectory()) {
214
+ if (n.length === 0 && r.name === "api") continue;
215
+ C(g(t, r.name), [...n, r.name], e);
216
+ continue;
217
+ }
218
+ if (!T.test(r.name)) continue;
219
+ const s = r.name.replace(T, "");
220
+ if (et.has(s)) continue;
221
+ const a = `/${(s === "index" ? n : [...n, s]).join("/")}`;
222
+ e.push({ routePath: a, isDynamic: /\[.+\]/.test(a) });
223
+ }
224
+ }
225
+ const nt = {
226
+ name: "next-pages",
227
+ detect(t) {
228
+ return j(t) !== null;
229
+ },
230
+ scan(t) {
231
+ const n = j(t);
232
+ if (!n) return [];
233
+ const e = [];
234
+ return C(n, [], e), e;
235
+ }
236
+ }, rt = `import { defineIA } from "pubuilder"
237
+
238
+ // pubuilder scan은 아직 이 프로젝트의 라우팅 방식을 지원하지 않아요 (v1은 Next.js 전용).
239
+ // 아래 예시를 참고해 앱의 화면 구조를 직접 작성해주세요.
240
+ // - path: 라우트 경로. 동적 라우트는 대표 샘플 경로 사용 (예: /notice/1)
241
+ // - variants: query string으로 분기되는 화면 (자식 노드로 표시)
242
+ // - external: 외부 URL은 링크아웃 노드로 표시
243
+ export default defineIA({
244
+ pages: [
245
+ { path: "/login", title: "로그인" },
246
+ {
247
+ path: "/",
248
+ title: "홈",
249
+ children: [
250
+ { path: "/example", title: "예시 페이지" },
251
+ ],
252
+ },
253
+ ],
254
+ })
255
+ `, st = [tt, nt];
256
+ function ot(t) {
257
+ const n = g(t, "package.json");
258
+ if (!w(n)) return !1;
259
+ try {
260
+ const e = JSON.parse(I(n, "utf8"));
261
+ return !!(e.dependencies?.next ?? e.devDependencies?.next);
262
+ } catch {
263
+ return console.warn("[pubuilder] package.json 파싱 실패 — JSON 문법을 확인해주세요"), !1;
264
+ }
265
+ }
266
+ function it(t) {
267
+ if (!ot(t)) return null;
268
+ const n = st.filter((s) => s.detect(t));
269
+ if (n.length === 0) return null;
270
+ const e = /* @__PURE__ */ new Map(), r = [];
271
+ for (const s of n)
272
+ for (const i of s.scan(t)) {
273
+ const a = e.get(i.routePath);
274
+ if (a) {
275
+ console.warn(`[pubuilder] path 충돌: ${i.routePath} (${a} 우선, ${s.name} 무시)`);
276
+ continue;
277
+ }
278
+ e.set(i.routePath, s.name), r.push(i);
279
+ }
280
+ return r;
281
+ }
282
+ async function at(t) {
283
+ const e = await J(import.meta.url).import(t, { default: !0 });
284
+ if (!e || !Array.isArray(e.pages))
285
+ throw new Error("[pubuilder] ia.config.ts의 default export에서 pages 배열을 찾지 못했어요");
286
+ return e;
287
+ }
288
+ async function ut(t, n) {
289
+ const e = it(t), r = g(t, "ia.config.ts");
290
+ if (e === null)
291
+ return console.error("[pubuilder] 지원하지 않는 라우팅 방식이에요. v1은 Next.js(App/Pages Router) 전용입니다."), !w(r) && !n.dryRun && (v(r, rt), console.log(`[pubuilder] 작성법 안내가 담긴 예시 템플릿을 생성했어요: ${r}`)), { kind: "unsupported" };
292
+ if (w(r)) {
293
+ const o = await at(r), { config: l, added: c, removed: h } = X(o, e);
294
+ b(l), n.dryRun || v(r, A(l, { newPaths: new Set(c) }));
295
+ for (const m of c) console.log(`[pubuilder] + 추가됨: ${m}`);
296
+ for (const m of h) console.log(`[pubuilder] - 라우트에서 사라짐 (config엔 유지): ${m}`);
297
+ return c.length === 0 && h.length === 0 && console.log("[pubuilder] 변경 없음"), n.dryRun && console.log("[pubuilder] (dry-run) 파일은 수정하지 않았어요"), { kind: "merged", added: c, removed: h };
298
+ }
299
+ const i = { pages: B(e) };
300
+ b(i);
301
+ const a = e.map((o) => o.routePath);
302
+ return n.dryRun ? console.log(`[pubuilder] (dry-run) 생성 예정: ${a.length}개 라우트. 파일은 수정하지 않았어요.`) : (v(r, A(i, { newPaths: new Set(a) })), console.log(`[pubuilder] ia.config.ts 초안을 생성했어요 (${a.length}개 라우트). title과 [param] 샘플 경로를 다듬어주세요.`)), { kind: "created", paths: a };
303
+ }
304
+ async function ct(t) {
305
+ const [n, ...e] = t;
306
+ if (n !== "scan")
307
+ return console.error("사용법: pubuilder scan [--dry-run]"), 1;
308
+ try {
309
+ return (await ut(process.cwd(), { dryRun: e.includes("--dry-run") })).kind === "unsupported" ? 1 : 0;
310
+ } catch (r) {
311
+ return console.error(`[pubuilder] 실패: ${r instanceof Error ? r.message : String(r)}`), 1;
312
+ }
313
+ }
314
+ function lt() {
315
+ const t = process.argv[1];
316
+ if (!t) return !1;
317
+ try {
318
+ return q(t) === G(import.meta.url);
319
+ } catch {
320
+ return !1;
321
+ }
322
+ }
323
+ lt() && ct(process.argv.slice(2)).then(
324
+ (t) => process.exit(t),
325
+ () => process.exit(1)
326
+ );
327
+ export {
328
+ ct as main,
329
+ ut as runScan
330
+ };
@@ -0,0 +1,4 @@
1
+ import type { NormalizedIA } from '../config';
2
+ export declare function CanvasOverlay({ ia }: {
3
+ ia: NormalizedIA;
4
+ }): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ export declare function Fab(): import("react").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import type { IAConfig } from '../types';
2
+ export interface PageMapProps {
3
+ config: IAConfig;
4
+ /**
5
+ * 렌더 여부. 기본 true. production 빌드에서는 이 값과 무관하게
6
+ * 자동으로 렌더하지 않는다 (dev 전용 안전장치).
7
+ */
8
+ enabled?: boolean;
9
+ }
10
+ export declare function PageMap({ config, enabled }: PageMapProps): import("react").ReactPortal | null;
@@ -0,0 +1,13 @@
1
+ import { type NodeProps } from '@xyflow/react';
2
+ import type { IANode } from '../types';
3
+ export interface PageNodeData {
4
+ page: IANode;
5
+ viewport: {
6
+ width: number;
7
+ height: number;
8
+ };
9
+ thumbWidth: number;
10
+ bodyHeight: number;
11
+ [key: string]: unknown;
12
+ }
13
+ export declare const PageNode: import("react").NamedExoticComponent<NodeProps>;
@@ -0,0 +1,4 @@
1
+ import type { NormalizedIA } from '../config';
2
+ export declare function PageViewer({ ia }: {
3
+ ia: NormalizedIA;
4
+ }): import("react").JSX.Element | null;
@@ -0,0 +1 @@
1
+ export declare function SelectionPanel(): import("react").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import type { IAConfig, IANode } from './types';
2
+ /** ia.config.ts 작성용 헬퍼 — 타입 추론만 제공 */
3
+ export declare function defineIA(config: IAConfig): IAConfig;
4
+ export interface NormalizedIA {
5
+ nodes: IANode[];
6
+ viewport: {
7
+ width: number;
8
+ height: number;
9
+ };
10
+ }
11
+ /** IAConfig 트리를 검증하고 평탄화한다 */
12
+ export declare function normalizeIA(config: IAConfig): NormalizedIA;
package/dist/env.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * production 번들 여부. 소비 측 번들러(Next/Vite 등)가 process.env.NODE_ENV를
3
+ * 정적 치환하므로 앱 빌드 시 상수로 접힌다. 번들러 없는 환경(process 미정의)에서는
4
+ * false — 렌더를 막는 쪽이 아니라 허용하는 쪽이 안전한 기본값.
5
+ *
6
+ * 주의: 라이브러리 빌드가 이 표현식을 미리 치환하면 안 된다 — vite.config.ts 참고.
7
+ */
8
+ export declare function isProductionEnv(): boolean;
@@ -0,0 +1,3 @@
1
+ export { PageMap, type PageMapProps } from './components/PageMap';
2
+ export { defineIA, normalizeIA, type NormalizedIA } from './config';
3
+ export type { BlockSelection, Granularity, IAConfig, IANode, IAPage, } from './types';