@dunx/create-app 0.1.0 → 0.2.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/README.md CHANGED
@@ -8,7 +8,7 @@ bunx @dunx/create-app my-api
8
8
 
9
9
  > `bun create dunx-app` does **not** work, and deliberately is not advertised:
10
10
  > `bun create <template>` resolves the unscoped npm package
11
- > `create-<template>`, which this package being scoped is not.
11
+ > `create-<template>`, which this package - being scoped - is not.
12
12
 
13
13
  ```
14
14
  cd my-api
@@ -32,8 +32,7 @@ a directory you just made.
32
32
  ## What it generates
33
33
 
34
34
  The `minimal` template, which is the same app as
35
- [`examples/minimal`](https://github.com/petarzarkov/dunx/tree/main/examples/minimal)
36
- — a service, a controller, a module, `HttpFactory.create`, one test against a real
35
+ [`examples/minimal`](https://github.com/petarzarkov/dunx/tree/main/examples/minimal) a service, a controller, a module, `HttpFactory.create`, one test against a real
37
36
  server, and the `bunfig.toml` preload line that makes constructor injection work.
38
37
 
39
38
  Its `src/` is a **byte-for-byte copy** of that example, and a test in this package
@@ -44,8 +43,8 @@ identical is what makes the template trustworthy rather than merely plausible.
44
43
 
45
44
  **Versions are resolved at run time, not written into the template.** Every
46
45
  `@dunx/*` range in the template manifest is `__DUNX_VERSION__`, replaced with a
47
- caret range on this package's own version. dunx versions in lockstep every
48
- package shares one number and ships together so the version doing the
46
+ caret range on this package's own version. dunx versions in lockstep - every
47
+ package shares one number and ships together - so the version doing the
49
48
  scaffolding is by definition a set that works together. Writing versions into the
50
49
  template would go stale on the next release.
51
50
 
@@ -64,6 +63,6 @@ const { directory, files } = await scaffold({
64
63
  });
65
64
  ```
66
65
 
67
- `scaffold` throws `ScaffoldError` for anything the caller can fix an unknown
68
- template, an unusable package name, a non-empty target without `force` and lets
66
+ `scaffold` throws `ScaffoldError` for anything the caller can fix - an unknown
67
+ template, an unusable package name, a non-empty target without `force` - and lets
69
68
  everything else propagate.
package/dist/cli.js.map CHANGED
@@ -3,7 +3,7 @@
3
3
  "sources": ["../src/cli.ts", "../src/scaffold.ts"],
4
4
  "sourcesContent": [
5
5
  "#!/usr/bin/env bun\nimport { parseArgs } from 'node:util';\nimport { relative } from 'node:path';\nimport { scaffold, ScaffoldError, TEMPLATES } from './scaffold.js';\nimport type { TemplateName } from './scaffold.js';\n\nconst USAGE = `Scaffold a new dunx application.\n\n bunx @dunx/create-app <directory> [options]\n\nOptions:\n --name <name> package name for the app (default: the directory name)\n --template <name> ${TEMPLATES.join(' | ')} (default: minimal)\n --force write into a directory that already has files in it\n --help print this\n`;\n\n// A declaration, not a `const` arrow: control-flow analysis only narrows past a\n// never-returning call when the callee is declared this way, so `target` stays\n// `string | undefined` below if this is an arrow.\nfunction fail(message: string): never {\n console.error(message);\n process.exit(1);\n}\n\nconst { values, positionals } = parseArgs({\n args: Bun.argv.slice(2),\n allowPositionals: true,\n options: {\n name: { type: 'string' },\n template: { type: 'string' },\n force: { type: 'boolean', default: false },\n help: { type: 'boolean', default: false, short: 'h' },\n },\n});\n\nif (values.help === true) {\n console.log(USAGE);\n process.exit(0);\n}\n\nconst target = positionals[0];\nif (target === undefined) {\n fail(`Missing the target directory.\\n\\n${USAGE}`);\n}\n\ntry {\n const result = await scaffold({\n target,\n ...(values.name === undefined ? {} : { name: values.name }),\n ...(values.template === undefined\n ? {}\n : { template: values.template as TemplateName }),\n force: values.force === true,\n });\n\n const where = relative(process.cwd(), result.directory) || '.';\n console.log(`Created ${result.name} in ${where}/`);\n console.log(\n ` ${result.files.length} files from the ${result.template} template\\n`,\n );\n console.log('Next:');\n console.log(` cd ${where}`);\n console.log(' bun install');\n console.log(' bun run start');\n} catch (error) {\n if (error instanceof ScaffoldError) fail(error.message);\n throw error;\n}\n",
6
- "import { existsSync, readdirSync } from 'node:fs';\nimport { basename, dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { Glob } from 'bun';\n\n/** The templates that ship with the package, as `templates/<name>/`. */\nexport const TEMPLATES = Object.freeze(['minimal'] as const);\nexport type TemplateName = (typeof TEMPLATES)[number];\n\n/**\n * Every `@dunx/*` version in a template manifest is this placeholder. Versioning\n * is lockstep, so the right version to install is whatever version of\n * `@dunx/create-app` is doing the scaffolding resolved at run time rather than\n * written into the template, which would go stale on the next release.\n */\nexport const VERSION_PLACEHOLDER = '__DUNX_VERSION__';\n\n/** npm renames a published `.gitignore` to `.npmignore`, so it ships prefixed. */\nconst RENAMED = Object.freeze({ _gitignore: '.gitignore' });\n\nexport interface ScaffoldOptions {\n /** Directory to create. Relative paths resolve against `cwd`. */\n readonly target: string;\n /** Package name for the generated app. Defaults to the target's basename. */\n readonly name?: string;\n readonly template?: TemplateName;\n /** Write into a directory that already has files in it. */\n readonly force?: boolean;\n readonly cwd?: string;\n /** Overrides the version written into the generated manifest. */\n readonly version?: string;\n}\n\nexport interface ScaffoldResult {\n readonly directory: string;\n readonly name: string;\n readonly template: TemplateName;\n readonly files: readonly string[];\n}\n\nexport class ScaffoldError extends Error {\n override readonly name = 'ScaffoldError';\n}\n\n/**\n * `dist/index.js` and `dist/cli.js` both sit one level under the package root, so\n * `../templates` resolves the same from either. In the source tree it resolves\n * from `src/`, which is the same depth so tests exercise the real path rather\n * than a special case.\n *\n * `fileURLToPath`, not `new URL(...).pathname`: the latter stays percent-encoded,\n * so an install under a directory with a space in it looks for `space%20test/`\n * and reports the template missing. On Windows it is worse it yields a\n * leading-slash, drive-lettered path that resolves nowhere.\n */\nconst templatesRoot = (): string =>\n resolve(dirname(fileURLToPath(import.meta.url)), '..', 'templates');\n\n/**\n * npm forbids uppercase and a leading dot or underscore, and a scope is legal.\n * Checked here because the failure would otherwise surface as a confusing\n * `bun install` error inside a directory the user just created.\n */\nconst isValidPackageName = (name: string): boolean =>\n /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name);\n\nconst readPackageVersion = async (): Promise<string> => {\n const file = Bun.file(join(templatesRoot(), '..', 'package.json'));\n const json = (await file.json()) as { version?: string };\n return json.version ?? '0.0.0';\n};\n\nexport const scaffold = async (\n options: ScaffoldOptions,\n): Promise<ScaffoldResult> => {\n const template = options.template ?? 'minimal';\n if (!TEMPLATES.includes(template)) {\n throw new ScaffoldError(\n `Unknown template \"${template}\". Available: ${TEMPLATES.join(', ')}.`,\n );\n }\n\n const directory = resolve(options.cwd ?? process.cwd(), options.target);\n const name = options.name ?? basename(directory);\n\n if (!isValidPackageName(name)) {\n throw new ScaffoldError(\n `\"${name}\" is not a usable package name. Pass --name to choose one.`,\n );\n }\n\n if (\n existsSync(directory) &&\n readdirSync(directory).length > 0 &&\n options.force !== true\n ) {\n throw new ScaffoldError(\n `${directory} is not empty. Pass --force to write into it anyway.`,\n );\n }\n\n const source = join(templatesRoot(), template);\n if (!existsSync(source)) {\n throw new ScaffoldError(\n `Template \"${template}\" is missing from ${source}.`,\n );\n }\n\n const version = options.version ?? `^${await readPackageVersion()}`;\n const written: string[] = [];\n\n // `**/*` with `dot: true` so a template can carry a dotfile that npm did not\n // rename; the explicit `_gitignore` mapping covers the one that it does.\n for await (const relative of new Glob('**/*').scan({\n cwd: source,\n dot: true,\n onlyFiles: true,\n })) {\n const base = relative.split('/').at(-1) ?? relative;\n const renamed = (RENAMED as Record<string, string | undefined>)[base];\n const target =\n renamed === undefined ? relative : join(dirname(relative), renamed);\n\n const contents = await Bun.file(join(source, relative)).text();\n // `Bun.write` creates parent directories, so there is no mkdir pass.\n await Bun.write(\n join(directory, target),\n contents\n .replaceAll(VERSION_PLACEHOLDER, version)\n .replaceAll('__DUNX_APP_NAME__', name),\n );\n written.push(target);\n }\n\n return { directory, name, template, files: written.sort() };\n};\n"
6
+ "import { existsSync, readdirSync } from 'node:fs';\nimport { basename, dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { Glob } from 'bun';\n\n/** The templates that ship with the package, as `templates/<name>/`. */\nexport const TEMPLATES = Object.freeze(['minimal'] as const);\nexport type TemplateName = (typeof TEMPLATES)[number];\n\n/**\n * Every `@dunx/*` version in a template manifest is this placeholder. Versioning\n * is lockstep, so the right version to install is whatever version of\n * `@dunx/create-app` is doing the scaffolding - resolved at run time rather than\n * written into the template, which would go stale on the next release.\n */\nexport const VERSION_PLACEHOLDER = '__DUNX_VERSION__';\n\n/** npm renames a published `.gitignore` to `.npmignore`, so it ships prefixed. */\nconst RENAMED = Object.freeze({ _gitignore: '.gitignore' });\n\nexport interface ScaffoldOptions {\n /** Directory to create. Relative paths resolve against `cwd`. */\n readonly target: string;\n /** Package name for the generated app. Defaults to the target's basename. */\n readonly name?: string;\n readonly template?: TemplateName;\n /** Write into a directory that already has files in it. */\n readonly force?: boolean;\n readonly cwd?: string;\n /** Overrides the version written into the generated manifest. */\n readonly version?: string;\n}\n\nexport interface ScaffoldResult {\n readonly directory: string;\n readonly name: string;\n readonly template: TemplateName;\n readonly files: readonly string[];\n}\n\nexport class ScaffoldError extends Error {\n override readonly name = 'ScaffoldError';\n}\n\n/**\n * `dist/index.js` and `dist/cli.js` both sit one level under the package root, so\n * `../templates` resolves the same from either. In the source tree it resolves\n * from `src/`, which is the same depth - so tests exercise the real path rather\n * than a special case.\n *\n * `fileURLToPath`, not `new URL(...).pathname`: the latter stays percent-encoded,\n * so an install under a directory with a space in it looks for `space%20test/`\n * and reports the template missing. On Windows it is worse - it yields a\n * leading-slash, drive-lettered path that resolves nowhere.\n */\nconst templatesRoot = (): string =>\n resolve(dirname(fileURLToPath(import.meta.url)), '..', 'templates');\n\n/**\n * npm forbids uppercase and a leading dot or underscore, and a scope is legal.\n * Checked here because the failure would otherwise surface as a confusing\n * `bun install` error inside a directory the user just created.\n */\nconst isValidPackageName = (name: string): boolean =>\n /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name);\n\nconst readPackageVersion = async (): Promise<string> => {\n const file = Bun.file(join(templatesRoot(), '..', 'package.json'));\n const json = (await file.json()) as { version?: string };\n return json.version ?? '0.0.0';\n};\n\nexport const scaffold = async (\n options: ScaffoldOptions,\n): Promise<ScaffoldResult> => {\n const template = options.template ?? 'minimal';\n if (!TEMPLATES.includes(template)) {\n throw new ScaffoldError(\n `Unknown template \"${template}\". Available: ${TEMPLATES.join(', ')}.`,\n );\n }\n\n const directory = resolve(options.cwd ?? process.cwd(), options.target);\n const name = options.name ?? basename(directory);\n\n if (!isValidPackageName(name)) {\n throw new ScaffoldError(\n `\"${name}\" is not a usable package name. Pass --name to choose one.`,\n );\n }\n\n if (\n existsSync(directory) &&\n readdirSync(directory).length > 0 &&\n options.force !== true\n ) {\n throw new ScaffoldError(\n `${directory} is not empty. Pass --force to write into it anyway.`,\n );\n }\n\n const source = join(templatesRoot(), template);\n if (!existsSync(source)) {\n throw new ScaffoldError(\n `Template \"${template}\" is missing from ${source}.`,\n );\n }\n\n const version = options.version ?? `^${await readPackageVersion()}`;\n const written: string[] = [];\n\n // `**/*` with `dot: true` so a template can carry a dotfile that npm did not\n // rename; the explicit `_gitignore` mapping covers the one that it does.\n for await (const relative of new Glob('**/*').scan({\n cwd: source,\n dot: true,\n onlyFiles: true,\n })) {\n const base = relative.split('/').at(-1) ?? relative;\n const renamed = (RENAMED as Record<string, string | undefined>)[base];\n const target =\n renamed === undefined ? relative : join(dirname(relative), renamed);\n\n const contents = await Bun.file(join(source, relative)).text();\n // `Bun.write` creates parent directories, so there is no mkdir pass.\n await Bun.write(\n join(directory, target),\n contents\n .replaceAll(VERSION_PLACEHOLDER, version)\n .replaceAll('__DUNX_APP_NAME__', name),\n );\n written.push(target);\n }\n\n return { directory, name, template, files: written.sort() };\n};\n"
7
7
  ],
8
8
  "mappings": "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",
9
9
  "debugId": "E45CB9A6B97979B864756E2164756E21",
package/dist/index.js.map CHANGED
@@ -2,7 +2,7 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/scaffold.ts"],
4
4
  "sourcesContent": [
5
- "import { existsSync, readdirSync } from 'node:fs';\nimport { basename, dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { Glob } from 'bun';\n\n/** The templates that ship with the package, as `templates/<name>/`. */\nexport const TEMPLATES = Object.freeze(['minimal'] as const);\nexport type TemplateName = (typeof TEMPLATES)[number];\n\n/**\n * Every `@dunx/*` version in a template manifest is this placeholder. Versioning\n * is lockstep, so the right version to install is whatever version of\n * `@dunx/create-app` is doing the scaffolding resolved at run time rather than\n * written into the template, which would go stale on the next release.\n */\nexport const VERSION_PLACEHOLDER = '__DUNX_VERSION__';\n\n/** npm renames a published `.gitignore` to `.npmignore`, so it ships prefixed. */\nconst RENAMED = Object.freeze({ _gitignore: '.gitignore' });\n\nexport interface ScaffoldOptions {\n /** Directory to create. Relative paths resolve against `cwd`. */\n readonly target: string;\n /** Package name for the generated app. Defaults to the target's basename. */\n readonly name?: string;\n readonly template?: TemplateName;\n /** Write into a directory that already has files in it. */\n readonly force?: boolean;\n readonly cwd?: string;\n /** Overrides the version written into the generated manifest. */\n readonly version?: string;\n}\n\nexport interface ScaffoldResult {\n readonly directory: string;\n readonly name: string;\n readonly template: TemplateName;\n readonly files: readonly string[];\n}\n\nexport class ScaffoldError extends Error {\n override readonly name = 'ScaffoldError';\n}\n\n/**\n * `dist/index.js` and `dist/cli.js` both sit one level under the package root, so\n * `../templates` resolves the same from either. In the source tree it resolves\n * from `src/`, which is the same depth so tests exercise the real path rather\n * than a special case.\n *\n * `fileURLToPath`, not `new URL(...).pathname`: the latter stays percent-encoded,\n * so an install under a directory with a space in it looks for `space%20test/`\n * and reports the template missing. On Windows it is worse it yields a\n * leading-slash, drive-lettered path that resolves nowhere.\n */\nconst templatesRoot = (): string =>\n resolve(dirname(fileURLToPath(import.meta.url)), '..', 'templates');\n\n/**\n * npm forbids uppercase and a leading dot or underscore, and a scope is legal.\n * Checked here because the failure would otherwise surface as a confusing\n * `bun install` error inside a directory the user just created.\n */\nconst isValidPackageName = (name: string): boolean =>\n /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name);\n\nconst readPackageVersion = async (): Promise<string> => {\n const file = Bun.file(join(templatesRoot(), '..', 'package.json'));\n const json = (await file.json()) as { version?: string };\n return json.version ?? '0.0.0';\n};\n\nexport const scaffold = async (\n options: ScaffoldOptions,\n): Promise<ScaffoldResult> => {\n const template = options.template ?? 'minimal';\n if (!TEMPLATES.includes(template)) {\n throw new ScaffoldError(\n `Unknown template \"${template}\". Available: ${TEMPLATES.join(', ')}.`,\n );\n }\n\n const directory = resolve(options.cwd ?? process.cwd(), options.target);\n const name = options.name ?? basename(directory);\n\n if (!isValidPackageName(name)) {\n throw new ScaffoldError(\n `\"${name}\" is not a usable package name. Pass --name to choose one.`,\n );\n }\n\n if (\n existsSync(directory) &&\n readdirSync(directory).length > 0 &&\n options.force !== true\n ) {\n throw new ScaffoldError(\n `${directory} is not empty. Pass --force to write into it anyway.`,\n );\n }\n\n const source = join(templatesRoot(), template);\n if (!existsSync(source)) {\n throw new ScaffoldError(\n `Template \"${template}\" is missing from ${source}.`,\n );\n }\n\n const version = options.version ?? `^${await readPackageVersion()}`;\n const written: string[] = [];\n\n // `**/*` with `dot: true` so a template can carry a dotfile that npm did not\n // rename; the explicit `_gitignore` mapping covers the one that it does.\n for await (const relative of new Glob('**/*').scan({\n cwd: source,\n dot: true,\n onlyFiles: true,\n })) {\n const base = relative.split('/').at(-1) ?? relative;\n const renamed = (RENAMED as Record<string, string | undefined>)[base];\n const target =\n renamed === undefined ? relative : join(dirname(relative), renamed);\n\n const contents = await Bun.file(join(source, relative)).text();\n // `Bun.write` creates parent directories, so there is no mkdir pass.\n await Bun.write(\n join(directory, target),\n contents\n .replaceAll(VERSION_PLACEHOLDER, version)\n .replaceAll('__DUNX_APP_NAME__', name),\n );\n written.push(target);\n }\n\n return { directory, name, template, files: written.sort() };\n};\n"
5
+ "import { existsSync, readdirSync } from 'node:fs';\nimport { basename, dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { Glob } from 'bun';\n\n/** The templates that ship with the package, as `templates/<name>/`. */\nexport const TEMPLATES = Object.freeze(['minimal'] as const);\nexport type TemplateName = (typeof TEMPLATES)[number];\n\n/**\n * Every `@dunx/*` version in a template manifest is this placeholder. Versioning\n * is lockstep, so the right version to install is whatever version of\n * `@dunx/create-app` is doing the scaffolding - resolved at run time rather than\n * written into the template, which would go stale on the next release.\n */\nexport const VERSION_PLACEHOLDER = '__DUNX_VERSION__';\n\n/** npm renames a published `.gitignore` to `.npmignore`, so it ships prefixed. */\nconst RENAMED = Object.freeze({ _gitignore: '.gitignore' });\n\nexport interface ScaffoldOptions {\n /** Directory to create. Relative paths resolve against `cwd`. */\n readonly target: string;\n /** Package name for the generated app. Defaults to the target's basename. */\n readonly name?: string;\n readonly template?: TemplateName;\n /** Write into a directory that already has files in it. */\n readonly force?: boolean;\n readonly cwd?: string;\n /** Overrides the version written into the generated manifest. */\n readonly version?: string;\n}\n\nexport interface ScaffoldResult {\n readonly directory: string;\n readonly name: string;\n readonly template: TemplateName;\n readonly files: readonly string[];\n}\n\nexport class ScaffoldError extends Error {\n override readonly name = 'ScaffoldError';\n}\n\n/**\n * `dist/index.js` and `dist/cli.js` both sit one level under the package root, so\n * `../templates` resolves the same from either. In the source tree it resolves\n * from `src/`, which is the same depth - so tests exercise the real path rather\n * than a special case.\n *\n * `fileURLToPath`, not `new URL(...).pathname`: the latter stays percent-encoded,\n * so an install under a directory with a space in it looks for `space%20test/`\n * and reports the template missing. On Windows it is worse - it yields a\n * leading-slash, drive-lettered path that resolves nowhere.\n */\nconst templatesRoot = (): string =>\n resolve(dirname(fileURLToPath(import.meta.url)), '..', 'templates');\n\n/**\n * npm forbids uppercase and a leading dot or underscore, and a scope is legal.\n * Checked here because the failure would otherwise surface as a confusing\n * `bun install` error inside a directory the user just created.\n */\nconst isValidPackageName = (name: string): boolean =>\n /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name);\n\nconst readPackageVersion = async (): Promise<string> => {\n const file = Bun.file(join(templatesRoot(), '..', 'package.json'));\n const json = (await file.json()) as { version?: string };\n return json.version ?? '0.0.0';\n};\n\nexport const scaffold = async (\n options: ScaffoldOptions,\n): Promise<ScaffoldResult> => {\n const template = options.template ?? 'minimal';\n if (!TEMPLATES.includes(template)) {\n throw new ScaffoldError(\n `Unknown template \"${template}\". Available: ${TEMPLATES.join(', ')}.`,\n );\n }\n\n const directory = resolve(options.cwd ?? process.cwd(), options.target);\n const name = options.name ?? basename(directory);\n\n if (!isValidPackageName(name)) {\n throw new ScaffoldError(\n `\"${name}\" is not a usable package name. Pass --name to choose one.`,\n );\n }\n\n if (\n existsSync(directory) &&\n readdirSync(directory).length > 0 &&\n options.force !== true\n ) {\n throw new ScaffoldError(\n `${directory} is not empty. Pass --force to write into it anyway.`,\n );\n }\n\n const source = join(templatesRoot(), template);\n if (!existsSync(source)) {\n throw new ScaffoldError(\n `Template \"${template}\" is missing from ${source}.`,\n );\n }\n\n const version = options.version ?? `^${await readPackageVersion()}`;\n const written: string[] = [];\n\n // `**/*` with `dot: true` so a template can carry a dotfile that npm did not\n // rename; the explicit `_gitignore` mapping covers the one that it does.\n for await (const relative of new Glob('**/*').scan({\n cwd: source,\n dot: true,\n onlyFiles: true,\n })) {\n const base = relative.split('/').at(-1) ?? relative;\n const renamed = (RENAMED as Record<string, string | undefined>)[base];\n const target =\n renamed === undefined ? relative : join(dirname(relative), renamed);\n\n const contents = await Bun.file(join(source, relative)).text();\n // `Bun.write` creates parent directories, so there is no mkdir pass.\n await Bun.write(\n join(directory, target),\n contents\n .replaceAll(VERSION_PLACEHOLDER, version)\n .replaceAll('__DUNX_APP_NAME__', name),\n );\n written.push(target);\n }\n\n return { directory, name, template, files: written.sort() };\n};\n"
6
6
  ],
7
7
  "mappings": ";;AAAA;AACA;AACA;AACA;AAGO,IAAM,YAAY,OAAO,OAAO,CAAC,SAAS,CAAU;AASpD,IAAM,sBAAsB;AAGnC,IAAM,UAAU,OAAO,OAAO,EAAE,YAAY,aAAa,CAAC;AAAA;AAsBnD,MAAM,sBAAsB,MAAM;AAAA,EACrB,OAAO;AAC3B;AAaA,IAAM,gBAAgB,MACpB,QAAQ,QAAQ,cAAc,YAAY,GAAG,CAAC,GAAG,MAAM,WAAW;AAOpE,IAAM,qBAAqB,CAAC,SAC1B,6DAA6D,KAAK,IAAI;AAExE,IAAM,qBAAqB,YAA6B;AAAA,EACtD,MAAM,OAAO,IAAI,KAAK,KAAK,cAAc,GAAG,MAAM,cAAc,CAAC;AAAA,EACjE,MAAM,OAAQ,MAAM,KAAK,KAAK;AAAA,EAC9B,OAAO,KAAK,WAAW;AAAA;AAGlB,IAAM,WAAW,OACtB,YAC4B;AAAA,EAC5B,MAAM,WAAW,QAAQ,YAAY;AAAA,EACrC,IAAI,CAAC,UAAU,SAAS,QAAQ,GAAG;AAAA,IACjC,MAAM,IAAI,cACR,qBAAqB,yBAAyB,UAAU,KAAK,IAAI,IACnE;AAAA,EACF;AAAA,EAEA,MAAM,YAAY,QAAQ,QAAQ,OAAO,QAAQ,IAAI,GAAG,QAAQ,MAAM;AAAA,EACtE,MAAM,OAAO,QAAQ,QAAQ,SAAS,SAAS;AAAA,EAE/C,IAAI,CAAC,mBAAmB,IAAI,GAAG;AAAA,IAC7B,MAAM,IAAI,cACR,IAAI,gEACN;AAAA,EACF;AAAA,EAEA,IACE,WAAW,SAAS,KACpB,YAAY,SAAS,EAAE,SAAS,KAChC,QAAQ,UAAU,MAClB;AAAA,IACA,MAAM,IAAI,cACR,GAAG,+DACL;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,KAAK,cAAc,GAAG,QAAQ;AAAA,EAC7C,IAAI,CAAC,WAAW,MAAM,GAAG;AAAA,IACvB,MAAM,IAAI,cACR,aAAa,6BAA6B,SAC5C;AAAA,EACF;AAAA,EAEA,MAAM,UAAU,QAAQ,WAAW,IAAI,MAAM,mBAAmB;AAAA,EAChE,MAAM,UAAoB,CAAC;AAAA,EAI3B,iBAAiB,YAAY,IAAI,KAAK,MAAM,EAAE,KAAK;AAAA,IACjD,KAAK;AAAA,IACL,KAAK;AAAA,IACL,WAAW;AAAA,EACb,CAAC,GAAG;AAAA,IACF,MAAM,OAAO,SAAS,MAAM,GAAG,EAAE,GAAG,EAAE,KAAK;AAAA,IAC3C,MAAM,UAAW,QAA+C;AAAA,IAChE,MAAM,SACJ,YAAY,YAAY,WAAW,KAAK,QAAQ,QAAQ,GAAG,OAAO;AAAA,IAEpE,MAAM,WAAW,MAAM,IAAI,KAAK,KAAK,QAAQ,QAAQ,CAAC,EAAE,KAAK;AAAA,IAE7D,MAAM,IAAI,MACR,KAAK,WAAW,MAAM,GACtB,SACG,WAAW,qBAAqB,OAAO,EACvC,WAAW,qBAAqB,IAAI,CACzC;AAAA,IACA,QAAQ,KAAK,MAAM;AAAA,EACrB;AAAA,EAEA,OAAO,EAAE,WAAW,MAAM,UAAU,OAAO,QAAQ,KAAK,EAAE;AAAA;",
8
8
  "debugId": "87D57428F30FA51264756E2164756E21",
@@ -4,7 +4,7 @@ export type TemplateName = (typeof TEMPLATES)[number];
4
4
  /**
5
5
  * Every `@dunx/*` version in a template manifest is this placeholder. Versioning
6
6
  * is lockstep, so the right version to install is whatever version of
7
- * `@dunx/create-app` is doing the scaffolding resolved at run time rather than
7
+ * `@dunx/create-app` is doing the scaffolding - resolved at run time rather than
8
8
  * written into the template, which would go stale on the next release.
9
9
  */
10
10
  export declare const VERSION_PLACEHOLDER = "__DUNX_VERSION__";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dunx/create-app",
3
- "version": "0.1.0",
4
- "description": "Scaffold a new dunx application bunx @dunx/create-app my-api",
3
+ "version": "0.2.0",
4
+ "description": "Scaffold a new dunx application - bunx @dunx/create-app my-api",
5
5
  "keywords": [
6
6
  "bun",
7
7
  "create",
@@ -10,7 +10,7 @@
10
10
  "starter"
11
11
  ],
12
12
  "homepage": "https://github.com/petarzarkov/dunx/tree/main/packages/create-app#readme",
13
- "license": "MIT",
13
+ "license": "Apache-2.0",
14
14
  "author": {
15
15
  "name": "Petar Zarkov",
16
16
  "email": "pzarko1@gmail.com",
@@ -13,7 +13,7 @@ bun test
13
13
  | File | Why |
14
14
  | --------------------------- | --------------------------------------------------------------- |
15
15
  | `src/main.ts` | Builds the container, discovers routes, starts `Bun.serve` |
16
- | `src/app.module.ts` | The root module `controllers` get routes, `providers` do not |
16
+ | `src/app.module.ts` | The root module - `controllers` get routes, `providers` do not |
17
17
  | `src/greetings.service.ts` | A provider, injected by constructor type with no annotation |
18
18
  | `src/greetings.controller.ts` | Routes, returning plain objects |
19
19
  | `src/app.test.ts` | The whole app behind a real server on port 0 |
@@ -27,7 +27,7 @@ preload = ["@dunx/transform/preload"]
27
27
 
28
28
  `@dunx/transform` reads each class's constructor parameter types when the file
29
29
  loads and records them, so the container can resolve them before calling `new`.
30
- Without it, providers are constructed with no arguments and boot fails saying so
30
+ Without it, providers are constructed with no arguments and boot fails saying so -
31
31
  it is not a silent `undefined`.
32
32
 
33
33
  That is also why there is no `@Injectable()` and no `@Inject()`. Being listed in a
@@ -37,9 +37,9 @@ have no parameter decorators, so `@Inject()` does not exist.
37
37
  ## Next
38
38
 
39
39
  - Add validation: give a route a schema and the body arrives typed and coerced.
40
- Any Standard Schema validator works zod, Valibot, ArkType.
40
+ Any Standard Schema validator works - zod, Valibot, ArkType.
41
41
  - Add a database: `@dunx/infra/db` is drizzle over `bun:sqlite` and `Bun.SQL`.
42
42
  - Serve an OpenAPI document and a page that can call your routes: `@dunx/openapi`.
43
43
 
44
44
  The [examples](https://github.com/petarzarkov/dunx/tree/main/examples) go in that
45
- order `minimal`, then `databases` and `testing`, then `full`.
45
+ order - `minimal`, then `databases` and `testing`, then `full`.
@@ -5,7 +5,7 @@ import { GreetingsService } from './greetings.service.js';
5
5
  /**
6
6
  * The root module. `controllers` are discovered for routes, `providers` are
7
7
  * everything else. Import order is construction order, and shutdown runs in
8
- * reverse which matters once there is a database to close.
8
+ * reverse - which matters once there is a database to close.
9
9
  */
10
10
  @Module({
11
11
  controllers: [GreetingsController],
@@ -4,7 +4,7 @@ import { AppModule } from './app.module.js';
4
4
 
5
5
  /**
6
6
  * The whole app behind a real `Bun.serve` on port 0. This is also what CI runs to
7
- * prove the example still boots see `examples/testing` for overrides and the
7
+ * prove the example still boots - see `examples/testing` for overrides and the
8
8
  * rest of `@dunx/testing`.
9
9
  */
10
10
  describe('minimal', () => {
@@ -5,7 +5,7 @@ import { GreetingsService } from './greetings.service.js';
5
5
  * A controller is a provider with routes on it. `GreetingsService` in the
6
6
  * constructor is resolved the same way the service's own `Logger` was.
7
7
  *
8
- * Returning a plain object is enough `@dunx/http` serialises it. There is no
8
+ * Returning a plain object is enough - `@dunx/http` serialises it. There is no
9
9
  * `Response.json()` to remember and no `res` to forget to send.
10
10
  */
11
11
  @Controller('greetings')
@@ -19,7 +19,7 @@ export class GreetingsController {
19
19
 
20
20
  /**
21
21
  * No schemas are declared, so a path param stays on `input.req.params` as a
22
- * string. Declaring a `params` schema is what makes it typed and coerced
22
+ * string. Declaring a `params` schema is what makes it typed and coerced -
23
23
  * `examples/full` does that; this one is showing the shape, not validation.
24
24
  */
25
25
  @Get('/:name')
@@ -1,7 +1,7 @@
1
1
  import { Logger, type OnInit } from '@dunx/core';
2
2
 
3
3
  /**
4
- * A provider. No decorator, no registration boilerplate being listed in a
4
+ * A provider. No decorator, no registration boilerplate - being listed in a
5
5
  * module's `providers` is what makes it injectable.
6
6
  *
7
7
  * `Logger` in the constructor is the whole dependency injection story: the