@agimon-ai/po-style-system-tooling 0.2.14-alpha.37 → 0.2.14-alpha.39

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.
@@ -0,0 +1,1731 @@
1
+ import { createRequire } from "node:module";
2
+ import { promises } from "node:fs";
3
+ import * as path$1 from "node:path";
4
+ import path, { dirname } from "node:path";
5
+ import * as os$1 from "node:os";
6
+ import pino from "pino";
7
+ import * as fs from "node:fs/promises";
8
+ import { readFile, readdir } from "node:fs/promises";
9
+ import { randomUUID } from "node:crypto";
10
+ import { z } from "zod";
11
+ import tailwindcss from "@tailwindcss/vite";
12
+ import { viteSingleFile } from "vite-plugin-singlefile";
13
+ import { DatabaseSync } from "node:sqlite";
14
+ import { fileURLToPath } from "node:url";
15
+ import react from "@vitejs/plugin-react";
16
+ //#region src/utils/log.ts
17
+ const logsDir = path$1.join(os$1.tmpdir(), "style-system-logs");
18
+ const logger = pino({
19
+ level: process.env.LOG_LEVEL || "debug",
20
+ timestamp: pino.stdTimeFunctions.isoTime
21
+ }, pino.destination({
22
+ dest: path$1.join(logsDir, "style-system.log"),
23
+ mkdir: true,
24
+ sync: true
25
+ }));
26
+ function createLog(directory) {
27
+ const target = directory === void 0 ? logger : pino({
28
+ level: "debug",
29
+ timestamp: pino.stdTimeFunctions.isoTime
30
+ }, pino.destination({
31
+ dest: path$1.join(directory, "style-system.log"),
32
+ mkdir: true,
33
+ sync: true
34
+ }));
35
+ return {
36
+ debug: (msg, ...args) => target.debug({ args }, msg),
37
+ info: (msg, ...args) => target.info({ args }, msg),
38
+ warn: (msg, ...args) => target.warn({ args }, msg),
39
+ error: (msg, ...args) => target.error({ args }, msg)
40
+ };
41
+ }
42
+ const log = createLog();
43
+ //#endregion
44
+ //#region src/utils/fsHelpers.ts
45
+ /** True when candidate is root itself or inside it (sibling prefixes like `<root>-evil` are outside). */
46
+ function isContained(root, candidate) {
47
+ const relative = path.relative(root, candidate);
48
+ return relative === "" || !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
49
+ }
50
+ async function pathExists(filePath) {
51
+ try {
52
+ await fs.access(filePath);
53
+ return true;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+ //#endregion
59
+ //#region src/utils/TemplatesManagerService.ts
60
+ var TemplatesManagerService = class TemplatesManagerService {
61
+ static TOOLKIT_FOLDER = ".toolkit";
62
+ static SETTINGS_FILE = "settings.yaml";
63
+ static SETTINGS_LOCAL_FILE = "settings.local.yaml";
64
+ static TOOLKIT_CONFIG_FILE = "toolkit.yaml";
65
+ static deepMerge(base, local) {
66
+ const result = { ...base };
67
+ for (const [key, localValue] of Object.entries(local)) {
68
+ const baseValue = result[key];
69
+ if (localValue !== null && typeof localValue === "object" && !Array.isArray(localValue) && baseValue !== null && typeof baseValue === "object" && !Array.isArray(baseValue)) result[key] = TemplatesManagerService.deepMerge(baseValue, localValue);
70
+ else result[key] = localValue;
71
+ }
72
+ return result;
73
+ }
74
+ static mergeToolkitConfigs(base, local) {
75
+ return TemplatesManagerService.deepMerge(base, local);
76
+ }
77
+ static async findWorkspaceRoot(startPath) {
78
+ let currentPath = path.resolve(startPath);
79
+ const rootPath = path.parse(currentPath).root;
80
+ while (true) {
81
+ if (await pathExists(path.join(currentPath, ".git"))) return currentPath;
82
+ if (currentPath === rootPath) return process.cwd();
83
+ currentPath = path.dirname(currentPath);
84
+ }
85
+ }
86
+ static async readToolkitConfig(startPath = process.cwd()) {
87
+ const workspaceRoot = await TemplatesManagerService.findWorkspaceRoot(startPath);
88
+ const yaml = await import("js-yaml");
89
+ const toolkitFolder = path.join(workspaceRoot, TemplatesManagerService.TOOLKIT_FOLDER);
90
+ const settingsPath = path.join(toolkitFolder, TemplatesManagerService.SETTINGS_FILE);
91
+ const settingsLocalPath = path.join(toolkitFolder, TemplatesManagerService.SETTINGS_LOCAL_FILE);
92
+ if (await pathExists(settingsPath)) {
93
+ const baseContent = await fs.readFile(settingsPath, "utf-8");
94
+ const base = yaml.load(baseContent);
95
+ if (await pathExists(settingsLocalPath)) {
96
+ const localContent = await fs.readFile(settingsLocalPath, "utf-8");
97
+ const local = yaml.load(localContent);
98
+ return TemplatesManagerService.mergeToolkitConfigs(base, local);
99
+ }
100
+ return base;
101
+ }
102
+ const legacyConfigPath = path.join(workspaceRoot, TemplatesManagerService.TOOLKIT_CONFIG_FILE);
103
+ if (!await pathExists(legacyConfigPath)) return null;
104
+ const content = await fs.readFile(legacyConfigPath, "utf-8");
105
+ return yaml.load(content);
106
+ }
107
+ static async getWorkspaceRoot(startPath = process.cwd()) {
108
+ return TemplatesManagerService.findWorkspaceRoot(startPath);
109
+ }
110
+ };
111
+ //#endregion
112
+ //#region src/services/BundlerService/BaseBundlerService.ts
113
+ /**
114
+ * Abstract base class for bundler service implementations.
115
+ *
116
+ * Subclasses must implement the abstract methods to provide
117
+ * bundler-specific (Vite, Webpack, etc.) and framework-specific
118
+ * (React, Vue, etc.) component rendering logic.
119
+ *
120
+ * @example
121
+ * ```typescript
122
+ * class MyCustomBundlerService extends BaseBundlerService {
123
+ * async startDevServer(appPath: string): Promise<DevServerResult> {
124
+ * // Custom dev server logic
125
+ * }
126
+ * // ... implement other abstract methods
127
+ * }
128
+ * ```
129
+ */
130
+ var BaseBundlerService = class {
131
+ config;
132
+ /**
133
+ * Creates a new bundler service instance.
134
+ * @param config - Service configuration options
135
+ */
136
+ constructor(config = {}) {
137
+ this.config = config;
138
+ }
139
+ };
140
+ //#endregion
141
+ //#region src/services/BundlerService/virtualFiles.ts
142
+ /**
143
+ * Virtual files for story bundles.
144
+ *
145
+ * DESIGN PATTERNS:
146
+ * - Vite runs with an empty root; generated inputs (build HTML, wrapper CSS) and per-render
147
+ * snapshots of declared sources (story, root component) are served from one SQLite file
148
+ * - One store per dev-server run or static build; its owner disposes it
149
+ *
150
+ * AVOID:
151
+ * - Writing generated inputs into the Vite root: Vite's dependency scan and Tailwind's
152
+ * source scan read everything there, so past renders grow every new one
153
+ */
154
+ const DATABASE_FILE = "inputs.db";
155
+ const VIRTUAL_FILES_PLUGIN_NAME = "style-system-virtual-files";
156
+ const ID_QUERY_PATTERN = /[?#].*$/;
157
+ /** Vite derives these modules (inline HTML scripts and styles) itself; answering them with the stored HTML breaks the build */
158
+ const HTML_PROXY_QUERY_PATTERN = /[?&]html-proxy\b/;
159
+ const CSS_COMMENT_PATTERN = /(\/\*[\s\S]*?\*\/)/;
160
+ const RELATIVE_DIRECTIVE_PATTERN = /(@(?:import|source|plugin|config|reference)\s+(?:not\s+)?)(['"])(\.\.?\/[^'"]*)\2/g;
161
+ const RELATIVE_SOURCE_FUNCTION_PATTERN = /(source\(\s*)(['"])(\.\.?\/[^'"]*)\2/g;
162
+ const RELATIVE_URL_PATTERN = /url\(\s*(['"]?)(\.\.?\/[^'")]*)\1\s*\)/g;
163
+ var VirtualFileStore = class VirtualFileStore {
164
+ dir;
165
+ db;
166
+ putStatement;
167
+ getStatement;
168
+ evictStatement;
169
+ constructor(dir, db) {
170
+ this.dir = dir;
171
+ this.db = db;
172
+ this.putStatement = db.prepare("INSERT OR REPLACE INTO files (path, render_id, content) VALUES (?, ?, ?)");
173
+ this.getStatement = db.prepare("SELECT content FROM files WHERE path = ?");
174
+ this.evictStatement = db.prepare("DELETE FROM files WHERE render_id = ?");
175
+ }
176
+ static async open(dir) {
177
+ await promises.mkdir(dir, { recursive: true });
178
+ const db = new DatabaseSync(path.join(dir, DATABASE_FILE));
179
+ db.exec("CREATE TABLE IF NOT EXISTS files (path TEXT PRIMARY KEY, render_id TEXT NOT NULL, content TEXT NOT NULL) STRICT");
180
+ return new VirtualFileStore(dir, db);
181
+ }
182
+ /** Stores a file for one render. Returns true when the content changed, so cached modules can be invalidated. */
183
+ put(filePath, renderId, content) {
184
+ const changed = this.get(filePath) !== content;
185
+ this.putStatement.run(filePath, renderId, content);
186
+ return changed;
187
+ }
188
+ get(filePath) {
189
+ return this.getStatement.get(filePath)?.content;
190
+ }
191
+ /** Drops the rows a render still owns; rows a later render rewrote stay. */
192
+ evict(renderId) {
193
+ this.evictStatement.run(renderId);
194
+ }
195
+ async dispose() {
196
+ if (this.db.isOpen) this.db.close();
197
+ await promises.rm(this.dir, {
198
+ recursive: true,
199
+ force: true
200
+ });
201
+ }
202
+ };
203
+ /**
204
+ * Serves stored files to Vite. Real paths (snapshotted sources) resolve as usual and only load
205
+ * from the store; generated files under the empty root do not exist on disk, so they resolve here.
206
+ */
207
+ function createVirtualFilesPlugin(store, root) {
208
+ return {
209
+ name: VIRTUAL_FILES_PLUGIN_NAME,
210
+ enforce: "pre",
211
+ resolveId(source) {
212
+ const file = source.replace(ID_QUERY_PATTERN, "");
213
+ if (!file.startsWith("/")) return void 0;
214
+ const candidate = file.startsWith(root) ? file : path.join(root, file);
215
+ return store.get(candidate) === void 0 ? void 0 : `${candidate}${source.slice(file.length)}`;
216
+ },
217
+ load(id) {
218
+ if (HTML_PROXY_QUERY_PATTERN.test(id)) return void 0;
219
+ return store.get(id.replace(ID_QUERY_PATTERN, ""));
220
+ }
221
+ };
222
+ }
223
+ /**
224
+ * Inlines a stylesheet into a wrapper in another folder: relative directives become absolute
225
+ * and relative url() values are rebased onto the wrapper folder. Comments are left untouched.
226
+ */
227
+ function flattenCss(css, fromDir, wrapperDir) {
228
+ const absolute = (prefix, quote, relative) => `${prefix}${quote}${path.resolve(fromDir, relative)}${quote}`;
229
+ return css.split(CSS_COMMENT_PATTERN).map((part, index) => index % 2 === 1 ? part : part.replace(RELATIVE_DIRECTIVE_PATTERN, (_match, prefix, quote, relative) => absolute(prefix, quote, relative)).replace(RELATIVE_SOURCE_FUNCTION_PATTERN, (_match, prefix, quote, relative) => absolute(prefix, quote, relative)).replace(RELATIVE_URL_PATTERN, (_match, quote, relative) => {
230
+ return `url(${quote}${path.relative(wrapperDir, path.resolve(fromDir, relative)).split(path.sep).join("/")}${quote})`;
231
+ })).join("");
232
+ }
233
+ //#endregion
234
+ //#region src/services/BundlerService/ViteReactBundlerService.ts
235
+ /**
236
+ * ViteReactBundlerService
237
+ *
238
+ * DESIGN PATTERNS:
239
+ * - Concrete implementation of BaseBundlerService
240
+ * - Singleton pattern for dev server management
241
+ * - Programmatic Vite build for React component SSR
242
+ *
243
+ * CODING STANDARDS:
244
+ * - Use async/await for asynchronous operations
245
+ * - Throw descriptive errors for error cases
246
+ * - Keep methods focused and well-named
247
+ * - Document complex logic with comments
248
+ *
249
+ * AVOID:
250
+ * - Mixing concerns (keep focused on Vite + React)
251
+ * - Direct tool implementation (services should be tool-agnostic)
252
+ */
253
+ /**
254
+ * Maximum age for story configs in milliseconds (5 minutes).
255
+ * Configs older than this are cleaned up to prevent memory leaks.
256
+ */
257
+ const STORY_CONFIG_MAX_AGE_MS = 3e5;
258
+ /**
259
+ * Maximum number of story configs to keep in memory.
260
+ * Oldest configs are removed when this limit is exceeded.
261
+ */
262
+ const STORY_CONFIG_MAX_COUNT = 100;
263
+ /**
264
+ * Maximum size for serialized args in bytes (1MB).
265
+ * Prevents memory issues with extremely large payloads.
266
+ */
267
+ const MAX_ARGS_SIZE_BYTES = 1048576;
268
+ /**
269
+ * Valid pattern for story names.
270
+ * Allows alphanumeric characters, underscores, hyphens, and spaces.
271
+ */
272
+ const VALID_STORY_NAME_PATTERN = /^[a-zA-Z0-9_\- ]+$/;
273
+ /**
274
+ * Valid pattern for component paths.
275
+ * Must end with .stories.tsx, .stories.ts, .stories.jsx, or .stories.js
276
+ */
277
+ const VALID_COMPONENT_PATH_PATTERN = /\.stories\.(tsx?|jsx?)$/;
278
+ const UTF8_ENCODING = "utf-8";
279
+ const TMP_DIR_NAME = ".tmp";
280
+ const STYLE_SYSTEM_TMP_DIR = "style-system";
281
+ const VITE_ROOT_DIR = "root";
282
+ const SCOPE_DIR_PREFIX = "scope-";
283
+ const WRAPPER_CSS_FILE_PREFIX = "wrapper-";
284
+ const CSS_FILE_EXTENSION = ".css";
285
+ const VIRTUAL_STORY_ENTRY_MODULE = "virtual:story-entry";
286
+ const VIRTUAL_STORY_ENTRY_SCRIPT_PREFIX = `@${VIRTUAL_STORY_ENTRY_MODULE}?id=`;
287
+ const PREVIEW_ROUTE_PREFIX = "/preview/";
288
+ const PREVIEW_ROUTE_PATTERN = /^\/preview\/([^/?]+)/;
289
+ const LOCALHOST_URL_PREFIX = "http://localhost:";
290
+ const VITE_BUNDLER_ID = "vite";
291
+ const REACT_FRAMEWORK_ID = "react";
292
+ const BUILD_HTML_FILE_PREFIX = "index-";
293
+ const BUILD_OUTPUT_DIR_PREFIX = "out-";
294
+ const HTML_FILE_EXTENSION = ".html";
295
+ const JSON_INDENT_SPACES = 2;
296
+ const HTTP_STATUS_OK = 200;
297
+ const CONTENT_TYPE_HEADER = "Content-Type";
298
+ const TEXT_HTML_CONTENT_TYPE = "text/html";
299
+ const RANDOM_ID_RADIX = 36;
300
+ const RANDOM_ID_SLICE_START = 2;
301
+ const PACKAGE_IMPORT_PREFIX = "@";
302
+ const TAILWIND_IMPORT_PREFIX = "tailwindcss/";
303
+ const WORKSPACE_PACKAGE_DIR_PREFIX = "packages/";
304
+ const WORKSPACE_APP_DIR_PREFIX = "apps/";
305
+ const APP_SOURCE_DIR$1 = "src";
306
+ const APP_SOURCE_ALIAS_PATTERN = /^@\//;
307
+ const WEB_THEME_PACKAGE_PATTERN = /^@agimonai\/frontend-web-theme$/;
308
+ const DEFAULT_CSS_ENTRYPOINTS = [
309
+ "src/assets/main.css",
310
+ "src/assets/index.css",
311
+ "src/assets/global.css",
312
+ "src/styles/main.css",
313
+ "src/styles/globals.css",
314
+ "src/styles/tailwind.css"
315
+ ];
316
+ const STORY_ID_BUILD_PREFIX = "build-";
317
+ const ROOT_ELEMENT_ID = "root";
318
+ const STORY_ENTRY_PLUGIN_NAME = "vite-plugin-story-entry";
319
+ const TANSTACK_START_STUB_PLUGIN_NAME = "vite-plugin-tanstack-start-stub";
320
+ const TANSTACK_START_STUB_ID_PREFIX = "\0tanstack-start-stub:";
321
+ /**
322
+ * Stub modules that let TanStack Start apps' stories build and render in isolation.
323
+ *
324
+ * `@tanstack/start-server-core` imports `#tanstack-start-entry` / `#tanstack-router-entry`, but
325
+ * only `@tanstack/start-client-core` declares them in its `package.json` `imports` map. Node and
326
+ * Vite resolve `#` specifiers against the *importing* package's own map, so these fail with
327
+ * `Missing "#tanstack-start-entry" specifier in "@tanstack/start-server-core"`. It also imports
328
+ * the `tanstack-start-manifest:v` virtual module, which only the plugin provides. Real apps never
329
+ * hit either problem because the `tanstackStart()` Vite plugin intercepts the specifiers first and
330
+ * strips server-only code from the client bundle; the story renderer intentionally builds without
331
+ * that plugin.
332
+ *
333
+ * Isolated story rendering never runs the SSR start handler or hydration, so these mirror the shape
334
+ * of TanStack's own fake entries (`dist/esm/fake-entries/*`). Specifiers a package declares itself
335
+ * (`#tanstack-start-server-fn-resolver`, `#tanstack-start-plugin-adapters`) resolve natively and are
336
+ * deliberately not stubbed here.
337
+ */
338
+ const TANSTACK_START_STUB_MODULES = /* @__PURE__ */ new Map([
339
+ ["#tanstack-start-entry", "export const startInstance = undefined;\n"],
340
+ ["#tanstack-router-entry", "export function getRouter() {}\n"],
341
+ ["tanstack-start-manifest:v", "export function tsrStartManifest() {\n return { routes: {} };\n}\n"],
342
+ ["node:async_hooks", `export class AsyncLocalStorage {
343
+ constructor() {
344
+ this.store = undefined;
345
+ }
346
+ getStore() {
347
+ return this.store;
348
+ }
349
+ run(store, callback, ...args) {
350
+ const previous = this.store;
351
+ this.store = store;
352
+ try {
353
+ return callback(...args);
354
+ } finally {
355
+ this.store = previous;
356
+ }
357
+ }
358
+ exit(callback, ...args) {
359
+ return this.run(undefined, callback, ...args);
360
+ }
361
+ enterWith(store) {
362
+ this.store = store;
363
+ }
364
+ disable() {
365
+ this.store = undefined;
366
+ }
367
+ }
368
+ `]
369
+ ]);
370
+ const VITE_PLUGIN_ENFORCE_PRE$1 = "pre";
371
+ const VITE_ROOT_BASE = "/";
372
+ const VITE_RELATIVE_BASE = "./";
373
+ const VITE_PREVIEW_MODE = "development";
374
+ const VITE_LOG_LEVEL = "silent";
375
+ const JSX_RUNTIME_AUTOMATIC = "automatic";
376
+ const SUPPORTED_CSS_EXTENSIONS = [
377
+ ".css",
378
+ ".scss",
379
+ ".sass",
380
+ ".less",
381
+ ".pcss",
382
+ ".postcss"
383
+ ];
384
+ const SUPPORTED_CSS_EXTENSIONS_LABEL = SUPPORTED_CSS_EXTENSIONS.join(", ");
385
+ /**
386
+ * Validates a story name to prevent code injection.
387
+ * @param storyName - The story name to validate
388
+ * @throws Error if the story name contains invalid characters
389
+ */
390
+ function validateStoryName(storyName) {
391
+ if (!storyName || typeof storyName !== "string") throw new Error("Story name is required and must be a string");
392
+ if (!VALID_STORY_NAME_PATTERN.test(storyName)) throw new Error(`Story name "${storyName}" contains invalid characters. Only alphanumeric characters, underscores, hyphens, and spaces are allowed.`);
393
+ }
394
+ /**
395
+ * Validates a component path to prevent code injection.
396
+ * @param componentPath - The component path to validate
397
+ * @throws Error if the component path is invalid
398
+ */
399
+ function validateComponentPath(componentPath) {
400
+ if (!componentPath || typeof componentPath !== "string") throw new Error("Component path is required and must be a string");
401
+ if (!VALID_COMPONENT_PATH_PATTERN.test(componentPath)) throw new Error(`Component path "${componentPath}" must be a valid Storybook story file (*.stories.tsx, *.stories.ts, *.stories.jsx, or *.stories.js)`);
402
+ if (componentPath.includes("..")) throw new Error("Component path must not contain path traversal sequences (..)");
403
+ }
404
+ /**
405
+ * Validates args object size to prevent memory issues.
406
+ * @param args - The args object to validate
407
+ * @throws Error if args exceed size limit
408
+ */
409
+ function validateArgsSize(args) {
410
+ const serialized = JSON.stringify(args);
411
+ const sizeBytes = Buffer.byteLength(serialized, UTF8_ENCODING);
412
+ if (sizeBytes > MAX_ARGS_SIZE_BYTES) throw new Error(`Args payload too large: ${sizeBytes} bytes exceeds maximum of ${MAX_ARGS_SIZE_BYTES} bytes (1MB)`);
413
+ }
414
+ /**
415
+ * Validates CSS file paths to prevent path traversal attacks.
416
+ * @param cssFiles - Array of CSS file paths to validate
417
+ * @param workspaceRoot - The workspace root directory
418
+ * @throws Error if any path is invalid or escapes workspace
419
+ */
420
+ function validateCssFiles(cssFiles, workspaceRoot) {
421
+ for (const cssFile of cssFiles) {
422
+ if (typeof cssFile !== "string") throw new Error("CSS file path must be a string");
423
+ if (cssFile.includes("..")) throw new Error(`CSS file path "${cssFile}" must not contain path traversal sequences (..)`);
424
+ if (path.isAbsolute(cssFile) && !isContained(workspaceRoot, path.normalize(cssFile))) throw new Error(`CSS file path "${cssFile}" must be within workspace boundaries`);
425
+ const ext = path.extname(cssFile).toLowerCase();
426
+ if (!SUPPORTED_CSS_EXTENSIONS.includes(ext)) throw new Error(`CSS file "${cssFile}" must have a valid CSS extension (${SUPPORTED_CSS_EXTENSIONS_LABEL})`);
427
+ }
428
+ }
429
+ /**
430
+ * Helper to create a Vite plugin that serves story entry files from memory.
431
+ */
432
+ function createStoryEntryPlugin(getStoryConfig, generateCode) {
433
+ return {
434
+ name: STORY_ENTRY_PLUGIN_NAME,
435
+ enforce: VITE_PLUGIN_ENFORCE_PRE$1,
436
+ resolveId(id) {
437
+ if (id.includes(VIRTUAL_STORY_ENTRY_MODULE)) {
438
+ log.debug(`[vite-plugin-story-entry] resolveId: ${id}`);
439
+ return `\0${id.replace(/^\//, "")}`;
440
+ }
441
+ },
442
+ load(id) {
443
+ if (id.includes(VIRTUAL_STORY_ENTRY_MODULE)) {
444
+ log.debug(`[vite-plugin-story-entry] load: ${id}`);
445
+ const storyId = id.match(/id=([^&]+)/)?.[1];
446
+ if (storyId) {
447
+ const config = getStoryConfig(storyId);
448
+ if (config) return generateCode(config);
449
+ }
450
+ throw new Error(`[Vite] Story config not found for id: ${storyId}`);
451
+ }
452
+ }
453
+ };
454
+ }
455
+ /** Where a package the app depends on is installed, resolved from the app rather than from this tool. */
456
+ function appPackageDir(appPath, packageName) {
457
+ return path.dirname(createRequire(path.join(appPath, "package.json")).resolve(`${packageName}/package.json`));
458
+ }
459
+ /**
460
+ * React and react-dom are peers of this tool: every story bundle takes the app's own pair, so the
461
+ * story, the generated entry and any workspace package it imports share one React that matches its
462
+ * react-dom. A pair resolved from the tool fails in the browser with React error #527.
463
+ */
464
+ function appReactAliases(appPath) {
465
+ const reactDir = appPackageDir(appPath, "react");
466
+ const reactDomDir = appPackageDir(appPath, "react-dom");
467
+ return [
468
+ {
469
+ find: /^react$/,
470
+ replacement: reactDir
471
+ },
472
+ {
473
+ find: /^react\/(.*)$/,
474
+ replacement: path.join(reactDir, "$1")
475
+ },
476
+ {
477
+ find: /^react-dom$/,
478
+ replacement: reactDomDir
479
+ },
480
+ {
481
+ find: /^react-dom\/(.*)$/,
482
+ replacement: path.join(reactDomDir, "$1")
483
+ }
484
+ ];
485
+ }
486
+ /**
487
+ * Helper to create a Vite plugin that resolves the module specifiers TanStack Start's
488
+ * `tanstackStart()` plugin normally provides to inert client-safe stubs, so stories in TanStack
489
+ * Start apps build and render without that plugin.
490
+ * See TANSTACK_START_STUB_MODULES for why this is needed.
491
+ */
492
+ function createTanStackStartStubPlugin() {
493
+ return {
494
+ name: TANSTACK_START_STUB_PLUGIN_NAME,
495
+ enforce: VITE_PLUGIN_ENFORCE_PRE$1,
496
+ resolveId(id) {
497
+ return TANSTACK_START_STUB_MODULES.has(id) ? `${TANSTACK_START_STUB_ID_PREFIX}${id}` : void 0;
498
+ },
499
+ load(id) {
500
+ if (!id.startsWith(TANSTACK_START_STUB_ID_PREFIX)) return;
501
+ const specifier = id.slice(21);
502
+ const stub = TANSTACK_START_STUB_MODULES.get(specifier);
503
+ if (!stub) throw new Error(`[Vite] No TanStack entry stub registered for specifier: ${specifier}`);
504
+ return stub;
505
+ }
506
+ };
507
+ }
508
+ function styleSystemTmpDir(appPath) {
509
+ return path.join(appPath, TMP_DIR_NAME, STYLE_SYSTEM_TMP_DIR);
510
+ }
511
+ function viteRootDir(appPath) {
512
+ return path.join(styleSystemTmpDir(appPath), VITE_ROOT_DIR);
513
+ }
514
+ function toImportPath(filePath) {
515
+ return filePath.replace(/\\/g, "/");
516
+ }
517
+ /**
518
+ * ViteReactBundlerService provides Vite + React bundling for component rendering.
519
+ *
520
+ * This is the default implementation of BaseBundlerService that uses Vite
521
+ * as the bundler and React as the framework for rendering components.
522
+ *
523
+ * @example
524
+ * ```typescript
525
+ * const service = ViteReactBundlerService.getInstance();
526
+ * await service.startDevServer('apps/my-app');
527
+ * const { url } = await service.serveComponent({
528
+ * componentPath: '/path/to/Button.stories.tsx',
529
+ * storyName: 'Primary',
530
+ * appPath: 'apps/my-app'
531
+ * });
532
+ * ```
533
+ */
534
+ var ViteReactBundlerService = class ViteReactBundlerService extends BaseBundlerService {
535
+ workspaceRoot;
536
+ static instance = null;
537
+ server = null;
538
+ monorepoRoot;
539
+ serverUrl = null;
540
+ serverPort = null;
541
+ currentAppPath = null;
542
+ storyConfigs = /* @__PURE__ */ new Map();
543
+ /** Timestamps for when each story config was created, used for cleanup */
544
+ storyConfigTimestamps = /* @__PURE__ */ new Map();
545
+ /** Promise that resolves when server startup completes, prevents race conditions */
546
+ serverStartPromise = null;
547
+ /** Virtual files of the running dev server */
548
+ devStore = null;
549
+ /**
550
+ * Creates a new ViteReactBundlerService instance.
551
+ * Use getInstance() for singleton access.
552
+ * @param config - Service configuration options
553
+ */
554
+ constructor(config = {}, workspaceRoot) {
555
+ super(config);
556
+ this.workspaceRoot = workspaceRoot;
557
+ this.monorepoRoot = workspaceRoot !== void 0 ? Promise.resolve(workspaceRoot) : void 0;
558
+ }
559
+ /**
560
+ * Get the singleton instance of ViteReactBundlerService.
561
+ * Singleton pattern ensures only one dev server runs at a time,
562
+ * preventing port conflicts and resource duplication.
563
+ * @returns The singleton ViteReactBundlerService instance
564
+ */
565
+ static getInstance() {
566
+ if (!ViteReactBundlerService.instance) ViteReactBundlerService.instance = new ViteReactBundlerService();
567
+ return ViteReactBundlerService.instance;
568
+ }
569
+ /**
570
+ * Reset the singleton instance.
571
+ * This is primarily used in testing to ensure a fresh instance.
572
+ * @example
573
+ * ```typescript
574
+ * afterEach(() => {
575
+ * ViteReactBundlerService.resetInstance();
576
+ * });
577
+ * ```
578
+ */
579
+ static resetInstance() {
580
+ ViteReactBundlerService.instance = null;
581
+ }
582
+ /**
583
+ * Get the bundler identifier.
584
+ * @returns The bundler ID string ('vite')
585
+ */
586
+ getBundlerId() {
587
+ return VITE_BUNDLER_ID;
588
+ }
589
+ /**
590
+ * Get the framework identifier.
591
+ * @returns The framework ID string ('react')
592
+ */
593
+ getFrameworkId() {
594
+ return REACT_FRAMEWORK_ID;
595
+ }
596
+ /**
597
+ * Get the current server URL.
598
+ * @returns Server URL or null if not running
599
+ */
600
+ getServerUrl() {
601
+ return this.serverUrl;
602
+ }
603
+ /**
604
+ * Get the current server port.
605
+ * @returns Server port or null if not running
606
+ */
607
+ getServerPort() {
608
+ return this.serverPort;
609
+ }
610
+ /**
611
+ * Check if the dev server is running.
612
+ * @returns True if server is running
613
+ */
614
+ isServerRunning() {
615
+ return this.server !== null && this.serverUrl !== null;
616
+ }
617
+ /**
618
+ * Get the current app path being served.
619
+ * @returns App path or null if not running
620
+ */
621
+ getCurrentAppPath() {
622
+ return this.currentAppPath;
623
+ }
624
+ /**
625
+ * Start a Vite dev server for hot reload and caching.
626
+ * Handles concurrent calls by returning the same promise if server is already starting.
627
+ * @param appPath - Absolute or relative path to the app directory
628
+ * @returns Promise resolving to server URL and port
629
+ * @throws Error if server fails to start
630
+ */
631
+ async startDevServer(appPath) {
632
+ const monorepoRoot = await (this.monorepoRoot ??= TemplatesManagerService.getWorkspaceRoot());
633
+ const resolvedAppPath = path.isAbsolute(appPath) ? appPath : path.join(monorepoRoot, appPath);
634
+ if (this.isServerRunning() && this.currentAppPath === resolvedAppPath) {
635
+ log.info(`[ViteReactBundlerService] Server already running for ${resolvedAppPath}`);
636
+ return {
637
+ url: this.serverUrl,
638
+ port: this.serverPort
639
+ };
640
+ }
641
+ if (this.serverStartPromise) {
642
+ log.info("[ViteReactBundlerService] Server start already in progress, waiting...");
643
+ return this.serverStartPromise;
644
+ }
645
+ this.serverStartPromise = this.doStartDevServer(resolvedAppPath);
646
+ try {
647
+ return await this.serverStartPromise;
648
+ } finally {
649
+ this.serverStartPromise = null;
650
+ }
651
+ }
652
+ /**
653
+ * Internal method that performs the actual server startup.
654
+ * @param resolvedAppPath - Resolved absolute path to the app directory
655
+ * @returns Promise resolving to server URL and port
656
+ */
657
+ async doStartDevServer(resolvedAppPath) {
658
+ const root = viteRootDir(resolvedAppPath);
659
+ let createdServer;
660
+ try {
661
+ await promises.mkdir(root, { recursive: true });
662
+ this.devStore = await VirtualFileStore.open(path.join(styleSystemTmpDir(resolvedAppPath), `${SCOPE_DIR_PREFIX}${randomUUID()}`));
663
+ const { createServer } = await import("vite");
664
+ this.server = await createServer({
665
+ root,
666
+ base: VITE_ROOT_BASE,
667
+ mode: VITE_PREVIEW_MODE,
668
+ envDir: resolvedAppPath,
669
+ configFile: false,
670
+ logLevel: VITE_LOG_LEVEL,
671
+ plugins: [
672
+ createVirtualFilesPlugin(this.devStore, root),
673
+ tailwindcss(),
674
+ createTanStackStartStubPlugin(),
675
+ ...await this.getAdditionalVitePlugins(resolvedAppPath),
676
+ {
677
+ name: "style-system-preview-html",
678
+ configureServer: (server) => {
679
+ server.middlewares.use(async (req, res, next) => {
680
+ const url = req.url || "/";
681
+ const match = url.match(PREVIEW_ROUTE_PATTERN);
682
+ if (match) {
683
+ const storyId = match[1];
684
+ const config = this.storyConfigs.get(storyId);
685
+ if (config) try {
686
+ const htmlTemplate = this.generateHtmlTemplate(`${VIRTUAL_STORY_ENTRY_SCRIPT_PREFIX}${storyId}`, config.darkMode);
687
+ const transformedHtml = await server.transformIndexHtml(url, htmlTemplate);
688
+ res.statusCode = HTTP_STATUS_OK;
689
+ res.setHeader(CONTENT_TYPE_HEADER, TEXT_HTML_CONTENT_TYPE);
690
+ res.end(transformedHtml);
691
+ return;
692
+ } catch (e) {
693
+ const err = e;
694
+ log.error(`[ViteMiddleware] Error serving preview: ${err.message}`);
695
+ next(err);
696
+ return;
697
+ }
698
+ }
699
+ next();
700
+ });
701
+ }
702
+ },
703
+ createStoryEntryPlugin((id) => this.storyConfigs.get(id), (opts) => this.generateEntryFile(opts))
704
+ ],
705
+ resolve: await this.getViteResolveConfig(resolvedAppPath),
706
+ define: this.getViteDefineConfig(),
707
+ esbuild: this.getViteEsbuildConfig(),
708
+ oxc: this.getViteOxcConfig(),
709
+ server: {
710
+ strictPort: false,
711
+ open: false
712
+ }
713
+ });
714
+ createdServer = this.server;
715
+ await this.server.listen();
716
+ const address = this.server.httpServer?.address();
717
+ if (!address || typeof address === "string") throw new Error("Failed to start Vite dev server. Ensure no other process is using the port.");
718
+ const port = address.port;
719
+ const url = `${LOCALHOST_URL_PREFIX}${port}`;
720
+ this.serverUrl = url;
721
+ this.serverPort = port;
722
+ this.currentAppPath = resolvedAppPath;
723
+ log.info(`[ViteReactBundlerService] Vite dev server started at ${url}`);
724
+ return {
725
+ url,
726
+ port
727
+ };
728
+ } catch (error) {
729
+ try {
730
+ await createdServer?.close();
731
+ } catch (closeError) {
732
+ log.warn(`[ViteReactBundlerService] Failed to close server after startup failure: ${String(closeError)}`);
733
+ }
734
+ this.server = null;
735
+ this.serverUrl = null;
736
+ this.serverPort = null;
737
+ this.currentAppPath = null;
738
+ await this.disposeDevStore();
739
+ const err = /* @__PURE__ */ new Error(`Failed to start dev server for ${resolvedAppPath}: ${error instanceof Error ? error.message : String(error)}`);
740
+ err.cause = error;
741
+ throw err;
742
+ }
743
+ }
744
+ /**
745
+ * Serve a component dynamically through the dev server.
746
+ * @param options - Component rendering options
747
+ * @returns Promise resolving to the component URL and HTML file path
748
+ * @throws Error if dev server is not running or file operations fail
749
+ */
750
+ async serveComponent(options) {
751
+ if (!this.isServerRunning()) throw new Error("Dev server is not running. Start it first using startDevServer().");
752
+ const { componentPath, storyName, args = {}, darkMode = false, appPath, cssFiles = [], rootComponent, unistylesConfig } = options;
753
+ validateStoryName(storyName);
754
+ validateComponentPath(componentPath);
755
+ validateArgsSize(args);
756
+ const monorepoRoot = await (this.monorepoRoot ??= TemplatesManagerService.getWorkspaceRoot());
757
+ const resolvedAppPath = path.isAbsolute(appPath) ? appPath : path.join(monorepoRoot, appPath);
758
+ validateCssFiles(cssFiles, this.workspaceRoot ?? await TemplatesManagerService.getWorkspaceRoot(resolvedAppPath));
759
+ if (this.currentAppPath !== resolvedAppPath) throw new Error(`Dev server is running for ${this.currentAppPath} but requested ${resolvedAppPath}.`);
760
+ const store = this.devStore;
761
+ if (!store) throw new Error("Dev server has no virtual file store. Start it first using startDevServer().");
762
+ try {
763
+ this.cleanupStaleStoryConfigs();
764
+ const timestamp = Date.now();
765
+ const storyId = `${timestamp}-${Math.random().toString(RANDOM_ID_RADIX).slice(RANDOM_ID_SLICE_START)}`;
766
+ const config = {
767
+ componentPath,
768
+ storyName,
769
+ args,
770
+ appPath: resolvedAppPath,
771
+ darkMode,
772
+ cssFiles,
773
+ rootComponent,
774
+ unistylesConfig,
775
+ wrapperCssPath: path.join(viteRootDir(resolvedAppPath), `${WRAPPER_CSS_FILE_PREFIX}${storyId}${CSS_FILE_EXTENSION}`)
776
+ };
777
+ for (const changedFile of await this.snapshotRender(store, storyId, config)) {
778
+ const moduleGraph = this.server?.environments.client.moduleGraph;
779
+ for (const module of moduleGraph?.getModulesByFile(changedFile) ?? []) moduleGraph?.invalidateModule(module);
780
+ }
781
+ this.storyConfigs.set(storyId, config);
782
+ this.storyConfigTimestamps.set(storyId, timestamp);
783
+ const url = `${this.serverUrl}${PREVIEW_ROUTE_PREFIX}${storyId}`;
784
+ const htmlContent = this.generateHtmlTemplate(`${VIRTUAL_STORY_ENTRY_SCRIPT_PREFIX}${storyId}`, darkMode);
785
+ log.info(`[ViteReactBundlerService] Component served at: ${url}`);
786
+ return {
787
+ url,
788
+ htmlContent
789
+ };
790
+ } catch (error) {
791
+ const err = /* @__PURE__ */ new Error(`Failed to serve component ${storyName}: ${error instanceof Error ? error.message : String(error)}`);
792
+ err.cause = error;
793
+ throw err;
794
+ }
795
+ }
796
+ /**
797
+ * Pre-render a component to a static HTML file.
798
+ * @param options - Component rendering options
799
+ * @returns Promise resolving to the HTML file path
800
+ * @throws Error if build fails
801
+ */
802
+ async prerenderComponent(options) {
803
+ const { componentPath, storyName, args = {}, darkMode = false, appPath, cssFiles = [], rootComponent, unistylesConfig } = options;
804
+ validateStoryName(storyName);
805
+ validateComponentPath(componentPath);
806
+ validateArgsSize(args);
807
+ const monorepoRoot = await (this.monorepoRoot ??= TemplatesManagerService.getWorkspaceRoot());
808
+ const resolvedAppPath = path.isAbsolute(appPath) ? appPath : path.join(monorepoRoot, appPath);
809
+ validateCssFiles(cssFiles, this.workspaceRoot ?? await TemplatesManagerService.getWorkspaceRoot(resolvedAppPath));
810
+ try {
811
+ return { htmlFilePath: await this.buildComponent({
812
+ componentPath,
813
+ storyName,
814
+ args,
815
+ appPath: resolvedAppPath,
816
+ darkMode,
817
+ cssFiles,
818
+ rootComponent,
819
+ unistylesConfig
820
+ }) };
821
+ } catch (error) {
822
+ const err = /* @__PURE__ */ new Error(`Failed to prerender component ${storyName}: ${error instanceof Error ? error.message : String(error)}`);
823
+ err.cause = error;
824
+ throw err;
825
+ }
826
+ }
827
+ /**
828
+ * Get Vite resolve configuration for the given app path.
829
+ * Subclasses can override to add platform-specific aliases (e.g., react-native-web).
830
+ */
831
+ async getViteResolveConfig(appPath) {
832
+ const workspaceRoot = this.workspaceRoot ?? await TemplatesManagerService.getWorkspaceRoot(appPath);
833
+ const frontendPackagePath = (packageDir, sourcePath = "index.ts") => path.join(workspaceRoot, WORKSPACE_PACKAGE_DIR_PREFIX, "frontend", packageDir, APP_SOURCE_DIR$1, sourcePath);
834
+ return { alias: [
835
+ ...appReactAliases(appPath),
836
+ {
837
+ find: APP_SOURCE_ALIAS_PATTERN,
838
+ replacement: `${path.join(appPath, APP_SOURCE_DIR$1)}/`
839
+ },
840
+ {
841
+ find: WEB_THEME_PACKAGE_PATTERN,
842
+ replacement: frontendPackagePath("web-theme", "modules/tailwindMerge.ts")
843
+ },
844
+ {
845
+ find: /^@agimonai\/frontend-web-theme\/modules\/(.*)$/,
846
+ replacement: `${path.join(workspaceRoot, WORKSPACE_PACKAGE_DIR_PREFIX, "frontend", "web-theme", APP_SOURCE_DIR$1, "modules")}/$1`
847
+ }
848
+ ] };
849
+ }
850
+ /**
851
+ * Get Vite define replacements.
852
+ * Subclasses can override to provide platform globals.
853
+ */
854
+ getViteDefineConfig() {}
855
+ /**
856
+ * Generate preview HTML styles.
857
+ * Subclasses can override for platform-specific layout constraints.
858
+ */
859
+ generatePreviewStyles() {
860
+ return "body { margin: 0; padding: 0; background: var(--color-background-shade, var(--color-background, #fff)); color: var(--color-foreground); }\n #root { display: inline-block; }";
861
+ }
862
+ /**
863
+ * Get Vite esbuild configuration.
864
+ * Subclasses can override to handle platform-specific JSX needs.
865
+ */
866
+ getViteEsbuildConfig() {
867
+ return {
868
+ jsx: JSX_RUNTIME_AUTOMATIC,
869
+ jsxImportSource: REACT_FRAMEWORK_ID
870
+ };
871
+ }
872
+ /**
873
+ * Get Vite Oxc configuration.
874
+ * Subclasses can override to disable Oxc for platform-specific parsing needs.
875
+ */
876
+ getViteOxcConfig() {}
877
+ /**
878
+ * Get additional Vite plugins.
879
+ * Subclasses can override to add platform-specific plugins (e.g., @vitejs/plugin-react).
880
+ */
881
+ getAdditionalVitePlugins(_appPath) {
882
+ return [];
883
+ }
884
+ /**
885
+ * Clean up server resources and reset state.
886
+ * Closes the Vite dev server if running.
887
+ *
888
+ * Note: Errors during server close are intentionally logged but not re-thrown.
889
+ * This ensures cleanup always completes and state is reset, even if the server
890
+ * is in an unexpected state. Callers should not depend on cleanup failure detection.
891
+ */
892
+ async cleanup() {
893
+ if (this.server) {
894
+ log.info("[ViteReactBundlerService] Closing Vite dev server...");
895
+ try {
896
+ await this.server.close();
897
+ } catch (error) {
898
+ log.error(`[ViteReactBundlerService] Error closing server: ${error instanceof Error ? error.message : String(error)}`);
899
+ }
900
+ this.server = null;
901
+ this.serverUrl = null;
902
+ this.serverPort = null;
903
+ this.currentAppPath = null;
904
+ this.serverStartPromise = null;
905
+ this.storyConfigs.clear();
906
+ this.storyConfigTimestamps.clear();
907
+ await this.disposeDevStore();
908
+ log.info("[ViteReactBundlerService] Vite dev server closed");
909
+ }
910
+ }
911
+ async disposeDevStore() {
912
+ const store = this.devStore;
913
+ this.devStore = null;
914
+ await store?.dispose().catch((error) => log.warn(`[ViteReactBundlerService] Failed to remove virtual files: ${error.message}`));
915
+ }
916
+ /**
917
+ * Store the files one render reads: the wrapper CSS and snapshots of its declared sources.
918
+ * Transitive imports, nested CSS imports and node_modules are still read from disk.
919
+ * @returns Snapshotted source paths whose content changed
920
+ */
921
+ async snapshotRender(store, renderId, options) {
922
+ const { appPath, componentPath, rootComponent, unistylesConfig } = options;
923
+ const declaredSources = [componentPath, ...[rootComponent, unistylesConfig].filter((file) => !!file && path.extname(file) !== "").map((file) => path.join(appPath, file))];
924
+ const changed = [];
925
+ for (const filePath of declaredSources) if (store.put(filePath, renderId, await promises.readFile(filePath, UTF8_ENCODING))) changed.push(filePath);
926
+ store.put(options.wrapperCssPath, renderId, await this.generateWrapperCss(appPath, options.cssFiles));
927
+ return changed;
928
+ }
929
+ /**
930
+ * Clean up stale story configs to prevent memory leaks.
931
+ * Removes configs that are older than STORY_CONFIG_MAX_AGE_MS or
932
+ * when the number of configs exceeds STORY_CONFIG_MAX_COUNT.
933
+ */
934
+ cleanupStaleStoryConfigs() {
935
+ const now = Date.now();
936
+ const entriesToDelete = [];
937
+ for (const [storyId, timestamp] of this.storyConfigTimestamps) if (now - timestamp > STORY_CONFIG_MAX_AGE_MS) entriesToDelete.push(storyId);
938
+ for (const storyId of entriesToDelete) {
939
+ this.storyConfigs.delete(storyId);
940
+ this.storyConfigTimestamps.delete(storyId);
941
+ this.devStore?.evict(storyId);
942
+ }
943
+ if (entriesToDelete.length > 0) log.debug(`[ViteReactBundlerService] Cleaned up ${entriesToDelete.length} stale story configs`);
944
+ if (this.storyConfigs.size > STORY_CONFIG_MAX_COUNT) {
945
+ const toRemove = Array.from(this.storyConfigTimestamps.entries()).sort((a, b) => a[1] - b[1]).slice(0, this.storyConfigs.size - STORY_CONFIG_MAX_COUNT);
946
+ for (const [storyId] of toRemove) {
947
+ this.storyConfigs.delete(storyId);
948
+ this.storyConfigTimestamps.delete(storyId);
949
+ this.devStore?.evict(storyId);
950
+ }
951
+ log.debug(`[ViteReactBundlerService] Removed ${toRemove.length} oldest story configs to stay under limit`);
952
+ }
953
+ }
954
+ /**
955
+ * Generate the wrapper CSS with @source directives for Tailwind v4.
956
+ * Stylesheets inside the app are snapshotted into it, so a render does not see later edits or a
957
+ * theme file removed mid-render; package and workspace stylesheets stay imports.
958
+ * @param appPath - Absolute path to the app directory
959
+ * @param cssFiles - Array of CSS file paths to import
960
+ * @returns Generated CSS content with @source directive
961
+ */
962
+ async generateWrapperCss(appPath, cssFiles) {
963
+ const resolvedCssFiles = cssFiles.length > 0 ? cssFiles : await this.getDefaultCssFiles(appPath);
964
+ const workspaceRoot = this.workspaceRoot ?? await TemplatesManagerService.getWorkspaceRoot(appPath);
965
+ const cssImportStatements = (await Promise.all(resolvedCssFiles.map(async (cssFile) => {
966
+ if (cssFile.startsWith(PACKAGE_IMPORT_PREFIX) || cssFile.startsWith(TAILWIND_IMPORT_PREFIX)) return `@import '${cssFile}';`;
967
+ const filePath = path.isAbsolute(cssFile) ? cssFile : cssFile.startsWith(WORKSPACE_PACKAGE_DIR_PREFIX) || cssFile.startsWith(WORKSPACE_APP_DIR_PREFIX) ? path.join(workspaceRoot, cssFile) : path.join(appPath, cssFile);
968
+ if (!isContained(appPath, filePath) || path.extname(filePath).toLowerCase() !== CSS_FILE_EXTENSION) return `@import '${filePath}';`;
969
+ const css = await promises.readFile(filePath, UTF8_ENCODING);
970
+ return `/* ${path.relative(appPath, filePath)} */\n${flattenCss(css, path.dirname(filePath), viteRootDir(appPath))}`;
971
+ }))).join("\n");
972
+ return `${resolvedCssFiles.length === 0 ? "@import 'tailwindcss';\n" : ""}${cssImportStatements}
973
+
974
+ /* Tailwind v4 source configuration for component scanning */
975
+ @source "${path.join(workspaceRoot, WORKSPACE_APP_DIR_PREFIX)}";
976
+ @source "${path.join(workspaceRoot, WORKSPACE_PACKAGE_DIR_PREFIX, "frontend")}";
977
+ @source "${path.join(appPath, APP_SOURCE_DIR$1)}";
978
+ `;
979
+ }
980
+ /**
981
+ * Infer an app stylesheet when project.json does not define style-system.cssFiles.
982
+ * Many Vite apps keep their Tailwind/theme entrypoint in src/assets/main.css.
983
+ */
984
+ async getDefaultCssFiles(appPath) {
985
+ const discoveredFiles = [];
986
+ for (const candidate of DEFAULT_CSS_ENTRYPOINTS) {
987
+ const candidatePath = path.join(appPath, candidate);
988
+ try {
989
+ await promises.access(candidatePath);
990
+ discoveredFiles.push(candidate);
991
+ } catch {}
992
+ }
993
+ return discoveredFiles;
994
+ }
995
+ /**
996
+ * Generate the React entry file content for rendering a story.
997
+ * @param options - Build options including component path and story name
998
+ * @returns Generated TypeScript/JSX entry file content
999
+ */
1000
+ generateEntryFile(options) {
1001
+ const { componentPath, storyName, args, appPath, darkMode, rootComponent, wrapperCssPath } = options;
1002
+ const argsJson = JSON.stringify(args, null, JSON_INDENT_SPACES);
1003
+ const cssImports = `import '${toImportPath(wrapperCssPath)}';`;
1004
+ const frameworkSetupImports = this.generateFrameworkSetupImports(options);
1005
+ const storyImport = this.generateStoryImport(componentPath);
1006
+ const rootComponentImport = rootComponent ? `import { RootDocument } from '${toImportPath(path.join(appPath, rootComponent))}';` : "";
1007
+ const wrapWithRoot = rootComponent ? "RootDocument" : "React.Fragment";
1008
+ const wrapperProps = rootComponent ? `{ darkMode: ${darkMode} }` : "{}";
1009
+ return `${cssImports}
1010
+ ${frameworkSetupImports}
1011
+
1012
+ import React from 'react';
1013
+ import { createRoot } from 'react-dom/client';
1014
+ ${storyImport}
1015
+ ${rootComponentImport}
1016
+
1017
+ // Extract story metadata and the specific story to render
1018
+ const meta = Stories.default;
1019
+ const storyName = ${JSON.stringify(storyName)};
1020
+ const storyExportName = storyName.replace(/[^a-zA-Z0-9]/g, '');
1021
+ const Story = Stories[storyName] ?? Stories[storyExportName] ?? Object.values(Stories).find((story) => {
1022
+ return story?.name === storyName || story?.storyName === storyName;
1023
+ });
1024
+ const metaArgs = meta?.args || {};
1025
+ const storyArgs = Story?.args || {};
1026
+ // Merge story's default args with any custom args passed in
1027
+ const args = { ...metaArgs, ...storyArgs, ...${argsJson} };
1028
+ const storyContext = { loaded: {}, args };
1029
+
1030
+ // Resolve the story element in the same order style-system visualizes it:
1031
+ // 1. A render() function on the story export
1032
+ // 2. A render() function on the meta export
1033
+ // 3. A component reference in meta.component
1034
+ const renderStory = () => {
1035
+ if (Story?.render) {
1036
+ return Story.render(args, storyContext);
1037
+ }
1038
+
1039
+ if (meta.render) {
1040
+ return meta.render(args, storyContext);
1041
+ }
1042
+
1043
+ const Component = meta.component;
1044
+ if (!Component) {
1045
+ throw new Error('Story is missing meta.component and render functions');
1046
+ }
1047
+
1048
+ return React.createElement(Component, args);
1049
+ };
1050
+
1051
+ const decorators = [
1052
+ ...(Array.isArray(meta.decorators) ? meta.decorators : []),
1053
+ ...(Array.isArray(Story?.decorators) ? Story.decorators : []),
1054
+ ];
1055
+
1056
+ const applyDecorators = (storyFn, storyDecorators) =>
1057
+ storyDecorators.reduceRight((acc, decorator) => {
1058
+ return () => decorator(acc, storyContext);
1059
+ }, storyFn);
1060
+
1061
+ const RenderComponent = () => applyDecorators(renderStory, decorators)();
1062
+ const element = React.createElement(RenderComponent);
1063
+
1064
+ // Wrap element with root component (theme provider, etc.) if specified
1065
+ const Wrapper = ${wrapWithRoot};
1066
+ const wrappedElement = React.createElement(Wrapper, ${wrapperProps}, element);
1067
+ const frameworkWrappedElement = ${this.wrapFrameworkElement("wrappedElement", options)};
1068
+
1069
+ // Mount to DOM. The single-file Vite plugin may inline the module script into
1070
+ // <head>, so retry briefly until the parsed document contains the root node.
1071
+ let mountAttempts = 0;
1072
+ const mount = () => {
1073
+ const rootEl = document.getElementById('${ROOT_ELEMENT_ID}');
1074
+ if (!rootEl) {
1075
+ mountAttempts += 1;
1076
+ if (mountAttempts > 120) throw new Error('Root element not found');
1077
+ requestAnimationFrame(mount);
1078
+ return;
1079
+ }
1080
+ const root = createRoot(rootEl);
1081
+ root.render(frameworkWrappedElement);
1082
+ };
1083
+
1084
+ mount();
1085
+ `;
1086
+ }
1087
+ /**
1088
+ * Generate framework-specific setup imports that must run before story imports.
1089
+ * Native rendering overrides this to load app-specific runtime setup.
1090
+ */
1091
+ generateFrameworkSetupImports(_options) {
1092
+ return "";
1093
+ }
1094
+ generateStoryImport(componentPath) {
1095
+ return `import * as Stories from '${componentPath}';`;
1096
+ }
1097
+ wrapFrameworkElement(elementExpression, _options) {
1098
+ return elementExpression;
1099
+ }
1100
+ /**
1101
+ * Generate the HTML template for component preview.
1102
+ * @param entryFileName - Name of the entry file to include
1103
+ * @param darkMode - Whether to add dark mode class to HTML
1104
+ * @returns Generated HTML template string
1105
+ */
1106
+ generateHtmlTemplate(entryFileName, darkMode) {
1107
+ return `<!DOCTYPE html>
1108
+ <html class="${darkMode ? "dark" : ""}">
1109
+ <head>
1110
+ <meta charset="UTF-8">
1111
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1112
+ <title>Component Preview</title>
1113
+ <style>
1114
+ ${this.generatePreviewStyles()}
1115
+ </style>
1116
+ </head>
1117
+ <body>
1118
+ <div id="root"></div>
1119
+ <script type="module" src="/${entryFileName}"><\/script>
1120
+ </body>
1121
+ </html>`;
1122
+ }
1123
+ async buildComponent(options) {
1124
+ const { appPath, darkMode } = options;
1125
+ const renderId = randomUUID();
1126
+ const root = viteRootDir(appPath);
1127
+ const outDir = path.join(styleSystemTmpDir(appPath), `${BUILD_OUTPUT_DIR_PREFIX}${renderId}`);
1128
+ const htmlTemplateFileName = `${BUILD_HTML_FILE_PREFIX}${renderId}${HTML_FILE_EXTENSION}`;
1129
+ const htmlTemplatePath = path.join(root, htmlTemplateFileName);
1130
+ const buildOptions = {
1131
+ ...options,
1132
+ wrapperCssPath: path.join(root, `${WRAPPER_CSS_FILE_PREFIX}${renderId}${CSS_FILE_EXTENSION}`)
1133
+ };
1134
+ let store;
1135
+ try {
1136
+ await promises.mkdir(root, { recursive: true });
1137
+ store = await VirtualFileStore.open(path.join(styleSystemTmpDir(appPath), `${SCOPE_DIR_PREFIX}${renderId}`));
1138
+ await this.snapshotRender(store, renderId, buildOptions);
1139
+ const storyId = `${STORY_ID_BUILD_PREFIX}${renderId}`;
1140
+ const virtualModuleId = `${VIRTUAL_STORY_ENTRY_SCRIPT_PREFIX}${storyId}`;
1141
+ store.put(htmlTemplatePath, renderId, this.generateHtmlTemplate(virtualModuleId, darkMode));
1142
+ const { build } = await import("vite");
1143
+ const buildStoryConfigs = /* @__PURE__ */ new Map();
1144
+ buildStoryConfigs.set(storyId, buildOptions);
1145
+ await build({
1146
+ root,
1147
+ base: VITE_RELATIVE_BASE,
1148
+ mode: VITE_PREVIEW_MODE,
1149
+ envDir: appPath,
1150
+ configFile: false,
1151
+ logLevel: VITE_LOG_LEVEL,
1152
+ plugins: [
1153
+ createVirtualFilesPlugin(store, root),
1154
+ tailwindcss(),
1155
+ viteSingleFile(),
1156
+ createTanStackStartStubPlugin(),
1157
+ ...await this.getAdditionalVitePlugins(appPath),
1158
+ createStoryEntryPlugin((id) => buildStoryConfigs.get(id), (opts) => this.generateEntryFile(opts))
1159
+ ],
1160
+ resolve: await this.getViteResolveConfig(appPath),
1161
+ define: this.getViteDefineConfig(),
1162
+ esbuild: this.getViteEsbuildConfig(),
1163
+ oxc: this.getViteOxcConfig(),
1164
+ build: {
1165
+ outDir,
1166
+ emptyOutDir: false,
1167
+ cssCodeSplit: false,
1168
+ rollupOptions: {
1169
+ input: htmlTemplatePath,
1170
+ treeshake: false
1171
+ }
1172
+ }
1173
+ });
1174
+ const builtHtmlPath = path.join(outDir, htmlTemplateFileName);
1175
+ log.info(`[ViteReactBundlerService] Component built to: ${builtHtmlPath}`);
1176
+ return builtHtmlPath;
1177
+ } catch (error) {
1178
+ await promises.rm(outDir, {
1179
+ recursive: true,
1180
+ force: true
1181
+ });
1182
+ const err = /* @__PURE__ */ new Error(`Failed to build component: ${error instanceof Error ? error.message : String(error)}`);
1183
+ err.cause = error;
1184
+ throw err;
1185
+ } finally {
1186
+ await store?.dispose().catch((err) => log.debug(`[ViteReactBundlerService] Failed to cleanup virtual files: ${err.message}`));
1187
+ }
1188
+ }
1189
+ };
1190
+ //#endregion
1191
+ //#region src/services/BundlerService/ViteReactNativeBundlerService.ts
1192
+ /**
1193
+ * ViteReactNativeBundlerService
1194
+ *
1195
+ * Extends ViteReactBundlerService with React Native → web aliases
1196
+ * so native-ui components can be rendered in a browser via Playwright.
1197
+ * Uses the app's own React Native Web and Unistyles web runtime.
1198
+ *
1199
+ * Shim paths resolve relative to this package; React, react-dom and
1200
+ * react-native-web are peers and always resolve from the app.
1201
+ */
1202
+ const __dirname = dirname(fileURLToPath(import.meta.url));
1203
+ const REACT_NATIVE_PACKAGE = "react-native";
1204
+ const REACT_NATIVE_WEB_PACKAGE = "react-native-web";
1205
+ const REACT_NATIVE_UNISTYLES_PACKAGE = "react-native-unistyles";
1206
+ const REACT_NATIVE_UNISTYLES_BABEL_PLUGIN = "react-native-unistyles/plugin";
1207
+ const REACT_NATIVE_REANIMATED_BABEL_PLUGIN = "react-native-reanimated/plugin";
1208
+ const BABEL_CORE_PACKAGE = "@babel/core";
1209
+ const BABEL_PLUGIN_SYNTAX_JSX = "@babel/plugin-syntax-jsx";
1210
+ const BABEL_PLUGIN_SYNTAX_TYPESCRIPT = "@babel/plugin-syntax-typescript";
1211
+ const EXPO_SYMBOLS_PACKAGE = "expo-symbols";
1212
+ const EXPO_VECTOR_ICONS_PREFIX = "@expo/vector-icons";
1213
+ const STORYBOOK_REACT_NATIVE_PACKAGE = "@storybook/react-native";
1214
+ const STORYBOOK_REACT_PACKAGE = "@storybook/react";
1215
+ const SAFE_AREA_CONTEXT_PACKAGE = "react-native-safe-area-context";
1216
+ const APP_SOURCE_ALIAS = "@";
1217
+ const APP_SOURCE_DIR = "src";
1218
+ const VITE_PLUGIN_ENFORCE_PRE = "pre";
1219
+ const VITE_REACT_NATIVE_BUNDLER_ID = "vite-react-native";
1220
+ const REACT_NATIVE_OPTIMIZE_DEPS_PLUGIN_NAME = "vite-react-native-optimize-deps";
1221
+ const EXPO_ICONS_REDIRECT_PLUGIN_NAME = "vite-redirect-expo-vector-icons";
1222
+ const NATIVE_BABEL_TRANSFORM_PLUGIN_NAME = "vite-react-native-babel";
1223
+ const EXPO_VECTOR_ICONS_RESOLVED_MODULE_PREFIX = `\0virtual:style-system-expo-vector-icons`;
1224
+ const DEFAULT_ICON_FAMILY = "MaterialIcons";
1225
+ const JAVASCRIPT_EXTENSION = ".js";
1226
+ const EXPO_VECTOR_ICONS_FALLBACK_CODEPOINT = 9633;
1227
+ const EXPO_VECTOR_ICONS_SHIM_FILE = "expo-vector-icons.ts";
1228
+ const EXPO_SYMBOLS_SHIM_FILE = "expo-symbols.ts";
1229
+ const QUICK_BASE64_SHIM_FILE = "react-native-quick-base64.ts";
1230
+ const REQUIRED_REACT_NATIVE_SHIM_FILES = [EXPO_VECTOR_ICONS_SHIM_FILE, EXPO_SYMBOLS_SHIM_FILE];
1231
+ const PACKAGE_JSON_SUBPATH = "package.json";
1232
+ const REACT_NATIVE_UNISTYLES_WEB_INDEX = "lib/module/index.js";
1233
+ const REACT_NATIVE_UNISTYLES_NATIVE_COMPONENTS = "lib/module/components/native";
1234
+ const EXPO_VECTOR_ICONS_GLYPHMAP_DIR = "build/vendor/react-native-vector-icons/glyphmaps";
1235
+ const EXPO_VECTOR_ICONS_FONT_DIR = "build/vendor/react-native-vector-icons/Fonts";
1236
+ const GLYPHMAP_EXTENSION = ".json";
1237
+ const FONT_EXTENSION = ".ttf";
1238
+ const EXPO_MODULES_CORE_TYPE_DECLARATIONS_SEGMENT = "expo-modules-core/src/ts-declarations/";
1239
+ const EXPO_TYPE_DECLARATION_STUB_PLUGIN_NAME = "vite-stub-expo-type-declarations";
1240
+ const EMPTY_MODULE_CODE = "export {};";
1241
+ const SVG_COMPONENT_PLUGIN_NAME = "vite-react-native-svg-component";
1242
+ const SVG_EXTENSION = ".svg";
1243
+ const SVG_DATA_URI_PREFIX = "data:image/svg+xml;base64,";
1244
+ const SVG_DEFAULT_SIZE = 24;
1245
+ const BASE64_ENCODING = "base64";
1246
+ const VITE_QUERY_SEPARATOR = "?";
1247
+ const REACT_NATIVE_VISUAL_RENDER_SHIMS_LABEL = "React Native visual render shims directory";
1248
+ const REACT_NATIVE_DEV_GLOBAL = "__DEV__";
1249
+ const REACT_NATIVE_DEV_VALUE = "false";
1250
+ const REACT_NATIVE_GLOBAL = "global";
1251
+ const REACT_NATIVE_GLOBAL_VALUE = "globalThis";
1252
+ const EXPO_OS_DEFINE = "process.env.EXPO_OS";
1253
+ const EXPO_OS_WEB_VALUE = JSON.stringify("web");
1254
+ const VITE_RESOLVE_EXTENSIONS = [
1255
+ ".web.tsx",
1256
+ ".web.ts",
1257
+ ".web.js",
1258
+ ".tsx",
1259
+ ".ts",
1260
+ ".js",
1261
+ ".jsx",
1262
+ ".json"
1263
+ ];
1264
+ const BABEL_TRANSFORM_EXTENSION_PATTERN = /\.[cm]?[jt]sx?$/;
1265
+ const NODE_MODULES_PATH_SEGMENT = `${path.sep}node_modules${path.sep}`;
1266
+ const REACT_NATIVE_MARKDOWN_DISPLAY_PATH_SEGMENT = "react-native-markdown-display";
1267
+ const WINDOWS_PATH_SEPARATOR_PATTERN = /\\/g;
1268
+ const POSIX_PATH_SEPARATOR = "/";
1269
+ const SHIMS_DIR_CANDIDATES = [
1270
+ path.resolve(__dirname, "../../shims"),
1271
+ path.resolve(__dirname, "shims"),
1272
+ path.resolve(__dirname, "../shims"),
1273
+ path.resolve(__dirname, "../src/shims")
1274
+ ];
1275
+ async function resolveExistingDir(candidates, label, isValidDir) {
1276
+ for (const candidate of candidates) if (await isValidDir(candidate)) return candidate;
1277
+ throw new Error(`Could not resolve ${label}. Tried: ${candidates.join(", ")}`);
1278
+ }
1279
+ function resolvePackageDirFrom(fromPath, packageName) {
1280
+ return appPackageDir(fromPath, packageName);
1281
+ }
1282
+ function resolvePackageSubpathFrom(fromPath, subpath) {
1283
+ return createRequire(path.join(fromPath, PACKAGE_JSON_SUBPATH)).resolve(subpath);
1284
+ }
1285
+ function toViteImportPath(filePath) {
1286
+ return filePath.replace(WINDOWS_PATH_SEPARATOR_PATTERN, POSIX_PATH_SEPARATOR);
1287
+ }
1288
+ /**
1289
+ * Every icon family shipped by `@expo/vector-icons` that has both a glyph map and a
1290
+ * matching TrueType font. Metadata-only glyph maps (e.g. `FontAwesome5Free_meta.json`)
1291
+ * have no font and are skipped.
1292
+ */
1293
+ async function resolveExpoVectorIconFamilies(appPath) {
1294
+ const packageDir = dirname(resolvePackageSubpathFrom(appPath, `${EXPO_VECTOR_ICONS_PREFIX}/${PACKAGE_JSON_SUBPATH}`));
1295
+ const glyphMapDir = path.join(packageDir, EXPO_VECTOR_ICONS_GLYPHMAP_DIR);
1296
+ const fontDir = path.join(packageDir, EXPO_VECTOR_ICONS_FONT_DIR);
1297
+ const families = [];
1298
+ for (const entry of await readdir(glyphMapDir)) {
1299
+ if (!entry.endsWith(GLYPHMAP_EXTENSION)) continue;
1300
+ const family = entry.slice(0, -5);
1301
+ const fontPath = path.join(fontDir, `${family}${FONT_EXTENSION}`);
1302
+ if (!await pathExists(fontPath)) continue;
1303
+ families.push({
1304
+ family,
1305
+ fontPath: toViteImportPath(fontPath),
1306
+ glyphMapPath: toViteImportPath(path.join(glyphMapDir, entry))
1307
+ });
1308
+ }
1309
+ return families;
1310
+ }
1311
+ function getExpoVectorIconsVirtualModuleId(source, families) {
1312
+ if (source === EXPO_VECTOR_ICONS_PREFIX) return `${EXPO_VECTOR_ICONS_RESOLVED_MODULE_PREFIX}/index`;
1313
+ if (!source.startsWith(`${EXPO_VECTOR_ICONS_PREFIX}/`)) return null;
1314
+ const iconFamilyWithExtension = source.slice(19);
1315
+ const iconFamily = iconFamilyWithExtension.endsWith(JAVASCRIPT_EXTENSION) ? iconFamilyWithExtension.slice(0, -3) : iconFamilyWithExtension;
1316
+ if (!families.has(iconFamily)) return null;
1317
+ return `${EXPO_VECTOR_ICONS_RESOLVED_MODULE_PREFIX}/${iconFamily}`;
1318
+ }
1319
+ /**
1320
+ * Expo ships pure type declarations as `.ts` files that use value-import syntax.
1321
+ * They carry no runtime code, so the bundler stubs them instead of failing on
1322
+ * their missing exports.
1323
+ */
1324
+ function createExpoTypeDeclarationStubPlugin() {
1325
+ return {
1326
+ name: EXPO_TYPE_DECLARATION_STUB_PLUGIN_NAME,
1327
+ enforce: VITE_PLUGIN_ENFORCE_PRE,
1328
+ load(id) {
1329
+ return toViteImportPath(id).includes(EXPO_MODULES_CORE_TYPE_DECLARATIONS_SEGMENT) ? EMPTY_MODULE_CODE : null;
1330
+ }
1331
+ };
1332
+ }
1333
+ /**
1334
+ * Metro uses `react-native-svg-transformer` so `.svg` imports are React components.
1335
+ * Vite treats them as asset URLs, so the bundler wraps each SVG in an image
1336
+ * component that accepts the same width/height/style props.
1337
+ */
1338
+ function createSvgComponentPlugin() {
1339
+ return {
1340
+ name: SVG_COMPONENT_PLUGIN_NAME,
1341
+ enforce: VITE_PLUGIN_ENFORCE_PRE,
1342
+ async load(id) {
1343
+ const [filePath] = id.split(VITE_QUERY_SEPARATOR);
1344
+ if (!filePath?.endsWith(SVG_EXTENSION)) return null;
1345
+ let content;
1346
+ try {
1347
+ content = await readFile(filePath);
1348
+ } catch (error) {
1349
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
1350
+ throw error;
1351
+ }
1352
+ return `
1353
+ import React from 'react';
1354
+
1355
+ const svgSource = '${`${SVG_DATA_URI_PREFIX}${content.toString(BASE64_ENCODING)}`}';
1356
+
1357
+ function normalizeStyle(style) {
1358
+ if (Array.isArray(style)) return Object.assign({}, ...style.filter(Boolean));
1359
+ return style && typeof style === 'object' ? style : {};
1360
+ }
1361
+
1362
+ function SvgAsset({ width = ${SVG_DEFAULT_SIZE}, height = ${SVG_DEFAULT_SIZE}, style, ...rest }) {
1363
+ return React.createElement('img', {
1364
+ src: svgSource,
1365
+ width,
1366
+ height,
1367
+ alt: '',
1368
+ style: { width, height, objectFit: 'contain', ...normalizeStyle(style) },
1369
+ 'aria-hidden': 'true',
1370
+ });
1371
+ }
1372
+
1373
+ export default SvgAsset;
1374
+ export { svgSource as ReactComponent };
1375
+ `;
1376
+ }
1377
+ };
1378
+ }
1379
+ function createNativeBabelTransformPlugin(appPath) {
1380
+ const unistylesBabelPlugin = resolvePackageSubpathFrom(appPath, REACT_NATIVE_UNISTYLES_BABEL_PLUGIN);
1381
+ const reanimatedBabelPlugin = resolvePackageSubpathFrom(appPath, REACT_NATIVE_REANIMATED_BABEL_PLUGIN);
1382
+ const reanimatedPluginRequire = createRequire(reanimatedBabelPlugin);
1383
+ const babel = reanimatedPluginRequire(BABEL_CORE_PACKAGE);
1384
+ const jsxSyntaxPlugin = reanimatedPluginRequire.resolve(BABEL_PLUGIN_SYNTAX_JSX);
1385
+ const typescriptSyntaxPlugin = reanimatedPluginRequire.resolve(BABEL_PLUGIN_SYNTAX_TYPESCRIPT);
1386
+ return {
1387
+ name: NATIVE_BABEL_TRANSFORM_PLUGIN_NAME,
1388
+ enforce: VITE_PLUGIN_ENFORCE_PRE,
1389
+ async transform(code, id) {
1390
+ id = id.split(VITE_QUERY_SEPARATOR)[0] ?? id;
1391
+ const isFlowModule = toViteImportPath(id).includes("/@react-native-segmented-control/segmented-control/");
1392
+ const isSupportedNodeModule = isFlowModule || id.includes(REACT_NATIVE_MARKDOWN_DISPLAY_PATH_SEGMENT);
1393
+ if (id.includes(NODE_MODULES_PATH_SEGMENT) && !isSupportedNodeModule || !BABEL_TRANSFORM_EXTENSION_PATTERN.test(id)) return null;
1394
+ let syntaxPlugins = [[typescriptSyntaxPlugin, {
1395
+ isTSX: true,
1396
+ allExtensions: true
1397
+ }]];
1398
+ if (isFlowModule) {
1399
+ const expoPresetRequire = createRequire(resolvePackageSubpathFrom(appPath, "babel-preset-expo"));
1400
+ syntaxPlugins = [expoPresetRequire.resolve("@babel/plugin-transform-flow-strip-types"), expoPresetRequire.resolve("@babel/plugin-transform-react-jsx")];
1401
+ }
1402
+ const result = await babel.transformAsync(code, {
1403
+ filename: id,
1404
+ configFile: false,
1405
+ babelrc: false,
1406
+ plugins: [
1407
+ jsxSyntaxPlugin,
1408
+ ...syntaxPlugins,
1409
+ [unistylesBabelPlugin, {
1410
+ root: appPath,
1411
+ autoProcessPaths: [dirname(id)],
1412
+ autoProcessImports: [REACT_NATIVE_PACKAGE]
1413
+ }],
1414
+ reanimatedBabelPlugin
1415
+ ],
1416
+ sourceMaps: false
1417
+ });
1418
+ if (!result?.code) return null;
1419
+ return {
1420
+ code: result.code,
1421
+ map: null
1422
+ };
1423
+ }
1424
+ };
1425
+ }
1426
+ async function createExpoVectorIconsVirtualModule(appPath) {
1427
+ const iconFamilies = await resolveExpoVectorIconFamilies(appPath);
1428
+ const iconFamilyNames = new Set(iconFamilies.map(({ family }) => family));
1429
+ const assetImports = iconFamilies.map(({ family, fontPath, glyphMapPath }) => `import ${family}FontUrl from '${fontPath}?url';\n import ${family}GlyphMap from '${glyphMapPath}';`).join("\n ");
1430
+ return {
1431
+ name: EXPO_ICONS_REDIRECT_PLUGIN_NAME,
1432
+ enforce: VITE_PLUGIN_ENFORCE_PRE,
1433
+ resolveId(source) {
1434
+ return getExpoVectorIconsVirtualModuleId(source, iconFamilyNames);
1435
+ },
1436
+ load(id) {
1437
+ if (!id.startsWith(EXPO_VECTOR_ICONS_RESOLVED_MODULE_PREFIX)) return null;
1438
+ const requestedFamily = id.slice(EXPO_VECTOR_ICONS_RESOLVED_MODULE_PREFIX.length + 1);
1439
+ const defaultFamily = iconFamilyNames.has(requestedFamily) ? requestedFamily : DEFAULT_ICON_FAMILY;
1440
+ const familyExports = iconFamilies.map(({ family }) => `export const ${family} = /* @__PURE__ */ createIconComponent('${family}', ${family}FontUrl, ${family}GlyphMap);`).join("\n ");
1441
+ return `
1442
+ // @ts-check
1443
+ import React from 'react';
1444
+ ${assetImports}
1445
+
1446
+ const loadedFonts = new Set();
1447
+
1448
+ /**
1449
+ * @param {unknown} style
1450
+ * @returns {Record<string, unknown>}
1451
+ */
1452
+ function normalizeStyle(style) {
1453
+ if (Array.isArray(style)) return Object.assign({}, ...style.filter(Boolean));
1454
+ return style && typeof style === 'object' ? style : {};
1455
+ }
1456
+
1457
+ /**
1458
+ * @param {string} fontFamily
1459
+ * @param {string} fontUrl
1460
+ */
1461
+ function ensureFontFace(fontFamily, fontUrl) {
1462
+ if (loadedFonts.has(fontFamily) || typeof document === 'undefined') return;
1463
+ const style = document.createElement('style');
1464
+ style.textContent = '@font-face { font-family: "' + fontFamily + '"; src: url("' + fontUrl + '") format("truetype"); font-weight: normal; font-style: normal; font-display: block; }';
1465
+ document.head.appendChild(style);
1466
+ loadedFonts.add(fontFamily);
1467
+ }
1468
+
1469
+ /**
1470
+ * @param {string} fontFamily
1471
+ * @param {string} fontUrl
1472
+ * @param {Record<string, number>} glyphMap
1473
+ */
1474
+ function createIconComponent(fontFamily, fontUrl, glyphMap) {
1475
+ /**
1476
+ * @param {{ name: string; size?: number; color?: string; style?: unknown }} props
1477
+ */
1478
+ function Icon({ name, size = 24, color = '#000', style }) {
1479
+ ensureFontFace(fontFamily, fontUrl);
1480
+ const glyph = glyphMap[name];
1481
+ return React.createElement('span', {
1482
+ style: {
1483
+ fontSize: size,
1484
+ color,
1485
+ fontFamily,
1486
+ fontWeight: 'normal',
1487
+ fontStyle: 'normal',
1488
+ lineHeight: 1,
1489
+ letterSpacing: 'normal',
1490
+ textTransform: 'none',
1491
+ display: 'inline-block',
1492
+ width: size,
1493
+ height: size,
1494
+ overflow: 'hidden',
1495
+ textAlign: 'center',
1496
+ whiteSpace: 'nowrap',
1497
+ verticalAlign: 'middle',
1498
+ WebkitFontSmoothing: 'antialiased',
1499
+ ...normalizeStyle(style),
1500
+ },
1501
+ children: String.fromCodePoint(typeof glyph === 'number' ? glyph : ${EXPO_VECTOR_ICONS_FALLBACK_CODEPOINT}),
1502
+ 'aria-hidden': 'true',
1503
+ });
1504
+ }
1505
+ Icon.glyphMap = glyphMap;
1506
+ return Icon;
1507
+ }
1508
+
1509
+ ${familyExports}
1510
+ const SelectedIcon = ${defaultFamily};
1511
+ export { SelectedIcon as default };
1512
+ `;
1513
+ }
1514
+ };
1515
+ }
1516
+ function resolveShimsDir() {
1517
+ return resolveExistingDir(SHIMS_DIR_CANDIDATES, REACT_NATIVE_VISUAL_RENDER_SHIMS_LABEL, hasRequiredReactNativeShims);
1518
+ }
1519
+ async function hasRequiredReactNativeShims(candidate) {
1520
+ return (await Promise.all(REQUIRED_REACT_NATIVE_SHIM_FILES.map((shimFile) => pathExists(path.join(candidate, shimFile))))).every(Boolean);
1521
+ }
1522
+ var ViteReactNativeBundlerService = class ViteReactNativeBundlerService extends ViteReactBundlerService {
1523
+ static nativeInstance = null;
1524
+ static getInstance() {
1525
+ if (!ViteReactNativeBundlerService.nativeInstance) ViteReactNativeBundlerService.nativeInstance = new ViteReactNativeBundlerService();
1526
+ return ViteReactNativeBundlerService.nativeInstance;
1527
+ }
1528
+ getBundlerId() {
1529
+ return VITE_REACT_NATIVE_BUNDLER_ID;
1530
+ }
1531
+ async getAdditionalVitePlugins(appPath) {
1532
+ return [
1533
+ {
1534
+ name: REACT_NATIVE_OPTIMIZE_DEPS_PLUGIN_NAME,
1535
+ config: (config) => ({ optimizeDeps: {
1536
+ rolldownOptions: {
1537
+ resolve: { extensions: config.resolve?.extensions },
1538
+ moduleTypes: {
1539
+ ".ts": "ts",
1540
+ ".tsx": "tsx"
1541
+ }
1542
+ },
1543
+ exclude: [
1544
+ "@react-native-segmented-control/segmented-control",
1545
+ SAFE_AREA_CONTEXT_PACKAGE,
1546
+ "expo-haptics",
1547
+ "expo-modules-core",
1548
+ "react-native-gesture-handler"
1549
+ ],
1550
+ include: ["react-native-gesture-handler > hoist-non-react-statics", "react-native-gesture-handler > invariant"]
1551
+ } })
1552
+ },
1553
+ createExpoTypeDeclarationStubPlugin(),
1554
+ createSvgComponentPlugin(),
1555
+ ...appPath ? [await createExpoVectorIconsVirtualModule(appPath)] : [],
1556
+ ...appPath ? [createNativeBabelTransformPlugin(appPath)] : [],
1557
+ ...react()
1558
+ ];
1559
+ }
1560
+ generateFrameworkSetupImports(options) {
1561
+ const { appPath, darkMode, unistylesConfig } = options;
1562
+ return `${unistylesConfig ? `import '${toViteImportPath(path.join(appPath, unistylesConfig))}';
1563
+ import { UnistylesRuntime } from '${REACT_NATIVE_UNISTYLES_PACKAGE}';
1564
+
1565
+ UnistylesRuntime.setAdaptiveThemes(false);
1566
+ UnistylesRuntime.setTheme('${darkMode ? "dark" : "light"}');` : ""}
1567
+ import { SafeAreaProvider } from '${SAFE_AREA_CONTEXT_PACKAGE}';
1568
+
1569
+ const styleSystemSafeAreaMetrics = {
1570
+ frame: { x: 0, y: 0, width: 393, height: 852 },
1571
+ insets: { top: 0, right: 0, bottom: 0, left: 0 },
1572
+ };`;
1573
+ }
1574
+ generateStoryImport(componentPath) {
1575
+ return `const Stories = await import('${componentPath}');`;
1576
+ }
1577
+ wrapFrameworkElement(elementExpression) {
1578
+ return `React.createElement(SafeAreaProvider, { initialMetrics: styleSystemSafeAreaMetrics }, ${elementExpression})`;
1579
+ }
1580
+ getViteEsbuildConfig() {}
1581
+ getViteOxcConfig() {
1582
+ return false;
1583
+ }
1584
+ getViteDefineConfig() {
1585
+ return {
1586
+ [REACT_NATIVE_DEV_GLOBAL]: REACT_NATIVE_DEV_VALUE,
1587
+ [REACT_NATIVE_GLOBAL]: REACT_NATIVE_GLOBAL_VALUE,
1588
+ [EXPO_OS_DEFINE]: EXPO_OS_WEB_VALUE
1589
+ };
1590
+ }
1591
+ generatePreviewStyles() {
1592
+ return `html { color-scheme: light; }
1593
+ html.dark { color-scheme: dark; }
1594
+ body {
1595
+ margin: 0;
1596
+ padding: 0;
1597
+ background: var(--colors-background, Canvas);
1598
+ color: var(--colors-foreground, CanvasText);
1599
+ }
1600
+ #root {
1601
+ display: block;
1602
+ width: 100%;
1603
+ min-height: 100vh;
1604
+ overflow: hidden;
1605
+ }
1606
+ #root > * {
1607
+ min-height: 100vh;
1608
+ }`;
1609
+ }
1610
+ async getViteResolveConfig(appPath) {
1611
+ const shimsDir = await resolveShimsDir();
1612
+ const reactNativeUnistylesDir = resolvePackageDirFrom(appPath, REACT_NATIVE_UNISTYLES_PACKAGE);
1613
+ const reactNativeUnistylesEntry = path.join(reactNativeUnistylesDir, REACT_NATIVE_UNISTYLES_WEB_INDEX);
1614
+ const reactNativeWebDir = resolvePackageDirFrom(appPath, REACT_NATIVE_WEB_PACKAGE);
1615
+ return {
1616
+ extensions: [...VITE_RESOLVE_EXTENSIONS],
1617
+ alias: [
1618
+ ...appReactAliases(appPath),
1619
+ {
1620
+ find: APP_SOURCE_ALIAS,
1621
+ replacement: path.join(appPath, APP_SOURCE_DIR)
1622
+ },
1623
+ {
1624
+ find: /^react-native$/,
1625
+ replacement: reactNativeWebDir
1626
+ },
1627
+ {
1628
+ find: /^react-native-unistyles\/components\/native\/(.+)$/,
1629
+ replacement: path.join(reactNativeUnistylesDir, REACT_NATIVE_UNISTYLES_NATIVE_COMPONENTS, "$1.js")
1630
+ },
1631
+ {
1632
+ find: new RegExp(`^${REACT_NATIVE_UNISTYLES_PACKAGE}$`),
1633
+ replacement: reactNativeUnistylesEntry
1634
+ },
1635
+ {
1636
+ find: EXPO_SYMBOLS_PACKAGE,
1637
+ replacement: path.join(shimsDir, EXPO_SYMBOLS_SHIM_FILE)
1638
+ },
1639
+ {
1640
+ find: /^react-native-quick-base64$/,
1641
+ replacement: path.join(shimsDir, QUICK_BASE64_SHIM_FILE)
1642
+ },
1643
+ {
1644
+ find: STORYBOOK_REACT_NATIVE_PACKAGE,
1645
+ replacement: STORYBOOK_REACT_PACKAGE
1646
+ }
1647
+ ]
1648
+ };
1649
+ }
1650
+ };
1651
+ //#endregion
1652
+ //#region src/services/BundlerService/childProtocol.ts
1653
+ const bundlerKeySchema = z.enum(["vite-react", "vite-react-native"]);
1654
+ const renderOptions = z.object({
1655
+ componentPath: z.string().min(1),
1656
+ storyName: z.string().min(1),
1657
+ appPath: z.string().min(1),
1658
+ args: z.record(z.string(), z.unknown()).optional(),
1659
+ themePath: z.string().optional(),
1660
+ darkMode: z.boolean().optional(),
1661
+ cssFiles: z.array(z.string()).optional(),
1662
+ rootComponent: z.string().optional(),
1663
+ unistylesConfig: z.string().optional()
1664
+ });
1665
+ const id = z.number().int().positive();
1666
+ const childRequestSchema = z.discriminatedUnion("method", [
1667
+ z.object({
1668
+ id,
1669
+ method: z.literal("initialize"),
1670
+ params: z.object({
1671
+ bundler: bundlerKeySchema,
1672
+ workspaceRoot: z.string().optional()
1673
+ })
1674
+ }),
1675
+ z.object({
1676
+ id,
1677
+ method: z.literal("startDevServer"),
1678
+ params: z.string().min(1)
1679
+ }),
1680
+ z.object({
1681
+ id,
1682
+ method: z.literal("serveComponent"),
1683
+ params: renderOptions
1684
+ }),
1685
+ z.object({
1686
+ id,
1687
+ method: z.literal("prerenderComponent"),
1688
+ params: renderOptions
1689
+ }),
1690
+ z.object({
1691
+ id,
1692
+ method: z.literal("cleanup"),
1693
+ params: z.null()
1694
+ })
1695
+ ]);
1696
+ const childStateSchema = z.object({
1697
+ url: z.string().nullable(),
1698
+ port: z.number().int().min(0).max(65535).nullable(),
1699
+ appPath: z.string().nullable()
1700
+ });
1701
+ const childReplySchema = z.discriminatedUnion("ok", [z.object({
1702
+ id,
1703
+ ok: z.literal(true),
1704
+ result: z.unknown(),
1705
+ state: childStateSchema
1706
+ }), z.object({
1707
+ id,
1708
+ ok: z.literal(false),
1709
+ error: z.object({
1710
+ name: z.string(),
1711
+ message: z.string(),
1712
+ stack: z.string().optional()
1713
+ }),
1714
+ state: childStateSchema
1715
+ })]);
1716
+ const childResultSchemas = {
1717
+ initialize: z.null(),
1718
+ startDevServer: z.object({
1719
+ url: z.string(),
1720
+ port: z.number().int().min(0).max(65535)
1721
+ }),
1722
+ serveComponent: z.object({
1723
+ url: z.string(),
1724
+ htmlFilePath: z.string().optional(),
1725
+ htmlContent: z.string().optional()
1726
+ }),
1727
+ prerenderComponent: z.object({ htmlFilePath: z.string().min(1) }),
1728
+ cleanup: z.null()
1729
+ };
1730
+ //#endregion
1731
+ export { ViteReactBundlerService as a, isContained as c, ViteReactNativeBundlerService as i, createLog as l, childRequestSchema as n, BaseBundlerService as o, childResultSchemas as r, TemplatesManagerService as s, childReplySchema as t, log as u };