chore(server): eslint keeps fast-xml-parser inside place-import and the WebDAV client

This commit is contained in:
Maurice
2026-10-10 13:07:28 +02:00
parent 1389fe6984
commit 04f38f9876
2 changed files with 129 additions and 19 deletions
+50 -19
View File
@@ -140,6 +140,27 @@ const MCP_DEMO_SELECTORS = [
{ selector: "CallExpression[callee.name='demoDenied']", message: DEMO_GATE_MESSAGE },
];
// XML is read in exactly two places: the GPX and KML codecs of place-import, and
// the WebDAV client (PROPFIND answers). One parser setup per format instead of
// one per caller is what keeps a GPX read in one module the same as in another.
const XML_PARSER_PATH = {
name: 'fast-xml-parser',
message:
'XML is parsed in src/nest/place-import (gpx.codec.ts, kml.codec.ts) and the WebDAV client only. Read GPX/KML through PlaceImportService instead of configuring another parser.',
};
const DRIVER_PATH = {
name: 'better-sqlite3',
message:
'The SQLite driver is an implementation detail of src/db. Go through the ORM (src/db/entities + src/db/repositories) or UnitOfWork; the handle itself is owned by src/db/database.ts.',
};
const SERVICES_WALL = {
group: ['**/services/*', '**/services/**/*'],
message:
'src/services/ is deleted. New backend code goes to src/nest/<domain>/ (service + controller + module, registered in app.module.ts). See src/nest/README.md.',
};
export default tseslint.config(
gitignore({ strict: false }),
{
@@ -319,25 +340,31 @@ export default tseslint.config(
'src/db/reseat-booked-nights.ts',
],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'better-sqlite3',
message:
'The SQLite driver is an implementation detail of src/db. Go through the ORM (src/db/entities + src/db/repositories) or UnitOfWork; the handle itself is owned by src/db/database.ts.',
},
],
patterns: [
{
group: ['**/services/*', '**/services/**/*'],
message:
'src/services/ is deleted. New backend code goes to src/nest/<domain>/ (service + controller + module, registered in app.module.ts). See src/nest/README.md.',
},
],
},
],
'no-restricted-imports': ['error', { paths: [DRIVER_PATH, XML_PARSER_PATH], patterns: [SERVICES_WALL] }],
},
},
{
// The files the block above leaves out for the driver still may not parse
// XML: restated with XML_PARSER_PATH and the services wall only.
files: [
'src/db/database.ts',
'src/db/connection.ts',
'src/db/orm-driver.ts',
'src/db/durability.ts',
'src/nest/plugins/host/plugin-data.service.ts',
'src/db/reseat-booked-nights.ts',
],
rules: {
'no-restricted-imports': ['error', { paths: [XML_PARSER_PATH], patterns: [SERVICES_WALL] }],
},
},
{
// The two homes of the XML parser (see XML_PARSER_PATH). Restated without
// it, for the same flat-config reason as above: this block's options
// replace the previous block's for the files it matches.
files: ['src/nest/place-import/**/*.ts', 'src/nest/doc-sync/providers/webdav.client.ts'],
rules: {
'no-restricted-imports': ['error', { paths: [DRIVER_PATH], patterns: [SERVICES_WALL] }],
},
},
{
@@ -372,6 +399,7 @@ export default tseslint.config(
message:
'Repositories are dialect-neutral: import from @mikro-orm/sql / @mikro-orm/core. The driver lives in src/db/orm-driver.ts.',
},
XML_PARSER_PATH,
],
patterns: [
{
@@ -433,6 +461,9 @@ export default tseslint.config(
'no-restricted-imports': [
'error',
{
// Restated for the flat-config reason above: without them the kit
// would drop the driver and XML walls every other src file holds.
paths: [DRIVER_PATH, XML_PARSER_PATH],
patterns: [
{
// A regex rather than gitignore-style groups: the two allowed files sit
@@ -0,0 +1,79 @@
import tsParser from '@typescript-eslint/parser';
import { ESLint, Linter } from 'eslint';
import path from 'path';
import { describe, expect, it } from 'vitest';
// fast-xml-parser is allowed in place-import's codecs and the WebDAV client
// only, through no-restricted-imports in eslint.config.mjs. That rule's options
// are replaced, not merged, by every flat-config block that restates it, so a
// later block can silently drop the restriction (or the driver and services
// walls beside it). This asks ESLint for the options it really applies to a
// path and runs them over an import of each restricted module.
const serverRoot = path.resolve(__dirname, '../../..');
const eslint = new ESLint({ cwd: serverRoot });
async function restrictedImportsFor(file: string): Promise<unknown[]> {
const config = (await eslint.calculateConfigForFile(path.join(serverRoot, file))) as {
rules?: Record<string, [string, ...unknown[]]>;
};
return config.rules?.['no-restricted-imports']?.slice(1) ?? [];
}
async function flags(file: string, source: string): Promise<boolean> {
const options = await restrictedImportsFor(file);
const linter = new Linter({ configType: 'flat' });
const result = linter.verify(
source,
[{ files: ['**/*.ts'], languageOptions: { parser: tsParser }, rules: { 'no-restricted-imports': ['error', ...(options as object[])] } }],
'probe.ts',
);
return result.some((m) => m.ruleId === 'no-restricted-imports');
}
const XML = "import { XMLParser } from 'fast-xml-parser';\n";
const DRIVER = "import Database from 'better-sqlite3';\n";
const SERVICES = "import { x } from '../services/x';\n";
describe('fast-xml-parser stays in place-import and the WebDAV client', () => {
it.each([
'src/nest/places/places.service.ts',
'src/nest/collections/collections.service.ts',
'src/nest/tours/tours.service.ts',
'src/nest/maps/maps.service.ts',
'src/db/repositories/Places.repository.ts',
// The driver's own homes and the plugin sandbox's database, which the
// driver wall leaves out, and the RPC kit, which restates the rule.
'src/db/database.ts',
'src/db/connection.ts',
'src/db/orm-driver.ts',
'src/db/durability.ts',
'src/db/reseat-booked-nights.ts',
'src/nest/plugins/host/plugin-data.service.ts',
'src/nest-rpc/rpc-kit/registry.ts',
])('refuses it in %s', async (file) => {
expect(await flags(file, XML)).toBe(true);
});
it.each([
'src/nest/place-import/gpx.codec.ts',
'src/nest/place-import/kml.codec.ts',
'src/nest/doc-sync/providers/webdav.client.ts',
])('allows it in %s, and keeps the driver and services walls there', async (file) => {
expect(await flags(file, XML)).toBe(false);
expect(await flags(file, DRIVER)).toBe(true);
expect(await flags(file, SERVICES)).toBe(true);
});
it('keeps the services wall in the driver-exempt files and the driver wall in the RPC kit', async () => {
expect(await flags('src/nest/plugins/host/plugin-data.service.ts', SERVICES)).toBe(true);
expect(await flags('src/nest/plugins/host/plugin-data.service.ts', DRIVER)).toBe(false);
expect(await flags('src/nest-rpc/rpc-kit/registry.ts', DRIVER)).toBe(true);
});
it('keeps the driver and services walls where the parser is refused', async () => {
expect(await flags('src/nest/places/places.service.ts', DRIVER)).toBe(true);
expect(await flags('src/nest/places/places.service.ts', SERVICES)).toBe(true);
});
});