@agimon-ai/po-style-system-tooling 0.2.14-alpha.38 → 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.
- package/dist/childProtocol-B7bkYqKI.mjs +1731 -0
- package/dist/childProtocol-iFAmYpDL.cjs +1822 -0
- package/dist/cli.cjs +5 -4
- package/dist/cli.mjs +2 -1
- package/dist/index.cjs +44 -17
- package/dist/index.d.cts +50 -19
- package/dist/index.d.mts +49 -18
- package/dist/index.mjs +33 -8
- package/dist/{stdio-B5wufO5-.mjs → stdio-BBoggLRy.mjs} +212 -1644
- package/dist/{stdio-x6EuV64Q.cjs → stdio-ngsbSNiY.cjs} +282 -1768
- package/dist/vite-child.cjs +70 -0
- package/dist/vite-child.d.cts +1 -0
- package/dist/vite-child.d.mts +1 -0
- package/dist/vite-child.mjs +71 -0
- package/package.json +1 -1
|
@@ -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 };
|