diff --git a/deno.json b/deno.json index cb791b3..167590b 100644 --- a/deno.json +++ b/deno.json @@ -6,12 +6,13 @@ "tasks": { "upgrade-deps": "deno cache --frozen-lockfile=false 'packages/*/src/index.ts' test example npm:@types/node", "check": "deno fmt --check && deno lint && deno check 'packages/**/*.ts' && deno doc --lint 'packages/**/*.ts'", - "test": "deno test --allow-net --no-check ./test", - "test:ci": "deno test --doc --allow-net --frozen-lockfile ./test './**/*.md'", + "test": "deno test --allow-net --allow-read --allow-write=tmp --allow-run=deno --no-check ./test", + "test:ci": "deno test --doc --allow-net --allow-read --allow-write=tmp --allow-run=deno --frozen-lockfile ./test './**/*.md'", "bump-package": "deno run --allow-read=. --allow-write=. --allow-run=deno,git-cliff tools/bump-package.ts" }, "workspace": { "members": [ + "./packages/create-nestjs", "./packages/nestjs-denokv", "./packages/nestjs-platform-hono", "./packages/nestjs-platform-oak" diff --git a/deno.lock b/deno.lock index 188cc4c..9b21dba 100644 --- a/deno.lock +++ b/deno.lock @@ -224,6 +224,13 @@ "jsr:@oak/oak@^17", "npm:@nestjs/common@^10", "npm:@nestjs/core@^10" - ] + ], + "members": { + "packages/create-nestjs": { + "dependencies": [ + "jsr:@david/dax@0.42.0" + ] + } + } } } diff --git a/packages/create-nestjs/cli.ts b/packages/create-nestjs/cli.ts new file mode 100644 index 0000000..bbcbcc0 --- /dev/null +++ b/packages/create-nestjs/cli.ts @@ -0,0 +1,137 @@ +import type { $Type } from "@david/dax"; +import { $ } from "@david/dax"; +import { parseArgs } from "node:util"; + +import denoJson from "./deno.json" with { type: "json" }; +import denoJsonTemplate from "./deno.template.json" with { type: "json" }; +import { join } from "jsr:@std/path@^1.0/join"; + +type Framework = "oak" | "hono"; +const supportedFrameworks: ReadonlyArray = ["oak", "hono"]; +const oakAdapterDeps = { + "@uki00a/nestjs-platform-hono": "jsr:@uki00a/nestjs-platform-hono", + "@oak/oak": "jsr:@oak/oak@^17", +}; +const honoAdapterDeps = { + "@uki00a/nestjs-platform-hono": "jsr:@uki00a/nestjs-platform-hono", + "@hono/hono": "jsr:@hono/hono@^4", +}; + +function isSupportedFramework( + maybeFramework: string, +): maybeFramework is Framework { + return supportedFrameworks.includes(maybeFramework as Framework); +} + +interface CreateOptions { + framework: Framework; + targetDir: string; +} +async function create( + options: CreateOptions, +) { + const additionalPackages = determineAdditionalPackages(options.framework); + const denoJson = { + ...denoJsonTemplate, + imports: { + ...denoJsonTemplate.imports, + ...additionalPackages, + }, + }; + await Deno.writeTextFile( + join(options.targetDir, "deno.json"), + JSON.stringify(denoJson, null, 2), + ); +} + +function determineAdditionalPackages( + framework: Framework, +): Record { + switch (framework) { + case "hono": + return honoAdapterDeps; + case "oak": + return oakAdapterDeps; + } +} + +interface MainOptions { + framework?: string; + targetDir?: string; +} +async function main(options: MainOptions) { + const targetDir = options.targetDir || + await $.prompt("What directory do you want to create a new project in?", { + default: "nestjs-project", + }); + const framework = await selectFramework($, options); + await $.path(targetDir).mkdir({ recursive: true }); + await create({ + ...options, + framework, + targetDir, + }); +} +async function selectFramework( + $: $Type, + options: MainOptions, +): Promise { + if (options.framework) { + if (!isSupportedFramework(options.framework)) { + throw new Error( + `--framework should be one of ${ + supportedFrameworks.map((x) => `"${x}"`).join(", ") + }`, + ); + } + return options.framework; + } + const indexOfFramework = await $.select({ + message: "Which framework will you use?", + options: [...supportedFrameworks], + }); + const framework = supportedFrameworks[indexOfFramework]; + return framework; +} + +if (import.meta.main) { + const parseArgsOptions = { + allowPositionals: true, + options: { + "framework": { + description: `A framework you want to use [${ + supportedFrameworks.join(", ") + }]`, + type: "string", + }, + "help": { + description: "Show this help text", + type: "boolean", + short: "h", + }, + }, + } as const; + const args = parseArgs(parseArgsOptions); + if (args.values.help) { + $.log(`${denoJson.name} - Creates a new NestJS project for Deno. + +Usage: + deno run jsr:${denoJson.name} [TARGET_DIRECTORY] + +Options: +${ + Object.entries(parseArgsOptions.options).map(( + [option, { description }], + ) => ` ${option} - ${description}`).join("\n") + }`); + Deno.exit(0); + } + + main({ + framework: args.values.framework, + targetDir: args.positionals[0], + }).catch((error) => { + $.logError("Failed", error); + Deno.exit(1); + }); +} diff --git a/packages/create-nestjs/deno.json b/packages/create-nestjs/deno.json new file mode 100644 index 0000000..73c5eed --- /dev/null +++ b/packages/create-nestjs/deno.json @@ -0,0 +1,10 @@ +{ + "name": "@uki00a/create-nestjs", + "version": "0.1.0", + "exports": { + ".": "./cli.ts" + }, + "imports": { + "@david/dax": "jsr:@david/dax@0.42.0" + } +} diff --git a/packages/create-nestjs/deno.template.json b/packages/create-nestjs/deno.template.json new file mode 100644 index 0000000..022d155 --- /dev/null +++ b/packages/create-nestjs/deno.template.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "imports": { + "@nestjs/common": "npm:@nestjs/common@^10", + "@nestjs/core": "npm:@nestjs/core@^10" + }, + "lint": { + "rules": { + "include": [ + "no-console" + ] + } + } +} diff --git a/test/create-nestjs/cli.test.ts b/test/create-nestjs/cli.test.ts new file mode 100644 index 0000000..bf36ad0 --- /dev/null +++ b/test/create-nestjs/cli.test.ts @@ -0,0 +1,76 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import assert from "node:assert/strict"; +import rootDenoJson from "../../deno.json" with { type: "json" }; + +async function withTempDir( + prefix: string, + thunk: (tmpdir: string) => Promise, +): Promise { + const tempDir = await mkdtemp(prefix); + try { + await thunk(tempDir); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } +} + +const rootDir = dirname(dirname(dirname(new URL(import.meta.url).pathname))); +const tmpDir = join(rootDir, "tmp"); + +Deno.test({ + name: "create-nestjs", + fn: async (t) => { + async function createNestJS( + targetDir: string, + framework: "hono" | "oak", + ) { + const result = await new Deno.Command(Deno.execPath(), { + args: [ + "run", + "--no-lock", + `--allow-read=${targetDir}`, + "--allow-write", + "--allow-run=deno", + join(rootDir, "packages/create-nestjs/cli.ts"), + "--framework", + framework, + targetDir, + ], + }).output(); + if (!result.success) { + throw new Error(new TextDecoder().decode(result.stderr)); + } + } + const tempDirPrefix = join(tmpDir, "/create-nestjs-"); + await t.step("hono", () => + withTempDir(tempDirPrefix, async (tempDir) => { + await createNestJS(tempDir, "hono"); + const denoJson = JSON.parse( + await Deno.readTextFile(join(tempDir, "deno.json")), + ); + assert.ok(denoJson.imports); + assert.equal( + denoJson.imports["@hono/hono"], + rootDenoJson.imports["@hono/hono"], + ); + assert.equal( + denoJson.imports["@uki00a/nestjs-platform-hono"], + "jsr:@uki00a/nestjs-platform-hono", + ); + assert.equal( + denoJson.imports["@nestjs/common"], + rootDenoJson.imports["@nestjs/common"], + ); + assert.equal( + denoJson.imports["@nestjs/core"], + rootDenoJson.imports["@nestjs/core"], + ); + })); + }, + permissions: { + read: true, + write: [tmpDir], + run: [Deno.execPath()], + }, +}); diff --git a/tools/bump-package.ts b/tools/bump-package.ts index 62bf687..ac02ebb 100644 --- a/tools/bump-package.ts +++ b/tools/bump-package.ts @@ -7,6 +7,7 @@ async function main() { const tmpDir = join(rootDir, "tmp"); const cliffConfigsDir = join(tmpDir, "cliff"); const allowedPackages = [ + "create-nestjs", "nestjs-denokv", "nestjs-platform-hono", "nestjs-platform-oak",