Skip to content

Commit

Permalink
Merge pull request #236 from vojtechszocs/console-adoption-related-ch…
Browse files Browse the repository at this point in the history
…anges

CONSOLE-3705: Allow transforming plugin manifest at build time
  • Loading branch information
openshift-merge-robot committed Oct 2, 2023
2 parents d457189 + 74b26c5 commit 0eab266
Show file tree
Hide file tree
Showing 12 changed files with 120 additions and 51 deletions.
1 change: 1 addition & 0 deletions packages/lib-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export {
PluginRegistrationMethod,
PluginRuntimeMetadata,
PluginManifest,
TransformPluginManifest,
PendingPlugin,
LoadedPlugin,
FailedPlugin,
Expand Down
14 changes: 7 additions & 7 deletions packages/lib-core/src/runtime/PluginLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { identity, noop } from 'lodash';
import * as semver from 'semver';
import { DEFAULT_REMOTE_ENTRY_CALLBACK } from '../constants';
import type { ResourceFetch } from '../types/fetch';
import type { PluginManifest } from '../types/plugin';
import type { PluginManifest, TransformPluginManifest } from '../types/plugin';
import type { PluginEntryModule, PluginEntryCallback } from '../types/runtime';
import { basicFetch } from '../utils/basic-fetch';
import { settleAllPromises } from '../utils/promise';
Expand Down Expand Up @@ -125,11 +125,11 @@ export type PluginLoaderOptions = Partial<{
sharedScope: AnyObject;

/**
* Post-process the plugin manifest.
* Transform the plugin manifest.
*
* By default, no post-processing is performed on the manifest.
* By default, no transformation is performed on the manifest.
*/
postProcessManifest: (manifest: PluginManifest) => PluginManifest;
transformPluginManifest: TransformPluginManifest;

/**
* Provide access to the plugin's entry module.
Expand Down Expand Up @@ -163,7 +163,7 @@ export class PluginLoader {
fetchImpl: options.fetchImpl ?? basicFetch,
fixedPluginDependencyResolutions: options.fixedPluginDependencyResolutions ?? {},
sharedScope: options.sharedScope ?? {},
postProcessManifest: options.postProcessManifest ?? identity,
transformPluginManifest: options.transformPluginManifest ?? identity,
getPluginEntryModule: options.getPluginEntryModule ?? noop,
};

Expand All @@ -187,10 +187,10 @@ export class PluginLoader {
}

/**
* Post-process and validate the given plugin manifest.
* Transform and validate the given plugin manifest.
*/
processPluginManifest(manifest: PluginManifest) {
const processedManifest = this.options.postProcessManifest(manifest);
const processedManifest = this.options.transformPluginManifest(manifest);

pluginManifestSchema.strict(true).validateSync(processedManifest, { abortEarly: false });

Expand Down
4 changes: 4 additions & 0 deletions packages/lib-core/src/types/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,7 @@ export type FailedPlugin = {
errorMessage: string;
errorCause?: unknown;
};

export type TransformPluginManifest = (manifest: PluginManifest) => PluginManifest & {
[customProperty: string]: unknown;
};
3 changes: 2 additions & 1 deletion packages/lib-core/src/yup-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as yup from 'yup';
import type { PluginRegistrationMethod } from './types/plugin';

/**
* Schema for a valid SemVer string.
* Schema for a valid semver string.
*
* @see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
*/
Expand Down Expand Up @@ -99,6 +99,7 @@ export const pluginRuntimeMetadataSchema = yup.object().required().shape({
name: pluginNameSchema,
version: semverStringSchema,
// TODO(vojtech): Yup lacks native support for map-like structures with arbitrary keys
// TODO(vojtech): we need to validate dependency values as semver ranges
dependencies: yup.object(),
customProperties: yup.object(),
});
Expand Down
3 changes: 3 additions & 0 deletions packages/lib-webpack/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ export {
EncodedExtension,
MapCodeRefsToEncodedCodeRefs,
ExtractExtensionProperties,
PluginRegistrationMethod,
PluginRuntimeMetadata,
PluginManifest,
TransformPluginManifest,
} from '@openshift/dynamic-plugin-sdk/src/shared-webpack';

export {
Expand Down
49 changes: 34 additions & 15 deletions packages/lib-webpack/src/webpack/DynamicRemotePlugin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { DEFAULT_REMOTE_ENTRY_CALLBACK } from '@openshift/dynamic-plugin-sdk/src/shared-webpack';
import type { EncodedExtension } from '@openshift/dynamic-plugin-sdk/src/shared-webpack';
import { isEmpty, mapValues } from 'lodash';
import type {
EncodedExtension,
TransformPluginManifest,
} from '@openshift/dynamic-plugin-sdk/src/shared-webpack';
import { identity, isEmpty, mapValues } from 'lodash';
import * as yup from 'yup';
import { WebpackPluginInstance, Compiler, container } from 'webpack';
import type { PluginBuildMetadata } from '../types/plugin';
Expand Down Expand Up @@ -46,7 +49,7 @@ export type PluginModuleFederationSettings = Partial<{
*
* @see https://webpack.js.org/plugins/module-federation-plugin/#sharescope
*/
sharedScope: string;
sharedScopeName: string;
}>;

/**
Expand Down Expand Up @@ -119,6 +122,13 @@ export type DynamicRemotePluginOptions = {
* Default value: `plugin-manifest.json`.
*/
pluginManifestFilename?: string;

/**
* Transform the plugin manifest before emitting the asset to webpack compilation.
*
* By default, no transformation is performed on the manifest.
*/
transformPluginManifest?: TransformPluginManifest;
};

export class DynamicRemotePlugin implements WebpackPluginInstance {
Expand All @@ -133,6 +143,7 @@ export class DynamicRemotePlugin implements WebpackPluginInstance {
entryCallbackSettings: options.entryCallbackSettings ?? {},
entryScriptFilename: options.entryScriptFilename ?? DEFAULT_ENTRY_SCRIPT,
pluginManifestFilename: options.pluginManifestFilename ?? DEFAULT_MANIFEST,
transformPluginManifest: options.transformPluginManifest ?? identity,
};

try {
Expand All @@ -156,12 +167,13 @@ export class DynamicRemotePlugin implements WebpackPluginInstance {
entryCallbackSettings,
entryScriptFilename,
pluginManifestFilename,
transformPluginManifest,
} = this.adaptedOptions;

const containerName = pluginMetadata.name;

const moduleFederationLibraryType = moduleFederationSettings.libraryType ?? 'jsonp';
const moduleFederationSharedScope = moduleFederationSettings.sharedScope ?? 'default';
const moduleFederationSharedScope = moduleFederationSettings.sharedScopeName ?? 'default';

const entryCallbackName = entryCallbackSettings.name ?? DEFAULT_REMOTE_ENTRY_CALLBACK;
const entryCallbackPluginID = entryCallbackSettings.pluginID ?? pluginMetadata.name;
Expand Down Expand Up @@ -207,23 +219,30 @@ export class DynamicRemotePlugin implements WebpackPluginInstance {
}

// Generate plugin manifest
new GenerateManifestPlugin(containerName, pluginManifestFilename, {
name: pluginMetadata.name,
version: pluginMetadata.version,
dependencies: pluginMetadata.dependencies,
customProperties: pluginMetadata.customProperties,
extensions,
registrationMethod: jsonp ? 'callback' : 'custom',
new GenerateManifestPlugin({
containerName,
manifestFilename: pluginManifestFilename,
manifestData: {
name: pluginMetadata.name,
version: pluginMetadata.version,
dependencies: pluginMetadata.dependencies,
customProperties: pluginMetadata.customProperties,
extensions,
registrationMethod: jsonp ? 'callback' : 'custom',
},
transformManifest: transformPluginManifest,
}).apply(compiler);

// Post-process container entry generated by ModuleFederationPlugin
if (jsonp) {
new PatchEntryCallbackPlugin(containerName, entryCallbackName, entryCallbackPluginID).apply(
compiler,
);
new PatchEntryCallbackPlugin({
containerName,
callbackName: entryCallbackName,
pluginID: entryCallbackPluginID,
}).apply(compiler);
}

// Validate webpack compilation
new ValidateCompilationPlugin(containerName, jsonp).apply(compiler);
new ValidateCompilationPlugin({ containerName, jsonpLibraryType: jsonp }).apply(compiler);
}
}
31 changes: 19 additions & 12 deletions packages/lib-webpack/src/webpack/GenerateManifestPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import type { PluginManifest } from '@openshift/dynamic-plugin-sdk/src/shared-webpack';
import type {
PluginManifest,
TransformPluginManifest,
} from '@openshift/dynamic-plugin-sdk/src/shared-webpack';
import { WebpackPluginInstance, Compiler, Compilation, sources, WebpackError } from 'webpack';
import { findPluginChunks } from '../utils/plugin-chunks';

type InputManifestData = Omit<PluginManifest, 'baseURL' | 'loadScripts' | 'buildHash'>;

type GenerateManifestPluginOptions = {
containerName: string;
manifestFilename: string;
manifestData: InputManifestData;
transformManifest: TransformPluginManifest;
};

export class GenerateManifestPlugin implements WebpackPluginInstance {
constructor(
private readonly containerName: string,
private readonly manifestFilename: string,
private readonly manifestData: InputManifestData,
) {}
constructor(private readonly options: GenerateManifestPluginOptions) {}

apply(compiler: Compiler) {
const { containerName, manifestFilename, manifestData, transformManifest } = this.options;
const publicPath = compiler.options.output.publicPath;

if (!publicPath) {
Expand All @@ -27,21 +34,21 @@ export class GenerateManifestPlugin implements WebpackPluginInstance {
stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
},
() => {
const { entryChunk, runtimeChunk } = findPluginChunks(this.containerName, compilation);
const { entryChunk, runtimeChunk } = findPluginChunks(containerName, compilation);

const loadScripts = (runtimeChunk ? [runtimeChunk, entryChunk] : [entryChunk]).reduce<
string[]
>((acc, chunk) => [...acc, ...chunk.files], []);

const manifest: PluginManifest = {
...this.manifestData,
const manifest = transformManifest({
...manifestData,
baseURL: compilation.getAssetPath(publicPath, {}),
loadScripts,
buildHash: compilation.fullHash,
};
});

compilation.emitAsset(
this.manifestFilename,
manifestFilename,
new sources.RawSource(Buffer.from(JSON.stringify(manifest, null, 2))),
);

Expand All @@ -57,7 +64,7 @@ export class GenerateManifestPlugin implements WebpackPluginInstance {

warnings.forEach((message) => {
const error = new WebpackError(message);
error.file = this.manifestFilename;
error.file = manifestFilename;
compilation.warnings.push(error);
});
},
Expand Down
22 changes: 13 additions & 9 deletions packages/lib-webpack/src/webpack/PatchEntryCallbackPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,36 @@
import { WebpackPluginInstance, Compiler, Compilation, sources, WebpackError } from 'webpack';
import { findPluginChunks } from '../utils/plugin-chunks';

type PatchEntryCallbackPluginOptions = {
containerName: string;
callbackName: string;
pluginID: string;
};

export class PatchEntryCallbackPlugin implements WebpackPluginInstance {
constructor(
private readonly containerName: string,
private readonly callbackName: string,
private readonly pluginID: string,
) {}
constructor(private readonly options: PatchEntryCallbackPluginOptions) {}

apply(compiler: Compiler) {
const { containerName, callbackName, pluginID } = this.options;

compiler.hooks.thisCompilation.tap(PatchEntryCallbackPlugin.name, (compilation) => {
compilation.hooks.processAssets.tap(
{
name: PatchEntryCallbackPlugin.name,
stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
},
() => {
const { entryChunk } = findPluginChunks(this.containerName, compilation);
const { entryChunk } = findPluginChunks(containerName, compilation);

entryChunk.files.forEach((fileName) => {
compilation.updateAsset(fileName, (source) => {
const newSource = new sources.ReplaceSource(source);
const fromIndex = source.source().toString().indexOf(`${this.callbackName}(`);
const fromIndex = source.source().toString().indexOf(`${callbackName}(`);

if (fromIndex >= 0) {
newSource.insert(fromIndex + this.callbackName.length + 1, `'${this.pluginID}', `);
newSource.insert(fromIndex + callbackName.length + 1, `'${pluginID}', `);
} else {
const error = new WebpackError(`Missing call to ${this.callbackName}`);
const error = new WebpackError(`Missing call to ${callbackName}`);
error.file = fileName;
error.chunk = entryChunk;
compilation.errors.push(error);
Expand Down
15 changes: 11 additions & 4 deletions packages/lib-webpack/src/webpack/ValidateCompilationPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { WebpackPluginInstance, Compiler, WebpackError } from 'webpack';
import { findPluginChunks } from '../utils/plugin-chunks';

type ValidateCompilationPluginOptions = {
containerName: string;
jsonpLibraryType: boolean;
};

export class ValidateCompilationPlugin implements WebpackPluginInstance {
constructor(private readonly containerName: string, private readonly jsonpLibraryType: boolean) {}
constructor(private readonly options: ValidateCompilationPluginOptions) {}

apply(compiler: Compiler) {
const { containerName, jsonpLibraryType } = this.options;

compiler.hooks.done.tap(ValidateCompilationPlugin.name, ({ compilation }) => {
const { runtimeChunk } = findPluginChunks(this.containerName, compilation);
const { runtimeChunk } = findPluginChunks(containerName, compilation);

if (runtimeChunk) {
const errorMessage = this.jsonpLibraryType
const errorMessage = jsonpLibraryType
? 'Detected separate runtime chunk while using jsonp library type.\n' +
'This configuration is not allowed since it will cause issues when reloading plugins at runtime.\n' +
'Please update your webpack configuration to avoid emitting a separate runtime chunk.'
Expand All @@ -19,7 +26,7 @@ export class ValidateCompilationPlugin implements WebpackPluginInstance {

const error = new WebpackError(errorMessage);
error.chunk = runtimeChunk;
(this.jsonpLibraryType ? compilation.errors : compilation.warnings).push(error);
(jsonpLibraryType ? compilation.errors : compilation.warnings).push(error);
}
});
}
Expand Down
2 changes: 1 addition & 1 deletion packages/lib-webpack/src/yup-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const pluginBuildMetadataSchema = pluginRuntimeMetadataSchema.shape({
*/
const pluginModuleFederationSettingsSchema = yup.object().required().shape({
libraryType: yup.string(),
sharedScope: yup.string(),
sharedScopeName: yup.string(),
});

/**
Expand Down
7 changes: 6 additions & 1 deletion reports/lib-core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export type PluginLoaderOptions = Partial<{
fetchImpl: ResourceFetch;
fixedPluginDependencyResolutions: Record<string, string>;
sharedScope: AnyObject;
postProcessManifest: (manifest: PluginManifest) => PluginManifest;
transformPluginManifest: TransformPluginManifest;
getPluginEntryModule: (manifest: PluginManifest) => PluginEntryModule | void;
}>;

Expand Down Expand Up @@ -249,6 +249,11 @@ export type ResolvedExtension<TExtension extends Extension = Extension> = Replac
// @public
export type ResourceFetch = (url: string, requestInit?: RequestInit, isK8sAPIRequest?: boolean) => Promise<Response>;

// @public (undocumented)
export type TransformPluginManifest = (manifest: PluginManifest) => PluginManifest & {
[customProperty: string]: unknown;
};

// @public
export const useExtensions: <TExtension extends Extension<string, AnyObject>>(predicate?: ExtensionPredicate<TExtension> | undefined) => LoadedExtension<TExtension>[];

Expand Down

0 comments on commit 0eab266

Please sign in to comment.