@addfox/cli 0.1.1-beta.2

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/cli.js ADDED
@@ -0,0 +1,1022 @@
1
+ #!/usr/bin/env node
2
+ import { dirname, resolve as external_path_resolve } from "path";
3
+ import { createWriteStream, existsSync as external_fs_existsSync, mkdirSync, readFileSync as external_fs_readFileSync, readdirSync, statSync, watch } from "fs";
4
+ import { mergeRsbuildConfig } from "@rsbuild/core";
5
+ import { ADDFOX_ERROR_CODES, AddfoxError, HookManager, error, exitWithError, formatError, getWebExtStdoutOriginDepth, log as common_log, logDone, logDoneTimed, logDoneWithValue, setAddfoxLoggerRawWrites, warn as common_warn } from "@addfox/common";
6
+ import { ADDFOX_OUTPUT_ROOT, CLI_COMMANDS, CONFIG_FILES, HMR_WS_PORT, clearConfigCache, getBrowserOutputDir, getManifestRecordForTarget, getResolvedConfigFilePath, getResolvedRstestConfigFilePath, resolveAddfoxConfig, toReloadManagerEntries } from "@addfox/core";
7
+ import { Pipeline } from "@addfox/core/pipeline";
8
+ import { hmrPlugin, launchBrowserOnly } from "@addfox/rsbuild-plugin-extension-hmr";
9
+ import { detectFromLockfile as pkg_manager_detectFromLockfile, getAddCommand, getMissingPackages } from "@addfox/pkg-manager";
10
+ import { entryPlugin } from "@addfox/rsbuild-plugin-extension-entry";
11
+ import { extensionPlugin } from "@addfox/rsbuild-plugin-extension-manifest";
12
+ import { monitorPlugin } from "@addfox/rsbuild-plugin-extension-monitor";
13
+ import { getVueRsbuildPlugins } from "@addfox/rsbuild-plugin-vue";
14
+ import { spawnSync as external_child_process_spawnSync } from "child_process";
15
+ import archiver from "archiver";
16
+ import { fileURLToPath } from "url";
17
+ import { createRequire } from "module";
18
+ function expandUserPlugins(userPlugins, appRoot) {
19
+ const out = [];
20
+ const list = userPlugins ?? [];
21
+ const arr = Array.isArray(list) ? list : [
22
+ list
23
+ ];
24
+ for (const p of arr){
25
+ const name = p?.name;
26
+ if ("rsbuild-plugin-vue" === name) {
27
+ const vuePlugins = getVueRsbuildPlugins(appRoot);
28
+ if (Array.isArray(vuePlugins)) out.push(...vuePlugins);
29
+ }
30
+ out.push(p);
31
+ }
32
+ return out;
33
+ }
34
+ function buildFrameworkPluginList(ctx) {
35
+ const useEntry = false !== ctx.config.entry;
36
+ const expanded = expandUserPlugins(ctx.config.plugins, ctx.root);
37
+ const useMonitor = ctx.isDev && true === ctx.config.debug;
38
+ const list = [];
39
+ if (useEntry) list.push(entryPlugin(ctx.config, ctx.entries, ctx.distPath, {
40
+ browser: ctx.browser
41
+ }));
42
+ list.push(...expanded);
43
+ if (useMonitor) list.push(monitorPlugin(ctx.config, ctx.entries, ctx.browser));
44
+ list.push(extensionPlugin(ctx.config, ctx.entries, ctx.browser, ctx.distPath));
45
+ return list;
46
+ }
47
+ class Pipeline_Pipeline {
48
+ hookManager;
49
+ corePipeline;
50
+ options;
51
+ constructor(options){
52
+ this.options = options;
53
+ this.hookManager = new HookManager();
54
+ this.corePipeline = new Pipeline(this.hookManager);
55
+ this.registerDefaultHooks();
56
+ }
57
+ get hooks() {
58
+ return this.hookManager;
59
+ }
60
+ async run() {
61
+ return this.corePipeline.execute(this.options.root, []);
62
+ }
63
+ registerDefaultHooks() {
64
+ this.hookManager.register('load', 'after', async (ctx)=>{
65
+ if (this.options.config) {
66
+ ctx.config = this.options.config;
67
+ ctx.baseEntries = this.options.baseEntries ?? [];
68
+ ctx.entries = this.options.entries ?? [];
69
+ } else {
70
+ const result = resolveAddfoxConfig(ctx.root);
71
+ ctx.config = result.config;
72
+ ctx.baseEntries = result.baseEntries;
73
+ ctx.entries = result.entries;
74
+ }
75
+ });
76
+ this.hookManager.register('resolve', 'after', async (ctx)=>{
77
+ ctx.command = this.options.command;
78
+ ctx.browser = this.options.browser;
79
+ ctx.cache = this.options.cache;
80
+ ctx.report = this.options.report;
81
+ ctx.isDev = 'dev' === this.options.command;
82
+ const browserSubDir = getBrowserOutputDir(ctx.browser);
83
+ ctx.distPath = external_path_resolve(ctx.root, ctx.config.outputRoot, ctx.config.outDir, browserSubDir);
84
+ if (void 0 !== this.options.debug) ctx.config = {
85
+ ...ctx.config,
86
+ debug: this.options.debug
87
+ };
88
+ if ('chromium' === ctx.browser) {
89
+ const record = getManifestRecordForTarget(ctx.config.manifest, ctx.browser);
90
+ if (record?.manifest_version === 2) common_warn('Warning: MV2 has been deprecated for Chrome. Please use MV3.');
91
+ }
92
+ });
93
+ this.hookManager.register('build', 'after', async (ctx)=>{
94
+ ctx.rsbuild = await this.buildRsbuildConfig(ctx);
95
+ });
96
+ }
97
+ async buildRsbuildConfig(ctx) {
98
+ const base = this.buildBaseRsbuildConfig(ctx);
99
+ const userConfig = await this.resolveUserRsbuildConfig(base, ctx.config);
100
+ let merged = mergeRsbuildConfig(base, userConfig);
101
+ if (ctx.isDev) {
102
+ const hmrOverrides = this.buildHmrOverrides(ctx);
103
+ if (hmrOverrides) merged = mergeRsbuildConfig(merged, hmrOverrides);
104
+ }
105
+ if (ctx.report) merged = await this.mergeRsdoctorPlugin(merged, ctx.root, ctx.config.outputRoot, ctx.report);
106
+ return merged;
107
+ }
108
+ buildBaseRsbuildConfig(ctx) {
109
+ const runtimeEnvConfig = buildRuntimeEnvConfig(ctx.config, ctx.browser, ctx.root, ctx.isDev);
110
+ const plugins = buildFrameworkPluginList(ctx);
111
+ const scopedProcessEnvPlugin = createScopedProcessEnvPlugin(runtimeEnvConfig.processEnvBase, runtimeEnvConfig.backgroundPrivateEnv);
112
+ if (scopedProcessEnvPlugin) plugins.push(scopedProcessEnvPlugin);
113
+ return {
114
+ root: ctx.root,
115
+ plugins,
116
+ source: {
117
+ define: runtimeEnvConfig.define
118
+ },
119
+ output: {
120
+ legalComments: 'none',
121
+ sourceMap: ctx.isDev ? {
122
+ js: 'inline-source-map'
123
+ } : false
124
+ }
125
+ };
126
+ }
127
+ async resolveUserRsbuildConfig(base, config) {
128
+ const user = config.rsbuild;
129
+ if ('function' == typeof user) return user(base, {
130
+ merge: mergeRsbuildConfig
131
+ });
132
+ return user && 'object' == typeof user ? user : {};
133
+ }
134
+ buildHmrOverrides(ctx) {
135
+ const hotReload = ctx.config.hotReload;
136
+ const hotReloadEnabled = false !== hotReload;
137
+ const hotReloadOpts = 'object' == typeof hotReload && null !== hotReload ? hotReload : void 0;
138
+ const isConfigRestart = '1' === process.env.ADDFOX_CONFIG_RESTART;
139
+ const reloadManagerEntries = toReloadManagerEntries(ctx.entries, ctx.root);
140
+ const browserPathConfig = ctx.config.browserPath ?? {};
141
+ const hmrOpts = {
142
+ distPath: ctx.distPath,
143
+ autoOpen: !isConfigRestart && false !== this.options.open,
144
+ browser: this.options.launch,
145
+ debug: ctx.config.debug,
146
+ root: ctx.root,
147
+ outputRoot: ctx.config.outputRoot,
148
+ chromePath: browserPathConfig.chrome,
149
+ chromiumPath: browserPathConfig.chromium,
150
+ edgePath: browserPathConfig.edge,
151
+ bravePath: browserPathConfig.brave,
152
+ vivaldiPath: browserPathConfig.vivaldi,
153
+ operaPath: browserPathConfig.opera,
154
+ santaPath: browserPathConfig.santa,
155
+ arcPath: browserPathConfig.arc,
156
+ yandexPath: browserPathConfig.yandex,
157
+ browserosPath: browserPathConfig.browseros,
158
+ customPath: browserPathConfig.custom,
159
+ firefoxPath: browserPathConfig.firefox,
160
+ cache: ctx.cache,
161
+ wsPort: hotReloadOpts?.port ?? HMR_WS_PORT,
162
+ enableReload: hotReloadEnabled,
163
+ autoRefreshContentPage: hotReloadEnabled ? hotReloadOpts?.autoRefreshContentPage ?? true : false,
164
+ reloadManagerEntries
165
+ };
166
+ const useRsbuildClientHmr = 'firefox' !== ctx.browser && hotReloadEnabled;
167
+ const devConfig = useRsbuildClientHmr ? {
168
+ hmr: true,
169
+ client: {
170
+ protocol: 'ws',
171
+ host: '127.0.0.1',
172
+ port: '<port>',
173
+ path: '/rsbuild-hmr'
174
+ },
175
+ liveReload: true,
176
+ writeToDisk: true
177
+ } : {
178
+ hmr: false,
179
+ liveReload: false,
180
+ writeToDisk: true
181
+ };
182
+ return {
183
+ dev: devConfig,
184
+ server: {
185
+ printUrls: false,
186
+ cors: {
187
+ origin: '*'
188
+ }
189
+ },
190
+ plugins: [
191
+ hmrPlugin(hmrOpts)
192
+ ]
193
+ };
194
+ }
195
+ async mergeRsdoctorPlugin(config, root, outputRoot, reportOption) {
196
+ const missing = getMissingPackages(root, [
197
+ '@rsdoctor/rspack-plugin'
198
+ ]);
199
+ if (missing.length > 0) {
200
+ const pm = pkg_manager_detectFromLockfile(root);
201
+ const cmd = getAddCommand(pm, missing.join(' '), true);
202
+ throw new AddfoxError({
203
+ code: ADDFOX_ERROR_CODES.RSDOCTOR_NOT_INSTALLED,
204
+ message: "Rsdoctor plugin not installed",
205
+ details: "report (-r/--report or config.report) requires @rsdoctor/rspack-plugin",
206
+ hint: `Install with: ${cmd}`
207
+ });
208
+ }
209
+ const reportDir = external_path_resolve(root, outputRoot, 'report');
210
+ const { RsdoctorRspackPlugin } = await import("@rsdoctor/rspack-plugin");
211
+ const tools = config.tools;
212
+ const existing = tools?.rspack?.plugins ?? [];
213
+ const pluginOptions = true === reportOption ? {
214
+ output: {
215
+ reportDir
216
+ },
217
+ mode: 'brief'
218
+ } : {
219
+ ...reportOption,
220
+ output: {
221
+ ...reportOption.output,
222
+ reportDir
223
+ }
224
+ };
225
+ const rsdoctorPlugin = new RsdoctorRspackPlugin(pluginOptions);
226
+ return mergeRsbuildConfig(config, {
227
+ tools: {
228
+ rspack: {
229
+ plugins: [
230
+ ...existing,
231
+ rsdoctorPlugin
232
+ ]
233
+ }
234
+ }
235
+ });
236
+ }
237
+ }
238
+ function getManifestVersionValue(config, browser) {
239
+ const manifestRecord = getManifestRecordForTarget(config.manifest, browser);
240
+ const mv = manifestRecord?.manifest_version;
241
+ if ("number" == typeof mv || "string" == typeof mv) return String(mv);
242
+ return "";
243
+ }
244
+ function buildRuntimeEnvConfig(config, browser, root, isDev) {
245
+ const mode = isDev ? "development" : "production";
246
+ const fileEnv = loadDotEnvByMode(root, mode);
247
+ const mergedEnv = {
248
+ ...fileEnv,
249
+ ...process.env
250
+ };
251
+ const envPrefixes = getLoadEnvPrefixes(config);
252
+ const publicEnv = pickPublicEnvVars(mergedEnv, envPrefixes);
253
+ const backgroundPrivateEnv = pickBackgroundPrivateEnvVars(mergedEnv, envPrefixes);
254
+ const manifestVersion = getManifestVersionValue(config, browser);
255
+ const importMetaEnv = {
256
+ ...publicEnv,
257
+ MODE: mode,
258
+ DEV: isDev,
259
+ PROD: !isDev,
260
+ BROWSER: browser,
261
+ MANIFEST_VERSION: manifestVersion
262
+ };
263
+ const processEnvForBundle = {
264
+ NODE_ENV: mode,
265
+ BROWSER: browser,
266
+ MANIFEST_VERSION: manifestVersion,
267
+ ...publicEnv
268
+ };
269
+ return {
270
+ define: {
271
+ "process.env": "globalThis.__ADDFOX_PROCESS_ENV__",
272
+ "import.meta.env": JSON.stringify(importMetaEnv),
273
+ "import.meta.env.BROWSER": JSON.stringify(browser),
274
+ "import.meta.env.MANIFEST_VERSION": JSON.stringify(manifestVersion)
275
+ },
276
+ processEnvBase: processEnvForBundle,
277
+ backgroundPrivateEnv
278
+ };
279
+ }
280
+ function getLoadEnvPrefixes(config) {
281
+ return config.envPrefix ?? [
282
+ "ADDFOX_PUBLIC_"
283
+ ];
284
+ }
285
+ function pickPublicEnvVars(env, prefixes) {
286
+ const result = {};
287
+ for (const [key, value] of Object.entries(env))if ("string" == typeof value) {
288
+ if (prefixes.some((prefix)=>key.startsWith(prefix))) result[key] = value;
289
+ }
290
+ return result;
291
+ }
292
+ function pickBackgroundPrivateEnvVars(env, publicPrefixes) {
293
+ const result = {};
294
+ for (const [key, value] of Object.entries(env)){
295
+ if ("string" == typeof value) {
296
+ if (key.startsWith("ADDFOX_")) {
297
+ if (!publicPrefixes.some((prefix)=>key.startsWith(prefix))) result[key] = value;
298
+ }
299
+ }
300
+ }
301
+ return result;
302
+ }
303
+ function createScopedProcessEnvPlugin(processEnvBase, privateEnv) {
304
+ if (0 === Object.keys(processEnvBase).length) return;
305
+ return {
306
+ name: "addfox-scoped-process-env",
307
+ setup (api) {
308
+ api.modifyRsbuildConfig((config)=>{
309
+ const source = config.source ?? {};
310
+ const entry = source.entry;
311
+ if (!entry) return;
312
+ const nextEntry = {
313
+ ...entry
314
+ };
315
+ const scopedEnvByEntry = buildScopedEnvByEntry(Object.keys(entry), processEnvBase, privateEnv);
316
+ for (const [entryName, entryValue] of Object.entries(entry)){
317
+ const scopedEnv = scopedEnvByEntry[entryName] ?? processEnvBase;
318
+ const snippet = buildScopedProcessEnvSnippet(scopedEnv);
319
+ const prepend = `data:text/javascript,${encodeURIComponent(snippet)}`;
320
+ nextEntry[entryName] = prependDataModule(entryValue, prepend);
321
+ }
322
+ config.source = {
323
+ ...source,
324
+ entry: nextEntry
325
+ };
326
+ });
327
+ }
328
+ };
329
+ }
330
+ function buildScopedEnvByEntry(entryNames, processEnvBase, privateEnv) {
331
+ const result = {};
332
+ for (const entryName of entryNames)result[entryName] = "background" === entryName ? {
333
+ ...processEnvBase,
334
+ ...privateEnv
335
+ } : {
336
+ ...processEnvBase
337
+ };
338
+ return result;
339
+ }
340
+ function buildScopedProcessEnvSnippet(processEnv) {
341
+ const envJson = JSON.stringify(processEnv);
342
+ return `
343
+ const addfoxScopedEnv = ${envJson};
344
+ if (!globalThis.__ADDFOX_PROCESS_ENV__) {
345
+ globalThis.__ADDFOX_PROCESS_ENV__ = {};
346
+ }
347
+ Object.assign(globalThis.__ADDFOX_PROCESS_ENV__, addfoxScopedEnv);
348
+ `;
349
+ }
350
+ function prependDataModule(entryValue, prependModule) {
351
+ if ("string" == typeof entryValue) return {
352
+ import: [
353
+ prependModule,
354
+ entryValue
355
+ ]
356
+ };
357
+ if (Array.isArray(entryValue.import)) return {
358
+ ...entryValue,
359
+ import: [
360
+ prependModule,
361
+ ...entryValue.import
362
+ ]
363
+ };
364
+ return {
365
+ ...entryValue,
366
+ import: [
367
+ prependModule,
368
+ entryValue.import
369
+ ]
370
+ };
371
+ }
372
+ function loadDotEnvByMode(root, mode) {
373
+ const result = {};
374
+ const files = [
375
+ ".env",
376
+ ".env.local",
377
+ `.env.${mode}`,
378
+ `.env.${mode}.local`
379
+ ];
380
+ for (const file of files){
381
+ const path = external_path_resolve(root, file);
382
+ if (!external_fs_existsSync(path)) continue;
383
+ const parsed = parseDotEnv(external_fs_readFileSync(path, "utf-8"));
384
+ Object.assign(result, parsed);
385
+ }
386
+ return result;
387
+ }
388
+ function parseDotEnv(content) {
389
+ const result = {};
390
+ const lines = content.split(/\r?\n/);
391
+ for (const line of lines){
392
+ const trimmed = line.trim();
393
+ if (!trimmed || trimmed.startsWith("#")) continue;
394
+ const eq = trimmed.indexOf("=");
395
+ if (eq <= 0) continue;
396
+ const key = trimmed.slice(0, eq).trim();
397
+ const rawValue = trimmed.slice(eq + 1).trim();
398
+ result[key] = stripQuotes(rawValue);
399
+ }
400
+ return result;
401
+ }
402
+ function stripQuotes(value) {
403
+ if (value.length < 2) return value;
404
+ const quote = value[0];
405
+ if ('"' !== quote && "'" !== quote || value[value.length - 1] !== quote) return value;
406
+ return value.slice(1, -1);
407
+ }
408
+ async function runPipeline(options) {
409
+ const pipeline = new Pipeline_Pipeline(options);
410
+ return pipeline.run();
411
+ }
412
+ const BROWSER_FLAGS = [
413
+ "-b",
414
+ "--browser"
415
+ ];
416
+ const REPORT_FLAGS = [
417
+ "-r",
418
+ "--report"
419
+ ];
420
+ const BROWSER_TO_TARGET = {
421
+ chromium: "chromium",
422
+ chrome: "chromium",
423
+ edge: "chromium",
424
+ brave: "chromium",
425
+ vivaldi: "chromium",
426
+ opera: "chromium",
427
+ santa: "chromium",
428
+ arc: "chromium",
429
+ yandex: "chromium",
430
+ browseros: "chromium",
431
+ custom: "chromium",
432
+ firefox: "firefox"
433
+ };
434
+ const VALID_LAUNCH_TARGETS = new Set([
435
+ "chrome",
436
+ "chromium",
437
+ "edge",
438
+ "brave",
439
+ "vivaldi",
440
+ "opera",
441
+ "santa",
442
+ "arc",
443
+ "yandex",
444
+ "browseros",
445
+ "custom",
446
+ "firefox"
447
+ ]);
448
+ class CliParser {
449
+ parse(argv) {
450
+ const cmdRaw = argv[0] ?? "dev";
451
+ const command = CLI_COMMANDS.includes(cmdRaw) ? cmdRaw : null;
452
+ if (null === command) throw new AddfoxError({
453
+ code: ADDFOX_ERROR_CODES.UNKNOWN_COMMAND,
454
+ message: "Unknown command",
455
+ details: `Command: "${cmdRaw}"`,
456
+ hint: "Supported: addfox dev | addfox build | addfox test [-b chrome|...]; custom requires browser.custom in config"
457
+ });
458
+ const { browser, launch, unknown: unknownBrowser } = this.getBrowserFromArgv(argv);
459
+ const cache = this.getCacheFromArgv(argv);
460
+ const debug = this.getDebugFromArgv(argv);
461
+ const report = this.getReportFromArgv(argv);
462
+ const open = this.getOpenFromArgv(argv);
463
+ return {
464
+ command,
465
+ browser,
466
+ launch,
467
+ unknownBrowser,
468
+ cache,
469
+ debug,
470
+ report,
471
+ open
472
+ };
473
+ }
474
+ getBrowserFromArgv(argv) {
475
+ for(let i = 0; i < argv.length; i++){
476
+ const arg = argv[i];
477
+ if (BROWSER_FLAGS.includes(arg)) {
478
+ const value = argv[i + 1];
479
+ if (value && !value.startsWith("-")) {
480
+ const normalized = value.trim().toLowerCase();
481
+ const browser = BROWSER_TO_TARGET[normalized];
482
+ if (browser) {
483
+ const launch = VALID_LAUNCH_TARGETS.has(normalized) ? normalized : void 0;
484
+ return {
485
+ browser,
486
+ launch
487
+ };
488
+ }
489
+ return {
490
+ unknown: value
491
+ };
492
+ }
493
+ }
494
+ if (arg.startsWith("-b=") || arg.startsWith("--browser=")) {
495
+ const normalized = (arg.split("=")[1] ?? "").trim().toLowerCase();
496
+ const browser = BROWSER_TO_TARGET[normalized];
497
+ if (browser) {
498
+ const launch = VALID_LAUNCH_TARGETS.has(normalized) ? normalized : void 0;
499
+ return {
500
+ browser,
501
+ launch
502
+ };
503
+ }
504
+ return {
505
+ unknown: normalized
506
+ };
507
+ }
508
+ }
509
+ return {};
510
+ }
511
+ getCacheFromArgv(argv) {
512
+ let cache;
513
+ for (const arg of argv){
514
+ if ("-c" === arg || "--cache" === arg) cache = true;
515
+ if ("--no-cache" === arg) cache = false;
516
+ }
517
+ return cache;
518
+ }
519
+ getDebugFromArgv(argv) {
520
+ return argv.some((arg)=>"--debug" === arg) ? true : void 0;
521
+ }
522
+ getReportFromArgv(argv) {
523
+ return argv.some((arg)=>REPORT_FLAGS.includes(arg)) ? true : void 0;
524
+ }
525
+ getOpenFromArgv(argv) {
526
+ return !argv.some((arg)=>"--no-open" === arg);
527
+ }
528
+ assertSupportedBrowser(value) {
529
+ const b = BROWSER_TO_TARGET[value.trim().toLowerCase()];
530
+ if (b) return;
531
+ throw new AddfoxError({
532
+ code: ADDFOX_ERROR_CODES.INVALID_BROWSER,
533
+ message: "Unsupported browser argument",
534
+ details: `Current value: "${value}"`,
535
+ hint: "Use -b chrome/chromium/edge/brave/vivaldi/opera/santa/arc/yandex/browseros/custom/firefox or --browser=...; use custom only with browser.custom in config"
536
+ });
537
+ }
538
+ }
539
+ const defaultParser = new CliParser();
540
+ function parseCliArgs(argv) {
541
+ return defaultParser.parse(argv);
542
+ }
543
+ function getDistSizeSync(dirPath) {
544
+ if (!external_fs_existsSync(dirPath)) return -1;
545
+ const stat = statSync(dirPath);
546
+ if (!stat.isDirectory()) return stat.size;
547
+ let total = 0;
548
+ for (const name of readdirSync(dirPath)){
549
+ const s = statSync(external_path_resolve(dirPath, name));
550
+ total += s.isDirectory() ? getDistSizeSync(external_path_resolve(dirPath, name)) : s.size;
551
+ }
552
+ return total;
553
+ }
554
+ function formatBytes(bytes) {
555
+ if (bytes < 1024) return bytes + " B";
556
+ if (bytes < 1048576) return (bytes / 1024).toFixed(2) + " KB";
557
+ return (bytes / 1048576).toFixed(2) + " MB";
558
+ }
559
+ function isSourceMapEnabled(rsbuildConfig) {
560
+ const sm = rsbuildConfig?.output?.sourceMap;
561
+ if (true === sm) return true;
562
+ if (sm && "object" == typeof sm && "string" == typeof sm.js) return true;
563
+ return false;
564
+ }
565
+ function getBuildOutputSize(result) {
566
+ const stats = result?.stats;
567
+ if (!stats?.toJson) return null;
568
+ try {
569
+ const json = stats.toJson({
570
+ all: false,
571
+ assets: true
572
+ });
573
+ const assets = json?.assets;
574
+ if (!Array.isArray(assets)) return null;
575
+ let total = 0;
576
+ for (const a of assets)if (a && "number" == typeof a.size) total += a.size;
577
+ return total > 0 ? total : null;
578
+ } catch {
579
+ return null;
580
+ }
581
+ }
582
+ const ADDFOX_PREFIX = "\x1b[38;5;208m[Addfox]\x1b[0m ";
583
+ const RSBUILD_PREFIX = "\x1b[38;5;141m[Rsbuild]\x1b[0m ";
584
+ const WEBEXT_PREFIX = "\x1b[38;5;45m[Web-ext]\x1b[0m ";
585
+ let rawStdoutWrite = null;
586
+ let rawStderrWrite = null;
587
+ let outputPrefix = "addfox";
588
+ function setOutputPrefixRsbuild() {
589
+ outputPrefix = "rsbuild";
590
+ }
591
+ function setOutputPrefixAddfox() {
592
+ outputPrefix = "addfox";
593
+ }
594
+ function getRawWrites() {
595
+ return {
596
+ stdout: rawStdoutWrite ?? process.stdout.write.bind(process.stdout),
597
+ stderr: rawStderrWrite ?? process.stderr.write.bind(process.stderr)
598
+ };
599
+ }
600
+ function isEncoding(x) {
601
+ return "string" == typeof x;
602
+ }
603
+ function createPrefixedWrite(stream, getPrefix) {
604
+ const originalWrite = stream.write.bind(stream);
605
+ let buffer = "";
606
+ function flushIncomplete() {
607
+ if (buffer.length > 0) {
608
+ originalWrite(getPrefix(buffer) + buffer);
609
+ buffer = "";
610
+ }
611
+ }
612
+ const write = function(chunk, encodingOrCallback, callback) {
613
+ const encoding = isEncoding(encodingOrCallback) ? encodingOrCallback : void 0;
614
+ const cb = "function" == typeof encodingOrCallback ? encodingOrCallback : callback;
615
+ const str = "string" == typeof chunk ? chunk : chunk.toString(encoding ?? "utf8");
616
+ buffer += str;
617
+ const lines = buffer.split("\n");
618
+ buffer = lines.pop() ?? "";
619
+ for (const line of lines)originalWrite(getPrefix(line) + line + "\n", encoding);
620
+ if ("function" == typeof cb) cb();
621
+ return true;
622
+ };
623
+ write.flush = flushIncomplete;
624
+ return write;
625
+ }
626
+ function wrapAddfoxOutput() {
627
+ rawStdoutWrite = process.stdout.write.bind(process.stdout);
628
+ rawStderrWrite = process.stderr.write.bind(process.stderr);
629
+ const getPrefix = (_line)=>{
630
+ if ("rsbuild" === outputPrefix && getWebExtStdoutOriginDepth() > 0) return WEBEXT_PREFIX;
631
+ return "rsbuild" === outputPrefix ? RSBUILD_PREFIX : ADDFOX_PREFIX;
632
+ };
633
+ const stdoutWrite = createPrefixedWrite(process.stdout, getPrefix);
634
+ const stderrWrite = createPrefixedWrite(process.stderr, getPrefix);
635
+ process.stdout.write = stdoutWrite;
636
+ process.stderr.write = stderrWrite;
637
+ const flush = ()=>{
638
+ const fOut = stdoutWrite.flush;
639
+ const fErr = stderrWrite.flush;
640
+ if (fOut) fOut();
641
+ if (fErr) fErr();
642
+ };
643
+ process.once("exit", flush);
644
+ }
645
+ const ZIP_OUTPUT_CODE = "ADDFOX_ZIP_OUTPUT";
646
+ const ZIP_ARCHIVE_CODE = "ADDFOX_ZIP_ARCHIVE";
647
+ const ZIP_LEVEL = 9;
648
+ function zipDist(distPath, root, outDir, browser, deps) {
649
+ const createStream = deps?.createWriteStream ?? createWriteStream;
650
+ const archiverFn = deps?.archiver ?? archiver;
651
+ const mkdir = deps?.mkdirSync ?? mkdirSync;
652
+ const exists = deps?.existsSync ?? external_fs_existsSync;
653
+ const outputRoot = ADDFOX_OUTPUT_ROOT;
654
+ const zipDir = external_path_resolve(root, outputRoot, outDir);
655
+ if (!exists(zipDir)) mkdir(zipDir, {
656
+ recursive: true
657
+ });
658
+ const browserSuffix = browser ? `-${browser}` : "";
659
+ const zipPath = external_path_resolve(zipDir, `${outDir}${browserSuffix}.zip`);
660
+ const output = createStream(zipPath);
661
+ const archive = archiverFn("zip", {
662
+ zlib: {
663
+ level: ZIP_LEVEL
664
+ }
665
+ });
666
+ return new Promise((resolvePromise, reject)=>{
667
+ output.on("error", (err)=>reject(new AddfoxError({
668
+ message: "Zip output stream failed",
669
+ code: ZIP_OUTPUT_CODE,
670
+ details: err instanceof Error ? err.message : String(err),
671
+ cause: err
672
+ })));
673
+ output.on("close", ()=>resolvePromise(zipPath));
674
+ archive.on("error", (err)=>reject(new AddfoxError({
675
+ message: "Zip archive failed",
676
+ code: ZIP_ARCHIVE_CODE,
677
+ details: err instanceof Error ? err.message : String(err),
678
+ cause: err
679
+ })));
680
+ archive.pipe(output);
681
+ archive.directory(distPath, false);
682
+ archive.finalize();
683
+ });
684
+ }
685
+ const version_require = createRequire(import.meta.url);
686
+ function readPackageVersion(pkgPath) {
687
+ try {
688
+ const pkg = JSON.parse(external_fs_readFileSync(pkgPath, "utf-8"));
689
+ return pkg.version ?? "?";
690
+ } catch {
691
+ return "?";
692
+ }
693
+ }
694
+ function getVersion() {
695
+ try {
696
+ const pkgPath = external_path_resolve(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
697
+ return readPackageVersion(pkgPath) || "0.0.0";
698
+ } catch {
699
+ return "0.0.0";
700
+ }
701
+ }
702
+ function getRsbuildVersion(projectRoot) {
703
+ const cliDir = dirname(fileURLToPath(import.meta.url));
704
+ const candidates = [
705
+ ()=>{
706
+ const p = external_path_resolve(projectRoot, "node_modules", "@rsbuild", "core", "package.json");
707
+ return external_fs_existsSync(p) ? p : null;
708
+ },
709
+ ()=>{
710
+ try {
711
+ return version_require.resolve("@rsbuild/core/package.json", {
712
+ paths: [
713
+ projectRoot
714
+ ]
715
+ });
716
+ } catch {
717
+ return null;
718
+ }
719
+ },
720
+ ()=>{
721
+ try {
722
+ return version_require.resolve("@rsbuild/core/package.json", {
723
+ paths: [
724
+ external_path_resolve(cliDir, ".."),
725
+ external_path_resolve(cliDir, "..", "..")
726
+ ]
727
+ });
728
+ } catch {
729
+ return null;
730
+ }
731
+ }
732
+ ];
733
+ for (const getPath of candidates){
734
+ const pkgPath = getPath();
735
+ if (pkgPath) return readPackageVersion(pkgPath);
736
+ }
737
+ return "?";
738
+ }
739
+ const test_require = createRequire(import.meta.url);
740
+ function hasRstestConfigFile(projectRoot) {
741
+ return null !== getResolvedRstestConfigFilePath(projectRoot);
742
+ }
743
+ function isRstestBrowserEnabled(projectRoot) {
744
+ const configPath = getResolvedRstestConfigFilePath(projectRoot);
745
+ if (!configPath) return false;
746
+ try {
747
+ const content = external_fs_readFileSync(configPath, "utf-8");
748
+ return /browser\s*:\s*\{[^}]*enabled\s*:\s*true/.test(content) || /browser\.enabled\s*===?\s*true/.test(content);
749
+ } catch {
750
+ return false;
751
+ }
752
+ }
753
+ function getRequiredTestPackages(projectRoot) {
754
+ const required = [
755
+ "@rstest/core"
756
+ ];
757
+ if (isRstestBrowserEnabled(projectRoot)) required.push("@rstest/browser", "playwright");
758
+ return required;
759
+ }
760
+ function getRstestBinPath(projectRoot) {
761
+ const binDir = external_path_resolve(projectRoot, "node_modules", ".bin");
762
+ const name = "win32" === process.platform ? "rstest.cmd" : "rstest";
763
+ const p = external_path_resolve(binDir, name);
764
+ if (external_fs_existsSync(p)) return p;
765
+ const fallback = external_path_resolve(binDir, "rstest");
766
+ if (external_fs_existsSync(fallback)) return fallback;
767
+ try {
768
+ const corePkgPath = test_require.resolve("@rstest/core/package.json", {
769
+ paths: [
770
+ projectRoot
771
+ ]
772
+ });
773
+ const coreDir = dirname(corePkgPath);
774
+ const binInCore = external_path_resolve(coreDir, "bin", "rstest.js");
775
+ if (external_fs_existsSync(binInCore)) return binInCore;
776
+ } catch {}
777
+ exitWithError(new Error("rstest binary not found in node_modules/.bin after installing @rstest/core"));
778
+ }
779
+ function runRstest(projectRoot, restArgs) {
780
+ const rstestBin = getRstestBinPath(projectRoot);
781
+ const result = external_child_process_spawnSync(rstestBin, restArgs, {
782
+ cwd: projectRoot,
783
+ stdio: "inherit",
784
+ shell: "win32" === process.platform
785
+ });
786
+ return result.status ?? 0;
787
+ }
788
+ async function runTest(projectRoot, argv) {
789
+ if (!hasRstestConfigFile(projectRoot)) {
790
+ const files = "rstest.config.cts, rstest.config.mts, rstest.config.cjs, rstest.config.js, rstest.config.ts, rstest.config.mjs";
791
+ throw new AddfoxError({
792
+ code: ADDFOX_ERROR_CODES.RSTEST_CONFIG_NOT_FOUND,
793
+ message: "Rstest config file not found",
794
+ details: `No rstest.config.* found under ${projectRoot}`,
795
+ hint: `Create one of: ${files}`
796
+ });
797
+ }
798
+ const required = getRequiredTestPackages(projectRoot);
799
+ const missing = getMissingPackages(projectRoot, required);
800
+ if (missing.length > 0) {
801
+ const pm = pkg_manager_detectFromLockfile(projectRoot);
802
+ const installCmd = getAddCommand(pm, missing.join(" "), true);
803
+ exitWithError(new Error(`Missing test dependencies: ${missing.join(", ")}. Please run:\n ${installCmd}`));
804
+ }
805
+ const restArgs = argv.slice(1);
806
+ const code = runRstest(projectRoot, restArgs);
807
+ process.exit(code);
808
+ }
809
+ const cli_root = process.cwd();
810
+ const PURPLE = "\x1b[38;5;141m";
811
+ const RESET = "\x1b[0m";
812
+ function hasConfigFile() {
813
+ return CONFIG_FILES.some((file)=>external_fs_existsSync(external_path_resolve(cli_root, file)));
814
+ }
815
+ function printHelp() {
816
+ const version = getVersion();
817
+ console.log(`
818
+ addfox v${version}
819
+
820
+ Build tool for browser extensions
821
+
822
+ Usage:
823
+ addfox <command> [options]
824
+
825
+ Commands:
826
+ dev Start development server with HMR
827
+ build Build for production
828
+ test Run tests with rstest (unit + optional E2E); forwards args to rstest
829
+
830
+ Options:
831
+ -b, --browser <browser> Target/launch browser (chromium | firefox | chrome | edge | brave | ...)
832
+ -c, --cache Cache browser profile between launches
833
+ --no-cache Disable browser profile cache for current run
834
+ -r, --report Enable Rsdoctor build report (opens analysis after build)
835
+ --no-open Do not auto-open browser (dev/build)
836
+ --debug Enable debug mode
837
+ --help Show this help message
838
+ --version Show version number
839
+ `);
840
+ }
841
+ function resolveOptions(argv, config) {
842
+ const parsed = parseCliArgs(argv);
843
+ const browser = parsed.browser ?? "chromium";
844
+ const launch = parsed.launch ?? ("firefox" === browser ? "firefox" : "chrome");
845
+ const debug = parsed.debug ?? config.debug ?? false;
846
+ return {
847
+ browser,
848
+ launch,
849
+ cache: parsed.cache ?? config.cache ?? true,
850
+ report: parsed.report ?? false,
851
+ debug,
852
+ open: parsed.open ?? true
853
+ };
854
+ }
855
+ async function createRsbuildInstance(ctx) {
856
+ setOutputPrefixRsbuild();
857
+ const { createRsbuild } = await import("@rsbuild/core");
858
+ const rsbuild = await createRsbuild({
859
+ rsbuildConfig: ctx.rsbuild,
860
+ cwd: ctx.root,
861
+ loadEnv: {
862
+ cwd: ctx.root,
863
+ prefixes: getLoadEnvPrefixes(ctx.config)
864
+ }
865
+ });
866
+ return rsbuild;
867
+ }
868
+ function logExtensionSize(distDir, rsbuildConfig) {
869
+ const size = getDistSizeSync(distDir);
870
+ if (size < 0) return;
871
+ const sizeStr = formatBytes(size);
872
+ const suffix = isSourceMapEnabled(rsbuildConfig) ? " (with inline-source-map)" : "";
873
+ logDoneWithValue("Extension size:", sizeStr + suffix);
874
+ }
875
+ const ADDFOX_CONFIG_DEBOUNCE_MS = 300;
876
+ function watchAddfoxConfig(configPath, onRestart) {
877
+ let timer = null;
878
+ const run = ()=>{
879
+ if (timer) clearTimeout(timer);
880
+ timer = setTimeout(()=>{
881
+ timer = null;
882
+ Promise.resolve(onRestart()).catch((e)=>{
883
+ error(formatError(e));
884
+ exitWithError(e);
885
+ });
886
+ }, ADDFOX_CONFIG_DEBOUNCE_MS);
887
+ };
888
+ try {
889
+ const watcher = watch(configPath, {
890
+ persistent: true
891
+ }, (eventType, filename)=>{
892
+ if (filename && ("change" === eventType || "rename" === eventType)) run();
893
+ });
894
+ return {
895
+ close () {
896
+ try {
897
+ watcher.close();
898
+ } catch {}
899
+ }
900
+ };
901
+ } catch {
902
+ return {
903
+ close () {}
904
+ };
905
+ }
906
+ }
907
+ async function runDev(root, argv) {
908
+ const { config, baseEntries, entries } = resolveAddfoxConfig(root);
909
+ const resolved = resolveOptions(argv, config);
910
+ const options = {
911
+ root,
912
+ command: 'dev',
913
+ ...resolved,
914
+ config,
915
+ baseEntries,
916
+ entries
917
+ };
918
+ const rsbuildReadyStart = performance.now();
919
+ const ctx = await runPipeline(options);
920
+ process.env.NODE_ENV = ctx.isDev ? "development" : "production";
921
+ const rsbuild = await createRsbuildInstance(ctx);
922
+ logDoneTimed("Rsbuild ready", Math.round(performance.now() - rsbuildReadyStart));
923
+ const configPath = getResolvedConfigFilePath(ctx.root);
924
+ let devServerRef = null;
925
+ let watcherRef = null;
926
+ const onAddfoxConfigChange = async ()=>{
927
+ if (watcherRef) {
928
+ watcherRef.close();
929
+ watcherRef = null;
930
+ }
931
+ if (devServerRef?.server?.close) await devServerRef.server.close();
932
+ if (configPath) clearConfigCache(configPath);
933
+ process.env.ADDFOX_CONFIG_RESTART = "1";
934
+ try {
935
+ await runDev(root, argv);
936
+ } finally{
937
+ delete process.env.ADDFOX_CONFIG_RESTART;
938
+ }
939
+ };
940
+ if (configPath) watcherRef = watchAddfoxConfig(configPath, onAddfoxConfigChange);
941
+ const devServerStart = performance.now();
942
+ devServerRef = await rsbuild.startDevServer({
943
+ getPortSilently: true
944
+ });
945
+ const urls = devServerRef?.urls ?? [];
946
+ const mainUrl = urls[0] ?? `http://localhost:${devServerRef?.port ?? "?"}`;
947
+ logDoneTimed("Dev server " + mainUrl, Math.round(performance.now() - devServerStart));
948
+ const devDistDir = rsbuild.context?.distPath ?? ctx.distPath;
949
+ setTimeout(()=>logExtensionSize(devDistDir, ctx.rsbuild), 2500);
950
+ }
951
+ async function runBuild(root, argv) {
952
+ const { config, baseEntries, entries } = resolveAddfoxConfig(root);
953
+ const resolved = resolveOptions(argv, config);
954
+ const options = {
955
+ root,
956
+ command: 'build',
957
+ ...resolved,
958
+ config,
959
+ baseEntries,
960
+ entries
961
+ };
962
+ const rsbuildReadyStart = performance.now();
963
+ const ctx = await runPipeline(options);
964
+ const rsbuild = await createRsbuildInstance(ctx);
965
+ logDoneTimed("Rsbuild ready", Math.round(performance.now() - rsbuildReadyStart));
966
+ const buildStart = performance.now();
967
+ const buildResult = await rsbuild.build();
968
+ logDoneTimed("Rsbuild build", Math.round(performance.now() - buildStart));
969
+ setOutputPrefixAddfox();
970
+ if (false !== ctx.config.zip) {
971
+ const zipPath = await zipDist(ctx.distPath, ctx.root, ctx.config.outDir, ctx.browser);
972
+ logDone("Zipped output to", zipPath);
973
+ }
974
+ const distDir = rsbuild.context?.distPath || ctx.distPath;
975
+ const distSize = getBuildOutputSize(buildResult) ?? getDistSizeSync(distDir);
976
+ if (distSize >= 0) logDoneWithValue("Extension size:", formatBytes(distSize));
977
+ if (resolved.open) {
978
+ const browserPathConfig = ctx.config.browserPath ?? {};
979
+ await launchBrowserOnly({
980
+ distPath: distDir,
981
+ browser: resolved.launch,
982
+ cache: resolved.cache,
983
+ chromePath: browserPathConfig.chrome,
984
+ chromiumPath: browserPathConfig.chromium,
985
+ edgePath: browserPathConfig.edge,
986
+ bravePath: browserPathConfig.brave,
987
+ vivaldiPath: browserPathConfig.vivaldi,
988
+ operaPath: browserPathConfig.opera,
989
+ santaPath: browserPathConfig.santa,
990
+ arcPath: browserPathConfig.arc,
991
+ yandexPath: browserPathConfig.yandex,
992
+ browserosPath: browserPathConfig.browseros,
993
+ customPath: browserPathConfig.custom,
994
+ firefoxPath: browserPathConfig.firefox
995
+ });
996
+ }
997
+ }
998
+ async function main() {
999
+ const argv = process.argv.slice(2);
1000
+ if (argv.includes("--help") || argv.includes("-h")) return void printHelp();
1001
+ if (argv.includes("--version") || argv.includes("-v")) return void console.log(getVersion());
1002
+ wrapAddfoxOutput();
1003
+ setAddfoxLoggerRawWrites(getRawWrites());
1004
+ common_log("Addfox " + getVersion() + " with " + PURPLE + "Rsbuild " + getRsbuildVersion(cli_root) + RESET);
1005
+ const command = argv[0];
1006
+ if ("test" === command) return void await runTest(cli_root, argv);
1007
+ if (!hasConfigFile()) throw new AddfoxError({
1008
+ code: ADDFOX_ERROR_CODES.CONFIG_NOT_FOUND,
1009
+ message: "Addfox config file not found",
1010
+ details: `No addfox.config.ts, addfox.config.js or addfox.config.mjs found under ${cli_root}`,
1011
+ hint: "Run the command from project root or create addfox.config.ts / addfox.config.js"
1012
+ });
1013
+ process.env.NODE_ENV = "dev" === command ? "development" : "production";
1014
+ if ("dev" === command) await runDev(cli_root, argv);
1015
+ else await runBuild(cli_root, argv);
1016
+ }
1017
+ main().catch((e)=>{
1018
+ error(formatError(e));
1019
+ exitWithError(e);
1020
+ });
1021
+
1022
+ //# sourceMappingURL=cli.js.map