Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/funny-ghosts-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@redocly/openapi-core": minor
---

Added support for clearing the plugins cache.
48 changes: 26 additions & 22 deletions packages/core/src/config/__tests__/config-resolvers.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import path from 'node:path';
import { after } from 'node:test';
import { fileURLToPath } from 'node:url';
import util from 'node:util';

Expand All @@ -10,13 +9,24 @@ import { Config } from '../config.js';
import recommended from '../recommended.js';
import type { RawUniversalConfig, RawGovernanceConfig } from '../types.js';

vi.mock('node:module', () => ({
default: {
createRequire: () => ({
resolve: (path: string) => `/mock/path/${path}`,
}),
},
}));
const resolveOverrides = new Map<string, string>();

vi.mock('node:module', async (importOriginal) => {
const nodeModule = (await importOriginal<Record<string, any>>()).default;
return {
default: {
...nodeModule,
createRequire(baseUrl: string) {
const req = nodeModule.createRequire(baseUrl);
return Object.assign((id: string) => req(id), req, {
resolve: (id: string, opts?: any) => {
return resolveOverrides.get(id) ?? req.resolve(id, opts);
},
}) as typeof req;
},
},
};
});

const __dirname = path.dirname(fileURLToPath(import.meta.url));

Expand All @@ -42,6 +52,10 @@ function makeDocument(rawConfig: RawUniversalConfig, configPath: string = '') {
};
}

afterEach(() => {
resolveOverrides.clear();
});

describe('resolveConfig', () => {
it('should return the config with no recommended', async () => {
const { resolvedConfig, plugins } = await resolveConfig({
Expand Down Expand Up @@ -638,20 +652,10 @@ describe('resolveApis', () => {
});

it('should work with npm dependencies', async () => {
after(() => {
(globalThis as any).__webpack_require__ = undefined;
(globalThis as any).__non_webpack_require__ = undefined;
});

(globalThis as any).__webpack_require__ = () => {};
(globalThis as any).__non_webpack_require__ = (p: string) =>
p === '/mock/path/test-plugin'
? {
id: 'npm-test-plugin',
}
: {
id: 'local-test-plugin',
};
const fixturesDir = path.join(__dirname, 'fixtures/resolve-config');
const pluginPath = path.join(fixturesDir, 'plugin.js');
resolveOverrides.set('test-plugin', pluginPath);
resolveOverrides.set('fixtures/plugin.cjs', pluginPath);

const { resolvedConfig } = await resolveConfig({
rawConfigDocument: makeDocument(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
plugins:
- plugin-with-init-logic.js
- plugin-with-init-logic.cjs
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
plugins:
- realm-plugin.js
- realm-plugin.cjs
46 changes: 34 additions & 12 deletions packages/core/src/config/config-resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ import {

// Cache instantiated plugins during a single execution
const pluginsCache: Map<string, Plugin[]> = new Map();
let pluginsCacheVersion = 0;

export const clearPluginsCache = (): void => {
pluginsCache.clear();
pluginsCacheVersion++;
};

export type PluginResolveInfo = {
absolutePath: string;
Expand Down Expand Up @@ -191,6 +197,32 @@ export const preResolvePluginPath = (
}
};

async function loadPluginModule(
absolutePluginPath: string,
pluginsCacheVersion: number
): Promise<Record<string, unknown>> {
// CJS: drop this plugin's subtree from `require.cache` so `require()` does not reuse a stale graph.
if (absolutePluginPath.endsWith('.cjs')) {
const nodeRequire = module.createRequire(absolutePluginPath);
if (pluginsCacheVersion) {
const pluginDir = path.dirname(absolutePluginPath) + path.sep;
for (const cachedPath of Object.keys(nodeRequire.cache)) {
if (cachedPath.startsWith(pluginDir)) {
delete nodeRequire.cache[cachedPath];
}
}
}
return nodeRequire(absolutePluginPath);
}

// ESM: the loader keys modules by URL; a distinct `?v=` makes dynamic `import()` bypass the prior cache entry.
const pluginUrl = url.pathToFileURL(absolutePluginPath);
if (pluginsCacheVersion) {
pluginUrl.searchParams.set('v', String(pluginsCacheVersion));
}
return import(pluginUrl.href);
}

export async function resolvePlugins(
plugins: (string | Plugin)[],
configDir: string
Expand All @@ -215,18 +247,8 @@ export async function resolvePlugins(
).absolutePath;

if (!pluginsCache.has(absolutePluginPath)) {
let requiredPlugin: ImportedPlugin | undefined;

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore FIXME: investigate if we still need this (2.0)
if (typeof __webpack_require__ === 'function') {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore FIXME: investigate if we still need this (2.0)
requiredPlugin = __non_webpack_require__(absolutePluginPath);
} else {
const mod = await import(url.pathToFileURL(absolutePluginPath).pathname);
requiredPlugin = mod.default || mod;
}
const mod = await loadPluginModule(absolutePluginPath, pluginsCacheVersion);
const requiredPlugin: ImportedPlugin | undefined = mod.default || mod;

const pluginCreatorOptions = { contentDir: configDir };

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export {
createConfig,
findConfig,
resolvePlugins,
clearPluginsCache,
ConfigValidationError,
Config, // FIXME: export it as a type
type RawUniversalConfig,
Expand Down
Loading