From 635e9de1704e9d4d382773264b5d8f4001f3d41a Mon Sep 17 00:00:00 2001 From: Bert De Block Date: Tue, 26 Nov 2024 09:16:55 +0100 Subject: [PATCH 1/2] Internal improvements --- README.md | 20 +++++-- bin/gember.js | 1 - eslint.config.js | 8 ++- package.json | 6 +- pnpm-lock.yaml | 6 +- src/cli.ts | 60 +++++++++---------- src/config.ts | 5 +- src/errors.ts | 19 ++++++ src/{generate-document.ts => generate.ts} | 60 ++++++++++--------- src/generators.ts | 42 ++++++------- test/config.test.ts | 13 ++-- test/generate-component.test.ts | 58 +++++++++--------- test/generate-document.test.ts | 34 ++++++----- test/generate-helper.test.ts | 52 ++++++++-------- test/generate-modifier.test.ts | 58 +++++++++--------- test/generate-service.test.ts | 42 ++++++------- test/helpers.ts | 41 +++++++++---- .../v1-addon/addon/.gitkeep | 0 .../v1-addon/package.json | 0 .../v1-app/app/.gitkeep | 0 .../v1-app/package.json | 0 .../v2-addon-hooks/gember.config.js | 6 +- .../v2-addon-hooks/package.json | 0 .../v2-addon/package.json | 0 24 files changed, 288 insertions(+), 243 deletions(-) create mode 100644 src/errors.ts rename src/{generate-document.ts => generate.ts} (62%) rename test/{blueprints => packages}/v1-addon/addon/.gitkeep (100%) rename test/{blueprints => packages}/v1-addon/package.json (100%) rename test/{blueprints => packages}/v1-app/app/.gitkeep (100%) rename test/{blueprints => packages}/v1-app/package.json (100%) rename test/{blueprints => packages}/v2-addon-hooks/gember.config.js (83%) rename test/{blueprints => packages}/v2-addon-hooks/package.json (100%) rename test/{blueprints => packages}/v2-addon/package.json (100%) diff --git a/README.md b/README.md index d7506e1..4467098 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,11 @@ [![CI](https://github.com/bertdeblock/gember/workflows/CI/badge.svg)](https://github.com/bertdeblock/gember/actions?query=workflow%3ACI) [![NPM Version](https://badge.fury.io/js/%40bertdeblock%2Fgember.svg)](https://badge.fury.io/js/%40bertdeblock%2Fgember) -Generate components, helpers, modifiers and services in v2 addons. +Generate components, helpers, modifiers and services in v1/v2 apps/addons. Uses [scaffdog](https://scaff.dog/) underneath. -> [!NOTE] -> -> - Only supports `.gjs` (default) and `.gts` files for components +> NOTE: Only supports `.gjs` (default) and `.gts` files for components. ## Installation @@ -55,6 +53,9 @@ yarn add -D @bertdeblock/gember Generating components ```shell +pnpm gember component --help # for all available options + +# examples: pnpm gember component foo pnpm gember component foo --class-based # or `--class` pnpm gember component foo --path="src/-private" @@ -67,6 +68,9 @@ pnpm gember component foo --typescript # or `--ts` Generating helpers ```shell +pnpm gember helper --help # for all available options + +# examples: pnpm gember helper foo pnpm gember helper foo --class-based # or `--class` pnpm gember helper foo --path="src/-private" @@ -79,6 +83,9 @@ pnpm gember helper foo --typescript # or `--ts` Generating modifiers ```shell +pnpm gember modifier --help # for all available options + +# examples: pnpm gember modifier foo pnpm gember modifier foo --class-based # or `--class` pnpm gember modifier foo --path="src/-private" @@ -91,6 +98,9 @@ pnpm gember modifier foo --typescript # or `--ts` Generating services ```shell +pnpm gember service --help # for all available options + +# examples: pnpm gember service foo pnpm gember service foo --path="src/-private" pnpm gember service foo --typescript # or `--ts` @@ -112,7 +122,7 @@ A gember config file must export a gember config object, or a sync/async functio #### `hooks.postGenerate` -A hook that will be executed post generating a document. +A hook that will be executed post running a generator. ```js // gember.config.js diff --git a/bin/gember.js b/bin/gember.js index a44a5a0..874f626 100755 --- a/bin/gember.js +++ b/bin/gember.js @@ -1,4 +1,3 @@ #!/usr/bin/env node -// eslint-disable-next-line n/no-missing-import import "../dist/cli.js"; diff --git a/eslint.config.js b/eslint.config.js index 686eb2b..05a0a33 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -6,5 +6,11 @@ export default typescriptEslint.config( eslint.configs.recommended, typescriptEslint.configs.recommended, eslintPluginNode.configs["flat/recommended-module"], - { ignores: ["coverage", "dist", "test/output"] }, + { ignores: ["bin", "coverage", "dist", "test/output"] }, + { + files: ["**/*.ts"], + rules: { + "@typescript-eslint/explicit-function-return-type": "error", + }, + }, ); diff --git a/package.json b/package.json index 03ee297..750f176 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@bertdeblock/gember", "version": "0.4.0", - "description": "Generate components, helpers, modifiers and services in v2 addons.", + "description": "Generate components, helpers, modifiers and services in v1/v2 apps/addons.", "repository": "https://github.com/bertdeblock/gember", "license": "MIT", "author": "Bert De Block", @@ -16,7 +16,7 @@ "CHANGELOG.md" ], "scripts": { - "build": "tsc --project tsconfig.json", + "build": "rm -rf dist && tsc --project tsconfig.json", "lint": "concurrently --group --prefix-colors auto \"npm:lint:*(!fix)\"", "lint:fix": "concurrently --group --prefix-colors auto \"npm:lint:*:fix\"", "lint:format": "prettier . --cache --check", @@ -29,8 +29,8 @@ "test:coverage": "vitest run --coverage" }, "dependencies": { - "chalk": "^5.3.0", "change-case": "^5.4.4", + "consola": "^3.2.3", "find-up": "^7.0.0", "fs-extra": "^11.2.0", "scaffdog": "^4.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 65a9c5d..caca570 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,12 @@ importers: .: dependencies: - chalk: - specifier: ^5.3.0 - version: 5.3.0 change-case: specifier: ^5.4.4 version: 5.4.4 + consola: + specifier: ^3.2.3 + version: 3.2.3 find-up: specifier: ^7.0.0 version: 7.0.0 diff --git a/src/cli.ts b/src/cli.ts index 44196e8..848975d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,5 +1,7 @@ +import { cwd } from "node:process"; import { hideBin } from "yargs/helpers"; import yargs from "yargs/yargs"; +import { logGemberErrors } from "./errors.js"; import { generateComponent, generateHelper, @@ -21,28 +23,27 @@ yargs(hideBin(process.argv)) }) .option("class-based", { alias: ["class"], - default: false, description: "Generate a class-based component", type: "boolean", }) .option("path", { - default: "", description: "Generate a component at a custom path", type: "string", }) .option("typescript", { alias: ["ts"], - default: false, description: "Generate a `.gts` component", type: "boolean", }); }, handler(options) { - generateComponent(options.name, { - classBased: options.classBased, - path: options.path, - typescript: options.typescript, - }); + logGemberErrors(() => + generateComponent(options.name, cwd(), { + classBased: options.classBased, + path: options.path, + typescript: options.typescript, + }), + ); }, }) .command({ @@ -58,28 +59,27 @@ yargs(hideBin(process.argv)) }) .option("class-based", { alias: ["class"], - default: false, description: "Generate a class-based helper", type: "boolean", }) .option("path", { - default: "", description: "Generate a helper at a custom path", type: "string", }) .option("typescript", { alias: ["ts"], - default: false, description: "Generate a `.ts` helper", type: "boolean", }); }, handler(options) { - generateHelper(options.name, { - classBased: options.classBased, - path: options.path, - typescript: options.typescript, - }); + logGemberErrors(() => + generateHelper(options.name, cwd(), { + classBased: options.classBased, + path: options.path, + typescript: options.typescript, + }), + ); }, }) .command({ @@ -95,28 +95,27 @@ yargs(hideBin(process.argv)) }) .option("class-based", { alias: ["class"], - default: false, description: "Generate a class-based modifier", type: "boolean", }) .option("path", { - default: "", description: "Generate a modifier at a custom path", type: "string", }) .option("typescript", { alias: ["ts"], - default: false, description: "Generate a `.ts` modifier", type: "boolean", }); }, handler(options) { - generateModifier(options.name, { - classBased: options.classBased, - path: options.path, - typescript: options.typescript, - }); + logGemberErrors(() => + generateModifier(options.name, cwd(), { + classBased: options.classBased, + path: options.path, + typescript: options.typescript, + }), + ); }, }) .command({ @@ -131,24 +130,25 @@ yargs(hideBin(process.argv)) type: "string", }) .option("path", { - default: "", description: "Generate a service at a custom path", type: "string", }) .option("typescript", { alias: ["ts"], - default: false, description: "Generate a `.ts` service", type: "boolean", }); }, handler(options) { - generateService(options.name, { - path: options.path, - typescript: options.typescript, - }); + logGemberErrors(() => + generateService(options.name, cwd(), { + path: options.path, + typescript: options.typescript, + }), + ); }, }) .demandCommand() + .epilogue("🫚 More info at https://github.com/bertdeblock/gember#usage") .strict() .parse(); diff --git a/src/config.ts b/src/config.ts index 1c1de6d..7929590 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,6 @@ import { findUp } from "find-up"; import { pathToFileURL } from "node:url"; +import { GemberError } from "./errors.js"; import { DocumentName, type File } from "./types.js"; export type Config = { @@ -32,13 +33,13 @@ export async function getConfig(cwd: string): Promise { try { config = (await import(pathToFileURL(path).toString())).default; } catch (cause) { - throw new Error(`Could not import gember config file at "${path}".`, { + throw new GemberError(`Could not import gember config file at "${path}".`, { cause, }); } if (config === undefined) { - throw new Error( + throw new GemberError( `gember config file at "${path}" must have a "default" export.`, ); } diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..4e1e54f --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,19 @@ +import { consola } from "consola"; +import process from "node:process"; + +export class GemberError extends Error {} + +export async function logGemberErrors( + func: () => Promise, +): Promise { + try { + await func(); + } catch (error) { + if (error instanceof GemberError) { + consola.error(error); + process.exitCode = 1; + } else { + throw error; + } + } +} diff --git a/src/generate-document.ts b/src/generate.ts similarity index 62% rename from src/generate-document.ts rename to src/generate.ts index c7e3af5..787ef24 100644 --- a/src/generate-document.ts +++ b/src/generate.ts @@ -1,39 +1,41 @@ -import chalk from "chalk"; import { camelCase, kebabCase, pascalCase } from "change-case"; +import { consola } from "consola"; import { ensureDir, readJson } from "fs-extra/esm"; import { writeFile } from "node:fs/promises"; import { dirname, isAbsolute, join, parse, relative } from "node:path"; -import { cwd as processCwd } from "node:process"; +import { cwd } from "node:process"; import { fileURLToPath } from "node:url"; import { type GenerateInputs, loadScaffdog } from "scaffdog"; import { getConfig } from "./config.js"; +import { GemberError } from "./errors.js"; import { isAddon, isV2Addon } from "./helpers.js"; import { type DocumentName } from "./types.js"; -export async function generateDocument( +export async function generate( documentName: DocumentName, entityName: string, + packagePath: string, { - cwd = processCwd(), - inputs = {}, - path = "", + inputs, + path, }: { - cwd?: string; inputs?: GenerateInputs; path?: string; - } = {}, -) { - const directory = dirname(fileURLToPath(import.meta.url)); - const scaffdog = await loadScaffdog(join(directory, "../documents")); + }, +): Promise { + const scaffdog = await loadScaffdog( + join(dirname(fileURLToPath(import.meta.url)), "../documents"), + ); + const documents = await scaffdog.list(); const document = documents.find((document) => document.name === documentName); if (document === undefined) { - throw new Error(`[BUG] Document \`${documentName}\` not found.`); + throw new GemberError(`[BUG] Document \`${documentName}\` not found.`); } - const documentPath = await getDocumentPath(documentName, cwd, path); - const files = await scaffdog.generate(document, documentPath, { + const generatePath = await getGeneratePath(documentName, packagePath, path); + const files = await scaffdog.generate(document, generatePath, { inputs: { ...inputs, name: { @@ -56,14 +58,12 @@ export async function generateDocument( await ensureDir(parse(file.path).dir); await writeFile(file.path, file.content); - console.log( - chalk.green( - `🫚 Generated ${documentName} \`${entityName}\` at \`${relative(cwd, file.path)}\`.`, - ), + consola.success( + `🫚 Generated ${documentName} \`${entityName}\` at \`${relative(cwd(), file.path)}\`.`, ); } - const config = await getConfig(cwd); + const config = await getConfig(packagePath); await config.hooks?.postGenerate?.({ documentName, @@ -83,25 +83,31 @@ const DOCUMENT_DIRECTORY: Record = { service: "services", }; -export async function getDocumentPath( +const SRC_DIRECTORY: Record = { + APP: "app", + V1_ADDON: "addon", + V2_ADDON: "src", +}; + +export async function getGeneratePath( documentName: DocumentName, - cwd: string, + packagePath: string, path?: string, ): Promise { if (path) { if (isAbsolute(path)) { return path; } else { - return join(cwd, path); + return join(packagePath, path); } } - const packageJson = await readJson(join(cwd, "package.json")); + const packageJson = await readJson(join(packagePath, "package.json")); const srcDirectory = isAddon(packageJson) ? isV2Addon(packageJson) - ? "src" // v2 addon - : "addon" // v1 addon - : "app"; // v1 app + ? SRC_DIRECTORY.V2_ADDON + : SRC_DIRECTORY.V1_ADDON + : SRC_DIRECTORY.APP; - return join(cwd, srcDirectory, DOCUMENT_DIRECTORY[documentName]); + return join(packagePath, srcDirectory, DOCUMENT_DIRECTORY[documentName]); } diff --git a/src/generators.ts b/src/generators.ts index 5accb52..e03c29b 100644 --- a/src/generators.ts +++ b/src/generators.ts @@ -1,21 +1,19 @@ -import { generateDocument } from "./generate-document.js"; +import { generate } from "./generate.js"; export function generateComponent( name: string, + packagePath: string, { classBased = false, - cwd = "", - path = "", + path, typescript = false, }: { classBased?: boolean; - cwd?: string; path?: string; typescript?: boolean; } = {}, -) { - return generateDocument("component", name, { - cwd, +): Promise { + return generate("component", name, packagePath, { inputs: { classBased, typescript }, path, }); @@ -23,20 +21,18 @@ export function generateComponent( export function generateHelper( name: string, + packagePath: string, { classBased = false, - cwd = "", - path = "", + path, typescript = false, }: { classBased?: boolean; - cwd?: string; path?: string; typescript?: boolean; } = {}, -) { - return generateDocument("helper", name, { - cwd, +): Promise { + return generate("helper", name, packagePath, { inputs: { classBased, typescript }, path, }); @@ -44,20 +40,18 @@ export function generateHelper( export function generateModifier( name: string, + packagePath: string, { classBased = false, - cwd = "", - path = "", + path, typescript = false, }: { classBased?: boolean; - cwd?: string; path?: string; typescript?: boolean; } = {}, -) { - return generateDocument("modifier", name, { - cwd, +): Promise { + return generate("modifier", name, packagePath, { inputs: { classBased, typescript }, path, }); @@ -65,18 +59,16 @@ export function generateModifier( export function generateService( name: string, + packagePath: string, { - cwd = "", - path = "", + path, typescript = false, }: { - cwd?: string; path?: string; typescript?: boolean; } = {}, -) { - return generateDocument("service", name, { - cwd, +): Promise { + return generate("service", name, packagePath, { inputs: { typescript }, path, }); diff --git a/test/config.test.ts b/test/config.test.ts index 94b5cda..b043f03 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -1,11 +1,8 @@ import { Project } from "fixturify-project"; -import { remove } from "fs-extra"; -import { readFile } from "node:fs/promises"; -import { join } from "node:path"; import { it } from "vitest"; import { getConfig } from "../src/config.js"; import { generateComponent } from "../src/generators.ts"; -import { copyBlueprint } from "./helpers.js"; +import { Package } from "./helpers.js"; it("supports a `gember.config.js` file", async (ctx) => { const project = new Project({ @@ -53,13 +50,13 @@ it("supports a `gember.config.mjs` file", async (ctx) => { }); it("runs the `postGenerate` hook", async (ctx) => { - const cwd = await copyBlueprint("v2-addon-hooks", "post-generate-info"); + const pkg = await Package.create("v2-addon-hooks", "post-generate-info"); - await generateComponent("foo", { cwd }); + await generateComponent("foo", pkg.path); - const content = await readFile(join(cwd, "post-generate-info.json"), "utf-8"); + const content = await pkg.readFile("post-generate-info.json"); ctx.expect(content).toMatchSnapshot(); - await remove(cwd); + await pkg.cleanUp(); }); diff --git a/test/generate-component.test.ts b/test/generate-component.test.ts index 2d0c9c6..c228baf 100644 --- a/test/generate-component.test.ts +++ b/test/generate-component.test.ts @@ -1,87 +1,83 @@ -import { remove } from "fs-extra"; -import { readFile } from "node:fs/promises"; -import { join } from "node:path"; import { afterEach, it } from "vitest"; import { generateComponent } from "../src/generators.ts"; -import { copyBlueprint } from "./helpers.ts"; +import { Package } from "./helpers.ts"; -let cwd: string; +let pkg: Package; -afterEach(() => remove(cwd)); +afterEach(() => pkg.cleanUp()); it("generates a template-only `.gjs` component", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateComponent("foo", { cwd }); + await generateComponent("foo", pkg.path); - const content = await readFile(join(cwd, "src/components/foo.gjs"), "utf-8"); + const content = await pkg.readFile("src/components/foo.gjs"); ctx.expect(content).toMatchSnapshot(); }); it("generates a class-based `.gjs` component", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateComponent("foo", { classBased: true, cwd }); + await generateComponent("foo", pkg.path, { classBased: true }); - const content = await readFile(join(cwd, "src/components/foo.gjs"), "utf-8"); + const content = await pkg.readFile("src/components/foo.gjs"); ctx.expect(content).toMatchSnapshot(); }); it("generates a template-only `.gjs` component at a custom path", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateComponent("foo", { cwd, path: "src/-private" }); + await generateComponent("foo", pkg.path, { path: "src/-private" }); - const content = await readFile(join(cwd, "src/-private/foo.gjs"), "utf-8"); + const content = await pkg.readFile("src/-private/foo.gjs"); ctx.expect(content).toMatchSnapshot(); }); it("generates a template-only `.gts` component", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateComponent("foo", { cwd, typescript: true }); + await generateComponent("foo", pkg.path, { typescript: true }); - const content = await readFile(join(cwd, "src/components/foo.gts"), "utf-8"); + const content = await pkg.readFile("src/components/foo.gts"); ctx.expect(content).toMatchSnapshot(); }); it("generates a class-based `.gts` component", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateComponent("foo", { classBased: true, cwd, typescript: true }); + await generateComponent("foo", pkg.path, { + classBased: true, + typescript: true, + }); - const content = await readFile(join(cwd, "src/components/foo.gts"), "utf-8"); + const content = await pkg.readFile("src/components/foo.gts"); ctx.expect(content).toMatchSnapshot(); }); it("generates a template-only `.gts` component at a custom path", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateComponent("foo", { - cwd, + await generateComponent("foo", pkg.path, { path: "src/-private", typescript: true, }); - const content = await readFile(join(cwd, "src/-private/foo.gts"), "utf-8"); + const content = await pkg.readFile("src/-private/foo.gts"); ctx.expect(content).toMatchSnapshot(); }); it("generates a nested template-only `.gjs` component", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateComponent("foo/bar", { cwd }); + await generateComponent("foo/bar", pkg.path); - const content = await readFile( - join(cwd, "src/components/foo/bar.gjs"), - "utf-8", - ); + const content = await pkg.readFile("src/components/foo/bar.gjs"); ctx.expect(content).toMatchSnapshot(); }); diff --git a/test/generate-document.test.ts b/test/generate-document.test.ts index ca93d92..ebda2f8 100644 --- a/test/generate-document.test.ts +++ b/test/generate-document.test.ts @@ -1,34 +1,40 @@ import { join } from "node:path"; import { it } from "vitest"; -import { getDocumentPath } from "../src/generate-document.ts"; -import { blueprintPath } from "./helpers.ts"; +import { getGeneratePath } from "../src/generate.ts"; +import { Package } from "./helpers.ts"; it("supports v1 apps", async (ctx) => { const name = "v1-app"; - const cwd = blueprintPath(name); - const documentPath = await getDocumentPath("component", cwd); + const generatePath = await getGeneratePath( + "component", + Package.createPath(name), + ); ctx - .expect(documentPath) - .toEqual(join("test/blueprints", name, "app/components")); + .expect(generatePath) + .toEqual(join("test/packages", name, "app/components")); }); it("supports v1 addons", async (ctx) => { const name = "v1-addon"; - const cwd = blueprintPath(name); - const documentPath = await getDocumentPath("component", cwd); + const generatePath = await getGeneratePath( + "component", + Package.createPath(name), + ); ctx - .expect(documentPath) - .toEqual(join("test/blueprints", name, "addon/components")); + .expect(generatePath) + .toEqual(join("test/packages", name, "addon/components")); }); it("supports v2 addons", async (ctx) => { const name = "v2-addon"; - const cwd = blueprintPath(name); - const documentPath = await getDocumentPath("component", cwd); + const generatePath = await getGeneratePath( + "component", + Package.createPath(name), + ); ctx - .expect(documentPath) - .toEqual(join("test/blueprints", name, "src/components")); + .expect(generatePath) + .toEqual(join("test/packages", name, "src/components")); }); diff --git a/test/generate-helper.test.ts b/test/generate-helper.test.ts index 7965212..061004e 100644 --- a/test/generate-helper.test.ts +++ b/test/generate-helper.test.ts @@ -1,84 +1,80 @@ -import { remove } from "fs-extra"; -import { readFile } from "node:fs/promises"; -import { join } from "node:path"; import { afterEach, it } from "vitest"; import { generateHelper } from "../src/generators.ts"; -import { copyBlueprint } from "./helpers.ts"; +import { Package } from "./helpers.ts"; -let cwd: string; +let pkg: Package; -afterEach(() => remove(cwd)); +afterEach(() => pkg.cleanUp()); it("generates a function-based `.js` helper", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateHelper("foo", { cwd }); + await generateHelper("foo", pkg.path); - const content = await readFile(join(cwd, "src/helpers/foo.js"), "utf-8"); + const content = await pkg.readFile("src/helpers/foo.js"); ctx.expect(content).toMatchSnapshot(); }); it("generates a class-based `.js` helper", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateHelper("foo", { classBased: true, cwd }); + await generateHelper("foo", pkg.path, { classBased: true }); - const content = await readFile(join(cwd, "src/helpers/foo.js"), "utf-8"); + const content = await pkg.readFile("src/helpers/foo.js"); ctx.expect(content).toMatchSnapshot(); }); it("generates a function-based `.js` helper at a custom path", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateHelper("foo", { cwd, path: "src/-private" }); + await generateHelper("foo", pkg.path, { path: "src/-private" }); - const content = await readFile(join(cwd, "src/-private/foo.js"), "utf-8"); + const content = await pkg.readFile("src/-private/foo.js"); ctx.expect(content).toMatchSnapshot(); }); it("generates a function-based `.ts` helper", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateHelper("foo", { cwd, typescript: true }); + await generateHelper("foo", pkg.path, { typescript: true }); - const content = await readFile(join(cwd, "src/helpers/foo.ts"), "utf-8"); + const content = await pkg.readFile("src/helpers/foo.ts"); ctx.expect(content).toMatchSnapshot(); }); it("generates a class-based `.ts` helper", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateHelper("foo", { classBased: true, cwd, typescript: true }); + await generateHelper("foo", pkg.path, { classBased: true, typescript: true }); - const content = await readFile(join(cwd, "src/helpers/foo.ts"), "utf-8"); + const content = await pkg.readFile("src/helpers/foo.ts"); ctx.expect(content).toMatchSnapshot(); }); it("generates a function-based `.ts` helper at a custom path", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateHelper("foo", { - cwd, + await generateHelper("foo", pkg.path, { path: "src/-private", typescript: true, }); - const content = await readFile(join(cwd, "src/-private/foo.ts"), "utf-8"); + const content = await pkg.readFile("src/-private/foo.ts"); ctx.expect(content).toMatchSnapshot(); }); it("generates a nested function-based `.js` helper", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateHelper("foo/bar", { cwd }); + await generateHelper("foo/bar", pkg.path); - const content = await readFile(join(cwd, "src/helpers/foo/bar.js"), "utf-8"); + const content = await pkg.readFile("src/helpers/foo/bar.js"); ctx.expect(content).toMatchSnapshot(); }); diff --git a/test/generate-modifier.test.ts b/test/generate-modifier.test.ts index b0cf2d4..56a04d5 100644 --- a/test/generate-modifier.test.ts +++ b/test/generate-modifier.test.ts @@ -1,87 +1,83 @@ -import { remove } from "fs-extra"; -import { readFile } from "node:fs/promises"; -import { join } from "node:path"; import { afterEach, it } from "vitest"; import { generateModifier } from "../src/generators.ts"; -import { copyBlueprint } from "./helpers.ts"; +import { Package } from "./helpers.ts"; -let cwd: string; +let pkg: Package; -afterEach(() => remove(cwd)); +afterEach(() => pkg.cleanUp()); it("generates a function-based `.js` modifier", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateModifier("foo", { cwd }); + await generateModifier("foo", pkg.path); - const content = await readFile(join(cwd, "src/modifiers/foo.js"), "utf-8"); + const content = await pkg.readFile("src/modifiers/foo.js"); ctx.expect(content).toMatchSnapshot(); }); it("generates a class-based `.js` modifier", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateModifier("foo", { classBased: true, cwd }); + await generateModifier("foo", pkg.path, { classBased: true }); - const content = await readFile(join(cwd, "src/modifiers/foo.js"), "utf-8"); + const content = await pkg.readFile("src/modifiers/foo.js"); ctx.expect(content).toMatchSnapshot(); }); it("generates a function-based `.js` modifier at a custom path", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateModifier("foo", { cwd, path: "src/-private" }); + await generateModifier("foo", pkg.path, { path: "src/-private" }); - const content = await readFile(join(cwd, "src/-private/foo.js"), "utf-8"); + const content = await pkg.readFile("src/-private/foo.js"); ctx.expect(content).toMatchSnapshot(); }); it("generates a function-based `.ts` modifier", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateModifier("foo", { cwd, typescript: true }); + await generateModifier("foo", pkg.path, { typescript: true }); - const content = await readFile(join(cwd, "src/modifiers/foo.ts"), "utf-8"); + const content = await pkg.readFile("src/modifiers/foo.ts"); ctx.expect(content).toMatchSnapshot(); }); it("generates a class-based `.ts` modifier", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateModifier("foo", { classBased: true, cwd, typescript: true }); + await generateModifier("foo", pkg.path, { + classBased: true, + typescript: true, + }); - const content = await readFile(join(cwd, "src/modifiers/foo.ts"), "utf-8"); + const content = await pkg.readFile("src/modifiers/foo.ts"); ctx.expect(content).toMatchSnapshot(); }); it("generates a function-based `.ts` modifier at a custom path", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateModifier("foo", { - cwd, + await generateModifier("foo", pkg.path, { path: "src/-private", typescript: true, }); - const content = await readFile(join(cwd, "src/-private/foo.ts"), "utf-8"); + const content = await pkg.readFile("src/-private/foo.ts"); ctx.expect(content).toMatchSnapshot(); }); it("generates a nested function-based `.js` modifier", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateModifier("foo/bar", { cwd }); + await generateModifier("foo/bar", pkg.path); - const content = await readFile( - join(cwd, "src/modifiers/foo/bar.js"), - "utf-8", - ); + const content = await pkg.readFile("src/modifiers/foo/bar.js"); ctx.expect(content).toMatchSnapshot(); }); diff --git a/test/generate-service.test.ts b/test/generate-service.test.ts index d7ea3e6..37867ec 100644 --- a/test/generate-service.test.ts +++ b/test/generate-service.test.ts @@ -1,60 +1,60 @@ -import { remove } from "fs-extra"; -import { readFile } from "node:fs/promises"; -import { join } from "node:path"; import { afterEach, it } from "vitest"; import { generateService } from "../src/generators.ts"; -import { copyBlueprint } from "./helpers.ts"; +import { Package } from "./helpers.ts"; -let cwd: string; +let pkg: Package; -afterEach(() => remove(cwd)); +afterEach(() => pkg.cleanUp()); it("generates a `.js` service", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateService("foo", { cwd }); + await generateService("foo", pkg.path); - const content = await readFile(join(cwd, "src/services/foo.js"), "utf-8"); + const content = await pkg.readFile("src/services/foo.js"); ctx.expect(content).toMatchSnapshot(); }); it("generates a `.ts` service", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateService("foo", { cwd, typescript: true }); + await generateService("foo", pkg.path, { typescript: true }); - const content = await readFile(join(cwd, "src/services/foo.ts"), "utf-8"); + const content = await pkg.readFile("src/services/foo.ts"); ctx.expect(content).toMatchSnapshot(); }); it("generates a `.js` service at a custom path", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateService("foo", { cwd, path: "src/-private" }); + await generateService("foo", pkg.path, { path: "src/-private" }); - const content = await readFile(join(cwd, "src/-private/foo.js"), "utf-8"); + const content = await pkg.readFile("src/-private/foo.js"); ctx.expect(content).toMatchSnapshot(); }); it("generates a `.ts` service at a custom path", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateService("foo", { cwd, path: "src/-private", typescript: true }); + await generateService("foo", pkg.path, { + path: "src/-private", + typescript: true, + }); - const content = await readFile(join(cwd, "src/-private/foo.ts"), "utf-8"); + const content = await pkg.readFile("src/-private/foo.ts"); ctx.expect(content).toMatchSnapshot(); }); it("generates a nested `.js` service", async (ctx) => { - cwd = await copyBlueprint("v2-addon"); + pkg = await Package.create("v2-addon"); - await generateService("foo/bar", { cwd }); + await generateService("foo/bar", pkg.path); - const content = await readFile(join(cwd, "src/services/foo/bar.js"), "utf-8"); + const content = await pkg.readFile("src/services/foo/bar.js"); ctx.expect(content).toMatchSnapshot(); }); diff --git a/test/helpers.ts b/test/helpers.ts index 31ff561..d7fb4e6 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -1,20 +1,39 @@ +import { remove } from "fs-extra"; +import { readFile } from "node:fs/promises"; import { join } from "node:path"; import recursiveCopy from "recursive-copy"; import { v4 as uuidv4 } from "uuid"; -type Blueprint = "v1-app" | "v1-addon" | "v2-addon" | "v2-addon-hooks"; +type PackageName = "v1-app" | "v1-addon" | "v2-addon" | "v2-addon-hooks"; -export function blueprintPath(name: Blueprint) { - return join("test/blueprints", name); -} +export class Package { + path: string; + + constructor(path: string) { + this.path = path; + } + + cleanUp(): Promise { + return remove(this.path); + } + + readFile(path: string): Promise { + return readFile(join(this.path, path), "utf-8"); + } + + static async create( + name: PackageName, + path: string = uuidv4(), + ): Promise { + const pkg = new this(join("test/output", path)); -export async function copyBlueprint( - name: Blueprint, - directory: string = uuidv4(), -) { - const cwd = join("test/output", directory); + await pkg.cleanUp(); + await recursiveCopy(this.createPath(name), pkg.path); - await recursiveCopy(blueprintPath(name), cwd); + return pkg; + } - return cwd; + static createPath(name: PackageName): string { + return join("test/packages", name); + } } diff --git a/test/blueprints/v1-addon/addon/.gitkeep b/test/packages/v1-addon/addon/.gitkeep similarity index 100% rename from test/blueprints/v1-addon/addon/.gitkeep rename to test/packages/v1-addon/addon/.gitkeep diff --git a/test/blueprints/v1-addon/package.json b/test/packages/v1-addon/package.json similarity index 100% rename from test/blueprints/v1-addon/package.json rename to test/packages/v1-addon/package.json diff --git a/test/blueprints/v1-app/app/.gitkeep b/test/packages/v1-app/app/.gitkeep similarity index 100% rename from test/blueprints/v1-app/app/.gitkeep rename to test/packages/v1-app/app/.gitkeep diff --git a/test/blueprints/v1-app/package.json b/test/packages/v1-app/package.json similarity index 100% rename from test/blueprints/v1-app/package.json rename to test/packages/v1-app/package.json diff --git a/test/blueprints/v2-addon-hooks/gember.config.js b/test/packages/v2-addon-hooks/gember.config.js similarity index 83% rename from test/blueprints/v2-addon-hooks/gember.config.js rename to test/packages/v2-addon-hooks/gember.config.js index b58f657..565ef96 100644 --- a/test/blueprints/v2-addon-hooks/gember.config.js +++ b/test/packages/v2-addon-hooks/gember.config.js @@ -7,8 +7,10 @@ import { fileURLToPath } from "node:url"; export default { hooks: { postGenerate: async (info) => { - const directory = dirname(fileURLToPath(import.meta.url)); - const file = join(directory, "post-generate-info.json"); + const file = join( + dirname(fileURLToPath(import.meta.url)), + "post-generate-info.json", + ); for (const file of info.files) { // Support Windows: diff --git a/test/blueprints/v2-addon-hooks/package.json b/test/packages/v2-addon-hooks/package.json similarity index 100% rename from test/blueprints/v2-addon-hooks/package.json rename to test/packages/v2-addon-hooks/package.json diff --git a/test/blueprints/v2-addon/package.json b/test/packages/v2-addon/package.json similarity index 100% rename from test/blueprints/v2-addon/package.json rename to test/packages/v2-addon/package.json From 879f6729fdde60f816eec5644313b0e6bd23a8a1 Mon Sep 17 00:00:00 2001 From: Bert De Block Date: Tue, 26 Nov 2024 15:19:21 +0100 Subject: [PATCH 2/2] Support all CLI options in the gember config file --- README.md | 64 +++++++++---- package.json | 5 +- pnpm-lock.yaml | 95 +++++++++++++++++++ src/cli.ts | 85 ++++++++++++----- src/config.ts | 75 +++++++++++---- src/generate.ts | 13 ++- test/__snapshots__/config.test.ts.snap | 19 ++++ test/config.test.ts | 24 +++-- test/generate-document.test.ts | 8 +- test/helpers.ts | 17 +++- .../gember.config.mjs} | 8 ++ .../package.json | 2 +- 12 files changed, 337 insertions(+), 78 deletions(-) rename test/packages/{v2-addon-hooks/gember.config.js => v2-addon-config/gember.config.mjs} (89%) rename test/packages/{v2-addon-hooks => v2-addon-config}/package.json (85%) diff --git a/README.md b/README.md index 4467098..841622e 100644 --- a/README.md +++ b/README.md @@ -116,28 +116,56 @@ gember supports the following config files: - `gember.config.cjs` - `gember.config.mjs` -A gember config file must export a gember config object, or a sync/async function that returns a gember config object. +A gember config file must export a gember config object, or a sync/async function that returns a gember config object: -### Configuration Options +```js +// gember.config.js -#### `hooks.postGenerate` +export default {}; -A hook that will be executed post running a generator. +// or: +export default () => ({}); -```js -// gember.config.js +// or: +export default async () => ({}); +``` -import { execa } from "execa"; - -export default { - hooks: { - postGenerate: async ({ files }) => { - await execa("npx", [ - "prettier", - "--write", - ...files.map((file) => file.path), - ]); - }, - }, +### Configuration Signature + +```ts +export type Config = { + generators?: { + component?: { + classBased?: boolean; + path?: string; + typescript?: boolean; + }; + helper?: { + classBased?: boolean; + path?: string; + typescript?: boolean; + }; + modifier?: { + classBased?: boolean; + path?: string; + typescript?: boolean; + }; + service?: { + path?: string; + typescript?: boolean; + }; + }; + + hooks?: { + // A hook that will be executed post running a generator: + postGenerate?: (info: { + documentName: DocumentName; + entityName: string; + files: File[]; + }) => Promise | void; + }; + + // Use TypeScript by default for all generators: + typescript?: boolean; }; ``` diff --git a/package.json b/package.json index 750f176..b6b1e8f 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "CHANGELOG.md" ], "scripts": { - "build": "rm -rf dist && tsc --project tsconfig.json", + "build": "tsc --project tsconfig.json", "lint": "concurrently --group --prefix-colors auto \"npm:lint:*(!fix)\"", "lint:fix": "concurrently --group --prefix-colors auto \"npm:lint:*:fix\"", "lint:format": "prettier . --cache --check", @@ -26,7 +26,7 @@ "prepack": "tsc --project tsconfig.json", "start": "pnpm build --watch", "test": "vitest", - "test:coverage": "vitest run --coverage" + "test:coverage": "pnpm build && vitest run --coverage" }, "dependencies": { "change-case": "^5.4.4", @@ -45,6 +45,7 @@ "concurrently": "^9.1.0", "eslint": "^9.15.0", "eslint-plugin-n": "^17.13.2", + "execa": "^9.5.1", "fixturify-project": "^7.1.3", "prettier": "^3.3.3", "recursive-copy": "^2.0.14", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index caca570..332f7b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,6 +51,9 @@ importers: eslint-plugin-n: specifier: ^17.13.2 version: 17.13.2(eslint@9.15.0(jiti@1.21.6)) + execa: + specifier: ^9.5.1 + version: 9.5.1 fixturify-project: specifier: ^7.1.3 version: 7.1.3 @@ -735,6 +738,9 @@ packages: '@scaffdog/types@4.1.0': resolution: {integrity: sha512-VW1mL0mRat+VQgTrg24UK7A+peSMstewAtATyIghd/nRUwk2vJ2WIXIcVlkhkLlF6AVakY6EtTKFU/IdcOXdgA==} + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@sindresorhus/is@4.6.0': resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} @@ -743,6 +749,10 @@ packages: resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} engines: {node: '>=18'} + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + '@tootallnate/once@1.1.2': resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} engines: {node: '>= 6'} @@ -1444,6 +1454,10 @@ packages: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} + execa@9.5.1: + resolution: {integrity: sha512-QY5PPtSonnGwhhHDNI7+3RvY285c7iuJFFB+lU+oEzMY/gEGJ808owqJsrr8Otd1E/x07po1LkUBmdAc5duPAg==} + engines: {node: ^18.19.0 || >=20.5.0} + expect-type@1.1.0: resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} engines: {node: '>=12.0.0'} @@ -1575,6 +1589,10 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + get-tsconfig@4.8.1: resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} @@ -1665,6 +1683,10 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + human-signals@8.0.0: + resolution: {integrity: sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==} + engines: {node: '>=18.18.0'} + humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} @@ -1815,6 +1837,10 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + is-subdir@1.2.0: resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} engines: {node: '>=4'} @@ -2258,6 +2284,10 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2339,6 +2369,10 @@ packages: resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} engines: {node: '>=6'} + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + parse5-htmlparser2-tree-adapter@6.0.1: resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} @@ -2368,6 +2402,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + path-name@1.0.0: resolution: {integrity: sha512-/dcAb5vMXH0f51yvMuSUqFpxUcA8JelbRmE5mW/p4CUJxrNgK24IkstnV7ENtg2IDGBOu6izKTG6eilbnbNKWQ==} @@ -2437,6 +2475,10 @@ packages: resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} engines: {node: '>=10'} + pretty-ms@9.2.0: + resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==} + engines: {node: '>=18'} + printable-characters@1.0.42: resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} @@ -2755,6 +2797,10 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + strip-json-comments@2.0.1: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} @@ -2898,6 +2944,10 @@ packages: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + unified@10.1.2: resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} @@ -3868,10 +3918,14 @@ snapshots: dependencies: type-fest: 4.26.1 + '@sec-ant/readable-stream@0.4.1': {} + '@sindresorhus/is@4.6.0': {} '@sindresorhus/merge-streams@2.3.0': {} + '@sindresorhus/merge-streams@4.0.0': {} + '@tootallnate/once@1.1.2': {} '@types/debug@4.1.12': @@ -4667,6 +4721,21 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 + execa@9.5.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.0 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.2.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.1 + expect-type@1.1.0: {} extend@3.0.2: {} @@ -4819,6 +4888,11 @@ snapshots: get-stream@6.0.1: {} + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + get-tsconfig@4.8.1: dependencies: resolve-pkg-maps: 1.0.0 @@ -4930,6 +5004,8 @@ snapshots: human-signals@2.1.0: {} + human-signals@8.0.0: {} + humanize-ms@1.2.1: dependencies: ms: 2.1.3 @@ -5051,6 +5127,8 @@ snapshots: is-stream@2.0.1: {} + is-stream@4.0.1: {} + is-subdir@1.2.0: dependencies: better-path-resolve: 1.0.0 @@ -5577,6 +5655,11 @@ snapshots: dependencies: path-key: 3.1.1 + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + object-assign@4.1.1: {} once@1.4.0: @@ -5666,6 +5749,8 @@ snapshots: parse-ms@2.1.0: {} + parse-ms@4.0.0: {} + parse5-htmlparser2-tree-adapter@6.0.1: dependencies: parse5: 6.0.1 @@ -5684,6 +5769,8 @@ snapshots: path-key@3.1.1: {} + path-key@4.0.0: {} + path-name@1.0.0: {} path-root-regex@0.1.2: {} @@ -5735,6 +5822,10 @@ snapshots: dependencies: parse-ms: 2.1.0 + pretty-ms@9.2.0: + dependencies: + parse-ms: 4.0.0 + printable-characters@1.0.42: {} proc-log@4.2.0: {} @@ -6102,6 +6193,8 @@ snapshots: strip-final-newline@2.0.0: {} + strip-final-newline@4.0.0: {} + strip-json-comments@2.0.1: {} strip-json-comments@3.1.1: {} @@ -6212,6 +6305,8 @@ snapshots: unicorn-magic@0.1.0: {} + unicorn-magic@0.3.0: {} + unified@10.1.2: dependencies: '@types/unist': 2.0.11 diff --git a/src/cli.ts b/src/cli.ts index 848975d..eacdc0c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,6 +1,7 @@ import { cwd } from "node:process"; import { hideBin } from "yargs/helpers"; import yargs from "yargs/yargs"; +import { resolveConfig } from "./config.js"; import { logGemberErrors } from "./errors.js"; import { generateComponent, @@ -8,6 +9,7 @@ import { generateModifier, generateService, } from "./generators.js"; +import { DocumentName } from "./types.js"; yargs(hideBin(process.argv)) .command({ @@ -37,12 +39,16 @@ yargs(hideBin(process.argv)) }); }, handler(options) { - logGemberErrors(() => - generateComponent(options.name, cwd(), { - classBased: options.classBased, - path: options.path, - typescript: options.typescript, - }), + logGemberErrors(async () => + generateComponent( + options.name, + cwd(), + await applyGemberConfig("component", { + classBased: options.classBased, + path: options.path, + typescript: options.typescript, + }), + ), ); }, }) @@ -73,12 +79,16 @@ yargs(hideBin(process.argv)) }); }, handler(options) { - logGemberErrors(() => - generateHelper(options.name, cwd(), { - classBased: options.classBased, - path: options.path, - typescript: options.typescript, - }), + logGemberErrors(async () => + generateHelper( + options.name, + cwd(), + await applyGemberConfig("helper", { + classBased: options.classBased, + path: options.path, + typescript: options.typescript, + }), + ), ); }, }) @@ -109,12 +119,16 @@ yargs(hideBin(process.argv)) }); }, handler(options) { - logGemberErrors(() => - generateModifier(options.name, cwd(), { - classBased: options.classBased, - path: options.path, - typescript: options.typescript, - }), + logGemberErrors(async () => + generateModifier( + options.name, + cwd(), + await applyGemberConfig("modifier", { + classBased: options.classBased, + path: options.path, + typescript: options.typescript, + }), + ), ); }, }) @@ -140,11 +154,15 @@ yargs(hideBin(process.argv)) }); }, handler(options) { - logGemberErrors(() => - generateService(options.name, cwd(), { - path: options.path, - typescript: options.typescript, - }), + logGemberErrors(async () => + generateService( + options.name, + cwd(), + await applyGemberConfig("service", { + path: options.path, + typescript: options.typescript, + }), + ), ); }, }) @@ -152,3 +170,24 @@ yargs(hideBin(process.argv)) .epilogue("🫚 More info at https://github.com/bertdeblock/gember#usage") .strict() .parse(); + +type Options = Record; + +async function applyGemberConfig( + documentName: DocumentName, + options: Options, +): Promise { + const config = await resolveConfig(cwd()); + const generatorConfig: Options = config.generators?.[documentName] ?? {}; + const result: Options = { typescript: config.typescript }; + + for (const key in options) { + if (options[key] !== undefined) { + result[key] = options[key]; + } else if (generatorConfig[key] !== undefined) { + result[key] = generatorConfig[key]; + } + } + + return result; +} diff --git a/src/config.ts b/src/config.ts index 7929590..8e70405 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,6 +4,28 @@ import { GemberError } from "./errors.js"; import { DocumentName, type File } from "./types.js"; export type Config = { + generators?: { + component?: { + classBased?: boolean; + path?: string; + typescript?: boolean; + }; + helper?: { + classBased?: boolean; + path?: string; + typescript?: boolean; + }; + modifier?: { + classBased?: boolean; + path?: string; + typescript?: boolean; + }; + service?: { + path?: string; + typescript?: boolean; + }; + }; + hooks?: { postGenerate?: (info: { documentName: DocumentName; @@ -11,6 +33,8 @@ export type Config = { files: File[]; }) => Promise | void; }; + + typescript?: boolean; }; const CONFIG_FILES = [ @@ -19,30 +43,45 @@ const CONFIG_FILES = [ "gember.config.mjs", ]; -const DEFAULT_CONFIG: Config = {}; +const RESOLVED_CONFIGS: Map = new Map(); -export async function getConfig(cwd: string): Promise { - const path = await findUp(CONFIG_FILES, { cwd }); +export async function resolveConfig(cwd: string): Promise { + let resolvedConfig = RESOLVED_CONFIGS.get(cwd); - if (path === undefined) { - return DEFAULT_CONFIG; + if (resolvedConfig) { + return resolvedConfig; } - let config; + const path = await findUp(CONFIG_FILES, { cwd }); - try { - config = (await import(pathToFileURL(path).toString())).default; - } catch (cause) { - throw new GemberError(`Could not import gember config file at "${path}".`, { - cause, - }); - } + if (path) { + let config; + + try { + config = (await import(pathToFileURL(path).toString())).default; + } catch (cause) { + throw new GemberError( + `Could not import gember config file at \`${path}\`.`, + { + cause, + }, + ); + } - if (config === undefined) { - throw new GemberError( - `gember config file at "${path}" must have a "default" export.`, - ); + if (config === undefined) { + throw new GemberError( + `gember config file at \`${path}\` must have a \`default\` export.`, + ); + } + + resolvedConfig = ( + typeof config === "function" ? await config() : config + ) as Config; + } else { + resolvedConfig = {}; } - return typeof config === "function" ? await config() : config; + RESOLVED_CONFIGS.set(cwd, resolvedConfig); + + return resolvedConfig; } diff --git a/src/generate.ts b/src/generate.ts index 787ef24..be28d20 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -6,7 +6,7 @@ import { dirname, isAbsolute, join, parse, relative } from "node:path"; import { cwd } from "node:process"; import { fileURLToPath } from "node:url"; import { type GenerateInputs, loadScaffdog } from "scaffdog"; -import { getConfig } from "./config.js"; +import { resolveConfig } from "./config.js"; import { GemberError } from "./errors.js"; import { isAddon, isV2Addon } from "./helpers.js"; import { type DocumentName } from "./types.js"; @@ -34,7 +34,12 @@ export async function generate( throw new GemberError(`[BUG] Document \`${documentName}\` not found.`); } - const generatePath = await getGeneratePath(documentName, packagePath, path); + const generatePath = await resolveGeneratePath( + documentName, + packagePath, + path, + ); + const files = await scaffdog.generate(document, generatePath, { inputs: { ...inputs, @@ -63,7 +68,7 @@ export async function generate( ); } - const config = await getConfig(packagePath); + const config = await resolveConfig(packagePath); await config.hooks?.postGenerate?.({ documentName, @@ -89,7 +94,7 @@ const SRC_DIRECTORY: Record = { V2_ADDON: "src", }; -export async function getGeneratePath( +export async function resolveGeneratePath( documentName: DocumentName, packagePath: string, path?: string, diff --git a/test/__snapshots__/config.test.ts.snap b/test/__snapshots__/config.test.ts.snap index bb56598..88ed3e7 100644 --- a/test/__snapshots__/config.test.ts.snap +++ b/test/__snapshots__/config.test.ts.snap @@ -1,5 +1,24 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`applies specific generator options 1`] = ` +"import Component from "@glimmer/component"; + +export interface FooSignature { + Args: {}; + Blocks: { + default: []; + }; + Element: null; +} + +export default class Foo extends Component { + +} +" +`; + exports[`runs the \`postGenerate\` hook 1`] = ` "{ "documentName": "component", diff --git a/test/config.test.ts b/test/config.test.ts index b043f03..eb528d8 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -1,8 +1,8 @@ import { Project } from "fixturify-project"; import { it } from "vitest"; -import { getConfig } from "../src/config.js"; +import { resolveConfig } from "../src/config.js"; import { generateComponent } from "../src/generators.ts"; -import { Package } from "./helpers.js"; +import { gember, Package } from "./helpers.js"; it("supports a `gember.config.js` file", async (ctx) => { const project = new Project({ @@ -14,7 +14,7 @@ it("supports a `gember.config.js` file", async (ctx) => { await project.write(); - const config = await getConfig(project.baseDir); + const config = await resolveConfig(project.baseDir); ctx.expect(config).to.deep.equal({ hooks: {} }); }); @@ -29,7 +29,7 @@ it("supports a `gember.config.cjs` file", async (ctx) => { await project.write(); - const config = await getConfig(project.baseDir); + const config = await resolveConfig(project.baseDir); ctx.expect(config).to.deep.equal({ hooks: {} }); }); @@ -44,13 +44,13 @@ it("supports a `gember.config.mjs` file", async (ctx) => { await project.write(); - const config = await getConfig(project.baseDir); + const config = await resolveConfig(project.baseDir); ctx.expect(config).to.deep.equal({ hooks: {} }); }); it("runs the `postGenerate` hook", async (ctx) => { - const pkg = await Package.create("v2-addon-hooks", "post-generate-info"); + const pkg = await Package.create("v2-addon-config", "post-generate-info"); await generateComponent("foo", pkg.path); @@ -60,3 +60,15 @@ it("runs the `postGenerate` hook", async (ctx) => { await pkg.cleanUp(); }); + +it("applies specific generator options", async (ctx) => { + const pkg = await Package.create("v2-addon-config"); + + await gember(["component", "foo"], { cwd: pkg.path }); + + const content = await pkg.readFile("src/components/foo.gts"); + + ctx.expect(content).toMatchSnapshot(); + + await pkg.cleanUp(); +}); diff --git a/test/generate-document.test.ts b/test/generate-document.test.ts index ebda2f8..8e49d7e 100644 --- a/test/generate-document.test.ts +++ b/test/generate-document.test.ts @@ -1,11 +1,11 @@ import { join } from "node:path"; import { it } from "vitest"; -import { getGeneratePath } from "../src/generate.ts"; +import { resolveGeneratePath } from "../src/generate.ts"; import { Package } from "./helpers.ts"; it("supports v1 apps", async (ctx) => { const name = "v1-app"; - const generatePath = await getGeneratePath( + const generatePath = await resolveGeneratePath( "component", Package.createPath(name), ); @@ -17,7 +17,7 @@ it("supports v1 apps", async (ctx) => { it("supports v1 addons", async (ctx) => { const name = "v1-addon"; - const generatePath = await getGeneratePath( + const generatePath = await resolveGeneratePath( "component", Package.createPath(name), ); @@ -29,7 +29,7 @@ it("supports v1 addons", async (ctx) => { it("supports v2 addons", async (ctx) => { const name = "v2-addon"; - const generatePath = await getGeneratePath( + const generatePath = await resolveGeneratePath( "component", Package.createPath(name), ); diff --git a/test/helpers.ts b/test/helpers.ts index d7fb4e6..4f9280a 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -1,10 +1,12 @@ +import { execa } from "execa"; import { remove } from "fs-extra"; import { readFile } from "node:fs/promises"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; import recursiveCopy from "recursive-copy"; import { v4 as uuidv4 } from "uuid"; -type PackageName = "v1-app" | "v1-addon" | "v2-addon" | "v2-addon-hooks"; +type PackageName = "v1-app" | "v1-addon" | "v2-addon" | "v2-addon-config"; export class Package { path: string; @@ -37,3 +39,14 @@ export class Package { return join("test/packages", name); } } + +export async function gember( + args: string[], + { cwd }: { cwd: string }, +): Promise { + await execa( + join(dirname(fileURLToPath(import.meta.url)), "../bin/gember.js"), + args, + { cwd }, + ); +} diff --git a/test/packages/v2-addon-hooks/gember.config.js b/test/packages/v2-addon-config/gember.config.mjs similarity index 89% rename from test/packages/v2-addon-hooks/gember.config.js rename to test/packages/v2-addon-config/gember.config.mjs index 565ef96..2356826 100644 --- a/test/packages/v2-addon-hooks/gember.config.js +++ b/test/packages/v2-addon-config/gember.config.mjs @@ -5,6 +5,12 @@ import { fileURLToPath } from "node:url"; /** @type {import('../../../src/config.ts').Config} */ export default { + generators: { + component: { + classBased: true, + }, + }, + hooks: { postGenerate: async (info) => { const file = join( @@ -23,4 +29,6 @@ export default { await writeFile(file, JSON.stringify(info, null, 2)); }, }, + + typescript: true, }; diff --git a/test/packages/v2-addon-hooks/package.json b/test/packages/v2-addon-config/package.json similarity index 85% rename from test/packages/v2-addon-hooks/package.json rename to test/packages/v2-addon-config/package.json index 5b74278..ba02973 100644 --- a/test/packages/v2-addon-hooks/package.json +++ b/test/packages/v2-addon-config/package.json @@ -1,5 +1,5 @@ { - "name": "v2-addon-hooks", + "name": "v2-addon-config", "private": true, "volta": { "extends": "../../../package.json"