@mettlecast/eslint-plugin-domain-module 0.1.0
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 +83 -0
- package/dist/__tests__/flow-domain-ownership.test.d.ts +1 -0
- package/dist/__tests__/flow-domain-ownership.test.js +33 -0
- package/dist/__tests__/no-cross-domain-internal-import.test.d.ts +1 -0
- package/dist/__tests__/no-cross-domain-internal-import.test.js +40 -0
- package/dist/__tests__/no-raw-aws-sdk.test.d.ts +1 -0
- package/dist/__tests__/no-raw-aws-sdk.test.js +30 -0
- package/dist/__tests__/no-raw-http-server.test.d.ts +1 -0
- package/dist/__tests__/no-raw-http-server.test.js +29 -0
- package/dist/__tests__/require-define-primitive.test.d.ts +1 -0
- package/dist/__tests__/require-define-primitive.test.js +26 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +42 -0
- package/dist/rules/flow-domain-ownership.d.ts +4 -0
- package/dist/rules/flow-domain-ownership.js +45 -0
- package/dist/rules/no-cross-domain-internal-import.d.ts +4 -0
- package/dist/rules/no-cross-domain-internal-import.js +47 -0
- package/dist/rules/no-raw-aws-sdk.d.ts +4 -0
- package/dist/rules/no-raw-aws-sdk.js +26 -0
- package/dist/rules/no-raw-http-server.d.ts +4 -0
- package/dist/rules/no-raw-http-server.js +27 -0
- package/dist/rules/require-define-primitive.d.ts +4 -0
- package/dist/rules/require-define-primitive.js +36 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# @mettlecast/eslint-plugin-domain-module
|
|
2
|
+
|
|
3
|
+
ESLint plugin enforcing TIB Domain Module framework usage patterns. Prevents accidental AWS SDK, HTTP server, and cross-domain violations.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install --save-dev @mettlecast/eslint-plugin-domain-module
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Configure in `eslint.config.js`:
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
import domainModulePlugin from '@mettlecast/eslint-plugin-domain-module';
|
|
15
|
+
|
|
16
|
+
export default [
|
|
17
|
+
{
|
|
18
|
+
files: ['src/domain/**/*.ts'],
|
|
19
|
+
...domainModulePlugin.configs.recommended,
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Rules
|
|
25
|
+
|
|
26
|
+
| Rule | Flags | Severity |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `no-raw-aws-sdk` | Direct imports of `@aws-sdk/*` packages in domain handler files | error |
|
|
29
|
+
| `no-raw-http-server` | Framework imports (`express`, `koa`, `fastify`, `aws-lambda`) in domain handlers | error |
|
|
30
|
+
| `require-define-primitive` | Handler files without a top-level `define*()` call (missing domain primitive declaration) | warn |
|
|
31
|
+
| `no-cross-domain-internal-import` | Importing internal modules (not exported types) from another domain directory | error |
|
|
32
|
+
|
|
33
|
+
## Recommended Config
|
|
34
|
+
|
|
35
|
+
The `recommended` preset enables all four rules at their default severity levels:
|
|
36
|
+
|
|
37
|
+
```javascript
|
|
38
|
+
domainModulePlugin.configs.recommended
|
|
39
|
+
// Results in:
|
|
40
|
+
// - no-raw-aws-sdk: error
|
|
41
|
+
// - no-raw-http-server: error
|
|
42
|
+
// - require-define-primitive: warn
|
|
43
|
+
// - no-cross-domain-internal-import: error
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Examples
|
|
47
|
+
|
|
48
|
+
**Violations caught:**
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// ❌ no-raw-aws-sdk
|
|
52
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
53
|
+
|
|
54
|
+
// ❌ no-raw-http-server
|
|
55
|
+
import express from 'express';
|
|
56
|
+
|
|
57
|
+
// ❌ require-define-primitive
|
|
58
|
+
export async function chargePayment(event, ctx) { /* ... */ }
|
|
59
|
+
|
|
60
|
+
// ❌ no-cross-domain-internal-import
|
|
61
|
+
import { internalFn } from '../payments/internal.js'; // not in payments/index.ts
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Correct usage:**
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// ✓ Use DomainContext.db instead
|
|
68
|
+
const { db } = ctx;
|
|
69
|
+
|
|
70
|
+
// ✓ Use defineApi() to declare handlers
|
|
71
|
+
export const chargeHandler = defineApi({
|
|
72
|
+
path: '/charge',
|
|
73
|
+
// ...
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ✓ Only import exported types from other domains
|
|
77
|
+
import type { PaymentEvent } from '@myorg/payments-contracts';
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## See Also
|
|
81
|
+
|
|
82
|
+
- `@mettlecast/domain-runtime` — handler definitions
|
|
83
|
+
- `@mettlecast/domain-cli` — validation and linting
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { afterAll, describe, it } from 'vitest';
|
|
2
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
3
|
+
import { flowDomainOwnership } from '../rules/flow-domain-ownership.js';
|
|
4
|
+
RuleTester.afterAll = afterAll;
|
|
5
|
+
RuleTester.describe = describe;
|
|
6
|
+
RuleTester.it = it;
|
|
7
|
+
const tester = new RuleTester({
|
|
8
|
+
languageOptions: { ecmaVersion: 2022, sourceType: 'module' },
|
|
9
|
+
});
|
|
10
|
+
tester.run('flow-domain-ownership', flowDomainOwnership, {
|
|
11
|
+
valid: [
|
|
12
|
+
{
|
|
13
|
+
filename: '/repo/domains/auth/flows/org-provisioning.ts',
|
|
14
|
+
code: `defineFlow({ id: 'org-provisioning', owningDomain: 'auth', name: 'x', steps: [] });`,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
filename: '/repo/flows/root-level.ts',
|
|
18
|
+
code: `defineFlow({ id: 'root-level', name: 'x', steps: [] });`,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
invalid: [
|
|
22
|
+
{
|
|
23
|
+
filename: '/repo/domains/auth/flows/bad.ts',
|
|
24
|
+
code: `defineFlow({ id: 'bad', owningDomain: 'payments', name: 'x', steps: [] });`,
|
|
25
|
+
errors: [{ messageId: 'owningDomainMismatch' }],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
filename: '/repo/domains/auth/flows/no-owner.ts',
|
|
29
|
+
code: `defineFlow({ id: 'no-owner', name: 'x', steps: [] });`,
|
|
30
|
+
errors: [{ messageId: 'missingOwningDomain' }],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
import { afterAll, describe, it } from 'vitest';
|
|
3
|
+
import { noCrossDomainInternalImport } from '../rules/no-cross-domain-internal-import.js';
|
|
4
|
+
RuleTester.afterAll = afterAll;
|
|
5
|
+
RuleTester.describe = describe;
|
|
6
|
+
RuleTester.it = it;
|
|
7
|
+
const tester = new RuleTester({
|
|
8
|
+
languageOptions: { ecmaVersion: 2022, sourceType: 'module' },
|
|
9
|
+
});
|
|
10
|
+
tester.run('no-cross-domain-internal-import', noCrossDomainInternalImport, {
|
|
11
|
+
valid: [
|
|
12
|
+
{
|
|
13
|
+
// File not in a domain directory — rule is a no-op
|
|
14
|
+
filename: '/project/src/utils/helper.ts',
|
|
15
|
+
code: "import { something } from '/project/.mc/domains/billing/api/charge.ts';",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
// Same-domain import — allowed
|
|
19
|
+
filename: '/project/.mc/domains/billing/api/charge.ts',
|
|
20
|
+
code: "import { something } from '../subscribers/payment-received.ts';",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
// Import from contracts package — allowed
|
|
24
|
+
filename: '/project/.mc/domains/billing/api/charge.ts',
|
|
25
|
+
code: "import { UserContracts } from '@tib-contracts/users';",
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
invalid: [
|
|
29
|
+
{
|
|
30
|
+
filename: '/project/.mc/domains/billing/api/charge.ts',
|
|
31
|
+
code: "import { UserHandler } from '/project/.mc/domains/users/api/get-user.ts';",
|
|
32
|
+
errors: [{ messageId: 'noCrossDomainImport' }],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
filename: '/project/.mc/domains/billing/api/charge.ts',
|
|
36
|
+
code: "import { X } from '../../users/api/handler.ts';",
|
|
37
|
+
errors: [{ messageId: 'noCrossDomainImport' }],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
import { afterAll, describe, it } from 'vitest';
|
|
3
|
+
import { noRawAwsSdk } from '../rules/no-raw-aws-sdk.js';
|
|
4
|
+
RuleTester.afterAll = afterAll;
|
|
5
|
+
RuleTester.describe = describe;
|
|
6
|
+
RuleTester.it = it;
|
|
7
|
+
const tester = new RuleTester({
|
|
8
|
+
languageOptions: { ecmaVersion: 2022, sourceType: 'module' },
|
|
9
|
+
});
|
|
10
|
+
tester.run('no-raw-aws-sdk', noRawAwsSdk, {
|
|
11
|
+
valid: [
|
|
12
|
+
{ code: "import { something } from '@my-org/utils';" },
|
|
13
|
+
{ code: "import { DomainContext } from '@mettlecast/domain-runtime';" },
|
|
14
|
+
{ code: "const x = require('fs');" },
|
|
15
|
+
],
|
|
16
|
+
invalid: [
|
|
17
|
+
{
|
|
18
|
+
code: "import { DynamoDBClient } from '@aws-sdk/client-dynamodb';",
|
|
19
|
+
errors: [{ messageId: 'noRawAwsSdk' }],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
code: "import { S3Client } from '@aws-sdk/client-s3';",
|
|
23
|
+
errors: [{ messageId: 'noRawAwsSdk' }],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
code: "import { EventBridgeClient } from '@aws-sdk/client-eventbridge';",
|
|
27
|
+
errors: [{ messageId: 'noRawAwsSdk' }],
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
import { afterAll, describe, it } from 'vitest';
|
|
3
|
+
import { noRawHttpServer } from '../rules/no-raw-http-server.js';
|
|
4
|
+
RuleTester.afterAll = afterAll;
|
|
5
|
+
RuleTester.describe = describe;
|
|
6
|
+
RuleTester.it = it;
|
|
7
|
+
const tester = new RuleTester({
|
|
8
|
+
languageOptions: { ecmaVersion: 2022, sourceType: 'module' },
|
|
9
|
+
});
|
|
10
|
+
tester.run('no-raw-http-server', noRawHttpServer, {
|
|
11
|
+
valid: [
|
|
12
|
+
{ code: "import { defineApi } from '@mettlecast/domain-runtime';" },
|
|
13
|
+
{ code: "import path from 'node:path';" },
|
|
14
|
+
],
|
|
15
|
+
invalid: [
|
|
16
|
+
{
|
|
17
|
+
code: "import express from 'express';",
|
|
18
|
+
errors: [{ messageId: 'noRawHttpServer' }],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
code: "import Fastify from 'fastify';",
|
|
22
|
+
errors: [{ messageId: 'noRawHttpServer' }],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
code: "import { Handler } from 'aws-lambda';",
|
|
26
|
+
errors: [{ messageId: 'noRawHttpServer' }],
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
import { afterAll, describe, it } from 'vitest';
|
|
3
|
+
import { requireDefinePrimitive } from '../rules/require-define-primitive.js';
|
|
4
|
+
RuleTester.afterAll = afterAll;
|
|
5
|
+
RuleTester.describe = describe;
|
|
6
|
+
RuleTester.it = it;
|
|
7
|
+
const tester = new RuleTester({
|
|
8
|
+
languageOptions: { ecmaVersion: 2022, sourceType: 'module' },
|
|
9
|
+
});
|
|
10
|
+
tester.run('require-define-primitive', requireDefinePrimitive, {
|
|
11
|
+
valid: [
|
|
12
|
+
{ code: "export const myApi = defineApi({ id: 'test', path: '/test' });" },
|
|
13
|
+
{ code: "export const sub = defineSubscriber({ id: 'x', event: 'y', semverRange: '^1.0' });" },
|
|
14
|
+
{ code: "const domain = defineDomain({ id: 'my-domain', name: 'My Domain', tenancy: 'single' });" },
|
|
15
|
+
],
|
|
16
|
+
invalid: [
|
|
17
|
+
{
|
|
18
|
+
code: "export const SOME_CONSTANT = 'value';",
|
|
19
|
+
errors: [{ messageId: 'missingDefinePrimitive' }],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
code: "import { something } from '@mettlecast/domain-runtime';",
|
|
23
|
+
errors: [{ messageId: 'missingDefinePrimitive' }],
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mettlecast/eslint-plugin-domain-module
|
|
3
|
+
*
|
|
4
|
+
* ESLint plugin enforcing Domain Module framework usage patterns.
|
|
5
|
+
*/
|
|
6
|
+
declare const _default: {
|
|
7
|
+
rules: {
|
|
8
|
+
readonly 'no-raw-aws-sdk': import("@typescript-eslint/utils/ts-eslint").RuleModule<"noRawAwsSdk", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
9
|
+
name: string;
|
|
10
|
+
};
|
|
11
|
+
readonly 'no-raw-http-server': import("@typescript-eslint/utils/ts-eslint").RuleModule<"noRawHttpServer", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
12
|
+
name: string;
|
|
13
|
+
};
|
|
14
|
+
readonly 'require-define-primitive': import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingDefinePrimitive", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
15
|
+
name: string;
|
|
16
|
+
};
|
|
17
|
+
readonly 'no-cross-domain-internal-import': import("@typescript-eslint/utils/ts-eslint").RuleModule<"noCrossDomainImport", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
18
|
+
name: string;
|
|
19
|
+
};
|
|
20
|
+
readonly 'flow-domain-ownership': import("@typescript-eslint/utils/ts-eslint").RuleModule<"owningDomainMismatch" | "missingOwningDomain", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
21
|
+
name: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
configs: {
|
|
25
|
+
recommended: {
|
|
26
|
+
readonly plugins: {
|
|
27
|
+
readonly '@mettlecast/domain-module': {
|
|
28
|
+
readonly rules: {
|
|
29
|
+
readonly 'no-raw-aws-sdk': import("@typescript-eslint/utils/ts-eslint").RuleModule<"noRawAwsSdk", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
30
|
+
name: string;
|
|
31
|
+
};
|
|
32
|
+
readonly 'no-raw-http-server': import("@typescript-eslint/utils/ts-eslint").RuleModule<"noRawHttpServer", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
33
|
+
name: string;
|
|
34
|
+
};
|
|
35
|
+
readonly 'require-define-primitive': import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingDefinePrimitive", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
36
|
+
name: string;
|
|
37
|
+
};
|
|
38
|
+
readonly 'no-cross-domain-internal-import': import("@typescript-eslint/utils/ts-eslint").RuleModule<"noCrossDomainImport", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
39
|
+
name: string;
|
|
40
|
+
};
|
|
41
|
+
readonly 'flow-domain-ownership': import("@typescript-eslint/utils/ts-eslint").RuleModule<"owningDomainMismatch" | "missingOwningDomain", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
42
|
+
name: string;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
readonly rules: {
|
|
48
|
+
readonly '@mettlecast/domain-module/no-raw-aws-sdk': "error";
|
|
49
|
+
readonly '@mettlecast/domain-module/no-raw-http-server': "error";
|
|
50
|
+
readonly '@mettlecast/domain-module/require-define-primitive': "warn";
|
|
51
|
+
readonly '@mettlecast/domain-module/no-cross-domain-internal-import': "error";
|
|
52
|
+
readonly '@mettlecast/domain-module/flow-domain-ownership': "error";
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
export default _default;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { noRawAwsSdk } from './rules/no-raw-aws-sdk.js';
|
|
2
|
+
import { noRawHttpServer } from './rules/no-raw-http-server.js';
|
|
3
|
+
import { requireDefinePrimitive } from './rules/require-define-primitive.js';
|
|
4
|
+
import { noCrossDomainInternalImport } from './rules/no-cross-domain-internal-import.js';
|
|
5
|
+
import { flowDomainOwnership } from './rules/flow-domain-ownership.js';
|
|
6
|
+
/**
|
|
7
|
+
* All rules exported by @mettlecast/eslint-plugin-domain-module.
|
|
8
|
+
*/
|
|
9
|
+
const rules = {
|
|
10
|
+
'no-raw-aws-sdk': noRawAwsSdk,
|
|
11
|
+
'no-raw-http-server': noRawHttpServer,
|
|
12
|
+
'require-define-primitive': requireDefinePrimitive,
|
|
13
|
+
'no-cross-domain-internal-import': noCrossDomainInternalImport,
|
|
14
|
+
'flow-domain-ownership': flowDomainOwnership,
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Recommended flat config preset.
|
|
18
|
+
* Enables all five rules at their default severity levels.
|
|
19
|
+
*/
|
|
20
|
+
const recommended = {
|
|
21
|
+
plugins: {
|
|
22
|
+
'@mettlecast/domain-module': { rules },
|
|
23
|
+
},
|
|
24
|
+
rules: {
|
|
25
|
+
'@mettlecast/domain-module/no-raw-aws-sdk': 'error',
|
|
26
|
+
'@mettlecast/domain-module/no-raw-http-server': 'error',
|
|
27
|
+
'@mettlecast/domain-module/require-define-primitive': 'warn',
|
|
28
|
+
'@mettlecast/domain-module/no-cross-domain-internal-import': 'error',
|
|
29
|
+
'@mettlecast/domain-module/flow-domain-ownership': 'error',
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* @mettlecast/eslint-plugin-domain-module
|
|
34
|
+
*
|
|
35
|
+
* ESLint plugin enforcing Domain Module framework usage patterns.
|
|
36
|
+
*/
|
|
37
|
+
export default {
|
|
38
|
+
rules,
|
|
39
|
+
configs: {
|
|
40
|
+
recommended,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/LawrenceGeeHouse/Mettlecast/blob/develop/packages/eslint-plugin-domain-module/docs/${name}.md`);
|
|
3
|
+
function extractOwningDomainFromPath(filePath) {
|
|
4
|
+
const match = /[/\\]domains[/\\]([^/\\]+)[/\\]flows[/\\]/.exec(filePath);
|
|
5
|
+
return match?.[1];
|
|
6
|
+
}
|
|
7
|
+
export const flowDomainOwnership = createRule({
|
|
8
|
+
name: 'flow-domain-ownership',
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'problem',
|
|
11
|
+
docs: {
|
|
12
|
+
description: 'A flow file in domains/{x}/flows/ must declare owningDomain matching x.',
|
|
13
|
+
},
|
|
14
|
+
messages: {
|
|
15
|
+
owningDomainMismatch: 'Flow owningDomain "{{declared}}" does not match directory domain "{{expected}}". Move the file or fix owningDomain.\n fixHint: Either move the flow file to domains/{{declared}}/flows/ or change owningDomain to \'{{expected}}\'.\n kNodeRef: K:convention:flow-vs-subscriber-rule',
|
|
16
|
+
missingOwningDomain: 'Flow in domains/{{expected}}/flows/ must declare owningDomain: \'{{expected}}\'.\n fixHint: Add `owningDomain: \'{{expected}}\'` to the defineFlow() call.\n kNodeRef: K:convention:flow-vs-subscriber-rule',
|
|
17
|
+
},
|
|
18
|
+
schema: [],
|
|
19
|
+
},
|
|
20
|
+
defaultOptions: [],
|
|
21
|
+
create(context) {
|
|
22
|
+
const filename = context.filename ?? context.getFilename?.() ?? '';
|
|
23
|
+
const expectedDomain = extractOwningDomainFromPath(filename);
|
|
24
|
+
if (!expectedDomain)
|
|
25
|
+
return {};
|
|
26
|
+
return {
|
|
27
|
+
CallExpression(node) {
|
|
28
|
+
if (node.callee.type !== 'Identifier' || node.callee.name !== 'defineFlow')
|
|
29
|
+
return;
|
|
30
|
+
const arg = node.arguments[0];
|
|
31
|
+
if (!arg || arg.type !== 'ObjectExpression')
|
|
32
|
+
return;
|
|
33
|
+
const owningDomainProp = arg.properties.find((p) => p.type === 'Property' && 'name' in p.key && p.key.name === 'owningDomain');
|
|
34
|
+
if (!owningDomainProp) {
|
|
35
|
+
context.report({ node, messageId: 'missingOwningDomain', data: { expected: expectedDomain } });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const val = owningDomainProp.value;
|
|
39
|
+
if (val.type === 'Literal' && typeof val.value === 'string' && val.value !== expectedDomain) {
|
|
40
|
+
context.report({ node, messageId: 'owningDomainMismatch', data: { declared: val.value, expected: expectedDomain } });
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
import { resolve, dirname } from 'node:path';
|
|
3
|
+
const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/LawrenceGeeHouse/Mettlecast/blob/develop/packages/eslint-plugin-domain-module/docs/${name}.md`);
|
|
4
|
+
function extractDomainName(filePath) {
|
|
5
|
+
const match = /[/\\]\.tib[/\\]domains[/\\]([^/\\]+)[/\\]/.exec(filePath);
|
|
6
|
+
return match?.[1];
|
|
7
|
+
}
|
|
8
|
+
export const noCrossDomainInternalImport = createRule({
|
|
9
|
+
name: 'no-cross-domain-internal-import',
|
|
10
|
+
meta: {
|
|
11
|
+
type: 'problem',
|
|
12
|
+
docs: {
|
|
13
|
+
description: 'Disallow direct imports from another domain directory. Use ctx.actions.<domain> or contracts package instead.',
|
|
14
|
+
},
|
|
15
|
+
messages: {
|
|
16
|
+
noCrossDomainImport: 'Cross-domain import "{{source}}" is not allowed. Use ctx.actions.{{targetDomain}} or import from the contracts package.\n fixHint: Replace direct import with `await ctx.actions.call(\'{{targetDomain}}.<action>\', params)` or import from the contracts package.\n kNodeRef: K:convention:no-cross-domain-import',
|
|
17
|
+
},
|
|
18
|
+
schema: [],
|
|
19
|
+
},
|
|
20
|
+
defaultOptions: [],
|
|
21
|
+
create(context) {
|
|
22
|
+
const filename = context.filename ?? context.getFilename?.() ?? '';
|
|
23
|
+
const currentDomain = extractDomainName(filename);
|
|
24
|
+
if (!currentDomain)
|
|
25
|
+
return {};
|
|
26
|
+
return {
|
|
27
|
+
ImportDeclaration(node) {
|
|
28
|
+
const source = node.source.value;
|
|
29
|
+
if (typeof source !== 'string')
|
|
30
|
+
return;
|
|
31
|
+
const absMatch = /[/\\]\.tib[/\\]domains[/\\]([^/\\]+)[/\\]/.exec(source);
|
|
32
|
+
if (absMatch?.[1] && absMatch[1] !== currentDomain) {
|
|
33
|
+
context.report({ node, messageId: 'noCrossDomainImport', data: { source, targetDomain: absMatch[1] } });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (source.startsWith('.')) {
|
|
37
|
+
const fileDir = dirname(filename);
|
|
38
|
+
const resolvedPath = resolve(fileDir, source);
|
|
39
|
+
const relMatch = /[/\\]\.tib[/\\]domains[/\\]([^/\\]+)[/\\]/.exec(resolvedPath);
|
|
40
|
+
if (relMatch?.[1] && relMatch[1] !== currentDomain) {
|
|
41
|
+
context.report({ node, messageId: 'noCrossDomainImport', data: { source, targetDomain: relMatch[1] } });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/LawrenceGeeHouse/Mettlecast/blob/develop/packages/eslint-plugin-domain-module/docs/${name}.md`);
|
|
3
|
+
export const noRawAwsSdk = createRule({
|
|
4
|
+
name: 'no-raw-aws-sdk',
|
|
5
|
+
meta: {
|
|
6
|
+
type: 'problem',
|
|
7
|
+
docs: {
|
|
8
|
+
description: 'Disallow direct @aws-sdk/* imports in domain handler files. Use ctx.* instead.',
|
|
9
|
+
},
|
|
10
|
+
messages: {
|
|
11
|
+
noRawAwsSdk: 'Direct AWS SDK import "{{source}}" is not allowed in domain handlers. Use ctx.* methods provided by DomainContext.\n fixHint: Replace `import ... from \'{{source}}\'` with the corresponding `ctx.*` method (e.g., ctx.db for DynamoDB, ctx.publish for EventBridge).\n kNodeRef: K:convention:tier-1-foundations',
|
|
12
|
+
},
|
|
13
|
+
schema: [],
|
|
14
|
+
},
|
|
15
|
+
defaultOptions: [],
|
|
16
|
+
create(context) {
|
|
17
|
+
return {
|
|
18
|
+
ImportDeclaration(node) {
|
|
19
|
+
const source = node.source.value;
|
|
20
|
+
if (typeof source === 'string' && /^@aws-sdk\//.test(source)) {
|
|
21
|
+
context.report({ node, messageId: 'noRawAwsSdk', data: { source } });
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/LawrenceGeeHouse/Mettlecast/blob/develop/packages/eslint-plugin-domain-module/docs/${name}.md`);
|
|
3
|
+
const BANNED_HTTP_PACKAGES = new Set(['express', 'koa', 'fastify', 'aws-lambda']);
|
|
4
|
+
export const noRawHttpServer = createRule({
|
|
5
|
+
name: 'no-raw-http-server',
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'problem',
|
|
8
|
+
docs: {
|
|
9
|
+
description: 'Disallow HTTP server framework imports (express, koa, fastify, aws-lambda) in domain files.',
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
noRawHttpServer: 'HTTP framework import "{{source}}" is not allowed in domain files. The Domain Module runtime handles transport binding.\n fixHint: Remove the `{{source}}` import. Domain primitives (defineApi, defineWebhook) are transport-agnostic — the runtime handles HTTP binding.\n kNodeRef: K:convention:tier-1-foundations',
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
},
|
|
16
|
+
defaultOptions: [],
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
ImportDeclaration(node) {
|
|
20
|
+
const source = node.source.value;
|
|
21
|
+
if (typeof source === 'string' && BANNED_HTTP_PACKAGES.has(source)) {
|
|
22
|
+
context.report({ node, messageId: 'noRawHttpServer', data: { source } });
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/LawrenceGeeHouse/Mettlecast/blob/develop/packages/eslint-plugin-domain-module/docs/${name}.md`);
|
|
3
|
+
const DEFINE_CALLS = new Set([
|
|
4
|
+
'defineApi', 'defineWebhook', 'defineSubscriber', 'defineSchedule',
|
|
5
|
+
'defineJob', 'defineAction', 'defineIntegration', 'defineEvent', 'defineDomain',
|
|
6
|
+
]);
|
|
7
|
+
export const requireDefinePrimitive = createRule({
|
|
8
|
+
name: 'require-define-primitive',
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'suggestion',
|
|
11
|
+
docs: {
|
|
12
|
+
description: 'Domain primitive files must export at least one define*() call result.',
|
|
13
|
+
},
|
|
14
|
+
messages: {
|
|
15
|
+
missingDefinePrimitive: 'No define*() call found in this domain primitive file. Export at least one defineApi(), defineWebhook(), defineSubscriber(), etc.\n fixHint: Wrap the file\'s export in a define*() call (e.g., `export const handler = defineApi({...})`). See domain.config.ts for the list of available primitives.\n kNodeRef: K:runbook:add-domain',
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
},
|
|
19
|
+
defaultOptions: [],
|
|
20
|
+
create(context) {
|
|
21
|
+
let foundDefineCall = false;
|
|
22
|
+
return {
|
|
23
|
+
CallExpression(node) {
|
|
24
|
+
const callee = node.callee;
|
|
25
|
+
if (callee.type === 'Identifier' && DEFINE_CALLS.has(callee.name)) {
|
|
26
|
+
foundDefineCall = true;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
'Program:exit'(node) {
|
|
30
|
+
if (!foundDefineCall) {
|
|
31
|
+
context.report({ node, messageId: 'missingDefinePrimitive' });
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mettlecast/eslint-plugin-domain-module",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": ["dist"],
|
|
6
|
+
"exports": {
|
|
7
|
+
".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"registry": "https://registry.npmjs.org",
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc -p tsconfig.json",
|
|
15
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
16
|
+
"test": "vitest"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"eslint": "^9.0.0"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@typescript-eslint/utils": "^8.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/eslint": "^9.0.0",
|
|
26
|
+
"@types/node": "^20.0.0",
|
|
27
|
+
"@typescript-eslint/rule-tester": "^8.0.0",
|
|
28
|
+
"typescript": "^5.0.0",
|
|
29
|
+
"vitest": "^2.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|