create-addfox-app 0.1.1-beta.7 → 0.1.1

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.
Files changed (160) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +184 -99
  3. package/package.json +9 -5
  4. package/templates/README.md +3 -0
  5. package/templates/template-preact-js/AGENTS.md +54 -0
  6. package/templates/template-preact-js/addfox.config.js +27 -0
  7. package/templates/template-preact-js/app/background/index.js +8 -0
  8. package/templates/template-preact-js/app/content/index.js +1 -0
  9. package/templates/template-preact-js/app/options/App.jsx +5 -0
  10. package/templates/template-preact-js/app/options/index.jsx +5 -0
  11. package/templates/template-preact-js/app/popup/App.jsx +31 -0
  12. package/templates/template-preact-js/app/popup/index.jsx +5 -0
  13. package/templates/template-preact-js/app/sidepanel/App.jsx +5 -0
  14. package/templates/template-preact-js/app/sidepanel/index.jsx +5 -0
  15. package/templates/template-preact-js/package.json +18 -0
  16. package/templates/template-preact-js/public/icons/icon_128.png +0 -0
  17. package/templates/template-preact-js/public/welcome.html +20 -0
  18. package/templates/template-preact-ts/AGENTS.md +61 -0
  19. package/templates/template-preact-ts/addfox.config.ts +27 -0
  20. package/templates/template-preact-ts/app/background/index.ts +8 -0
  21. package/templates/template-preact-ts/app/content/index.ts +1 -0
  22. package/templates/template-preact-ts/app/options/App.tsx +5 -0
  23. package/templates/template-preact-ts/app/options/index.tsx +5 -0
  24. package/templates/template-preact-ts/app/popup/App.tsx +31 -0
  25. package/templates/template-preact-ts/app/popup/index.tsx +5 -0
  26. package/templates/template-preact-ts/app/sidepanel/App.tsx +5 -0
  27. package/templates/template-preact-ts/app/sidepanel/index.tsx +5 -0
  28. package/templates/template-preact-ts/package.json +20 -0
  29. package/templates/template-preact-ts/public/icons/icon_128.png +0 -0
  30. package/templates/template-preact-ts/public/welcome.html +20 -0
  31. package/templates/template-preact-ts/tsconfig.json +19 -0
  32. package/templates/template-react-js/AGENTS.md +66 -0
  33. package/templates/template-react-js/addfox.config.js +27 -0
  34. package/templates/template-react-js/app/background/index.js +8 -0
  35. package/templates/template-react-js/app/content/index.js +1 -0
  36. package/templates/template-react-js/app/options/App.jsx +3 -0
  37. package/templates/template-react-js/app/options/index.jsx +5 -0
  38. package/templates/template-react-js/app/popup/App.jsx +29 -0
  39. package/templates/template-react-js/app/popup/index.jsx +5 -0
  40. package/templates/template-react-js/app/sidepanel/App.jsx +3 -0
  41. package/templates/template-react-js/app/sidepanel/index.jsx +5 -0
  42. package/templates/template-react-js/package.json +19 -0
  43. package/templates/template-react-js/public/icons/icon_128.png +0 -0
  44. package/templates/template-react-js/public/welcome.html +20 -0
  45. package/templates/template-react-ts/AGENTS.md +93 -0
  46. package/templates/template-react-ts/addfox.config.ts +27 -0
  47. package/templates/template-react-ts/app/background/index.ts +8 -0
  48. package/templates/template-react-ts/app/content/index.ts +1 -0
  49. package/templates/template-react-ts/app/options/App.tsx +3 -0
  50. package/templates/template-react-ts/app/options/index.tsx +5 -0
  51. package/templates/template-react-ts/app/popup/App.tsx +29 -0
  52. package/templates/template-react-ts/app/popup/index.tsx +5 -0
  53. package/templates/template-react-ts/app/sidepanel/App.tsx +3 -0
  54. package/templates/template-react-ts/app/sidepanel/index.tsx +5 -0
  55. package/templates/template-react-ts/package.json +23 -0
  56. package/templates/template-react-ts/public/icons/icon_128.png +0 -0
  57. package/templates/template-react-ts/public/welcome.html +20 -0
  58. package/templates/template-react-ts/tsconfig.json +19 -0
  59. package/templates/template-solid-js/AGENTS.md +54 -0
  60. package/templates/template-solid-js/addfox.config.js +28 -0
  61. package/templates/template-solid-js/app/background/index.js +8 -0
  62. package/templates/template-solid-js/app/content/index.js +1 -0
  63. package/templates/template-solid-js/app/options/App.jsx +3 -0
  64. package/templates/template-solid-js/app/options/index.jsx +5 -0
  65. package/templates/template-solid-js/app/popup/App.jsx +34 -0
  66. package/templates/template-solid-js/app/popup/index.jsx +5 -0
  67. package/templates/template-solid-js/app/sidepanel/App.jsx +3 -0
  68. package/templates/template-solid-js/app/sidepanel/index.jsx +5 -0
  69. package/templates/template-solid-js/package.json +19 -0
  70. package/templates/template-solid-js/public/icons/icon_128.png +0 -0
  71. package/templates/template-solid-js/public/welcome.html +20 -0
  72. package/templates/template-solid-ts/AGENTS.md +61 -0
  73. package/templates/template-solid-ts/addfox.config.ts +28 -0
  74. package/templates/template-solid-ts/app/background/index.ts +8 -0
  75. package/templates/template-solid-ts/app/content/index.ts +1 -0
  76. package/templates/template-solid-ts/app/options/App.tsx +3 -0
  77. package/templates/template-solid-ts/app/options/index.tsx +5 -0
  78. package/templates/template-solid-ts/app/popup/App.tsx +34 -0
  79. package/templates/template-solid-ts/app/popup/index.tsx +5 -0
  80. package/templates/template-solid-ts/app/sidepanel/App.tsx +3 -0
  81. package/templates/template-solid-ts/app/sidepanel/index.tsx +5 -0
  82. package/templates/template-solid-ts/package.json +21 -0
  83. package/templates/template-solid-ts/public/icons/icon_128.png +0 -0
  84. package/templates/template-solid-ts/public/welcome.html +20 -0
  85. package/templates/template-solid-ts/tsconfig.json +19 -0
  86. package/templates/template-svelte-js/AGENTS.md +54 -0
  87. package/templates/template-svelte-js/addfox.config.js +27 -0
  88. package/templates/template-svelte-js/app/background/index.js +8 -0
  89. package/templates/template-svelte-js/app/content/index.js +1 -0
  90. package/templates/template-svelte-js/app/options/App.svelte +4 -0
  91. package/templates/template-svelte-js/app/options/index.js +4 -0
  92. package/templates/template-svelte-js/app/popup/App.svelte +30 -0
  93. package/templates/template-svelte-js/app/popup/index.js +4 -0
  94. package/templates/template-svelte-js/app/sidepanel/App.svelte +4 -0
  95. package/templates/template-svelte-js/app/sidepanel/index.js +4 -0
  96. package/templates/template-svelte-js/package.json +18 -0
  97. package/templates/template-svelte-js/public/icons/icon_128.png +0 -0
  98. package/templates/template-svelte-js/public/welcome.html +20 -0
  99. package/templates/template-svelte-ts/AGENTS.md +61 -0
  100. package/templates/template-svelte-ts/addfox.config.ts +27 -0
  101. package/templates/template-svelte-ts/app/background/index.ts +8 -0
  102. package/templates/template-svelte-ts/app/content/index.ts +1 -0
  103. package/templates/template-svelte-ts/app/options/App.svelte +4 -0
  104. package/templates/template-svelte-ts/app/options/index.ts +4 -0
  105. package/templates/template-svelte-ts/app/popup/App.svelte +30 -0
  106. package/templates/template-svelte-ts/app/popup/index.ts +4 -0
  107. package/templates/template-svelte-ts/app/sidepanel/App.svelte +4 -0
  108. package/templates/template-svelte-ts/app/sidepanel/index.ts +4 -0
  109. package/templates/template-svelte-ts/package.json +20 -0
  110. package/templates/template-svelte-ts/public/icons/icon_128.png +0 -0
  111. package/templates/template-svelte-ts/public/welcome.html +20 -0
  112. package/templates/template-svelte-ts/tsconfig.json +19 -0
  113. package/templates/template-vanilla-js/AGENTS.md +52 -0
  114. package/templates/template-vanilla-js/addfox.config.js +25 -0
  115. package/templates/template-vanilla-js/app/background/index.js +8 -0
  116. package/templates/template-vanilla-js/app/content/index.js +1 -0
  117. package/templates/template-vanilla-js/app/options/index.js +2 -0
  118. package/templates/template-vanilla-js/app/popup/index.js +48 -0
  119. package/templates/template-vanilla-js/app/sidepanel/index.js +2 -0
  120. package/templates/template-vanilla-js/package.json +13 -0
  121. package/templates/template-vanilla-js/public/icons/icon_128.png +0 -0
  122. package/templates/template-vanilla-js/public/welcome.html +20 -0
  123. package/templates/template-vanilla-ts/AGENTS.md +59 -0
  124. package/templates/template-vanilla-ts/addfox.config.ts +25 -0
  125. package/templates/template-vanilla-ts/app/background/index.ts +8 -0
  126. package/templates/template-vanilla-ts/app/content/index.ts +1 -0
  127. package/templates/template-vanilla-ts/app/options/index.ts +2 -0
  128. package/templates/template-vanilla-ts/app/popup/index.ts +48 -0
  129. package/templates/template-vanilla-ts/app/sidepanel/index.ts +2 -0
  130. package/templates/template-vanilla-ts/package.json +14 -0
  131. package/templates/template-vanilla-ts/public/icons/icon_128.png +0 -0
  132. package/templates/template-vanilla-ts/public/welcome.html +20 -0
  133. package/templates/template-vanilla-ts/tsconfig.json +17 -0
  134. package/templates/template-vue-js/AGENTS.md +60 -0
  135. package/templates/template-vue-js/addfox.config.js +27 -0
  136. package/templates/template-vue-js/app/background/index.js +8 -0
  137. package/templates/template-vue-js/app/content/index.js +1 -0
  138. package/templates/template-vue-js/app/options/App.vue +6 -0
  139. package/templates/template-vue-js/app/options/index.js +5 -0
  140. package/templates/template-vue-js/app/popup/App.vue +34 -0
  141. package/templates/template-vue-js/app/popup/index.js +5 -0
  142. package/templates/template-vue-js/app/sidepanel/App.vue +6 -0
  143. package/templates/template-vue-js/app/sidepanel/index.js +5 -0
  144. package/templates/template-vue-js/package.json +18 -0
  145. package/templates/template-vue-js/public/icons/icon_128.png +0 -0
  146. package/templates/template-vue-js/public/welcome.html +20 -0
  147. package/templates/template-vue-ts/AGENTS.md +65 -0
  148. package/templates/template-vue-ts/addfox.config.ts +27 -0
  149. package/templates/template-vue-ts/app/background/index.ts +8 -0
  150. package/templates/template-vue-ts/app/content/index.ts +1 -0
  151. package/templates/template-vue-ts/app/options/App.vue +6 -0
  152. package/templates/template-vue-ts/app/options/index.ts +5 -0
  153. package/templates/template-vue-ts/app/popup/App.vue +32 -0
  154. package/templates/template-vue-ts/app/popup/index.ts +5 -0
  155. package/templates/template-vue-ts/app/sidepanel/App.vue +6 -0
  156. package/templates/template-vue-ts/app/sidepanel/index.ts +5 -0
  157. package/templates/template-vue-ts/package.json +20 -0
  158. package/templates/template-vue-ts/public/icons/icon_128.png +0 -0
  159. package/templates/template-vue-ts/public/welcome.html +20 -0
  160. package/templates/template-vue-ts/tsconfig.json +19 -0
package/README.md CHANGED
@@ -15,4 +15,4 @@ Interactive scaffolder: generates a addfox-based extension project from options
15
15
 
16
16
  ## Templates
17
17
 
18
- Scaffold templates live at **repo root** in **`templates/`** (e.g. `template-vanilla-ts`, `template-react-ts`). The CLI downloads the chosen template from the GitHub repo tarball, so the repo must have `templates/` at root and committed. The npm package does not bundle templates; it fetches from GitHub at scaffold time.
18
+ Scaffold templates ship inside this package under **`templates/`** (e.g. `template-vanilla-ts`, `template-react-ts`). They are included in the published npm tarball; the CLI copies the chosen folder into your new project (no remote download). The template’s **`addfox.config`** (including `manifest`) is **preserved**; the CLI only **merges** Rsbuild Less/Sass plugins when those style engines are selected. If no config file exists, a full fallback config is generated.
package/dist/cli.js CHANGED
@@ -1,82 +1,93 @@
1
1
  #!/usr/bin/env node
2
- import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
3
- import { dirname, join, relative, resolve } from "node:path";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { dirname, join, relative, resolve as external_node_path_resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { createRequire } from "node:module";
6
6
  import { execSync } from "node:child_process";
7
7
  import prompts from "prompts";
8
8
  import { blue, cyan, dim, gray, green, lightBlue, magenta, red, trueColor, yellow } from "kolorist";
9
9
  import minimist from "minimist";
10
- import { gunzipSync } from "node:zlib";
10
+ import { cp, mkdir } from "node:fs/promises";
11
11
  import { detectPackageManager, getExecCommand, getInstallCommand, getRunCommand } from "@addfox/pkg-manager";
12
- const GITHUB_REPO = "addfox/addfox";
13
12
  const TEMPLATE_BASE = "templates";
14
- function getRepoRoot() {
15
- const fromDist = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
16
- return fromDist;
17
- }
18
- function tryLocalTemplates(templateName, destDir) {
19
- const repoRoot = getRepoRoot();
20
- const templatePath = join(repoRoot, TEMPLATE_BASE, templateName);
21
- if (!existsSync(templatePath)) return false;
22
- mkdirSync(destDir, {
13
+ function getPackageRoot() {
14
+ return external_node_path_resolve(dirname(fileURLToPath(import.meta.url)), "..");
15
+ }
16
+ function getBundledTemplatesDir() {
17
+ return join(getPackageRoot(), TEMPLATE_BASE);
18
+ }
19
+ function hasLocalTemplate(templateName) {
20
+ return existsSync(join(getBundledTemplatesDir(), templateName));
21
+ }
22
+ const SKIP_RELATIVE_SEGMENTS = new Set([
23
+ "node_modules",
24
+ ".git",
25
+ ".pnpm"
26
+ ]);
27
+ function shouldCopyLocalTemplatePath(src, templateRoot) {
28
+ const root = external_node_path_resolve(templateRoot);
29
+ const rel = relative(root, external_node_path_resolve(src));
30
+ if (rel.startsWith("..")) return true;
31
+ for (const part of rel.split(/[/\\]/))if (SKIP_RELATIVE_SEGMENTS.has(part)) return false;
32
+ return true;
33
+ }
34
+ async function copyBundledTemplate(templateName, destDir) {
35
+ const templatePath = join(getBundledTemplatesDir(), templateName);
36
+ if (!existsSync(templatePath)) throw new Error(`Bundled template "${templateName}" not found under create-addfox-app. Reinstall the package.`);
37
+ await mkdir(destDir, {
23
38
  recursive: true
24
39
  });
25
- cpSync(templatePath, destDir, {
26
- recursive: true
40
+ const resolvedTemplate = external_node_path_resolve(templatePath);
41
+ await cp(resolvedTemplate, destDir, {
42
+ recursive: true,
43
+ filter: (src)=>shouldCopyLocalTemplatePath(src, resolvedTemplate)
27
44
  });
28
- return true;
29
- }
30
- function parseTarHeader(buf) {
31
- if (buf.every((b)=>0 === b)) return null;
32
- const rawName = buf.subarray(0, 100).toString("utf8").replace(/\0+$/, "");
33
- const sizeStr = buf.subarray(124, 136).toString("utf8").replace(/\0+$/, "").trim();
34
- const type = String.fromCharCode(buf[156]);
35
- const prefix = buf.subarray(345, 500).toString("utf8").replace(/\0+$/, "");
36
- return {
37
- name: prefix ? `${prefix}/${rawName}` : rawName,
38
- size: parseInt(sizeStr, 8) || 0,
39
- type
40
- };
41
45
  }
42
- function extractMatchingFiles(tarBuffer, templatePrefix, destDir) {
43
- let offset = 0;
44
- while(offset + 512 <= tarBuffer.length){
45
- const header = parseTarHeader(tarBuffer.subarray(offset, offset + 512));
46
- offset += 512;
47
- if (!header) break;
48
- const dataBlocks = 512 * Math.ceil(header.size / 512);
49
- const idx = header.name.indexOf(templatePrefix);
50
- if (-1 !== idx) {
51
- const relativePath = header.name.substring(idx + templatePrefix.length).replace(/^\//, "");
52
- if (relativePath) {
53
- const destPath = join(destDir, relativePath);
54
- const isDir = "5" === header.type || relativePath.endsWith("/");
55
- if (isDir) mkdirSync(destPath, {
56
- recursive: true
57
- });
58
- else if ("0" === header.type || "\0" === header.type) {
59
- mkdirSync(dirname(destPath), {
60
- recursive: true
61
- });
62
- writeFileSync(destPath, tarBuffer.subarray(offset, offset + header.size));
63
- }
64
- }
65
- }
66
- offset += dataBlocks;
67
- }
46
+ const FRAMES = [
47
+ "⠋",
48
+ "⠙",
49
+ "⠹",
50
+ "⠸",
51
+ "⠼",
52
+ "⠴",
53
+ "⠦",
54
+ "⠧",
55
+ "",
56
+ "⠏"
57
+ ];
58
+ const INTERVAL_MS = 80;
59
+ function clearSpinnerLine() {
60
+ process.stdout.write("\r\x1b[K");
68
61
  }
69
- async function downloadTemplate(templateName, destDir, branch = "main") {
70
- const url = `https://codeload.github.com/${GITHUB_REPO}/tar.gz/${branch}`;
71
- const templatePrefix = `${TEMPLATE_BASE}/${templateName}/`;
72
- const response = await fetch(url);
73
- if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
74
- const compressed = Buffer.from(await response.arrayBuffer());
75
- const tarBuffer = gunzipSync(compressed);
76
- mkdirSync(destDir, {
77
- recursive: true
62
+ function sleep(ms) {
63
+ return new Promise((resolve)=>{
64
+ setTimeout(resolve, ms);
78
65
  });
79
- extractMatchingFiles(tarBuffer, templatePrefix, destDir);
66
+ }
67
+ async function runWithTemplateSpinner(label, fn, options) {
68
+ const minVisibleMs = options?.minVisibleMs ?? 0;
69
+ if (!process.stdout.isTTY) {
70
+ console.log(yellow(`\n ${label}\n`));
71
+ return fn();
72
+ }
73
+ let frameIndex = 0;
74
+ process.stdout.write("\n");
75
+ const timer = setInterval(()=>{
76
+ const frame = FRAMES[frameIndex % FRAMES.length];
77
+ frameIndex += 1;
78
+ process.stdout.write(`\r ${yellow(frame)} ${label}`);
79
+ }, INTERVAL_MS);
80
+ const startedAt = Date.now();
81
+ try {
82
+ const result = await fn();
83
+ const elapsed = Date.now() - startedAt;
84
+ if (elapsed < minVisibleMs) await sleep(minVisibleMs - elapsed);
85
+ return result;
86
+ } finally{
87
+ clearInterval(timer);
88
+ clearSpinnerLine();
89
+ process.stdout.write("\n");
90
+ }
80
91
  }
81
92
  const STYLE_ENGINES = [
82
93
  {
@@ -233,7 +244,7 @@ function getStylePlugin(engine) {
233
244
  };
234
245
  return null;
235
246
  }
236
- const MINIMAL_MANIFEST = ' name: "My Extension",\n version: "1.0.0",\n manifest_version: 3,\n description: "Browser extension built with addfox",\n permissions: ["storage", "activeTab"],';
247
+ const MINIMAL_MANIFEST = ' name: "My Extension",\n version: "1.0.0",\n manifest_version: 3,\n description: "Browser extension built with addfox",\n permissions: ["activeTab"],\n icons: {\n "16": "icons/icon_128.png",\n "48": "icons/icon_128.png",\n "128": "icons/icon_128.png",\n },\n action: {\n default_icon: {\n "16": "icons/icon_128.png",\n "48": "icons/icon_128.png",\n "128": "icons/icon_128.png",\n },\n },';
237
248
  function generateAddfoxConfig(framework, _language, styleEngine) {
238
249
  const importLines = [];
239
250
  const fwImport = getFrameworkPluginImport(framework);
@@ -261,6 +272,67 @@ function generateAddfoxConfig(framework, _language, styleEngine) {
261
272
  ];
262
273
  return parts.filter(Boolean).join("\n");
263
274
  }
275
+ const MANIFEST_LINE = " manifest: { chromium: manifest, firefox: { ...manifest } },";
276
+ function findMatchingArrayClose(source, openBracketIdx) {
277
+ if ("[" !== source[openBracketIdx]) return -1;
278
+ let depth = 0;
279
+ for(let i = openBracketIdx; i < source.length; i++){
280
+ const c = source[i];
281
+ if ("[" === c) depth++;
282
+ else if ("]" === c) {
283
+ depth--;
284
+ if (0 === depth) return i;
285
+ }
286
+ }
287
+ return -1;
288
+ }
289
+ function hasImportFromModule(source, modulePath) {
290
+ return source.includes(`from "${modulePath}"`) || source.includes(`from '${modulePath}'`) || source.includes(`from \`${modulePath}\``);
291
+ }
292
+ function insertImportAfterLastImport(source, importLine) {
293
+ const trimmed = importLine.trim();
294
+ const moduleMatch = /from\s+["']([^"']+)["']/.exec(trimmed);
295
+ if (moduleMatch && hasImportFromModule(source, moduleMatch[1])) return source;
296
+ const lines = source.split("\n");
297
+ let lastImportIdx = -1;
298
+ for(let i = 0; i < lines.length; i++)if (lines[i].trim().startsWith("import ")) lastImportIdx = i;
299
+ if (-1 === lastImportIdx) return `${importLine}\n${source}`;
300
+ const next = [
301
+ ...lines
302
+ ];
303
+ next.splice(lastImportIdx + 1, 0, importLine);
304
+ return next.join("\n");
305
+ }
306
+ function pluginCallPresent(inner, call) {
307
+ const fn = call.replace(/\(\)$/, "");
308
+ return inner.includes(`${fn}(`);
309
+ }
310
+ function mergeCallsIntoPluginsArray(source, calls) {
311
+ const pluginsIdx = source.indexOf("plugins:");
312
+ if (-1 === pluginsIdx) return addPluginsBlockAfterManifest(source, calls);
313
+ const bracketStart = source.indexOf("[", pluginsIdx);
314
+ if (-1 === bracketStart) return source;
315
+ const bracketEnd = findMatchingArrayClose(source, bracketStart);
316
+ if (-1 === bracketEnd) return source;
317
+ let inner = source.slice(bracketStart + 1, bracketEnd).trim();
318
+ for (const c of calls)if (!pluginCallPresent(inner, c)) inner = inner ? `${inner}, ${c}` : c;
319
+ return source.slice(0, bracketStart + 1) + inner + source.slice(bracketEnd);
320
+ }
321
+ function addPluginsBlockAfterManifest(source, calls) {
322
+ const idx = source.indexOf(MANIFEST_LINE);
323
+ if (-1 === idx) return source;
324
+ const insertPos = idx + MANIFEST_LINE.length;
325
+ return `${source.slice(0, insertPos)}\n plugins: [${calls.join(", ")}],${source.slice(insertPos)}`;
326
+ }
327
+ function mergeScaffoldIntoAddfoxConfig(source, styleEngine) {
328
+ const st = getStylePlugin(styleEngine);
329
+ if (!st) return source;
330
+ let out = insertImportAfterLastImport(source, st.importLine);
331
+ out = mergeCallsIntoPluginsArray(out, [
332
+ st.call
333
+ ]);
334
+ return out;
335
+ }
264
336
  const UTF8_BOM = "\uFEFF";
265
337
  function stripUtf8Bom(text) {
266
338
  return text.startsWith(UTF8_BOM) ? text.slice(UTF8_BOM.length) : text;
@@ -346,10 +418,10 @@ function generateRstestConfig(language, testKinds) {
346
418
  }
347
419
  function writeUnitSample(root, language) {
348
420
  const ext = getTestExt(language);
349
- mkdirSync(resolve(root, "__tests__"), {
421
+ mkdirSync(external_node_path_resolve(root, "__tests__"), {
350
422
  recursive: true
351
423
  });
352
- const path = resolve(root, "__tests__", `example.test.${ext}`);
424
+ const path = external_node_path_resolve(root, "__tests__", `example.test.${ext}`);
353
425
  const body = "ts" === language ? `import { describe, expect, it } from "@rstest/core";
354
426
 
355
427
  describe("example unit tests", () => {
@@ -369,11 +441,11 @@ describe("example unit tests", () => {
369
441
  }
370
442
  function writeE2eSample(root, language) {
371
443
  const ext = getTestExt(language);
372
- const dir = resolve(root, "__tests__", "e2e");
444
+ const dir = external_node_path_resolve(root, "__tests__", "e2e");
373
445
  mkdirSync(dir, {
374
446
  recursive: true
375
447
  });
376
- const path = resolve(dir, `example.browser.test.${ext}`);
448
+ const path = external_node_path_resolve(dir, `example.browser.test.${ext}`);
377
449
  const body = `import { describe, expect, it } from "@rstest/core";
378
450
  import { page } from "@rstest/browser";
379
451
 
@@ -410,7 +482,7 @@ function mergeScripts(pkg, testKinds) {
410
482
  function applyTestAndReportSetup(root, language, selection) {
411
483
  const { testKinds, installRsdoctor } = selection;
412
484
  if (0 === testKinds.length && !installRsdoctor) return;
413
- const pkgPath = resolve(root, "package.json");
485
+ const pkgPath = external_node_path_resolve(root, "package.json");
414
486
  const pkg = readJsonFile(pkgPath);
415
487
  mergeDevDeps(pkg, testKinds, installRsdoctor);
416
488
  mergeScripts(pkg, testKinds);
@@ -418,22 +490,12 @@ function applyTestAndReportSetup(root, language, selection) {
418
490
  if (testKinds.length > 0) {
419
491
  const configName = "ts" === language ? "rstest.config.ts" : "rstest.config.js";
420
492
  const content = generateRstestConfig(language, testKinds);
421
- writeFileSync(resolve(root, configName), content, "utf-8");
493
+ writeFileSync(external_node_path_resolve(root, configName), content, "utf-8");
422
494
  if (testKinds.includes("unit")) writeUnitSample(root, language);
423
495
  if (testKinds.includes("e2e")) writeE2eSample(root, language);
424
496
  }
425
497
  }
426
498
  const RESET = "\x1b[0m";
427
- const ORANGE = [
428
- 255,
429
- 118,
430
- 38
431
- ];
432
- const PINK = [
433
- 255,
434
- 92,
435
- 186
436
- ];
437
499
  const LOGO_LINES = [
438
500
  " █████╗ ██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗",
439
501
  "██╔══██╗██╔══██╗██╔══██╗██╔════╝██╔═══██╗╚██╗██╔╝",
@@ -442,13 +504,15 @@ const LOGO_LINES = [
442
504
  "██║ ██║██████╔╝██████╔╝██║ ╚██████╔╝██╔╝ ██╗",
443
505
  "╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝"
444
506
  ];
445
- function lerpChannel(a, b, t) {
446
- return Math.round(a + (b - a) * t);
507
+ const GRADIENT_START_256 = 208;
508
+ const GRADIENT_END_256 = 213;
509
+ function shouldEmitAnsiColor() {
510
+ return !process.env.NO_COLOR;
447
511
  }
448
- function rgbFg(r, g, b) {
449
- return `\x1b[38;2;${r};${g};${b}m`;
512
+ function ansi256Fg(code) {
513
+ return `\x1b[38;5;${code}m`;
450
514
  }
451
- function colorizeLine(line) {
515
+ function colorizeLineAnsi256(line) {
452
516
  const len = line.length;
453
517
  if (0 === len) return "";
454
518
  let out = "";
@@ -459,16 +523,20 @@ function colorizeLine(line) {
459
523
  continue;
460
524
  }
461
525
  const t = len <= 1 ? 0 : i / (len - 1);
462
- const r = lerpChannel(ORANGE[0], PINK[0], t);
463
- const g = lerpChannel(ORANGE[1], PINK[1], t);
464
- const b = lerpChannel(ORANGE[2], PINK[2], t);
465
- out += rgbFg(r, g, b) + ch;
526
+ const code = Math.round(GRADIENT_START_256 + t * (GRADIENT_END_256 - GRADIENT_START_256));
527
+ out += ansi256Fg(code) + ch;
466
528
  }
467
529
  return out + RESET;
468
530
  }
531
+ function printPlainLogo() {
532
+ console.log("");
533
+ for (const line of LOGO_LINES)console.log(line);
534
+ console.log("");
535
+ }
469
536
  function printAddfoxLogo() {
537
+ if (!shouldEmitAnsiColor()) return void printPlainLogo();
470
538
  console.log("");
471
- for (const line of LOGO_LINES)console.log(colorizeLine(line));
539
+ for (const line of LOGO_LINES)console.log(colorizeLineAnsi256(line));
472
540
  console.log("");
473
541
  }
474
542
  const SKIP_STYLE_ENTRIES = new Set([
@@ -671,7 +739,7 @@ function getStyleEngineChoicesColored() {
671
739
  }
672
740
  function getVersion() {
673
741
  try {
674
- const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
742
+ const pkgPath = external_node_path_resolve(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
675
743
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
676
744
  return pkg.version ?? "0.0.0";
677
745
  } catch {
@@ -841,7 +909,7 @@ function resolveTestSelectionFromArgv(argv) {
841
909
  };
842
910
  }
843
911
  function updatePackageName(destDir, projectName) {
844
- const pkgPath = resolve(destDir, "package.json");
912
+ const pkgPath = external_node_path_resolve(destDir, "package.json");
845
913
  if (!existsSync(pkgPath)) return;
846
914
  const pkg = readJsonFile(pkgPath);
847
915
  pkg.name = projectName.replace(/\s+/g, "-").toLowerCase();
@@ -856,10 +924,14 @@ async function main() {
856
924
  const cliLanguage = argv.language;
857
925
  printAddfoxLogo();
858
926
  console.log(blue("\n Create Addfox App\n"));
859
- const root = resolve(process.cwd(), targetDir);
927
+ const root = external_node_path_resolve(process.cwd(), targetDir);
860
928
  if (existsSync(root)) {
861
929
  const confirmed = await confirmOverwrite(targetDir);
862
930
  if (!confirmed) process.exit(0);
931
+ rmSync(root, {
932
+ recursive: true,
933
+ force: true
934
+ });
863
935
  }
864
936
  const options = cliFramework && cliLanguage ? {
865
937
  framework: cliFramework,
@@ -875,11 +947,20 @@ async function main() {
875
947
  if (!testSelection) process.exit(0);
876
948
  const pm = options.packageManager;
877
949
  const templateName = getTemplateName(options.framework, options.language);
878
- console.log(yellow("\n Downloading template...\n"));
950
+ const templateReady = hasLocalTemplate(templateName);
951
+ const templateLabel = "Copying template...";
952
+ const TEMPLATE_SPINNER_MIN_MS = 800;
879
953
  try {
880
- tryLocalTemplates(templateName, root) || await downloadTemplate(templateName, root);
954
+ await runWithTemplateSpinner(templateLabel, async ()=>{
955
+ await new Promise((resolveSpinner)=>{
956
+ setImmediate(resolveSpinner);
957
+ });
958
+ await copyBundledTemplate(templateName, root);
959
+ }, templateReady ? {
960
+ minVisibleMs: TEMPLATE_SPINNER_MIN_MS
961
+ } : void 0);
881
962
  } catch (err) {
882
- console.error(red(`\n Failed to download template: ${err.message}\n`));
963
+ console.error(red(`\n Failed to copy template: ${err.message}\n`));
883
964
  process.exit(1);
884
965
  }
885
966
  const useAllEntries = options.entries.includes("__all__");
@@ -889,8 +970,12 @@ async function main() {
889
970
  ] : options.entries.filter((e)=>"__all__" !== e && existingDirs.includes(e));
890
971
  if (!useAllEntries && effectiveEntries.length > 0) filterAppEntries(root, effectiveEntries);
891
972
  const configExt = "ts" === options.language ? "ts" : "js";
892
- const configPath = resolve(root, `addfox.config.${configExt}`);
893
- const configContent = generateAddfoxConfig(options.framework, options.language, options.styleEngine);
973
+ const configPath = external_node_path_resolve(root, `addfox.config.${configExt}`);
974
+ let configContent;
975
+ if (existsSync(configPath)) {
976
+ const existing = readFileSync(configPath, "utf-8");
977
+ configContent = existing.trim().length > 0 && existing.includes("defineConfig") ? mergeScaffoldIntoAddfoxConfig(existing, options.styleEngine) : generateAddfoxConfig(options.framework, options.language, options.styleEngine);
978
+ } else configContent = generateAddfoxConfig(options.framework, options.language, options.styleEngine);
894
979
  writeFileSync(configPath, configContent, "utf-8");
895
980
  updatePackageName(root, targetDir);
896
981
  applyStyleEngine(root, options.framework, options.language, options.styleEngine);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-addfox-app",
3
- "version": "0.1.1-beta.7",
3
+ "version": "0.1.1",
4
4
  "description": "Interactive scaffolder for addfox extension projects",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "dist",
12
+ "templates",
12
13
  "LICENSE"
13
14
  ],
14
15
  "dependencies": {
@@ -16,12 +17,15 @@
16
17
  "prompts": "^2.4.2",
17
18
  "kolorist": "^1.8.0",
18
19
  "minimist": "^1.2.8",
19
- "@addfox/pkg-manager": "0.1.1-beta.4"
20
+ "@addfox/pkg-manager": "0.1.1"
21
+ },
22
+ "engines": {
23
+ "node": ">=18.0.0"
20
24
  },
21
25
  "devDependencies": {
22
- "@rslib/core": "^0.20.0",
23
- "@rstest/core": "^0.9.4",
24
- "@rstest/coverage-istanbul": "^0.3.0",
26
+ "@rslib/core": "^0.21.3",
27
+ "@rstest/core": "^0.9.9",
28
+ "@rstest/coverage-istanbul": "^0.3.2",
25
29
  "@types/node": "^20.0.0",
26
30
  "typescript": "^5.0.0"
27
31
  },
@@ -0,0 +1,3 @@
1
+ # Scaffold templates
2
+
3
+ Templates used by **create-addfox-app**. Each subdirectory (e.g. `template-vanilla-ts`, `template-react-ts`) is a full project skeleton. They live under this package and ship with the published npm tarball; the CLI copies the chosen folder into the new project directory.
@@ -0,0 +1,54 @@
1
+ # AI Agent Guide - Preact Addfox Extension
2
+
3
+ > **Priority**: Follow `.agents/skills/` first, then this guide, then [official docs](https://addfox.dev)
4
+
5
+ ## Project Overview
6
+
7
+ Preact browser extension using Addfox build tool. Uses `@rsbuild/plugin-preact` for JSX support.
8
+
9
+ ## Key Configuration Fields (addfox.config.js)
10
+
11
+ ### Required Fields
12
+
13
+ - **`manifest`** - Extension manifest.json content
14
+ - Supports `chromium` and `firefox` variants
15
+ - Addfox auto-injects entry paths
16
+
17
+ - **`plugins`** - Rsbuild plugins array
18
+ - Must include `pluginPreact()` for JSX support
19
+
20
+ ### Optional Fields (use when needed)
21
+
22
+ - **`appDir`** - Source directory (default: "app")
23
+ - **`outDir`** - Build output directory
24
+ - **`entry`** - Manual entry mapping
25
+ - **`hotReload`** - Dev server settings
26
+ - **`debug`** - Enable debug logging
27
+ - **`zip`** - Output zip configuration
28
+ - **`rsbuild`** - Override Rsbuild configuration
29
+
30
+ ## Entry Structure
31
+
32
+ Entries auto-discovered from `appDir` subdirectories. Each needs `index.jsx` as entry.
33
+
34
+ ## Framework-Specific Notes
35
+
36
+ ### Preact
37
+
38
+ - JSX-based (`.jsx` files)
39
+ - React-compatible API with smaller bundle size
40
+
41
+ ### Browser Extension APIs
42
+
43
+ - Install `webextension-polyfill` for cross-browser compatibility
44
+
45
+ ### Styling
46
+
47
+ - Supports CSS Modules
48
+ - Content scripts: use `@addfox/utils` with Shadow DOM
49
+
50
+ ## Resources
51
+
52
+ - Addfox docs: https://addfox.dev
53
+ - Rsbuild Preact plugin: https://rsbuild.dev/plugins/list/plugin-preact
54
+ - Preact docs: https://preactjs.com
@@ -0,0 +1,27 @@
1
+ import { defineConfig } from "addfox";
2
+ import { pluginPreact } from "@rsbuild/plugin-preact";
3
+
4
+ const manifest = {
5
+ name: "My Extension",
6
+ version: "1.0.0",
7
+ manifest_version: 3,
8
+ description: "Browser extension built with addfox",
9
+ permissions: ["activeTab"],
10
+ icons: {
11
+ "16": "icons/icon_128.png",
12
+ "48": "icons/icon_128.png",
13
+ "128": "icons/icon_128.png",
14
+ },
15
+ action: {
16
+ default_icon: {
17
+ "16": "icons/icon_128.png",
18
+ "48": "icons/icon_128.png",
19
+ "128": "icons/icon_128.png",
20
+ },
21
+ },
22
+ };
23
+
24
+ export default defineConfig({
25
+ manifest: { chromium: manifest, firefox: { ...manifest } },
26
+ plugins: [pluginPreact()],
27
+ });
@@ -0,0 +1,8 @@
1
+ function openWelcomePage() {
2
+ chrome.tabs.create({ url: chrome.runtime.getURL("welcome.html") });
3
+ }
4
+
5
+ chrome.runtime.onInstalled.addListener((details) => {
6
+ if (details.reason === "install" || details.reason === "update") openWelcomePage();
7
+ console.log("Extension installed");
8
+ });
@@ -0,0 +1 @@
1
+ console.log("Content script loaded");
@@ -0,0 +1,5 @@
1
+ import { h } from "preact";
2
+
3
+ export default function App() {
4
+ return <div>Hello from Preact</div>;
5
+ }
@@ -0,0 +1,5 @@
1
+ import { render, h } from "preact";
2
+ import App from "./App";
3
+
4
+ const root = document.getElementById("root");
5
+ if (root) render(h(App, null), root);
@@ -0,0 +1,31 @@
1
+ import { h } from "preact";
2
+
3
+ export default function App() {
4
+ const popupStyle = {
5
+ width: 320,
6
+ minHeight: 200,
7
+ padding: 16,
8
+ boxSizing: "border-box",
9
+ display: "flex",
10
+ flexDirection: "column",
11
+ alignItems: "center",
12
+ justifyContent: "center",
13
+ gap: 12,
14
+ fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
15
+ };
16
+
17
+ return (
18
+ <div style={popupStyle}>
19
+ <img src="/icons/icon_128.png" alt="" width={64} height={64} />
20
+ <div style={{ fontSize: 18, fontWeight: 600 }}>AddFox Preact</div>
21
+ <a
22
+ href="https://addfox.dev/"
23
+ target="_blank"
24
+ rel="noopener noreferrer"
25
+ style={{ fontSize: 13, color: "#2563eb" }}
26
+ >
27
+ addfox.dev
28
+ </a>
29
+ </div>
30
+ );
31
+ }
@@ -0,0 +1,5 @@
1
+ import { render, h } from "preact";
2
+ import App from "./App";
3
+
4
+ const root = document.getElementById("root");
5
+ if (root) render(h(App, null), root);
@@ -0,0 +1,5 @@
1
+ import { h } from "preact";
2
+
3
+ export default function App() {
4
+ return <div>Hello from Preact</div>;
5
+ }
@@ -0,0 +1,5 @@
1
+ import { render, h } from "preact";
2
+ import App from "./App";
3
+
4
+ const root = document.getElementById("root");
5
+ if (root) render(h(App, null), root);
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "my-extension",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "addfox dev",
8
+ "build": "addfox build"
9
+ },
10
+ "dependencies": {
11
+ "@addfox/utils": "^0.1.1",
12
+ "preact": "^10.29.0"
13
+ },
14
+ "devDependencies": {
15
+ "addfox": "^0.1.1",
16
+ "@rsbuild/plugin-preact": "^1.7.2"
17
+ }
18
+ }