Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.

import { parseAst } from 'rollup/parseAst';

import type { ModuleDependency, ParsedModuleRecord } from './module-graph';
import { ensureProgram } from './type-guards';
import { walkModuleGraph } from './walk-module-graph';

const buildRoot = '/project';

function createRecord(
id: string,
staticDependencies: string[] = [],
unsupportedDependencies: ModuleDependency[] = [],
): ParsedModuleRecord {
return {
id,
ast: ensureProgram(parseAst('export const value = true;'), id),
staticDependencies,
unsupportedDependencies,
};
}

describe('Backend Functions - module graph walk', () => {
test('Should visit each reachable local module once', () => {
const modules = new Map<string, ParsedModuleRecord>([
[
'/project/src/backend/actions.backend.ts',
createRecord('/project/src/backend/actions.backend.ts', [
'/project/src/backend/helper.ts',
]),
],
[
'/project/src/backend/helper.ts',
createRecord('/project/src/backend/helper.ts', ['/project/src/backend/shared.ts']),
],
[
'/project/src/backend/shared.ts',
createRecord('/project/src/backend/shared.ts', ['/project/src/backend/helper.ts']),
],
]);
const visited: string[] = [];

walkModuleGraph(
'/project/src/backend/actions.backend.ts',
modules,
buildRoot,
(context) => {
visited.push(context.moduleId);
},
);

expect(visited).toEqual([
'/project/src/backend/actions.backend.ts',
'/project/src/backend/helper.ts',
'/project/src/backend/shared.ts',
]);
});

test('Should skip dependencies outside the app-local parseable graph', () => {
const modules = new Map<string, ParsedModuleRecord>([
[
'/project/src/backend/actions.backend.ts',
createRecord('/project/src/backend/actions.backend.ts', [
'/project/src/backend/helper.ts',
'/project/node_modules/package/index.js',
'/project/src/backend/data.json',
'/external/helper.ts',
]),
],
['/project/src/backend/helper.ts', createRecord('/project/src/backend/helper.ts')],
]);
const visited: string[] = [];

walkModuleGraph(
'/project/src/backend/actions.backend.ts',
modules,
buildRoot,
(context) => {
visited.push(context.moduleId);
},
);

expect(visited).toEqual([
'/project/src/backend/actions.backend.ts',
'/project/src/backend/helper.ts',
]);
});

test('Should fail closed when the entry record is missing', () => {
expect(() => {
walkModuleGraph(
'/project/src/backend/actions.backend.ts',
new Map(),
buildRoot,
() => {},
);
}).toThrow(
'Unsupported local module graph for /project/src/backend/actions.backend.ts: missing module record for /project/src/backend/actions.backend.ts could hide an action-catalog connectionId.',
);
});

test('Should fail closed when a reachable local dependency was not collected', () => {
const modules = new Map<string, ParsedModuleRecord>([
[
'/project/src/backend/actions.backend.ts',
createRecord('/project/src/backend/actions.backend.ts', [
'/project/src/backend/helper.ts',
]),
],
]);

expect(() => {
walkModuleGraph(
'/project/src/backend/actions.backend.ts',
modules,
buildRoot,
() => {},
);
}).toThrow(
'Unsupported local module graph for /project/src/backend/actions.backend.ts: uncollected local import /project/src/backend/helper.ts from /project/src/backend/actions.backend.ts could hide an action-catalog connectionId.',
);
});

test('Should fail closed for unsupported reachable local dependencies', () => {
const modules = new Map<string, ParsedModuleRecord>([
[
'/project/src/backend/actions.backend.ts',
createRecord(
'/project/src/backend/actions.backend.ts',
[],
[{ kind: 'dynamic-import', specifier: './helper' }],
),
],
]);

expect(() => {
walkModuleGraph(
'/project/src/backend/actions.backend.ts',
modules,
buildRoot,
() => {},
);
}).toThrow(
'Unsupported local module graph for /project/src/backend/actions.backend.ts: dynamic-import ./helper could hide an action-catalog connectionId.',
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.

import {
type ParsedModuleRecord,
shouldTraverseCollectedModule,
unsupportedModuleGraphDependency,
} from './module-graph';

export interface ModuleGraphWalkContext {
entryId: string;
moduleId: string;
record: ParsedModuleRecord;
}

/**
* Walks every collected app-local module statically reachable from a backend
* entry and applies fail-closed graph validation before following dependency
* edges.
*/
export function walkModuleGraph(
entryId: string,
modules: ReadonlyMap<string, ParsedModuleRecord>,
buildRoot: string,
visit: (context: ModuleGraphWalkContext) => void,
): void {
// Traverse from the real backend entry, not the virtual wrapper used by
// the backend build. Every backend export in this file receives this same
// conservative file-level allowlist.
const pending = [entryId];
const visited = new Set<string>();

while (pending.length > 0) {
// Process each collected module at most once so local cycles cannot
// loop forever.
const moduleId = pending.shift()!;
if (visited.has(moduleId)) {
continue;
}
visited.add(moduleId);

// A reachable local module that Rollup did not parse means the
// collected graph is incomplete, so fail closed instead of silently
// omitting a possible connection ID.
const record = modules.get(moduleId);
if (!record) {
throw unsupportedModuleGraphDependency(
entryId,
`missing module record for ${moduleId}`,
);
}

visit({ entryId, moduleId, record });

// Dynamic local imports and local require calls can hide reachable
// action-catalog calls from static traversal. Treat them as unsupported
// graph shapes for this PR.
for (const dependency of record.unsupportedDependencies) {
throw unsupportedModuleGraphDependency(
entryId,
`${dependency.kind} ${dependency.specifier}`,
);
}

// Follow only collected local source modules. Package imports, virtual
// entries, generated files, and files outside buildRoot are ignored by
// design because they are outside the app-local backend graph.
for (const dependencyId of record.staticDependencies) {
if (!shouldTraverseCollectedModule(dependencyId, buildRoot)) {
continue;
}

// A local dependency can be statically reachable but absent from
// the collector if Rollup did not parse it. Fail closed rather than
// trusting an incomplete allowlist.
if (!modules.has(dependencyId)) {
throw unsupportedModuleGraphDependency(
entryId,
`uncollected local import ${dependencyId} from ${record.id}`,
);
}

pending.push(dependencyId);
}
}
}
Loading