feat(F-001): scaffold modular monolith skeleton with boundary checker
- TypeScript + Fastify skeleton under project/ (src/modules, shared, infrastructure, app) - scripts/check-module-boundaries.mjs enforcing module public-API rules (tested with fixtures) - GET /health endpoint, error envelope without stack leakage - specs/F-001-scaffold (SPEC/DESIGN/TASKS/TESTS), spec/tech.md dependency justification - 30-ticket MercadoDeVida roadmap in backlog/features.json, spec/roadmap.md - All gates approved: reviewer, security, qa; verify.sh green
This commit is contained in:
4
project/.gitignore
vendored
Normal file
4
project/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
coverage/
|
||||
*.log
|
||||
0
project/.gitkeep
Normal file
0
project/.gitkeep
Normal file
6
project/.prettierignore
Normal file
6
project/.prettierignore
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
coverage/
|
||||
package-lock.json
|
||||
scripts/tests/fixtures/
|
||||
design_prompt.md
|
||||
6
project/.prettierrc.json
Normal file
6
project/.prettierrc.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
38
project/README.md
Normal file
38
project/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# MercadoDeVida backend — modular monolith skeleton
|
||||
|
||||
TypeScript + Fastify modular monolith. Simple code, clear modules, small changes, no magic.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js >= 22
|
||||
- npm
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
npm install # install dependencies
|
||||
npm run build # compile to dist/
|
||||
npm start # run compiled server (PORT, HOST env vars)
|
||||
npm test # vitest unit/integration tests
|
||||
npm run typecheck # tsc --noEmit
|
||||
npm run lint # eslint + prettier check
|
||||
npm run lint:boundaries # module boundary check
|
||||
```
|
||||
|
||||
## Layout
|
||||
|
||||
```text
|
||||
src/
|
||||
├── app/ # composition root (only place that wires modules)
|
||||
├── infrastructure/ # http server entrypoint (later: db, redis, providers)
|
||||
├── modules/ # business modules, one folder each
|
||||
│ └── health/ # exemplar module: public API only via index.ts
|
||||
└── shared/ # cross-cutting helpers (error envelope)
|
||||
```
|
||||
|
||||
## Module rules
|
||||
|
||||
- A module exposes its public API only through its `index.ts`.
|
||||
- Files inside a module may import: own subtree, `src/shared`, Node builtins, npm packages.
|
||||
- Code outside modules (app/infrastructure) may import a module only via its `index.ts`.
|
||||
- `npm run lint:boundaries` enforces these rules.
|
||||
1315
project/design_prompt.md
Normal file
1315
project/design_prompt.md
Normal file
File diff suppressed because it is too large
Load Diff
34
project/eslint.config.mjs
Normal file
34
project/eslint.config.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
// @ts-check
|
||||
import eslint from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ['dist/**', 'node_modules/**', 'coverage/**', 'scripts/tests/fixtures/**'],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
prettier,
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||
],
|
||||
'no-console': ['error', { allow: ['error'] }],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.mjs'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
console: 'readonly',
|
||||
process: 'readonly',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-console': ['error', { allow: ['error', 'log'] }],
|
||||
},
|
||||
},
|
||||
);
|
||||
3732
project/package-lock.json
generated
Normal file
3732
project/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
project/package.json
Normal file
30
project/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "mercadodevida-backend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "MercadoDeVida vNext backend - modular monolith skeleton",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"start": "node dist/infrastructure/http/server.js",
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"lint:boundaries": "node scripts/check-module-boundaries.mjs src",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"fastify": "^5.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"prettier": "^3.4.0",
|
||||
"typescript": "^5.7.0",
|
||||
"typescript-eslint": "^8.18.0",
|
||||
"vitest": "^3.0.0"
|
||||
}
|
||||
}
|
||||
112
project/scripts/check-module-boundaries.mjs
Normal file
112
project/scripts/check-module-boundaries.mjs
Normal file
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Module boundary checker.
|
||||
*
|
||||
* Rules (see specs/F-001-scaffold/DESIGN.md):
|
||||
* R1: Files inside <root>/modules/<mod>/ may only import their own module
|
||||
* subtree, <root>/shared/, Node builtins, or npm packages.
|
||||
* R2: Files outside modules may import a module only through its index.ts.
|
||||
* Deep imports into <root>/modules/<mod>/... are violations.
|
||||
*
|
||||
* Usage: node scripts/check-module-boundaries.mjs <srcRoot>
|
||||
* Exit codes: 0 = clean, 1 = violations found, 2 = usage error.
|
||||
*/
|
||||
|
||||
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
|
||||
function isBareSpecifier(specifier) {
|
||||
return !specifier.startsWith('.') && !specifier.startsWith('/');
|
||||
}
|
||||
|
||||
function walk(dir, files = []) {
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const full = path.join(dir, entry);
|
||||
const stat = statSync(full);
|
||||
if (stat.isDirectory()) {
|
||||
if (entry === 'node_modules' || entry === 'dist' || entry === 'fixtures') continue;
|
||||
walk(full, files);
|
||||
} else if (entry.endsWith('.ts') && !entry.endsWith('.d.ts')) {
|
||||
files.push(full);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function extractSpecifiers(source) {
|
||||
const specifiers = [];
|
||||
const fromRegex = /(?:import|export)\s+[^'"]*?from\s*['"]([^'"]+)['"]/g;
|
||||
const sideEffectRegex = /^\s*import\s*['"]([^'"]+)['"]/gm;
|
||||
const dynamicRegex = /import\(\s*['"]([^'"]+)['"]\s*\)/g;
|
||||
let match;
|
||||
while ((match = fromRegex.exec(source)) !== null) specifiers.push(match[1]);
|
||||
while ((match = sideEffectRegex.exec(source)) !== null) specifiers.push(match[1]);
|
||||
while ((match = dynamicRegex.exec(source)) !== null) specifiers.push(match[1]);
|
||||
return specifiers;
|
||||
}
|
||||
|
||||
/** Strip a .js/.ts extension so we can compare logical paths. */
|
||||
function stripExtension(p) {
|
||||
return p.replace(/\.(js|ts|mjs|cjs)$/, '');
|
||||
}
|
||||
|
||||
function checkFile(file, rootAbs, violations) {
|
||||
const source = readFileSync(file, 'utf8');
|
||||
const relFile = path.relative(rootAbs, file);
|
||||
const fileDir = path.dirname(file);
|
||||
const relFileParts = relFile.split(path.sep);
|
||||
const sourceInModule =
|
||||
relFileParts[0] === 'modules' && relFileParts.length >= 2 ? relFileParts[1] : null;
|
||||
|
||||
for (const specifier of extractSpecifiers(source)) {
|
||||
if (isBareSpecifier(specifier)) continue; // npm package or node builtin
|
||||
|
||||
const targetAbs = stripExtension(path.resolve(fileDir, specifier));
|
||||
const relTarget = path.relative(rootAbs, targetAbs);
|
||||
const targetParts = relTarget.split(path.sep);
|
||||
const targetInModule =
|
||||
targetParts[0] === 'modules' && targetParts.length >= 2 ? targetParts[1] : null;
|
||||
|
||||
if (sourceInModule !== null) {
|
||||
// R1: stay inside own module or go to shared
|
||||
const ownModule = relTarget.startsWith(path.join('modules', sourceInModule) + path.sep);
|
||||
const toShared = targetParts[0] === 'shared';
|
||||
if (!ownModule && !toShared) {
|
||||
violations.push(
|
||||
`R1 violation: ${relFile} imports "${specifier}" (escapes module "${sourceInModule}")`,
|
||||
);
|
||||
}
|
||||
} else if (targetInModule !== null) {
|
||||
// R2: outside code may only use a module's public index
|
||||
const isIndex = targetParts.length === 3 && targetParts[2] === 'index';
|
||||
if (!isIndex) {
|
||||
violations.push(
|
||||
`R2 violation: ${relFile} imports "${specifier}" (deep import into module "${targetInModule}", use its index)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const rootArg = process.argv[2];
|
||||
if (!rootArg) {
|
||||
console.error('Usage: node scripts/check-module-boundaries.mjs <srcRoot>');
|
||||
process.exit(2);
|
||||
}
|
||||
const rootAbs = path.resolve(rootArg);
|
||||
const files = walk(rootAbs);
|
||||
const violations = [];
|
||||
for (const file of files) {
|
||||
checkFile(file, rootAbs, violations);
|
||||
}
|
||||
if (violations.length > 0) {
|
||||
for (const violation of violations) console.error(violation);
|
||||
console.error(`Boundary check FAILED: ${violations.length} violation(s)`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`Boundary check OK: ${files.length} file(s) checked`);
|
||||
}
|
||||
|
||||
await main();
|
||||
49
project/scripts/tests/boundary-checker.test.ts
Normal file
49
project/scripts/tests/boundary-checker.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { execFile } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const scriptPath = path.resolve(here, '..', 'check-module-boundaries.mjs');
|
||||
const fixturesDir = path.resolve(here, 'fixtures');
|
||||
|
||||
function runChecker(rootDir: string): Promise<{ code: number; output: string }> {
|
||||
return new Promise((resolve) => {
|
||||
execFile(
|
||||
process.execPath,
|
||||
[scriptPath, rootDir],
|
||||
{ encoding: 'utf8' },
|
||||
(error, stdout, stderr) => {
|
||||
const code = error !== null && 'code' in error ? (error.code as number) : 0;
|
||||
resolve({ code, output: `${stdout}\n${stderr}` });
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
describe('check-module-boundaries', () => {
|
||||
it('passes on a clean tree', async () => {
|
||||
const { code, output } = await runChecker(path.join(fixturesDir, 'ok'));
|
||||
expect(output).toContain('Boundary check OK');
|
||||
expect(code).toBe(0);
|
||||
});
|
||||
|
||||
it('fails when a module imports another module internal file (R1)', async () => {
|
||||
const { code, output } = await runChecker(path.join(fixturesDir, 'cross-module-internal'));
|
||||
expect(output).toContain('R1 violation');
|
||||
expect(code).toBe(1);
|
||||
});
|
||||
|
||||
it('fails when outside code deep-imports a module internal file (R2)', async () => {
|
||||
const { code, output } = await runChecker(path.join(fixturesDir, 'deep-from-app'));
|
||||
expect(output).toContain('R2 violation');
|
||||
expect(code).toBe(1);
|
||||
});
|
||||
|
||||
it('allows outside code to import a module index (R2 ok)', async () => {
|
||||
const { code, output } = await runChecker(path.join(fixturesDir, 'ok'));
|
||||
expect(output).toContain('Boundary check OK');
|
||||
expect(code).toBe(0);
|
||||
});
|
||||
});
|
||||
2
project/scripts/tests/fixtures/cross-module-internal/modules/alpha/api/routes.ts
vendored
Normal file
2
project/scripts/tests/fixtures/cross-module-internal/modules/alpha/api/routes.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import { secret } from '../../beta/domain/secret.js';
|
||||
export const routes = [secret];
|
||||
1
project/scripts/tests/fixtures/cross-module-internal/modules/beta/domain/secret.ts
vendored
Normal file
1
project/scripts/tests/fixtures/cross-module-internal/modules/beta/domain/secret.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export const secret = 'secret';
|
||||
2
project/scripts/tests/fixtures/deep-from-app/app/main.ts
vendored
Normal file
2
project/scripts/tests/fixtures/deep-from-app/app/main.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import { routes } from '../modules/alpha/api/routes.js';
|
||||
export const main = [routes];
|
||||
1
project/scripts/tests/fixtures/deep-from-app/modules/alpha/api/routes.ts
vendored
Normal file
1
project/scripts/tests/fixtures/deep-from-app/modules/alpha/api/routes.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export const routes = 'routes';
|
||||
3
project/scripts/tests/fixtures/ok/app/main.ts
vendored
Normal file
3
project/scripts/tests/fixtures/ok/app/main.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import { alpha } from '../modules/alpha/index.js';
|
||||
import { beta } from '../modules/beta/index.js';
|
||||
export const main = [alpha, beta];
|
||||
3
project/scripts/tests/fixtures/ok/modules/alpha/api/routes.ts
vendored
Normal file
3
project/scripts/tests/fixtures/ok/modules/alpha/api/routes.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import { alpha } from '../index.js';
|
||||
import { sharedUtil } from '../../../shared/util.js';
|
||||
export const routes = [alpha, sharedUtil];
|
||||
1
project/scripts/tests/fixtures/ok/modules/alpha/index.ts
vendored
Normal file
1
project/scripts/tests/fixtures/ok/modules/alpha/index.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export const alpha = 'alpha';
|
||||
1
project/scripts/tests/fixtures/ok/modules/beta/index.ts
vendored
Normal file
1
project/scripts/tests/fixtures/ok/modules/beta/index.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export const beta = 'beta';
|
||||
1
project/scripts/tests/fixtures/ok/shared/util.ts
vendored
Normal file
1
project/scripts/tests/fixtures/ok/shared/util.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export const sharedUtil = 'shared';
|
||||
31
project/src/app/build-app.ts
Normal file
31
project/src/app/build-app.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import Fastify, { type FastifyInstance } from 'fastify';
|
||||
import type { FastifyError, FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { registerHealthRoutes } from '../modules/health/index.js';
|
||||
import { errorEnvelope } from '../shared/errors.js';
|
||||
|
||||
function notFoundHandler(_request: FastifyRequest, reply: FastifyReply): void {
|
||||
void reply.code(404).send(errorEnvelope(404, 'Not Found'));
|
||||
}
|
||||
|
||||
function errorHandler(error: FastifyError, _request: FastifyRequest, reply: FastifyReply): void {
|
||||
const statusCode =
|
||||
error.statusCode !== undefined && error.statusCode >= 400 ? error.statusCode : 500;
|
||||
const message = statusCode >= 500 ? 'Internal Server Error' : error.message;
|
||||
void reply.code(statusCode).send(errorEnvelope(statusCode, message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Composition root. The only place allowed to wire modules together.
|
||||
*/
|
||||
export async function buildApp(): Promise<FastifyInstance> {
|
||||
const app = Fastify({ logger: false });
|
||||
|
||||
app.setErrorHandler(errorHandler);
|
||||
app.setNotFoundHandler(notFoundHandler);
|
||||
|
||||
await app.register(async (instance) => {
|
||||
await registerHealthRoutes(instance);
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
25
project/src/app/tests/build-app.test.ts
Normal file
25
project/src/app/tests/build-app.test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildApp } from '../build-app.js';
|
||||
|
||||
describe('composition root', () => {
|
||||
it('exposes GET /health through the wired app', async () => {
|
||||
const app = await buildApp();
|
||||
const response = await app.inject({ method: 'GET', url: '/health' });
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toEqual({ status: 'ok' });
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it('returns 404 JSON envelope for unknown routes without leaking internals', async () => {
|
||||
const app = await buildApp();
|
||||
const response = await app.inject({ method: 'GET', url: '/does-not-exist' });
|
||||
|
||||
expect(response.statusCode).toBe(404);
|
||||
const body = response.json() as { error: { statusCode: number; message: string } };
|
||||
expect(body.error.statusCode).toBe(404);
|
||||
expect(body.error.message).toBe('Not Found');
|
||||
expect(response.body).not.toContain('stack');
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
12
project/src/infrastructure/http/server.ts
Normal file
12
project/src/infrastructure/http/server.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { buildApp } from '../../app/build-app.js';
|
||||
|
||||
const port = Number(process.env.PORT ?? 3000);
|
||||
const host = process.env.HOST ?? '0.0.0.0';
|
||||
|
||||
try {
|
||||
const app = await buildApp();
|
||||
await app.listen({ port, host });
|
||||
} catch (error) {
|
||||
console.error('Failed to start HTTP server', error);
|
||||
process.exit(1);
|
||||
}
|
||||
11
project/src/modules/health/api/health.routes.ts
Normal file
11
project/src/modules/health/api/health.routes.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
|
||||
interface HealthResponse {
|
||||
status: 'ok';
|
||||
}
|
||||
|
||||
export async function registerHealthRoutes(app: FastifyInstance): Promise<void> {
|
||||
app.get('/health', async (): Promise<HealthResponse> => {
|
||||
return { status: 'ok' };
|
||||
});
|
||||
}
|
||||
5
project/src/modules/health/index.ts
Normal file
5
project/src/modules/health/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Public API of the health module. Everything a module exposes to the
|
||||
* outside world goes through this file.
|
||||
*/
|
||||
export { registerHealthRoutes } from './api/health.routes.js';
|
||||
18
project/src/modules/health/tests/health.test.ts
Normal file
18
project/src/modules/health/tests/health.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import Fastify from 'fastify';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { registerHealthRoutes } from '../index.js';
|
||||
|
||||
describe('health module API', () => {
|
||||
it('GET /health returns 200 and status ok', async () => {
|
||||
const app = Fastify({ logger: false });
|
||||
await app.register(async (instance) => {
|
||||
await registerHealthRoutes(instance);
|
||||
});
|
||||
|
||||
const response = await app.inject({ method: 'GET', url: '/health' });
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toEqual({ status: 'ok' });
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
15
project/src/shared/errors.ts
Normal file
15
project/src/shared/errors.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Shared error envelope. Single shape for every API error.
|
||||
* Never leak stack traces or internal details to the client.
|
||||
*/
|
||||
|
||||
export interface ErrorEnvelope {
|
||||
error: {
|
||||
statusCode: number;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function errorEnvelope(statusCode: number, message: string): ErrorEnvelope {
|
||||
return { error: { statusCode, message } };
|
||||
}
|
||||
12
project/tsconfig.build.json
Normal file
12
project/tsconfig.build.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"declaration": false,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
18
project/tsconfig.json
Normal file
18
project/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2023",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2023"],
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "scripts/tests/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist", "scripts/tests/fixtures"]
|
||||
}
|
||||
9
project/vitest.config.ts
Normal file
9
project/vitest.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'node',
|
||||
include: ['src/**/*.test.ts', 'scripts/tests/**/*.test.ts'],
|
||||
exclude: ['node_modules/**', 'dist/**', 'scripts/tests/fixtures/**'],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user