peekr-ui 0.1.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/dist/index.js ADDED
@@ -0,0 +1,2211 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import { resolve as resolve3 } from "path";
6
+ import { existsSync as existsSync4 } from "fs";
7
+ import pc from "picocolors";
8
+
9
+ // src/config.ts
10
+ import { existsSync } from "fs";
11
+ import { join } from "path";
12
+ import { pathToFileURL } from "url";
13
+ var CONFIG_FILES = ["peekr.config.ts", "peekr.config.js", "peek.config.ts", "peek.config.js"];
14
+ async function loadConfig(rootDir) {
15
+ for (const filename of CONFIG_FILES) {
16
+ const configPath = join(rootDir, filename);
17
+ if (existsSync(configPath)) {
18
+ try {
19
+ const mod = await import(pathToFileURL(configPath).href);
20
+ const config = mod.default ?? mod;
21
+ return config;
22
+ } catch {
23
+ }
24
+ }
25
+ }
26
+ return {};
27
+ }
28
+ function mergeConfig(base, overrides) {
29
+ return {
30
+ port: overrides.port ?? base.port ?? 4321,
31
+ open: overrides.open ?? base.open ?? true,
32
+ include: overrides.include ?? base.include ?? [],
33
+ exclude: overrides.exclude ?? base.exclude ?? [],
34
+ framework: overrides.framework ?? base.framework ?? "react",
35
+ globalCss: overrides.globalCss ?? base.globalCss ?? []
36
+ };
37
+ }
38
+
39
+ // src/server.ts
40
+ import { createServer, transformWithEsbuild } from "vite";
41
+ import { readFileSync as readFileSync2, existsSync as existsSync3, statSync as statSync3 } from "fs";
42
+ import { join as join4, resolve as resolve2, relative as relative2, extname as extname2, dirname as dirname2 } from "path";
43
+ import { createRequire } from "module";
44
+
45
+ // src/scanner.ts
46
+ import { readFileSync, statSync } from "fs";
47
+ import { join as join2, relative, dirname, basename, extname, resolve } from "path";
48
+ import fg from "fast-glob";
49
+ import { parse } from "@babel/parser";
50
+ var BABEL_PLUGINS = [
51
+ "jsx",
52
+ "typescript",
53
+ "decorators-legacy",
54
+ "classProperties",
55
+ "optionalChaining",
56
+ "nullishCoalescingOperator",
57
+ "dynamicImport",
58
+ "exportDefaultFrom",
59
+ "importMeta"
60
+ ];
61
+ function containsJSX(node) {
62
+ if (!node) return false;
63
+ const type = node.type;
64
+ if (type === "JSXElement" || type === "JSXFragment") return true;
65
+ if (type === "ReturnStatement") return containsJSX(node.argument);
66
+ if (type === "BlockStatement") {
67
+ return node.body.some(containsJSX);
68
+ }
69
+ if (type === "ArrowFunctionExpression" || type === "FunctionExpression") {
70
+ return containsJSX(node.body);
71
+ }
72
+ if (type === "FunctionDeclaration") {
73
+ return containsJSX(node.body);
74
+ }
75
+ if (type === "ConditionalExpression") {
76
+ return containsJSX(node.consequent) || containsJSX(node.alternate);
77
+ }
78
+ if (type === "LogicalExpression") {
79
+ return containsJSX(node.left) || containsJSX(node.right);
80
+ }
81
+ if (type === "IfStatement") {
82
+ return containsJSX(node.consequent) || containsJSX(node.alternate);
83
+ }
84
+ if (type === "ParenthesizedExpression" || type === "ExpressionStatement") {
85
+ return containsJSX(node.expression);
86
+ }
87
+ if (type === "CallExpression") {
88
+ const callee = node.callee;
89
+ const calleeStr = callee?.type === "MemberExpression" ? `${callee.object?.name}.${callee.property?.name}` : callee?.name ?? "";
90
+ if (calleeStr === "React.forwardRef" || calleeStr === "React.memo" || calleeStr === "forwardRef" || calleeStr === "memo") {
91
+ return true;
92
+ }
93
+ return node.arguments.some(containsJSX);
94
+ }
95
+ return false;
96
+ }
97
+ function isComponentNode(name, init) {
98
+ if (!name) return false;
99
+ if (!/^[A-Z]/.test(name)) return false;
100
+ return containsJSX(init);
101
+ }
102
+ function parseFileForComponents(code, filePath) {
103
+ let ast;
104
+ try {
105
+ ast = parse(code, {
106
+ sourceType: "module",
107
+ plugins: BABEL_PLUGINS,
108
+ errorRecovery: true
109
+ });
110
+ } catch {
111
+ return [];
112
+ }
113
+ const exports = [];
114
+ const body = ast.program.body;
115
+ const declaredComponents = /* @__PURE__ */ new Map();
116
+ for (const stmt of body) {
117
+ const type = stmt.type;
118
+ if (type === "VariableDeclaration") {
119
+ for (const decl of stmt.declarations) {
120
+ const id = decl.id;
121
+ if (id?.type === "Identifier") {
122
+ const name = id.name;
123
+ if (/^[A-Z]/.test(name)) {
124
+ declaredComponents.set(name, decl.init);
125
+ }
126
+ }
127
+ }
128
+ }
129
+ if (type === "FunctionDeclaration") {
130
+ const name = stmt.id?.name ?? "";
131
+ if (/^[A-Z]/.test(name)) {
132
+ declaredComponents.set(name, stmt);
133
+ }
134
+ }
135
+ if (type === "ExportNamedDeclaration") {
136
+ const decl = stmt.declaration;
137
+ if (decl?.type === "VariableDeclaration") {
138
+ for (const d of decl.declarations) {
139
+ const name = d.id?.name ?? "";
140
+ if (isComponentNode(name, d.init)) {
141
+ exports.push({ exportName: name, isDefault: false });
142
+ declaredComponents.set(name, d.init);
143
+ }
144
+ }
145
+ }
146
+ if (decl?.type === "FunctionDeclaration") {
147
+ const name = decl.id?.name ?? "";
148
+ if (isComponentNode(name, decl)) {
149
+ exports.push({ exportName: name, isDefault: false });
150
+ declaredComponents.set(name, decl);
151
+ }
152
+ }
153
+ if (!decl && stmt.specifiers?.length > 0 && !stmt.source) {
154
+ for (const spec of stmt.specifiers) {
155
+ const localName = spec.local?.name ?? "";
156
+ if (declaredComponents.has(localName)) {
157
+ const init = declaredComponents.get(localName);
158
+ if (isComponentNode(localName, init)) {
159
+ exports.push({ exportName: spec.exported?.name ?? localName, isDefault: false });
160
+ }
161
+ }
162
+ }
163
+ }
164
+ }
165
+ if (type === "ExportDefaultDeclaration") {
166
+ const decl = stmt.declaration;
167
+ if (decl?.type === "ArrowFunctionExpression" || decl?.type === "FunctionExpression") {
168
+ if (containsJSX(decl)) {
169
+ exports.push({ exportName: "default", isDefault: true });
170
+ }
171
+ }
172
+ if (decl?.type === "FunctionDeclaration") {
173
+ if (containsJSX(decl)) {
174
+ exports.push({ exportName: "default", isDefault: true });
175
+ }
176
+ }
177
+ if (decl?.type === "Identifier") {
178
+ const name = decl.name;
179
+ if (declaredComponents.has(name)) {
180
+ const init = declaredComponents.get(name);
181
+ if (/^[A-Z]/.test(name) && containsJSX(init)) {
182
+ exports.push({ exportName: "default", isDefault: true });
183
+ }
184
+ }
185
+ }
186
+ if (decl?.type === "CallExpression") {
187
+ if (containsJSX(decl)) {
188
+ exports.push({ exportName: "default", isDefault: true });
189
+ }
190
+ }
191
+ }
192
+ }
193
+ return deduplicateExports(exports);
194
+ }
195
+ function parseBarrelForReExports(code) {
196
+ const result = [];
197
+ let ast;
198
+ try {
199
+ ast = parse(code, {
200
+ sourceType: "module",
201
+ plugins: BABEL_PLUGINS,
202
+ errorRecovery: true
203
+ });
204
+ } catch {
205
+ return [];
206
+ }
207
+ for (const stmt of ast.program.body) {
208
+ if (stmt.type === "ExportNamedDeclaration" && !stmt.declaration && stmt.source) {
209
+ const source = stmt.source.value;
210
+ if (!source.startsWith(".")) continue;
211
+ for (const spec of stmt.specifiers ?? []) {
212
+ const localName = spec.local?.name ?? "";
213
+ const exportedName = spec.exported?.name ?? localName;
214
+ if (localName) {
215
+ result.push({ localName, exportedName, source, isStar: false });
216
+ }
217
+ }
218
+ }
219
+ if (stmt.type === "ExportAllDeclaration" && stmt.source) {
220
+ const source = stmt.source.value;
221
+ if (source.startsWith(".")) {
222
+ result.push({ localName: "*", exportedName: "*", source, isStar: true });
223
+ }
224
+ }
225
+ }
226
+ return result;
227
+ }
228
+ function deduplicateExports(exports) {
229
+ const seen = /* @__PURE__ */ new Set();
230
+ return exports.filter((e) => {
231
+ if (seen.has(e.exportName)) return false;
232
+ seen.add(e.exportName);
233
+ return true;
234
+ });
235
+ }
236
+ function makeId(filePath, exportName) {
237
+ return `${filePath}::${exportName}`;
238
+ }
239
+ function getDisplayName(filePath, exportName) {
240
+ if (exportName !== "default") return exportName;
241
+ const base = basename(filePath, extname(filePath));
242
+ return base === "index" ? basename(dirname(filePath)) : base;
243
+ }
244
+ async function runWithConcurrency(tasks, limit) {
245
+ const results = [];
246
+ let idx = 0;
247
+ const workers = Array.from({ length: Math.min(limit, tasks.length) }, async () => {
248
+ while (idx < tasks.length) {
249
+ const i = idx++;
250
+ results[i] = await tasks[i]();
251
+ }
252
+ });
253
+ await Promise.all(workers);
254
+ return results;
255
+ }
256
+ var DEFAULT_EXCLUDE = [
257
+ "**/node_modules/**",
258
+ "**/.next/**",
259
+ "**/.nuxt/**",
260
+ "**/dist/**",
261
+ "**/build/**",
262
+ "**/.cache/**",
263
+ "**/*.test.*",
264
+ "**/*.spec.*",
265
+ "**/*.stories.*",
266
+ "**/*.story.*",
267
+ // Config & tooling files (never React components)
268
+ "**/vite.config.*",
269
+ "**/vitest.config.*",
270
+ "**/webpack.config.*",
271
+ "**/babel.config.*",
272
+ "**/jest.config.*",
273
+ "**/rollup.config.*",
274
+ "**/tailwind.config.*",
275
+ "**/postcss.config.*",
276
+ "**/.eslintrc.*",
277
+ "**/eslint.config.*",
278
+ "**/prettier.config.*",
279
+ "**/playwright.config.*",
280
+ "**/cypress.config.*",
281
+ "**/craco.config.*",
282
+ // Next.js Pages Router — special files only
283
+ "**/pages/api/**",
284
+ "**/pages/_app.*",
285
+ "**/pages/_document.*",
286
+ "**/pages/_error.*",
287
+ // Next.js App Router — route-special files only
288
+ "**/app/layout.*",
289
+ "**/app/page.*",
290
+ "**/app/**/page.*",
291
+ "**/app/**/layout.*",
292
+ "**/app/**/loading.*",
293
+ "**/app/**/error.*",
294
+ "**/app/**/not-found.*",
295
+ "**/app/route.*",
296
+ "**/app/**/route.*",
297
+ "**/__tests__/**",
298
+ "**/.storybook/**"
299
+ ];
300
+ async function scanComponents(rootDir, cache, extraExclude = []) {
301
+ const excludePatterns = [...DEFAULT_EXCLUDE, ...extraExclude];
302
+ const files = await fg(["**/*.{tsx,jsx,js}"], {
303
+ cwd: rootDir,
304
+ ignore: excludePatterns,
305
+ absolute: false,
306
+ dot: false
307
+ });
308
+ const tasks = files.map((relPath) => async () => {
309
+ const absPath = join2(rootDir, relPath);
310
+ let mtime = 0;
311
+ try {
312
+ mtime = statSync(absPath).mtimeMs;
313
+ } catch {
314
+ return;
315
+ }
316
+ const cached = cache.files.get(absPath);
317
+ if (cached && cached.mtime === mtime) return;
318
+ let code = "";
319
+ try {
320
+ code = readFileSync(absPath, "utf-8");
321
+ } catch {
322
+ cache.files.delete(absPath);
323
+ return;
324
+ }
325
+ const components = parseFileForComponents(code, relPath);
326
+ cache.files.set(absPath, { mtime, components });
327
+ });
328
+ await runWithConcurrency(tasks, 50);
329
+ for (const [absPath] of cache.files) {
330
+ if (!files.includes(relative(rootDir, absPath))) {
331
+ cache.files.delete(absPath);
332
+ }
333
+ }
334
+ const barrelFiles = await fg(["**/index.{ts,js}"], {
335
+ cwd: rootDir,
336
+ ignore: excludePatterns,
337
+ absolute: false,
338
+ dot: false
339
+ });
340
+ const barrelTasks = barrelFiles.map((relPath) => async () => {
341
+ const absPath = join2(rootDir, relPath);
342
+ let mtime = 0;
343
+ try {
344
+ mtime = statSync(absPath).mtimeMs;
345
+ } catch {
346
+ return;
347
+ }
348
+ const cached = cache.barrels.get(absPath);
349
+ if (cached && cached.mtime === mtime) return;
350
+ let code = "";
351
+ try {
352
+ code = readFileSync(absPath, "utf-8");
353
+ } catch {
354
+ cache.barrels.delete(absPath);
355
+ return;
356
+ }
357
+ const reExports = parseBarrelForReExports(code);
358
+ cache.barrels.set(absPath, { mtime, reExports });
359
+ });
360
+ await runWithConcurrency(barrelTasks, 50);
361
+ for (const [absPath] of cache.barrels) {
362
+ if (!barrelFiles.includes(relative(rootDir, absPath))) {
363
+ cache.barrels.delete(absPath);
364
+ }
365
+ }
366
+ const seenIds = /* @__PURE__ */ new Set();
367
+ const all = [];
368
+ for (const [absPath, { components }] of cache.files) {
369
+ const filePath = relative(rootDir, absPath);
370
+ for (const exp of components) {
371
+ const id = makeId(filePath, exp.exportName);
372
+ if (seenIds.has(id)) continue;
373
+ seenIds.add(id);
374
+ all.push({
375
+ id,
376
+ name: getDisplayName(filePath, exp.exportName),
377
+ filePath,
378
+ exportName: exp.exportName,
379
+ isDefault: exp.isDefault,
380
+ folder: dirname(filePath)
381
+ });
382
+ }
383
+ }
384
+ for (const [barrelAbsPath, { reExports }] of cache.barrels) {
385
+ const barrelDir = dirname(barrelAbsPath);
386
+ for (const reExport of reExports) {
387
+ const sourceBase = resolve(barrelDir, reExport.source);
388
+ const sourceCandidates = [
389
+ sourceBase + ".tsx",
390
+ sourceBase + ".jsx",
391
+ sourceBase + ".js",
392
+ join2(sourceBase, "index.tsx"),
393
+ join2(sourceBase, "index.jsx"),
394
+ join2(sourceBase, "index.js")
395
+ ];
396
+ for (const sourcePath of sourceCandidates) {
397
+ const sourceEntry = cache.files.get(sourcePath);
398
+ if (!sourceEntry) continue;
399
+ const relSourcePath = relative(rootDir, sourcePath);
400
+ if (reExport.isStar) {
401
+ for (const comp of sourceEntry.components) {
402
+ const id = makeId(relSourcePath, comp.exportName);
403
+ if (seenIds.has(id)) continue;
404
+ seenIds.add(id);
405
+ all.push({
406
+ id,
407
+ name: getDisplayName(relSourcePath, comp.exportName),
408
+ filePath: relSourcePath,
409
+ exportName: comp.exportName,
410
+ isDefault: comp.isDefault,
411
+ folder: dirname(relSourcePath)
412
+ });
413
+ }
414
+ } else {
415
+ const found = sourceEntry.components.find(
416
+ (c) => c.exportName === reExport.localName || reExport.localName === "default" && c.isDefault
417
+ );
418
+ if (found) {
419
+ const id = makeId(relSourcePath, found.exportName);
420
+ if (!seenIds.has(id)) {
421
+ seenIds.add(id);
422
+ const displayName = reExport.exportedName !== reExport.localName && reExport.exportedName !== "*" ? reExport.exportedName : getDisplayName(relSourcePath, found.exportName);
423
+ all.push({
424
+ id,
425
+ name: displayName,
426
+ filePath: relSourcePath,
427
+ exportName: found.exportName,
428
+ isDefault: found.isDefault,
429
+ folder: dirname(relSourcePath)
430
+ });
431
+ }
432
+ }
433
+ }
434
+ break;
435
+ }
436
+ }
437
+ }
438
+ all.sort((a, b) => {
439
+ if (a.folder !== b.folder) return a.folder.localeCompare(b.folder);
440
+ return a.name.localeCompare(b.name);
441
+ });
442
+ cache.all = all;
443
+ return all;
444
+ }
445
+ function invalidateScanFile(rootDir, relPath, cache) {
446
+ const absPath = join2(rootDir, relPath);
447
+ cache.files.delete(absPath);
448
+ cache.barrels.delete(absPath);
449
+ }
450
+
451
+ // src/typeParser.ts
452
+ import { statSync as statSync2 } from "fs";
453
+ import { join as join3 } from "path";
454
+ import {
455
+ Project,
456
+ SyntaxKind
457
+ } from "ts-morph";
458
+ var _project = null;
459
+ function getProject(rootDir) {
460
+ if (_project) return _project;
461
+ const tsconfigPath = join3(rootDir, "tsconfig.json");
462
+ try {
463
+ _project = new Project({
464
+ tsConfigFilePath: tsconfigPath,
465
+ skipAddingFilesFromTsConfig: true
466
+ });
467
+ } catch {
468
+ _project = new Project({
469
+ compilerOptions: {
470
+ jsx: 4,
471
+ // JsxEmit.ReactJSX
472
+ strict: true,
473
+ moduleResolution: 99,
474
+ // ModuleResolutionKind.Bundler
475
+ allowJs: true
476
+ }
477
+ });
478
+ }
479
+ return _project;
480
+ }
481
+ function mapType(type, seenTypes = /* @__PURE__ */ new Set()) {
482
+ const typeText = type.getText();
483
+ if (seenTypes.has(typeText)) return { type: "unknown" };
484
+ seenTypes.add(typeText);
485
+ if (type.isBoolean() || type.isBooleanLiteral()) {
486
+ return { type: "boolean" };
487
+ }
488
+ if (type.isNumber() || type.isNumberLiteral()) {
489
+ return { type: "number" };
490
+ }
491
+ if (type.isString()) {
492
+ return { type: "string" };
493
+ }
494
+ if (type.isStringLiteral()) {
495
+ return { type: "select", options: [type.getLiteralValue()] };
496
+ }
497
+ if (type.getCallSignatures().length > 0) {
498
+ return { type: "function" };
499
+ }
500
+ if (typeText.includes("ReactNode") || typeText.includes("ReactElement") || typeText.includes("JSX.Element") || typeText.includes("React.ReactChild") || typeText === "ReactNode" || typeText === "ReactElement") {
501
+ return { type: "node" };
502
+ }
503
+ if (type.isIntersection()) {
504
+ const intersectTypes = type.getIntersectionTypes();
505
+ for (const t of intersectTypes) {
506
+ const mapped = mapType(t, new Set(seenTypes));
507
+ if (mapped.type !== "unknown") return mapped;
508
+ }
509
+ return { type: "unknown" };
510
+ }
511
+ if (typeText === "string" && typeText.toLowerCase().includes("color")) {
512
+ return { type: "color" };
513
+ }
514
+ if (type.isUnion()) {
515
+ const unionTypes = type.getUnionTypes();
516
+ const meaningful = unionTypes.filter(
517
+ (t) => !t.isUndefined() && !t.isNull()
518
+ );
519
+ if (meaningful.length === 0) return { type: "unknown" };
520
+ if (meaningful.every((t) => t.isBoolean() || t.isBooleanLiteral())) {
521
+ return { type: "boolean" };
522
+ }
523
+ if (meaningful.every((t) => t.isStringLiteral())) {
524
+ return {
525
+ type: "select",
526
+ options: meaningful.map((t) => t.getLiteralValue())
527
+ };
528
+ }
529
+ if (meaningful.some((t) => t.isString() || t.isStringLiteral())) {
530
+ return { type: "string" };
531
+ }
532
+ return mapType(meaningful[0], seenTypes);
533
+ }
534
+ if (type.isEnum() || type.isEnumLiteral()) {
535
+ const symbol = type.getSymbol();
536
+ if (symbol) {
537
+ const decls = symbol.getDeclarations();
538
+ const options = [];
539
+ for (const decl of decls) {
540
+ if (decl.getKind() === SyntaxKind.EnumMember) {
541
+ const val = decl.getValue?.();
542
+ if (typeof val === "string") options.push(val);
543
+ else options.push(decl.getName?.() ?? "");
544
+ }
545
+ }
546
+ if (options.length > 0) return { type: "select", options };
547
+ }
548
+ return { type: "select", options: [] };
549
+ }
550
+ return { type: "unknown" };
551
+ }
552
+ function extractPropsFromCallExpr(callExpr, checker, depth = 0) {
553
+ if (depth > 6) return null;
554
+ const calleeExpr = callExpr.getExpression?.();
555
+ const calleeName = calleeExpr?.getText?.() ?? "";
556
+ const isForwardRef = calleeName === "forwardRef" || calleeName === "React.forwardRef";
557
+ const isMemo = calleeName === "memo" || calleeName === "React.memo";
558
+ if (isForwardRef) {
559
+ const typeArgs = callExpr.getTypeArguments?.();
560
+ if (typeArgs?.length >= 2) {
561
+ return checker.getTypeAtLocation(typeArgs[1]);
562
+ }
563
+ const callArgs2 = callExpr.getArguments?.();
564
+ if (callArgs2?.length > 0) {
565
+ const cb = callArgs2[0];
566
+ const cbParams = cb.getParameters?.();
567
+ if (cbParams?.length > 0) {
568
+ return checker.getTypeAtLocation(cbParams[0]);
569
+ }
570
+ if (cb.getKind?.() === SyntaxKind.CallExpression) {
571
+ return extractPropsFromCallExpr(cb, checker, depth + 1);
572
+ }
573
+ }
574
+ return null;
575
+ }
576
+ if (isMemo) {
577
+ const callArgs2 = callExpr.getArguments?.();
578
+ if (callArgs2?.length > 0) {
579
+ const inner = callArgs2[0];
580
+ const innerParams = inner.getParameters?.();
581
+ if (innerParams?.length > 0) {
582
+ return checker.getTypeAtLocation(innerParams[0]);
583
+ }
584
+ if (inner.getKind?.() === SyntaxKind.CallExpression) {
585
+ return extractPropsFromCallExpr(inner, checker, depth + 1);
586
+ }
587
+ }
588
+ return null;
589
+ }
590
+ const callArgs = callExpr.getArguments?.();
591
+ if (callArgs?.length > 0) {
592
+ for (let i = callArgs.length - 1; i >= 0; i--) {
593
+ const arg = callArgs[i];
594
+ const argParams = arg.getParameters?.();
595
+ if (argParams?.length > 0) {
596
+ return checker.getTypeAtLocation(argParams[0]);
597
+ }
598
+ if (arg.getKind?.() === SyntaxKind.CallExpression) {
599
+ const inner = extractPropsFromCallExpr(arg, checker, depth + 1);
600
+ if (inner) return inner;
601
+ }
602
+ }
603
+ }
604
+ if (calleeExpr?.getKind?.() === SyntaxKind.CallExpression) {
605
+ const innerResult = extractPropsFromCallExpr(calleeExpr, checker, depth + 1);
606
+ if (innerResult) return innerResult;
607
+ }
608
+ return null;
609
+ }
610
+ async function parseComponentProps(rootDir, relFilePath, exportName, cache) {
611
+ const absPath = join3(rootDir, relFilePath);
612
+ const cacheKey = `${relFilePath}::${exportName}`;
613
+ let mtime = 0;
614
+ try {
615
+ mtime = statSync2(absPath).mtimeMs;
616
+ } catch {
617
+ return [];
618
+ }
619
+ const cached = cache.entries.get(cacheKey);
620
+ if (cached && cached.mtime === mtime) return cached.props;
621
+ try {
622
+ const project = getProject(rootDir);
623
+ let sourceFile;
624
+ sourceFile = project.getSourceFile(absPath);
625
+ if (!sourceFile) {
626
+ sourceFile = project.addSourceFileAtPath(absPath);
627
+ } else {
628
+ sourceFile.refreshFromFileSystemSync();
629
+ }
630
+ const props = extractPropsFromFile(sourceFile, exportName, rootDir);
631
+ cache.entries.set(cacheKey, { mtime, props });
632
+ return props;
633
+ } catch {
634
+ cache.entries.set(cacheKey, { mtime, props: [] });
635
+ return [];
636
+ }
637
+ }
638
+ function extractPropsFromFile(sourceFile, exportName, rootDir) {
639
+ const checker = sourceFile.getProject().getTypeChecker();
640
+ const exportedDecls = sourceFile.getExportedDeclarations();
641
+ let componentDecl = null;
642
+ if (exportName === "default") {
643
+ const defaultExports = exportedDecls.get("default");
644
+ componentDecl = defaultExports?.[0] ?? null;
645
+ } else {
646
+ const namedExports = exportedDecls.get(exportName);
647
+ componentDecl = namedExports?.[0] ?? null;
648
+ }
649
+ if (!componentDecl) return [];
650
+ let propsType = null;
651
+ const kind = componentDecl.getKind();
652
+ if (kind === SyntaxKind.ArrowFunction || kind === SyntaxKind.FunctionExpression || kind === SyntaxKind.FunctionDeclaration) {
653
+ const params = componentDecl.getParameters?.();
654
+ if (params?.length > 0) {
655
+ propsType = checker.getTypeAtLocation(params[0]);
656
+ }
657
+ }
658
+ if (kind === SyntaxKind.VariableDeclaration) {
659
+ const init = componentDecl.getInitializer?.();
660
+ if (init) {
661
+ const initKind = init.getKind?.();
662
+ const params = init.getParameters?.();
663
+ if (params?.length > 0) {
664
+ propsType = checker.getTypeAtLocation(params[0]);
665
+ }
666
+ if (!propsType && initKind === SyntaxKind.CallExpression) {
667
+ propsType = extractPropsFromCallExpr(init, checker);
668
+ }
669
+ }
670
+ const typeNode = componentDecl.getTypeNode?.();
671
+ if (typeNode && !propsType) {
672
+ const typeArgs = typeNode.getTypeArguments?.();
673
+ if (typeArgs?.length > 0) {
674
+ propsType = checker.getTypeAtLocation(typeArgs[0]);
675
+ }
676
+ }
677
+ }
678
+ if (!propsType) return [];
679
+ const props = [];
680
+ const project = sourceFile.getProject();
681
+ const properties = propsType.getProperties();
682
+ for (const prop of properties) {
683
+ const propName = prop.getName();
684
+ if (propName.startsWith("__") || propName === "key" || propName === "ref") continue;
685
+ const declarations = prop.getDeclarations();
686
+ if (!declarations.length) continue;
687
+ const decl = declarations[0];
688
+ const propType = project.getTypeChecker().getTypeAtLocation(decl);
689
+ const { type: mappedType, options } = mapType(propType);
690
+ const finalType = mappedType === "string" && /colou?r/i.test(propName) ? "color" : mappedType;
691
+ const isOptional = decl.hasQuestionToken?.() ?? false;
692
+ const hasUndefined = propType.isUnion() ? propType.getUnionTypes().some((t) => t.isUndefined()) : propType.isUndefined();
693
+ let defaultValue = void 0;
694
+ const init = decl.getInitializer?.();
695
+ if (init) {
696
+ try {
697
+ const text = init.getText();
698
+ if (text === "true") defaultValue = true;
699
+ else if (text === "false") defaultValue = false;
700
+ else if (!isNaN(Number(text))) defaultValue = Number(text);
701
+ else if (text.startsWith('"') && text.endsWith('"') || text.startsWith("'") && text.endsWith("'")) {
702
+ defaultValue = text.slice(1, -1);
703
+ }
704
+ } catch {
705
+ }
706
+ }
707
+ let description;
708
+ try {
709
+ const jsDocs = decl.getJsDocs?.();
710
+ if (jsDocs?.length) {
711
+ description = jsDocs[0].getDescription?.()?.trim() || void 0;
712
+ }
713
+ } catch {
714
+ }
715
+ props.push({
716
+ name: propName,
717
+ type: finalType,
718
+ options,
719
+ required: !isOptional && !hasUndefined,
720
+ defaultValue,
721
+ description
722
+ });
723
+ }
724
+ return props;
725
+ }
726
+ function invalidateTypeCache(relFilePath, cache) {
727
+ for (const key of cache.entries.keys()) {
728
+ if (key.startsWith(relFilePath + "::")) {
729
+ cache.entries.delete(key);
730
+ }
731
+ }
732
+ if (_project) {
733
+ const sf = _project.getSourceFiles().find(
734
+ (f) => f.getFilePath().includes(relFilePath)
735
+ );
736
+ if (sf) {
737
+ try {
738
+ _project.removeSourceFile(sf);
739
+ } catch {
740
+ }
741
+ }
742
+ }
743
+ }
744
+
745
+ // src/server.ts
746
+ var _require = createRequire(import.meta.url);
747
+ var NODE_BUILTINS = /* @__PURE__ */ new Set([
748
+ "assert",
749
+ "async_hooks",
750
+ "buffer",
751
+ "child_process",
752
+ "cluster",
753
+ "console",
754
+ "constants",
755
+ "crypto",
756
+ "dgram",
757
+ "diagnostics_channel",
758
+ "dns",
759
+ "domain",
760
+ "events",
761
+ "fs",
762
+ "http",
763
+ "http2",
764
+ "https",
765
+ "inspector",
766
+ "module",
767
+ "net",
768
+ "os",
769
+ "path",
770
+ "perf_hooks",
771
+ "process",
772
+ "punycode",
773
+ "querystring",
774
+ "readline",
775
+ "repl",
776
+ "stream",
777
+ "string_decoder",
778
+ "sys",
779
+ "timers",
780
+ "tls",
781
+ "trace_events",
782
+ "tty",
783
+ "url",
784
+ "util",
785
+ "v8",
786
+ "vm",
787
+ "wasi",
788
+ "worker_threads",
789
+ "zlib"
790
+ ]);
791
+ var SERVER_PACKAGES = /* @__PURE__ */ new Set([
792
+ // Email
793
+ "nodemailer",
794
+ "@sendgrid/mail",
795
+ "mailgun.js",
796
+ "postmark",
797
+ "resend",
798
+ "@emailjs/nodejs",
799
+ // Auth / crypto
800
+ "bcrypt",
801
+ "bcryptjs",
802
+ "argon2",
803
+ "passport",
804
+ "passport-local",
805
+ "passport-jwt",
806
+ "passport-github",
807
+ "passport-google-oauth20",
808
+ "passport-oauth2",
809
+ "passport-saml",
810
+ "jsonwebtoken",
811
+ "jose",
812
+ "express-session",
813
+ "cookie-session",
814
+ "connect-flash",
815
+ // Database / ORM
816
+ "mongoose",
817
+ "mongodb",
818
+ "prisma",
819
+ "@prisma/client",
820
+ "sequelize",
821
+ "typeorm",
822
+ "@mikro-orm/core",
823
+ "knex",
824
+ "objection",
825
+ "drizzle-orm",
826
+ "@planetscale/database",
827
+ "pg",
828
+ "mysql",
829
+ "mysql2",
830
+ "sqlite3",
831
+ "better-sqlite3",
832
+ "mariadb",
833
+ "tedious",
834
+ "mssql",
835
+ // Redis / cache
836
+ "redis",
837
+ "ioredis",
838
+ "memcached",
839
+ "node-cache",
840
+ "cache-manager",
841
+ // Server frameworks
842
+ "express",
843
+ "fastify",
844
+ "koa",
845
+ "@hapi/hapi",
846
+ "restify",
847
+ "polka",
848
+ "micro",
849
+ "body-parser",
850
+ "cors",
851
+ "helmet",
852
+ "morgan",
853
+ "compression",
854
+ "cookie-parser",
855
+ "multer",
856
+ "express-validator",
857
+ "express-rate-limit",
858
+ "@nestjs/core",
859
+ "@nestjs/common",
860
+ "@nestjs/platform-express",
861
+ // Queue / jobs
862
+ "bull",
863
+ "bullmq",
864
+ "agenda",
865
+ "bee-queue",
866
+ "node-cron",
867
+ "cron",
868
+ // AWS
869
+ "aws-sdk",
870
+ "@aws-sdk/client-s3",
871
+ "@aws-sdk/client-ses",
872
+ "@aws-sdk/client-sns",
873
+ "@aws-sdk/client-sqs",
874
+ "@aws-sdk/client-dynamodb",
875
+ "@aws-sdk/lib-dynamodb",
876
+ // Google / Firebase
877
+ "googleapis",
878
+ "@google-cloud/storage",
879
+ "@google-cloud/firestore",
880
+ "firebase-admin",
881
+ // Logging (server)
882
+ "winston",
883
+ "pino",
884
+ "bunyan",
885
+ "log4js",
886
+ // File / image processing
887
+ "sharp",
888
+ "jimp",
889
+ "canvas",
890
+ "puppeteer",
891
+ "playwright",
892
+ "jsdom",
893
+ "cheerio",
894
+ "archiver",
895
+ "adm-zip",
896
+ "unzipper",
897
+ "pdfkit",
898
+ "exceljs",
899
+ "xlsx",
900
+ // SSH / FTP
901
+ "node-ssh",
902
+ "ssh2",
903
+ // WebSocket server (Node.js only — browser uses native WebSocket)
904
+ "ws",
905
+ "socket.io",
906
+ // Misc server
907
+ "node-forge",
908
+ "pem",
909
+ "selfsigned",
910
+ "newrelic",
911
+ "@sentry/node",
912
+ "swagger-ui-express",
913
+ "swagger-jsdoc"
914
+ ]);
915
+ var STUB_IMPORT_PREFIX = "__peekrStub__";
916
+ function isNodeBuiltin(id) {
917
+ return id.startsWith("node:") || NODE_BUILTINS.has(id);
918
+ }
919
+ function isServerPkg(id) {
920
+ const pkg2 = id.startsWith("@") ? id.split("/").slice(0, 2).join("/") : id.split("/")[0];
921
+ return SERVER_PACKAGES.has(pkg2);
922
+ }
923
+ function detectReactVersion(rootDir) {
924
+ try {
925
+ const pkg2 = JSON.parse(readFileSync2(join4(rootDir, "node_modules/react/package.json"), "utf-8"));
926
+ return parseInt(pkg2.version.split(".")[0], 10) || 18;
927
+ } catch {
928
+ return 18;
929
+ }
930
+ }
931
+ function generateStubModule(moduleId) {
932
+ return `// [peekr] stub for "${moduleId}" \u2014 server-only, unavailable in browser
933
+ const __s = (n) => {
934
+ const f = (...a) => Promise.resolve()
935
+ f.toString = () => '[peekr stub: ' + n + ']'
936
+ return new Proxy(f, {
937
+ get(_, k) {
938
+ if (k === 'then' || k === 'catch' || k === 'finally') return undefined
939
+ if (typeof k === 'symbol') return undefined
940
+ return __s(n + '.' + String(k))
941
+ },
942
+ apply: () => Promise.resolve(),
943
+ construct: () => ({ destroy: () => {}, end: () => {}, close: () => {}, emit: () => {} }),
944
+ })
945
+ }
946
+ export default __s(${JSON.stringify(moduleId)})
947
+ `;
948
+ }
949
+ function rewriteServerImports(code) {
950
+ const IMPORT_RE = /^([ \t]*)import\s+(type\s+)?((?:\*\s+as\s+\w+|\{[^}]*\}|\w+)(?:\s*,\s*\{[^}]*\})?)\s+from\s+(['"])([^'"]+)\4[ \t]*;?/gm;
951
+ const SIDE_RE = /^[ \t]*import\s+(['"])([^'"]+)\1[ \t]*;?[ \t]*$/gm;
952
+ const REEXPORT_RE = /^([ \t]*)export\s+(\*|\{[^}]*\})\s+from\s+(['"])([^'"]+)\3[ \t]*;?/gm;
953
+ const patches = [];
954
+ const stubs = /* @__PURE__ */ new Map();
955
+ let m;
956
+ const getStub = (pkg2) => {
957
+ if (!stubs.has(pkg2)) stubs.set(pkg2, `__stub${stubs.size}`);
958
+ return stubs.get(pkg2);
959
+ };
960
+ SIDE_RE.lastIndex = 0;
961
+ while ((m = SIDE_RE.exec(code)) !== null) {
962
+ if (isNodeBuiltin(m[2]) || isServerPkg(m[2])) {
963
+ patches.push({ start: m.index, end: m.index + m[0].length, text: `// [peekr] stubbed: import '${m[2]}'` });
964
+ }
965
+ }
966
+ IMPORT_RE.lastIndex = 0;
967
+ while ((m = IMPORT_RE.exec(code)) !== null) {
968
+ const [full, indent, typeOnly, clause, , pkg2] = m;
969
+ if (!isNodeBuiltin(pkg2) && !isServerPkg(pkg2)) continue;
970
+ if (typeOnly) {
971
+ patches.push({ start: m.index, end: m.index + full.length, text: `// [peekr] removed type import: '${pkg2}'` });
972
+ continue;
973
+ }
974
+ const v = getStub(pkg2);
975
+ let text;
976
+ if (clause.startsWith("*")) {
977
+ const ns = clause.match(/\*\s+as\s+(\w+)/)?.[1] ?? "_ns";
978
+ text = `${indent}const ${ns} = ${v}`;
979
+ } else if (clause.startsWith("{")) {
980
+ text = `${indent}const ${clause} = ${v}`;
981
+ } else {
982
+ const parts = clause.split(/,\s*/);
983
+ const defName = parts[0].trim();
984
+ text = `${indent}const ${defName} = ${v}`;
985
+ if (parts[1]?.trim().startsWith("{")) text += `
986
+ ${indent}const ${parts[1].trim()} = ${v}`;
987
+ }
988
+ patches.push({ start: m.index, end: m.index + full.length, text: text + " // [peekr stub]" });
989
+ }
990
+ REEXPORT_RE.lastIndex = 0;
991
+ while ((m = REEXPORT_RE.exec(code)) !== null) {
992
+ const [full, indent, clause, , pkg2] = m;
993
+ if (!isNodeBuiltin(pkg2) && !isServerPkg(pkg2)) continue;
994
+ if (clause === "*") {
995
+ patches.push({ start: m.index, end: m.index + full.length, text: `// [peekr] stubbed: export * from '${pkg2}'` });
996
+ } else {
997
+ const v = getStub(pkg2);
998
+ const names = clause.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
999
+ const lines = names.map((n) => {
1000
+ const [local, exported] = n.split(/\s+as\s+/).map((s) => s.trim());
1001
+ const exportName = exported ?? local;
1002
+ return `${indent}export const ${exportName} = ${v}.${local}`;
1003
+ });
1004
+ patches.push({ start: m.index, end: m.index + full.length, text: lines.join("\n") + " // [peekr stub]" });
1005
+ }
1006
+ }
1007
+ if (patches.length === 0) return null;
1008
+ patches.sort((a, b) => b.start - a.start);
1009
+ let out = code;
1010
+ for (const p of patches) out = out.slice(0, p.start) + p.text + out.slice(p.end);
1011
+ if (stubs.size > 0) {
1012
+ const decls = [...stubs.entries()].map(
1013
+ ([pkg2, v]) => `import ${v} from '${STUB_IMPORT_PREFIX}${encodeURIComponent(pkg2)}'`
1014
+ ).join("\n");
1015
+ out = decls + "\n" + out;
1016
+ }
1017
+ return { code: out, map: null };
1018
+ }
1019
+ function getAppDistDir() {
1020
+ const thisFileDir = new URL(".", import.meta.url).pathname;
1021
+ const candidates = [
1022
+ join4(thisFileDir, "../app-dist"),
1023
+ join4(thisFileDir, "app-dist"),
1024
+ join4(thisFileDir, "../../app/dist"),
1025
+ join4(thisFileDir, "../../../packages/app/dist")
1026
+ ];
1027
+ for (const candidate of candidates) {
1028
+ if (existsSync3(candidate)) return resolve2(candidate);
1029
+ }
1030
+ throw new Error(
1031
+ "[peekr] Cannot find the built app. Run `pnpm build:app` first or reinstall peekr-ui."
1032
+ );
1033
+ }
1034
+ function detectProjectType(rootDir) {
1035
+ let deps = {};
1036
+ try {
1037
+ const pkg2 = JSON.parse(readFileSync2(join4(rootDir, "package.json"), "utf-8"));
1038
+ deps = { ...pkg2.dependencies ?? {}, ...pkg2.devDependencies ?? {} };
1039
+ } catch {
1040
+ }
1041
+ if ("next" in deps) {
1042
+ const appLayoutExists = existsSync3(join4(rootDir, "app/layout.tsx")) || existsSync3(join4(rootDir, "app/layout.jsx")) || existsSync3(join4(rootDir, "app/layout.js")) || existsSync3(join4(rootDir, "src/app/layout.tsx")) || existsSync3(join4(rootDir, "src/app/layout.jsx"));
1043
+ if (appLayoutExists) return "next-app-router";
1044
+ const pagesAppExists = existsSync3(join4(rootDir, "pages/_app.tsx")) || existsSync3(join4(rootDir, "pages/_app.jsx")) || existsSync3(join4(rootDir, "pages/_app.js")) || existsSync3(join4(rootDir, "src/pages/_app.tsx")) || existsSync3(join4(rootDir, "src/pages/_app.jsx"));
1045
+ if (pagesAppExists) return "next-pages-router";
1046
+ return "next-app-router";
1047
+ }
1048
+ if ("react-scripts" in deps) return "cra";
1049
+ if ("@vitejs/plugin-react" in deps || "@vitejs/plugin-react-swc" in deps || "vite" in deps) {
1050
+ return "vite";
1051
+ }
1052
+ return "unknown";
1053
+ }
1054
+ function loadNextConfig(rootDir) {
1055
+ const candidates = ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"];
1056
+ for (const candidate of candidates) {
1057
+ const abs = join4(rootDir, candidate);
1058
+ if (!existsSync3(abs)) continue;
1059
+ try {
1060
+ const content = readFileSync2(abs, "utf-8");
1061
+ const vars = {};
1062
+ const envBlock = content.match(/\benv\s*:\s*\{([^}]*)\}/s);
1063
+ if (envBlock) {
1064
+ const kvRe = /['"]?(\w+)['"]?\s*:\s*['"]([^'"]*)['"]/g;
1065
+ let m;
1066
+ while ((m = kvRe.exec(envBlock[1])) !== null) {
1067
+ if (m[1] && m[2] !== void 0) vars[m[1]] = m[2];
1068
+ }
1069
+ }
1070
+ return vars;
1071
+ } catch {
1072
+ }
1073
+ break;
1074
+ }
1075
+ return {};
1076
+ }
1077
+ var CSS_CANDIDATES = [
1078
+ "src/index.css",
1079
+ "src/index.scss",
1080
+ "src/index.sass",
1081
+ "src/globals.css",
1082
+ "src/global.css",
1083
+ "src/App.css",
1084
+ // CRA default — App.js imports this
1085
+ "src/app.css",
1086
+ // lowercase variant (Linux is case-sensitive)
1087
+ "src/style.css",
1088
+ "src/styles.css",
1089
+ "src/main.css",
1090
+ "src/styles/global.css",
1091
+ "src/styles/globals.css",
1092
+ "src/styles/index.css",
1093
+ "src/styles/main.css",
1094
+ "src/app/globals.css",
1095
+ // Next.js App Router
1096
+ "src/app/global.css",
1097
+ "app/globals.css",
1098
+ // Next.js 13+ app dir
1099
+ "styles/globals.css",
1100
+ "styles/global.css",
1101
+ "index.css",
1102
+ "App.css",
1103
+ "app.css"
1104
+ ];
1105
+ function extractCssImports(filePath) {
1106
+ try {
1107
+ const content = readFileSync2(filePath, "utf-8");
1108
+ const found = [];
1109
+ const re = /import\s+['"]([^'"]+\.(?:css|scss|sass|less|styl))['"]/g;
1110
+ let m;
1111
+ while ((m = re.exec(content)) !== null) {
1112
+ found.push(m[1]);
1113
+ }
1114
+ return found;
1115
+ } catch {
1116
+ return [];
1117
+ }
1118
+ }
1119
+ function traceEntryCss(rootDir) {
1120
+ const entryPoints = [
1121
+ "src/index.js",
1122
+ "src/index.jsx",
1123
+ "src/index.ts",
1124
+ "src/index.tsx",
1125
+ "src/main.js",
1126
+ "src/main.jsx",
1127
+ "src/main.ts",
1128
+ "src/main.tsx",
1129
+ "src/App.js",
1130
+ "src/App.jsx",
1131
+ "src/App.ts",
1132
+ "src/App.tsx",
1133
+ "App.js",
1134
+ "App.jsx",
1135
+ "index.js",
1136
+ "index.jsx"
1137
+ ];
1138
+ const found = [];
1139
+ for (const entry of entryPoints) {
1140
+ const entryAbs = join4(rootDir, entry);
1141
+ if (!existsSync3(entryAbs)) continue;
1142
+ const cssImports = extractCssImports(entryAbs);
1143
+ for (const cssImport of cssImports) {
1144
+ if (!cssImport.startsWith(".")) continue;
1145
+ const resolved = resolve2(dirname2(entryAbs), cssImport);
1146
+ const relPath = relative2(rootDir, resolved);
1147
+ if (existsSync3(resolved) && !found.includes(relPath)) {
1148
+ found.push(relPath);
1149
+ }
1150
+ }
1151
+ }
1152
+ return found;
1153
+ }
1154
+ function detectGlobalCss(rootDir, extraCss = []) {
1155
+ const detected = [];
1156
+ const addIfExists = (relPath) => {
1157
+ if (!detected.includes(relPath) && existsSync3(join4(rootDir, relPath))) {
1158
+ detected.push(relPath);
1159
+ }
1160
+ };
1161
+ for (const candidate of CSS_CANDIDATES) addIfExists(candidate);
1162
+ for (const relPath of traceEntryCss(rootDir)) addIfExists(relPath);
1163
+ for (const css of extraCss) addIfExists(css);
1164
+ return detected;
1165
+ }
1166
+ var ROOT_LAYOUT_CANDIDATES = [
1167
+ { path: "app/layout.tsx", type: "layout" },
1168
+ { path: "app/layout.jsx", type: "layout" },
1169
+ { path: "app/layout.js", type: "layout" },
1170
+ { path: "src/app/layout.tsx", type: "layout" },
1171
+ { path: "src/app/layout.jsx", type: "layout" },
1172
+ { path: "src/app/layout.js", type: "layout" },
1173
+ { path: "pages/_app.tsx", type: "pages-app" },
1174
+ { path: "pages/_app.jsx", type: "pages-app" },
1175
+ { path: "pages/_app.js", type: "pages-app" },
1176
+ { path: "src/pages/_app.tsx", type: "pages-app" },
1177
+ { path: "src/pages/_app.jsx", type: "pages-app" },
1178
+ { path: "src/pages/_app.js", type: "pages-app" },
1179
+ // CRA / Vite style — detected last so Next.js files take priority
1180
+ { path: "src/App.tsx", type: "app" },
1181
+ { path: "src/App.jsx", type: "app" },
1182
+ { path: "src/App.js", type: "app" },
1183
+ { path: "App.tsx", type: "app" },
1184
+ { path: "App.jsx", type: "app" },
1185
+ { path: "App.js", type: "app" }
1186
+ ];
1187
+ var SERVER_ONLY_PATTERNS = [
1188
+ /import\s+['"]server-only['"]/,
1189
+ /from\s+['"]next\/headers['"]/,
1190
+ /from\s+['"]next\/cookies['"]/,
1191
+ /from\s+['"]next\/font\//,
1192
+ /from\s+['"]next\/server['"]/,
1193
+ /from\s+['"]next\/navigation['"]/
1194
+ // useRouter / redirect / notFound are server-side in App Router
1195
+ ];
1196
+ function detectRootLayout(rootDir, projectType = "unknown") {
1197
+ const isNext = projectType === "next-app-router" || projectType === "next-pages-router";
1198
+ const candidates = ROOT_LAYOUT_CANDIDATES.filter((c) => {
1199
+ if (projectType === "next-app-router") return c.type === "layout";
1200
+ if (projectType === "next-pages-router") return c.type === "pages-app";
1201
+ if (projectType === "cra" || projectType === "vite") return c.type === "app";
1202
+ return true;
1203
+ });
1204
+ const toTry = candidates.length > 0 ? candidates : ROOT_LAYOUT_CANDIDATES;
1205
+ for (const candidate of toTry) {
1206
+ const absPath = join4(rootDir, candidate.path);
1207
+ if (!existsSync3(absPath)) continue;
1208
+ try {
1209
+ const content = readFileSync2(absPath, "utf-8");
1210
+ if (SERVER_ONLY_PATTERNS.some((re) => re.test(content))) {
1211
+ continue;
1212
+ }
1213
+ if (!/export\s+default/.test(content)) continue;
1214
+ } catch {
1215
+ continue;
1216
+ }
1217
+ return { path: candidate.path, type: candidate.type };
1218
+ }
1219
+ if (isNext) {
1220
+ const providerCandidates = [
1221
+ "src/providers.tsx",
1222
+ "src/providers.jsx",
1223
+ "app/providers.tsx",
1224
+ "app/providers.jsx",
1225
+ "src/app/providers.tsx",
1226
+ "src/app/providers.jsx"
1227
+ ];
1228
+ for (const relPath of providerCandidates) {
1229
+ const absPath = join4(rootDir, relPath);
1230
+ if (!existsSync3(absPath)) continue;
1231
+ try {
1232
+ const content = readFileSync2(absPath, "utf-8");
1233
+ if (SERVER_ONLY_PATTERNS.some((re) => re.test(content))) continue;
1234
+ if (!/export\s+default/.test(content)) continue;
1235
+ return { path: relPath, type: "app" };
1236
+ } catch {
1237
+ continue;
1238
+ }
1239
+ }
1240
+ }
1241
+ return null;
1242
+ }
1243
+ function extractHtmlLinkTags(rootDir) {
1244
+ const candidates = ["public/index.html", "index.html"];
1245
+ for (const candidate of candidates) {
1246
+ const absPath = join4(rootDir, candidate);
1247
+ if (!existsSync3(absPath)) continue;
1248
+ try {
1249
+ const html = readFileSync2(absPath, "utf-8");
1250
+ const tags = [];
1251
+ const linkRe = /<link\b[^>]*\brel=["']stylesheet["'][^>]*>/gi;
1252
+ let m;
1253
+ while ((m = linkRe.exec(html)) !== null) {
1254
+ if (/href=["'](https?:)?\/\//i.test(m[0])) {
1255
+ tags.push(" " + m[0].trim());
1256
+ }
1257
+ }
1258
+ const preRe = /<link\b[^>]*\brel=["'](preconnect|preload)["'][^>]*>/gi;
1259
+ while ((m = preRe.exec(html)) !== null) {
1260
+ if (/href=["'](https?:)?\/\//i.test(m[0])) {
1261
+ tags.push(" " + m[0].trim());
1262
+ }
1263
+ }
1264
+ const styleRe = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
1265
+ while ((m = styleRe.exec(html)) !== null) {
1266
+ const content = m[1].trim();
1267
+ if (content) tags.push(` <style>${content}</style>`);
1268
+ }
1269
+ if (tags.length > 0) return tags.join("\n") + "\n";
1270
+ } catch {
1271
+ }
1272
+ }
1273
+ return "";
1274
+ }
1275
+ function loadDotEnv(rootDir) {
1276
+ const envFiles = [".env", ".env.development", ".env.local", ".env.development.local"];
1277
+ const vars = { NODE_ENV: "development" };
1278
+ for (const f of envFiles) {
1279
+ const abs = join4(rootDir, f);
1280
+ if (!existsSync3(abs)) continue;
1281
+ try {
1282
+ for (const line of readFileSync2(abs, "utf-8").split("\n")) {
1283
+ const t = line.trim();
1284
+ if (!t || t.startsWith("#")) continue;
1285
+ const eq = t.indexOf("=");
1286
+ if (eq === -1) continue;
1287
+ const key = t.slice(0, eq).trim();
1288
+ let val = t.slice(eq + 1).trim();
1289
+ if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) val = val.slice(1, -1);
1290
+ if (key) vars[key] = val;
1291
+ }
1292
+ } catch {
1293
+ }
1294
+ }
1295
+ return vars;
1296
+ }
1297
+ var jsxInJsPlugin = {
1298
+ name: "peekr:jsx-in-js",
1299
+ async transform(code, id) {
1300
+ if (id.includes("node_modules")) return null;
1301
+ if (!id.endsWith(".js")) return null;
1302
+ try {
1303
+ const needsReact = !/import\s+(React|\*\s+as\s+React)\b/.test(code);
1304
+ const src = needsReact ? `import React from 'react'
1305
+ ${code}` : code;
1306
+ return await transformWithEsbuild(src, id, {
1307
+ loader: "jsx",
1308
+ jsx: "transform"
1309
+ // classic: compiles to React.createElement(...)
1310
+ });
1311
+ } catch {
1312
+ return null;
1313
+ }
1314
+ }
1315
+ };
1316
+ var autoReactImportPlugin = {
1317
+ name: "peekr:auto-react-import",
1318
+ enforce: "pre",
1319
+ transform(code, id) {
1320
+ if (id.includes("node_modules")) return null;
1321
+ if (!/\.[jt]sx$/.test(id)) return null;
1322
+ if (/import\s+(React|\*\s+as\s+React)\b/.test(code)) return null;
1323
+ return `import React from 'react'
1324
+ ${code}`;
1325
+ }
1326
+ };
1327
+ function createStubPlugin() {
1328
+ return {
1329
+ name: "peekr:stubs",
1330
+ enforce: "pre",
1331
+ resolveId(id) {
1332
+ if (id.startsWith(STUB_IMPORT_PREFIX)) {
1333
+ const pkg2 = decodeURIComponent(id.slice(STUB_IMPORT_PREFIX.length));
1334
+ return `\0peekr:stub:${pkg2}`;
1335
+ }
1336
+ if (isNodeBuiltin(id)) return `\0peekr:stub:${id}`;
1337
+ if (isServerPkg(id)) return `\0peekr:stub:${id}`;
1338
+ },
1339
+ load(id) {
1340
+ if (id.startsWith("\0peekr:stub:")) {
1341
+ return generateStubModule(id.slice("\0peekr:stub:".length));
1342
+ }
1343
+ },
1344
+ transform(code, id) {
1345
+ if (id.includes("node_modules") || id.startsWith("\0") || !/\.[jt]sx?$/.test(id)) return null;
1346
+ if (!code.includes("from '") && !code.includes('from "')) return null;
1347
+ const hasCandidate = /from\s+['"](?:node:|fs|path|os|crypto|http|https|net|child_process|stream|events|buffer|util|url|tls|zlib|worker_threads|nodemailer|bcrypt|mongoose|prisma|sequelize|typeorm|express|passport|jsonwebtoken|bull|redis|ioredis|aws-sdk|@aws-sdk|googleapis|firebase-admin|winston|pino|puppeteer|sharp|pg['"\/]|mysql|sqlite|ws['"]|\@prisma|\@nestjs|\@hapi|\@google-cloud)/.test(code);
1348
+ if (!hasCandidate) return null;
1349
+ return rewriteServerImports(code);
1350
+ }
1351
+ };
1352
+ }
1353
+ var VIRTUAL_PREFIX = "virtual:peekr-frame";
1354
+ var RESOLVED_PREFIX = "\0virtual:peekr-frame";
1355
+ var FRAME_SHARED_JS = `
1356
+ class __PeekrErrorBoundary extends React.Component {
1357
+ constructor(props) { super(props); this.state = { error: null } }
1358
+ static getDerivedStateFromError(e) { return { error: e } }
1359
+ componentDidCatch(error, info) {
1360
+ window.parent.postMessage({
1361
+ type: 'RENDER_ERROR', message: error.message,
1362
+ stack: error.stack, componentStack: info?.componentStack,
1363
+ }, '*')
1364
+ }
1365
+ render() {
1366
+ if (this.state.error) {
1367
+ return React.createElement('div', {
1368
+ style: {
1369
+ padding: '20px', fontFamily: 'ui-monospace, monospace', color: '#dc2626',
1370
+ background: '#fef2f2', borderRadius: '8px', margin: '16px',
1371
+ fontSize: '13px', lineHeight: '1.6', border: '1px solid #fecaca',
1372
+ }
1373
+ },
1374
+ React.createElement('div', { style: { fontWeight: 600, marginBottom: 8, fontSize: 14 } }, '\u26A0 Component Error'),
1375
+ React.createElement('pre', { style: { margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, this.state.error.message)
1376
+ )
1377
+ }
1378
+ return this.props.children
1379
+ }
1380
+ }
1381
+ function __resolveComponent(mod, exportName) {
1382
+ if (exportName === 'default') {
1383
+ return mod.default
1384
+ ?? Object.values(mod).find(v => typeof v === 'function' && /^[A-Z]/.test((v.displayName ?? v.name) ?? ''))
1385
+ }
1386
+ return mod[exportName] ?? mod.default
1387
+ }
1388
+ function __applyTheme(theme) {
1389
+ document.documentElement.className = theme === 'dark' ? 'dark' : ''
1390
+ document.body.style.background = theme === 'dark' ? '#0f172a' : 'transparent'
1391
+ }
1392
+ // Props sent via postMessage use structuredClone \u2014 functions can't be cloned.
1393
+ // Sender marks function values as { __peekrFn: true, name }; we reconstruct them here.
1394
+ function __hydrateProps(raw) {
1395
+ if (!raw || typeof raw !== 'object') return {}
1396
+ const out = {}
1397
+ for (const [k, v] of Object.entries(raw)) {
1398
+ if (v && typeof v === 'object' && v.__peekrFn === true) {
1399
+ const n = v.name || k
1400
+ out[k] = function() { console.log('[peekr] ' + n + '() called') }
1401
+ } else {
1402
+ out[k] = v
1403
+ }
1404
+ }
1405
+ return out
1406
+ }
1407
+ // NOTE: __installContextStub is intentionally NOT included here.
1408
+ // Full context entries have real providers (AuthProvider etc.) so they don't
1409
+ // need stubs. Including the stub in full context mode breaks react-router-dom:
1410
+ // Router.useInRouterContext() calls useContext(NavigationContext) expecting null
1411
+ // (meaning "no router above me yet"), but the stub returns a truthy Proxy,
1412
+ // causing "cannot render a <Router> inside another <Router>".
1413
+ // The stub is injected only into isolated entries \u2014 see generateIsolatedEntry.
1414
+
1415
+ // Mock API mode \u2014 intercepts network primitives to return empty responses.
1416
+ // Prevents network errors from crashing component previews when real APIs
1417
+ // are unavailable (auth required, CORS, no network, etc.).
1418
+ //
1419
+ // Covered:
1420
+ // fetch \u2192 React Query v5, SWR, native fetch
1421
+ // XMLHttpRequest \u2192 Axios (XHR adapter), jQuery.ajax
1422
+ // WebSocket \u2192 socket.io, ws-based real-time queries
1423
+ // EventSource \u2192 SSE-based streaming / server-sent events
1424
+ //
1425
+ // Activated by SET_MOCK_MODE postMessage from the parent toolbar toggle.
1426
+ // Vite HMR and peekr internal requests are always passed through.
1427
+ let __mockActive = false
1428
+ let __origFetch = null
1429
+ let __origXHR = null
1430
+ let __origWS = null
1431
+ let __origES = null
1432
+ function __installMockApi() {
1433
+ if (__mockActive) return
1434
+ __mockActive = true
1435
+
1436
+ // fetch mock
1437
+ __origFetch = window.fetch
1438
+ window.fetch = function(input) {
1439
+ const url = typeof input === 'string' ? input
1440
+ : (input instanceof Request) ? input.url : String(input)
1441
+ if (url.includes('/__peek/') || url.includes('@vite') || url.includes('?v=')) {
1442
+ return __origFetch.apply(window, arguments)
1443
+ }
1444
+ return Promise.resolve(new Response('{}', {
1445
+ status: 200, headers: { 'Content-Type': 'application/json' },
1446
+ }))
1447
+ }
1448
+
1449
+ // XMLHttpRequest mock
1450
+ __origXHR = window.XMLHttpRequest
1451
+ function _MockXHR() { this.readyState = 0; this.status = 0 }
1452
+ _MockXHR.prototype = {
1453
+ open: function(m, url) { this._url = url },
1454
+ send: function() {
1455
+ const self = this
1456
+ setTimeout(function() {
1457
+ self.readyState = 4; self.status = 200; self.statusText = 'OK'
1458
+ self.response = '{}'; self.responseText = '{}'
1459
+ self.onreadystatechange && self.onreadystatechange.call(self)
1460
+ self.onload && self.onload.call(self)
1461
+ }, 0)
1462
+ },
1463
+ setRequestHeader: function() {},
1464
+ getResponseHeader: function() { return 'application/json' },
1465
+ getAllResponseHeaders: function() { return 'content-type: application/json\\r\\n' },
1466
+ abort: function() {},
1467
+ }
1468
+ window.XMLHttpRequest = _MockXHR
1469
+
1470
+ // WebSocket mock
1471
+ if (window.WebSocket) {
1472
+ __origWS = window.WebSocket
1473
+ function _MockWS(url) {
1474
+ this.readyState = 0 // CONNECTING
1475
+ this.url = url
1476
+ const self = this
1477
+ setTimeout(function() {
1478
+ self.readyState = 1 // OPEN
1479
+ self.onopen && self.onopen({ type: 'open' })
1480
+ }, 0)
1481
+ }
1482
+ _MockWS.CONNECTING = 0; _MockWS.OPEN = 1; _MockWS.CLOSING = 2; _MockWS.CLOSED = 3
1483
+ _MockWS.prototype = {
1484
+ send: function() { /* silently drop */ },
1485
+ close: function() {
1486
+ this.readyState = 3
1487
+ this.onclose && this.onclose({ type: 'close', code: 1000, reason: 'mock', wasClean: true })
1488
+ },
1489
+ addEventListener: function(type, fn) { this['on' + type] = fn },
1490
+ removeEventListener: function() {},
1491
+ }
1492
+ window.WebSocket = _MockWS
1493
+ }
1494
+
1495
+ // EventSource mock
1496
+ if (window.EventSource) {
1497
+ __origES = window.EventSource
1498
+ function _MockES(url) {
1499
+ this.readyState = 0 // CONNECTING
1500
+ this.url = url
1501
+ const self = this
1502
+ setTimeout(function() {
1503
+ self.readyState = 1 // OPEN
1504
+ self.onopen && self.onopen({ type: 'open' })
1505
+ // Immediately close after opening \u2014 no messages sent
1506
+ setTimeout(function() {
1507
+ self.readyState = 2 // CLOSED
1508
+ self.onclose && self.onclose({ type: 'close' })
1509
+ }, 50)
1510
+ }, 0)
1511
+ }
1512
+ _MockES.CONNECTING = 0; _MockES.OPEN = 1; _MockES.CLOSED = 2
1513
+ _MockES.prototype = {
1514
+ close: function() { this.readyState = 2 },
1515
+ addEventListener: function(type, fn) { this['on' + type] = fn },
1516
+ removeEventListener: function() {},
1517
+ dispatchEvent: function() { return true },
1518
+ }
1519
+ window.EventSource = _MockES
1520
+ }
1521
+ }
1522
+ function __uninstallMockApi() {
1523
+ if (!__mockActive) return
1524
+ __mockActive = false
1525
+ if (__origFetch) { window.fetch = __origFetch; __origFetch = null }
1526
+ if (__origXHR) { window.XMLHttpRequest = __origXHR; __origXHR = null }
1527
+ if (__origWS) { window.WebSocket = __origWS; __origWS = null }
1528
+ if (__origES) { window.EventSource = __origES; __origES = null }
1529
+ }
1530
+ `;
1531
+ function generateIsolatedEntry(file, exportName, hasReactRouter, reactMajor) {
1532
+ const absFile = file.startsWith("/") ? file : `/${file}`;
1533
+ const safeFile = JSON.stringify(absFile);
1534
+ const safeExport = JSON.stringify(exportName);
1535
+ const reactDomImport = reactMajor >= 18 ? `import { createRoot } from 'react-dom/client'` : `import ReactDOM from 'react-dom'`;
1536
+ const rootInit = reactMajor >= 18 ? `let __root = null
1537
+ function __getRoot() {
1538
+ if (!__root) { const el = document.getElementById('peek-root'); if (el) __root = createRoot(el) }
1539
+ return __root
1540
+ }` : `function __getRoot() { return document.getElementById('peek-root') }`;
1541
+ const renderFn = reactMajor >= 18 ? ` const root = __getRoot()
1542
+ if (!root) { window.parent.postMessage({ type: 'RENDER_ERROR', message: '#peek-root not found.' }, '*'); return }
1543
+ root.render(` : ` const el = __getRoot()
1544
+ if (!el) { window.parent.postMessage({ type: 'RENDER_ERROR', message: '#peek-root not found.' }, '*'); return }
1545
+ ReactDOM.render(`;
1546
+ const renderClose = reactMajor >= 18 ? `)` : `, el)`;
1547
+ const contextStub = `
1548
+ ;(function __installContextStub() {
1549
+ const __origUseContext = React.useContext
1550
+ function __ctxStub() {
1551
+ const fn = function __peekrStub() { return __ctxStub() }
1552
+ const _str = { toLowerCase: () => '', toUpperCase: () => '', trim: () => '',
1553
+ replace: () => '', slice: () => '', split: () => [], includes: () => false,
1554
+ startsWith: () => false, endsWith: () => false, toString: () => '', valueOf: () => null }
1555
+ Object.assign(fn, _str) // NOTE: fn.length is read-only on functions, skip it
1556
+ return new Proxy(fn, {
1557
+ get(t, k) {
1558
+ if (typeof k === 'symbol') return t[k]
1559
+ if (k in t) return t[k]
1560
+ return __ctxStub()
1561
+ },
1562
+ apply() { return __ctxStub() },
1563
+ has() { return true },
1564
+ set() { return true },
1565
+ })
1566
+ }
1567
+ React.useContext = function(ctx) {
1568
+ const val = __origUseContext.call(this, ctx)
1569
+ if (val !== null && val !== undefined) return val
1570
+ return __ctxStub()
1571
+ }
1572
+ })()
1573
+ `;
1574
+ const routerSetup = hasReactRouter ? `
1575
+ import { MemoryRouter as __MemoryRouter } from 'react-router-dom'
1576
+ let __wrapWithRouter = false
1577
+ // __RouterBoundary: catches errors thrown by components that need a Router context
1578
+ // but don't have one. After catching, adds MemoryRouter and re-renders once.
1579
+ // Detects router-context errors by message patterns OR by the common symptom of
1580
+ // react-router-dom trying to call string methods on undefined context values
1581
+ // (e.g. "Cannot read properties of undefined (reading 'toLowerCase')").
1582
+ class __RouterBoundary extends React.Component {
1583
+ constructor(p) { super(p); this.state = { err: null } }
1584
+ static getDerivedStateFromError(err) { return { err } }
1585
+ componentDidUpdate(_, prevState) {
1586
+ if (this.state.err && !prevState.err) {
1587
+ const msg = (this.state.err.message || '') + (this.state.err.stack || '')
1588
+ const isOutsideRouter = /useNavigate|useLocation|useParams|useMatch|useHref|<Link>|<NavLink>|outside.*Router|may be used only in the context|toLowerCase|Cannot read.*location|Cannot read.*pathname/i.test(msg)
1589
+ if (isOutsideRouter && !__wrapWithRouter) {
1590
+ __wrapWithRouter = true
1591
+ setTimeout(() => { this.setState({ err: null }); __render(__mod, __currentProps, __currentTheme) }, 0)
1592
+ }
1593
+ }
1594
+ }
1595
+ render() {
1596
+ if (this.state.err) {
1597
+ const msg = (this.state.err.message || '') + (this.state.err.stack || '')
1598
+ const isOutsideRouter = /useNavigate|useLocation|useParams|useMatch|useHref|<Link>|<NavLink>|outside.*Router|may be used only in the context|toLowerCase|Cannot read.*location|Cannot read.*pathname/i.test(msg)
1599
+ if (isOutsideRouter) return null
1600
+ throw this.state.err
1601
+ }
1602
+ return this.props.children
1603
+ }
1604
+ }` : "";
1605
+ return `
1606
+ import React from 'react'
1607
+ ${reactDomImport}
1608
+ ${routerSetup}
1609
+ ${FRAME_SHARED_JS}
1610
+ ${contextStub}
1611
+ let __currentProps = {}
1612
+ let __currentTheme = 'light'
1613
+ let __mod = null
1614
+ let __renderScheduled = false
1615
+
1616
+ ${rootInit}
1617
+ function __render(mod, props, theme) {
1618
+ if (!mod) return
1619
+ const Component = __resolveComponent(mod, ${safeExport})
1620
+ if (!Component) {
1621
+ window.parent.postMessage({ type: 'RENDER_ERROR',
1622
+ message: 'Export "' + ${safeExport} + '" not found in ' + ${safeFile}, }, '*')
1623
+ return
1624
+ }
1625
+ __applyTheme(theme)
1626
+ try {
1627
+ const inner = React.createElement(__PeekrErrorBoundary, { key: ${safeFile} + ':' + ${safeExport} },
1628
+ ${hasReactRouter ? `React.createElement(__RouterBoundary, null, React.createElement(Component, props))` : `React.createElement(Component, props)`})
1629
+ ${renderFn}${hasReactRouter ? `__wrapWithRouter ? React.createElement(__MemoryRouter, { initialEntries: ['/'] }, inner) : inner` : `inner`}${renderClose}
1630
+ requestAnimationFrame(() => { window.parent.postMessage({ type: 'RENDER_SUCCESS' }, '*') })
1631
+ } catch (err) {
1632
+ window.parent.postMessage({ type: 'RENDER_ERROR', message: err.message, stack: err.stack }, '*')
1633
+ }
1634
+ }
1635
+ function __scheduleRender(mod, props, theme) {
1636
+ if (__renderScheduled) return
1637
+ __renderScheduled = true
1638
+ requestAnimationFrame(() => { __renderScheduled = false; __render(mod, props, theme) })
1639
+ }
1640
+ window.addEventListener('message', (e) => {
1641
+ if (e.source !== window.parent) return
1642
+ const { type } = e.data
1643
+ if (type === 'UPDATE_PROPS') { __currentProps = __hydrateProps(e.data.props); if (__mod) __scheduleRender(__mod, __currentProps, __currentTheme) }
1644
+ if (type === 'SET_THEME') { __currentTheme = e.data.theme; __applyTheme(__currentTheme); if (__mod) __scheduleRender(__mod, __currentProps, __currentTheme) }
1645
+ if (type === 'SET_MOCK_MODE') { e.data.enabled ? __installMockApi() : __uninstallMockApi() }
1646
+ })
1647
+ if (import.meta.hot) {
1648
+ import.meta.hot.accept(${safeFile}, (newMod) => { if (newMod) { __mod = newMod; __scheduleRender(__mod, __currentProps, __currentTheme) } })
1649
+ import.meta.hot.on('vite:afterUpdate', () => { if (__mod) __scheduleRender(__mod, __currentProps, __currentTheme) })
1650
+ }
1651
+ // Dynamically import the component so Vite doesn't eagerly transform it.
1652
+ // If the component or any of its imports are broken/missing, the error is
1653
+ // caught here and reported as RENDER_ERROR \u2014 the frame still loads cleanly.
1654
+ import(${safeFile}).then((mod) => {
1655
+ __mod = mod
1656
+ __render(__mod, __currentProps, __currentTheme)
1657
+ window.parent.postMessage({ type: 'FRAME_READY' }, '*')
1658
+ }).catch((err) => {
1659
+ window.parent.postMessage({ type: 'RENDER_ERROR', message: err.message, stack: err.stack }, '*')
1660
+ window.parent.postMessage({ type: 'FRAME_READY' }, '*')
1661
+ })
1662
+ `;
1663
+ }
1664
+ function generateFullContextEntry(file, exportName, rootLayout, reactMajor) {
1665
+ const absFile = file.startsWith("/") ? file : `/${file}`;
1666
+ const safeFile = JSON.stringify(absFile);
1667
+ const safeExport = JSON.stringify(exportName);
1668
+ const safeRootPath = JSON.stringify(rootLayout.path.startsWith("/") ? rootLayout.path : `/${rootLayout.path}`);
1669
+ const safeRootType = JSON.stringify(rootLayout.type);
1670
+ const reactDomImportFC = reactMajor >= 18 ? `import { createRoot } from 'react-dom/client'` : `import ReactDOM from 'react-dom'`;
1671
+ const mountRoot = reactMajor >= 18 ? `const root = createRoot(rootEl)
1672
+ // root.render called below
1673
+ ` : ``;
1674
+ const rootRender = reactMajor >= 18 ? `root.render(content)` : `ReactDOM.render(content, rootEl)`;
1675
+ let contentInitCode;
1676
+ if (rootLayout.type === "pages-app") {
1677
+ contentInitCode = `
1678
+ content = React.createElement(__PeekrErrorBoundary, null,
1679
+ React.createElement(RootLayout, { Component: __ComponentSlot, pageProps: {} }))`;
1680
+ } else if (rootLayout.type === "app") {
1681
+ contentInitCode = `
1682
+ let __MR = null
1683
+ const __rtrRefs = new Set()
1684
+ try {
1685
+ const __rrd = await import(/* @vite-ignore */ 'react-router-dom')
1686
+ __MR = __rrd.MemoryRouter
1687
+ for (const k of ['RouterProvider','BrowserRouter','HashRouter','StaticRouter','NativeRouter']) {
1688
+ if (__rrd[k]) __rtrRefs.add(__rrd[k])
1689
+ }
1690
+ } catch(e) {}
1691
+ const __origCE = React.createElement.bind(React)
1692
+ if (__MR && __rtrRefs.size > 0) {
1693
+ React.createElement = function(type) {
1694
+ if (__rtrRefs.has(type)) {
1695
+ return __origCE(__MR, { initialEntries: ['/'] }, __origCE(__ComponentSlot, null))
1696
+ }
1697
+ return __origCE.apply(null, arguments)
1698
+ }
1699
+ }
1700
+ content = React.createElement(__PeekrErrorBoundary, null, React.createElement(RootLayout))`;
1701
+ } else {
1702
+ contentInitCode = `
1703
+ content = React.createElement(__PeekrErrorBoundary, null,
1704
+ React.createElement(RootLayout, { params: {}, searchParams: {} },
1705
+ React.createElement(__ComponentSlot)))`;
1706
+ }
1707
+ return `
1708
+ import React from 'react'
1709
+ ${reactDomImportFC}
1710
+ ${FRAME_SHARED_JS}
1711
+ let __mod = null
1712
+ let __currentProps = {}
1713
+ let __currentTheme = 'light'
1714
+ // Slot updater \u2014 set by ComponentSlot's useEffect after mount
1715
+ let __setSlotProps = null
1716
+
1717
+ // ComponentSlot: the injection point inside the real provider tree.
1718
+ // Only this function re-renders on prop / module change \u2014 root never remounts.
1719
+ function __ComponentSlot() {
1720
+ const [slotProps, setSlotProps] = React.useState(__currentProps)
1721
+ React.useEffect(() => {
1722
+ __setSlotProps = setSlotProps
1723
+ return () => { if (__setSlotProps === setSlotProps) __setSlotProps = null }
1724
+ }, [setSlotProps])
1725
+ if (!__mod) return null
1726
+ const Component = __resolveComponent(__mod, ${safeExport})
1727
+ if (!Component) {
1728
+ return React.createElement('div', {
1729
+ style: { padding: 20, color: '#dc2626', fontFamily: 'ui-monospace, monospace', fontSize: 13 }
1730
+ }, 'Export not found: ' + ${safeExport})
1731
+ }
1732
+ return React.createElement(Component, slotProps)
1733
+ }
1734
+
1735
+ async function __initRoot() {
1736
+ __applyTheme(__currentTheme)
1737
+ const rootEl = document.getElementById('peek-root')
1738
+ if (!rootEl) {
1739
+ window.parent.postMessage({ type: 'RENDER_ERROR', message: '#peek-root not found.' }, '*')
1740
+ return
1741
+ }
1742
+ // Load the component module first. Any import errors here are fatal for this frame.
1743
+ try {
1744
+ __mod = await import(${safeFile})
1745
+ } catch (err) {
1746
+ window.parent.postMessage({ type: 'RENDER_ERROR', message: err instanceof Error ? err.message : String(err), stack: err instanceof Error ? err.stack : undefined }, '*')
1747
+ window.parent.postMessage({ type: 'FRAME_READY' }, '*')
1748
+ return
1749
+ }
1750
+ ${mountRoot}let content
1751
+ try {
1752
+ // @vite-ignore prevents Vite from eagerly pre-analyzing this import.
1753
+ // The layout may import packages that are not installed or server-only;
1754
+ // any failure becomes a runtime error caught below \u2192 FULLCONTEXT_FALLBACK.
1755
+ // A 5-second timeout guards against imports that hang indefinitely.
1756
+ const __layoutTimeout = new Promise((_, reject) =>
1757
+ setTimeout(() => reject(new Error('Root layout import timed out (5s). Use Isolated mode or add \\'use client\\' to your layout.')), 5000)
1758
+ )
1759
+ const rootMod = await Promise.race([import(/* @vite-ignore */ ${safeRootPath}), __layoutTimeout])
1760
+ const RootLayout = rootMod.default
1761
+ if (typeof RootLayout !== 'function') {
1762
+ throw new Error('Root layout ' + ${safeRootPath} + ' has no default function export.')
1763
+ }
1764
+ ${contentInitCode}
1765
+ window.parent.postMessage({ type: 'FULLCONTEXT_READY', rootPath: ${safeRootPath} }, '*')
1766
+ } catch (err) {
1767
+ // Root import failed (server-only imports, missing deps, etc.) \u2192 fall back gracefully
1768
+ content = React.createElement(__PeekrErrorBoundary, null, React.createElement(__ComponentSlot))
1769
+ window.parent.postMessage({
1770
+ type: 'FULLCONTEXT_FALLBACK',
1771
+ reason: err instanceof Error ? err.message : String(err),
1772
+ }, '*')
1773
+ }
1774
+ ${rootRender}
1775
+ requestAnimationFrame(() => { window.parent.postMessage({ type: 'RENDER_SUCCESS' }, '*') })
1776
+ window.parent.postMessage({ type: 'FRAME_READY' }, '*')
1777
+ }
1778
+
1779
+ window.addEventListener('message', (e) => {
1780
+ if (e.source !== window.parent) return
1781
+ const { type } = e.data
1782
+ if (type === 'UPDATE_PROPS') {
1783
+ __currentProps = __hydrateProps(e.data.props)
1784
+ // Update slot ONLY \u2014 root provider tree stays mounted
1785
+ if (__setSlotProps) __setSlotProps(__currentProps)
1786
+ }
1787
+ if (type === 'SET_THEME') { __applyTheme(e.data.theme) }
1788
+ if (type === 'SET_MOCK_MODE') { e.data.enabled ? __installMockApi() : __uninstallMockApi() }
1789
+ })
1790
+
1791
+ if (import.meta.hot) {
1792
+ import.meta.hot.accept(${safeFile}, (newMod) => {
1793
+ if (newMod) {
1794
+ __mod = newMod
1795
+ // Trigger slot re-render with new module (same props, new component)
1796
+ if (__setSlotProps) __setSlotProps(p => ({ ...p }))
1797
+ }
1798
+ })
1799
+ import.meta.hot.on('vite:afterUpdate', () => {
1800
+ if (__setSlotProps) __setSlotProps(p => ({ ...p }))
1801
+ })
1802
+ }
1803
+
1804
+ __initRoot()
1805
+ `;
1806
+ }
1807
+ function generateFrameEntry(file, exportName, fullContext = false, rootLayout = null, hasReactRouter = false, reactMajor = 18) {
1808
+ if (fullContext && rootLayout) {
1809
+ return generateFullContextEntry(file, exportName, rootLayout, reactMajor);
1810
+ }
1811
+ return generateIsolatedEntry(file, exportName, hasReactRouter, reactMajor);
1812
+ }
1813
+ async function createPeekrServer(rootDir, config) {
1814
+ const scanCache = { files: /* @__PURE__ */ new Map(), barrels: /* @__PURE__ */ new Map(), all: [] };
1815
+ const typeCache = { entries: /* @__PURE__ */ new Map() };
1816
+ let projectType = detectProjectType(rootDir);
1817
+ const reactMajor = detectReactVersion(rootDir);
1818
+ await scanComponents(rootDir, scanCache, config.exclude);
1819
+ let globalCssFiles = detectGlobalCss(rootDir, config.globalCss ?? []);
1820
+ let rootLayout = detectRootLayout(rootDir, projectType);
1821
+ const dotEnvVars = loadDotEnv(rootDir);
1822
+ if (projectType === "next-app-router" || projectType === "next-pages-router") {
1823
+ const nextConfigVars = loadNextConfig(rootDir);
1824
+ Object.assign(dotEnvVars, nextConfigVars);
1825
+ }
1826
+ const hasReactRouter = existsSync3(join4(rootDir, "node_modules/react-router-dom"));
1827
+ const htmlCdnLinks = extractHtmlLinkTags(rootDir);
1828
+ let appDistDir;
1829
+ try {
1830
+ appDistDir = getAppDistDir();
1831
+ } catch (e) {
1832
+ console.warn(e.message);
1833
+ appDistDir = "";
1834
+ }
1835
+ let appIndexHtml = "";
1836
+ if (appDistDir) {
1837
+ const indexPath = join4(appDistDir, "index.html");
1838
+ if (existsSync3(indexPath)) {
1839
+ appIndexHtml = readFileSync2(indexPath, "utf-8");
1840
+ }
1841
+ }
1842
+ const peekrPlugin = {
1843
+ name: "peekr",
1844
+ enforce: "pre",
1845
+ resolveId(id) {
1846
+ if (id.startsWith(VIRTUAL_PREFIX)) {
1847
+ return RESOLVED_PREFIX + id.slice(VIRTUAL_PREFIX.length);
1848
+ }
1849
+ },
1850
+ load(id) {
1851
+ if (id.startsWith(RESOLVED_PREFIX)) {
1852
+ const qIdx = id.indexOf("?");
1853
+ const query = qIdx !== -1 ? id.slice(qIdx + 1) : "";
1854
+ const params = new URLSearchParams(query);
1855
+ const file = params.get("file") ?? "";
1856
+ if (!file) return `import React from 'react'
1857
+ export default null
1858
+ `;
1859
+ const exportName = params.get("export") ?? "default";
1860
+ const fullContext = params.get("fullContext") === "true";
1861
+ return generateFrameEntry(file, exportName, fullContext, rootLayout, hasReactRouter, reactMajor);
1862
+ }
1863
+ },
1864
+ configureServer(server) {
1865
+ const watcher = server.watcher;
1866
+ watcher.on("add", async (absPath) => {
1867
+ const relPath = relative2(rootDir, absPath);
1868
+ const ext = extname2(absPath);
1869
+ if ([".css", ".scss", ".sass", ".less"].includes(ext)) {
1870
+ globalCssFiles = detectGlobalCss(rootDir, config.globalCss ?? []);
1871
+ }
1872
+ if ([".tsx", ".jsx", ".ts", ".js"].includes(ext)) {
1873
+ if ([".tsx", ".jsx", ".js"].includes(ext)) {
1874
+ const newLayout = detectRootLayout(rootDir, projectType);
1875
+ if (newLayout?.path !== rootLayout?.path) {
1876
+ rootLayout = newLayout;
1877
+ server.hot.send({ type: "custom", event: "peekr:root-layout-changed", data: rootLayout });
1878
+ }
1879
+ }
1880
+ await scanComponents(rootDir, scanCache, config.exclude);
1881
+ server.hot.send({ type: "custom", event: "peekr:components-updated", data: {} });
1882
+ }
1883
+ });
1884
+ watcher.on("change", async (absPath) => {
1885
+ const relPath = relative2(rootDir, absPath);
1886
+ const ext = extname2(absPath);
1887
+ if (relPath === "package.json") {
1888
+ projectType = detectProjectType(rootDir);
1889
+ const newLayout = detectRootLayout(rootDir, projectType);
1890
+ if (newLayout?.path !== rootLayout?.path) {
1891
+ rootLayout = newLayout;
1892
+ server.hot.send({ type: "custom", event: "peekr:root-layout-changed", data: rootLayout });
1893
+ }
1894
+ return;
1895
+ }
1896
+ if ([".tsx", ".jsx", ".ts", ".js"].includes(ext)) {
1897
+ invalidateScanFile(rootDir, relPath, scanCache);
1898
+ invalidateTypeCache(relPath, typeCache);
1899
+ await scanComponents(rootDir, scanCache, config.exclude);
1900
+ server.hot.send({ type: "custom", event: "peekr:components-updated", data: {} });
1901
+ }
1902
+ });
1903
+ watcher.on("unlink", async (absPath) => {
1904
+ const relPath = relative2(rootDir, absPath);
1905
+ invalidateScanFile(rootDir, relPath, scanCache);
1906
+ invalidateTypeCache(relPath, typeCache);
1907
+ if ([".tsx", ".jsx", ".js"].includes(extname2(absPath))) {
1908
+ const prevPath = rootLayout?.path;
1909
+ rootLayout = detectRootLayout(rootDir, projectType);
1910
+ if (prevPath !== rootLayout?.path) {
1911
+ server.hot.send({ type: "custom", event: "peekr:root-layout-changed", data: rootLayout });
1912
+ }
1913
+ }
1914
+ await scanComponents(rootDir, scanCache, config.exclude);
1915
+ server.hot.send({ type: "custom", event: "peekr:components-updated", data: {} });
1916
+ });
1917
+ server.middlewares.use(async (req, res, next) => {
1918
+ const url = req.url ?? "/";
1919
+ const urlObj = new URL(url, "http://localhost");
1920
+ const pathname = urlObj.pathname;
1921
+ if (pathname === "/__peek/components") {
1922
+ res.setHeader("Content-Type", "application/json");
1923
+ res.setHeader("Cache-Control", "no-store");
1924
+ res.end(JSON.stringify(scanCache.all));
1925
+ return;
1926
+ }
1927
+ if (pathname === "/__peek/types") {
1928
+ const file = urlObj.searchParams.get("file");
1929
+ const exportName = urlObj.searchParams.get("export") ?? "default";
1930
+ if (!file) {
1931
+ res.statusCode = 400;
1932
+ res.end(JSON.stringify({ error: "Missing ?file= param" }));
1933
+ return;
1934
+ }
1935
+ res.setHeader("Content-Type", "application/json");
1936
+ res.setHeader("Cache-Control", "no-store");
1937
+ try {
1938
+ const props = await parseComponentProps(rootDir, file, exportName, typeCache);
1939
+ res.end(JSON.stringify(props));
1940
+ } catch {
1941
+ res.end(JSON.stringify([]));
1942
+ }
1943
+ return;
1944
+ }
1945
+ if (pathname === "/__peek/global-css") {
1946
+ res.setHeader("Content-Type", "application/json");
1947
+ res.setHeader("Cache-Control", "no-store");
1948
+ res.end(JSON.stringify(globalCssFiles));
1949
+ return;
1950
+ }
1951
+ if (pathname === "/__peek/root-layout") {
1952
+ res.setHeader("Content-Type", "application/json");
1953
+ res.setHeader("Cache-Control", "no-store");
1954
+ res.end(JSON.stringify(rootLayout));
1955
+ return;
1956
+ }
1957
+ if (pathname === "/__peek/frame") {
1958
+ const file = urlObj.searchParams.get("file") ?? "";
1959
+ const exportName = urlObj.searchParams.get("export") ?? "default";
1960
+ const theme = urlObj.searchParams.get("theme") ?? "light";
1961
+ const fullContext = urlObj.searchParams.get("fullContext") === "true";
1962
+ const virtualParams = new URLSearchParams({ file, export: exportName });
1963
+ if (fullContext) virtualParams.set("fullContext", "true");
1964
+ const virtualSrc = `/@id/__x00__${VIRTUAL_PREFIX}?${virtualParams.toString()}`;
1965
+ const cssLinkTags = globalCssFiles.length > 0 ? globalCssFiles.map((f) => ` <link rel="stylesheet" href="${f.startsWith("/") ? f : `/${f}`}">`).join("\n") + "\n" : "";
1966
+ const frameHtml = `<!DOCTYPE html>
1967
+ <html lang="en" class="${theme === "dark" ? "dark" : ""}">
1968
+ <head>
1969
+ <meta charset="UTF-8" />
1970
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1971
+ ${htmlCdnLinks}${cssLinkTags} <style>
1972
+ *, *::before, *::after { box-sizing: border-box; }
1973
+ html, body {
1974
+ margin: 0;
1975
+ padding: 0;
1976
+ overflow-x: hidden;
1977
+ background: ${theme === "dark" ? "#0f172a" : "transparent"};
1978
+ transition: background 0.15s ease;
1979
+ }
1980
+ #peek-root { min-height: 100vh; width: 100%; }
1981
+ </style>
1982
+ </head>
1983
+ <body>
1984
+ <div id="peek-root"></div>
1985
+ <script type="module" src="${virtualSrc}"></script>
1986
+ </body>
1987
+ </html>`;
1988
+ const transformed = await server.transformIndexHtml("/__peek/frame", frameHtml);
1989
+ res.setHeader("Content-Type", "text/html");
1990
+ res.end(transformed);
1991
+ return;
1992
+ }
1993
+ if ((pathname === "/" || pathname === "/index.html") && appIndexHtml) {
1994
+ res.setHeader("Content-Type", "text/html");
1995
+ res.end(appIndexHtml);
1996
+ return;
1997
+ }
1998
+ if (pathname.startsWith("/__peek/app/") && appDistDir) {
1999
+ const assetRelPath = pathname.slice("/__peek/app/".length);
2000
+ const assetAbsPath = join4(appDistDir, assetRelPath);
2001
+ if (existsSync3(assetAbsPath)) {
2002
+ try {
2003
+ const stat = statSync3(assetAbsPath);
2004
+ if (stat.isFile()) {
2005
+ const ext = extname2(assetAbsPath);
2006
+ const mimeTypes = {
2007
+ ".js": "application/javascript",
2008
+ ".css": "text/css",
2009
+ ".html": "text/html",
2010
+ ".svg": "image/svg+xml",
2011
+ ".png": "image/png",
2012
+ ".ico": "image/x-icon",
2013
+ ".woff2": "font/woff2",
2014
+ ".woff": "font/woff"
2015
+ };
2016
+ res.setHeader("Content-Type", mimeTypes[ext] ?? "application/octet-stream");
2017
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
2018
+ res.end(readFileSync2(assetAbsPath));
2019
+ return;
2020
+ }
2021
+ } catch {
2022
+ }
2023
+ }
2024
+ }
2025
+ next();
2026
+ });
2027
+ }
2028
+ };
2029
+ const resolvedAliases = [];
2030
+ function readConfigPaths(configFile) {
2031
+ try {
2032
+ const raw = readFileSync2(join4(rootDir, configFile), "utf-8");
2033
+ const cleaned = raw.replace(/\/\/.*$/gm, "");
2034
+ const json = JSON.parse(cleaned);
2035
+ return json?.compilerOptions?.paths ?? {};
2036
+ } catch {
2037
+ return {};
2038
+ }
2039
+ }
2040
+ let configPaths = readConfigPaths("tsconfig.json");
2041
+ if (Object.keys(configPaths).length === 0) {
2042
+ configPaths = readConfigPaths("jsconfig.json");
2043
+ }
2044
+ let hasAtAlias = false;
2045
+ for (const [alias, targets] of Object.entries(configPaths)) {
2046
+ const target = (targets[0] ?? "").replace(/\/\*$/, "");
2047
+ if (!target) continue;
2048
+ const absTarget = resolve2(rootDir, target);
2049
+ if (alias === "@/*" || alias === "@") {
2050
+ hasAtAlias = true;
2051
+ resolvedAliases.push({ find: /^@\//, replacement: absTarget + "/" });
2052
+ } else {
2053
+ const cleanAlias = alias.replace(/\/\*$/, "");
2054
+ resolvedAliases.push({ find: cleanAlias, replacement: absTarget });
2055
+ }
2056
+ }
2057
+ if (!hasAtAlias) {
2058
+ const isNextProject = projectType === "next-app-router" || projectType === "next-pages-router";
2059
+ const nextConfigExists = ["next.config.js", "next.config.ts", "next.config.mjs", "next.config.cjs"].some((f) => existsSync3(join4(rootDir, f)));
2060
+ if (isNextProject || nextConfigExists) {
2061
+ const hasSrcDir = existsSync3(join4(rootDir, "src"));
2062
+ resolvedAliases.push({ find: /^@\//, replacement: join4(rootDir, hasSrcDir ? "src" : ".") + "/" });
2063
+ }
2064
+ }
2065
+ const vite = await createServer({
2066
+ root: rootDir,
2067
+ base: "/",
2068
+ cacheDir: join4(rootDir, "node_modules/.vite-peekr"),
2069
+ server: {
2070
+ port: config.port,
2071
+ strictPort: false,
2072
+ open: false,
2073
+ cors: true,
2074
+ host: "localhost"
2075
+ },
2076
+ plugins: [createStubPlugin(), autoReactImportPlugin, jsxInJsPlugin, peekrPlugin],
2077
+ resolve: {
2078
+ alias: resolvedAliases
2079
+ },
2080
+ css: {
2081
+ // Ensure Tailwind/PostCSS runs on user's CSS files.
2082
+ // Search for postcss config in common formats.
2083
+ postcss: (() => {
2084
+ const candidates = [
2085
+ "postcss.config.js",
2086
+ "postcss.config.cjs",
2087
+ "postcss.config.mjs",
2088
+ "postcss.config.ts"
2089
+ ];
2090
+ for (const f of candidates) {
2091
+ const abs = join4(rootDir, f);
2092
+ if (existsSync3(abs)) return abs;
2093
+ }
2094
+ return void 0;
2095
+ })()
2096
+ },
2097
+ // Use classic JSX transform globally — all JSX compiles to React.createElement(...).
2098
+ // This is required so our React.createElement intercept in full context mode fires
2099
+ // when user code calls RouterProvider/BrowserRouter etc.
2100
+ // autoReactImportPlugin injects "import React from 'react'" for files that need it.
2101
+ esbuild: {
2102
+ jsx: "transform"
2103
+ },
2104
+ optimizeDeps: {
2105
+ // Only pre-bundle react/react-dom if they're actually installed, to avoid
2106
+ // "Failed to resolve dependency" warnings in monorepos without node_modules.
2107
+ include: existsSync3(join4(rootDir, "node_modules/react")) ? ["react", "react-dom", "react-dom/client"] : [],
2108
+ exclude: ["ws", "socket.io", "bufferutil", "utf-8-validate"]
2109
+ },
2110
+ // Shim process.env with real values from the project's .env / .env.local files.
2111
+ // CRA uses process.env.REACT_APP_*; Next.js uses process.env.NEXT_PUBLIC_*.
2112
+ // Vite's own import.meta.env.VITE_* is handled natively (root = user project).
2113
+ define: {
2114
+ "process.env": JSON.stringify(dotEnvVars)
2115
+ },
2116
+ appType: "custom",
2117
+ clearScreen: false,
2118
+ logLevel: "warn"
2119
+ });
2120
+ await vite.listen();
2121
+ const actualPort = vite.config.server.port ?? config.port;
2122
+ const typeLabel = {
2123
+ "next-app-router": "Next.js App Router",
2124
+ "next-pages-router": "Next.js Pages Router",
2125
+ "vite": "Vite React",
2126
+ "cra": "CRA",
2127
+ "unknown": "React (unknown bundler)"
2128
+ };
2129
+ console.log(` [peekr] Project type : ${typeLabel[projectType]}`);
2130
+ console.log(` [peekr] Components : ${scanCache.all.length} found`);
2131
+ if (rootLayout) {
2132
+ console.log(` [peekr] Root layout : ${rootLayout.path} (${rootLayout.type})`);
2133
+ } else {
2134
+ console.log(` [peekr] Root layout : none \u2014 Full Context disabled`);
2135
+ }
2136
+ if (globalCssFiles.length > 0) {
2137
+ console.log(` [peekr] Global CSS : ${globalCssFiles.join(", ")}`);
2138
+ }
2139
+ return {
2140
+ vite,
2141
+ port: actualPort,
2142
+ stop: () => vite.close()
2143
+ };
2144
+ }
2145
+
2146
+ // src/index.ts
2147
+ var pkg = { name: "peekr-ui", version: "0.1.0" };
2148
+ function printBanner() {
2149
+ console.log("");
2150
+ console.log(pc.bold(pc.cyan(" \u26A1 peekr")));
2151
+ console.log(pc.dim(" Zero-config component playground"));
2152
+ console.log("");
2153
+ }
2154
+ var program = new Command();
2155
+ program.name("peekr").description("Zero-config component playground").version(pkg.version, "-v, --version").argument("[dir]", "Directory to scan for components", ".").option("-p, --port <number>", "Port to run on", "4321").option("--no-open", "Do not open browser automatically").option("--framework <name>", "Framework override (react | vue | svelte)", "react").option("--exclude <patterns...>", "Additional glob patterns to exclude").action(async (dir, options) => {
2156
+ const rootDir = resolve3(process.cwd(), dir);
2157
+ if (!existsSync4(rootDir)) {
2158
+ console.error(pc.red(` Directory not found: ${rootDir}`));
2159
+ process.exit(1);
2160
+ }
2161
+ printBanner();
2162
+ console.log(pc.dim(` Scanning: ${rootDir}`));
2163
+ const fileConfig = await loadConfig(rootDir);
2164
+ const config = mergeConfig(fileConfig, {
2165
+ port: parseInt(options.port, 10),
2166
+ open: options.open,
2167
+ framework: options.framework,
2168
+ exclude: options.exclude ?? []
2169
+ });
2170
+ let server;
2171
+ try {
2172
+ server = await createPeekrServer(rootDir, config);
2173
+ } catch (err) {
2174
+ console.error(pc.red(`
2175
+ Failed to start server: ${err.message}`));
2176
+ if (err.code === "EADDRINUSE") {
2177
+ console.error(pc.dim(` Port ${config.port} is in use. Try --port <number>`));
2178
+ }
2179
+ process.exit(1);
2180
+ }
2181
+ const url = `http://localhost:${server.port}`;
2182
+ const componentCount = server.vite.config.root ? "..." : "0";
2183
+ console.log("");
2184
+ console.log(` ${pc.green("\u2713")} Ready at ${pc.bold(pc.cyan(url))}`);
2185
+ console.log("");
2186
+ console.log(pc.dim(" Press Ctrl+C to stop"));
2187
+ console.log("");
2188
+ if (config.open) {
2189
+ try {
2190
+ const { default: open } = await import("open");
2191
+ await open(url);
2192
+ } catch {
2193
+ }
2194
+ }
2195
+ const shutdown = async (signal) => {
2196
+ console.log("");
2197
+ console.log(pc.dim(` Stopping (${signal})...`));
2198
+ await server.stop();
2199
+ process.exit(0);
2200
+ };
2201
+ process.on("SIGINT", () => shutdown("SIGINT"));
2202
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
2203
+ });
2204
+ program.command("build").description("Build a static preview site (coming soon)").action(() => {
2205
+ console.log(pc.yellow(" \u26A0 `peekr build` is coming in a future release."));
2206
+ process.exit(0);
2207
+ });
2208
+ program.parseAsync(process.argv).catch((err) => {
2209
+ console.error(pc.red(" Error: " + err.message));
2210
+ process.exit(1);
2211
+ });