diff --git a/README.md b/README.md index af54309..b9cada3 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Generate components, helpers, modifiers and services in v1/v2 apps/addons. -> NOTE: Only supports `.gjs` (default) and `.gts` files for components. +> ℹ️ Only supports `.gjs` (default) and `.gts` files for components. ## Installation @@ -47,6 +47,10 @@ yarn add -D @bertdeblock/gember ## Usage +> 💡 Run `pnpm gember --help` for all available generators. + +> 💡 Run `pnpm gember --help` for all available generator options. +
Generating components @@ -140,20 +144,36 @@ export type Config = { path?: string; typescript?: boolean; }; + "component-test"?: { + path?: string; + typescript?: boolean; + }; helper?: { classBased?: boolean; path?: string; typescript?: boolean; }; + "helper-test"?: { + path?: string; + typescript?: boolean; + }; modifier?: { classBased?: boolean; path?: string; typescript?: boolean; }; + "modifier-test"?: { + path?: string; + typescript?: boolean; + }; service?: { path?: string; typescript?: boolean; }; + "service-test"?: { + path?: string; + typescript?: boolean; + }; }; hooks?: { @@ -161,7 +181,7 @@ export type Config = { postGenerate?: (info: { entityName: string; files: GeneratorFile[]; - generatorName: GeneratorName; + generatorName: string; }) => Promise | void; }; diff --git a/package.json b/package.json index c8ee7b9..5a87766 100644 --- a/package.json +++ b/package.json @@ -31,17 +31,16 @@ }, "dependencies": { "change-case": "^5.4.4", + "citty": "^0.1.6", "consola": "^3.4.2", "find-up": "^7.0.0", "fs-extra": "^11.3.0", - "handlebars": "^4.7.8", - "yargs": "^17.7.2" + "handlebars": "^4.7.8" }, "devDependencies": { "@eslint/js": "^9.26.0", "@types/fs-extra": "^11.0.4", "@types/node": "^22.15.12", - "@types/yargs": "^17.0.33", "@vitest/coverage-v8": "^3.1.3", "concurrently": "^9.1.2", "eslint": "^9.26.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 87ae2d1..03d0e7d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: change-case: specifier: ^5.4.4 version: 5.4.4 + citty: + specifier: ^0.1.6 + version: 0.1.6 consola: specifier: ^3.4.2 version: 3.4.2 @@ -23,9 +26,6 @@ importers: handlebars: specifier: ^4.7.8 version: 4.7.8 - yargs: - specifier: ^17.7.2 - version: 17.7.2 devDependencies: '@eslint/js': specifier: ^9.26.0 @@ -36,9 +36,6 @@ importers: '@types/node': specifier: ^22.15.12 version: 22.15.12 - '@types/yargs': - specifier: ^17.0.33 - version: 17.0.33 '@vitest/coverage-v8': specifier: ^3.1.3 version: 3.1.3(vitest@3.1.3(@types/node@22.15.12)(jiti@1.21.7)) @@ -591,12 +588,6 @@ packages: '@types/node@22.15.12': resolution: {integrity: sha512-K0fpC/ZVeb8G9rm7bH7vI0KAec4XHEhBam616nVJCV51bKzJ6oA3luG4WdKoaztxe70QaNjS/xBmcDLmr4PiGw==} - '@types/yargs-parser@21.0.3': - resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} - - '@types/yargs@17.0.33': - resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} - '@typescript-eslint/eslint-plugin@8.32.0': resolution: {integrity: sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -826,6 +817,9 @@ packages: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -2754,12 +2748,6 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/yargs-parser@21.0.3': {} - - '@types/yargs@17.0.33': - dependencies: - '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.32.0(@typescript-eslint/parser@8.32.0(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.26.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -3049,6 +3037,10 @@ snapshots: chownr@2.0.0: {} + citty@0.1.6: + dependencies: + consola: 3.4.2 + clean-stack@2.2.0: {} cli-highlight@2.1.11: diff --git a/src/cli.ts b/src/cli.ts index 570708f..ff54163 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,199 +1,55 @@ -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, - generateHelper, - generateModifier, - generateService, -} from "./generators.js"; -import type { GeneratorName } from "./types.js"; - -yargs(hideBin(process.argv)) - .command({ - command: "component [name]", - describe: "Generate a new component", - - builder(yargs) { - return yargs - .positional("name", { - demandOption: true, - description: "The component's name", - type: "string", - }) - .option("class-based", { - alias: ["class"], - description: "Generate a class-based component", - type: "boolean", - }) - .option("nested", { - description: - "Generate a nested colocated component, e.g. `foo/bar/index.gjs`", - type: "boolean", - }) - .option("path", { - description: "Generate a component at a custom path", - type: "string", - }) - .option("typescript", { - alias: ["ts"], - description: "Generate a `.gts` component", - type: "boolean", - }); - }, - handler(options) { - logGemberErrors(async () => - generateComponent( - options.name, - cwd(), - await applyGemberConfig("component", { - classBased: options.classBased, - nested: options.nested, - path: options.path, - typescript: options.typescript, - }), - ), - ); - }, - }) - .command({ - command: "helper [name]", - describe: "Generate a new helper", - - builder(yargs) { - return yargs - .positional("name", { - demandOption: true, - description: "The helper's name", - type: "string", - }) - .option("class-based", { - alias: ["class"], - description: "Generate a class-based helper", - type: "boolean", - }) - .option("path", { - description: "Generate a helper at a custom path", - type: "string", - }) - .option("typescript", { - alias: ["ts"], - description: "Generate a `.ts` helper", - type: "boolean", - }); - }, - handler(options) { - logGemberErrors(async () => - generateHelper( - options.name, - cwd(), - await applyGemberConfig("helper", { - classBased: options.classBased, - path: options.path, - typescript: options.typescript, - }), - ), - ); - }, - }) - .command({ - command: "modifier [name]", - describe: "Generate a new modifier", - - builder(yargs) { - return yargs - .positional("name", { - demandOption: true, - description: "The modifier's name", - type: "string", - }) - .option("class-based", { - alias: ["class"], - description: "Generate a class-based modifier", - type: "boolean", - }) - .option("path", { - description: "Generate a modifier at a custom path", - type: "string", - }) - .option("typescript", { - alias: ["ts"], - description: "Generate a `.ts` modifier", - type: "boolean", - }); - }, - handler(options) { - logGemberErrors(async () => - generateModifier( - options.name, - cwd(), - await applyGemberConfig("modifier", { - classBased: options.classBased, - path: options.path, - typescript: options.typescript, - }), - ), - ); - }, - }) - .command({ - command: "service [name]", - describe: "Generate a new service", - - builder(yargs) { - return yargs - .positional("name", { - demandOption: true, - description: "The service's name", - type: "string", - }) - .option("path", { - description: "Generate a service at a custom path", - type: "string", - }) - .option("typescript", { - alias: ["ts"], - description: "Generate a `.ts` service", - type: "boolean", + defineCommand, + runMain, + type ArgsDef, + type SubCommandsDef, +} from "citty"; +import { readJsonSync } from "fs-extra/esm"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +// eslint-disable-next-line n/no-missing-import +import type { PackageJson } from "type-fest"; +import { logGemberErrors } from "./errors.js"; +import { generators } from "./generators.js"; + +const { description, name, version }: PackageJson = readJsonSync( + join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), +); + +const main = defineCommand({ + meta: { + description, + name, + version, + }, + + subCommands: generators.reduce((subCommands: SubCommandsDef, generator) => { + subCommands[generator.name] = { + args: generator.args.reduce((args: ArgsDef, arg) => { + args[arg.name] = { + alias: arg.alias, + description: arg.description, + required: arg.required, + type: arg.type, + }; + + return args; + }, {}), + + meta: { + description: generator.description, + name: generator.name, + }, + + run: (context): void => { + logGemberErrors(async () => { + await generator.run(context.args); }); - }, - handler(options) { - logGemberErrors(async () => - generateService( - options.name, - cwd(), - await applyGemberConfig("service", { - path: options.path, - typescript: options.typescript, - }), - ), - ); - }, - }) - .demandCommand() - .epilogue("🫚 More info at https://github.com/bertdeblock/gember#usage") - .strict() - .parse(); - -type Options = Record; - -async function applyGemberConfig( - generatorName: GeneratorName, - options: Options, -): Promise { - const config = await resolveConfig(cwd()); - const generatorConfig: Options = config.generators?.[generatorName] ?? {}; - 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 subCommands; + }, {}), +}); - return result; -} +runMain(main); diff --git a/src/config.ts b/src/config.ts index 1b7cf28..372e7b0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,12 +1,17 @@ import { findUp } from "find-up"; import { pathToFileURL } from "node:url"; import { GemberError } from "./errors.js"; -import type { GeneratorFile, GeneratorName } from "./types.js"; +import type { GeneratorFile } from "./types.js"; export type Config = { generators?: { component?: { classBased?: boolean; + nested?: boolean; + path?: string; + typescript?: boolean; + }; + "component-test"?: { path?: string; typescript?: boolean; }; @@ -15,29 +20,43 @@ export type Config = { path?: string; typescript?: boolean; }; + "helper-test"?: { + path?: string; + typescript?: boolean; + }; modifier?: { classBased?: boolean; path?: string; typescript?: boolean; }; + "modifier-test"?: { + path?: string; + typescript?: boolean; + }; service?: { path?: string; typescript?: boolean; }; + "service-test"?: { + path?: string; + typescript?: boolean; + }; }; hooks?: { + // A hook that will be executed post running a generator: postGenerate?: (info: { entityName: string; files: GeneratorFile[]; - generatorName: GeneratorName; + generatorName: string; }) => Promise | void; }; + // Use TypeScript by default for all generators: typescript?: boolean; }; -const CONFIG_FILES = [ +const CONFIG_FILES: string[] = [ "gember.config.js", "gember.config.cjs", "gember.config.mjs", diff --git a/src/file-reference.ts b/src/file-reference.ts new file mode 100644 index 0000000..8507d48 --- /dev/null +++ b/src/file-reference.ts @@ -0,0 +1,33 @@ +import { join, parse, type ParsedPath } from "node:path"; + +export class FileReference { + ext: string; + name: string; + rootDir: string; + subDir: string; + + constructor({ + ext, + name, + rootDir, + subDir, + }: { + ext: string; + name: string; + rootDir: string; + subDir: string; + }) { + this.ext = ext; + this.name = name; + this.rootDir = rootDir; + this.subDir = subDir; + } + + parse(): ParsedPath { + return parse(this.path()); + } + + path(): string { + return join(this.rootDir, this.subDir, this.name + this.ext); + } +} diff --git a/src/generate.ts b/src/generate.ts deleted file mode 100644 index 3f2aca3..0000000 --- a/src/generate.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { camelCase, pascalCase, pathCase } from "change-case"; -import { consola } from "consola"; -import { ensureDir, readJson } from "fs-extra/esm"; -import Handlebars from "handlebars"; -import { readFile, writeFile } from "node:fs/promises"; -import { dirname, isAbsolute, join, parse, relative } from "node:path"; -import { cwd } from "node:process"; -import { fileURLToPath } from "node:url"; -import { resolveConfig } from "./config.js"; -import { isV1Addon, isV2Addon } from "./helpers.js"; -import type { - EmberPackageJson, - GeneratorFile, - GeneratorName, -} from "./types.js"; - -export async function generate({ - customTargetPath, - entityName, - generatorName, - nested = false, - packagePath, - targetDir, - templateFilename, -}: { - customTargetPath?: string; - entityName: string; - generatorName: GeneratorName; - nested?: boolean; - packagePath: string; - targetDir: string; - templateFilename: string; -}): Promise { - const templatePath = join( - dirname(fileURLToPath(import.meta.url)), - "../templates", - generatorName, - templateFilename, - ); - - const templateContent = await readFile(templatePath, "utf-8"); - const template = Handlebars.compile(templateContent); - - const packageJson = await readJson(join(packagePath, "package.json")); - const filePath = await generateFilePath( - packageJson, - packagePath, - targetDir, - entityName + (nested ? "/index" : "") + parse(templateFilename).ext, - customTargetPath, - ); - - const fileParsed = parse(filePath); - const name = { - camel: camelCase(entityName), - pascal: pascalCase(entityName), - path: pathCase(entityName), - }; - - const file: GeneratorFile = { - base: fileParsed.base, - content: template({ - name: { - ...name, - pathMaybeQuoted: /(-|\/)/.test(name.path) - ? `"${name.path}"` - : name.path, - signature: name.pascal + "Signature", - }, - package: packageJson, - }), - dir: fileParsed.dir, - ext: fileParsed.ext, - name: fileParsed.name, - path: filePath, - root: fileParsed.root, - }; - - await ensureDir(file.dir); - await writeFile(file.path, file.content); - - consola.success( - `🫚 Generated ${generatorName} \`${entityName}\` at \`${relative(cwd(), file.path)}\`.`, - ); - - const config = await resolveConfig(packagePath); - const postGenerate = config.hooks?.postGenerate; - - if (postGenerate) { - consola.success("🫚 `hooks.postGenerate`: Running..."); - - await postGenerate({ - entityName, - files: [file], - generatorName, - }); - - consola.success("🫚 `hooks.postGenerate`: Done!"); - } -} - -const SRC_DIR: Record = { - APP: "app", - V1_ADDON: "addon", - V2_ADDON: "src", -}; - -export async function generateFilePath( - packageJson: EmberPackageJson, - packagePath: string, - targetDir: string, - fileBase: string, - customTargetPath?: string, -): Promise { - if (customTargetPath) { - if (isAbsolute(customTargetPath)) { - return join(customTargetPath, fileBase); - } else { - return join(packagePath, customTargetPath, fileBase); - } - } - - const srcDir = isV2Addon(packageJson) - ? SRC_DIR.V2_ADDON - : isV1Addon(packageJson) - ? SRC_DIR.V1_ADDON - : SRC_DIR.APP; - - return join(packagePath, srcDir, targetDir, fileBase); -} diff --git a/src/generator.ts b/src/generator.ts new file mode 100644 index 0000000..2f3db9b --- /dev/null +++ b/src/generator.ts @@ -0,0 +1,300 @@ +import { camelCase, pascalCase, pathCase } from "change-case"; +import consola from "consola"; +import { ensureDir, readJson } from "fs-extra/esm"; +import Handlebars from "handlebars"; +import { readFile, writeFile } from "node:fs/promises"; +import { dirname, join, relative } from "node:path"; +import { cwd, stdout } from "node:process"; +import { fileURLToPath } from "node:url"; +import { resolveConfig, type Config } from "./config.js"; +import { FileReference } from "./file-reference.js"; +import { isV1Addon, isV2Addon } from "./helpers.js"; +import type { EmberPackageJson, GeneratorFile } from "./types.js"; + +export type Generator = { + args: GeneratorArg[]; + description: string; + name: string; + run: (args: Args) => Promise; +}; + +type GeneratorOptions = { + args: GeneratorArgFactory[]; + description?: string; + modifyTargetFile?: ModifyTargetFile; + modifyTemplateFile?: ModifyTemplateFile; + name: string; +}; + +type GeneratorArgFactory = (generatorName: string) => GeneratorArg; + +type GeneratorArg = { + alias?: string[]; + description: string; + modifyTargetFile?: ModifyTargetFile; + modifyTemplateFile?: ModifyTemplateFile; + name: string; + required?: boolean; + type: "boolean" | "positional" | "string"; +}; + +type ModifyTargetFile = (targetFile: FileReference, args: Args) => void; +type ModifyTemplateFile = (templateFile: FileReference, args: Args) => void; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Args = Record; + +export function defineGenerator({ + args, + description, + modifyTargetFile, + modifyTemplateFile, + name, +}: GeneratorOptions): Generator { + const generatorName = name; + const generatorArgs = [log(), ...args] + .map((argFactory) => argFactory(generatorName)) + .sort((a, b) => a.name.localeCompare(b.name)); + + async function run(args: Args): Promise { + const packagePath = cwd(); + const packageJson: EmberPackageJson = await readJson( + join(packagePath, "package.json"), + ); + + const config = await resolveConfig(packagePath); + const resolvedArgs = resolveArgs( + config, + generatorName, + generatorArgs, + args, + ); + + const entityName: string = resolvedArgs.name; + const entityPath: string | undefined = resolvedArgs.path; + + const targetFile = new FileReference({ + ext: ".ts", + name: entityName, + rootDir: packagePath, + subDir: entityPath ?? "", + }); + + const templateFile = new FileReference({ + ext: ".ts", + name: generatorName, + rootDir: join(dirname(fileURLToPath(import.meta.url)), "..", "templates"), + subDir: generatorName, + }); + + modifyTargetFile?.(targetFile, resolvedArgs); + modifyTemplateFile?.(templateFile, resolvedArgs); + + if (targetFile.subDir === "") { + targetFile.subDir = join(getSrcDir(packageJson), generatorName + "s"); + } + + for (const arg of generatorArgs) { + arg.modifyTargetFile?.(targetFile, resolvedArgs); + arg.modifyTemplateFile?.(templateFile, resolvedArgs); + } + + const templateContent = await readFile(templateFile.path(), "utf-8"); + const template = Handlebars.compile(templateContent); + + const entityNameCases = { + camel: camelCase(entityName), + pascal: pascalCase(entityName), + path: pathCase(entityName), + }; + + const templateCompiled = template({ + name: { + ...entityNameCases, + pathMaybeQuoted: /(-|\/)/.test(entityNameCases.path) + ? `"${entityNameCases.path}"` + : entityNameCases.path, + signature: entityNameCases.pascal + "Signature", + }, + package: packageJson, + }); + + if (resolvedArgs.log) { + const border = "─".repeat(stdout.columns ? stdout.columns / 2 : 120); + + consola.log(border); + consola.log(""); + consola.log(templateCompiled); + consola.log(border); + } else { + const targetFileParsed = targetFile.parse(); + const generatorFile: GeneratorFile = { + base: targetFileParsed.base, + content: templateCompiled, + dir: targetFileParsed.dir, + ext: targetFileParsed.ext, + name: targetFileParsed.name, + path: targetFile.path(), + root: targetFileParsed.root, + }; + + await ensureDir(generatorFile.dir); + await writeFile(generatorFile.path, generatorFile.content); + + consola.success( + `🫚 Generated ${generatorName} \`${entityName}\` at \`${relative(packagePath, generatorFile.path)}\`.`, + ); + + const postGenerate = config.hooks?.postGenerate; + + if (postGenerate) { + consola.success("🫚 `hooks.postGenerate`: Running..."); + + await postGenerate({ + entityName, + files: [generatorFile], + generatorName, + }); + + consola.success("🫚 `hooks.postGenerate`: Done!"); + } + } + } + + return { + args: generatorArgs, + description: description ?? `Generate a new ${generatorName}`, + name: generatorName, + run, + }; +} + +export function defineTestGenerator( + options: GeneratorOptions & { testsDir: string }, +): Generator { + return defineGenerator({ + ...options, + modifyTargetFile: (targetFile, args) => { + if (args.path === undefined) { + targetFile.subDir = join("tests", options.testsDir, options.name + "s"); + } + + targetFile.name += "-test"; + }, + name: `${options.name}-test`, + }); +} + +export function classBased({ + functionBasedName = "function-based", +}: { functionBasedName?: string } = {}): GeneratorArgFactory { + return (generatorName) => ({ + alias: ["class", "class-based"], + description: `Generate a \`class-based\` ${generatorName}, instead of a \`${functionBasedName}\` ${generatorName}`, + modifyTemplateFile: (templateFile, args): void => { + templateFile.name = [ + templateFile.name, + args.classBased ? "class-based" : functionBasedName, + ].join("."); + }, + name: "classBased", + type: "boolean", + }); +} + +export function log(): GeneratorArgFactory { + return (generatorName) => ({ + description: `Log the generated ${generatorName} to the console, instead of writing it to disk`, + name: "log", + type: "boolean", + }); +} + +export function name(): GeneratorArgFactory { + return (generatorName) => ({ + description: `The ${generatorName}'s name`, + name: "name", + required: true, + type: "positional", + }); +} + +export function nested({ + description, +}: { + description: string; +}): GeneratorArgFactory { + return () => ({ + description, + modifyTargetFile: (targetFile, args): void => { + if (args.nested) { + targetFile.subDir = join(targetFile.subDir, targetFile.name); + targetFile.name = "index"; + } + }, + name: "nested", + type: "boolean", + }); +} + +export function path(): GeneratorArgFactory { + return (generatorName) => ({ + description: `Generate a ${generatorName} at a custom path, e.g. \`--path=src/-private\``, + name: "path", + type: "string", + }); +} + +export function typescript({ + gts = false, +}: { + gts?: boolean; +} = {}): GeneratorArgFactory { + const jsExt = gts ? ".gjs" : ".js"; + const tsExt = gts ? ".gts" : ".ts"; + + return (generatorName) => ({ + alias: [...(gts ? ["gts"] : []), "ts"], + description: `Generate a \`${tsExt}\` ${generatorName}, instead of a \`${jsExt}\` ${generatorName}`, + modifyTargetFile: (targetFile, args): void => { + targetFile.ext = args.typescript ? tsExt : jsExt; + }, + modifyTemplateFile: (templateFile, args): void => { + templateFile.ext = args.typescript ? tsExt : jsExt; + }, + name: "typescript", + type: "boolean", + }); +} + +function getSrcDir(packageJson: EmberPackageJson): string { + return isV2Addon(packageJson) + ? "src" + : isV1Addon(packageJson) + ? "addon" + : "app"; +} + +function resolveArgs( + config: Config, + generatorName: string, + generatorArgs: Generator["args"], + args: Args, +): Args { + const generatorConfig = + config.generators?.[generatorName as keyof Config["generators"]]; + + const resolvedArgs: Args = { + typescript: config.typescript, + }; + + for (const arg of generatorArgs) { + if (args[arg.name] !== undefined) { + resolvedArgs[arg.name] = args[arg.name]; + } else if (generatorConfig?.[arg.name] !== undefined) { + resolvedArgs[arg.name] = generatorConfig[arg.name]; + } + } + + return resolvedArgs; +} diff --git a/src/generators.ts b/src/generators.ts index c99716d..3a7ee32 100644 --- a/src/generators.ts +++ b/src/generators.ts @@ -1,100 +1,65 @@ -import { generate } from "./generate.js"; +import { + classBased, + defineGenerator, + defineTestGenerator, + name, + nested, + path, + typescript, + type Generator, +} from "./generator.js"; -export function generateComponent( - name: string, - packagePath: string, - { - classBased = false, - nested, - path, - typescript = false, - }: { - classBased?: boolean; - nested?: boolean; - path?: string; - typescript?: boolean; - } = {}, -): Promise { - return generate({ - customTargetPath: path, - entityName: name, - generatorName: "component", - nested, - packagePath, - targetDir: "components", - templateFilename: - (classBased ? "class-based" : "template-only") + - (typescript ? ".gts" : ".gjs"), - }); -} +export const generators: Generator[] = [ + defineGenerator({ + args: [ + classBased({ functionBasedName: "template-only" }), + name(), + nested({ + description: + "Generate a nested colocated component, e.g. `foo/bar/index.gts`", + }), + path(), + typescript({ gts: true }), + ], + name: "component", + }), -export function generateHelper( - name: string, - packagePath: string, - { - classBased = false, - path, - typescript = false, - }: { - classBased?: boolean; - path?: string; - typescript?: boolean; - } = {}, -): Promise { - return generate({ - customTargetPath: path, - entityName: name, - generatorName: "helper", - packagePath, - targetDir: "helpers", - templateFilename: - (classBased ? "class-based" : "function-based") + - (typescript ? ".ts" : ".js"), - }); -} + defineTestGenerator({ + args: [name(), path(), typescript({ gts: true })], + name: "component", + testsDir: "integration", + }), -export function generateModifier( - name: string, - packagePath: string, - { - classBased = false, - path, - typescript = false, - }: { - classBased?: boolean; - path?: string; - typescript?: boolean; - } = {}, -): Promise { - return generate({ - customTargetPath: path, - entityName: name, - generatorName: "modifier", - packagePath, - targetDir: "modifiers", - templateFilename: - (classBased ? "class-based" : "function-based") + - (typescript ? ".ts" : ".js"), - }); -} + defineGenerator({ + args: [classBased(), name(), path(), typescript()], + name: "helper", + }), -export function generateService( - name: string, - packagePath: string, - { - path, - typescript = false, - }: { - path?: string; - typescript?: boolean; - } = {}, -): Promise { - return generate({ - customTargetPath: path, - entityName: name, - generatorName: "service", - packagePath, - targetDir: "services", - templateFilename: typescript ? "service.ts" : "service.js", - }); -} + defineTestGenerator({ + args: [name(), path(), typescript({ gts: true })], + name: "helper", + testsDir: "integration", + }), + + defineGenerator({ + args: [classBased(), name(), path(), typescript()], + name: "modifier", + }), + + defineTestGenerator({ + args: [name(), path(), typescript({ gts: true })], + name: "modifier", + testsDir: "integration", + }), + + defineGenerator({ + args: [name(), path(), typescript()], + name: "service", + }), + + defineTestGenerator({ + args: [name(), path(), typescript()], + name: "service", + testsDir: "unit", + }), +]; diff --git a/src/types.ts b/src/types.ts index 4f38072..8b716a0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,5 +19,3 @@ export type GeneratorFile = { path: string; root: string; }; - -export type GeneratorName = "component" | "helper" | "modifier" | "service"; diff --git a/templates/component-test/component-test.gjs b/templates/component-test/component-test.gjs new file mode 100644 index 0000000..da45d40 --- /dev/null +++ b/templates/component-test/component-test.gjs @@ -0,0 +1,24 @@ +import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import {{name.pascal}} from '{{package.name}}/components/{{name.path}}'; +import { setupRenderingTest } from '{{package.name}}/tests/helpers'; + +module('Integration | Component | {{name.pascal}}', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + await render(); + + assert.dom().hasText(''); + + await render( + + ); + + assert.dom().hasText('template block text'); + }); +}); diff --git a/templates/component-test/component-test.gts b/templates/component-test/component-test.gts new file mode 100644 index 0000000..da45d40 --- /dev/null +++ b/templates/component-test/component-test.gts @@ -0,0 +1,24 @@ +import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import {{name.pascal}} from '{{package.name}}/components/{{name.path}}'; +import { setupRenderingTest } from '{{package.name}}/tests/helpers'; + +module('Integration | Component | {{name.pascal}}', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + await render(); + + assert.dom().hasText(''); + + await render( + + ); + + assert.dom().hasText('template block text'); + }); +}); diff --git a/templates/component/class-based.gjs b/templates/component/component.class-based.gjs similarity index 100% rename from templates/component/class-based.gjs rename to templates/component/component.class-based.gjs diff --git a/templates/component/class-based.gts b/templates/component/component.class-based.gts similarity index 100% rename from templates/component/class-based.gts rename to templates/component/component.class-based.gts diff --git a/templates/component/template-only.gjs b/templates/component/component.template-only.gjs similarity index 100% rename from templates/component/template-only.gjs rename to templates/component/component.template-only.gjs diff --git a/templates/component/template-only.gts b/templates/component/component.template-only.gts similarity index 100% rename from templates/component/template-only.gts rename to templates/component/component.template-only.gts diff --git a/templates/helper-test/helper-test.gjs b/templates/helper-test/helper-test.gjs new file mode 100644 index 0000000..e07a681 --- /dev/null +++ b/templates/helper-test/helper-test.gjs @@ -0,0 +1,15 @@ +import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from '{{package.name}}/tests/helpers'; + +module('Integration | Helper | {{name.camel}}', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + const inputValue = '1234'; + + await render(); + + assert.dom().hasText('1234'); + }); +}); diff --git a/templates/helper-test/helper-test.gts b/templates/helper-test/helper-test.gts new file mode 100644 index 0000000..e07a681 --- /dev/null +++ b/templates/helper-test/helper-test.gts @@ -0,0 +1,15 @@ +import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from '{{package.name}}/tests/helpers'; + +module('Integration | Helper | {{name.camel}}', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + const inputValue = '1234'; + + await render(); + + assert.dom().hasText('1234'); + }); +}); diff --git a/templates/helper/class-based.js b/templates/helper/helper.class-based.js similarity index 100% rename from templates/helper/class-based.js rename to templates/helper/helper.class-based.js diff --git a/templates/helper/class-based.ts b/templates/helper/helper.class-based.ts similarity index 100% rename from templates/helper/class-based.ts rename to templates/helper/helper.class-based.ts diff --git a/templates/helper/function-based.js b/templates/helper/helper.function-based.js similarity index 100% rename from templates/helper/function-based.js rename to templates/helper/helper.function-based.js diff --git a/templates/helper/function-based.ts b/templates/helper/helper.function-based.ts similarity index 100% rename from templates/helper/function-based.ts rename to templates/helper/helper.function-based.ts diff --git a/templates/modifier-test/modifier-test.gjs b/templates/modifier-test/modifier-test.gjs new file mode 100644 index 0000000..8826f8d --- /dev/null +++ b/templates/modifier-test/modifier-test.gjs @@ -0,0 +1,13 @@ +import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from '{{package.name}}/tests/helpers'; + +module('Integration | Modifier | {{name.camel}}', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + await render(); + + assert.ok(true); + }); +}); diff --git a/templates/modifier-test/modifier-test.gts b/templates/modifier-test/modifier-test.gts new file mode 100644 index 0000000..8826f8d --- /dev/null +++ b/templates/modifier-test/modifier-test.gts @@ -0,0 +1,13 @@ +import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from '{{package.name}}/tests/helpers'; + +module('Integration | Modifier | {{name.camel}}', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + await render(); + + assert.ok(true); + }); +}); diff --git a/templates/modifier/class-based.js b/templates/modifier/modifier.class-based.js similarity index 100% rename from templates/modifier/class-based.js rename to templates/modifier/modifier.class-based.js diff --git a/templates/modifier/class-based.ts b/templates/modifier/modifier.class-based.ts similarity index 100% rename from templates/modifier/class-based.ts rename to templates/modifier/modifier.class-based.ts diff --git a/templates/modifier/function-based.js b/templates/modifier/modifier.function-based.js similarity index 100% rename from templates/modifier/function-based.js rename to templates/modifier/modifier.function-based.js diff --git a/templates/modifier/function-based.ts b/templates/modifier/modifier.function-based.ts similarity index 100% rename from templates/modifier/function-based.ts rename to templates/modifier/modifier.function-based.ts diff --git a/templates/service-test/service-test.js b/templates/service-test/service-test.js new file mode 100644 index 0000000..d935a9e --- /dev/null +++ b/templates/service-test/service-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from '{{package.name}}/tests/helpers'; + +module('Unit | Service | {{name.pascal}}', function (hooks) { + setupTest(hooks); + + test('it exists', function (assert) { + const service = this.owner.lookup('service:{{name.path}}'); + + assert.ok(service); + }); +}); diff --git a/templates/service-test/service-test.ts b/templates/service-test/service-test.ts new file mode 100644 index 0000000..d935a9e --- /dev/null +++ b/templates/service-test/service-test.ts @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from '{{package.name}}/tests/helpers'; + +module('Unit | Service | {{name.pascal}}', function (hooks) { + setupTest(hooks); + + test('it exists', function (assert) { + const service = this.owner.lookup('service:{{name.path}}'); + + assert.ok(service); + }); +}); diff --git a/test/__snapshots__/component-test.test.ts.snap b/test/__snapshots__/component-test.test.ts.snap new file mode 100644 index 0000000..0a86559 --- /dev/null +++ b/test/__snapshots__/component-test.test.ts.snap @@ -0,0 +1,113 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`generates a \`.gjs\` component-test 1`] = ` +"import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import Foo from 'v2-addon/components/foo'; +import { setupRenderingTest } from 'v2-addon/tests/helpers'; + +module('Integration | Component | Foo', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + await render(); + + assert.dom().hasText(''); + + await render( + + ); + + assert.dom().hasText('template block text'); + }); +}); +" +`; + +exports[`generates a \`.gjs\` component-test at a custom path 1`] = ` +"import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import Foo from 'v2-addon/components/foo'; +import { setupRenderingTest } from 'v2-addon/tests/helpers'; + +module('Integration | Component | Foo', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + await render(); + + assert.dom().hasText(''); + + await render( + + ); + + assert.dom().hasText('template block text'); + }); +}); +" +`; + +exports[`generates a \`.gts\` component-test 1`] = ` +"import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import Foo from 'v2-addon/components/foo'; +import { setupRenderingTest } from 'v2-addon/tests/helpers'; + +module('Integration | Component | Foo', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + await render(); + + assert.dom().hasText(''); + + await render( + + ); + + assert.dom().hasText('template block text'); + }); +}); +" +`; + +exports[`generates a \`.gts\` component-test at a custom path 1`] = ` +"import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import Foo from 'v2-addon/components/foo'; +import { setupRenderingTest } from 'v2-addon/tests/helpers'; + +module('Integration | Component | Foo', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + await render(); + + assert.dom().hasText(''); + + await render( + + ); + + assert.dom().hasText('template block text'); + }); +}); +" +`; diff --git a/test/__snapshots__/generate-component.test.ts.snap b/test/__snapshots__/component.test.ts.snap similarity index 100% rename from test/__snapshots__/generate-component.test.ts.snap rename to test/__snapshots__/component.test.ts.snap diff --git a/test/__snapshots__/config.test.ts.snap b/test/__snapshots__/config.test.ts.snap index bbf51d4..ec1b8fb 100644 --- a/test/__snapshots__/config.test.ts.snap +++ b/test/__snapshots__/config.test.ts.snap @@ -24,13 +24,13 @@ exports[`runs the \`postGenerate\` hook 1`] = ` "entityName": "foo", "files": [ { - "base": "foo.gjs", - "content": "\\n", - "dir": "test/output/post-generate-info/src/components", - "ext": ".gjs", + "base": "foo.gts", + "content": "import Component from \\"@glimmer/component\\";\\n\\nexport interface FooSignature {\\n Args: {};\\n Blocks: {\\n default: [];\\n };\\n Element: null;\\n}\\n\\nexport default class Foo extends Component {\\n \\n}\\n", + "dir": "/src/components", + "ext": ".gts", "name": "foo", - "path": "post-generate-info/src/components/foo.gjs", - "root": "" + "path": "/src/components/foo.gts", + "root": "/" } ], "generatorName": "component" diff --git a/test/__snapshots__/helper-test.test.ts.snap b/test/__snapshots__/helper-test.test.ts.snap new file mode 100644 index 0000000..c6486fc --- /dev/null +++ b/test/__snapshots__/helper-test.test.ts.snap @@ -0,0 +1,77 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`generates a \`.gjs\` helper-test 1`] = ` +"import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'v2-addon/tests/helpers'; + +module('Integration | Helper | foo', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + const inputValue = '1234'; + + await render(); + + assert.dom().hasText('1234'); + }); +}); +" +`; + +exports[`generates a \`.gjs\` helper-test at a custom path 1`] = ` +"import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'v2-addon/tests/helpers'; + +module('Integration | Helper | foo', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + const inputValue = '1234'; + + await render(); + + assert.dom().hasText('1234'); + }); +}); +" +`; + +exports[`generates a \`.gts\` helper-test 1`] = ` +"import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'v2-addon/tests/helpers'; + +module('Integration | Helper | foo', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + const inputValue = '1234'; + + await render(); + + assert.dom().hasText('1234'); + }); +}); +" +`; + +exports[`generates a \`.gts\` helper-test at a custom path 1`] = ` +"import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'v2-addon/tests/helpers'; + +module('Integration | Helper | foo', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + const inputValue = '1234'; + + await render(); + + assert.dom().hasText('1234'); + }); +}); +" +`; diff --git a/test/__snapshots__/generate-helper.test.ts.snap b/test/__snapshots__/helper.test.ts.snap similarity index 100% rename from test/__snapshots__/generate-helper.test.ts.snap rename to test/__snapshots__/helper.test.ts.snap diff --git a/test/__snapshots__/modifier-test.test.ts.snap b/test/__snapshots__/modifier-test.test.ts.snap new file mode 100644 index 0000000..8d346cc --- /dev/null +++ b/test/__snapshots__/modifier-test.test.ts.snap @@ -0,0 +1,69 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`generates a \`.gjs\` modifier-test 1`] = ` +"import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'v2-addon/tests/helpers'; + +module('Integration | Modifier | foo', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + await render(); + + assert.ok(true); + }); +}); +" +`; + +exports[`generates a \`.gjs\` modifier-test at a custom path 1`] = ` +"import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'v2-addon/tests/helpers'; + +module('Integration | Modifier | foo', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + await render(); + + assert.ok(true); + }); +}); +" +`; + +exports[`generates a \`.gts\` modifier-test 1`] = ` +"import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'v2-addon/tests/helpers'; + +module('Integration | Modifier | foo', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + await render(); + + assert.ok(true); + }); +}); +" +`; + +exports[`generates a \`.gts\` modifier-test at a custom path 1`] = ` +"import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'v2-addon/tests/helpers'; + +module('Integration | Modifier | foo', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + await render(); + + assert.ok(true); + }); +}); +" +`; diff --git a/test/__snapshots__/generate-modifier.test.ts.snap b/test/__snapshots__/modifier.test.ts.snap similarity index 100% rename from test/__snapshots__/generate-modifier.test.ts.snap rename to test/__snapshots__/modifier.test.ts.snap diff --git a/test/__snapshots__/service-test.test.ts.snap b/test/__snapshots__/service-test.test.ts.snap new file mode 100644 index 0000000..2c53c29 --- /dev/null +++ b/test/__snapshots__/service-test.test.ts.snap @@ -0,0 +1,65 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`generates a \`.js\` service-test 1`] = ` +"import { module, test } from 'qunit'; +import { setupTest } from 'v2-addon/tests/helpers'; + +module('Unit | Service | Foo', function (hooks) { + setupTest(hooks); + + test('it exists', function (assert) { + const service = this.owner.lookup('service:foo'); + + assert.ok(service); + }); +}); +" +`; + +exports[`generates a \`.js\` service-test at a custom path 1`] = ` +"import { module, test } from 'qunit'; +import { setupTest } from 'v2-addon/tests/helpers'; + +module('Unit | Service | Foo', function (hooks) { + setupTest(hooks); + + test('it exists', function (assert) { + const service = this.owner.lookup('service:foo'); + + assert.ok(service); + }); +}); +" +`; + +exports[`generates a \`.ts\` service-test 1`] = ` +"import { module, test } from 'qunit'; +import { setupTest } from 'v2-addon/tests/helpers'; + +module('Unit | Service | Foo', function (hooks) { + setupTest(hooks); + + test('it exists', function (assert) { + const service = this.owner.lookup('service:foo'); + + assert.ok(service); + }); +}); +" +`; + +exports[`generates a \`.ts\` service-test at a custom path 1`] = ` +"import { module, test } from 'qunit'; +import { setupTest } from 'v2-addon/tests/helpers'; + +module('Unit | Service | Foo', function (hooks) { + setupTest(hooks); + + test('it exists', function (assert) { + const service = this.owner.lookup('service:foo'); + + assert.ok(service); + }); +}); +" +`; diff --git a/test/__snapshots__/generate-service.test.ts.snap b/test/__snapshots__/service.test.ts.snap similarity index 100% rename from test/__snapshots__/generate-service.test.ts.snap rename to test/__snapshots__/service.test.ts.snap diff --git a/test/__snapshots__/generate.test.ts.snap b/test/__snapshots__/support.test.ts.snap similarity index 100% rename from test/__snapshots__/generate.test.ts.snap rename to test/__snapshots__/support.test.ts.snap diff --git a/test/component-test.test.ts b/test/component-test.test.ts new file mode 100644 index 0000000..3d2d52d --- /dev/null +++ b/test/component-test.test.ts @@ -0,0 +1,50 @@ +import { afterEach, it } from "vitest"; +import { Package } from "./helpers.ts"; + +let pkg: Package; + +afterEach(() => pkg.cleanUp()); + +it("generates a `.gjs` component-test", async (ctx) => { + pkg = await Package.create("v2-addon"); + + await pkg.gember("component-test", "foo"); + + const content = await pkg.readFile( + "tests/integration/components/foo-test.gjs", + ); + + ctx.expect(content).toMatchSnapshot(); +}); + +it("generates a `.gjs` component-test at a custom path", async (ctx) => { + pkg = await Package.create("v2-addon"); + + await pkg.gember("component-test", "foo", "--path=tests/foo"); + + const content = await pkg.readFile("tests/foo/foo-test.gjs"); + + ctx.expect(content).toMatchSnapshot(); +}); + +it("generates a `.gts` component-test", async (ctx) => { + pkg = await Package.create("v2-addon"); + + await pkg.gember("component-test", "foo", "--ts"); + + const content = await pkg.readFile( + "tests/integration/components/foo-test.gts", + ); + + ctx.expect(content).toMatchSnapshot(); +}); + +it("generates a `.gts` component-test at a custom path", async (ctx) => { + pkg = await Package.create("v2-addon"); + + await pkg.gember("component-test", "foo", "--path=tests/foo", "--ts"); + + const content = await pkg.readFile("tests/foo/foo-test.gts"); + + ctx.expect(content).toMatchSnapshot(); +}); diff --git a/test/generate-component.test.ts b/test/component.test.ts similarity index 76% rename from test/generate-component.test.ts rename to test/component.test.ts index 047473a..96648f8 100644 --- a/test/generate-component.test.ts +++ b/test/component.test.ts @@ -1,5 +1,4 @@ import { afterEach, it } from "vitest"; -import { generateComponent } from "../src/generators.ts"; import { Package } from "./helpers.ts"; let pkg: Package; @@ -9,7 +8,7 @@ afterEach(() => pkg.cleanUp()); it("generates a template-only `.gjs` component", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateComponent("foo", pkg.path); + await pkg.gember("component", "foo"); const content = await pkg.readFile("src/components/foo.gjs"); @@ -19,7 +18,7 @@ it("generates a template-only `.gjs` component", async (ctx) => { it("generates a class-based `.gjs` component", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateComponent("foo", pkg.path, { classBased: true }); + await pkg.gember("component", "foo", "--class"); const content = await pkg.readFile("src/components/foo.gjs"); @@ -29,7 +28,7 @@ it("generates a class-based `.gjs` component", async (ctx) => { it("generates a template-only `.gjs` component at a custom path", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateComponent("foo", pkg.path, { path: "src/-private" }); + await pkg.gember("component", "foo", "--path=src/-private"); const content = await pkg.readFile("src/-private/foo.gjs"); @@ -39,7 +38,7 @@ it("generates a template-only `.gjs` component at a custom path", async (ctx) => it("generates a template-only `.gts` component", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateComponent("foo", pkg.path, { typescript: true }); + await pkg.gember("component", "foo", "--ts"); const content = await pkg.readFile("src/components/foo.gts"); @@ -49,10 +48,7 @@ it("generates a template-only `.gts` component", async (ctx) => { it("generates a class-based `.gts` component", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateComponent("foo", pkg.path, { - classBased: true, - typescript: true, - }); + await pkg.gember("component", "foo", "--class", "--ts"); const content = await pkg.readFile("src/components/foo.gts"); @@ -62,10 +58,7 @@ it("generates a class-based `.gts` component", async (ctx) => { it("generates a template-only `.gts` component at a custom path", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateComponent("foo", pkg.path, { - path: "src/-private", - typescript: true, - }); + await pkg.gember("component", "foo", "--path=src/-private", "--ts"); const content = await pkg.readFile("src/-private/foo.gts"); @@ -75,7 +68,7 @@ it("generates a template-only `.gts` component at a custom path", async (ctx) => it("generates a nested template-only `.gjs` component", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateComponent("foo/bar", pkg.path); + await pkg.gember("component", "foo/bar"); const content = await pkg.readFile("src/components/foo/bar.gjs"); @@ -85,7 +78,7 @@ it("generates a nested template-only `.gjs` component", async (ctx) => { it("generates a nested colocated template-only `.gjs` component", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateComponent("foo/bar", pkg.path, { nested: true }); + await pkg.gember("component", "foo/bar", "--nested"); const content = await pkg.readFile("src/components/foo/bar/index.gjs"); diff --git a/test/config.test.ts b/test/config.test.ts index fa07bc8..561d90a 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -1,7 +1,6 @@ import { it } from "vitest"; import { resolveConfig } from "../src/config.js"; -import { generateComponent } from "../src/generators.ts"; -import { gember, Package } from "./helpers.js"; +import { Package } from "./helpers.js"; it("supports a `gember.config.js` file", async (ctx) => { const pkg = await Package.create("v2-addon-config-js"); @@ -30,7 +29,7 @@ it("supports a `gember.config.mjs` file", async (ctx) => { it("runs the `postGenerate` hook", async (ctx) => { const pkg = await Package.create("v2-addon-config", "post-generate-info"); - await generateComponent("foo", pkg.path); + await pkg.gember("component", "foo"); const content = await pkg.readFile("post-generate-info.json"); @@ -42,7 +41,7 @@ it("runs the `postGenerate` hook", async (ctx) => { it("applies specific generator options", async (ctx) => { const pkg = await Package.create("v2-addon-config"); - await gember(["component", "foo"], { cwd: pkg.path }); + await pkg.gember("component", "foo"); const content = await pkg.readFile("src/components/foo.gts"); diff --git a/test/helper-test.test.ts b/test/helper-test.test.ts new file mode 100644 index 0000000..d530972 --- /dev/null +++ b/test/helper-test.test.ts @@ -0,0 +1,46 @@ +import { afterEach, it } from "vitest"; +import { Package } from "./helpers.ts"; + +let pkg: Package; + +afterEach(() => pkg.cleanUp()); + +it("generates a `.gjs` helper-test", async (ctx) => { + pkg = await Package.create("v2-addon"); + + await pkg.gember("helper-test", "foo"); + + const content = await pkg.readFile("tests/integration/helpers/foo-test.gjs"); + + ctx.expect(content).toMatchSnapshot(); +}); + +it("generates a `.gjs` helper-test at a custom path", async (ctx) => { + pkg = await Package.create("v2-addon"); + + await pkg.gember("helper-test", "foo", "--path=tests/foo"); + + const content = await pkg.readFile("tests/foo/foo-test.gjs"); + + ctx.expect(content).toMatchSnapshot(); +}); + +it("generates a `.gts` helper-test", async (ctx) => { + pkg = await Package.create("v2-addon"); + + await pkg.gember("helper-test", "foo", "--ts"); + + const content = await pkg.readFile("tests/integration/helpers/foo-test.gts"); + + ctx.expect(content).toMatchSnapshot(); +}); + +it("generates a `.gts` helper-test at a custom path", async (ctx) => { + pkg = await Package.create("v2-addon"); + + await pkg.gember("helper-test", "foo", "--path=tests/foo", "--ts"); + + const content = await pkg.readFile("tests/foo/foo-test.gts"); + + ctx.expect(content).toMatchSnapshot(); +}); diff --git a/test/generate-helper.test.ts b/test/helper.test.ts similarity index 76% rename from test/generate-helper.test.ts rename to test/helper.test.ts index 061004e..46aad35 100644 --- a/test/generate-helper.test.ts +++ b/test/helper.test.ts @@ -1,5 +1,4 @@ import { afterEach, it } from "vitest"; -import { generateHelper } from "../src/generators.ts"; import { Package } from "./helpers.ts"; let pkg: Package; @@ -9,7 +8,7 @@ afterEach(() => pkg.cleanUp()); it("generates a function-based `.js` helper", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateHelper("foo", pkg.path); + await pkg.gember("helper", "foo"); const content = await pkg.readFile("src/helpers/foo.js"); @@ -19,7 +18,7 @@ it("generates a function-based `.js` helper", async (ctx) => { it("generates a class-based `.js` helper", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateHelper("foo", pkg.path, { classBased: true }); + await pkg.gember("helper", "foo", "--class"); const content = await pkg.readFile("src/helpers/foo.js"); @@ -29,7 +28,7 @@ it("generates a class-based `.js` helper", async (ctx) => { it("generates a function-based `.js` helper at a custom path", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateHelper("foo", pkg.path, { path: "src/-private" }); + await pkg.gember("helper", "foo", "--path=src/-private"); const content = await pkg.readFile("src/-private/foo.js"); @@ -39,7 +38,7 @@ it("generates a function-based `.js` helper at a custom path", async (ctx) => { it("generates a function-based `.ts` helper", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateHelper("foo", pkg.path, { typescript: true }); + await pkg.gember("helper", "foo", "--ts"); const content = await pkg.readFile("src/helpers/foo.ts"); @@ -49,7 +48,7 @@ it("generates a function-based `.ts` helper", async (ctx) => { it("generates a class-based `.ts` helper", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateHelper("foo", pkg.path, { classBased: true, typescript: true }); + await pkg.gember("helper", "foo", "--class", "--ts"); const content = await pkg.readFile("src/helpers/foo.ts"); @@ -59,10 +58,7 @@ it("generates a class-based `.ts` helper", async (ctx) => { it("generates a function-based `.ts` helper at a custom path", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateHelper("foo", pkg.path, { - path: "src/-private", - typescript: true, - }); + await pkg.gember("helper", "foo", "--path=src/-private", "--ts"); const content = await pkg.readFile("src/-private/foo.ts"); @@ -72,7 +68,7 @@ it("generates a function-based `.ts` helper at a custom path", async (ctx) => { it("generates a nested function-based `.js` helper", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateHelper("foo/bar", pkg.path); + await pkg.gember("helper", "foo/bar"); const content = await pkg.readFile("src/helpers/foo/bar.js"); diff --git a/test/helpers.ts b/test/helpers.ts index 1e9d2b6..c7590b3 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -1,5 +1,5 @@ import { execa } from "execa"; -import { remove } from "fs-extra"; +import { remove } from "fs-extra/esm"; import { readFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -13,8 +13,12 @@ export class Package { this.path = path; } - cleanUp(): Promise { - return remove(this.path); + async cleanUp(): Promise { + await remove(this.path); + } + + async gember(...args: string[]): Promise { + await gember(args, { cwd: this.path }); } readFile(path: string): Promise { @@ -22,7 +26,7 @@ export class Package { } static async create(name: string, path: string = uuidv4()): Promise { - const pkg = new this(join("test/output", path)); + const pkg = new this(join("test", "output", path)); await pkg.cleanUp(); await recursiveCopy(this.createPath(name), pkg.path); @@ -31,7 +35,7 @@ export class Package { } static createPath(name: string): string { - return join("test/packages", name); + return join("test", "packages", name); } } @@ -40,7 +44,7 @@ export async function gember( { cwd }: { cwd: string }, ): Promise { await execa( - join(dirname(fileURLToPath(import.meta.url)), "../bin/gember.js"), + join(dirname(fileURLToPath(import.meta.url)), "..", "bin", "gember.js"), args, { cwd }, ); diff --git a/test/modifier-test.test.ts b/test/modifier-test.test.ts new file mode 100644 index 0000000..545455c --- /dev/null +++ b/test/modifier-test.test.ts @@ -0,0 +1,50 @@ +import { afterEach, it } from "vitest"; +import { Package } from "./helpers.ts"; + +let pkg: Package; + +afterEach(() => pkg.cleanUp()); + +it("generates a `.gjs` modifier-test", async (ctx) => { + pkg = await Package.create("v2-addon"); + + await pkg.gember("modifier-test", "foo"); + + const content = await pkg.readFile( + "tests/integration/modifiers/foo-test.gjs", + ); + + ctx.expect(content).toMatchSnapshot(); +}); + +it("generates a `.gjs` modifier-test at a custom path", async (ctx) => { + pkg = await Package.create("v2-addon"); + + await pkg.gember("modifier-test", "foo", "--path=tests/foo"); + + const content = await pkg.readFile("tests/foo/foo-test.gjs"); + + ctx.expect(content).toMatchSnapshot(); +}); + +it("generates a `.gts` modifier-test", async (ctx) => { + pkg = await Package.create("v2-addon"); + + await pkg.gember("modifier-test", "foo", "--ts"); + + const content = await pkg.readFile( + "tests/integration/modifiers/foo-test.gts", + ); + + ctx.expect(content).toMatchSnapshot(); +}); + +it("generates a `.gts` modifier-test at a custom path", async (ctx) => { + pkg = await Package.create("v2-addon"); + + await pkg.gember("modifier-test", "foo", "--path=tests/foo", "--ts"); + + const content = await pkg.readFile("tests/foo/foo-test.gts"); + + ctx.expect(content).toMatchSnapshot(); +}); diff --git a/test/generate-modifier.test.ts b/test/modifier.test.ts similarity index 75% rename from test/generate-modifier.test.ts rename to test/modifier.test.ts index 56a04d5..a6f3dc8 100644 --- a/test/generate-modifier.test.ts +++ b/test/modifier.test.ts @@ -1,5 +1,4 @@ import { afterEach, it } from "vitest"; -import { generateModifier } from "../src/generators.ts"; import { Package } from "./helpers.ts"; let pkg: Package; @@ -9,7 +8,7 @@ afterEach(() => pkg.cleanUp()); it("generates a function-based `.js` modifier", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateModifier("foo", pkg.path); + await pkg.gember("modifier", "foo"); const content = await pkg.readFile("src/modifiers/foo.js"); @@ -19,7 +18,7 @@ it("generates a function-based `.js` modifier", async (ctx) => { it("generates a class-based `.js` modifier", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateModifier("foo", pkg.path, { classBased: true }); + await pkg.gember("modifier", "foo", "--class"); const content = await pkg.readFile("src/modifiers/foo.js"); @@ -29,7 +28,7 @@ it("generates a class-based `.js` modifier", async (ctx) => { it("generates a function-based `.js` modifier at a custom path", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateModifier("foo", pkg.path, { path: "src/-private" }); + await pkg.gember("modifier", "foo", "--path=src/-private"); const content = await pkg.readFile("src/-private/foo.js"); @@ -39,7 +38,7 @@ it("generates a function-based `.js` modifier at a custom path", async (ctx) => it("generates a function-based `.ts` modifier", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateModifier("foo", pkg.path, { typescript: true }); + await pkg.gember("modifier", "foo", "--ts"); const content = await pkg.readFile("src/modifiers/foo.ts"); @@ -49,10 +48,7 @@ it("generates a function-based `.ts` modifier", async (ctx) => { it("generates a class-based `.ts` modifier", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateModifier("foo", pkg.path, { - classBased: true, - typescript: true, - }); + await pkg.gember("modifier", "foo", "--class", "--ts"); const content = await pkg.readFile("src/modifiers/foo.ts"); @@ -62,10 +58,7 @@ it("generates a class-based `.ts` modifier", async (ctx) => { it("generates a function-based `.ts` modifier at a custom path", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateModifier("foo", pkg.path, { - path: "src/-private", - typescript: true, - }); + await pkg.gember("modifier", "foo", "--path=src/-private", "--ts"); const content = await pkg.readFile("src/-private/foo.ts"); @@ -75,7 +68,7 @@ it("generates a function-based `.ts` modifier at a custom path", async (ctx) => it("generates a nested function-based `.js` modifier", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateModifier("foo/bar", pkg.path); + await pkg.gember("modifier", "foo/bar"); const content = await pkg.readFile("src/modifiers/foo/bar.js"); diff --git a/test/packages/v2-addon-config/gember.config.mjs b/test/packages/v2-addon-config/gember.config.mjs index f022c13..4d27ef8 100644 --- a/test/packages/v2-addon-config/gember.config.mjs +++ b/test/packages/v2-addon-config/gember.config.mjs @@ -1,6 +1,7 @@ import { writeFile } from "node:fs/promises"; import { EOL } from "node:os"; -import { dirname, join, relative, sep } from "node:path"; +import { dirname, join, sep } from "node:path"; +import { cwd } from "node:process"; import { fileURLToPath } from "node:url"; /** @type {import('../../../src/config.ts').Config} */ @@ -20,11 +21,12 @@ export default { for (const file of info.files) { // Support Windows: - file.content = file.content.replace(EOL, "\n"); + file.content = file.content.replaceAll(EOL, "\n"); // Because the absolute path is different on each machine: - file.dir = file.dir.split(sep).join("/"); - file.path = relative("test/output", file.path).split(sep).join("/"); + file.dir = file.dir.replace(cwd(), "").split(sep).join("/"); + file.path = file.path.replace(cwd(), "").split(sep).join("/"); + file.root = "/"; } await writeFile(file, JSON.stringify(info, null, 2)); diff --git a/test/service-test.test.ts b/test/service-test.test.ts new file mode 100644 index 0000000..16a9785 --- /dev/null +++ b/test/service-test.test.ts @@ -0,0 +1,46 @@ +import { afterEach, it } from "vitest"; +import { Package } from "./helpers.ts"; + +let pkg: Package; + +afterEach(() => pkg.cleanUp()); + +it("generates a `.js` service-test", async (ctx) => { + pkg = await Package.create("v2-addon"); + + await pkg.gember("service-test", "foo"); + + const content = await pkg.readFile("tests/unit/services/foo-test.js"); + + ctx.expect(content).toMatchSnapshot(); +}); + +it("generates a `.js` service-test at a custom path", async (ctx) => { + pkg = await Package.create("v2-addon"); + + await pkg.gember("service-test", "foo", "--path=tests/foo"); + + const content = await pkg.readFile("tests/foo/foo-test.js"); + + ctx.expect(content).toMatchSnapshot(); +}); + +it("generates a `.ts` service-test", async (ctx) => { + pkg = await Package.create("v2-addon"); + + await pkg.gember("service-test", "foo", "--ts"); + + const content = await pkg.readFile("tests/unit/services/foo-test.ts"); + + ctx.expect(content).toMatchSnapshot(); +}); + +it("generates a `.ts` service-test at a custom path", async (ctx) => { + pkg = await Package.create("v2-addon"); + + await pkg.gember("service-test", "foo", "--path=tests/foo", "--ts"); + + const content = await pkg.readFile("tests/foo/foo-test.ts"); + + ctx.expect(content).toMatchSnapshot(); +}); diff --git a/test/generate-service.test.ts b/test/service.test.ts similarity index 75% rename from test/generate-service.test.ts rename to test/service.test.ts index 3ca3120..e1ad2af 100644 --- a/test/generate-service.test.ts +++ b/test/service.test.ts @@ -1,5 +1,4 @@ import { afterEach, it } from "vitest"; -import { generateService } from "../src/generators.ts"; import { Package } from "./helpers.ts"; let pkg: Package; @@ -9,7 +8,7 @@ afterEach(() => pkg.cleanUp()); it("generates a `.js` service", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateService("foo", pkg.path); + await pkg.gember("service", "foo"); const content = await pkg.readFile("src/services/foo.js"); @@ -19,7 +18,7 @@ it("generates a `.js` service", async (ctx) => { it("generates a `.ts` service", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateService("foo", pkg.path, { typescript: true }); + await pkg.gember("service", "foo", "--ts"); const content = await pkg.readFile("src/services/foo.ts"); @@ -29,7 +28,7 @@ it("generates a `.ts` service", async (ctx) => { it("generates a `.js` service at a custom path", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateService("foo", pkg.path, { path: "src/-private" }); + await pkg.gember("service", "foo", "--path=src/-private"); const content = await pkg.readFile("src/-private/foo.js"); @@ -39,10 +38,7 @@ it("generates a `.js` service at a custom path", async (ctx) => { it("generates a `.ts` service at a custom path", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateService("foo", pkg.path, { - path: "src/-private", - typescript: true, - }); + await pkg.gember("service", "foo", "--path=src/-private", "--ts"); const content = await pkg.readFile("src/-private/foo.ts"); @@ -52,7 +48,7 @@ it("generates a `.ts` service at a custom path", async (ctx) => { it("generates a nested `.js` service", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateService("foo/bar", pkg.path); + await pkg.gember("service", "foo/bar"); const content = await pkg.readFile("src/services/foo/bar.js"); @@ -62,7 +58,7 @@ it("generates a nested `.js` service", async (ctx) => { it("generates a nested `.ts` service", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateService("foo/bar", pkg.path, { typescript: true }); + await pkg.gember("service", "foo/bar", "--ts"); const content = await pkg.readFile("src/services/foo/bar.ts"); diff --git a/test/generate.test.ts b/test/support.test.ts similarity index 79% rename from test/generate.test.ts rename to test/support.test.ts index 7cd0766..e5a7e2b 100644 --- a/test/generate.test.ts +++ b/test/support.test.ts @@ -1,5 +1,4 @@ import { afterEach, it } from "vitest"; -import { generateComponent } from "../src/generators.ts"; import { Package } from "./helpers.ts"; let pkg: Package; @@ -9,7 +8,7 @@ afterEach(() => pkg.cleanUp()); it("supports v1 apps", async (ctx) => { pkg = await Package.create("v1-app"); - await generateComponent("foo", pkg.path); + await pkg.gember("component", "foo"); const content = await pkg.readFile("app/components/foo.gjs"); @@ -19,7 +18,7 @@ it("supports v1 apps", async (ctx) => { it("supports v2 apps", async (ctx) => { pkg = await Package.create("v2-app"); - await generateComponent("foo", pkg.path); + await pkg.gember("component", "foo"); const content = await pkg.readFile("app/components/foo.gjs"); @@ -29,7 +28,7 @@ it("supports v2 apps", async (ctx) => { it("supports v1 addons", async (ctx) => { pkg = await Package.create("v1-addon"); - await generateComponent("foo", pkg.path); + await pkg.gember("component", "foo"); const content = await pkg.readFile("addon/components/foo.gjs"); @@ -39,7 +38,7 @@ it("supports v1 addons", async (ctx) => { it("supports v2 addons", async (ctx) => { pkg = await Package.create("v2-addon"); - await generateComponent("foo", pkg.path); + await pkg.gember("component", "foo"); const content = await pkg.readFile("src/components/foo.gjs"); diff --git a/vitest.config.ts b/vitest.config.ts index 01e860e..9ee325a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -7,6 +7,6 @@ export default defineConfig({ }, }, test: { - forceRerunTriggers: ["**/templates/**"], + forceRerunTriggers: ["**/dist/**", "**/templates/**"], }, });