mirror of
https://github.com/LukeHagar/redocly-cli.git
synced 2025-12-06 04:21:09 +00:00
feat: cache instantiated plugins and resolve realm-specific properties (#1716)
This commit is contained in:
committed by
GitHub
parent
e046f7b1de
commit
92652e2346
5
.changeset/flat-days-train.md
Normal file
5
.changeset/flat-days-train.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@redocly/openapi-core": minor
|
||||
---
|
||||
|
||||
Added a mechanism that resolves plugin properties specific to the Reunite-hosted product family.
|
||||
6
.changeset/tame-laws-film.md
Normal file
6
.changeset/tame-laws-film.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@redocly/openapi-core": minor
|
||||
"@redocly/cli": minor
|
||||
---
|
||||
|
||||
Added a cache for resolved plugins to ensure that plugins are only instantiated once during a single execution.
|
||||
@@ -18,7 +18,6 @@ info:
|
||||
name: test 2
|
||||
components: {}
|
||||
|
||||
Deprecated plugin format detected: plugin
|
||||
Deprecated plugin format detected: plugin
|
||||
bundling ./openapi.yaml...
|
||||
📦 Created a bundle for ./openapi.yaml at stdout <test>ms.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as util from 'util';
|
||||
import { colorize } from '../../logger';
|
||||
import { Asserts, asserts } from '../../rules/common/assertions/asserts';
|
||||
import { resolveStyleguideConfig, resolveApis, resolveConfig } from '../config-resolvers';
|
||||
@@ -91,6 +92,59 @@ describe('resolveStyleguideConfig', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should instantiate the plugin once', async () => {
|
||||
// Called by plugin during init
|
||||
const deprecateSpy = jest.spyOn(util, 'deprecate');
|
||||
|
||||
const config = {
|
||||
...baseStyleguideConfig,
|
||||
extends: ['local-config-with-plugin-init.yaml'],
|
||||
};
|
||||
|
||||
await resolveStyleguideConfig({
|
||||
styleguideConfig: config,
|
||||
configPath,
|
||||
});
|
||||
|
||||
expect(deprecateSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
await resolveStyleguideConfig({
|
||||
styleguideConfig: config,
|
||||
configPath,
|
||||
});
|
||||
|
||||
// Should not execute the init logic again
|
||||
expect(deprecateSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should resolve realm plugin properties', async () => {
|
||||
const config = {
|
||||
...baseStyleguideConfig,
|
||||
extends: ['local-config-with-realm-plugin.yaml'],
|
||||
};
|
||||
|
||||
const { plugins } = await resolveStyleguideConfig({
|
||||
styleguideConfig: config,
|
||||
configPath,
|
||||
});
|
||||
|
||||
const localPlugin = plugins?.find((p) => p.id === 'realm-plugin');
|
||||
expect(localPlugin).toBeDefined();
|
||||
|
||||
expect(localPlugin).toMatchObject({
|
||||
id: 'realm-plugin',
|
||||
processContent: expect.any(Function),
|
||||
afterRoutesCreated: expect.any(Function),
|
||||
loaders: {
|
||||
'test-loader': expect.any(Function),
|
||||
},
|
||||
requiredEntitlements: ['test-entitlement'],
|
||||
ssoConfigSchema: { type: 'object', additionalProperties: true },
|
||||
redoclyConfigSchema: { type: 'object', additionalProperties: false },
|
||||
ejectIgnore: ['Navbar.tsx', 'Footer.tsx'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve local file config with esm plugin', async () => {
|
||||
const config = {
|
||||
...baseStyleguideConfig,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
plugins:
|
||||
- plugin-with-init-logic.js
|
||||
@@ -0,0 +1,2 @@
|
||||
plugins:
|
||||
- realm-plugin.js
|
||||
@@ -0,0 +1,9 @@
|
||||
var util = require('util');
|
||||
|
||||
module.exports = function pluginWithInitLogic() {
|
||||
util.deprecate(() => null);
|
||||
|
||||
return {
|
||||
id: 'test-plugin',
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
module.exports = function realmPlugin() {
|
||||
return {
|
||||
id: 'realm-plugin',
|
||||
processContent: () => {},
|
||||
afterRoutesCreated: () => {},
|
||||
loaders: { 'test-loader': () => {} },
|
||||
requiredEntitlements: ['test-entitlement'],
|
||||
ssoConfigSchema: { type: 'object', additionalProperties: true },
|
||||
redoclyConfigSchema: { type: 'object', additionalProperties: false },
|
||||
ejectIgnore: ['Navbar.tsx', 'Footer.tsx'],
|
||||
};
|
||||
};
|
||||
@@ -43,6 +43,9 @@ const DEFAULT_PROJECT_PLUGIN_PATHS = ['@theme/plugin.js', '@theme/plugin.cjs', '
|
||||
// Workaround for dynamic imports being transpiled to require by Typescript: https://github.com/microsoft/TypeScript/issues/43329#issuecomment-811606238
|
||||
const _importDynamic = new Function('modulePath', 'return import(modulePath)');
|
||||
|
||||
// Cache instantiated plugins during a single execution
|
||||
const pluginsCache: Map<string, Plugin> = new Map();
|
||||
|
||||
export async function resolveConfigFileAndRefs({
|
||||
configPath,
|
||||
externalRefResolver = new BaseResolver(),
|
||||
@@ -111,9 +114,9 @@ export async function resolveConfig({
|
||||
);
|
||||
}
|
||||
|
||||
function getDefaultPluginPath(configPath: string): string | undefined {
|
||||
function getDefaultPluginPath(configDir: string): string | undefined {
|
||||
for (const pluginPath of DEFAULT_PROJECT_PLUGIN_PATHS) {
|
||||
const absolutePluginPath = path.resolve(path.dirname(configPath), pluginPath);
|
||||
const absolutePluginPath = path.resolve(configDir, pluginPath);
|
||||
if (existsSync(absolutePluginPath)) {
|
||||
return pluginPath;
|
||||
}
|
||||
@@ -123,32 +126,58 @@ function getDefaultPluginPath(configPath: string): string | undefined {
|
||||
|
||||
export async function resolvePlugins(
|
||||
plugins: (string | Plugin)[] | null,
|
||||
configPath: string = ''
|
||||
configDir: string = ''
|
||||
): Promise<Plugin[]> {
|
||||
if (!plugins) return [];
|
||||
|
||||
// TODO: implement or reuse Resolver approach so it will work in node and browser envs
|
||||
const requireFunc = async (plugin: string | Plugin): Promise<ImportedPlugin | undefined> => {
|
||||
const requireFunc = async (plugin: string | Plugin): Promise<Plugin | undefined> => {
|
||||
if (isString(plugin)) {
|
||||
try {
|
||||
const maybeAbsolutePluginPath = path.resolve(path.dirname(configPath), plugin);
|
||||
const maybeAbsolutePluginPath = path.resolve(configDir, plugin);
|
||||
|
||||
const absolutePluginPath = existsSync(maybeAbsolutePluginPath)
|
||||
? maybeAbsolutePluginPath
|
||||
: // For plugins imported from packages specifically
|
||||
require.resolve(plugin);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
if (typeof __webpack_require__ === 'function') {
|
||||
if (!pluginsCache.has(absolutePluginPath)) {
|
||||
let requiredPlugin: ImportedPlugin | undefined;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
return __non_webpack_require__(absolutePluginPath);
|
||||
} else {
|
||||
// you can import both cjs and mjs
|
||||
const mod = await _importDynamic(pathToFileURL(absolutePluginPath).href);
|
||||
return mod.default || mod;
|
||||
if (typeof __webpack_require__ === 'function') {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
requiredPlugin = __non_webpack_require__(absolutePluginPath);
|
||||
} else {
|
||||
// you can import both cjs and mjs
|
||||
const mod = await _importDynamic(pathToFileURL(absolutePluginPath).href);
|
||||
requiredPlugin = mod.default || mod;
|
||||
}
|
||||
|
||||
const pluginCreatorOptions = { contentDir: configDir };
|
||||
|
||||
const pluginModule = isDeprecatedPluginFormat(requiredPlugin)
|
||||
? requiredPlugin
|
||||
: isCommonJsPlugin(requiredPlugin)
|
||||
? await requiredPlugin(pluginCreatorOptions)
|
||||
: await requiredPlugin?.default?.(pluginCreatorOptions);
|
||||
|
||||
if (pluginModule?.id && isDeprecatedPluginFormat(requiredPlugin)) {
|
||||
logger.info(`Deprecated plugin format detected: ${pluginModule.id}\n`);
|
||||
}
|
||||
|
||||
if (pluginModule) {
|
||||
pluginsCache.set(absolutePluginPath, {
|
||||
...pluginModule,
|
||||
path: plugin,
|
||||
absolutePath: absolutePluginPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return pluginsCache.get(absolutePluginPath);
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to load plugin "${plugin}": ${e.message}\n\n${e.stack}`);
|
||||
}
|
||||
@@ -162,7 +191,7 @@ export async function resolvePlugins(
|
||||
/**
|
||||
* Include the default plugin automatically if it's not in configuration
|
||||
*/
|
||||
const defaultPluginPath = getDefaultPluginPath(configPath);
|
||||
const defaultPluginPath = getDefaultPluginPath(configDir);
|
||||
if (defaultPluginPath) {
|
||||
plugins.push(defaultPluginPath);
|
||||
}
|
||||
@@ -182,24 +211,12 @@ export async function resolvePlugins(
|
||||
resolvedPlugins.add(p);
|
||||
}
|
||||
|
||||
const requiredPlugin: ImportedPlugin | undefined = await requireFunc(p);
|
||||
|
||||
const pluginCreatorOptions = { contentDir: path.dirname(configPath) };
|
||||
|
||||
const pluginModule = isDeprecatedPluginFormat(requiredPlugin)
|
||||
? requiredPlugin
|
||||
: isCommonJsPlugin(requiredPlugin)
|
||||
? await requiredPlugin(pluginCreatorOptions)
|
||||
: await requiredPlugin?.default?.(pluginCreatorOptions);
|
||||
const pluginModule: Plugin | undefined = await requireFunc(p);
|
||||
|
||||
if (!pluginModule) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isString(p) && pluginModule.id && isDeprecatedPluginFormat(requiredPlugin)) {
|
||||
logger.info(`Deprecated plugin format detected: ${pluginModule.id}\n`);
|
||||
}
|
||||
|
||||
const id = pluginModule.id;
|
||||
if (typeof id !== 'string') {
|
||||
throw new Error(
|
||||
@@ -313,7 +330,10 @@ export async function resolvePlugins(
|
||||
plugin.assertions = pluginModule.assertions;
|
||||
}
|
||||
|
||||
return plugin;
|
||||
return {
|
||||
...pluginModule,
|
||||
...plugin,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
@@ -371,7 +391,10 @@ async function resolveAndMergeNestedStyleguideConfig(
|
||||
? // In browser, we don't support plugins from config file yet
|
||||
[defaultPlugin]
|
||||
: getUniquePlugins(
|
||||
await resolvePlugins([...(styleguideConfig?.plugins || []), defaultPlugin], configPath)
|
||||
await resolvePlugins(
|
||||
[...(styleguideConfig?.plugins || []), defaultPlugin],
|
||||
path.dirname(configPath)
|
||||
)
|
||||
);
|
||||
const pluginPaths = styleguideConfig?.plugins
|
||||
?.filter(isString)
|
||||
|
||||
@@ -30,6 +30,7 @@ import type {
|
||||
BuiltInOAS3RuleId,
|
||||
BuiltInArazzoRuleId,
|
||||
} from '../types/redocly-yaml';
|
||||
import type { JSONSchema } from 'json-schema-to-ts';
|
||||
|
||||
export type RuleSeverity = ProblemSeverity | 'off';
|
||||
|
||||
@@ -136,12 +137,27 @@ export type AssertionsConfig = Record<string, CustomFunction>;
|
||||
|
||||
export type Plugin = {
|
||||
id: string;
|
||||
|
||||
configs?: Record<string, PluginStyleguideConfig>;
|
||||
rules?: CustomRulesConfig;
|
||||
preprocessors?: PreprocessorsConfig;
|
||||
decorators?: DecoratorsConfig;
|
||||
typeExtension?: TypeExtensionsConfig;
|
||||
assertions?: AssertionsConfig;
|
||||
|
||||
// Realm properties
|
||||
path?: string;
|
||||
absolutePath?: string;
|
||||
processContent?: (actions: any, content: any) => Promise<void> | void;
|
||||
afterRoutesCreated?: (actions: any, content: any) => Promise<void> | void;
|
||||
loaders?: Record<
|
||||
string,
|
||||
(path: string, context: any, reportError: (error: Error) => void) => Promise<unknown>
|
||||
>;
|
||||
requiredEntitlements?: string[];
|
||||
ssoConfigSchema?: JSONSchema;
|
||||
redoclyConfigSchema?: JSONSchema;
|
||||
ejectIgnore?: string[];
|
||||
};
|
||||
|
||||
type PluginCreatorOptions = {
|
||||
|
||||
Reference in New Issue
Block a user