create-extro 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sahil Mulani
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # create-extro
2
+
3
+ The scaffolder for [Extro](https://github.com/Sahilm416/extro), a framework for building Chrome extensions with file-based entrypoints, automatic Manifest V3 generation, and React routing.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ pnpm create extro
9
+ # npm create extro@latest
10
+ # yarn create extro
11
+ # bun create extro
12
+ ```
13
+
14
+ Answer the prompts, or pass a directory and skip them:
15
+
16
+ ```bash
17
+ pnpm create extro my-extension --template minimal
18
+ ```
19
+
20
+ Then start the dev server and load the unpacked extension:
21
+
22
+ ```bash
23
+ cd my-extension
24
+ pnpm install # if you skipped the install step
25
+ extro dev # writes output/chrome-mv3-dev, starts Vite with HMR
26
+ ```
27
+
28
+ Open `chrome://extensions`, turn on Developer mode, and **Load unpacked** the `output/chrome-mv3-dev` directory.
29
+
30
+ ## What you get
31
+
32
+ A clean starting point: a popup and a background service worker, plus `extro.config.ts`, icons, and a TypeScript setup. Nothing you have to delete.
33
+
34
+ Extro is file-based, so you grow it by dropping a file under `src/app/`:
35
+
36
+ - `options/page.tsx` - the options page
37
+ - `sidepanel/page.tsx` - the side panel
38
+ - `content/page.tsx` - a content-script UI (React, shadow DOM)
39
+ - `popup/settings/page.tsx`, `popup/[id]/page.tsx` - nested and dynamic routes
40
+
41
+ See the [Extro docs](https://github.com/Sahilm416/extro) for routing, layouts, and the manifest reference.
42
+
43
+ ## Options
44
+
45
+ ```
46
+ create-extro [directory] [options]
47
+
48
+ -t, --template <name> Template to use: default
49
+ --pm <manager> Force a package manager: npm, pnpm, yarn, bun
50
+ --install Install dependencies
51
+ --no-install Skip installing dependencies
52
+ --git Initialize a git repository
53
+ --no-git Skip git initialization
54
+ --overwrite Overwrite the target directory if it is not empty
55
+ -y, --yes Accept defaults and skip the prompts
56
+ -h, --help Show this help
57
+ -v, --version Show the version
58
+ ```
59
+
60
+ When the terminal is not interactive (CI, piped output) or `--yes` is passed, the prompts are skipped and flags plus defaults drive the run.
61
+
62
+ ## License
63
+
64
+ [MIT](https://github.com/Sahilm416/extro/blob/main/LICENSE) © Sahil Mulani
package/dist/args.js ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @describe Hand-rolled argv parser. A scaffolder's UX lives in its prompts,
3
+ * not its flags, so we avoid a parser dependency and keep full control over
4
+ * `--help`. Supports `--flag`, `--no-flag`, `--key value`, `--key=value`, and
5
+ * the short aliases `-t`, `-y`, `-h`, `-v`.
6
+ */
7
+ export const parseArgs = (argv) => {
8
+ const opts = { unknown: [] };
9
+ for (let i = 0; i < argv.length; i++) {
10
+ const arg = argv[i];
11
+ if (!arg.startsWith("-")) {
12
+ if (opts.projectName === undefined)
13
+ opts.projectName = arg;
14
+ continue;
15
+ }
16
+ let key = arg;
17
+ let inlineValue;
18
+ const eq = arg.indexOf("=");
19
+ if (arg.startsWith("--") && eq !== -1) {
20
+ key = arg.slice(0, eq);
21
+ inlineValue = arg.slice(eq + 1);
22
+ }
23
+ const takeValue = () => inlineValue !== undefined ? inlineValue : argv[++i];
24
+ switch (key) {
25
+ case "-h":
26
+ case "--help":
27
+ opts.help = true;
28
+ break;
29
+ case "-v":
30
+ case "--version":
31
+ opts.version = true;
32
+ break;
33
+ case "-y":
34
+ case "--yes":
35
+ opts.yes = true;
36
+ break;
37
+ case "-t":
38
+ case "--template":
39
+ opts.template = takeValue();
40
+ break;
41
+ case "--pm":
42
+ case "--package-manager":
43
+ opts.packageManager = takeValue();
44
+ break;
45
+ case "--install":
46
+ opts.install = true;
47
+ break;
48
+ case "--no-install":
49
+ opts.install = false;
50
+ break;
51
+ case "--git":
52
+ opts.git = true;
53
+ break;
54
+ case "--no-git":
55
+ opts.git = false;
56
+ break;
57
+ case "--overwrite":
58
+ opts.overwrite = true;
59
+ break;
60
+ default:
61
+ opts.unknown.push(arg);
62
+ }
63
+ }
64
+ return opts;
65
+ };
package/dist/cli.js ADDED
@@ -0,0 +1,186 @@
1
+ import path from "node:path";
2
+ import * as p from "@clack/prompts";
3
+ import validatePackageName from "validate-npm-package-name";
4
+ import { parseArgs } from "./args.js";
5
+ import { printHelp } from "./help.js";
6
+ import { pkg } from "./pkg.js";
7
+ import { brand, brandTag, bold, dim, green } from "./colors.js";
8
+ import { TEMPLATES, DEFAULT_TEMPLATE, isTemplate } from "./templates.js";
9
+ import { detectPkgManager, isPkgManager, runScript, } from "./pkg-manager.js";
10
+ import { scaffold } from "./scaffold.js";
11
+ import { isDirEmpty, emptyDir, toValidPackageName, relativeTarget, } from "./fs.js";
12
+ import { installDependencies, isGitInstalled, isInsideGitRepo, initGitRepo, } from "./system.js";
13
+ const DEFAULT_DIR = "my-extension";
14
+ /** Abort cleanly on Ctrl-C or an explicit cancel. */
15
+ const bail = () => {
16
+ p.cancel("Cancelled.");
17
+ process.exit(0);
18
+ };
19
+ const unwrap = (value) => {
20
+ if (p.isCancel(value))
21
+ bail();
22
+ return value;
23
+ };
24
+ /** First validation error/warning for a package name, or undefined if valid. */
25
+ const packageNameError = (name) => {
26
+ const { validForNewPackages, errors, warnings } = validatePackageName(name);
27
+ if (validForNewPackages)
28
+ return undefined;
29
+ return (errors ?? warnings ?? ["is not a valid package name"])[0];
30
+ };
31
+ export const run = async () => {
32
+ const opts = parseArgs(process.argv.slice(2));
33
+ if (opts.help)
34
+ return printHelp();
35
+ if (opts.version) {
36
+ console.log(pkg.version);
37
+ return;
38
+ }
39
+ const cwd = process.cwd();
40
+ const interactive = Boolean(process.stdout.isTTY) && !opts.yes;
41
+ p.intro(`${brandTag(" create-extro ")} ${dim(`v${pkg.version}`)}`);
42
+ if (opts.unknown.length) {
43
+ p.log.warn(`Ignoring unknown ${plural(opts.unknown.length, "option")}: ${opts.unknown.join(", ")}`);
44
+ }
45
+ // Package manager: an explicit --pm wins, otherwise infer from the runner.
46
+ let pm = detectPkgManager();
47
+ if (opts.packageManager) {
48
+ if (!isPkgManager(opts.packageManager)) {
49
+ p.log.error(`Unknown package manager "${opts.packageManager}". Use npm, pnpm, yarn, or bun.`);
50
+ process.exit(1);
51
+ }
52
+ pm = opts.packageManager;
53
+ }
54
+ // 1. Target directory + package name.
55
+ let dirInput = opts.projectName;
56
+ if (dirInput === undefined) {
57
+ if (!interactive) {
58
+ dirInput = DEFAULT_DIR;
59
+ }
60
+ else {
61
+ dirInput = unwrap(await p.text({
62
+ message: "Where should we create your extension?",
63
+ placeholder: `./${DEFAULT_DIR}`,
64
+ defaultValue: DEFAULT_DIR,
65
+ validate: (value) => {
66
+ const input = (value ?? "").trim() || DEFAULT_DIR;
67
+ const name = toValidPackageName(path.basename(path.resolve(input)));
68
+ return packageNameError(name);
69
+ },
70
+ })).trim();
71
+ }
72
+ }
73
+ dirInput = dirInput || DEFAULT_DIR;
74
+ const targetDir = path.resolve(cwd, dirInput);
75
+ const rel = relativeTarget(cwd, targetDir);
76
+ const packageName = toValidPackageName(path.basename(targetDir));
77
+ const nameError = packageNameError(packageName);
78
+ if (nameError) {
79
+ p.log.error(`Cannot use "${packageName}" as a package name: it ${nameError}.`);
80
+ process.exit(1);
81
+ }
82
+ // 2. Resolve a conflicting, non-empty target directory.
83
+ if (!isDirEmpty(targetDir)) {
84
+ if (opts.overwrite) {
85
+ emptyDir(targetDir);
86
+ }
87
+ else if (!interactive) {
88
+ p.log.error(`${bold(rel)} is not empty. Pass --overwrite to replace its contents.`);
89
+ process.exit(1);
90
+ }
91
+ else {
92
+ const choice = unwrap(await p.select({
93
+ message: `${bold(rel)} is not empty. How should we proceed?`,
94
+ initialValue: "cancel",
95
+ options: [
96
+ { value: "cancel", label: "Cancel" },
97
+ { value: "overwrite", label: "Remove existing files and continue" },
98
+ { value: "ignore", label: "Ignore files and continue", hint: "may conflict" },
99
+ ],
100
+ }));
101
+ if (choice === "cancel")
102
+ bail();
103
+ if (choice === "overwrite")
104
+ emptyDir(targetDir);
105
+ }
106
+ }
107
+ // 3. Template.
108
+ let template = opts.template ?? DEFAULT_TEMPLATE;
109
+ if (opts.template) {
110
+ if (!isTemplate(opts.template)) {
111
+ p.log.error(`Unknown template "${opts.template}". Choose from: ${TEMPLATES.map((t) => t.name).join(", ")}.`);
112
+ process.exit(1);
113
+ }
114
+ }
115
+ else if (interactive && TEMPLATES.length > 1) {
116
+ template = unwrap(await p.select({
117
+ message: "Which template?",
118
+ initialValue: DEFAULT_TEMPLATE,
119
+ options: TEMPLATES.map((t) => ({ value: t.name, label: t.label, hint: t.hint })),
120
+ }));
121
+ }
122
+ // 4. Install dependencies?
123
+ let doInstall = opts.install;
124
+ if (doInstall === undefined) {
125
+ doInstall = interactive
126
+ ? unwrap(await p.confirm({ message: `Install dependencies with ${brand(pm)}?`, initialValue: true }))
127
+ : false;
128
+ }
129
+ // 5. Initialize git? Never re-init when already inside a repo.
130
+ const gitAvailable = isGitInstalled() && !isInsideGitRepo(cwd);
131
+ let doGit = opts.git;
132
+ if (doGit === undefined) {
133
+ doGit =
134
+ interactive && gitAvailable
135
+ ? unwrap(await p.confirm({ message: "Initialize a git repository?", initialValue: true }))
136
+ : false;
137
+ }
138
+ if (doGit && !gitAvailable) {
139
+ p.log.warn("Skipping git: already inside a repository or git is not installed.");
140
+ doGit = false;
141
+ }
142
+ // 6. Write the template.
143
+ const scaffolding = p.spinner();
144
+ scaffolding.start(`Scaffolding the ${brand(template)} template`);
145
+ try {
146
+ scaffold({ templateName: template, targetDir, packageName });
147
+ }
148
+ catch (error) {
149
+ scaffolding.stop("Failed to scaffold the template");
150
+ throw error;
151
+ }
152
+ scaffolding.stop(`Created ${bold(rel)}`);
153
+ // 7. Install.
154
+ if (doInstall) {
155
+ const installing = p.spinner();
156
+ installing.start(`Installing dependencies with ${brand(pm)}`);
157
+ if (installDependencies(pm, targetDir)) {
158
+ installing.stop("Installed dependencies");
159
+ }
160
+ else {
161
+ installing.stop("Could not install dependencies");
162
+ p.log.warn(`Run ${brand(installCommand(pm))} yourself to finish setup.`);
163
+ doInstall = false;
164
+ }
165
+ }
166
+ // 8. Git.
167
+ if (doGit) {
168
+ const initializing = p.spinner();
169
+ initializing.start("Initializing a git repository");
170
+ initializing.stop(initGitRepo(targetDir)
171
+ ? "Initialized a git repository"
172
+ : "Could not initialize git");
173
+ }
174
+ // 9. Next steps.
175
+ const steps = [];
176
+ if (rel !== ".")
177
+ steps.push(`cd ${rel}`);
178
+ if (!doInstall)
179
+ steps.push(installCommand(pm));
180
+ steps.push(runScript(pm, "dev"));
181
+ p.note(steps.map((step, i) => `${dim(`${i + 1}.`)} ${brand(step)}`).join("\n"), "Next steps");
182
+ p.log.message(`Then open ${bold("chrome://extensions")}, enable Developer mode, and ${bold("Load unpacked")}\n${dim(`from ${rel === "." ? "" : `${rel}/`}output/chrome-mv3-dev`)}.`);
183
+ p.outro(`${green("✓")} Welcome to Extro. ${dim("https://github.com/Sahilm416/extro")}`);
184
+ };
185
+ const installCommand = (pm) => pm === "yarn" ? "yarn" : `${pm} install`;
186
+ const plural = (count, word) => count === 1 ? word : `${word}s`;
package/dist/colors.js ADDED
@@ -0,0 +1,13 @@
1
+ import pc from "picocolors";
2
+ // Extro brand terracotta (#CC785C / rgb 204,120,92) on the logo's near-black
3
+ // (#0a0a0a). This mirrors the framework CLI (packages/extrojs logger) so
4
+ // `create-extro` and `extro dev` read as one coherent voice. picocolors only
5
+ // covers the 16 ANSI names, so emit 24-bit truecolor directly, gated on the
6
+ // same color-support check picocolors uses (auto-plain in pipes and CI).
7
+ export const brand = (s) => pc.isColorSupported ? `\x1b[38;2;204;120;92m${s}\x1b[39m` : s;
8
+ export const brandTag = (s) => pc.isColorSupported
9
+ ? `\x1b[1m\x1b[48;2;204;120;92m\x1b[38;2;10;10;10m${s}\x1b[0m`
10
+ : s;
11
+ export const dim = pc.dim;
12
+ export const bold = pc.bold;
13
+ export const green = pc.green;
package/dist/fs.js ADDED
@@ -0,0 +1,33 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ /** True when the path does not exist, or exists with nothing but a `.git` dir. */
4
+ export const isDirEmpty = (dir) => {
5
+ if (!fs.existsSync(dir))
6
+ return true;
7
+ const entries = fs.readdirSync(dir);
8
+ return entries.length === 0 || (entries.length === 1 && entries[0] === ".git");
9
+ };
10
+ /** Remove every child of `dir` except `.git`, leaving the directory itself. */
11
+ export const emptyDir = (dir) => {
12
+ if (!fs.existsSync(dir))
13
+ return;
14
+ for (const entry of fs.readdirSync(dir)) {
15
+ if (entry === ".git")
16
+ continue;
17
+ fs.rmSync(path.join(dir, entry), { recursive: true, force: true });
18
+ }
19
+ };
20
+ /**
21
+ * @describe Coerce a directory basename into a valid npm package name: lower
22
+ * case, spaces to hyphens, strip leading dots/underscores, and replace any
23
+ * remaining illegal characters. Mirrors the normalization create-vite applies.
24
+ */
25
+ export const toValidPackageName = (input) => input
26
+ .trim()
27
+ .toLowerCase()
28
+ .replace(/\s+/g, "-")
29
+ .replace(/^[._]+/, "")
30
+ .replace(/[^a-z0-9-~]+/g, "-")
31
+ .replace(/^-+|-+$/g, "") || "extro-extension";
32
+ /** Display form of the target: the cwd-relative path, or "." for the cwd itself. */
33
+ export const relativeTarget = (cwd, targetDir) => path.relative(cwd, targetDir) || ".";
package/dist/help.js ADDED
@@ -0,0 +1,34 @@
1
+ import { brand, brandTag, bold, dim } from "./colors.js";
2
+ import { pkg } from "./pkg.js";
3
+ import { TEMPLATES } from "./templates.js";
4
+ export const printHelp = () => {
5
+ const templates = TEMPLATES.map((t) => t.name).join(", ");
6
+ const lines = [
7
+ "",
8
+ ` ${brandTag(" create-extro ")} ${dim(`v${pkg.version}`)}`,
9
+ "",
10
+ ` Scaffold a new ${brand("Extro")} extension.`,
11
+ "",
12
+ ` ${bold("Usage")}`,
13
+ ` ${brand("create-extro")} [directory] [options]`,
14
+ "",
15
+ ` ${bold("Options")}`,
16
+ ` -t, --template <name> Template to use: ${dim(templates)}`,
17
+ ` --pm <manager> Force a package manager: ${dim("npm, pnpm, yarn, bun")}`,
18
+ ` --install Install dependencies`,
19
+ ` --no-install Skip installing dependencies`,
20
+ ` --git Initialize a git repository`,
21
+ ` --no-git Skip git initialization`,
22
+ ` --overwrite Overwrite the target directory if it is not empty`,
23
+ ` -y, --yes Accept defaults and skip the prompts`,
24
+ ` -h, --help Show this help`,
25
+ ` -v, --version Show the version`,
26
+ "",
27
+ ` ${bold("Examples")}`,
28
+ ` ${dim("pnpm create extro")}`,
29
+ ` ${dim("pnpm create extro my-extension")}`,
30
+ ` ${dim("npm create extro@latest my-extension -- --no-install")}`,
31
+ "",
32
+ ];
33
+ console.log(lines.join("\n"));
34
+ };
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "./cli.js";
3
+ run().catch((err) => {
4
+ console.error(err);
5
+ process.exit(1);
6
+ });
@@ -0,0 +1,21 @@
1
+ const KNOWN = ["npm", "pnpm", "yarn", "bun"];
2
+ export const isPkgManager = (name) => KNOWN.includes(name);
3
+ /**
4
+ * @describe Resolves the package manager that invoked the scaffolder from
5
+ * `npm_config_user_agent` (set by every runner, e.g. `pnpm/10.15.1 ...`), so
6
+ * `pnpm create extro` defaults to pnpm and `npm create` defaults to npm.
7
+ * Falls back to npm when the agent is absent (an unusual direct invocation).
8
+ */
9
+ export const detectPkgManager = (userAgent = process.env.npm_config_user_agent) => {
10
+ if (!userAgent)
11
+ return "npm";
12
+ const name = userAgent.split(" ")[0]?.split("/")[0];
13
+ return name && isPkgManager(name) ? name : "npm";
14
+ };
15
+ /** The argv that installs dependencies. yarn installs with a bare invocation. */
16
+ export const installArgs = (pm) => pm === "yarn" ? [] : ["install"];
17
+ /**
18
+ * @describe The command a user types to run a package script. npm and bun need
19
+ * the explicit `run`; pnpm and yarn forward bare script names.
20
+ */
21
+ export const runScript = (pm, script) => pm === "npm" || pm === "bun" ? `${pm} run ${script}` : `${pm} ${script}`;
package/dist/pkg.js ADDED
@@ -0,0 +1,5 @@
1
+ import { readFileSync } from "node:fs";
2
+ // Read at runtime rather than `import`ing package.json: it lives outside
3
+ // `rootDir: src`, so tsc would reject a static import. The URL resolves the
4
+ // same in the workspace (dist/pkg.js -> ../package.json) and once published.
5
+ export const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
@@ -0,0 +1,41 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const TEMPLATES_ROOT = fileURLToPath(new URL("../templates", import.meta.url));
5
+ // Dotfiles ship under `_`-prefixed names so npm does not rewrite them at
6
+ // publish time (it renames a packaged `.gitignore` to `.npmignore`) and so
7
+ // they survive `npm pack`. They are restored to their real names on copy.
8
+ const RENAME = {
9
+ _gitignore: ".gitignore",
10
+ "_env.example": ".env.example",
11
+ };
12
+ export const templateDir = (name) => path.join(TEMPLATES_ROOT, name);
13
+ /**
14
+ * @describe Copy a template into the target directory, restoring `_`-prefixed
15
+ * dotfiles to their real names and stamping the chosen package name into
16
+ * package.json. Everything else is copied byte-for-byte.
17
+ */
18
+ export const scaffold = ({ templateName, targetDir, packageName, }) => {
19
+ copyDir(templateDir(templateName), targetDir, packageName);
20
+ };
21
+ const copyDir = (srcDir, destDir, packageName) => {
22
+ fs.mkdirSync(destDir, { recursive: true });
23
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
24
+ const srcPath = path.join(srcDir, entry.name);
25
+ const destPath = path.join(destDir, RENAME[entry.name] ?? entry.name);
26
+ if (entry.isDirectory()) {
27
+ copyDir(srcPath, destPath, packageName);
28
+ }
29
+ else if (entry.name === "package.json") {
30
+ writePackageJson(srcPath, destPath, packageName);
31
+ }
32
+ else {
33
+ fs.copyFileSync(srcPath, destPath);
34
+ }
35
+ }
36
+ };
37
+ const writePackageJson = (srcPath, destPath, packageName) => {
38
+ const manifest = JSON.parse(fs.readFileSync(srcPath, "utf8"));
39
+ manifest.name = packageName;
40
+ fs.writeFileSync(destPath, `${JSON.stringify(manifest, null, 2)}\n`);
41
+ };
package/dist/system.js ADDED
@@ -0,0 +1,30 @@
1
+ import spawn from "cross-spawn";
2
+ import { installArgs } from "./pkg-manager.js";
3
+ /** Install dependencies in `cwd` with the given manager. Returns success. */
4
+ export const installDependencies = (pm, cwd) => {
5
+ const result = spawn.sync(pm, installArgs(pm), { cwd, stdio: "ignore" });
6
+ return result.status === 0;
7
+ };
8
+ /** Whether a `git` binary is on PATH. */
9
+ export const isGitInstalled = () => spawn.sync("git", ["--version"], { stdio: "ignore" }).status === 0;
10
+ /** Whether `dir` already sits inside a git work tree (so we should not re-init). */
11
+ export const isInsideGitRepo = (dir) => spawn.sync("git", ["rev-parse", "--is-inside-work-tree"], {
12
+ cwd: dir,
13
+ stdio: "ignore",
14
+ }).status === 0;
15
+ /**
16
+ * @describe Initialize a git repository in `dir` with a single initial commit.
17
+ * The commit is best-effort: a missing git identity fails only the commit, not
18
+ * the init, so a fresh checkout still lands as a valid (if uncommitted) repo.
19
+ * Returns whether `git init` succeeded.
20
+ */
21
+ export const initGitRepo = (dir) => {
22
+ const opts = { cwd: dir, stdio: "ignore" };
23
+ const inited = spawn.sync("git", ["init", "-b", "main"], opts).status === 0 ||
24
+ spawn.sync("git", ["init"], opts).status === 0;
25
+ if (!inited)
26
+ return false;
27
+ spawn.sync("git", ["add", "-A"], opts);
28
+ spawn.sync("git", ["commit", "-m", "Initial commit from create-extro"], opts);
29
+ return true;
30
+ };
@@ -0,0 +1,14 @@
1
+ // One curated, minimal starter. Extro's premise is that surfaces are files you
2
+ // drop under src/app/, so the scaffold ships the canonical surface (a popup)
3
+ // plus a background worker and lets the convention add the rest, rather than
4
+ // pre-generating every surface. The registry stays a list so more starters can
5
+ // be added later without reshaping the call sites.
6
+ export const TEMPLATES = [
7
+ {
8
+ name: "default",
9
+ label: "Default",
10
+ hint: "a popup and a background service worker",
11
+ },
12
+ ];
13
+ export const DEFAULT_TEMPLATE = "default";
14
+ export const isTemplate = (name) => TEMPLATES.some((template) => template.name === name);
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "create-extro",
3
+ "version": "0.0.0",
4
+ "description": "Scaffold a new Extro extension. The official starter for the Extro framework.",
5
+ "keywords": [
6
+ "extro",
7
+ "create-extro",
8
+ "chrome-extension",
9
+ "browser-extension",
10
+ "manifest-v3",
11
+ "scaffold",
12
+ "starter",
13
+ "vite",
14
+ "react"
15
+ ],
16
+ "author": "Sahil Mulani <sahilmulani501@gmail.com>",
17
+ "license": "MIT",
18
+ "homepage": "https://github.com/Sahilm416/extro#readme",
19
+ "bugs": {
20
+ "url": "https://github.com/Sahilm416/extro/issues"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/Sahilm416/extro.git",
25
+ "directory": "packages/create-extro"
26
+ },
27
+ "type": "module",
28
+ "bin": {
29
+ "create-extro": "./dist/index.js"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "templates"
34
+ ],
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "engines": {
39
+ "node": ">=20"
40
+ },
41
+ "dependencies": {
42
+ "@clack/prompts": "^1.5.1",
43
+ "cross-spawn": "^7.0.6",
44
+ "picocolors": "^1.1.1",
45
+ "validate-npm-package-name": "^8.0.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/cross-spawn": "^6.0.6",
49
+ "@types/validate-npm-package-name": "^4.0.2"
50
+ },
51
+ "scripts": {
52
+ "build": "tsc",
53
+ "dev": "tsc -w",
54
+ "typecheck": "tsc --noEmit",
55
+ "test": "vitest run"
56
+ }
57
+ }
@@ -0,0 +1,31 @@
1
+ # Extro extension
2
+
3
+ An extension scaffolded with [Extro](https://github.com/Sahilm416/extro): file-based entrypoints, automatic Manifest V3 generation, and React routing, all driven by a single Vite plugin.
4
+
5
+ ## Develop
6
+
7
+ ```bash
8
+ pnpm dev
9
+ ```
10
+
11
+ `extro dev` starts a Vite dev server with HMR and writes an unpacked extension to `output/chrome-mv3-dev`. Open `chrome://extensions`, turn on Developer mode, and load that directory with **Load unpacked**. It stays loaded across dev restarts, so there is no manual reload between sessions.
12
+
13
+ ## Build
14
+
15
+ ```bash
16
+ pnpm build
17
+ ```
18
+
19
+ Writes a production bundle to `output/chrome-mv3-prod`, ready to load or zip for the Chrome Web Store.
20
+
21
+ ## Project layout
22
+
23
+ This starter ships two surfaces. Add more by dropping a file under `src/app/`:
24
+
25
+ ```
26
+ src/app/
27
+ ├── popup/page.tsx the toolbar popup
28
+ └── background/index.ts the background service worker
29
+ ```
30
+
31
+ Add `options/page.tsx`, `sidepanel/page.tsx`, or `content/page.tsx` to grow into the other surfaces. Configure the generated manifest in `extro.config.ts`. See the [Extro docs](https://github.com/Sahilm416/extro) for the full reference.
@@ -0,0 +1,5 @@
1
+ # Copy to .env.development / .env.production and fill in.
2
+ # Only EXTRO_PUBLIC_* is inlined into surfaces via import.meta.env. Anything
3
+ # else stays build-time only and is never shipped into a bundle.
4
+
5
+ EXTRO_PUBLIC_GREETING=Hello from Extro
@@ -0,0 +1,12 @@
1
+ node_modules
2
+ output
3
+ .output
4
+
5
+ *.tsbuildinfo
6
+ *.log
7
+
8
+ .env
9
+ .env.*
10
+ !.env.example
11
+
12
+ .DS_Store
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "extrojs"
2
+
3
+ export default defineConfig({
4
+ name: "My Extension",
5
+ description: "An extension built with Extro.",
6
+ permissions: ["storage"],
7
+ })
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "extro-extension",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "extro dev",
8
+ "build": "extro build",
9
+ "typecheck": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "extrojs": "^0.3.0",
13
+ "react": "^19.2.4",
14
+ "react-dom": "^19.2.4"
15
+ },
16
+ "devDependencies": {
17
+ "@types/chrome": "^0.1.42",
18
+ "@types/react": "^19.2.14",
19
+ "@types/react-dom": "^19.2.3",
20
+ "typescript": "^5.9.3"
21
+ }
22
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
2
+ <rect x="13.6" y="13.6" width="56" height="56" rx="10.08" fill="#0a0a0a" />
3
+ <rect x="30.4" y="30.4" width="56" height="56" rx="10.08" fill="#CC785C" />
4
+ </svg>
@@ -0,0 +1,6 @@
1
+ // The background service worker. Runs in the extension's own context with full
2
+ // access to the chrome.* APIs. This file is the entrypoint; import freely.
3
+
4
+ chrome.runtime.onInstalled.addListener(() => {
5
+ console.log("Extension installed")
6
+ })
@@ -0,0 +1,29 @@
1
+ import { useState } from "react"
2
+ import { asset } from "extrojs/asset"
3
+
4
+ export default function Popup() {
5
+ const [count, setCount] = useState(0)
6
+
7
+ return (
8
+ <div style={{ width: 240, padding: 16, fontFamily: "system-ui, sans-serif" }}>
9
+ <div
10
+ style={{
11
+ display: "flex",
12
+ alignItems: "center",
13
+ gap: 8,
14
+ marginBottom: 16,
15
+ }}
16
+ >
17
+ {/* asset() wraps chrome.runtime.getURL. logo.svg lives in public/. */}
18
+ <img src={asset("logo.svg")} width={20} height={20} alt="Extro" />
19
+ <strong>My Extension</strong>
20
+ </div>
21
+
22
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
23
+ <button onClick={() => setCount((n) => n - 1)}>-</button>
24
+ <span style={{ minWidth: 24, textAlign: "center" }}>{count}</span>
25
+ <button onClick={() => setCount((n) => n + 1)}>+</button>
26
+ </div>
27
+ </div>
28
+ )
29
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "types": ["chrome"]
14
+ },
15
+ "include": ["src", "extro.config.ts"]
16
+ }