mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
build(server): lint:boundaries counts the RPC and MCP layers as domains and sees getRepository calls
src/nest-rpc, src/nest-mcp and src/mcp import domains and are imported by them, but the domain map only knew src/nest, so their edges dropped out of domainCycles and their repository injections out of foreignRepositories. Each layer now counts as one domain. The repository rule reads the syntax tree for @InjectRepository(X) and for getRepository(X) on an EntityManager, so a comment no longer counts and a foreign read through the EntityManager no longer passes. The baseline takes the cycles and the repository reaches the wider view finds; settings -> Users goes, it was a comment.
This commit is contained in:
+1
-1
@@ -37,7 +37,7 @@ npm run lint:response-contracts # route handlers without @ResponseContract may o
|
||||
npm run contracts:dto-open # open shapes in the createZodDto schemas may only shrink (runs in the unit tests)
|
||||
```
|
||||
|
||||
**`lint:boundaries`** (`scripts/import-boundaries.mjs`) parses every import under `src/` and holds seven lists in `scripts/import-boundaries-baseline.json`: load-time file cycles (type-only and lazy imports, and the MikroORM entity relations, do not count), dependencies between two `nest/<domain>` folders that already depend on each other, shared-kernel files (`nest/common`, `nest/database`, `nest/app-config`, `nest/auth-core`) importing a domain, a domain importing another domain's internals, `src/db` importing `src/nest`, a file under a domain's `providers/` folder importing that domain's orchestrator (`<domain>.service.ts`) or `MapsService` (a provider knows how to ask one source; the orchestrator decides which one answers), and a domain injecting (`@InjectRepository`) the repository of an entity another domain owns (`foreignRepositories`; the owner of every injected entity is listed in `scripts/repository-owners.json`, and an entity missing there stops the run). For trip visibility, inject `TripAccessService` (trip-membership) instead of `TripsRepository`. A domain's public surface is its `*.module`, `*.service`, `*.guard`, `*.decorator`, `*.types`, `*.interface`, `*.contract`, `*.constants` and `*.logger` files plus the short `PUBLIC_FILES` list in the script; anything else is an internal. A new entry fails the check, and so does a baseline entry that no longer occurs, so the fix lands with `npm run lint:boundaries -- --update`, which drops such entries and never adds one.
|
||||
**`lint:boundaries`** (`scripts/import-boundaries.mjs`) parses every import under `src/` and holds seven lists (`src/nest-rpc`, `src/nest-mcp` and `src/mcp` count as one domain each, so moving a file out of `src/nest` does not take its edges out of the count) in `scripts/import-boundaries-baseline.json`: load-time file cycles (type-only and lazy imports, and the MikroORM entity relations, do not count), dependencies between two `nest/<domain>` folders that already depend on each other, shared-kernel files (`nest/common`, `nest/database`, `nest/app-config`, `nest/auth-core`) importing a domain, a domain importing another domain's internals, `src/db` importing `src/nest`, a file under a domain's `providers/` folder importing that domain's orchestrator (`<domain>.service.ts`) or `MapsService` (a provider knows how to ask one source; the orchestrator decides which one answers), and a domain reaching (`@InjectRepository(X)` or `em.getRepository(X)`) the repository of an entity another domain owns (`foreignRepositories`; the owner of every injected entity is listed in `scripts/repository-owners.json`, and an entity missing there stops the run). For trip visibility, inject `TripAccessService` (trip-membership) instead of `TripsRepository`. A domain's public surface is its `*.module`, `*.service`, `*.guard`, `*.decorator`, `*.types`, `*.interface`, `*.contract`, `*.constants` and `*.logger` files plus the short `PUBLIC_FILES` list in the script; anything else is an internal. A new entry fails the check, and so does a baseline entry that no longer occurs, so the fix lands with `npm run lint:boundaries -- --update`, which drops such entries and never adds one.
|
||||
|
||||
**`contracts:dto-open`** (`tests/unit/contracts/dto-open-shapes.test.ts`, wrapped by `scripts/dto-open-shapes.mjs`) finds every `export class X extends createZodDto(...)` under `src/`, imports it and counts the open shapes (loose object, record of unknown, `z.unknown()`/`z.any()`, `z.custom()`) in the schema the Zod pipe validates, with shared's `contracts:open` walk. Each DTO class is held to its entry in `scripts/dto-open-shapes-baseline.json`, and a new one may hold none, whatever its schema is called and wherever it is declared. A `createZodDto` call that is not an exported class, or two DTO classes with one name, fail the check. After narrowing a schema, `npm run contracts:dto-open -- --update` lowers the baseline (it never raises or adds an entry). It needs `shared/` built.
|
||||
|
||||
|
||||
@@ -2,27 +2,106 @@
|
||||
"fileCycles": [],
|
||||
"domainCycles": [
|
||||
"accommodations -> assignments",
|
||||
"accommodations -> nest-rpc",
|
||||
"accommodations -> places",
|
||||
"assignments -> days",
|
||||
"assignments -> journey",
|
||||
"assignments -> nest-rpc",
|
||||
"atlas -> nest-rpc",
|
||||
"auth -> budget",
|
||||
"auth -> files",
|
||||
"auth -> plugins",
|
||||
"budget -> nest-rpc",
|
||||
"calendar -> days",
|
||||
"calendar -> reservations",
|
||||
"categories -> nest-rpc",
|
||||
"collab -> files",
|
||||
"collab -> nest-rpc",
|
||||
"collections -> nest-rpc",
|
||||
"collections -> places",
|
||||
"day-notes -> nest-rpc",
|
||||
"days -> accommodations",
|
||||
"days -> assignments",
|
||||
"days -> nest-rpc",
|
||||
"days -> places",
|
||||
"files -> nest-rpc",
|
||||
"geo -> maps",
|
||||
"integrations -> assignments",
|
||||
"integrations -> atlas",
|
||||
"integrations -> journey",
|
||||
"integrations -> places",
|
||||
"integrations -> reservations",
|
||||
"journey -> atlas",
|
||||
"journey -> files",
|
||||
"journey -> nest-rpc",
|
||||
"maps -> geo",
|
||||
"memories -> photos",
|
||||
"nest-rpc -> plugins",
|
||||
"packing -> nest-rpc",
|
||||
"photos -> memories",
|
||||
"places -> accommodations",
|
||||
"places -> assignments",
|
||||
"places -> atlas",
|
||||
"places -> journey",
|
||||
"places -> nest-rpc",
|
||||
"plugins -> accommodations",
|
||||
"plugins -> assignments",
|
||||
"plugins -> atlas",
|
||||
"plugins -> budget",
|
||||
"plugins -> categories",
|
||||
"plugins -> collab",
|
||||
"plugins -> collections",
|
||||
"plugins -> day-notes",
|
||||
"plugins -> days",
|
||||
"plugins -> files",
|
||||
"plugins -> journey",
|
||||
"plugins -> nest-rpc",
|
||||
"plugins -> packing",
|
||||
"plugins -> places",
|
||||
"plugins -> reservations",
|
||||
"plugins -> tags",
|
||||
"plugins -> todo",
|
||||
"plugins -> trips",
|
||||
"plugins -> vacay",
|
||||
"plugins -> weather",
|
||||
"reservations -> accommodations",
|
||||
"reservations -> assignments",
|
||||
"reservations -> budget",
|
||||
"reservations -> days",
|
||||
"reservations -> integrations",
|
||||
"reservations -> nest-rpc",
|
||||
"tags -> nest-rpc",
|
||||
"todo -> nest-rpc",
|
||||
"trip-members -> auth",
|
||||
"trip-members -> budget",
|
||||
"trip-members -> trips",
|
||||
"trip-read-model -> accommodations",
|
||||
"trip-read-model -> budget",
|
||||
"trip-read-model -> collab",
|
||||
"trip-read-model -> days",
|
||||
"trip-read-model -> files",
|
||||
"trip-read-model -> packing",
|
||||
"trip-read-model -> places",
|
||||
"trip-read-model -> reservations",
|
||||
"trip-read-model -> todo",
|
||||
"trip-read-model -> trip-members",
|
||||
"trip-read-model -> trips",
|
||||
"trips -> accommodations",
|
||||
"trips -> budget",
|
||||
"trips -> calendar",
|
||||
"trips -> collab",
|
||||
"trips -> days",
|
||||
"trips -> files",
|
||||
"trips -> nest-rpc",
|
||||
"trips -> packing",
|
||||
"trips -> places",
|
||||
"trips -> reservations",
|
||||
"trips -> todo",
|
||||
"trips -> trip-members",
|
||||
"trips -> trip-read-model"
|
||||
"trips -> trip-read-model",
|
||||
"trips -> vacay",
|
||||
"vacay -> nest-rpc",
|
||||
"weather -> nest-rpc"
|
||||
],
|
||||
"sharedImportsDomain": [
|
||||
"nest/common/idempotency-cleanup.job.ts -> nest/audit/audit-log.logger.ts",
|
||||
@@ -61,6 +140,7 @@
|
||||
"journey -> storage/storage-upload.factory.ts",
|
||||
"maps -> geo/nominatim.client.ts",
|
||||
"maps -> settings/instance-api-keys.ts",
|
||||
"nest-rpc -> plugins/protocol/envelope.ts",
|
||||
"place-enrichment -> maps/maps.helpers.ts",
|
||||
"place-enrichment -> maps/trek-places.client.ts",
|
||||
"places -> atlas/atlas-geo.ts",
|
||||
@@ -144,8 +224,10 @@
|
||||
"auth -> TripMembers (owned by trip-membership)",
|
||||
"auth -> Trips (owned by trips)",
|
||||
"auth-core -> AppSettings (owned by settings)",
|
||||
"auth-core -> UserSessions (owned by sessions)",
|
||||
"auth-core -> Users (owned by auth)",
|
||||
"auth-core -> WebauthnCredentials (owned by auth)",
|
||||
"backup -> UserSessions (owned by sessions)",
|
||||
"booking-import -> Days (owned by days)",
|
||||
"booking-import -> Reservations (owned by reservations)",
|
||||
"budget -> Places (owned by places)",
|
||||
@@ -164,6 +246,7 @@
|
||||
"collections -> TripMembers (owned by trip-membership)",
|
||||
"collections -> Trips (owned by trips)",
|
||||
"collections -> Users (owned by auth)",
|
||||
"common -> Users (owned by auth)",
|
||||
"day-notes -> Days (owned by days)",
|
||||
"days -> DayAccommodations (owned by accommodations)",
|
||||
"days -> DayAssignments (owned by assignments)",
|
||||
@@ -184,6 +267,7 @@
|
||||
"files -> DayAssignments (owned by assignments)",
|
||||
"files -> Places (owned by places)",
|
||||
"files -> Reservations (owned by reservations)",
|
||||
"files -> UserSessions (owned by sessions)",
|
||||
"files -> Users (owned by auth)",
|
||||
"google-quota -> AppSettings (owned by settings)",
|
||||
"integrations -> AppSettings (owned by settings)",
|
||||
@@ -210,6 +294,8 @@
|
||||
"memories -> TripPhotos (owned by photos)",
|
||||
"memories -> Trips (owned by trips)",
|
||||
"memories -> Users (owned by auth)",
|
||||
"nest-rpc -> Trips (owned by trips)",
|
||||
"nest-rpc -> Users (owned by auth)",
|
||||
"notifications -> AppSettings (owned by settings)",
|
||||
"notifications -> Settings (owned by settings)",
|
||||
"notifications -> TodoItems (owned by todo)",
|
||||
@@ -235,6 +321,10 @@
|
||||
"places -> Tags (owned by tags)",
|
||||
"places -> TripMembers (owned by trip-membership)",
|
||||
"places -> Trips (owned by trips)",
|
||||
"platform -> Photos (owned by photos)",
|
||||
"platform -> ShareTokens (owned by share)",
|
||||
"platform -> UserSessions (owned by sessions)",
|
||||
"platform -> Users (owned by auth)",
|
||||
"plugins -> DayAccommodations (owned by accommodations)",
|
||||
"plugins -> Days (owned by days)",
|
||||
"plugins -> JourneyEntries (owned by journey)",
|
||||
@@ -243,6 +333,7 @@
|
||||
"plugins -> Reservations (owned by reservations)",
|
||||
"plugins -> Settings (owned by settings)",
|
||||
"plugins -> Trips (owned by trips)",
|
||||
"plugins -> UserSessions (owned by sessions)",
|
||||
"plugins -> Users (owned by auth)",
|
||||
"public-api -> BucketList (owned by atlas)",
|
||||
"public-api -> DayNotes (owned by day-notes)",
|
||||
@@ -274,7 +365,6 @@
|
||||
"roadtrip -> Users (owned by auth)",
|
||||
"route-usage -> AppSettings (owned by settings)",
|
||||
"school-holidays -> VacayHolidayCalendars (owned by vacay)",
|
||||
"settings -> Users (owned by auth)",
|
||||
"share -> BudgetItems (owned by budget)",
|
||||
"share -> Categories (owned by categories)",
|
||||
"share -> CollabMessages (owned by collab)",
|
||||
@@ -299,6 +389,30 @@
|
||||
"trip-members -> Users (owned by auth)",
|
||||
"trip-membership -> Trips (owned by trips)",
|
||||
"trip-read-model -> Trips (owned by trips)",
|
||||
"trips -> AssignmentParticipants (owned by assignments)",
|
||||
"trips -> BudgetCategoryOrder (owned by budget)",
|
||||
"trips -> BudgetItemMembers (owned by budget)",
|
||||
"trips -> BudgetItemPayers (owned by budget)",
|
||||
"trips -> BudgetItems (owned by budget)",
|
||||
"trips -> DayAccommodations (owned by accommodations)",
|
||||
"trips -> DayAssignments (owned by assignments)",
|
||||
"trips -> DayNotes (owned by day-notes)",
|
||||
"trips -> Days (owned by days)",
|
||||
"trips -> JourneyEntries (owned by journey)",
|
||||
"trips -> PackingBags (owned by packing)",
|
||||
"trips -> PackingItems (owned by packing)",
|
||||
"trips -> Places (owned by places)",
|
||||
"trips -> Reservations (owned by reservations)",
|
||||
"trips -> RoadtripDayBoundaries (owned by roadtrip)",
|
||||
"trips -> RoadtripDayTracks (owned by roadtrip)",
|
||||
"trips -> RoadtripPreferences (owned by roadtrip)",
|
||||
"trips -> RoadtripVias (owned by roadtrip)",
|
||||
"trips -> Tags (owned by tags)",
|
||||
"trips -> TodoItems (owned by todo)",
|
||||
"trips -> TourWaypoints (owned by tours)",
|
||||
"trips -> Tours (owned by tours)",
|
||||
"trips -> TripMembers (owned by trip-membership)",
|
||||
"trips -> Users (owned by auth)",
|
||||
"unsplash -> AppSettings (owned by settings)",
|
||||
"unsplash -> Users (owned by auth)",
|
||||
"vacay -> SchoolHolidayRegions (owned by school-holidays)"
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
* So are the imports of src/db/entities/: MikroORM relations
|
||||
* point both ways by design and are read through `() => X`
|
||||
* thunks after every file has loaded.
|
||||
* domainCycles the same, between src/nest/<domain> folders: domain A
|
||||
* domainCycles the same, between src/nest/<domain> folders (and the
|
||||
* layers in LAYERS, src/nest-rpc, src/nest-mcp and src/mcp,
|
||||
* each counted as one domain): domain A
|
||||
* imports from B while B (directly or through others)
|
||||
* imports from A. Keyed by the domain pair, so a further
|
||||
* import along a dependency that already exists is not new;
|
||||
@@ -38,8 +40,10 @@
|
||||
* dbImportsNest a file under src/db/ importing from src/nest/. The data
|
||||
* layer sits below the domains, never the other way round.
|
||||
* foreignRepositories
|
||||
* a class in src/nest/<A>/ injecting (@InjectRepository) the
|
||||
* repository of an entity another domain owns. Ownership is
|
||||
* a class in src/nest/<A>/ (or one of LAYERS) reaching the
|
||||
* repository of an entity another domain owns, through
|
||||
* @InjectRepository(X) or a getRepository(X) call on an
|
||||
* EntityManager. Ownership is
|
||||
* the map in scripts/repository-owners.json (entity class ->
|
||||
* owning domain folder); an injected entity missing from it
|
||||
* stops the run. Keyed by domain and entity, so a second
|
||||
@@ -282,7 +286,22 @@ function cycleEdges(pairs) {
|
||||
return out;
|
||||
}
|
||||
|
||||
const domainOf = (file) => file.match(/^nest\/([^/]+)\//)?.[1] ?? null;
|
||||
/**
|
||||
* Layers outside src/nest that work like a domain of their own: the plugin RPC
|
||||
* kit, the MCP decorator layer and the process-wide MCP state. They import
|
||||
* domains and domains import them, so they take part in domainCycles and
|
||||
* foreignRepositories under their folder name; moving a file out of src/nest
|
||||
* does not take its edges out of the count.
|
||||
*/
|
||||
export const LAYERS = ['nest-rpc', 'nest-mcp', 'mcp'];
|
||||
|
||||
/** The domain a src/-relative path belongs to: nest/<domain>/..., one of LAYERS, or null. */
|
||||
export const domainOf = (file) => {
|
||||
const nest = file.match(/^nest\/([^/]+)\//)?.[1];
|
||||
if (nest) return nest;
|
||||
const top = file.split('/')[0];
|
||||
return file.includes('/') && LAYERS.includes(top) ? top : null;
|
||||
};
|
||||
|
||||
/** The orchestrators no provider may import: its own domain's main service, and MapsService everywhere. */
|
||||
function isOrchestratorOf(providerFile, target) {
|
||||
@@ -293,6 +312,8 @@ function isOrchestratorOf(providerFile, target) {
|
||||
const isProviderFile = (file) => /^nest\/[^/]+\/(?:.+\/)?providers\//.test(file);
|
||||
|
||||
export function isPublic(target) {
|
||||
// A layer outside src/nest is a toolkit every domain is written against.
|
||||
if (!target.startsWith('nest/')) return true;
|
||||
const inner = target.slice('nest/'.length);
|
||||
const domain = inner.split('/')[0];
|
||||
if (SHARED_DOMAINS.has(domain)) return true;
|
||||
@@ -300,18 +321,44 @@ export function isPublic(target) {
|
||||
return PUBLIC_SUFFIXES.some((s) => target.endsWith(s));
|
||||
}
|
||||
|
||||
/** Every @InjectRepository(Entity) under src/nest: { domain, entity, file }. */
|
||||
/**
|
||||
* The entity classes a file reaches a repository of: `@InjectRepository(X)`
|
||||
* and `<anything>.getRepository(X)`, read from the syntax tree so a comment or
|
||||
* a string naming them does not count.
|
||||
*/
|
||||
export function repositoryEntitiesOf(text, fileName = 'file.ts') {
|
||||
const source = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
|
||||
const found = [];
|
||||
const visit = (node) => {
|
||||
if (ts.isCallExpression(node) && node.arguments.length > 0 && ts.isIdentifier(node.arguments[0])) {
|
||||
const callee = node.expression;
|
||||
const name = ts.isIdentifier(callee)
|
||||
? callee.text
|
||||
: ts.isPropertyAccessExpression(callee)
|
||||
? callee.name.text
|
||||
: null;
|
||||
if (name === 'InjectRepository' || name === 'getRepository') found.push(node.arguments[0].text);
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(source);
|
||||
return found;
|
||||
}
|
||||
|
||||
/** Every repository a class under src/nest or one of LAYERS reaches: { domain, entity, file }. */
|
||||
export function collectRepositoryInjections(serverDir) {
|
||||
const src = join(serverDir, 'src');
|
||||
const nest = join(src, 'nest');
|
||||
if (!existsSync(nest)) return [];
|
||||
const found = [];
|
||||
for (const file of walk(nest)) {
|
||||
const rel = relative(src, file).split(sep).join('/');
|
||||
const domain = domainOf(rel);
|
||||
if (!domain) continue;
|
||||
for (const m of readFileSync(file, 'utf8').matchAll(/@InjectRepository\(\s*([A-Za-z_$][\w$]*)\s*\)/g)) {
|
||||
found.push({ domain, entity: m[1], file: rel });
|
||||
for (const top of ['nest', ...LAYERS]) {
|
||||
const dir = join(src, top);
|
||||
if (!existsSync(dir)) continue;
|
||||
for (const file of walk(dir)) {
|
||||
const rel = relative(src, file).split(sep).join('/');
|
||||
const domain = domainOf(rel);
|
||||
if (!domain) continue;
|
||||
for (const entity of repositoryEntitiesOf(readFileSync(file, 'utf8'), file)) {
|
||||
found.push({ domain, entity, file: rel });
|
||||
}
|
||||
}
|
||||
}
|
||||
return found;
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"PasswordResetTokens": "auth",
|
||||
"PhotoProviderFields": "addons",
|
||||
"PhotoProviders": "addons",
|
||||
"Photos": "photos",
|
||||
"PlaceDetailsCache": "maps",
|
||||
"PlaceRatings": "places",
|
||||
"PlaceRegions": "atlas",
|
||||
@@ -89,9 +90,11 @@
|
||||
"Reservations": "reservations",
|
||||
"RoadtripDayBoundaries": "roadtrip",
|
||||
"RoadtripDayTracks": "roadtrip",
|
||||
"RoadtripPreferences": "roadtrip",
|
||||
"RoadtripPreferencesEntity": "roadtrip",
|
||||
"RoadtripVias": "roadtrip",
|
||||
"RouteUsageDaily": "route-usage",
|
||||
"SchedulerLeases": "scheduling",
|
||||
"SchoolHolidayCountries": "school-holidays",
|
||||
"SchoolHolidayPeriods": "school-holidays",
|
||||
"SchoolHolidayRegions": "school-holidays",
|
||||
|
||||
@@ -291,6 +291,56 @@ describe('import-boundaries.mjs', () => {
|
||||
expect(res.out).toContain('no owner for injected entity Mystery (nest/a/a.service.ts)');
|
||||
});
|
||||
|
||||
it('BOUND-015: getRepository calls count like injections, a comment naming one does not', () => {
|
||||
const owners = JSON.stringify({ Trips: 'trips', Users: 'auth' });
|
||||
const dir = serverRoot({
|
||||
'nest/perm/perm.guard.ts': [
|
||||
'// em.getRepository(Users) would be foreign too',
|
||||
'class G { run(em: any) { return em.getRepository(Trips).findAccessible(1, 2); } }',
|
||||
'export { G };',
|
||||
'',
|
||||
].join('\n'),
|
||||
});
|
||||
writeFileSync(path.join(dir, 'scripts/repository-owners.json'), owners);
|
||||
const res = run(dir);
|
||||
expect(res.status).toBe(1);
|
||||
expect(res.out).toContain('FAIL foreignRepositories: perm -> Trips (owned by trips)');
|
||||
expect(res.out).not.toContain('Users');
|
||||
});
|
||||
|
||||
it('BOUND-016: src/nest-rpc, src/nest-mcp and src/mcp are domains of their own for cycles and repositories', () => {
|
||||
const owners = JSON.stringify({ Trips: 'trips' });
|
||||
const dir = serverRoot({
|
||||
'nest/addons/addons.service.ts': [
|
||||
"import { kit } from '../../nest-rpc/rpc-kit/kit';",
|
||||
'export class AddonsService { k = kit; }',
|
||||
'',
|
||||
].join('\n'),
|
||||
'nest-rpc/rpc-kit/kit.ts': 'export const kit = 1;\n',
|
||||
'nest-rpc/plugin-guards.service.ts': [
|
||||
"import { AddonsService } from '../nest/addons/addons.service';",
|
||||
'@InjectRepository(Trips) class PluginGuardsService { a = AddonsService; }',
|
||||
'export { PluginGuardsService };',
|
||||
'',
|
||||
].join('\n'),
|
||||
'nest/weather/weather.rpc.ts': [
|
||||
"import { PluginGuardsService } from '../../nest-rpc/plugin-guards.service';",
|
||||
'export const r = PluginGuardsService;',
|
||||
'',
|
||||
].join('\n'),
|
||||
});
|
||||
writeFileSync(path.join(dir, 'scripts/repository-owners.json'), owners);
|
||||
const res = run(dir);
|
||||
expect(res.status).toBe(1);
|
||||
expect(res.out).toContain('FAIL domainCycles: addons -> nest-rpc');
|
||||
expect(res.out).toContain('FAIL domainCycles: nest-rpc -> addons');
|
||||
expect(res.out).toContain('FAIL foreignRepositories: nest-rpc -> Trips (owned by trips)');
|
||||
// The layers are toolkits: a domain using one reaches no internals, and
|
||||
// weather, which nothing imports back, is on no cycle.
|
||||
expect(res.out).not.toContain('FAIL domainInternals');
|
||||
expect(res.out).not.toContain('weather -> nest-rpc');
|
||||
});
|
||||
|
||||
it('BOUND-010: resolves .js specifiers and index files', () => {
|
||||
const dir = serverRoot({
|
||||
'x/a.ts': "import { b } from './b.js';\nimport { c } from './c';\nexport const a = [b, c];\n",
|
||||
|
||||
Reference in New Issue
Block a user