diff --git a/src/__snapshots__/entry.test.ts.snap b/src/__snapshots__/entry.test.ts.snap index e4070a7e..5357d02c 100644 --- a/src/__snapshots__/entry.test.ts.snap +++ b/src/__snapshots__/entry.test.ts.snap @@ -1,7 +1,18 @@ // Vitest Snapshot v1 -exports[`getEntriesFromContext - in TypeScript project > types entry does not accept nesting 1`] = `"\\"types\\" entry must be .d.ts file and cannot be nested!"`; +exports[`getEntriesFromContext > throw if "main" and "module" is on conflict 1`] = ` +"Hint: Did you forgot to set \\"type\\" to 'module' for ESM-first approach? +" +`; -exports[`getEntriesFromContext - in TypeScript project > types entry must has .d.ts extension 1`] = `"\\"types\\" entry must has .d.ts extension!"`; +exports[`getEntriesFromContext - in TypeScript project > types entry does not accept nesting 1`] = ` +"\\"types\\" entry must be .d.ts file and cannot be nested! +" +`; -exports[`getEntriesFromContext - in TypeScript project > types entry must occur first in conditional exports 1`] = `"\\"types\\" entry must occur first in conditional exports for correct type resolution."`; +exports[`getEntriesFromContext - in TypeScript project > types entry must has .d.ts extension 1`] = `"Only .d.ts or .d.cts or .d.mts allowed for \\"types\\" entry."`; + +exports[`getEntriesFromContext - in TypeScript project > types entry must occur first in conditional exports 1`] = ` +"\\"types\\" entry must occur first in conditional exports for correct type resolution. +" +`; diff --git a/src/bin.ts b/src/bin.ts index 25a2fcac..2d6c64d8 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -5,7 +5,7 @@ import { parse as parseTsConfig } from 'tsconfck'; import dedent from 'string-dedent'; import { cli } from './cli'; -import { Reporter } from './reporter'; +import { ConsoleReporter } from './reporter'; import { loadTargets } from './target'; import { loadManifest } from './manifest'; import { parseConfig } from './context'; @@ -15,7 +15,7 @@ import { buildCommand } from './commands/build/build'; const { flags, input } = cli; const [command] = input; -const reporter = new Reporter(console); +const reporter = new ConsoleReporter(console); reporter.level = process.env.DEBUG === 'true' ? 'debug' : 'default'; const resolve = (cwd: string, subpath: string) => path.resolve(cwd, subpath); diff --git a/src/context.test.ts b/src/context.test.ts index f85fd7d7..81cf0739 100644 --- a/src/context.test.ts +++ b/src/context.test.ts @@ -5,11 +5,19 @@ import { type PathResolver } from './common'; import { type Context } from './context'; import { type Flags } from './cli'; import { type Manifest } from './manifest'; -import { Reporter } from './reporter'; +import { type Reporter } from './reporter'; import { parseConfig } from './context'; +class ViReporter implements Reporter { + debug = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + error = vi.fn(); + captureException = vi.fn(); +} + describe('parseConfig', () => { - const reporter = new Reporter(console); + const reporter = new ViReporter(); const resolve: PathResolver = vi.fn(); const defaultFlags: Flags = { cwd: '/project', diff --git a/src/entry.test.ts b/src/entry.test.ts index 3ca0d3fe..0b44c88b 100644 --- a/src/entry.test.ts +++ b/src/entry.test.ts @@ -10,13 +10,21 @@ import { import { type Flags } from './cli'; import { type Manifest } from './manifest'; import { type Entry } from './entry'; -import { Reporter } from './reporter'; +import { type Reporter } from './reporter'; import { parseConfig } from './context'; import { getEntriesFromContext } from "./entry"; import * as formatUtils from './formatUtils'; const resolve = (cwd: string, to: string) => path.join(cwd, to); +class ViReporter implements Reporter { + debug = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + error = vi.fn(); + captureException = vi.fn(); +} + const defaultTargets: string[] = [ 'chrome', 'firefox', @@ -38,16 +46,8 @@ describe('getEntriesFromContext', () => { platform: undefined, }; - const reporter = new Reporter(console); - - const info = vi.spyOn(reporter, 'info'); - const warn = vi.spyOn(reporter, 'warn'); - const error = vi.spyOn(reporter, 'error'); - afterEach(() => { - vi.restoreAllMocks(); - }); - const getEntriesFromManifest = (manifest: Manifest) => { + const reporter = new ViReporter(); const context = parseConfig({ flags: defaultFlags, targets: defaultTargets, @@ -55,30 +55,31 @@ describe('getEntriesFromContext', () => { reporter, resolve, }); - return getEntriesFromContext({ + return { context, - resolve, reporter, - }); - }; - - test("empty package", () => { - expect( - getEntriesFromManifest({ - name: "my-package", + getEntries: () => getEntriesFromContext({ + context, + resolve, + reporter, }), - ).toEqual([]); + }; + }; - expect(warn).not.toHaveBeenCalled(); + test('empty package', () => { + const { getEntries, reporter } = getEntriesFromManifest({ + name: 'my-package', + }); + expect(getEntries()).toEqual([]); + expect(reporter.warn).not.toHaveBeenCalled(); }); - test("main entry, implicit commonjs", () => { - expect( - getEntriesFromManifest({ - name: "my-package", - main: "./lib/index.js", - }), - ).toEqual([ + test('main entry, implicit commonjs', () => { + const { getEntries, reporter } = getEntriesFromManifest({ + name: 'my-package', + main: './lib/index.js', + }); + expect(getEntries()).toEqual([ { key: "main", module: "commonjs", @@ -91,22 +92,20 @@ describe('getEntriesFromContext', () => { outputFile: "/project/lib/index.js", }, ]); - - expect(warn).toHaveBeenCalledWith( + expect(reporter.warn).toHaveBeenCalledWith( expect.stringContaining( `Using ${formatUtils.key('exports')} field is highly recommended.`, ), ); }); - test("main field, explicit commonjs", () => { - expect( - getEntriesFromManifest({ - name: "my-package", - type: "commonjs", - main: "./lib/index.js", - }), - ).toEqual([ + test('main field, explicit commonjs', () => { + const { getEntries, reporter } = getEntriesFromManifest({ + name: 'my-package', + type: 'commonjs', + main: './lib/index.js', + }); + expect(getEntries()).toEqual([ { key: "main", module: "commonjs", @@ -119,22 +118,20 @@ describe('getEntriesFromContext', () => { outputFile: "/project/lib/index.js", }, ]); - - expect(warn).toHaveBeenCalledWith( + expect(reporter.warn).toHaveBeenCalledWith( expect.stringContaining( `Using ${formatUtils.key('exports')} field is highly recommended.`, ), ); }); - test("main field, explicit esmodule", () => { - expect( - getEntriesFromManifest({ - name: "my-package", - type: "module", - main: "./lib/index.js", - }), - ).toEqual([ + test('main field, explicit esmodule', () => { + const { getEntries, reporter } = getEntriesFromManifest({ + name: 'my-package', + type: 'module', + main: './lib/index.js', + }); + expect(getEntries()).toEqual([ { key: "main", module: "esmodule", @@ -147,22 +144,20 @@ describe('getEntriesFromContext', () => { outputFile: "/project/lib/index.js", }, ]); - - expect(warn).toHaveBeenCalledWith( + expect(reporter.warn).toHaveBeenCalledWith( expect.stringContaining( `Using ${formatUtils.key('exports')} field is highly recommended.`, ), ); }); - test("main field with module extension", () => { - expect( - getEntriesFromManifest({ - name: "my-package", - type: "module", - main: "./lib/index.cjs", - }), - ).toEqual([ + test('main field with module extension - module/cjs', () => { + const { getEntries, reporter } = getEntriesFromManifest({ + name: 'my-package', + type: 'module', + main: './lib/index.cjs', + }); + expect(getEntries()).toEqual([ { key: "main", module: "commonjs", @@ -175,21 +170,20 @@ describe('getEntriesFromContext', () => { outputFile: "/project/lib/index.cjs", }, ]); - - expect(warn).toHaveBeenNthCalledWith( - 1, + expect(reporter.warn).toHaveBeenCalledWith( expect.stringContaining( `Using ${formatUtils.key('exports')} field is highly recommended.`, ), ); + }); - expect( - getEntriesFromManifest({ - name: "my-package", - type: "commonjs", - main: "./lib/index.mjs", - }), - ).toEqual([ + test('main field with module extension - commonjs/mjs', () => { + const { getEntries, reporter } = getEntriesFromManifest({ + name: 'my-package', + type: 'commonjs', + main: './lib/index.mjs', + }); + expect(getEntries()).toEqual([ { key: "main", module: "esmodule", @@ -202,22 +196,19 @@ describe('getEntriesFromContext', () => { outputFile: "/project/lib/index.mjs", }, ]); - - expect(warn).toHaveBeenNthCalledWith( - 2, + expect(reporter.warn).toHaveBeenCalledWith( expect.stringContaining( `Using ${formatUtils.key('exports')} field is highly recommended.`, ), ); }); - test("main field accepts js, json, node addon entryPaths", () => { - expect( - getEntriesFromManifest({ - name: "my-package", - main: "./lib/index.json", - }), - ).toEqual([ + test('main field accepts json', () => { + const { getEntries } = getEntriesFromManifest({ + name: 'my-package', + main: './lib/index.json', + }); + expect(getEntries()).toEqual([ { key: "main", module: "file", @@ -230,20 +221,14 @@ describe('getEntriesFromContext', () => { outputFile: "/project/lib/index.json", }, ]); + }); - expect(warn).toHaveBeenNthCalledWith( - 1, - expect.stringContaining( - `Using ${formatUtils.key('exports')} field is highly recommended.`, - ), - ); - - expect( - getEntriesFromManifest({ - name: "my-package", - main: "./lib/index.node", - }), - ).toEqual([ + test('main field accepts node addon', () => { + const { getEntries } = getEntriesFromManifest({ + name: 'my-package', + main: './lib/index.node', + }); + expect(getEntries()).toEqual([ { key: "main", module: "file", @@ -256,21 +241,14 @@ describe('getEntriesFromContext', () => { outputFile: "/project/lib/index.node", }, ]); + }); - expect(warn).toHaveBeenNthCalledWith( - 2, - expect.stringContaining( - `Using ${formatUtils.key('exports')} field is highly recommended.`, - ), - ); - - // otherwise treated as JavaScript text - expect( - getEntriesFromManifest({ - name: "my-package", - main: "./lib/index.css", - }), - ).toEqual([ + test('main field treat unknown extensions as a js file', () => { + const { getEntries } = getEntriesFromManifest({ + name: 'my-package', + main: './lib/index.css', + }); + expect(getEntries()).toEqual([ { key: "main", module: "commonjs", @@ -287,23 +265,15 @@ describe('getEntriesFromContext', () => { outputFile: "/project/lib/index.css", }, ]); - - expect(warn).toHaveBeenNthCalledWith( - 3, - expect.stringContaining( - `Using ${formatUtils.key('exports')} field is highly recommended.`, - ), - ); }); - test("module field", () => { - expect( - getEntriesFromManifest({ - name: "my-package", - main: "./lib/main.js", - module: "./lib/module.js", - }), - ).toEqual([ + test('module field', () => { + const { getEntries, reporter } = getEntriesFromManifest({ + name: 'my-package', + main: './lib/main.js', + module: './lib/module.js', + }); + expect(getEntries()).toEqual([ { key: "main", module: "commonjs", @@ -327,8 +297,7 @@ describe('getEntriesFromContext', () => { outputFile: "/project/lib/module.js", }, ]); - - expect(warn).toHaveBeenCalledWith( + expect(reporter.warn).toHaveBeenCalledWith( expect.stringContaining( `${formatUtils.key('module')} field is not standard and may works in only legacy bundlers.`, ), @@ -336,29 +305,21 @@ describe('getEntriesFromContext', () => { }); test('throw if "main" and "module" is on conflict', () => { - expect( - () => getEntriesFromManifest({ - name: "my-package", - main: "./lib/index.js", - module: "./lib/index.js", - }) - ).toThrowError(); - - expect(error).toHaveBeenCalledWith( - expect.stringContaining( - `Conflict found for ${formatUtils.path('./lib/index.js')}`, - ), - ); + const { getEntries } = getEntriesFromManifest({ + name: 'my-package', + main: './lib/index.js', + module: './lib/index.js', + }); + expect(getEntries).toThrowErrorMatchingSnapshot(); }); test('prefer "exports" over "module" on conflict', () => { - expect( - getEntriesFromManifest({ - name: "my-package", - exports: "./lib/index.js", - module: "./lib/index.js", - }), - ).toEqual([ + const { getEntries, reporter } = getEntriesFromManifest({ + name: 'my-package', + exports: './lib/index.js', + module: './lib/index.js', + }); + expect(getEntries()).toEqual([ { key: "exports", module: "commonjs", @@ -371,33 +332,24 @@ describe('getEntriesFromContext', () => { outputFile: "/project/lib/index.js", }, ]); - - expect(warn).toHaveBeenNthCalledWith( - 1, + expect(reporter.warn).toHaveBeenCalledWith( expect.stringContaining( `Entry ${formatUtils.key('module')} will be ignored since`, ), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), ); - - expect(warn).toHaveBeenNthCalledWith( - 2, + expect(reporter.warn).toHaveBeenCalledWith( expect.stringContaining( `${formatUtils.key('module')} field is not standard and may works in only legacy bundlers.`, ), ); }); - test("exports field", () => { - expect( - getEntriesFromManifest({ - name: "my-package", - exports: "./lib/index.js", - }), - ).toEqual([ + test('exports field', () => { + const { getEntries } = getEntriesFromManifest({ + name: 'my-package', + exports: './lib/index.js', + }); + expect(getEntries()).toEqual([ { key: "exports", module: "commonjs", @@ -412,14 +364,13 @@ describe('getEntriesFromContext', () => { ]); }); - test("exports field always precedense over main", () => { - expect( - getEntriesFromManifest({ - name: "my-package", - main: "./lib/index.js", - exports: "./lib/index.js", - }), - ).toEqual([ + test('exports field always precedense over main #1', () => { + const { getEntries, reporter } = getEntriesFromManifest({ + name: 'my-package', + main: './lib/index.js', + exports: './lib/index.js', + }); + expect(getEntries()).toEqual([ { key: "exports", module: "commonjs", @@ -432,19 +383,19 @@ describe('getEntriesFromContext', () => { outputFile: "/project/lib/index.js", }, ]); + expect(reporter.warn).not.toHaveBeenCalled(); + }); - expect(warn).not.toHaveBeenCalled(); - - expect( - getEntriesFromManifest({ - name: "my-package", - type: "module", - main: "./lib/index.js", - exports: { - require: "./lib/index.js", - }, - }), - ).toEqual([ + test('exports field always precedense over main #2', () => { + const { getEntries, reporter } = getEntriesFromManifest({ + name: 'my-package', + type: 'module', + main: './lib/index.js', + exports: { + require: './lib/index.js', + }, + }); + expect(getEntries()).toEqual([ { key: "exports.require", module: "commonjs", @@ -457,20 +408,14 @@ describe('getEntriesFromContext', () => { outputFile: "/project/lib/index.js", }, ]); - - expect(warn).toHaveBeenNthCalledWith( - 1, + expect(reporter.warn).toHaveBeenCalledWith( expect.stringContaining( `Entry ${formatUtils.key('main')} will be ignored since`, ), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), ); }); - test("conditional exports", () => { + test('conditional exports', () => { expect( getEntriesFromManifest({ name: "my-package", @@ -478,7 +423,7 @@ describe('getEntriesFromContext', () => { require: "./lib/require.js", import: "./lib/import.js", }, - }), + }).getEntries(), ).toEqual([ { key: "exports.require", @@ -515,7 +460,7 @@ describe('getEntriesFromContext', () => { "./feature/index.js": "./lib/feature/index.js", "./package.json": "./package.json", }, - }), + }).getEntries(), ).toEqual([ { key: 'exports["."]', @@ -566,7 +511,7 @@ describe('getEntriesFromContext', () => { import: "./lib/index.js", }, }, - }), + }).getEntries(), ).toEqual([ { key: 'exports["."].require', @@ -617,7 +562,7 @@ describe('getEntriesFromContext', () => { "./deno": "./lib/deno.js", }, }, - }), + }).getEntries(), ).toEqual([ { key: 'exports["."].node.require', @@ -703,30 +648,21 @@ describe('getEntriesFromContext', () => { }); describe('getEntriesFromContext - when rootDir=./src, outDir=.', () => { - const defaultFlags: Flags = { - cwd: '/project', - rootDir: 'src', - outDir: '.', - tsconfig: 'tsconfig.json', - importMaps: 'package.json', - external: [], - standalone: false, - noMinify: false, - noSourcemap: false, - noDts: true, - platform: undefined, - }; - - const reporter = new Reporter(console); - - const info = vi.spyOn(reporter, 'info'); - const warn = vi.spyOn(reporter, 'warn'); - const error = vi.spyOn(reporter, 'error'); - afterEach(() => { - vi.restoreAllMocks(); - }); - const getEntriesFromManifest = (manifest: Manifest) => { + const defaultFlags: Flags = { + cwd: '/project', + rootDir: 'src', + outDir: '.', + tsconfig: 'tsconfig.json', + importMaps: 'package.json', + external: [], + standalone: false, + noMinify: false, + noSourcemap: false, + noDts: true, + platform: undefined, + }; + const reporter = new ViReporter(); const context = parseConfig({ flags: defaultFlags, targets: defaultTargets, @@ -734,11 +670,12 @@ describe('getEntriesFromContext', () => { reporter, resolve, }); - return getEntriesFromContext({ + const getEntries = () => getEntriesFromContext({ context, resolve, reporter, }); + return { getEntries, context, reporter }; }; test('conditional exports', () => { @@ -749,7 +686,7 @@ describe('getEntriesFromContext', () => { require: './require.js', import: './import.js', }, - }), + }).getEntries(), ).toEqual([ { key: 'exports.require', @@ -792,7 +729,7 @@ describe('getEntriesFromContext', () => { './feature/index.js': './feature/index.js', './package.json': './package.json', }, - }), + }).getEntries(), ).toEqual([ { key: 'exports["."]', @@ -838,42 +775,25 @@ describe('getEntriesFromContext', () => { }); describe('getEntriesFromContext - when rootDir=., outDir=./lib', () => { - const defaultFlags: Flags = { - cwd: '/project', - rootDir: '.', - outDir: 'lib', - tsconfig: 'tsconfig.json', - importMaps: 'package.json', - external: [], - standalone: false, - noMinify: false, - noSourcemap: false, - noDts: true, - platform: undefined, - }; - - const reporter = new Reporter(console); - - const info = vi.spyOn(reporter, 'info'); - const warn = vi.spyOn(reporter, 'warn'); - const error = vi.spyOn(reporter, 'error'); - afterEach(() => { - vi.restoreAllMocks(); - }); - const getEntriesFromManifest = (manifest: Manifest) => { + const reporter = new ViReporter(); const context = parseConfig({ - flags: defaultFlags, + flags: { + ...defaultFlags, + rootDir: '.', + outDir: 'lib', + }, targets: defaultTargets, manifest, reporter, resolve, }); - return getEntriesFromContext({ + const getEntries = () => getEntriesFromContext({ context, resolve, reporter, }); + return { getEntries, context, reporter }; }; test('conditional exports', () => { @@ -884,7 +804,7 @@ describe('getEntriesFromContext', () => { require: './lib/require.js', import: './lib/import.js', }, - }), + }).getEntries(), ).toEqual([ { key: 'exports.require', @@ -927,7 +847,7 @@ describe('getEntriesFromContext', () => { './feature/index.js': './lib/feature/index.js', './package.json': './package.json', }, - }), + }).getEntries(), ).toEqual([ { key: 'exports["."]', @@ -988,16 +908,8 @@ describe('getEntriesFromContext - in TypeScript project', () => { platform: undefined, }; - const reporter = new Reporter(console); - - const info = vi.spyOn(reporter, 'info'); - const warn = vi.spyOn(reporter, 'warn'); - const error = vi.spyOn(reporter, 'error'); - afterEach(() => { - vi.restoreAllMocks(); - }); - const getEntriesFromManifest = (manifest: Manifest) => { + const reporter = new ViReporter(); const context = parseConfig({ flags: defaultFlags, targets: defaultTargets, @@ -1006,11 +918,12 @@ describe('getEntriesFromContext - in TypeScript project', () => { resolve, tsconfig: {}, }); - return getEntriesFromContext({ + const getEntries = () => getEntriesFromContext({ context, resolve, reporter, }); + return { getEntries, context, reporter }; }; test('types entry in root manifest', () => { @@ -1020,7 +933,7 @@ describe('getEntriesFromContext - in TypeScript project', () => { main: './lib/index.js', module: './lib/index.mjs', types: './lib/index.d.ts', - }), + }).getEntries(), ).toEqual([ { key: 'main', @@ -1082,7 +995,7 @@ describe('getEntriesFromContext - in TypeScript project', () => { import: './lib/index.mjs', }, }, - }), + }).getEntries(), ).toEqual([ { key: 'exports["."].types', @@ -1133,16 +1046,16 @@ describe('getEntriesFromContext - in TypeScript project', () => { }); test('types entry must has .d.ts extension', () => { - expect(() => + expect( getEntriesFromManifest({ name: 'my-package', types: './lib/index.ts', - }), + }).getEntries, ).toThrowErrorMatchingSnapshot(); }); test('types entry must occur first in conditional exports', () => { - expect(() => + expect( getEntriesFromManifest({ name: 'my-package', exports: { @@ -1153,7 +1066,7 @@ describe('getEntriesFromContext - in TypeScript project', () => { import: './lib/index.mjs', }, }, - }), + }).getEntries, ).toThrowErrorMatchingSnapshot(); }); @@ -1171,7 +1084,7 @@ describe('getEntriesFromContext - in TypeScript project', () => { }, }, }, - }), + }).getEntries(), ).toEqual([ { key: 'exports["."].types', @@ -1249,7 +1162,7 @@ describe('getEntriesFromContext - in TypeScript project', () => { default: './lib/index.js', }, }, - }), + }).getEntries(), ).toEqual([ { key: 'exports["."].types', @@ -1351,7 +1264,7 @@ describe('getEntriesFromContext - in TypeScript project', () => { }, }, }, - }), + }).getEntries(), ).toEqual([ { key: 'exports["."].require.types', @@ -1479,7 +1392,7 @@ describe('getEntriesFromContext - in TypeScript project', () => { }); test('types entry does not accept nesting', () => { - expect(() => + expect( getEntriesFromManifest({ name: 'my-package', exports: { @@ -1491,21 +1404,13 @@ describe('getEntriesFromContext - in TypeScript project', () => { import: './lib/index.mjs', }, }, - }), + }).getEntries, ).toThrowErrorMatchingSnapshot(); }); describe('getEntriesFromContext - when rootDir=outDir=.', () => { - const reporter = new Reporter(console); - - const info = vi.spyOn(reporter, 'info'); - const warn = vi.spyOn(reporter, 'warn'); - const error = vi.spyOn(reporter, 'error'); - afterEach(() => { - vi.restoreAllMocks(); - }); - const getEntriesFromManifest = (manifest: Manifest) => { + const reporter = new ViReporter(); const context = parseConfig({ flags: defaultFlags, targets: defaultTargets, @@ -1519,11 +1424,12 @@ describe('getEntriesFromContext - in TypeScript project', () => { }, }, }); - return getEntriesFromContext({ + const getEntries = () => getEntriesFromContext({ context, resolve, reporter, }); + return { getEntries, context, reporter }; }; test('conditional exports', () => { @@ -1537,7 +1443,7 @@ describe('getEntriesFromContext - in TypeScript project', () => { import: './index.mjs', }, }, - }), + }).getEntries(), ).toEqual([ { key: 'exports["."].types', diff --git a/src/entry.ts b/src/entry.ts index d2bd13bf..be100f66 100644 --- a/src/entry.ts +++ b/src/entry.ts @@ -6,11 +6,7 @@ import { type ConditionalExport } from './manifest'; import { type Context } from './context'; import { type Reporter } from './reporter'; import * as formatUtils from './formatUtils'; -import { - NanobundleConfusingDtsEntryError, - NanobundleInvalidDtsEntryError, - NanobundleInvalidDtsEntryOrderError, -} from './errors'; +import { NanobundleError } from './errors'; export type Entry = { key: string; @@ -24,6 +20,15 @@ export type Entry = { outputFile: string; }; +type EntryTarget = { + key: string, + entryPath: string, + platform: Entry['platform'], + mode: Entry['mode'], + module: Entry['module'], + preferredModule?: 'esmodule' | 'commonjs', +}; + interface GetEntriesFromContext { (props: { context: Context; @@ -65,37 +70,32 @@ export const getEntriesFromContext: GetEntriesFromContext = ({ const entryMap = new Map(); - function addEntry({ - key, - entryPath, - platform, - module, - mode, - preferredModule, - }: { - key: string, - entryPath: string, - platform: Entry['platform'], - mode: Entry['mode'], - module: Entry['module'], - preferredModule?: 'esmodule' | 'commonjs', - }) { + function addEntry(target: EntryTarget) { + const { + key, + entryPath, + platform, + module, + mode, + preferredModule, + } = target; + if (!entryPath.startsWith('./')) { - reporter.error( - `Invalid entry ${formatUtils.key(key)}, entry path should starts with ${formatUtils.literal('./')}`, + throw new NanobundleEntryError( + Message.INVALID_PATH_KEY(key), ); - throw new Error("FIXME"); } - if (key.includes("*") || entryPath.includes("*")) { - reporter.error( - `Ignoring ${formatUtils.key(key)}: subpath pattern(\`*\`) is not supported yet`, + if (entryPath.includes('*')) { + throw new NanobundleEntryError( + Message.SUBPATH_PATTERN(entryPath) ); - throw new Error("FIXME"); } if (module === 'dts' && !/\.d\.(c|m)?ts$/.test(entryPath)) { - throw new NanobundleInvalidDtsEntryError(); + throw new NanobundleEntryError( + Message.INVALID_DTS_FORMAT(), + ); } const entry = entryMap.get(entryPath); @@ -104,45 +104,12 @@ export const getEntriesFromContext: GetEntriesFromContext = ({ if (entry.key.startsWith("exports") && !key.startsWith("exports")) { if (entry.platform !== platform || entry.module !== module) { reporter.warn( - dedent` - Entry ${formatUtils.key(key)} will be ignored since - - %s - %s - - precedense over - - %s ${kleur.bold('(ignored)')} - %s - - `, - formatUtils.key(entry.key), - formatUtils.object({ module: entry.module, platform: entry.platform }), - formatUtils.key(key), - formatUtils.object({ module, platform }), + Message.PRECEDENSE_ENTRY(entry, target), ); } return; } - if (entry.platform !== platform || entry.module !== module) { - let msg = formatUtils.format( - dedent` - Conflict found for ${formatUtils.path(entryPath)} - - %s - %s - - vs - - %s ${kleur.bold('(conflited)')} - %s - `, - formatUtils.key(entry.key), - formatUtils.object({ module: entry.module, platform: entry.platform }), - formatUtils.key(key), - formatUtils.object({ module, platform }), - ); let hint = ''; if ( (entry.key === 'main' && key === 'module') || @@ -161,13 +128,9 @@ export const getEntriesFromContext: GetEntriesFromContext = ({ Or you may not need to specify ${formatUtils.key('require')} or ${formatUtils.key('node')} entries. `; } - if (hint) { - msg += '\n\n'; - msg += hint; - msg += '\n'; - } - reporter.error(msg); - throw new Error("FIXME"); + throw new NanobundleEntryError( + Message.CONFLICT_ENTRY(entry, target, hint), + ); } return; } @@ -348,7 +311,7 @@ export const getEntriesFromContext: GetEntriesFromContext = ({ entryPath, }); } else { - // FIXME: warn + throw new NanobundleEntryError(Message.INVALID_MODULE_EXTENSION()); } } @@ -369,7 +332,7 @@ export const getEntriesFromContext: GetEntriesFromContext = ({ entryPath, }); } else { - throw new Error('"types" entry must has .d.ts extension!'); + throw new NanobundleEntryError(Message.INVALID_TYPES_EXTENSION()); } } @@ -464,7 +427,7 @@ export const getEntriesFromContext: GetEntriesFromContext = ({ } } else if (typeof entryPath === 'object') { if (parentKey === 'types') { - throw new NanobundleInvalidDtsEntryError(); + throw new NanobundleEntryError(Message.INVALID_DTS_FORMAT()); } let entries = Object.entries(entryPath); @@ -472,7 +435,7 @@ export const getEntriesFromContext: GetEntriesFromContext = ({ if (typeof entryPath.types !== 'undefined') { const typesEntryIndex = entries.findIndex(entry => entry[0] === 'types'); if (typesEntryIndex !== 0) { - throw new NanobundleInvalidDtsEntryOrderError(); + throw new NanobundleEntryError(Message.INVALID_DTS_ORDER()); } } else { const firstLeaf = entries.find(entry => typeof entry[1] === 'string'); @@ -487,7 +450,9 @@ export const getEntriesFromContext: GetEntriesFromContext = ({ ]; entries = [dtsExport, ...entries]; } else if (typeof entryPath.require === 'string' && typeof entryPath.import === 'string') { - throw new NanobundleConfusingDtsEntryError(key, entryPath.require, entryPath.import); + throw new NanobundleEntryError( + Message.UNDETEMINED_DTS_SOURCE(key, entryPath.require, entryPath.import), + ); } else if (typeof entryPath.require === 'string') { const dtsExport: [string, ConditionalExport] = [ 'types$implicit', @@ -507,10 +472,7 @@ export const getEntriesFromContext: GetEntriesFromContext = ({ ]; entries = [dtsExport, ...entries]; } else { - reporter.warn(dedent` - ${formatUtils.key(key)} entry may not resolve correctly in TypeScript's Node16 moduleResolution. - Consider to specify ${formatUtils.key('types')} entry for it. - `); + reporter.warn(Message.TYPES_MAY_NOT_BE_RESOLVED(key)); } } } @@ -681,11 +643,7 @@ export const getEntriesFromContext: GetEntriesFromContext = ({ entryPath: manifest.exports, }); } else if (manifest.main || manifest.module) { - reporter.warn(dedent` - Using ${formatUtils.key('exports')} field is highly recommended. - See ${formatUtils.hyperlink('https://nodejs.org/api/packages.html')} for more detail. - - `); + reporter.warn(Message.RECOMMEND_EXPORTS()); } if (typeof manifest.main === 'string') { @@ -700,12 +658,7 @@ export const getEntriesFromContext: GetEntriesFromContext = ({ key: 'module', entryPath: manifest.module, }); - - reporter.warn(dedent` - ${formatUtils.key('module')} field is not standard and may works in only legacy bundlers. Consider using ${formatUtils.key('exports')} instead. - See ${formatUtils.hyperlink('https://nodejs.org/api/packages.html')} for more detail. - - `); + reporter.warn(Message.MODULE_NOT_RECOMMENDED()); } if (typeof manifest.types === 'string') { @@ -720,4 +673,120 @@ export const getEntriesFromContext: GetEntriesFromContext = ({ function inferDtsEntry(entryPath: string): string { return entryPath.replace(/(\.min)?\.(m|c)?js$/, '.d.$2ts'); -} \ No newline at end of file +} + +export class NanobundleEntryError extends NanobundleError { + +} + +export const Message = { + INVALID_MODULE_EXTENSION: () => dedent` + Only ${formatUtils.path('.js')} or ${formatUtils.path('.mjs')} allowed for ${formatUtils.key('module')} entry. + `, + + INVALID_TYPES_EXTENSION: () => dedent` + Only ${formatUtils.path('.d.ts')} or ${formatUtils.path('.d.cts')} or ${formatUtils.path('.d.mts')} allowed for ${formatUtils.key('types')} entry. + `, + + INVALID_PATH_KEY: (path: string) => dedent` + Invalid entry path ${formatUtils.path(path)}, entry path should starts with ${formatUtils.literal('./')}. + + `, + + INVALID_DTS_FORMAT: () => dedent` + ${formatUtils.key('types')} entry must be .d.ts file and cannot be nested! + + `, + + INVALID_DTS_ORDER: () => dedent` + ${formatUtils.key('types')} entry must occur first in conditional exports for correct type resolution. + + `, + + UNDETEMINED_DTS_SOURCE: (key: string, requirePath: string, importPath: string) => dedent` + ${formatUtils.key('types')} entry doesn't set properly for ${formatUtils.key(key)}: + + "require": "${requirePath}", + "import": "${importPath}" + + Solution 1. Explicitly set ${formatUtils.key('types')} entry + + "require": { + "types": "${requirePath.replace(/\.(m|c)?js$/, '.d.$1ts')}", + "default": "${requirePath}" + }, + "import": { + "types": "${importPath.replace(/\.(m|c)?js$/, '.d.$1ts')}", + "default": "${importPath}" + } + + Solution 2. Add ${formatUtils.key('default')} entry + + "require": "${requirePath}", + "import": "${importPath}", + + "default": "/path/to/entry.js" + + `, + + SUBPATH_PATTERN: (path: string) => dedent` + Subpath pattern (${formatUtils.path(path)}) is not supported yet. + `, + + CONFLICT_ENTRY: (a: EntryTarget, b: EntryTarget, hint: string) => formatUtils.format( + dedent` + Conflict found for ${formatUtils.path(a.entryPath)} + + %s + %s + + vs + + %s ${kleur.bold('(conflited)')} + %s + + `, + formatUtils.key(a.key), + formatUtils.object({ module: a.module, platform: a.platform }), + formatUtils.key(b.key), + formatUtils.object({ module: b.module, platform: b.platform }), + ) + hint ? `Hint: ${hint}\n` : '', + + PRECEDENSE_ENTRY: (a: EntryTarget, b: EntryTarget) => formatUtils.format( + dedent` + Entry ${formatUtils.key(b.key)} will be ignored since + + %s + %s + + precedense over + + %s ${kleur.bold('(ignored)')} + %s + + `, + formatUtils.key(a.key), + formatUtils.object({ module: a.module, platform: a.platform }), + formatUtils.key(b.key), + formatUtils.object({ module: b.module, platform: b.platform }), + ), + + RECOMMEND_EXPORTS: () => dedent` + Using ${formatUtils.key('exports')} field is highly recommended. + See ${formatUtils.hyperlink('https://nodejs.org/api/packages.html')} for more detail. + + `, + + MODULE_NOT_RECOMMENDED: () => dedent` + ${formatUtils.key('module')} field is not standard and may works in only legacy bundlers. Consider using ${formatUtils.key('exports')} instead. + See ${formatUtils.hyperlink('https://nodejs.org/api/packages.html')} for more detail. + + `, + + TYPES_MAY_NOT_BE_RESOLVED: (key: string) => dedent` + ${formatUtils.key(key)} entry might not be resolved correctly in ${formatUtils.key('moduleResolution')}: ${formatUtils.literal('Node16')}. + + Consider to specify ${formatUtils.key('types')} entry for it. + + ` + +} as const; diff --git a/src/errors.ts b/src/errors.ts index db5c7b2d..c9588469 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -5,43 +5,7 @@ import * as formatUtils from './formatUtils'; export class NanobundleError extends Error { } -export class NanobundleInvalidDtsEntryError extends NanobundleError { - constructor() { - super('"types" entry must be .d.ts file and cannot be nested!'); - } -} +export class NanobundleConfigError extends NanobundleError { -export class NanobundleInvalidDtsEntryOrderError extends NanobundleError { - constructor() { - super('"types" entry must occur first in conditional exports for correct type resolution.'); - } } -export class NanobundleConfusingDtsEntryError extends NanobundleError { - constructor(key: string, requirePath: string, importPath: string) { - super(dedent` - ${formatUtils.key('types')} entry doesn't set properly for ${formatUtils.key(key)}: - - "require": "${requirePath}", - "import": "${importPath}" - - Solution 1. Explicitly set ${formatUtils.key('types')} entry - - "require": { - "types": "${requirePath.replace(/\.(m|c)?js$/, '.d.$1ts')}", - "default": "${requirePath}" - }, - "import": { - "types": "${importPath.replace(/\.(m|c)?js$/, '.d.$1ts')}", - "default": "${importPath}" - } - - Solution 2. Add ${formatUtils.key('default')} entry - - "require": "${requirePath}", - "import": "${importPath}", - + "default": "/path/to/entry.js" - - `); - } -} \ No newline at end of file diff --git a/src/reporter.ts b/src/reporter.ts index b907c432..71bf83c7 100644 --- a/src/reporter.ts +++ b/src/reporter.ts @@ -3,7 +3,15 @@ import kleur from 'kleur'; import { colorEnabled } from './formatUtils'; -export class Reporter { +export interface Reporter { + debug(msg: string, ...args: any[]): void; + info(msg: string, ...args: any[]): void; + warn(msg: string, ...args: any[]): void; + error(msg: string, ...args: any[]): void; + captureException(exn: unknown): void; +} + +export class ConsoleReporter implements Reporter { #level: number; #console: Console; @@ -72,18 +80,18 @@ export class Reporter { ); } - captureError(error: unknown): void { + captureException(exn: unknown): void { let formatted; - if (error instanceof Error) { + if (exn instanceof Error) { formatted = formatWithOptions( { colors: this.color }, - error.stack, + exn.stack, ); } else { formatted = formatWithOptions( { colors: this.color }, '%s', - error, + exn, ); } const indented = this.#indent(formatted); @@ -92,8 +100,8 @@ export class Reporter { ); } - createChildReporter(): Reporter { - const child = new Reporter(this.#console, this.#level + 1); + createChildReporter(): ConsoleReporter { + const child = new ConsoleReporter(this.#console, this.#level + 1); child.color = this.color; return child; }