UNPKG

@esmx/core

Version:

A high-performance microfrontend framework supporting Vue, React, Preact, Solid, and Svelte with SSR and Module Federation capabilities.

799 lines (699 loc) 27.2 kB
import path from 'node:path'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { type ModuleConfig, type ModuleConfigExportExports, type ModuleConfigExportObject, type ParsedModuleConfig, type ParsedModuleConfigExport, parseModuleConfig } from './module-config'; describe('module-config', () => { const testModuleName = 'test-module'; const testRoot = '/test/root'; describe('parseModuleConfig', () => { it('should parse empty configuration with defaults', () => { // Arrange const config: ModuleConfig = {}; // Act const result = parseModuleConfig(testModuleName, testRoot, config); // Assert expect(result.name).toBe(testModuleName); expect(result.root).toBe(testRoot); expect(result.imports).toEqual({}); expect(result.links).toHaveProperty(testModuleName); expect(result.exports).toHaveProperty('src/entry.client'); expect(result.exports).toHaveProperty('src/entry.server'); }); it('should parse configuration without config parameter', () => { // Arrange & Act const result = parseModuleConfig(testModuleName, testRoot); // Assert expect(result.name).toBe(testModuleName); expect(result.root).toBe(testRoot); expect(result.imports).toEqual({}); }); it('should parse complete configuration', () => { // Arrange const config: ModuleConfig = { links: { 'shared-lib': '../shared-lib/dist', 'api-utils': '/absolute/path/api-utils/dist' }, imports: { axios: 'shared-lib/axios', lodash: 'shared-lib/lodash' }, exports: { axios: 'axios', 'src/utils/format': './src/utils/format.ts', 'custom-api': './src/api/custom.ts' } }; // Act const result = parseModuleConfig(testModuleName, testRoot, config); // Assert expect(result.name).toBe(testModuleName); expect(result.root).toBe(testRoot); expect(result.imports).toEqual(config.imports); expect(result.links).toHaveProperty('shared-lib'); expect(result.links).toHaveProperty('api-utils'); expect(result.exports).toHaveProperty('axios'); expect(result.exports).toHaveProperty('src/utils/format'); expect(result.exports).toHaveProperty('custom-api'); }); }); describe('links processing', () => { it('should create self-link with default dist path', () => { // Arrange const config: ModuleConfig = {}; // Act const result = parseModuleConfig(testModuleName, testRoot, config); // Assert const selfLink = result.links[testModuleName]; expect(selfLink).toBeDefined(); expect(selfLink.name).toBe(testModuleName); expect(selfLink.root).toBe(path.resolve(testRoot, 'dist')); expect(selfLink.client).toBe(path.resolve(testRoot, 'dist/client')); expect(selfLink.server).toBe(path.resolve(testRoot, 'dist/server')); expect(selfLink.clientManifestJson).toBe( path.resolve(testRoot, 'dist/client/manifest.json') ); expect(selfLink.serverManifestJson).toBe( path.resolve(testRoot, 'dist/server/manifest.json') ); }); it('should process relative path links', () => { // Arrange const config: ModuleConfig = { links: { 'shared-lib': '../shared-lib/dist' } }; // Act const result = parseModuleConfig(testModuleName, testRoot, config); // Assert const sharedLibLink = result.links['shared-lib']; expect(sharedLibLink.name).toBe('shared-lib'); expect(sharedLibLink.root).toBe('../shared-lib/dist'); expect(sharedLibLink.client).toBe( path.resolve(testRoot, '../shared-lib/dist/client') ); expect(sharedLibLink.server).toBe( path.resolve(testRoot, '../shared-lib/dist/server') ); }); it('should process absolute path links', () => { // Arrange const absolutePath = '/absolute/path/api-utils/dist'; const config: ModuleConfig = { links: { 'api-utils': absolutePath } }; // Act const result = parseModuleConfig(testModuleName, testRoot, config); // Assert const apiUtilsLink = result.links['api-utils']; expect(apiUtilsLink.name).toBe('api-utils'); expect(apiUtilsLink.root).toBe(absolutePath); expect(apiUtilsLink.client).toBe( path.resolve(absolutePath, 'client') ); expect(apiUtilsLink.server).toBe( path.resolve(absolutePath, 'server') ); }); it('should handle multiple links', () => { // Arrange const config: ModuleConfig = { links: { lib1: '../lib1/dist', lib2: '/absolute/lib2/dist', lib3: './relative/lib3/dist' } }; // Act const result = parseModuleConfig(testModuleName, testRoot, config); // Assert expect(Object.keys(result.links)).toHaveLength(4); // 3 + self-link expect(result.links).toHaveProperty('lib1'); expect(result.links).toHaveProperty('lib2'); expect(result.links).toHaveProperty('lib3'); expect(result.links).toHaveProperty(testModuleName); }); }); describe('exports processing', () => { describe('default exports', () => { it('should add default entry exports', () => { // Arrange const config: ModuleConfig = {}; // Act const result = parseModuleConfig( testModuleName, testRoot, config ); // Assert expect(result.exports['src/entry.client']).toEqual({ name: 'src/entry.client', rewrite: true, inputTarget: { client: './src/entry.client', server: false } }); expect(result.exports['src/entry.server']).toEqual({ name: 'src/entry.server', rewrite: true, inputTarget: { client: false, server: './src/entry.server' } }); }); }); describe('array format', () => { it('should process npm: prefix exports', () => { // Arrange const config: ModuleConfig = { exports: ['npm:axios', 'npm:lodash'] }; // Act const result = parseModuleConfig( testModuleName, testRoot, config ); // Assert expect(result.exports.axios).toEqual({ name: 'axios', rewrite: false, inputTarget: { client: 'axios', server: 'axios' } }); expect(result.exports.lodash).toEqual({ name: 'lodash', rewrite: false, inputTarget: { client: 'lodash', server: 'lodash' } }); }); it('should process root: prefix exports with file extensions', () => { // Arrange const config: ModuleConfig = { exports: [ 'root:src/utils/format.ts', 'root:src/components/Button.jsx', 'root:src/api/client.js' ] }; // Act const result = parseModuleConfig( testModuleName, testRoot, config ); // Assert expect(result.exports['src/utils/format']).toEqual({ name: 'src/utils/format', rewrite: true, inputTarget: { client: './src/utils/format', server: './src/utils/format' } }); expect(result.exports['src/components/Button']).toEqual({ name: 'src/components/Button', rewrite: true, inputTarget: { client: './src/components/Button', server: './src/components/Button' } }); expect(result.exports['src/api/client']).toEqual({ name: 'src/api/client', rewrite: true, inputTarget: { client: './src/api/client', server: './src/api/client' } }); }); it('should handle all supported file extensions', () => { // Arrange const extensions = [ 'js', 'mjs', 'cjs', 'jsx', 'mjsx', 'cjsx', 'ts', 'mts', 'cts', 'tsx', 'mtsx', 'ctsx' ]; const config: ModuleConfig = { exports: extensions.map((ext) => `root:src/test.${ext}`) }; // Act const result = parseModuleConfig( testModuleName, testRoot, config ); // Assert extensions.forEach((ext) => { expect(result.exports['src/test']).toBeDefined(); }); }); it('should handle object exports in array', () => { // Arrange const config: ModuleConfig = { exports: [ 'npm:axios', { 'custom-api': './src/api/custom.ts', utils: { input: './src/utils/index.ts', rewrite: true } } ] }; // Act const result = parseModuleConfig( testModuleName, testRoot, config ); // Assert expect(result.exports['custom-api']).toEqual({ name: 'custom-api', rewrite: true, inputTarget: { client: './src/api/custom.ts', server: './src/api/custom.ts' } }); expect(result.exports.utils).toEqual({ name: 'utils', rewrite: true, inputTarget: { client: './src/utils/index.ts', server: './src/utils/index.ts' } }); }); it('should handle invalid export strings', () => { // Arrange const consoleSpy = vi .spyOn(console, 'error') .mockImplementation(() => {}); const config: ModuleConfig = { exports: ['invalid-export', 'another-invalid'] }; // Act parseModuleConfig(testModuleName, testRoot, config); // Assert expect(consoleSpy).toHaveBeenCalledWith( 'Invalid module export: invalid-export' ); expect(consoleSpy).toHaveBeenCalledWith( 'Invalid module export: another-invalid' ); consoleSpy.mockRestore(); }); }); describe('object format', () => { it('should process simple string mappings', () => { // Arrange const config: ModuleConfig = { exports: { axios: 'axios', utils: './src/utils/index.ts' } }; // Act const result = parseModuleConfig( testModuleName, testRoot, config ); // Assert expect(result.exports.axios).toEqual({ name: 'axios', rewrite: true, inputTarget: { client: 'axios', server: 'axios' } }); expect(result.exports.utils).toEqual({ name: 'utils', rewrite: true, inputTarget: { client: './src/utils/index.ts', server: './src/utils/index.ts' } }); }); it('should process complete export objects', () => { // Arrange const config: ModuleConfig = { exports: { storage: { inputTarget: { client: './src/storage/indexedDB.ts', server: './src/storage/filesystem.ts' }, rewrite: true }, 'npm-package': { input: 'some-package', rewrite: false } } }; // Act const result = parseModuleConfig( testModuleName, testRoot, config ); // Assert expect(result.exports.storage).toEqual({ name: 'storage', rewrite: true, inputTarget: { client: './src/storage/indexedDB.ts', server: './src/storage/filesystem.ts' } }); expect(result.exports['npm-package']).toEqual({ name: 'npm-package', rewrite: false, inputTarget: { client: 'some-package', server: 'some-package' } }); }); it('should handle inputTarget with false values', () => { // Arrange const config: ModuleConfig = { exports: { 'client-only': { inputTarget: { client: './src/client-feature.ts', server: false } }, 'server-only': { inputTarget: { client: false, server: './src/server-feature.ts' } } } }; // Act const result = parseModuleConfig( testModuleName, testRoot, config ); // Assert expect(result.exports['client-only']).toEqual({ name: 'client-only', rewrite: true, inputTarget: { client: './src/client-feature.ts', server: false } }); expect(result.exports['server-only']).toEqual({ name: 'server-only', rewrite: true, inputTarget: { client: false, server: './src/server-feature.ts' } }); }); }); describe('mixed configurations', () => { it('should handle complex mixed export configuration', () => { // Arrange const config: ModuleConfig = { exports: { // Simple string mapping simple: './src/simple.ts', // Complete object with inputTarget complex: { inputTarget: { client: './src/complex.client.ts', server: './src/complex.server.ts' }, rewrite: false }, // Object with just input 'with-input': { input: './src/with-input.ts' }, // Object with just rewrite 'with-rewrite': { rewrite: false } } }; // Act const result = parseModuleConfig( testModuleName, testRoot, config ); // Assert expect(result.exports.simple).toEqual({ name: 'simple', rewrite: true, inputTarget: { client: './src/simple.ts', server: './src/simple.ts' } }); expect(result.exports.complex).toEqual({ name: 'complex', rewrite: false, inputTarget: { client: './src/complex.client.ts', server: './src/complex.server.ts' } }); expect(result.exports['with-input']).toEqual({ name: 'with-input', rewrite: true, inputTarget: { client: './src/with-input.ts', server: './src/with-input.ts' } }); expect(result.exports['with-rewrite']).toEqual({ name: 'with-rewrite', rewrite: false, inputTarget: { client: 'with-rewrite', server: 'with-rewrite' } }); }); }); describe('default value handling', () => { it('should use default rewrite value of true', () => { // Arrange const config: ModuleConfig = { exports: { 'test-export': { input: './src/test.ts' // rewrite not specified, should default to true } } }; // Act const result = parseModuleConfig( testModuleName, testRoot, config ); // Assert expect(result.exports['test-export'].rewrite).toBe(true); }); it('should use export name as fallback for input paths', () => { // Arrange const config: ModuleConfig = { exports: { 'fallback-test': { // No input or inputTarget specified rewrite: false } } }; // Act const result = parseModuleConfig( testModuleName, testRoot, config ); // Assert expect(result.exports['fallback-test'].inputTarget).toEqual({ client: 'fallback-test', server: 'fallback-test' }); }); }); }); describe('imports processing', () => { it('should pass through imports configuration unchanged', () => { // Arrange const imports = { axios: 'shared-lib/axios', lodash: 'shared-lib/lodash', 'custom-lib': 'api-utils/custom' }; const config: ModuleConfig = { imports }; // Act const result = parseModuleConfig(testModuleName, testRoot, config); // Assert expect(result.imports).toEqual(imports); }); it('should handle empty imports', () => { // Arrange const config: ModuleConfig = {}; // Act const result = parseModuleConfig(testModuleName, testRoot, config); // Assert expect(result.imports).toEqual({}); }); it('should handle undefined imports', () => { // Arrange const config: ModuleConfig = { links: { test: './test' }, exports: ['npm:axios'] // imports intentionally omitted }; // Act const result = parseModuleConfig(testModuleName, testRoot, config); // Assert expect(result.imports).toEqual({}); }); }); describe('edge cases', () => { it('should handle empty exports array', () => { // Arrange const config: ModuleConfig = { exports: [] }; // Act const result = parseModuleConfig(testModuleName, testRoot, config); // Assert // Should still have default exports expect(result.exports).toHaveProperty('src/entry.client'); expect(result.exports).toHaveProperty('src/entry.server'); expect(Object.keys(result.exports)).toHaveLength(2); }); it('should handle empty exports object', () => { // Arrange const config: ModuleConfig = { exports: {} }; // Act const result = parseModuleConfig(testModuleName, testRoot, config); // Assert // Should still have default exports expect(result.exports).toHaveProperty('src/entry.client'); expect(result.exports).toHaveProperty('src/entry.server'); expect(Object.keys(result.exports)).toHaveLength(2); }); it('should handle null and undefined values gracefully', () => { // Arrange const config: ModuleConfig = { links: undefined, imports: undefined, exports: undefined }; // Act const result = parseModuleConfig(testModuleName, testRoot, config); // Assert expect(result.links).toHaveProperty(testModuleName); expect(result.imports).toEqual({}); expect(result.exports).toHaveProperty('src/entry.client'); expect(result.exports).toHaveProperty('src/entry.server'); }); it('should handle special characters in module names and paths', () => { // Arrange const specialModuleName = 'test-module_with.special@chars'; const config: ModuleConfig = { links: { 'special-lib@1.0.0': '../special-lib/dist' }, exports: { 'special_export-name': './src/special.ts' } }; // Act const result = parseModuleConfig( specialModuleName, testRoot, config ); // Assert expect(result.name).toBe(specialModuleName); expect(result.links).toHaveProperty('special-lib@1.0.0'); expect(result.exports).toHaveProperty('special_export-name'); }); }); describe('type safety', () => { it('should maintain type safety for ParsedModuleConfig', () => { // Arrange const config: ModuleConfig = { links: { test: './test' }, imports: { axios: 'test/axios' }, exports: ['npm:lodash'] }; // Act const result: ParsedModuleConfig = parseModuleConfig( testModuleName, testRoot, config ); // Assert expect(typeof result.name).toBe('string'); expect(typeof result.root).toBe('string'); expect(typeof result.links).toBe('object'); expect(typeof result.imports).toBe('object'); expect(typeof result.exports).toBe('object'); }); it('should maintain type safety for ParsedModuleConfigExport', () => { // Arrange const config: ModuleConfig = { exports: ['npm:axios'] }; // Act const result = parseModuleConfig(testModuleName, testRoot, config); // Assert const exportConfig: ParsedModuleConfigExport = result.exports.axios; expect(typeof exportConfig.name).toBe('string'); expect(typeof exportConfig.rewrite).toBe('boolean'); expect(typeof exportConfig.inputTarget).toBe('object'); expect(typeof exportConfig.inputTarget.client).toBe('string'); expect(typeof exportConfig.inputTarget.server).toBe('string'); }); }); });