domain-driver 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -24
- package/dist/cli.js +5 -4
- package/dist/commands/action.js +19 -5
- package/dist/commands/controller-renderer.js +25 -0
- package/dist/commands/controller.js +6 -4
- package/dist/commands/feature.js +8 -3
- package/dist/commands/hints.js +10 -0
- package/dist/commands/hook-renderer.js +63 -0
- package/dist/commands/hook.js +21 -43
- package/dist/commands/repository.js +8 -1
- package/dist/commands/target.js +7 -3
- package/dist/init/content.js +9 -4
- package/dist/stack/detect.js +57 -11
- package/dist/stack/profiles/nest.js +1 -0
- package/dist/stack/profiles/next-frontend.js +1 -0
- package/dist/stack/profiles/next-fullstack.js +1 -0
- package/dist/stack/profiles/node.js +1 -0
- package/dist/stack/profiles/react.js +1 -0
- package/dist/stack/profiles/tanstack-start.js +46 -0
- package/dist/stack/registry.js +4 -1
- package/dist/stack/types.js +1 -1
- package/dist/templates/actions.js +12 -0
- package/dist/templates/controllers/server-fn.js +42 -0
- package/dist/templates/frontend/container.js +10 -6
- package/dist/templates/frontend/hook.js +63 -79
- package/dist/templates/frontend/query-hook.js +83 -0
- package/dist/templates/frontend/query-keys.js +15 -0
- package/dist/templates/frontend/route.js +33 -0
- package/dist/templates/frontend/server-fn-repository.js +27 -0
- package/package.json +6 -3
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderQueryHook = renderQueryHook;
|
|
4
|
+
const actions_1 = require("../actions");
|
|
5
|
+
const signatures_1 = require("../signatures");
|
|
6
|
+
const query_keys_1 = require("./query-keys");
|
|
7
|
+
function preamble(ctx, spec, entity, fromFile, imported) {
|
|
8
|
+
const servicePath = ctx.importLayer(fromFile, 'clientService', `${spec.name}.service`);
|
|
9
|
+
const keysPath = ctx.importLayer(fromFile, 'hook', `${ctx.feature}.keys`);
|
|
10
|
+
const domain = (0, signatures_1.domainImports)(ctx, fromFile, spec, entity);
|
|
11
|
+
return `import { ${imported} } from '@tanstack/react-query';
|
|
12
|
+
${domain.join('\n')}${domain.length > 0 ? '\n' : ''}import { ${spec.name}Service } from '${servicePath}';
|
|
13
|
+
import { ${(0, query_keys_1.keysConstant)(ctx.feature)} } from '${keysPath}';
|
|
14
|
+
|
|
15
|
+
const service = new ${spec.name}Service();
|
|
16
|
+
`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A custom GET action (e.g. `FindActiveCats`) has `usesId: false`, just like List. Testing
|
|
20
|
+
* only `usesId` would give it the exact same key as List's `all` — one cache entry shared by
|
|
21
|
+
* two hooks, with whichever observer mounts last winning the `queryFn`. `spec.path === '/'`
|
|
22
|
+
* is what actually distinguishes List from a custom action, so it is the second branch.
|
|
23
|
+
* The `[...all, name]` spread (rather than a bare `[feature, name]`) is deliberate: TanStack
|
|
24
|
+
* Query invalidates by key prefix, so a Create mutation invalidating `keys.all` still
|
|
25
|
+
* invalidates this custom list for free.
|
|
26
|
+
*/
|
|
27
|
+
function queryKey(ctx, spec) {
|
|
28
|
+
const keys = (0, query_keys_1.keysConstant)(ctx.feature);
|
|
29
|
+
if (spec.usesId)
|
|
30
|
+
return `${keys}.detail(id)`;
|
|
31
|
+
if (spec.path === '/')
|
|
32
|
+
return `${keys}.all`;
|
|
33
|
+
return `[...${keys}.all, '${spec.name}']`;
|
|
34
|
+
}
|
|
35
|
+
function renderQuery(ctx, spec, entity, fromFile) {
|
|
36
|
+
return `${preamble(ctx, spec, entity, fromFile, 'useQuery')}
|
|
37
|
+
export function use${spec.name}(${spec.params}) {
|
|
38
|
+
return useQuery({
|
|
39
|
+
queryKey: ${queryKey(ctx, spec)},
|
|
40
|
+
queryFn: (): ${spec.returns} => service.handle(${spec.args}),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
}
|
|
45
|
+
function mutationInput(spec) {
|
|
46
|
+
if (spec.usesId && spec.schema !== null) {
|
|
47
|
+
return {
|
|
48
|
+
signature: `({ id, data }: { id: string; data: ${spec.schema} })`,
|
|
49
|
+
call: 'id, data',
|
|
50
|
+
idFrom: '{ id }',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (spec.usesId)
|
|
54
|
+
return { signature: '(id: string)', call: 'id', idFrom: 'id' };
|
|
55
|
+
if (spec.schema !== null)
|
|
56
|
+
return { signature: `(data: ${spec.schema})`, call: 'data', idFrom: null };
|
|
57
|
+
return { signature: '()', call: '', idFrom: null };
|
|
58
|
+
}
|
|
59
|
+
function renderMutation(ctx, spec, entity, fromFile) {
|
|
60
|
+
const keys = (0, query_keys_1.keysConstant)(ctx.feature);
|
|
61
|
+
const { signature, call, idFrom } = mutationInput(spec);
|
|
62
|
+
const args = idFrom === null ? '()' : `(_result, ${idFrom})`;
|
|
63
|
+
const detail = idFrom === null
|
|
64
|
+
? ''
|
|
65
|
+
: `\n void queryClient.invalidateQueries({ queryKey: ${keys}.detail(id) });`;
|
|
66
|
+
return `${preamble(ctx, spec, entity, fromFile, 'useMutation, useQueryClient')}
|
|
67
|
+
export function use${spec.name}() {
|
|
68
|
+
const queryClient = useQueryClient();
|
|
69
|
+
|
|
70
|
+
return useMutation({
|
|
71
|
+
mutationFn: ${signature}: ${spec.returns} => service.handle(${call}),
|
|
72
|
+
onSuccess: ${args} => {
|
|
73
|
+
void queryClient.invalidateQueries({ queryKey: ${keys}.all });${detail}
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
function renderQueryHook(ctx, spec, entity, fromFile) {
|
|
80
|
+
return (0, actions_1.isQueryAction)(spec)
|
|
81
|
+
? renderQuery(ctx, spec, entity, fromFile)
|
|
82
|
+
: renderMutation(ctx, spec, entity, fromFile);
|
|
83
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.keysConstant = keysConstant;
|
|
4
|
+
exports.renderQueryKeys = renderQueryKeys;
|
|
5
|
+
const naming_1 = require("../../utils/naming");
|
|
6
|
+
function keysConstant(feature) {
|
|
7
|
+
return `${(0, naming_1.lowerFirst)((0, naming_1.toPascalCase)(feature))}Keys`;
|
|
8
|
+
}
|
|
9
|
+
function renderQueryKeys(feature) {
|
|
10
|
+
return `export const ${keysConstant(feature)} = {
|
|
11
|
+
all: ['${feature}'] as const,
|
|
12
|
+
detail: (id: string) => ['${feature}', id] as const,
|
|
13
|
+
};
|
|
14
|
+
`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderRoute = renderRoute;
|
|
4
|
+
function renderRoute(ctx, entity, fromFile, withContainer) {
|
|
5
|
+
const head = `import { createFileRoute } from '@tanstack/react-router';`;
|
|
6
|
+
const routeBlock = `export const Route = createFileRoute('/${ctx.feature}/')({
|
|
7
|
+
component: ${entity}Page,
|
|
8
|
+
});`;
|
|
9
|
+
if (!withContainer) {
|
|
10
|
+
return `${head}
|
|
11
|
+
|
|
12
|
+
${routeBlock}
|
|
13
|
+
|
|
14
|
+
function ${entity}Page() {
|
|
15
|
+
return (
|
|
16
|
+
<div>
|
|
17
|
+
<h1>${entity}</h1>
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
}
|
|
23
|
+
const containerPath = ctx.importLayer(fromFile, 'container', `${entity}Container`);
|
|
24
|
+
return `${head}
|
|
25
|
+
import ${entity}Container from '${containerPath}';
|
|
26
|
+
|
|
27
|
+
${routeBlock}
|
|
28
|
+
|
|
29
|
+
function ${entity}Page() {
|
|
30
|
+
return <${entity}Container />;
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderServerFnRepository = renderServerFnRepository;
|
|
4
|
+
const naming_1 = require("../../utils/naming");
|
|
5
|
+
const signatures_1 = require("../signatures");
|
|
6
|
+
function callArgument(spec) {
|
|
7
|
+
if (spec.usesId && spec.schema !== null)
|
|
8
|
+
return '({ data: { id, data } })';
|
|
9
|
+
if (spec.usesId)
|
|
10
|
+
return '({ data: id })';
|
|
11
|
+
if (spec.schema !== null)
|
|
12
|
+
return '({ data })';
|
|
13
|
+
return '()';
|
|
14
|
+
}
|
|
15
|
+
function renderServerFnRepository(ctx, spec, entity, fromFile) {
|
|
16
|
+
const fnName = (0, naming_1.lowerFirst)(spec.name);
|
|
17
|
+
const fnPath = ctx.importLayer(fromFile, 'controller', `${spec.name}.fn`);
|
|
18
|
+
const imports = [...(0, signatures_1.domainImports)(ctx, fromFile, spec, entity), `import { ${fnName} } from '${fnPath}';`];
|
|
19
|
+
return `${imports.join('\n')}
|
|
20
|
+
|
|
21
|
+
export class ${spec.name}Repository {
|
|
22
|
+
async handle(${spec.params}): ${spec.returns} {
|
|
23
|
+
return ${fnName}${callArgument(spec)};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "domain-driver",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CLI scaffolding tool for domain-driven feature folders in Next.js, React, Node, and
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "CLI scaffolding tool for domain-driven feature folders in Next.js, React, Node, NestJS, and TanStack Start projects, with per-action files, bespoke actions, and agent guidance",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"domain-driver": "./dist/index.js"
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"build": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\" && tsc",
|
|
19
19
|
"test": "vitest run",
|
|
20
20
|
"test:coverage": "vitest run --coverage",
|
|
21
|
+
"typecheck:tanstack-fixture": "node scripts/typecheck-tanstack-fixture.js",
|
|
21
22
|
"postinstall": "node ./scripts/postinstall.js"
|
|
22
23
|
},
|
|
23
24
|
"keywords": [
|
|
@@ -30,7 +31,9 @@
|
|
|
30
31
|
"node",
|
|
31
32
|
"express",
|
|
32
33
|
"fastify",
|
|
33
|
-
"hono"
|
|
34
|
+
"hono",
|
|
35
|
+
"tanstack",
|
|
36
|
+
"tanstack-start"
|
|
34
37
|
],
|
|
35
38
|
"author": "Isaac Hatilima",
|
|
36
39
|
"license": "MIT",
|