create-blixis 0.1.0 → 0.1.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/README.md CHANGED
@@ -9,11 +9,12 @@ yarn create blixis my-app
9
9
  bun create blixis my-app
10
10
  ```
11
11
 
12
- Writes a runnable starter (service, controller, module, `main.ts`, `tsconfig.json` with the decorator settings Blixis needs), then installs `@blixis-io/core`, `@blixis-io/di`, `@blixis-io/http`, `typescript` and `@types/node` with whichever package manager invoked it.
12
+ Writes a runnable starter (service, controller, module, `main.ts`, `tsconfig.json` with the decorator settings Blixis needs), then installs `@blixis-io/core`, `@blixis-io/di`, `@blixis-io/http`, `typescript`, `@types/node` and `concurrently` with whichever package manager invoked it.
13
13
 
14
14
  ```bash
15
15
  cd my-app
16
- pnpm build && pnpm start
16
+ pnpm dev # rebuilds and restarts on every change
17
+ # or: pnpm build && pnpm start
17
18
  curl http://localhost:3000/hello/world # {"message":"Hello, world!"}
18
19
  ```
19
20
 
package/dist/index.d.ts CHANGED
@@ -14,6 +14,8 @@ export interface CreateOptions {
14
14
  install?: InstallRunner;
15
15
  }
16
16
  export declare function detectPackageManager(userAgent: string | undefined): PackageManager;
17
+ /** `"pnpm@11.25.0"` from an `npm_config_user_agent` like `pnpm/11.25.0 npm/? node/v24.0.0`, for the `packageManager` field. Bun isn't managed by corepack, so it gets none. */
18
+ export declare function packageManagerPin(userAgent: string | undefined): string | undefined;
17
19
  /** The whole CLI as a function of argv + environment — no process.exit()/console.log(), so it is directly testable. */
18
20
  export declare function runCreate(argv: readonly string[], options: CreateOptions): Promise<CreateResult>;
19
21
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";YAgBY;iBAEK;EACf;EACA;EACA;;;YAIU,iBAAiB,iBAAiB,yBAAyB,gBAAgB;iBAEtE;EACf;;EAEA;EACA,UAAU;;wBAGI,qBAAqB,gCAAgC;;wBAqB/C,UAAU,yBAAyB,SAAS,gBAAgB,QAAQ"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";YAgBY;iBAEK;EACf;EACA;EACA;;;YAIU,iBAAiB,iBAAiB,yBAAyB,gBAAgB;iBAEtE;EACf;;EAEA;EACA,UAAU;;wBAGI,qBAAqB,gCAAgC;;wBAMrD,kBAAkB;;wBAqBZ,UAAU,yBAAyB,SAAS,gBAAgB,QAAQ"}
package/dist/index.js CHANGED
@@ -5,15 +5,17 @@ import { basename, dirname, join, relative, resolve } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
  //#region src/template.ts
7
7
  /** Every file of the generated app, keyed by path relative to the project root. */
8
- function templateFiles(packageName) {
8
+ function templateFiles(packageName, packageManagerPin) {
9
9
  return {
10
10
  "package.json": `${JSON.stringify({
11
11
  name: packageName,
12
12
  version: "0.0.0",
13
13
  private: true,
14
14
  type: "module",
15
+ ...packageManagerPin ? { packageManager: packageManagerPin } : {},
15
16
  engines: { node: ">=24" },
16
17
  scripts: {
18
+ dev: "tsc -p tsconfig.json && concurrently -k -n tsc,app \"tsc -p tsconfig.json --watch --preserveWatchOutput\" \"node --watch dist/main.js\"",
17
19
  build: "tsc -p tsconfig.json",
18
20
  start: "node dist/main.js",
19
21
  typecheck: "tsc --noEmit -p tsconfig.json"
@@ -43,7 +45,8 @@ function templateFiles(packageName) {
43
45
  A [Blixis](https://blixis-io.github.io/framework/) app.
44
46
 
45
47
  \`\`\`bash
46
- npm run build && npm start
48
+ npm run dev # rebuilds and restarts on every change
49
+ # or: npm run build && npm start
47
50
  curl http://localhost:3000/hello/world
48
51
  \`\`\`
49
52
 
@@ -102,7 +105,11 @@ const RUNTIME_DEPENDENCIES = [
102
105
  "@blixis-io/di",
103
106
  "@blixis-io/http"
104
107
  ];
105
- const DEV_DEPENDENCIES = ["typescript", "@types/node"];
108
+ const DEV_DEPENDENCIES = [
109
+ "typescript",
110
+ "@types/node",
111
+ "concurrently"
112
+ ];
106
113
  //#endregion
107
114
  //#region src/scaffold.ts
108
115
  const PACKAGE_NAME = /^[a-z0-9-~][a-z0-9-._~]*$/;
@@ -113,12 +120,12 @@ var ScaffoldError = class extends Error {
113
120
  }
114
121
  };
115
122
  /** Writes the starter app into `cwd/<target>`. The directory may not exist yet, or must be empty. */
116
- function scaffold(cwd, target) {
123
+ function scaffold(cwd, target, packageManagerPin) {
117
124
  const directory = resolve(cwd, target);
118
125
  const packageName = basename(directory);
119
126
  if (!PACKAGE_NAME.test(packageName)) throw new ScaffoldError(`"${packageName}" is not a valid package name — use lowercase letters, digits, "-", "." or "_", starting with a letter or digit.`);
120
127
  if (existsSync(directory) && readdirSync(directory).length > 0) throw new ScaffoldError(`${target} already exists and is not empty.`);
121
- const files = Object.entries(templateFiles(packageName));
128
+ const files = Object.entries(templateFiles(packageName, packageManagerPin));
122
129
  for (const [path, content] of files) {
123
130
  const absolute = join(directory, path);
124
131
  mkdirSync(dirname(absolute), { recursive: true });
@@ -143,6 +150,11 @@ function detectPackageManager(userAgent) {
143
150
  const name = userAgent?.split("/")[0];
144
151
  return name === "pnpm" || name === "yarn" || name === "bun" ? name : "npm";
145
152
  }
153
+ /** `"pnpm@11.25.0"` from an `npm_config_user_agent` like `pnpm/11.25.0 npm/? node/v24.0.0`, for the `packageManager` field. Bun isn't managed by corepack, so it gets none. */
154
+ function packageManagerPin(userAgent) {
155
+ const match = /^(pnpm|npm|yarn)\/(\d+\.\d+\.\d+)/.exec(userAgent ?? "");
156
+ return match ? `${match[1]}@${match[2]}` : void 0;
157
+ }
146
158
  function addArgs(pm, dev, packages) {
147
159
  return [
148
160
  pm === "npm" ? "install" : "add",
@@ -183,7 +195,7 @@ async function runCreate(argv, options) {
183
195
  };
184
196
  let result;
185
197
  try {
186
- result = scaffold(options.cwd, target);
198
+ result = scaffold(options.cwd, target, packageManagerPin(options.userAgent));
187
199
  } catch (error) {
188
200
  if (error instanceof ScaffoldError) return {
189
201
  exitCode: 1,
@@ -208,7 +220,7 @@ async function runCreate(argv, options) {
208
220
  stderr: `\n"${pm} ${args.join(" ")}" failed (exit ${code}). The files are in ${dir}; run the install yourself.\n`
209
221
  };
210
222
  }
211
- stdout += `\nNext:\n cd ${dir}\n ${pm === "npm" ? "npm run" : pm} build && ${pm} start\n curl http://localhost:3000/hello/world\n`;
223
+ stdout += `\nNext:\n cd ${dir}\n ${pm === "npm" ? "npm run" : pm} dev\n curl http://localhost:3000/hello/world\n`;
212
224
  }
213
225
  return {
214
226
  exitCode: 0,
@@ -228,6 +240,6 @@ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.ar
228
240
  }
229
241
  /* v8 ignore stop */
230
242
  //#endregion
231
- export { detectPackageManager, runCreate };
243
+ export { detectPackageManager, packageManagerPin, runCreate };
232
244
 
233
245
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/template.ts","../src/scaffold.ts","../src/index.ts"],"sourcesContent":["/** Every file of the generated app, keyed by path relative to the project root. */\nexport function templateFiles(packageName: string): Record<string, string> {\n return {\n \"package.json\": `${JSON.stringify(\n {\n name: packageName,\n version: \"0.0.0\",\n private: true,\n type: \"module\",\n engines: { node: \">=24\" },\n scripts: {\n build: \"tsc -p tsconfig.json\",\n start: \"node dist/main.js\",\n typecheck: \"tsc --noEmit -p tsconfig.json\",\n },\n },\n null,\n 2,\n )}\\n`,\n\n \"tsconfig.json\": `${JSON.stringify(\n {\n compilerOptions: {\n target: \"es2023\",\n lib: [\"es2023\"],\n types: [\"node\"],\n module: \"nodenext\",\n moduleResolution: \"nodenext\",\n strict: true,\n experimentalDecorators: true,\n emitDecoratorMetadata: true,\n useDefineForClassFields: false,\n esModuleInterop: true,\n skipLibCheck: true,\n outDir: \"dist\",\n rootDir: \"src\",\n },\n include: [\"src\"],\n },\n null,\n 2,\n )}\\n`,\n\n \".gitignore\": \"node_modules\\ndist\\n\",\n\n \"README.md\": `# ${packageName}\n\nA [Blixis](https://blixis-io.github.io/framework/) app.\n\n\\`\\`\\`bash\nnpm run build && npm start\ncurl http://localhost:3000/hello/world\n\\`\\`\\`\n\nDecorators need \\`tsc\\` (not tsx/esbuild) — see the\n[installation notes](https://blixis-io.github.io/framework/start-here/installation/).\n`,\n\n \"src/hello.service.ts\": `import { Injectable } from \"@blixis-io/di\";\n\n@Injectable()\nexport class HelloService {\n greet(name: string): string {\n return \\`Hello, \\${name}!\\`;\n }\n}\n`,\n\n \"src/hello.controller.ts\": `import { Controller, Get, Param } from \"@blixis-io/http\";\nimport { HelloService } from \"./hello.service.js\";\n\n@Controller(\"hello\")\nexport class HelloController {\n constructor(private readonly hello: HelloService) {}\n\n @Get(\":name\")\n greet(@Param(\"name\") name: string) {\n return { message: this.hello.greet(name) };\n }\n}\n`,\n\n \"src/app.module.ts\": `import { Module } from \"@blixis-io/core\";\nimport { HelloController } from \"./hello.controller.js\";\nimport { HelloService } from \"./hello.service.js\";\n\n@Module({\n providers: [HelloService],\n controllers: [HelloController],\n})\nexport class AppModule {}\n`,\n\n \"src/main.ts\": `import { createHttpApplication } from \"@blixis-io/http\";\nimport { AppModule } from \"./app.module.js\";\n\nconst app = await createHttpApplication(AppModule);\nconst port = Number(process.env[\"PORT\"] ?? 3000);\nawait app.listen(port);\n\nconsole.log(\\`Listening on http://localhost:\\${port}\\`);\n\nprocess.on(\"SIGTERM\", () => {\n void app.close(\"SIGTERM\").then(() => process.exit(0));\n});\n`,\n };\n}\n\nexport const RUNTIME_DEPENDENCIES = [\"@blixis-io/core\", \"@blixis-io/di\", \"@blixis-io/http\"] as const;\nexport const DEV_DEPENDENCIES = [\"typescript\", \"@types/node\"] as const;\n","import { existsSync, mkdirSync, readdirSync, writeFileSync } from \"node:fs\";\nimport { basename, dirname, join, resolve } from \"node:path\";\nimport { templateFiles } from \"./template.js\";\n\nconst PACKAGE_NAME = /^[a-z0-9-~][a-z0-9-._~]*$/;\n\nexport class ScaffoldError extends Error {\n override readonly name = \"ScaffoldError\";\n}\n\nexport interface ScaffoldResult {\n directory: string;\n packageName: string;\n files: string[];\n}\n\n/** Writes the starter app into `cwd/<target>`. The directory may not exist yet, or must be empty. */\nexport function scaffold(cwd: string, target: string): ScaffoldResult {\n const directory = resolve(cwd, target);\n const packageName = basename(directory);\n\n if (!PACKAGE_NAME.test(packageName)) {\n throw new ScaffoldError(\n `\"${packageName}\" is not a valid package name — use lowercase letters, digits, \"-\", \".\" or \"_\", starting with a letter or digit.`,\n );\n }\n if (existsSync(directory) && readdirSync(directory).length > 0) {\n throw new ScaffoldError(`${target} already exists and is not empty.`);\n }\n\n const files = Object.entries(templateFiles(packageName));\n for (const [path, content] of files) {\n const absolute = join(directory, path);\n mkdirSync(dirname(absolute), { recursive: true });\n writeFileSync(absolute, content);\n }\n\n return { directory, packageName, files: files.map(([path]) => path) };\n}\n","#!/usr/bin/env node\nimport { spawn } from \"node:child_process\";\nimport { realpathSync } from \"node:fs\";\nimport { relative } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport { ScaffoldError, scaffold } from \"./scaffold.js\";\nimport { DEV_DEPENDENCIES, RUNTIME_DEPENDENCIES } from \"./template.js\";\n\nconst USAGE = `create-blixis <directory> [--no-install]\n\n pnpm create blixis my-app\n npm create blixis@latest my-app\n\n --no-install write the files only; print the install commands instead\n`;\n\nexport type PackageManager = \"pnpm\" | \"npm\" | \"yarn\" | \"bun\";\n\nexport interface CreateResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\n/** Runs one package-manager command; resolves with its exit code. Injected so tests never touch the network. */\nexport type InstallRunner = (command: string, args: readonly string[], cwd: string) => Promise<number>;\n\nexport interface CreateOptions {\n cwd: string;\n /** `npm_config_user_agent` — tells us which package manager invoked `create`. */\n userAgent?: string | undefined;\n install?: InstallRunner;\n}\n\nexport function detectPackageManager(userAgent: string | undefined): PackageManager {\n const name = userAgent?.split(\"/\")[0];\n return name === \"pnpm\" || name === \"yarn\" || name === \"bun\" ? name : \"npm\";\n}\n\nfunction addArgs(pm: PackageManager, dev: boolean, packages: readonly string[]): string[] {\n const verb = pm === \"npm\" ? \"install\" : \"add\";\n const devFlag = pm === \"bun\" ? \"-d\" : \"-D\";\n return [verb, ...(dev ? [devFlag] : []), ...packages];\n}\n\nconst runWithInheritedStdio: InstallRunner = (command, args, cwd) =>\n new Promise((resolve, reject) => {\n const child = spawn(command, args, { cwd, stdio: \"inherit\", shell: process.platform === \"win32\" });\n child.once(\"error\", reject);\n child.once(\"close\", (code) => {\n resolve(code ?? 1);\n });\n });\n\n/** The whole CLI as a function of argv + environment — no process.exit()/console.log(), so it is directly testable. */\nexport async function runCreate(argv: readonly string[], options: CreateOptions): Promise<CreateResult> {\n const flags = argv.filter((arg) => arg.startsWith(\"-\"));\n const [target, ...extra] = argv.filter((arg) => !arg.startsWith(\"-\"));\n\n if (flags.includes(\"--help\") || flags.includes(\"-h\")) {\n return { exitCode: 0, stdout: USAGE, stderr: \"\" };\n }\n const unknown = flags.find((flag) => flag !== \"--no-install\");\n if (unknown) {\n return { exitCode: 1, stdout: \"\", stderr: `Unknown option \"${unknown}\"\\n\\n${USAGE}` };\n }\n if (!target || extra.length > 0) {\n return { exitCode: 1, stdout: \"\", stderr: `Usage: ${USAGE}` };\n }\n\n let result;\n try {\n result = scaffold(options.cwd, target);\n } catch (error) {\n if (error instanceof ScaffoldError) {\n return { exitCode: 1, stdout: \"\", stderr: `${error.message}\\n` };\n }\n throw error;\n }\n\n const pm = detectPackageManager(options.userAgent);\n const runtime = addArgs(pm, false, RUNTIME_DEPENDENCIES);\n const dev = addArgs(pm, true, DEV_DEPENDENCIES);\n const dir = relative(options.cwd, result.directory) || \".\";\n let stdout = `Created ${result.packageName} in ${dir}\\n`;\n\n if (flags.includes(\"--no-install\")) {\n stdout += `\\nNext:\\n cd ${dir}\\n ${pm} ${runtime.join(\" \")}\\n ${pm} ${dev.join(\" \")}\\n`;\n } else {\n const install = options.install ?? runWithInheritedStdio;\n for (const args of [runtime, dev]) {\n const code = await install(pm, args, result.directory);\n if (code !== 0) {\n return {\n exitCode: code,\n stdout,\n stderr: `\\n\"${pm} ${args.join(\" \")}\" failed (exit ${code}). The files are in ${dir}; run the install yourself.\\n`,\n };\n }\n }\n const run = pm === \"npm\" ? \"npm run\" : pm;\n stdout += `\\nNext:\\n cd ${dir}\\n ${run} build && ${pm} start\\n curl http://localhost:3000/hello/world\\n`;\n }\n\n return { exitCode: 0, stdout, stderr: \"\" };\n}\n\n// Same entry-point check as @blixis-io/cli: pathToFileURL(realpathSync(...)) so a\n// bin shim's symlink and a path with spaces both still match import.meta.url.\n/* v8 ignore start -- @preserve: process wiring, exercised by index.test.ts spawning the real built binary */\nif (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {\n const result = await runCreate(process.argv.slice(2), {\n cwd: process.cwd(),\n userAgent: process.env[\"npm_config_user_agent\"],\n });\n if (result.stdout) {\n process.stdout.write(result.stdout);\n }\n if (result.stderr) {\n process.stderr.write(result.stderr);\n }\n process.exitCode = result.exitCode;\n}\n/* v8 ignore stop */\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/template.ts","../src/scaffold.ts","../src/index.ts"],"sourcesContent":["/** Every file of the generated app, keyed by path relative to the project root. */\nexport function templateFiles(packageName: string, packageManagerPin?: string): Record<string, string> {\n return {\n \"package.json\": `${JSON.stringify(\n {\n name: packageName,\n version: \"0.0.0\",\n private: true,\n type: \"module\",\n // Pins the package manager (corepack honours it), so a Docker image or CI job installs with the same\n // major version the project was created with instead of whatever is newest.\n ...(packageManagerPin ? { packageManager: packageManagerPin } : {}),\n engines: { node: \">=24\" },\n scripts: {\n // Decorators need tsc (esbuild/tsx drop the metadata), so dev is \"compile once, then\n // recompile on change while node restarts on the new output\".\n dev: 'tsc -p tsconfig.json && concurrently -k -n tsc,app \"tsc -p tsconfig.json --watch --preserveWatchOutput\" \"node --watch dist/main.js\"',\n build: \"tsc -p tsconfig.json\",\n start: \"node dist/main.js\",\n typecheck: \"tsc --noEmit -p tsconfig.json\",\n },\n },\n null,\n 2,\n )}\\n`,\n\n \"tsconfig.json\": `${JSON.stringify(\n {\n compilerOptions: {\n target: \"es2023\",\n lib: [\"es2023\"],\n types: [\"node\"],\n module: \"nodenext\",\n moduleResolution: \"nodenext\",\n strict: true,\n experimentalDecorators: true,\n emitDecoratorMetadata: true,\n useDefineForClassFields: false,\n esModuleInterop: true,\n skipLibCheck: true,\n outDir: \"dist\",\n rootDir: \"src\",\n },\n include: [\"src\"],\n },\n null,\n 2,\n )}\\n`,\n\n \".gitignore\": \"node_modules\\ndist\\n\",\n\n \"README.md\": `# ${packageName}\n\nA [Blixis](https://blixis-io.github.io/framework/) app.\n\n\\`\\`\\`bash\nnpm run dev # rebuilds and restarts on every change\n# or: npm run build && npm start\ncurl http://localhost:3000/hello/world\n\\`\\`\\`\n\nDecorators need \\`tsc\\` (not tsx/esbuild) — see the\n[installation notes](https://blixis-io.github.io/framework/start-here/installation/).\n`,\n\n \"src/hello.service.ts\": `import { Injectable } from \"@blixis-io/di\";\n\n@Injectable()\nexport class HelloService {\n greet(name: string): string {\n return \\`Hello, \\${name}!\\`;\n }\n}\n`,\n\n \"src/hello.controller.ts\": `import { Controller, Get, Param } from \"@blixis-io/http\";\nimport { HelloService } from \"./hello.service.js\";\n\n@Controller(\"hello\")\nexport class HelloController {\n constructor(private readonly hello: HelloService) {}\n\n @Get(\":name\")\n greet(@Param(\"name\") name: string) {\n return { message: this.hello.greet(name) };\n }\n}\n`,\n\n \"src/app.module.ts\": `import { Module } from \"@blixis-io/core\";\nimport { HelloController } from \"./hello.controller.js\";\nimport { HelloService } from \"./hello.service.js\";\n\n@Module({\n providers: [HelloService],\n controllers: [HelloController],\n})\nexport class AppModule {}\n`,\n\n \"src/main.ts\": `import { createHttpApplication } from \"@blixis-io/http\";\nimport { AppModule } from \"./app.module.js\";\n\nconst app = await createHttpApplication(AppModule);\nconst port = Number(process.env[\"PORT\"] ?? 3000);\nawait app.listen(port);\n\nconsole.log(\\`Listening on http://localhost:\\${port}\\`);\n\nprocess.on(\"SIGTERM\", () => {\n void app.close(\"SIGTERM\").then(() => process.exit(0));\n});\n`,\n };\n}\n\nexport const RUNTIME_DEPENDENCIES = [\"@blixis-io/core\", \"@blixis-io/di\", \"@blixis-io/http\"] as const;\nexport const DEV_DEPENDENCIES = [\"typescript\", \"@types/node\", \"concurrently\"] as const;\n","import { existsSync, mkdirSync, readdirSync, writeFileSync } from \"node:fs\";\nimport { basename, dirname, join, resolve } from \"node:path\";\nimport { templateFiles } from \"./template.js\";\n\nconst PACKAGE_NAME = /^[a-z0-9-~][a-z0-9-._~]*$/;\n\nexport class ScaffoldError extends Error {\n override readonly name = \"ScaffoldError\";\n}\n\nexport interface ScaffoldResult {\n directory: string;\n packageName: string;\n files: string[];\n}\n\n/** Writes the starter app into `cwd/<target>`. The directory may not exist yet, or must be empty. */\nexport function scaffold(cwd: string, target: string, packageManagerPin?: string): ScaffoldResult {\n const directory = resolve(cwd, target);\n const packageName = basename(directory);\n\n if (!PACKAGE_NAME.test(packageName)) {\n throw new ScaffoldError(\n `\"${packageName}\" is not a valid package name — use lowercase letters, digits, \"-\", \".\" or \"_\", starting with a letter or digit.`,\n );\n }\n if (existsSync(directory) && readdirSync(directory).length > 0) {\n throw new ScaffoldError(`${target} already exists and is not empty.`);\n }\n\n const files = Object.entries(templateFiles(packageName, packageManagerPin));\n for (const [path, content] of files) {\n const absolute = join(directory, path);\n mkdirSync(dirname(absolute), { recursive: true });\n writeFileSync(absolute, content);\n }\n\n return { directory, packageName, files: files.map(([path]) => path) };\n}\n","#!/usr/bin/env node\nimport { spawn } from \"node:child_process\";\nimport { realpathSync } from \"node:fs\";\nimport { relative } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport { ScaffoldError, scaffold } from \"./scaffold.js\";\nimport { DEV_DEPENDENCIES, RUNTIME_DEPENDENCIES } from \"./template.js\";\n\nconst USAGE = `create-blixis <directory> [--no-install]\n\n pnpm create blixis my-app\n npm create blixis@latest my-app\n\n --no-install write the files only; print the install commands instead\n`;\n\nexport type PackageManager = \"pnpm\" | \"npm\" | \"yarn\" | \"bun\";\n\nexport interface CreateResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\n/** Runs one package-manager command; resolves with its exit code. Injected so tests never touch the network. */\nexport type InstallRunner = (command: string, args: readonly string[], cwd: string) => Promise<number>;\n\nexport interface CreateOptions {\n cwd: string;\n /** `npm_config_user_agent` — tells us which package manager invoked `create`. */\n userAgent?: string | undefined;\n install?: InstallRunner;\n}\n\nexport function detectPackageManager(userAgent: string | undefined): PackageManager {\n const name = userAgent?.split(\"/\")[0];\n return name === \"pnpm\" || name === \"yarn\" || name === \"bun\" ? name : \"npm\";\n}\n\n/** `\"pnpm@11.25.0\"` from an `npm_config_user_agent` like `pnpm/11.25.0 npm/? node/v24.0.0`, for the `packageManager` field. Bun isn't managed by corepack, so it gets none. */\nexport function packageManagerPin(userAgent: string | undefined): string | undefined {\n const match = /^(pnpm|npm|yarn)\\/(\\d+\\.\\d+\\.\\d+)/.exec(userAgent ?? \"\");\n return match ? `${match[1]}@${match[2]}` : undefined;\n}\n\nfunction addArgs(pm: PackageManager, dev: boolean, packages: readonly string[]): string[] {\n const verb = pm === \"npm\" ? \"install\" : \"add\";\n const devFlag = pm === \"bun\" ? \"-d\" : \"-D\";\n return [verb, ...(dev ? [devFlag] : []), ...packages];\n}\n\nconst runWithInheritedStdio: InstallRunner = (command, args, cwd) =>\n new Promise((resolve, reject) => {\n const child = spawn(command, args, { cwd, stdio: \"inherit\", shell: process.platform === \"win32\" });\n child.once(\"error\", reject);\n child.once(\"close\", (code) => {\n resolve(code ?? 1);\n });\n });\n\n/** The whole CLI as a function of argv + environment — no process.exit()/console.log(), so it is directly testable. */\nexport async function runCreate(argv: readonly string[], options: CreateOptions): Promise<CreateResult> {\n const flags = argv.filter((arg) => arg.startsWith(\"-\"));\n const [target, ...extra] = argv.filter((arg) => !arg.startsWith(\"-\"));\n\n if (flags.includes(\"--help\") || flags.includes(\"-h\")) {\n return { exitCode: 0, stdout: USAGE, stderr: \"\" };\n }\n const unknown = flags.find((flag) => flag !== \"--no-install\");\n if (unknown) {\n return { exitCode: 1, stdout: \"\", stderr: `Unknown option \"${unknown}\"\\n\\n${USAGE}` };\n }\n if (!target || extra.length > 0) {\n return { exitCode: 1, stdout: \"\", stderr: `Usage: ${USAGE}` };\n }\n\n let result;\n try {\n result = scaffold(options.cwd, target, packageManagerPin(options.userAgent));\n } catch (error) {\n if (error instanceof ScaffoldError) {\n return { exitCode: 1, stdout: \"\", stderr: `${error.message}\\n` };\n }\n throw error;\n }\n\n const pm = detectPackageManager(options.userAgent);\n const runtime = addArgs(pm, false, RUNTIME_DEPENDENCIES);\n const dev = addArgs(pm, true, DEV_DEPENDENCIES);\n const dir = relative(options.cwd, result.directory) || \".\";\n let stdout = `Created ${result.packageName} in ${dir}\\n`;\n\n if (flags.includes(\"--no-install\")) {\n stdout += `\\nNext:\\n cd ${dir}\\n ${pm} ${runtime.join(\" \")}\\n ${pm} ${dev.join(\" \")}\\n`;\n } else {\n const install = options.install ?? runWithInheritedStdio;\n for (const args of [runtime, dev]) {\n const code = await install(pm, args, result.directory);\n if (code !== 0) {\n return {\n exitCode: code,\n stdout,\n stderr: `\\n\"${pm} ${args.join(\" \")}\" failed (exit ${code}). The files are in ${dir}; run the install yourself.\\n`,\n };\n }\n }\n const run = pm === \"npm\" ? \"npm run\" : pm;\n stdout += `\\nNext:\\n cd ${dir}\\n ${run} dev\\n curl http://localhost:3000/hello/world\\n`;\n }\n\n return { exitCode: 0, stdout, stderr: \"\" };\n}\n\n// Same entry-point check as @blixis-io/cli: pathToFileURL(realpathSync(...)) so a\n// bin shim's symlink and a path with spaces both still match import.meta.url.\n/* v8 ignore start -- @preserve: process wiring, exercised by index.test.ts spawning the real built binary */\nif (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {\n const result = await runCreate(process.argv.slice(2), {\n cwd: process.cwd(),\n userAgent: process.env[\"npm_config_user_agent\"],\n });\n if (result.stdout) {\n process.stdout.write(result.stdout);\n }\n if (result.stderr) {\n process.stderr.write(result.stderr);\n }\n process.exitCode = result.exitCode;\n}\n/* v8 ignore stop */\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-blixis",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Scaffold a new Blixis app: pnpm create blixis my-app. No @blixis-io/* runtime dependency.",
5
5
  "license": "MIT",
6
6
  "author": "Michael Voeten <michael@voeten.online>",