diff --git a/playground/css-export/package.json b/playground/css-export/package.json new file mode 100644 index 00000000..4b5e6de2 --- /dev/null +++ b/playground/css-export/package.json @@ -0,0 +1,27 @@ +{ + "name": "css-export", + "version": "1.0.0", + "private": true, + "license": "MIT", + "sideEffects": false, + "type": "commonjs", + "exports": { + ".": { + "source": "./src/index.js", + "import": "./dist/index.mjs", + "require": "./dist/index.js", + "default": "./dist/index.js" + }, + "./css/styles.css": "./src/css/styles.css", + "./package.json": "./package.json" + }, + "main": "./dist/index.js", + "files": [ + "dist", + "src" + ], + "scripts": { + "build": "pkg build --strict --check --clean", + "clean": "rimraf dist" + } +} diff --git a/playground/css-export/src/css/styles.css b/playground/css-export/src/css/styles.css new file mode 100644 index 00000000..8d966863 --- /dev/null +++ b/playground/css-export/src/css/styles.css @@ -0,0 +1,3 @@ +body { + background-color: palevioletred; +} diff --git a/playground/css-export/src/index.js b/playground/css-export/src/index.js new file mode 100644 index 00000000..ead516c9 --- /dev/null +++ b/playground/css-export/src/index.js @@ -0,0 +1 @@ +export default () => {} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ddac1c8..c1d1f79f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -255,6 +255,8 @@ importers: playground/browser-bundle: {} + playground/css-export: {} + playground/custom-dist: {} playground/default-export: {} diff --git a/src/node/core/pkg/parseExports.ts b/src/node/core/pkg/parseExports.ts index 22e33a71..61eb2f12 100644 --- a/src/node/core/pkg/parseExports.ts +++ b/src/node/core/pkg/parseExports.ts @@ -1,3 +1,6 @@ +import {existsSync} from 'node:fs' +import {resolve as resolvePath} from 'node:path' + import type {Logger} from '../../logger' import type {InferredStrictOptions} from '../../strict' import {defaultEnding, fileEnding, legacyEnding} from '../../tasks/dts/getTargetPaths' @@ -9,13 +12,14 @@ import {validateExports} from './validateExports' /** @internal */ export function parseExports(options: { + cwd: string pkg: PackageJSON strict: boolean strictOptions: InferredStrictOptions legacyExports: boolean logger: Logger }): (PkgExport & {_path: string})[] { - const {pkg, strict, strictOptions, legacyExports, logger} = options + const {cwd, pkg, strict, strictOptions, legacyExports, logger} = options const type = pkg.type || 'commonjs' const errors: string[] = [] @@ -169,9 +173,19 @@ export function parseExports(options: { ) { if (exportPath === './package.json') { if (exportEntry !== './package.json') { - errors.push('package.json: `exports["./package.json"] must be "./package.json".') + errors.push('package.json: `exports["./package.json"]` must be "./package.json".') } } + } else if (exportPath.endsWith('.css')) { + if (typeof exportEntry === 'string' && !existsSync(resolvePath(cwd, exportEntry))) { + errors.push( + `package.json: \`exports[${JSON.stringify(exportPath)}]\`: file does not exist.`, + ) + } else if (typeof exportEntry !== 'string') { + errors.push( + `package.json: \`exports[${JSON.stringify(exportPath)}]\`: export conditions not supported for CSS files.`, + ) + } } else if (isRecord(exportEntry) && 'svelte' in exportEntry) { // @TODO should we report a warning or a debug message here about a detected svelte export that is ignored? } else if (isPkgExport(exportEntry)) { diff --git a/src/node/core/pkg/types.ts b/src/node/core/pkg/types.ts index ded1b525..31ae3e9b 100644 --- a/src/node/core/pkg/types.ts +++ b/src/node/core/pkg/types.ts @@ -14,6 +14,7 @@ export interface PackageJSON { exports?: Record< string, | `./${string}.json` + | `./${string}.css` | { source?: string types?: string diff --git a/src/node/core/pkg/validatePkg.ts b/src/node/core/pkg/validatePkg.ts index 8e83e20c..ca433fca 100644 --- a/src/node/core/pkg/validatePkg.ts +++ b/src/node/core/pkg/validatePkg.ts @@ -20,6 +20,7 @@ const pkgSchema = z.object({ z.record( z.union([ z.custom<`./${string}.json`>((val) => /^\.\/.*\.json$/.test(val as string)), + z.custom<`./${string}.css`>((val) => /^\.\/.*\.css$/.test(val as string)), z.object({ 'types': z.optional(z.string()), 'source': z.optional(z.string()), diff --git a/src/node/resolveBuildContext.ts b/src/node/resolveBuildContext.ts index 13787aab..99b8cfe1 100644 --- a/src/node/resolveBuildContext.ts +++ b/src/node/resolveBuildContext.ts @@ -93,6 +93,7 @@ export async function resolveBuildContext(options: { } const parsedExports = parseExports({ + cwd, pkg, strict, legacyExports: config?.legacyExports ?? false, diff --git a/test/cli.test.ts b/test/cli.test.ts index 62f6337a..e54971db 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -161,6 +161,19 @@ test.skipIf(isWindows)('should build `ts-bundler` package', async () => { await project.remove() }) +test.skipIf(isWindows)('should build `css-export` package', async () => { + const project = await spawnProject('css-export') + + await project.install() + + const {stdout} = await project.run('build') + + expect(stdout).toContain('./src/index.js → ./dist/index.js') + // @TODO test that styles.css is available as an export from the package + + await project.remove() +}) + describe.skip('runtime: webpack v3', () => { test('import `dist/*.browser.js` from package', async () => { const exportsDummy = await spawnProject('dummy-module') diff --git a/test/parseExports.test.ts b/test/parseExports.test.ts index 4cc7533b..3d4a0253 100644 --- a/test/parseExports.test.ts +++ b/test/parseExports.test.ts @@ -16,6 +16,7 @@ const defaults = { const files = ['dist'] const strictOptions = parseStrictOptions({}) const logger = createLogger() +const cwd = process.cwd() describe.each([ {type: 'commonjs' as const, legacyExports: false}, @@ -28,9 +29,9 @@ describe.each([ const testParseExports = ( options: Omit< Parameters[0], - 'strict' | 'strictOptions' | 'legacyExports' | 'logger' + 'strict' | 'strictOptions' | 'legacyExports' | 'logger' | 'cwd' >, - ) => parseExports({strict: true, legacyExports, logger, strictOptions, ...options}) + ) => parseExports({strict: true, legacyExports, logger, strictOptions, cwd, ...options}) const reference = { '.': { source: defaults['.'].source, @@ -464,6 +465,74 @@ describe.each([ expect(() => testParseExports({pkg})).toThrowErrorMatchingSnapshot() }) + test('css exports must be strings (paths)', () => { + const pkg = { + type, + name, + version, + files, + main: './lib/index.js', + types: './lib/index.d.ts', + exports: { + '.': { + source: './src/index.ts', + default: './lib/index.js', + }, + './style.css': {source: './src/style.css', default: './lib/style.css'}, + './package.json': './package.json', + }, + } satisfies PackageJSON + + expect(() => testParseExports({pkg})).toThrowError( + 'package.json: `exports["./style.css"]`: export conditions not supported for CSS files.', + ) + }) + + test('css exports must exist on file system', () => { + const pkg = { + type, + name, + version, + files, + main: './lib/index.js', + types: './lib/index.d.ts', + exports: { + '.': { + source: './src/index.ts', + default: './lib/index.js', + }, + './style.css': './src/style.css', + './package.json': './package.json', + }, + } satisfies PackageJSON + + expect(() => testParseExports({pkg})).toThrowError( + 'package.json: `exports["./style.css"]`: file does not exist.', + ) + }) + + test('the "package.json" field should be set to "./package.json" (if set)', () => { + const pkg = { + type, + name, + version, + files, + main: './lib/index.js', + types: './lib/index.d.ts', + exports: { + '.': { + source: './src/index.ts', + default: './lib/index.js', + }, + './package.json': './other.json', + }, + } satisfies PackageJSON + + expect(() => testParseExports({pkg})).toThrowError( + 'package.json: `exports["./package.json"]` must be "./package.json"', + ) + }) + describe.runIf(legacyExports)('legacyExports: true', () => { test('it handles "browsers" if it only redirects "source"', () => { const pkg = { diff --git a/test/parseTasks.test.ts b/test/parseTasks.test.ts index 917df8fc..e05e23f2 100644 --- a/test/parseTasks.test.ts +++ b/test/parseTasks.test.ts @@ -11,6 +11,7 @@ import {parseStrictOptions} from '../src/node/strict' const strictOptions = parseStrictOptions({}) const logger = createLogger() +const cwd = process.cwd() test('should parse tasks (type: module)', () => { const pkg: PackageJSON = { @@ -41,9 +42,17 @@ test('should parse tasks (type: module)', () => { }, } - const exports = parseExports({pkg, strict: true, strictOptions, logger, legacyExports: false}) + const exports = parseExports({ + cwd, + pkg, + strict: true, + strictOptions, + logger, + legacyExports: false, + }) const ctx: BuildContext = { + bundledPackages: [], cwd: '/test', distPath: '/test/dist', emitDeclarationOnly: false, @@ -177,9 +186,17 @@ test('should parse tasks (type: commonjs, legacyExports: true)', () => { }, } - const exports = parseExports({pkg, strict: true, logger, strictOptions, legacyExports: true}) + const exports = parseExports({ + cwd, + pkg, + strict: true, + logger, + strictOptions, + legacyExports: true, + }) const ctx: BuildContext = { + bundledPackages: [], config: {legacyExports: true}, cwd: '/test', distPath: '/test/dist',