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
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
NX_DEAMON=false
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,4 @@ vitest.config.*.timestamp*
ssg
.claude
__mocks__/
/.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// @ts-nocheck

import {
ModuleFederationPlugin,
dependencies,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// @ts-nocheck
/*
* @jest-environment node
*/

import { vol } from 'memfs';
import SharePlugin from '../../../src/lib/sharing/SharePlugin';
import {
createRealCompiler,
createMemfsCompilation,
createNormalModuleFactory,
} from '../../helpers/webpackMocks';

// Use memfs for fs inside this suite
jest.mock('fs', () => require('memfs').fs);
jest.mock('fs/promises', () => require('memfs').fs.promises);

// Mock child plugins to avoid deep integration
jest.mock('../../../src/lib/sharing/ConsumeSharedPlugin', () => {
return jest
.fn()
.mockImplementation((opts) => ({ options: opts, apply: jest.fn() }));
});
jest.mock('../../../src/lib/sharing/ProvideSharedPlugin', () => {
return jest
.fn()
.mockImplementation((opts) => ({ options: opts, apply: jest.fn() }));
});

import ConsumeSharedPlugin from '../../../src/lib/sharing/ConsumeSharedPlugin';
import ProvideSharedPlugin from '../../../src/lib/sharing/ProvideSharedPlugin';

describe('SharePlugin smoke (memfs)', () => {
beforeEach(() => {
vol.reset();
jest.clearAllMocks();
});

it('applies child plugins with derived options', () => {
// Create a tiny project in memfs
vol.fromJSON({
'/test-project/src/index.js': 'console.log("hello")',
'/test-project/package.json': '{"name":"test","version":"1.0.0"}',
'/test-project/node_modules/react/index.js': 'module.exports = {}',
'/test-project/node_modules/lodash/index.js': 'module.exports = {}',
});

const plugin = new SharePlugin({
shareScope: 'default',
shared: {
react: '^17.0.0',
lodash: { version: '4.17.21', singleton: true },
'components/': { version: '1.0.0', eager: false },
},
});

const compiler = createRealCompiler('/test-project');
expect(() => plugin.apply(compiler as any)).not.toThrow();

// Child plugins constructed
expect(ConsumeSharedPlugin).toHaveBeenCalledTimes(1);
expect(ProvideSharedPlugin).toHaveBeenCalledTimes(1);

// Each child plugin receives shareScope and normalized arrays
const consumeOpts = (ConsumeSharedPlugin as jest.Mock).mock.calls[0][0];
const provideOpts = (ProvideSharedPlugin as jest.Mock).mock.calls[0][0];
expect(consumeOpts.shareScope).toBe('default');
expect(Array.isArray(consumeOpts.consumes)).toBe(true);
expect(provideOpts.shareScope).toBe('default');
expect(Array.isArray(provideOpts.provides)).toBe(true);

// Simulate compilation lifecycle
const compilation = createMemfsCompilation(compiler as any);
const normalModuleFactory = createNormalModuleFactory();
expect(() =>
(compiler as any).hooks.thisCompilation.call(compilation, {
normalModuleFactory,
}),
).not.toThrow();
expect(() =>
(compiler as any).hooks.compilation.call(compilation, {
normalModuleFactory,
}),
).not.toThrow();

// Child plugin instances should be applied to the compiler
const consumeInst = (ConsumeSharedPlugin as jest.Mock).mock.results[0]
.value;
const provideInst = (ProvideSharedPlugin as jest.Mock).mock.results[0]
.value;
expect(consumeInst.apply).toHaveBeenCalledWith(compiler);
expect(provideInst.apply).toHaveBeenCalledWith(compiler);
});
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @ts-nocheck
/*
* @jest-environment node
*/
Expand Down
7 changes: 7 additions & 0 deletions packages/enhanced/test/helpers/snapshots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function normalizeCode(source: string): string {
return source
.replace(/[ \t]+/g, ' ')
.replace(/\r\n/g, '\n')
.replace(/\n+/g, '\n')
.trim();
}
122 changes: 122 additions & 0 deletions packages/enhanced/test/helpers/webpackMocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { SyncHook, AsyncSeriesHook, HookMap } from 'tapable';

export type BasicCompiler = {
hooks: {
thisCompilation: SyncHook<any> & { taps?: any[] };
compilation: SyncHook<any> & { taps?: any[] };
finishMake: AsyncSeriesHook<any> & { taps?: any[] };
make: AsyncSeriesHook<any> & { taps?: any[] };
environment: SyncHook<any> & { taps?: any[] };
afterEnvironment: SyncHook<any> & { taps?: any[] };
afterPlugins: SyncHook<any> & { taps?: any[] };
afterResolvers: SyncHook<any> & { taps?: any[] };
};
context: string;
options: any;
};

export function createTapTrackedHook<
T extends SyncHook<any> | AsyncSeriesHook<any>,
>(hook: T): T {
const tracked = hook as any;
const wrap = (method: 'tap' | 'tapAsync' | 'tapPromise') => {
if (typeof tracked[method] === 'function') {
const original = tracked[method].bind(tracked);
tracked.__tapCalls = tracked.__tapCalls || [];
tracked[method] = (name: string, fn: any) => {
tracked.__tapCalls.push({ name, fn, method });
return original(name, fn);
};
}
};
wrap('tap');
wrap('tapAsync');
wrap('tapPromise');
return tracked as T;
}

export function createRealCompiler(context = '/test-project'): BasicCompiler {
return {
hooks: {
thisCompilation: createTapTrackedHook(
new SyncHook<[unknown, unknown]>(['compilation', 'params']),
) as any,
compilation: createTapTrackedHook(
new SyncHook<[unknown, unknown]>(['compilation', 'params']),
) as any,
finishMake: createTapTrackedHook(
new AsyncSeriesHook<[unknown]>(['compilation']),
) as any,
make: createTapTrackedHook(
new AsyncSeriesHook<[unknown]>(['compilation']),
) as any,
environment: createTapTrackedHook(new SyncHook<[]>([])) as any,
afterEnvironment: createTapTrackedHook(new SyncHook<[]>([])) as any,
afterPlugins: createTapTrackedHook(
new SyncHook<[unknown]>(['compiler']),
) as any,
afterResolvers: createTapTrackedHook(
new SyncHook<[unknown]>(['compiler']),
) as any,
},
context,
options: {
plugins: [],
resolve: { alias: {} },
context,
},
} as any;
}

export function createMemfsCompilation(compiler: BasicCompiler) {
return {
dependencyFactories: new Map(),
hooks: {
additionalTreeRuntimeRequirements: { tap: jest.fn() },
finishModules: { tap: jest.fn(), tapAsync: jest.fn() },
seal: { tap: jest.fn() },
runtimeRequirementInTree: new HookMap<
SyncHook<[unknown, unknown, unknown]>
>(
() =>
new SyncHook<[unknown, unknown, unknown]>([
'chunk',
'set',
'context',
]),
),
processAssets: { tap: jest.fn() },
},
addRuntimeModule: jest.fn(),
contextDependencies: { addAll: jest.fn() },
fileDependencies: { addAll: jest.fn() },
missingDependencies: { addAll: jest.fn() },
warnings: [],
errors: [],
resolverFactory: {
get: jest.fn(() => ({
resolve: jest.fn(
(
_context: unknown,
lookupStartPath: string,
request: string,
_resolveContext: unknown,
callback: (err: any, result?: string) => void,
) => callback(null, `${lookupStartPath}/${request}`),
),
})),
},
compiler,
options: compiler.options,
} as any;
}

export function createNormalModuleFactory() {
return {
hooks: {
module: { tap: jest.fn() },
factorize: { tapPromise: jest.fn(), tapAsync: jest.fn(), tap: jest.fn() },
createModule: { tapPromise: jest.fn() },
},
};
}
1 change: 1 addition & 0 deletions packages/enhanced/test/types/memfs.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module 'memfs';
50 changes: 41 additions & 9 deletions packages/enhanced/test/unit/container/ContainerEntryModule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
* @jest-environment node
*/

import type {
ObjectDeserializerContext,
ObjectSerializerContext,
} from 'webpack/lib/serialization/ObjectMiddleware';
import { createMockCompilation, createWebpackMock } from './utils';

// Mock webpack
Expand Down Expand Up @@ -29,15 +33,7 @@ import ContainerEntryModule from '../../../src/lib/container/ContainerEntryModul
import ContainerEntryDependency from '../../../src/lib/container/ContainerEntryDependency';
import ContainerExposedDependency from '../../../src/lib/container/ContainerExposedDependency';

// Add these types at the top, after the imports
type ObjectSerializerContext = {
write: (value: any) => number;
};

type ObjectDeserializerContext = {
read: () => any;
setCircularReference: (ref: any) => void;
};
// We will stub the serializer contexts inline using the proper types

describe('ContainerEntryModule', () => {
let mockCompilation: ReturnType<
Expand Down Expand Up @@ -227,6 +223,7 @@ describe('ContainerEntryModule', () => {
serializedData.push(value);
return serializedData.length - 1;
}),
setCircularReference: jest.fn(),
};

// Serialize
Expand Down Expand Up @@ -299,6 +296,7 @@ describe('ContainerEntryModule', () => {
serializedData.push(value);
return serializedData.length - 1;
}),
setCircularReference: jest.fn(),
};

// Serialize
Expand Down Expand Up @@ -343,6 +341,40 @@ describe('ContainerEntryModule', () => {
expect(deserializedModule['_name']).toBe(name);
expect(deserializedModule['_shareScope']).toEqual(shareScope);
});

it('should handle incomplete deserialization data gracefully', () => {
const name = 'test-container';
const exposesFormatted: [string, any][] = [
['component', { import: './Component' }],
];

// Missing some fields (shareScope/injectRuntimeEntry/dataPrefetch)
const deserializedData = [name, exposesFormatted];

let index = 0;
const deserializeContext: any = {
read: jest.fn(() => deserializedData[index++]),
setCircularReference: jest.fn(),
};

const staticDeserialize = ContainerEntryModule.deserialize as unknown as (
context: any,
) => ContainerEntryModule;

const deserializedModule = staticDeserialize(deserializeContext);
jest
.spyOn(webpack.Module.prototype, 'deserialize')
.mockImplementation(() => undefined);

// Known values are set; missing ones may be undefined
expect(deserializedModule['_name']).toBe(name);
expect(deserializedModule['_exposes']).toEqual(exposesFormatted);
expect(
['default', undefined].includes(
(deserializedModule as any)['_shareScope'],
),
).toBe(true);
});
});

describe('codeGeneration', () => {
Expand Down
Loading