@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 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,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
+ });
@@ -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,4 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const flowDomainOwnership: ESLintUtils.RuleModule<"owningDomainMismatch" | "missingOwningDomain", [], unknown, ESLintUtils.RuleListener> & {
3
+ name: string;
4
+ };
@@ -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,4 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const noCrossDomainInternalImport: ESLintUtils.RuleModule<"noCrossDomainImport", [], unknown, ESLintUtils.RuleListener> & {
3
+ name: string;
4
+ };
@@ -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,4 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const noRawAwsSdk: ESLintUtils.RuleModule<"noRawAwsSdk", [], unknown, ESLintUtils.RuleListener> & {
3
+ name: string;
4
+ };
@@ -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,4 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const noRawHttpServer: ESLintUtils.RuleModule<"noRawHttpServer", [], unknown, ESLintUtils.RuleListener> & {
3
+ name: string;
4
+ };
@@ -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,4 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const requireDefinePrimitive: ESLintUtils.RuleModule<"missingDefinePrimitive", [], unknown, ESLintUtils.RuleListener> & {
3
+ name: string;
4
+ };
@@ -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
+ }