diff --git a/README.md b/README.md
index d7506e1..841622e 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`
@@ -106,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 generating a document.
+// 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/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..b6b1e8f 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",
@@ -26,11 +26,11 @@
"prepack": "tsc --project tsconfig.json",
"start": "pnpm build --watch",
"test": "vitest",
- "test:coverage": "vitest run --coverage"
+ "test:coverage": "pnpm build && 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",
@@ -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 65a9c5d..332f7b9 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
@@ -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 44196e8..eacdc0c 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -1,11 +1,15 @@
+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 { DocumentName } from "./types.js";
yargs(hideBin(process.argv))
.command({
@@ -21,28 +25,31 @@ 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(async () =>
+ generateComponent(
+ options.name,
+ cwd(),
+ await applyGemberConfig("component", {
+ classBased: options.classBased,
+ path: options.path,
+ typescript: options.typescript,
+ }),
+ ),
+ );
},
})
.command({
@@ -58,28 +65,31 @@ 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(async () =>
+ generateHelper(
+ options.name,
+ cwd(),
+ await applyGemberConfig("helper", {
+ classBased: options.classBased,
+ path: options.path,
+ typescript: options.typescript,
+ }),
+ ),
+ );
},
})
.command({
@@ -95,28 +105,31 @@ 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(async () =>
+ generateModifier(
+ options.name,
+ cwd(),
+ await applyGemberConfig("modifier", {
+ classBased: options.classBased,
+ path: options.path,
+ typescript: options.typescript,
+ }),
+ ),
+ );
},
})
.command({
@@ -131,24 +144,50 @@ 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(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(
+ 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 1c1de6d..8e70405 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,8 +1,31 @@
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 = {
+ 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;
@@ -10,6 +33,8 @@ export type Config = {
files: File[];
}) => Promise | void;
};
+
+ typescript?: boolean;
};
const CONFIG_FILES = [
@@ -18,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 Error(`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 Error(
- `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/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 60%
rename from src/generate-document.ts
rename to src/generate.ts
index c7e3af5..be28d20 100644
--- a/src/generate-document.ts
+++ b/src/generate.ts
@@ -1,39 +1,46 @@
-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 { resolveConfig } 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 resolveGeneratePath(
+ documentName,
+ packagePath,
+ path,
+ );
+
+ const files = await scaffdog.generate(document, generatePath, {
inputs: {
...inputs,
name: {
@@ -56,14 +63,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 resolveConfig(packagePath);
await config.hooks?.postGenerate?.({
documentName,
@@ -83,25 +88,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 resolveGeneratePath(
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/__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 {
+
+ {{yield}}
+
+}
+"
+`;
+
exports[`runs the \`postGenerate\` hook 1`] = `
"{
"documentName": "component",
diff --git a/test/config.test.ts b/test/config.test.ts
index 94b5cda..eb528d8 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 { resolveConfig } from "../src/config.js";
import { generateComponent } from "../src/generators.ts";
-import { copyBlueprint } from "./helpers.js";
+import { gember, Package } from "./helpers.js";
it("supports a `gember.config.js` file", async (ctx) => {
const project = new Project({
@@ -17,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: {} });
});
@@ -32,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: {} });
});
@@ -47,19 +44,31 @@ 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 cwd = await copyBlueprint("v2-addon-hooks", "post-generate-info");
+ const pkg = await Package.create("v2-addon-config", "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();
+});
+
+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-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..8e49d7e 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 { resolveGeneratePath } 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 resolveGeneratePath(
+ "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 resolveGeneratePath(
+ "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 resolveGeneratePath(
+ "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..4f9280a 100644
--- a/test/helpers.ts
+++ b/test/helpers.ts
@@ -1,20 +1,52 @@
-import { join } from "node:path";
+import { execa } from "execa";
+import { remove } from "fs-extra";
+import { readFile } from "node:fs/promises";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
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-config";
-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");
+ }
-export async function copyBlueprint(
- name: Blueprint,
- directory: string = uuidv4(),
-) {
- const cwd = join("test/output", directory);
+ static async create(
+ name: PackageName,
+ path: string = uuidv4(),
+ ): Promise {
+ const pkg = new this(join("test/output", path));
- await recursiveCopy(blueprintPath(name), cwd);
+ await pkg.cleanUp();
+ await recursiveCopy(this.createPath(name), pkg.path);
+
+ return pkg;
+ }
+
+ static createPath(name: PackageName): string {
+ return join("test/packages", name);
+ }
+}
- return cwd;
+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/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-config/gember.config.mjs
similarity index 75%
rename from test/blueprints/v2-addon-hooks/gember.config.js
rename to test/packages/v2-addon-config/gember.config.mjs
index b58f657..2356826 100644
--- a/test/blueprints/v2-addon-hooks/gember.config.js
+++ b/test/packages/v2-addon-config/gember.config.mjs
@@ -5,10 +5,18 @@ import { fileURLToPath } from "node:url";
/** @type {import('../../../src/config.ts').Config} */
export default {
+ generators: {
+ component: {
+ classBased: true,
+ },
+ },
+
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:
@@ -21,4 +29,6 @@ export default {
await writeFile(file, JSON.stringify(info, null, 2));
},
},
+
+ typescript: true,
};
diff --git a/test/blueprints/v2-addon-hooks/package.json b/test/packages/v2-addon-config/package.json
similarity index 85%
rename from test/blueprints/v2-addon-hooks/package.json
rename to test/packages/v2-addon-config/package.json
index 5b74278..ba02973 100644
--- a/test/blueprints/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"
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