@tailor-platform/erp-kit 0.0.1 → 0.1.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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +196 -28
- package/dist/cli.js +914 -0
- package/package.json +67 -8
- package/schemas/app-compose/actors.yml +34 -0
- package/schemas/app-compose/business-flow.yml +50 -0
- package/schemas/app-compose/requirements.yml +33 -0
- package/schemas/app-compose/resolver.yml +47 -0
- package/schemas/app-compose/screen.yml +81 -0
- package/schemas/app-compose/story.yml +67 -0
- package/schemas/module/command.yml +52 -0
- package/schemas/module/feature.yml +58 -0
- package/schemas/module/model.yml +70 -0
- package/schemas/module/module.yml +50 -0
- package/skills/1-module-docs/SKILL.md +111 -0
- package/skills/1-module-docs/references/structure.md +22 -0
- package/skills/2-module-feature-breakdown/SKILL.md +72 -0
- package/skills/2-module-feature-breakdown/references/commands.md +48 -0
- package/skills/2-module-feature-breakdown/references/models.md +29 -0
- package/skills/2-module-feature-breakdown/references/structure.md +22 -0
- package/skills/3-module-doc-review/SKILL.md +236 -0
- package/skills/3-module-doc-review/references/commands.md +54 -0
- package/skills/3-module-doc-review/references/models.md +29 -0
- package/skills/3-module-doc-review/references/testing.md +37 -0
- package/skills/4-module-tdd-implementation/SKILL.md +74 -0
- package/skills/4-module-tdd-implementation/references/commands.md +45 -0
- package/skills/4-module-tdd-implementation/references/db-relations.md +69 -0
- package/skills/4-module-tdd-implementation/references/errors.md +7 -0
- package/skills/4-module-tdd-implementation/references/exports.md +8 -0
- package/skills/4-module-tdd-implementation/references/models.md +30 -0
- package/skills/4-module-tdd-implementation/references/structure.md +22 -0
- package/skills/4-module-tdd-implementation/references/testing.md +37 -0
- package/skills/5-module-implementation-review/SKILL.md +408 -0
- package/skills/5-module-implementation-review/references/commands.md +45 -0
- package/skills/5-module-implementation-review/references/errors.md +7 -0
- package/skills/5-module-implementation-review/references/exports.md +8 -0
- package/skills/5-module-implementation-review/references/models.md +30 -0
- package/skills/5-module-implementation-review/references/testing.md +29 -0
- package/skills/app-compose-1-requirement-analysis/SKILL.md +89 -0
- package/skills/app-compose-1-requirement-analysis/references/structure.md +27 -0
- package/skills/app-compose-2-requirements-breakdown/SKILL.md +95 -0
- package/skills/app-compose-2-requirements-breakdown/references/screen-detailview.md +106 -0
- package/skills/app-compose-2-requirements-breakdown/references/screen-form.md +139 -0
- package/skills/app-compose-2-requirements-breakdown/references/screen-listview.md +153 -0
- package/skills/app-compose-2-requirements-breakdown/references/structure.md +27 -0
- package/skills/app-compose-3-doc-review/SKILL.md +116 -0
- package/skills/app-compose-3-doc-review/references/structure.md +27 -0
- package/skills/app-compose-4-design-mock/SKILL.md +256 -0
- package/skills/app-compose-4-design-mock/references/component.md +50 -0
- package/skills/app-compose-4-design-mock/references/screen-detailview.md +106 -0
- package/skills/app-compose-4-design-mock/references/screen-form.md +139 -0
- package/skills/app-compose-4-design-mock/references/screen-listview.md +153 -0
- package/skills/app-compose-4-design-mock/references/structure.md +27 -0
- package/skills/app-compose-5-design-mock-review/SKILL.md +290 -0
- package/skills/app-compose-5-design-mock-review/references/component.md +50 -0
- package/skills/app-compose-5-design-mock-review/references/screen-detailview.md +106 -0
- package/skills/app-compose-5-design-mock-review/references/screen-form.md +139 -0
- package/skills/app-compose-5-design-mock-review/references/screen-listview.md +153 -0
- package/skills/app-compose-6-implementation-spec/SKILL.md +127 -0
- package/skills/app-compose-6-implementation-spec/references/auth.md +72 -0
- package/skills/app-compose-6-implementation-spec/references/structure.md +27 -0
- package/skills/mock-scenario/SKILL.md +118 -0
- package/src/app.ts +1 -0
- package/src/cli.ts +120 -0
- package/src/commands/check.test.ts +30 -0
- package/src/commands/check.ts +66 -0
- package/src/commands/init.test.ts +88 -0
- package/src/commands/init.ts +120 -0
- package/src/commands/mock/index.ts +53 -0
- package/src/commands/mock/start.ts +179 -0
- package/src/commands/mock/validate.test.ts +185 -0
- package/src/commands/mock/validate.ts +198 -0
- package/src/commands/scaffold.test.ts +76 -0
- package/src/commands/scaffold.ts +119 -0
- package/src/commands/sync-check.test.ts +125 -0
- package/src/commands/sync-check.ts +182 -0
- package/src/integration.test.ts +63 -0
- package/src/mdschema.ts +48 -0
- package/src/mockServer.ts +55 -0
- package/src/module.ts +86 -0
- package/src/modules/accounting/.gitkeep +0 -0
- package/src/modules/coa-management/.gitkeep +0 -0
- package/src/modules/inventory/.gitkeep +0 -0
- package/src/modules/manufacturing/.gitkeep +0 -0
- package/src/modules/primitives/README.md +39 -0
- package/src/modules/primitives/command/activateCategory.test.ts +75 -0
- package/src/modules/primitives/command/activateCategory.ts +50 -0
- package/src/modules/primitives/command/activateCurrency.test.ts +70 -0
- package/src/modules/primitives/command/activateCurrency.ts +50 -0
- package/src/modules/primitives/command/activateUnit.test.ts +53 -0
- package/src/modules/primitives/command/activateUnit.ts +50 -0
- package/src/modules/primitives/command/convertAmount.test.ts +275 -0
- package/src/modules/primitives/command/convertAmount.ts +126 -0
- package/src/modules/primitives/command/convertQuantity.test.ts +219 -0
- package/src/modules/primitives/command/convertQuantity.ts +73 -0
- package/src/modules/primitives/command/createCategory.test.ts +126 -0
- package/src/modules/primitives/command/createCategory.ts +89 -0
- package/src/modules/primitives/command/createCurrency.test.ts +191 -0
- package/src/modules/primitives/command/createCurrency.ts +77 -0
- package/src/modules/primitives/command/createExchangeRate.test.ts +216 -0
- package/src/modules/primitives/command/createExchangeRate.ts +91 -0
- package/src/modules/primitives/command/createUnit.test.ts +214 -0
- package/src/modules/primitives/command/createUnit.ts +88 -0
- package/src/modules/primitives/command/deactivateCategory.test.ts +97 -0
- package/src/modules/primitives/command/deactivateCategory.ts +62 -0
- package/src/modules/primitives/command/deactivateCurrency.test.ts +85 -0
- package/src/modules/primitives/command/deactivateCurrency.ts +55 -0
- package/src/modules/primitives/command/deactivateUnit.test.ts +78 -0
- package/src/modules/primitives/command/deactivateUnit.ts +62 -0
- package/src/modules/primitives/command/setBaseCurrency.test.ts +98 -0
- package/src/modules/primitives/command/setBaseCurrency.ts +74 -0
- package/src/modules/primitives/command/setReferenceUnit.test.ts +108 -0
- package/src/modules/primitives/command/setReferenceUnit.ts +84 -0
- package/src/modules/primitives/db/currency.ts +30 -0
- package/src/modules/primitives/db/exchangeRate.ts +28 -0
- package/src/modules/primitives/db/unit.ts +32 -0
- package/src/modules/primitives/db/uomCategory.ts +32 -0
- package/src/modules/primitives/docs/commands/ActivateCategory.md +34 -0
- package/src/modules/primitives/docs/commands/ActivateCurrency.md +33 -0
- package/src/modules/primitives/docs/commands/ActivateUnit.md +34 -0
- package/src/modules/primitives/docs/commands/ConvertAmount.md +50 -0
- package/src/modules/primitives/docs/commands/ConvertQuantity.md +43 -0
- package/src/modules/primitives/docs/commands/CreateCategory.md +44 -0
- package/src/modules/primitives/docs/commands/CreateCurrency.md +47 -0
- package/src/modules/primitives/docs/commands/CreateExchangeRate.md +48 -0
- package/src/modules/primitives/docs/commands/CreateUnit.md +48 -0
- package/src/modules/primitives/docs/commands/DeactivateCategory.md +38 -0
- package/src/modules/primitives/docs/commands/DeactivateCurrency.md +38 -0
- package/src/modules/primitives/docs/commands/DeactivateUnit.md +38 -0
- package/src/modules/primitives/docs/commands/SetBaseCurrency.md +39 -0
- package/src/modules/primitives/docs/commands/SetReferenceUnit.md +43 -0
- package/src/modules/primitives/docs/features/currency-definitions.md +55 -0
- package/src/modules/primitives/docs/features/exchange-rates.md +61 -0
- package/src/modules/primitives/docs/features/unit-conversion.md +66 -0
- package/src/modules/primitives/docs/features/uom-categories.md +52 -0
- package/src/modules/primitives/docs/models/Currency.md +45 -0
- package/src/modules/primitives/docs/models/ExchangeRate.md +33 -0
- package/src/modules/primitives/docs/models/Unit.md +46 -0
- package/src/modules/primitives/docs/models/UoMCategory.md +44 -0
- package/src/modules/primitives/generated/kysely-tailordb.ts +95 -0
- package/src/modules/primitives/index.ts +40 -0
- package/src/modules/primitives/lib/errors.ts +138 -0
- package/src/modules/primitives/lib/types.ts +20 -0
- package/src/modules/primitives/module.ts +66 -0
- package/src/modules/primitives/permissions.ts +18 -0
- package/src/modules/primitives/tailor.config.ts +11 -0
- package/src/modules/primitives/testing/fixtures.ts +161 -0
- package/src/modules/product-management/.gitkeep +0 -0
- package/src/modules/purchase/.gitkeep +0 -0
- package/src/modules/sales/.gitkeep +0 -0
- package/src/modules/shared/createContext.test.ts +39 -0
- package/src/modules/shared/createContext.ts +15 -0
- package/src/modules/shared/defineCommand.test.ts +42 -0
- package/src/modules/shared/defineCommand.ts +19 -0
- package/src/modules/shared/definePermissions.test.ts +146 -0
- package/src/modules/shared/definePermissions.ts +94 -0
- package/src/modules/shared/entityTypes.ts +15 -0
- package/src/modules/shared/errors.ts +22 -0
- package/src/modules/shared/index.ts +1 -0
- package/src/modules/shared/internal.ts +13 -0
- package/src/modules/shared/requirePermission.test.ts +47 -0
- package/src/modules/shared/requirePermission.ts +8 -0
- package/src/modules/shared/types.ts +4 -0
- package/src/modules/supplier-management/.gitkeep +0 -0
- package/src/modules/supplier-portal/.gitkeep +0 -0
- package/src/modules/testing/index.ts +120 -0
- package/src/modules/user-management/README.md +38 -0
- package/src/modules/user-management/command/activateUser.test.ts +112 -0
- package/src/modules/user-management/command/activateUser.ts +67 -0
- package/src/modules/user-management/command/assignPermissionToRole.test.ts +119 -0
- package/src/modules/user-management/command/assignPermissionToRole.ts +87 -0
- package/src/modules/user-management/command/assignRoleToUser.test.ts +162 -0
- package/src/modules/user-management/command/assignRoleToUser.ts +93 -0
- package/src/modules/user-management/command/createPermission.test.ts +143 -0
- package/src/modules/user-management/command/createPermission.ts +66 -0
- package/src/modules/user-management/command/createRole.test.ts +115 -0
- package/src/modules/user-management/command/createRole.ts +52 -0
- package/src/modules/user-management/command/createUser.test.ts +198 -0
- package/src/modules/user-management/command/createUser.ts +85 -0
- package/src/modules/user-management/command/deactivateUser.test.ts +112 -0
- package/src/modules/user-management/command/deactivateUser.ts +67 -0
- package/src/modules/user-management/command/logAuditEvent.test.ts +179 -0
- package/src/modules/user-management/command/logAuditEvent.ts +59 -0
- package/src/modules/user-management/command/reactivateUser.test.ts +115 -0
- package/src/modules/user-management/command/reactivateUser.ts +67 -0
- package/src/modules/user-management/command/revokePermissionFromRole.test.ts +112 -0
- package/src/modules/user-management/command/revokePermissionFromRole.ts +81 -0
- package/src/modules/user-management/command/revokeRoleFromUser.test.ts +112 -0
- package/src/modules/user-management/command/revokeRoleFromUser.ts +81 -0
- package/src/modules/user-management/db/auditEvent.ts +47 -0
- package/src/modules/user-management/db/permission.ts +31 -0
- package/src/modules/user-management/db/role.ts +28 -0
- package/src/modules/user-management/db/rolePermission.ts +44 -0
- package/src/modules/user-management/db/user.ts +38 -0
- package/src/modules/user-management/db/userRole.ts +44 -0
- package/src/modules/user-management/docs/commands/ActivateUser.md +36 -0
- package/src/modules/user-management/docs/commands/AssignPermissionToRole.md +39 -0
- package/src/modules/user-management/docs/commands/AssignRoleToUser.md +43 -0
- package/src/modules/user-management/docs/commands/CreatePermission.md +35 -0
- package/src/modules/user-management/docs/commands/CreateRole.md +35 -0
- package/src/modules/user-management/docs/commands/CreateUser.md +41 -0
- package/src/modules/user-management/docs/commands/DeactivateUser.md +38 -0
- package/src/modules/user-management/docs/commands/LogAuditEvent.md +37 -0
- package/src/modules/user-management/docs/commands/ReactivateUser.md +37 -0
- package/src/modules/user-management/docs/commands/RevokePermissionFromRole.md +40 -0
- package/src/modules/user-management/docs/commands/RevokeRoleFromUser.md +40 -0
- package/src/modules/user-management/docs/features/audit-trail.md +80 -0
- package/src/modules/user-management/docs/features/role-based-access-control.md +76 -0
- package/src/modules/user-management/docs/features/user-account-management.md +64 -0
- package/src/modules/user-management/docs/models/AuditEvent.md +34 -0
- package/src/modules/user-management/docs/models/Permission.md +31 -0
- package/src/modules/user-management/docs/models/Role.md +31 -0
- package/src/modules/user-management/docs/models/RolePermission.md +33 -0
- package/src/modules/user-management/docs/models/User.md +47 -0
- package/src/modules/user-management/docs/models/UserRole.md +34 -0
- package/src/modules/user-management/docs/plans/2026-01-30-flattened-permissions-design.md +52 -0
- package/src/modules/user-management/executor/recomputeOnRolePermissionChange.ts +61 -0
- package/src/modules/user-management/generated/enums.ts +24 -0
- package/src/modules/user-management/generated/kysely-tailordb.ts +112 -0
- package/src/modules/user-management/index.ts +32 -0
- package/src/modules/user-management/lib/errors.ts +81 -0
- package/src/modules/user-management/lib/recomputeUserPermissions.ts +53 -0
- package/src/modules/user-management/lib/types.ts +31 -0
- package/src/modules/user-management/module.ts +77 -0
- package/src/modules/user-management/permissions.ts +15 -0
- package/src/modules/user-management/tailor.config.ts +11 -0
- package/src/modules/user-management/testing/fixtures.ts +98 -0
- package/src/schemas.ts +25 -0
- package/src/testing.ts +10 -0
- package/src/util.ts +3 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# ListView Screen Implementation
|
|
2
|
+
|
|
3
|
+
Implementation pattern for screens with `Screen Type: ListView`.
|
|
4
|
+
Assumes `page.md` and `component.md` rules.
|
|
5
|
+
|
|
6
|
+
## File Structure
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
{screen-path}/
|
|
10
|
+
├── components/
|
|
11
|
+
│ └── {screen-name}-table.tsx # Table component with fragments
|
|
12
|
+
└── page.tsx
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Page Component (page.tsx)
|
|
16
|
+
|
|
17
|
+
Data fetching and `Layout` must be co-located in the same page component.
|
|
18
|
+
Do NOT split into an inner Content component — `Layout` requires `Layout.Column` as direct children.
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
const ResourcesQuery = graphql(
|
|
22
|
+
`
|
|
23
|
+
query Resources {
|
|
24
|
+
resources {
|
|
25
|
+
...ResourceTable
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
`,
|
|
29
|
+
[ResourceTableFragment],
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const ResourcesPage = () => {
|
|
33
|
+
const [{ data, error, fetching }, reexecuteQuery] = useQuery({
|
|
34
|
+
query: ResourcesQuery,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (fetching) return <Loading />;
|
|
38
|
+
|
|
39
|
+
if (error || !data) {
|
|
40
|
+
return (
|
|
41
|
+
<ErrorFallback
|
|
42
|
+
title="Failed to load resources"
|
|
43
|
+
message="An error occurred while fetching the list."
|
|
44
|
+
onReset={() => reexecuteQuery({ requestPolicy: "network-only" })}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Layout
|
|
51
|
+
columns={1}
|
|
52
|
+
title="Resources"
|
|
53
|
+
actions={[
|
|
54
|
+
<Button key="create" asChild>
|
|
55
|
+
<Link to="create">Create</Link>
|
|
56
|
+
</Button>,
|
|
57
|
+
]}
|
|
58
|
+
>
|
|
59
|
+
<Layout.Column>
|
|
60
|
+
<ResourceTable data={data.resources} />
|
|
61
|
+
</Layout.Column>
|
|
62
|
+
</Layout>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Table Component (components/{screen-name}-table.tsx)
|
|
68
|
+
|
|
69
|
+
### Fragment Collocation
|
|
70
|
+
|
|
71
|
+
Define a row fragment and a table fragment wrapping the Connection type.
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
const ResourceRowFragment = graphql(`
|
|
75
|
+
fragment ResourceRow on Resource {
|
|
76
|
+
id
|
|
77
|
+
title
|
|
78
|
+
status
|
|
79
|
+
createdAt
|
|
80
|
+
}
|
|
81
|
+
`);
|
|
82
|
+
|
|
83
|
+
export const ResourceTableFragment = graphql(
|
|
84
|
+
`
|
|
85
|
+
fragment ResourceTable on ResourceConnection {
|
|
86
|
+
edges {
|
|
87
|
+
node {
|
|
88
|
+
...ResourceRow
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
`,
|
|
93
|
+
[ResourceRowFragment],
|
|
94
|
+
);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Row Component
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
const ResourceRow = ({ resource: resourceFragment }: ResourceRowProps) => {
|
|
101
|
+
const resource = readFragment(ResourceRowFragment, resourceFragment);
|
|
102
|
+
return (
|
|
103
|
+
<TableRow>
|
|
104
|
+
<TableCell>{resource.title}</TableCell>
|
|
105
|
+
<TableCell>
|
|
106
|
+
<Badge variant={resource.status === "ACTIVE" ? "default" : "secondary"}>
|
|
107
|
+
{resource.status}
|
|
108
|
+
</Badge>
|
|
109
|
+
</TableCell>
|
|
110
|
+
<TableCell>
|
|
111
|
+
<Button variant="ghost" size="sm" asChild>
|
|
112
|
+
<Link to={resource.id}>View</Link>
|
|
113
|
+
</Button>
|
|
114
|
+
</TableCell>
|
|
115
|
+
</TableRow>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Empty State
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
if (connection.edges.length === 0) {
|
|
124
|
+
return (
|
|
125
|
+
<EmptyState
|
|
126
|
+
title="No resources"
|
|
127
|
+
message="Get started by creating a new resource."
|
|
128
|
+
action={
|
|
129
|
+
<Button asChild>
|
|
130
|
+
<Link to="create">Create</Link>
|
|
131
|
+
</Button>
|
|
132
|
+
}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Column Property Mapping
|
|
139
|
+
|
|
140
|
+
| Property | Implementation |
|
|
141
|
+
| --------------- | ----------------------------------------------------------- |
|
|
142
|
+
| Hideable: Yes | Column visibility state to toggle show/hide |
|
|
143
|
+
| Sortable: Yes | Sort icon on `<TableHead>` + onClick to toggle query vars |
|
|
144
|
+
| Filterable: Yes | Filter UI above table (`<Select />`) |
|
|
145
|
+
| Searchable: Yes | Search input above table (`<Input placeholder="Search" />`) |
|
|
146
|
+
|
|
147
|
+
## Key Points
|
|
148
|
+
|
|
149
|
+
- Use `<Badge />` for status columns
|
|
150
|
+
- Format dates with `toLocaleDateString()`
|
|
151
|
+
- Use `<EmptyState />` with Create action for empty lists
|
|
152
|
+
- Add a View button per row to navigate to the detail page
|
|
153
|
+
- Iterate data using Connection type `edges.node` pattern
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: app-compose-6-implementation-spec
|
|
3
|
+
description: Create Tier 4 documentation (resolvers) for GraphQL operations. Use after completing design review with app-compose-5-design-mock-review.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Implementation Spec Workflow
|
|
7
|
+
|
|
8
|
+
Create Tier 4 documentation: GraphQL Resolvers (mutations and queries).
|
|
9
|
+
|
|
10
|
+
## Prerequisites
|
|
11
|
+
|
|
12
|
+
Tier 1-3 documentation and design mockups must exist:
|
|
13
|
+
|
|
14
|
+
- `README.md` (requirements)
|
|
15
|
+
- `docs/actors/*.md` (actors)
|
|
16
|
+
- `docs/business-flow/*/README.md` (flows)
|
|
17
|
+
- `docs/business-flow/*/story/*.md` (stories)
|
|
18
|
+
- `docs/screen/*.md` (screens)
|
|
19
|
+
- `docs/design.pen` (UI mockups)
|
|
20
|
+
|
|
21
|
+
## Workflow Phases
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
ANALYZE → IDENTIFY → CHECK MODULES → CREATE → VALIDATE
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Phase 1: Analyze
|
|
28
|
+
|
|
29
|
+
Read all story files and identify:
|
|
30
|
+
|
|
31
|
+
1. **Operations** that modify data → mutations
|
|
32
|
+
2. **Operations** that retrieve data → queries
|
|
33
|
+
3. **Error scenarios** for each operation
|
|
34
|
+
|
|
35
|
+
### Phase 2: Identify
|
|
36
|
+
|
|
37
|
+
Map stories to resolvers:
|
|
38
|
+
|
|
39
|
+
| Story Element | Resolver Type |
|
|
40
|
+
| -------------------- | ------------- |
|
|
41
|
+
| Create/Update/Delete | Mutation |
|
|
42
|
+
| View/List/Search | Query |
|
|
43
|
+
| Real-time updates | Subscription |
|
|
44
|
+
|
|
45
|
+
**Naming:** Use action-verb names: `create-`, `update-`, `delete-`, `submit-`, `approve-`
|
|
46
|
+
|
|
47
|
+
### Phase 3: Check Module Dependencies
|
|
48
|
+
|
|
49
|
+
Resolvers reference module commands. Before creating resolver docs, verify required modules exist.
|
|
50
|
+
|
|
51
|
+
**Check existing modules:**
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
ls modules/
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Check module commands:**
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
ls modules/<module-name>/docs/commands/
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Map resolvers to modules:**
|
|
64
|
+
|
|
65
|
+
| Resolver Operation | Typical Module |
|
|
66
|
+
| ------------------ | --------------------- |
|
|
67
|
+
| User invite/roles | `user-management` |
|
|
68
|
+
| Supplier CRUD | `supplier-management` |
|
|
69
|
+
| Document handling | `document-management` |
|
|
70
|
+
| Task workflows | `task-management` |
|
|
71
|
+
|
|
72
|
+
**If modules don't exist:**
|
|
73
|
+
|
|
74
|
+
1. **Identify needed modules** - Group resolver operations by domain
|
|
75
|
+
2. **Create module docs first** - Use `1-module-docs` skill
|
|
76
|
+
3. **Return to Tier 4** - After modules have documented commands
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
Tier 4 blocked → Create modules → Return to Tier 4
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Only reference existing module commands** in resolver documentation. Never reference modules or commands that don't exist.
|
|
83
|
+
|
|
84
|
+
### Phase 4: Create
|
|
85
|
+
|
|
86
|
+
Generate documentation using `erp-kit` CLI:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
erp-kit scaffold --app-root examples resolver <app> <resolver-name>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Naming conventions:**
|
|
93
|
+
|
|
94
|
+
- Filename must match H1 heading exactly (kebab-case)
|
|
95
|
+
- Action-focused names: `create-supplier-invitation.md`
|
|
96
|
+
|
|
97
|
+
### Phase 5: Validate
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pnpm run app:doc:check
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Schema Reference
|
|
104
|
+
|
|
105
|
+
| Schema | Tier | Output Path |
|
|
106
|
+
| -------------- | ---- | ------------------------- |
|
|
107
|
+
| `resolver.yml` | 4 | `docs/resolver/<name>.md` |
|
|
108
|
+
|
|
109
|
+
## Tips
|
|
110
|
+
|
|
111
|
+
- One story may require 1-3 resolvers
|
|
112
|
+
- Group related operations logically
|
|
113
|
+
- Document all error scenarios
|
|
114
|
+
|
|
115
|
+
## Completion
|
|
116
|
+
|
|
117
|
+
After Tier 4, the application specification is complete:
|
|
118
|
+
|
|
119
|
+
- **Tier 1**: Application overview
|
|
120
|
+
- **Tier 2**: Actors and business workflows
|
|
121
|
+
- **Tier 3**: User stories and screens
|
|
122
|
+
- **Tier 4**: Implementation specifications
|
|
123
|
+
|
|
124
|
+
## References
|
|
125
|
+
|
|
126
|
+
- [Application structure](references/structure.md)
|
|
127
|
+
- [Backend auth](references/auth.md)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Backend Auth Configuration
|
|
2
|
+
|
|
3
|
+
Use Tailor SDK auth resources as the default pattern for backend authentication setup.
|
|
4
|
+
|
|
5
|
+
## Standard Auth Shape
|
|
6
|
+
|
|
7
|
+
In `tailor.config.ts`:
|
|
8
|
+
|
|
9
|
+
- Define IdP with `defineIdp(...)`
|
|
10
|
+
- Define Auth with `defineAuth(...)`
|
|
11
|
+
- Define static website with `defineStaticWebSite(...)` when frontend is deployed as static hosting
|
|
12
|
+
- Wire `idProvider` via `idp.provider(...)`
|
|
13
|
+
- Set exactly one OAuth2 client for frontend login (default client name: `default`)
|
|
14
|
+
- Export config with `auth: auth` and `idp: [idp]`
|
|
15
|
+
- Include static website URL in CORS when using deployed frontend (for example, `cors: ["http://localhost:5173", website.url]`)
|
|
16
|
+
|
|
17
|
+
Prefer this stable naming unless there is a clear reason to change:
|
|
18
|
+
|
|
19
|
+
- Auth name: `default`
|
|
20
|
+
- IdP name: `default`
|
|
21
|
+
- OAuth2 client name: `default`
|
|
22
|
+
|
|
23
|
+
## User Mapping
|
|
24
|
+
|
|
25
|
+
Use `userProfile` to map authenticated identities to the application user type.
|
|
26
|
+
|
|
27
|
+
- `type`: module-provided user type
|
|
28
|
+
- `usernameField`: stable unique field (for example, `email`)
|
|
29
|
+
- `attributes`: explicitly list required attributes
|
|
30
|
+
|
|
31
|
+
Avoid implicit attribute assumptions.
|
|
32
|
+
|
|
33
|
+
## CORS for Static Website Login
|
|
34
|
+
|
|
35
|
+
When frontend is deployed via `staticWebsites`, OAuth metadata and query calls are cross-origin from `https://<name>-<workspace>.web.erp.dev` to app backend.
|
|
36
|
+
|
|
37
|
+
- Always include the deployed static website origin via `website.url` in backend `cors`
|
|
38
|
+
- Keep local development origin in `cors` as well (for example, `http://localhost:5173`)
|
|
39
|
+
- If your template uses IP allow lists, set `allowedIpAddresses` appropriately for Tailor internal access
|
|
40
|
+
|
|
41
|
+
If `website.url` is missing from `cors`, login may fail with browser CORS errors before authentication starts.
|
|
42
|
+
|
|
43
|
+
## Seed Permission Alignment
|
|
44
|
+
|
|
45
|
+
Seeded login users in `seed/data/User.jsonl` must receive module permissions required by guarded commands/resolvers.
|
|
46
|
+
|
|
47
|
+
- Do not assume `ADMIN` is automatically privileged unless your permission evaluator explicitly supports it
|
|
48
|
+
- Grant concrete permission keys defined by the module (for example, `user-management:createUser`)
|
|
49
|
+
- Keep `User.jsonl` permissions aligned with the module currently wired in `src/modules.ts`
|
|
50
|
+
|
|
51
|
+
## Workspace and CLI Inputs
|
|
52
|
+
|
|
53
|
+
Use `TAILOR_PLATFORM_WORKSPACE_ID` in backend `.env` for CLI operations.
|
|
54
|
+
|
|
55
|
+
Required for non-interactive resource lookup and automation:
|
|
56
|
+
|
|
57
|
+
- `tailor-sdk oauth2client list`
|
|
58
|
+
- `tailor-sdk oauth2client get <name>`
|
|
59
|
+
|
|
60
|
+
Use `--env-file <backend/.env>` for deterministic command execution in local/dev scripts.
|
|
61
|
+
|
|
62
|
+
## OAuth Client ID Source of Truth
|
|
63
|
+
|
|
64
|
+
Frontend `VITE_TAILOR_CLIENT_ID` must come from backend-managed OAuth2 client data.
|
|
65
|
+
|
|
66
|
+
Canonical retrieval flow:
|
|
67
|
+
|
|
68
|
+
1. `tailor-sdk oauth2client get default --json`
|
|
69
|
+
2. Copy `clientId` from command output
|
|
70
|
+
3. Set it in frontend env
|
|
71
|
+
|
|
72
|
+
Do not hardcode OAuth client IDs in source code.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Application Directory Structure
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
{app_name}/
|
|
5
|
+
├── backend/
|
|
6
|
+
│ ├── src/
|
|
7
|
+
│ │ ├── modules.ts # Declaring module usage
|
|
8
|
+
│ │ ├── modules/
|
|
9
|
+
│ │ │ └── {module-name}/ # Module-specific directory
|
|
10
|
+
│ │ │ ├── resolvers/ # API Definition to expose graphql apis
|
|
11
|
+
│ │ │ └── executors/ # PubSub Automation (one file per declaration)
|
|
12
|
+
│ │ └── generated/ # Auto-generated code (do not edit)
|
|
13
|
+
│ └── tailor.config.ts # tailor application config
|
|
14
|
+
│
|
|
15
|
+
└── frontend/
|
|
16
|
+
└── src/
|
|
17
|
+
├── pages/ # File-based routing (auto-discovered by Vite plugin)
|
|
18
|
+
│ └── {page-path}/
|
|
19
|
+
│ ├── page.tsx
|
|
20
|
+
│ └── {page-path}/
|
|
21
|
+
│ ├── components/
|
|
22
|
+
│ └── page.tsx
|
|
23
|
+
├── components/
|
|
24
|
+
│ └── ui/ # Generic UI components
|
|
25
|
+
├── graphql/ # gql.tada settings
|
|
26
|
+
└── providers/ # react providers
|
|
27
|
+
```
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mock-scenario
|
|
3
|
+
description: Scaffold a new Mockoon mock scenario with CRUD routes, error scenarios, and registry updates
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Mock Scenario Scaffolder
|
|
7
|
+
|
|
8
|
+
Generate a complete Mockoon mock API config for a new provider scenario and wire it into the registry.
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
12
|
+
### Step 1: Identify target API
|
|
13
|
+
|
|
14
|
+
Ask the user for:
|
|
15
|
+
|
|
16
|
+
- **Provider name** (e.g. `stripe`, `github`, `twilio`)
|
|
17
|
+
- **Scenario name** (e.g. `admin-api`, `payments-api`, `product-sync`)
|
|
18
|
+
- **Key resources to mock** (e.g. customers, invoices, messages)
|
|
19
|
+
- **Scenario to test** (e.g. initial full sync, incremental sync, webhook-triggered update, error recovery)
|
|
20
|
+
|
|
21
|
+
### Step 2: Research real API
|
|
22
|
+
|
|
23
|
+
Use web search to find:
|
|
24
|
+
|
|
25
|
+
- Base URL and API version
|
|
26
|
+
- Authentication scheme (API key, OAuth, bearer token)
|
|
27
|
+
- Endpoint patterns and HTTP methods
|
|
28
|
+
- Response shapes for the key resources
|
|
29
|
+
- Error response format (status codes, error body structure)
|
|
30
|
+
- Rate limit headers
|
|
31
|
+
|
|
32
|
+
### Step 3: Generate Mockoon JSON
|
|
33
|
+
|
|
34
|
+
Create `mocks/{provider}/{scenario}/mock.json` following the Shopify admin-api mock as the canonical reference (`mocks/shopify/admin-api/mock.json`).
|
|
35
|
+
|
|
36
|
+
The config must include:
|
|
37
|
+
|
|
38
|
+
- **uuid**: a valid UUID v4
|
|
39
|
+
- **name**: Human-readable API name
|
|
40
|
+
- **endpointPrefix**: Match the real API's base path
|
|
41
|
+
- **port**: `3000` (overridden at runtime by the reverse proxy launcher)
|
|
42
|
+
- **hostname**: `0.0.0.0`
|
|
43
|
+
- **latency**: `0`
|
|
44
|
+
- **folders**: `[]`
|
|
45
|
+
- **rootChildren**: `[]`
|
|
46
|
+
- **proxyMode**: `false`
|
|
47
|
+
- **proxyHost**: `""`
|
|
48
|
+
- **proxyRemovePrefix**: `false`
|
|
49
|
+
- **tlsOptions**: `{ "enabled": false, "type": "CERT", "pfxPath": "", "certPath": "", "keyPath": "", "caPath": "", "passphrase": "" }`
|
|
50
|
+
- **cors**: `true`
|
|
51
|
+
- **headers**: `[{ "key": "Content-Type", "value": "application/json" }]`
|
|
52
|
+
- **proxyReqHeaders**: `[]`
|
|
53
|
+
- **proxyResHeaders**: `[]`
|
|
54
|
+
- **callbacks**: `[]`
|
|
55
|
+
|
|
56
|
+
All UUIDs (environment, routes, responses, data buckets) must be valid UUID v4 values.
|
|
57
|
+
|
|
58
|
+
**Data buckets** — for each stateful/CRUD resource:
|
|
59
|
+
|
|
60
|
+
- 2–3 seed records with realistic field values
|
|
61
|
+
- Use the `id` field matching Mockoon's `"id"` property for CRUD lookup
|
|
62
|
+
|
|
63
|
+
**Routes:**
|
|
64
|
+
|
|
65
|
+
1. **CRUD routes** — for mutable resources:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"type": "crud",
|
|
70
|
+
"endpoint": "{resource}",
|
|
71
|
+
"responses": [
|
|
72
|
+
{
|
|
73
|
+
"label": "CRUD {Resource}",
|
|
74
|
+
"statusCode": 200,
|
|
75
|
+
"headers": [{ "key": "Content-Type", "value": "application/json" }],
|
|
76
|
+
"bodyType": "DATABUCKET",
|
|
77
|
+
"databucketID": "{resource-bucket-id}"
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
2. **Static routes** — for read-only endpoints:
|
|
84
|
+
- List endpoints returning JSON arrays
|
|
85
|
+
- Single-item endpoints using `{{urlParam 'id'}}` interpolation
|
|
86
|
+
|
|
87
|
+
3. **Error scenarios** — triggered via `X-Test-Scenario` header:
|
|
88
|
+
- `unauthorized` → 401
|
|
89
|
+
- `not-found` → 404
|
|
90
|
+
- `rate-limit` → 429 with `Retry-After` header
|
|
91
|
+
- `server-error` → 500
|
|
92
|
+
|
|
93
|
+
4. **Catch-all** — `*` endpoint returning 500 when `X-Test-Scenario: server-error`
|
|
94
|
+
|
|
95
|
+
**Every route must have:** `type`, `documentation`, `responseMode: null`, `streamingMode: null`, `streamingInterval: 0`
|
|
96
|
+
|
|
97
|
+
**Every response must have:** `latency: 0`, `bodyType`, `databucketID`, `filePath: ""`, `sendFileAsBody: false`, `rules`, `rulesOperator: "OR"`, `disableTemplating: false`, `fallbackTo404: false`, `default`, `crudKey: "id"`, `callbacks: []`, a `Content-Type` header, and a non-empty `label`
|
|
98
|
+
|
|
99
|
+
**Every rule must have:** `target`, `modifier`, `value`, `operator`, `invert: false`
|
|
100
|
+
|
|
101
|
+
### Step 4: Generate scenario README
|
|
102
|
+
|
|
103
|
+
Create `mocks/{provider}/{scenario}/README.md` with:
|
|
104
|
+
|
|
105
|
+
- Title and one-line description
|
|
106
|
+
- Quick start with both methods: `erp-kit mock start` (proxy) and direct `npx @mockoon/cli start`
|
|
107
|
+
- Endpoints table (method, path, scenarios)
|
|
108
|
+
- CRUD workflow example with curl commands using proxy URL (`http://localhost:3000/{provider}/{scenario}/...`)
|
|
109
|
+
- Error scenario examples
|
|
110
|
+
- Test data description
|
|
111
|
+
|
|
112
|
+
### Step 5: Validate
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
erp-kit mock validate
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Fix any failures before finishing.
|
package/src/app.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createContext } from "./modules/shared/createContext";
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { defineCommand, runMain, arg } from "politty";
|
|
5
|
+
import { runCheck } from "./commands/check.js";
|
|
6
|
+
import { runSyncCheck, formatSyncCheckReport } from "./commands/sync-check.js";
|
|
7
|
+
import { runScaffold, ALL_TYPES, isModuleType, type ScaffoldType } from "./commands/scaffold.js";
|
|
8
|
+
import { runInit } from "./commands/init.js";
|
|
9
|
+
import { mockCommand } from "./commands/mock/index.js";
|
|
10
|
+
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
|
|
13
|
+
const rootArgs = z.object({
|
|
14
|
+
modulesRoot: arg(z.string().optional(), {
|
|
15
|
+
alias: "m",
|
|
16
|
+
description: "Path to modules directory",
|
|
17
|
+
}),
|
|
18
|
+
appRoot: arg(z.string().optional(), {
|
|
19
|
+
alias: "a",
|
|
20
|
+
description: "Path to app-compose directory (apps/ or examples/)",
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function requireRoot(args: { modulesRoot?: string; appRoot?: string }) {
|
|
25
|
+
const paths = { modulesRoot: args.modulesRoot, appRoot: args.appRoot };
|
|
26
|
+
if (!paths.modulesRoot && !paths.appRoot) {
|
|
27
|
+
console.error("At least one of --modules-root or --app-root is required.");
|
|
28
|
+
process.exit(2);
|
|
29
|
+
}
|
|
30
|
+
return paths;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const checkCommand = defineCommand({
|
|
34
|
+
name: "check",
|
|
35
|
+
description: "Validate docs against schemas",
|
|
36
|
+
args: rootArgs,
|
|
37
|
+
run: async (args) => {
|
|
38
|
+
const paths = requireRoot(args);
|
|
39
|
+
const exitCode = await runCheck(paths, cwd);
|
|
40
|
+
process.exit(exitCode);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const syncCheckCommand = defineCommand({
|
|
45
|
+
name: "sync-check",
|
|
46
|
+
description: "Validate source <-> doc correspondence",
|
|
47
|
+
args: rootArgs,
|
|
48
|
+
run: async (args) => {
|
|
49
|
+
const paths = requireRoot(args);
|
|
50
|
+
const result = await runSyncCheck(paths, cwd);
|
|
51
|
+
console.log(formatSyncCheckReport(result));
|
|
52
|
+
process.exit(result.exitCode);
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const scaffoldCommand = defineCommand({
|
|
57
|
+
name: "scaffold",
|
|
58
|
+
description: "Generate doc file from schema template",
|
|
59
|
+
args: rootArgs.extend({
|
|
60
|
+
type: arg(z.enum(ALL_TYPES as unknown as [string, ...string[]]), {
|
|
61
|
+
positional: true,
|
|
62
|
+
description: `Scaffold type (${ALL_TYPES.join(", ")})`,
|
|
63
|
+
}),
|
|
64
|
+
parent: arg(z.string(), {
|
|
65
|
+
positional: true,
|
|
66
|
+
description: "Parent name (module or app name)",
|
|
67
|
+
}),
|
|
68
|
+
name: arg(z.string().optional(), {
|
|
69
|
+
positional: true,
|
|
70
|
+
description: "Item name (required for most types)",
|
|
71
|
+
}),
|
|
72
|
+
}),
|
|
73
|
+
run: async (args) => {
|
|
74
|
+
const paths = requireRoot(args);
|
|
75
|
+
const root = isModuleType(args.type) ? paths.modulesRoot : paths.appRoot;
|
|
76
|
+
if (!root) {
|
|
77
|
+
console.error(
|
|
78
|
+
`--${isModuleType(args.type) ? "modules-root" : "app-root"} is required for scaffold type "${args.type}".`,
|
|
79
|
+
);
|
|
80
|
+
process.exit(2);
|
|
81
|
+
}
|
|
82
|
+
const exitCode = await runScaffold(
|
|
83
|
+
args.type as ScaffoldType,
|
|
84
|
+
args.parent,
|
|
85
|
+
args.name,
|
|
86
|
+
root,
|
|
87
|
+
cwd,
|
|
88
|
+
);
|
|
89
|
+
process.exit(exitCode);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const initCommand = defineCommand({
|
|
94
|
+
name: "init",
|
|
95
|
+
description: "Set up consumer repo with framework skills",
|
|
96
|
+
args: z.object({
|
|
97
|
+
force: arg(z.boolean().default(false), {
|
|
98
|
+
alias: "f",
|
|
99
|
+
description: "Overwrite existing skills",
|
|
100
|
+
}),
|
|
101
|
+
}),
|
|
102
|
+
run: (args) => {
|
|
103
|
+
const exitCode = runInit(cwd, args.force);
|
|
104
|
+
process.exit(exitCode);
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const main = defineCommand({
|
|
109
|
+
name: "erp-kit",
|
|
110
|
+
description: "Documentation validation and scaffolding tool",
|
|
111
|
+
subCommands: {
|
|
112
|
+
check: checkCommand,
|
|
113
|
+
"sync-check": syncCheckCommand,
|
|
114
|
+
scaffold: scaffoldCommand,
|
|
115
|
+
init: initCommand,
|
|
116
|
+
mock: mockCommand,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
void runMain(main);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { buildCheckTargets } from "./check.js";
|
|
3
|
+
|
|
4
|
+
describe("buildCheckTargets", () => {
|
|
5
|
+
it("generates module targets when modulesRoot is set", () => {
|
|
6
|
+
const targets = buildCheckTargets({ modulesRoot: "modules", appRoot: undefined });
|
|
7
|
+
expect(targets).toEqual([
|
|
8
|
+
{ glob: "modules/[a-zA-Z]*/docs/features/*.md", schemaKey: "feature" },
|
|
9
|
+
{ glob: "modules/[a-zA-Z]*/docs/commands/*.md", schemaKey: "command" },
|
|
10
|
+
{ glob: "modules/[a-zA-Z]*/docs/models/*.md", schemaKey: "model" },
|
|
11
|
+
{ glob: "modules/[a-zA-Z]*/README.md", schemaKey: "module" },
|
|
12
|
+
]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("generates app-compose targets when appRoot is set", () => {
|
|
16
|
+
const targets = buildCheckTargets({ modulesRoot: undefined, appRoot: "apps" });
|
|
17
|
+
expect(targets[0].glob).toBe("apps/[a-zA-Z]*/README.md");
|
|
18
|
+
expect(targets).toHaveLength(6);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("generates both when both roots are set", () => {
|
|
22
|
+
const targets = buildCheckTargets({ modulesRoot: "modules", appRoot: "examples" });
|
|
23
|
+
expect(targets).toHaveLength(10);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("returns empty when neither root is set", () => {
|
|
27
|
+
const targets = buildCheckTargets({ modulesRoot: undefined, appRoot: undefined });
|
|
28
|
+
expect(targets).toHaveLength(0);
|
|
29
|
+
});
|
|
30
|
+
});
|