diff --git a/e2e/cases/auto-external/default/package.json b/e2e/cases/auto-external/default/package.json new file mode 100644 index 00000000..7dfa7c46 --- /dev/null +++ b/e2e/cases/auto-external/default/package.json @@ -0,0 +1,6 @@ +{ + "name": "@e2e/auto-external-default", + "dependencies": { + "react": "^18.3.1" + } +} diff --git a/e2e/cases/auto-external/default/rslib.config.ts b/e2e/cases/auto-external/default/rslib.config.ts new file mode 100644 index 00000000..6f1252ec --- /dev/null +++ b/e2e/cases/auto-external/default/rslib.config.ts @@ -0,0 +1,11 @@ +import { generateBundleCjsConfig, generateBundleEsmConfig } from '@e2e/helper'; +import { defineConfig } from '@rslib/core'; + +export default defineConfig({ + lib: [generateBundleEsmConfig(__dirname), generateBundleCjsConfig(__dirname)], + source: { + entry: { + main: '../fixtures/src/index.ts', + }, + }, +}); diff --git a/e2e/cases/auto-external/external-sub-path/package.json b/e2e/cases/auto-external/external-sub-path/package.json new file mode 100644 index 00000000..5c98adea --- /dev/null +++ b/e2e/cases/auto-external/external-sub-path/package.json @@ -0,0 +1,6 @@ +{ + "name": "@e2e/auto-external-with-sub-path", + "dependencies": { + "react": "^18.3.1" + } +} diff --git a/e2e/cases/auto-external/external-sub-path/rslib.config.ts b/e2e/cases/auto-external/external-sub-path/rslib.config.ts new file mode 100644 index 00000000..03252f35 --- /dev/null +++ b/e2e/cases/auto-external/external-sub-path/rslib.config.ts @@ -0,0 +1,11 @@ +import { generateBundleCjsConfig, generateBundleEsmConfig } from '@e2e/helper'; +import { defineConfig } from '@rslib/core'; + +export default defineConfig({ + lib: [generateBundleEsmConfig(__dirname), generateBundleCjsConfig(__dirname)], + source: { + entry: { + main: './src/index.ts', + }, + }, +}); diff --git a/e2e/cases/auto-external/external-sub-path/src/index.ts b/e2e/cases/auto-external/external-sub-path/src/index.ts new file mode 100644 index 00000000..25bd8868 --- /dev/null +++ b/e2e/cases/auto-external/external-sub-path/src/index.ts @@ -0,0 +1,6 @@ +import React from 'react'; +import ReactJsx from 'react/jsx-runtime'; + +export const foo = () => { + return [React.version, ReactJsx.jsx]; +}; diff --git a/e2e/cases/auto-external/false/package.json b/e2e/cases/auto-external/false/package.json new file mode 100644 index 00000000..6fe114cc --- /dev/null +++ b/e2e/cases/auto-external/false/package.json @@ -0,0 +1,6 @@ +{ + "name": "@e2e/auto-external-false", + "dependencies": { + "react": "^18.3.1" + } +} diff --git a/e2e/cases/auto-external/false/rslib.config.ts b/e2e/cases/auto-external/false/rslib.config.ts new file mode 100644 index 00000000..3a82365b --- /dev/null +++ b/e2e/cases/auto-external/false/rslib.config.ts @@ -0,0 +1,20 @@ +import { generateBundleCjsConfig, generateBundleEsmConfig } from '@e2e/helper'; +import { defineConfig } from '@rslib/core'; + +export default defineConfig({ + lib: [ + { + ...generateBundleEsmConfig(__dirname), + autoExternal: false, + }, + { + ...generateBundleCjsConfig(__dirname), + autoExternal: false, + }, + ], + source: { + entry: { + main: '../fixtures/src/index.ts', + }, + }, +}); diff --git a/e2e/cases/auto-external/fixtures/src/index.ts b/e2e/cases/auto-external/fixtures/src/index.ts new file mode 100644 index 00000000..065585aa --- /dev/null +++ b/e2e/cases/auto-external/fixtures/src/index.ts @@ -0,0 +1,5 @@ +import React from 'react'; + +export const foo = () => { + return React.version; +}; diff --git a/e2e/cases/auto-external/index.test.ts b/e2e/cases/auto-external/index.test.ts new file mode 100644 index 00000000..c47715f0 --- /dev/null +++ b/e2e/cases/auto-external/index.test.ts @@ -0,0 +1,61 @@ +import { join } from 'node:path'; +import { buildAndGetResults } from '@e2e/helper'; +import { expect, test } from 'vitest'; + +test('auto external default should works', async () => { + const fixturePath = join(__dirname, 'default'); + const { entries } = await buildAndGetResults(fixturePath); + + expect(entries.esm).toContain( + 'import * as __WEBPACK_EXTERNAL_MODULE_react__ from "react"', + ); + + expect(entries.cjs).toContain( + 'var external_react_namespaceObject = require("react");', + ); +}); + +test('auto external sub path should works', async () => { + const fixturePath = join(__dirname, 'external-sub-path'); + const { entries } = await buildAndGetResults(fixturePath); + + expect(entries.esm).toContain( + 'import * as __WEBPACK_EXTERNAL_MODULE_react__ from "react"', + ); + expect(entries.esm).toContain( + 'import * as __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime__ from "react/jsx-runtime"', + ); + + expect(entries.cjs).toContain( + 'var external_react_namespaceObject = require("react");', + ); + expect(entries.cjs).toContain( + 'var jsx_runtime_namespaceObject = require("react/jsx-runtime");', + ); +}); + +test('auto external false should works', async () => { + const fixturePath = join(__dirname, 'false'); + const { entries } = await buildAndGetResults(fixturePath); + + expect(entries.esm).not.toContain( + 'import * as __WEBPACK_EXTERNAL_MODULE_react__ from "react"', + ); + + expect(entries.cjs).not.toContain( + 'var external_react_namespaceObject = require("react");', + ); +}); + +test('externals should overrides auto external', async () => { + const fixturePath = join(__dirname, 'with-externals'); + const { entries } = await buildAndGetResults(fixturePath); + + expect(entries.esm).toContain( + 'import * as __WEBPACK_EXTERNAL_MODULE_react1__ from "react1"', + ); + + expect(entries.cjs).toContain( + 'var external_react1_namespaceObject = require("react1");', + ); +}); diff --git a/e2e/cases/auto-external/with-externals/package.json b/e2e/cases/auto-external/with-externals/package.json new file mode 100644 index 00000000..9d79d066 --- /dev/null +++ b/e2e/cases/auto-external/with-externals/package.json @@ -0,0 +1,6 @@ +{ + "name": "@e2e/auto-external-with-externals", + "dependencies": { + "react": "^18.3.1" + } +} diff --git a/e2e/cases/auto-external/with-externals/rslib.config.ts b/e2e/cases/auto-external/with-externals/rslib.config.ts new file mode 100644 index 00000000..96d469e6 --- /dev/null +++ b/e2e/cases/auto-external/with-externals/rslib.config.ts @@ -0,0 +1,16 @@ +import { generateBundleCjsConfig, generateBundleEsmConfig } from '@e2e/helper'; +import { defineConfig } from '@rslib/core'; + +export default defineConfig({ + lib: [generateBundleEsmConfig(__dirname), generateBundleCjsConfig(__dirname)], + output: { + externals: { + react: 'react1', + }, + }, + source: { + entry: { + main: '../fixtures/src/index.ts', + }, + }, +}); diff --git a/examples/express-plugin/rslib.config.ts b/examples/express-plugin/rslib.config.ts index f4d96ea5..e71c9b75 100644 --- a/examples/express-plugin/rslib.config.ts +++ b/examples/express-plugin/rslib.config.ts @@ -33,9 +33,6 @@ export default defineConfig({ }, }, output: { - externals: { - express: 'express', - }, target: 'node', }, }); diff --git a/examples/react-component/rslib.config.ts b/examples/react-component/rslib.config.ts index d9701100..561d3c6e 100644 --- a/examples/react-component/rslib.config.ts +++ b/examples/react-component/rslib.config.ts @@ -33,11 +33,5 @@ export default defineConfig({ main: './src/index.tsx', }, }, - output: { - externals: { - react: 'react', - 'react/jsx-runtime': 'react/jsx-runtime', - }, - }, plugins: [pluginReact()], }); diff --git a/packages/core/__mocks__/rslog.cjs b/packages/core/__mocks__/rslog.cjs new file mode 100644 index 00000000..a0bcd4bd --- /dev/null +++ b/packages/core/__mocks__/rslog.cjs @@ -0,0 +1,5 @@ +module.exports = { + logger: { + warn: () => {}, + }, +}; diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index e39f100b..98fa4e3a 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -11,18 +11,24 @@ import { import glob from 'fast-glob'; import { DEFAULT_CONFIG_NAME, DEFAULT_EXTENSIONS } from './constant'; import type { + AutoExternal, Format, LibConfig, + PkgJson, RslibConfig, RslibConfigAsyncFn, RslibConfigExport, RslibConfigSyncFn, Syntax, -} from './types/config'; +} from './types'; import { getDefaultExtension } from './utils/extension'; -import { calcLongestCommonPath } from './utils/helper'; -import { color } from './utils/helper'; -import { nodeBuiltInModules } from './utils/helper'; +import { + calcLongestCommonPath, + color, + isObject, + nodeBuiltInModules, + readPackageJson, +} from './utils/helper'; import { logger } from './utils/logger'; import { transformSyntaxToBrowserslist } from './utils/syntax'; @@ -77,6 +83,63 @@ export async function loadConfig( return content as RslibConfig; } +export const composeAutoExternalConfig = (options: { + autoExternal: AutoExternal; + pkgJson?: PkgJson; + userExternals?: NonNullable['externals']; +}): RsbuildConfig => { + const { autoExternal, pkgJson, userExternals } = options; + + if (!autoExternal) { + return {}; + } + + if (!pkgJson) { + logger.warn( + 'autoExternal configuration will not be applied due to read package.json failed', + ); + return {}; + } + + const externalOptions = { + dependencies: true, + peerDependencies: true, + devDependencies: false, + ...(autoExternal === true ? {} : autoExternal), + }; + + // User externals configuration has higher priority than autoExternal + // eg: autoExternal: ['react'], user: output: { externals: { react: 'react-1' } } + // Only handle the case where the externals type is object, string / string[] does not need to be processed, other types are too complex. + const userExternalKeys = + userExternals && isObject(userExternals) ? Object.keys(userExternals) : []; + + const externals = ( + ['dependencies', 'peerDependencies', 'devDependencies'] as const + ) + .reduce((prev, type) => { + if (externalOptions[type]) { + return pkgJson[type] ? prev.concat(Object.keys(pkgJson[type]!)) : prev; + } + return prev; + }, []) + .filter((name) => !userExternalKeys.includes(name)); + + const uniqueExternals = Array.from(new Set(externals)); + + return externals.length + ? { + output: { + externals: [ + // Exclude dependencies, e.g. `react`, `react/jsx-runtime` + ...uniqueExternals.map((dep) => new RegExp(`^${dep}($|\\/|\\\\)`)), + ...uniqueExternals, + ], + }, + } + : {}; +}; + export async function createInternalRsbuildConfig(): Promise { return defineRsbuildConfig({ dev: { @@ -166,15 +229,15 @@ const composeFormatConfig = (format: Format): RsbuildConfig => { const composeAutoExtensionConfig = ( format: Format, - root: string, autoExtension: boolean, + pkgJson?: PkgJson, ): { config: RsbuildConfig; dtsExtension: string; } => { const { jsExtension, dtsExtension } = getDefaultExtension({ format, - root, + pkgJson, autoExtension, }); @@ -394,17 +457,24 @@ async function composeLibRsbuildConfig( configPath: string, ) { const config = mergeRsbuildConfig(rsbuildConfig, libConfig); + const rootPath = dirname(configPath); + const pkgJson = readPackageJson(rootPath); - const { format, autoExtension = true } = config; + const { format, autoExtension = true, autoExternal = true } = config; const formatConfig = composeFormatConfig(format!); const { config: autoExtensionConfig, dtsExtension } = - composeAutoExtensionConfig(format!, dirname(configPath), autoExtension); + composeAutoExtensionConfig(format!, autoExtension, pkgJson); const bundleConfig = composeBundleConfig(config.bundle); const targetConfig = composeTargetConfig(config.output?.target); const syntaxConfig = composeSyntaxConfig( config.output?.syntax, config.output?.target, ); + const autoExternalConfig = composeAutoExternalConfig({ + autoExternal, + pkgJson, + userExternals: rsbuildConfig.output?.externals, + }); const entryConfig = await composeEntryConfig( config.source?.entry, config.bundle, @@ -415,6 +485,7 @@ async function composeLibRsbuildConfig( return mergeRsbuildConfig( formatConfig, autoExtensionConfig, + autoExternalConfig, syntaxConfig, bundleConfig, targetConfig, diff --git a/packages/core/src/types/config/index.ts b/packages/core/src/types/config/index.ts index c7486c80..df08a54f 100644 --- a/packages/core/src/types/config/index.ts +++ b/packages/core/src/types/config/index.ts @@ -31,10 +31,19 @@ export type Dts = } | false; +export type AutoExternal = + | boolean + | { + dependencies?: boolean; + devDependencies?: boolean; + peerDependencies?: boolean; + }; + export interface LibConfig extends RsbuildConfig { bundle?: boolean; format?: Format; autoExtension?: boolean; + autoExternal?: AutoExternal; output?: RsbuildConfig['output'] & { /** Support esX and browserslist query */ syntax?: Syntax; diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts new file mode 100644 index 00000000..1d6a9e83 --- /dev/null +++ b/packages/core/src/types/index.ts @@ -0,0 +1,2 @@ +export * from './config'; +export * from './utils'; diff --git a/packages/core/src/types/utils.ts b/packages/core/src/types/utils.ts new file mode 100644 index 00000000..8b3d3129 --- /dev/null +++ b/packages/core/src/types/utils.ts @@ -0,0 +1,6 @@ +export type PkgJson = { + dependencies?: Record; + peerDependencies?: Record; + devDependencies?: Record; + [key: string]: unknown; +}; diff --git a/packages/core/src/utils/extension.ts b/packages/core/src/utils/extension.ts index fc6b5f50..98e64c2b 100644 --- a/packages/core/src/utils/extension.ts +++ b/packages/core/src/utils/extension.ts @@ -1,18 +1,16 @@ -import fs from 'node:fs'; -import { resolve } from 'node:path'; -import type { Format } from 'src/types/config'; +import type { Format, PkgJson } from 'src/types'; import { logger } from './logger'; export const getDefaultExtension = (options: { format: Format; - root: string; + pkgJson?: PkgJson; autoExtension: boolean; }): { jsExtension: string; dtsExtension: string; isModule?: boolean; } => { - const { format, root, autoExtension } = options; + const { format, pkgJson, autoExtension } = options; let jsExtension = '.js'; let dtsExtension = '.d.ts'; @@ -24,10 +22,9 @@ export const getDefaultExtension = (options: { }; } - const pkgJsonPath = resolve(root, './package.json'); - if (!fs.existsSync(pkgJsonPath)) { + if (!pkgJson) { logger.warn( - `package.json does not exist in ${pkgJsonPath}, autoExtension will not be applied.`, + 'autoExtension configuration will not be applied due to read package.json failed', ); return { jsExtension, @@ -35,20 +32,7 @@ export const getDefaultExtension = (options: { }; } - let isModule = false; - - try { - const json = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')); - isModule = json.type === 'module'; - } catch (e) { - logger.warn( - `Failed to parse ${pkgJsonPath}, it might not be valid JSON, autoExtension will not be applied.`, - ); - return { - jsExtension, - dtsExtension, - }; - } + const isModule = pkgJson.type === 'module'; if (isModule && format === 'cjs') { jsExtension = '.cjs'; diff --git a/packages/core/src/utils/helper.ts b/packages/core/src/utils/helper.ts index 6d522bf7..ca36e081 100644 --- a/packages/core/src/utils/helper.ts +++ b/packages/core/src/utils/helper.ts @@ -1,6 +1,9 @@ +import fs from 'node:fs'; import fsP from 'node:fs/promises'; import path from 'node:path'; import color from 'picocolors'; +import type { PkgJson } from '../types'; +import { logger } from './logger'; /** * Node.js built-in modules. @@ -99,4 +102,23 @@ async function calcLongestCommonPath( return lca; } +export const readPackageJson = (rootPath: string): undefined | PkgJson => { + const pkgJsonPath = path.resolve(rootPath, './package.json'); + + if (!fs.existsSync(pkgJsonPath)) { + logger.warn(`package.json does not exist in the ${rootPath} directory`); + return; + } + + try { + return JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')); + } catch (err) { + logger.warn(`Failed to parse ${pkgJsonPath}, it might not be valid JSON`); + return; + } +}; + +export const isObject = (obj: unknown): obj is Record => + Object.prototype.toString.call(obj) === '[object Object]'; + export { color, calcLongestCommonPath }; diff --git a/packages/core/tests/config.test.ts b/packages/core/tests/config.test.ts index d64f8bd3..290d310e 100644 --- a/packages/core/tests/config.test.ts +++ b/packages/core/tests/config.test.ts @@ -1,8 +1,10 @@ import { join } from 'node:path'; -import { describe, expect, test } from 'vitest'; +import { describe, expect, test, vi } from 'vitest'; import { composeCreateRsbuildConfig, loadConfig } from '../src/config'; import type { RslibConfig } from '../src/types/config'; +vi.mock('rslog'); + describe('Should load config file correctly', () => { test('Load config.js in cjs project', async () => { const fixtureDir = join(__dirname, 'fixtures/config/cjs'); diff --git a/packages/core/tests/extension.test.ts b/packages/core/tests/extension.test.ts index eedadcf6..7e5b05f3 100644 --- a/packages/core/tests/extension.test.ts +++ b/packages/core/tests/extension.test.ts @@ -1,12 +1,13 @@ -import { join } from 'node:path'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { getDefaultExtension } from '../src/utils/extension'; +vi.mock('rslog'); + describe('should get extension correctly', () => { it('autoExtension is false', () => { const options = { format: 'cjs', - root: '/path/to/root', + pkgJson: {}, autoExtension: false, }; @@ -21,7 +22,6 @@ describe('should get extension correctly', () => { it('package.json is broken', () => { const options = { format: 'cjs', - root: '/path/to/root', autoExtension: true, }; @@ -36,7 +36,9 @@ describe('should get extension correctly', () => { it('format is cjs and type is module in package.json', () => { const options = { format: 'cjs', - root: join(__dirname, 'fixtures/extension/type-module'), + pkgJson: { + type: 'module', + }, autoExtension: true, }; @@ -52,7 +54,9 @@ describe('should get extension correctly', () => { it('format is cjs and type is commonjs in package.json', () => { const options = { format: 'cjs', - root: join(__dirname, 'fixtures/extension/type-commonjs'), + pkgJson: { + type: 'commonjs', + }, autoExtension: true, }; @@ -68,7 +72,9 @@ describe('should get extension correctly', () => { it('format is esm and type is commonjs in package.json', () => { const options = { format: 'esm', - root: join(__dirname, 'fixtures/extension/type-commonjs'), + pkgJson: { + type: 'commonjs', + }, autoExtension: true, }; @@ -84,7 +90,9 @@ describe('should get extension correctly', () => { it('format is esm and type is module in package.json', () => { const options = { format: 'esm', - root: join(__dirname, 'fixtures/extension/type-module'), + pkgJson: { + type: 'module', + }, autoExtension: true, }; diff --git a/packages/core/tests/external.test.ts b/packages/core/tests/external.test.ts new file mode 100644 index 00000000..343b35d7 --- /dev/null +++ b/packages/core/tests/external.test.ts @@ -0,0 +1,138 @@ +import { describe, expect, it, vi } from 'vitest'; +import { composeAutoExternalConfig } from '../src/config'; + +vi.mock('rslog'); + +describe('should composeAutoExternalConfig correctly', () => { + it('autoExternal is true', () => { + const result = composeAutoExternalConfig({ + autoExternal: true, + pkgJson: { + dependencies: { + foo: '1.0.0', + foo1: '1.0.0', + }, + devDependencies: { + bar: '1.0.0', + }, + peerDependencies: { + baz: '1.0.0', + }, + }, + }); + + expect(result).toEqual({ + output: { + externals: [ + /^foo($|\/|\\)/, + /^foo1($|\/|\\)/, + /^baz($|\/|\\)/, + 'foo', + 'foo1', + 'baz', + ], + }, + }); + }); + + it('autoExternal will deduplication ', () => { + const result = composeAutoExternalConfig({ + autoExternal: true, + pkgJson: { + dependencies: { + foo: '1.0.0', + foo1: '1.0.0', + }, + devDependencies: { + bar: '1.0.0', + }, + peerDependencies: { + baz: '1.0.0', + foo: '1.0.0', + foo1: '1.0.0', + }, + }, + }); + + expect(result).toEqual({ + output: { + externals: [ + /^foo($|\/|\\)/, + /^foo1($|\/|\\)/, + /^baz($|\/|\\)/, + 'foo', + 'foo1', + 'baz', + ], + }, + }); + }); + + it('autoExternal is object', () => { + const result = composeAutoExternalConfig({ + autoExternal: { + peerDependencies: false, + devDependencies: true, + }, + pkgJson: { + dependencies: { + foo: '1.0.0', + }, + devDependencies: { + bar: '1.0.0', + }, + peerDependencies: { + baz: '1.0.0', + }, + }, + }); + + expect(result).toEqual({ + output: { + externals: [/^foo($|\/|\\)/, /^bar($|\/|\\)/, 'foo', 'bar'], + }, + }); + }); + + it('autoExternal is false', () => { + const result = composeAutoExternalConfig({ + autoExternal: false, + pkgJson: { + dependencies: { + foo: '1.0.0', + }, + }, + }); + + expect(result).toEqual({}); + }); + + it('autoExternal with user externals object', () => { + const result = composeAutoExternalConfig({ + autoExternal: true, + pkgJson: { + dependencies: { + foo: '1.0.0', + bar: '1.0.0', + }, + }, + userExternals: { + foo: 'foo-1', + }, + }); + + expect(result).toEqual({ + output: { + externals: [/^bar($|\/|\\)/, 'bar'], + }, + }); + }); + + it('read package.json failed', () => { + const result = composeAutoExternalConfig({ + autoExternal: true, + }); + + expect(result).toEqual({}); + }); +}); diff --git a/packages/core/tests/fixtures/extension/type-commonjs/package.json b/packages/core/tests/fixtures/extension/type-commonjs/package.json deleted file mode 100644 index 5bbefffb..00000000 --- a/packages/core/tests/fixtures/extension/type-commonjs/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "commonjs" -} diff --git a/packages/core/tests/fixtures/extension/type-module/package.json b/packages/core/tests/fixtures/extension/type-module/package.json deleted file mode 100644 index 3dbc1ca5..00000000 --- a/packages/core/tests/fixtures/extension/type-module/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "module" -} diff --git a/packages/core/tests/helper.test.ts b/packages/core/tests/helper.test.ts index 4da34e31..15fd5fdc 100644 --- a/packages/core/tests/helper.test.ts +++ b/packages/core/tests/helper.test.ts @@ -1,52 +1,13 @@ -import { vol } from 'memfs'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { calcLongestCommonPath } from '../src/utils/helper'; +import { join } from 'node:path'; +import { expect, it, vi } from 'vitest'; +import { readPackageJson } from '../src/utils/helper'; -vi.mock('node:fs'); -vi.mock('node:fs/promises'); +vi.mock('rslog'); -describe('LCA calculate correctly', () => { - beforeEach(() => { - vol.reset(); - }); - - it('empty array', async () => { - const result = await calcLongestCommonPath([]); - expect(result).toBe(null); - }); - - it('correct 1', async () => { - vol.fromJSON({ '/Users/Someone/project-a/src': null }); - - const result = await calcLongestCommonPath([ - '/Users/Someone/project-a/src/helpers', - '/Users/Someone/project-a/src', - '/Users/Someone/project-a/src/utils', - ]); - expect(result).toBe('/Users/Someone/project-a/src'); - }); - - it('correct 2', async () => { - vol.fromJSON({ '/Users/Someone/project-monorepo': null }); - - const result = await calcLongestCommonPath([ - '/Users/Someone/project-monorepo/packages-a/src/index.ts', - '/Users/Someone/project-monorepo/packages-util/src/index.js', - '/Users/Someone/project-monorepo/script.js', - ]); - - expect(result).toBe('/Users/Someone/project-monorepo'); - }); - - it('correct 3', async () => { - vol.fromJSON({ - '/Users/Someone/project/src/index.js': '', - }); - - const result = await calcLongestCommonPath([ - '/Users/Someone/project/src/index.js', - ]); +it('readPackageJson correctly', async () => { + expect(readPackageJson('path/to/root')).toBeUndefined(); - expect(result).toBe('/Users/Someone/project/src'); + expect(readPackageJson(join(__dirname, 'fixtures/config/esm'))).toEqual({ + type: 'module', }); }); diff --git a/packages/core/tests/lcp.test.ts b/packages/core/tests/lcp.test.ts new file mode 100644 index 00000000..97bdcc63 --- /dev/null +++ b/packages/core/tests/lcp.test.ts @@ -0,0 +1,53 @@ +import { vol } from 'memfs'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { calcLongestCommonPath } from '../src/utils/helper'; + +vi.mock('node:fs'); +vi.mock('node:fs/promises'); + +// LCP test mock will affect other tests +describe('LCP calculate correctly', () => { + beforeEach(() => { + vol.reset(); + }); + + it('empty array', async () => { + const result = await calcLongestCommonPath([]); + expect(result).toBe(null); + }); + + it('correct 1', async () => { + vol.fromJSON({ '/Users/Someone/project-a/src': null }); + + const result = await calcLongestCommonPath([ + '/Users/Someone/project-a/src/helpers', + '/Users/Someone/project-a/src', + '/Users/Someone/project-a/src/utils', + ]); + expect(result).toBe('/Users/Someone/project-a/src'); + }); + + it('correct 2', async () => { + vol.fromJSON({ '/Users/Someone/project-monorepo': null }); + + const result = await calcLongestCommonPath([ + '/Users/Someone/project-monorepo/packages-a/src/index.ts', + '/Users/Someone/project-monorepo/packages-util/src/index.js', + '/Users/Someone/project-monorepo/script.js', + ]); + + expect(result).toBe('/Users/Someone/project-monorepo'); + }); + + it('correct 3', async () => { + vol.fromJSON({ + '/Users/Someone/project/src/index.js': '', + }); + + const result = await calcLongestCommonPath([ + '/Users/Someone/project/src/index.js', + ]); + + expect(result).toBe('/Users/Someone/project/src'); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ab6872a..aea969d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -94,6 +94,30 @@ importers: specifier: ^11.2.0 version: 11.2.0 + e2e/cases/auto-external/default: + dependencies: + react: + specifier: ^18.3.1 + version: 18.3.1 + + e2e/cases/auto-external/external-sub-path: + dependencies: + react: + specifier: ^18.3.1 + version: 18.3.1 + + e2e/cases/auto-external/false: + dependencies: + react: + specifier: ^18.3.1 + version: 18.3.1 + + e2e/cases/auto-external/with-externals: + dependencies: + react: + specifier: ^18.3.1 + version: 18.3.1 + e2e/cases/autoExtension/type-commonjs: {} e2e/cases/autoExtension/type-module: {}