diff --git a/packages/configs/default/index.json b/packages/configs/default/index.json index e69b559f4a5..13bf64b4ba0 100644 --- a/packages/configs/default/index.json +++ b/packages/configs/default/index.json @@ -51,13 +51,13 @@ "data-url:*": ["...", "@parcel/optimizer-data-url"], "*.css": ["@parcel/optimizer-cssnano"], "*.html": ["@parcel/optimizer-htmlnano"], - "*.js": ["@parcel/optimizer-terser"], + "*.{js,mjs,cjs}": ["@parcel/optimizer-terser"], "*.svg": ["@parcel/optimizer-svgo"] }, "packagers": { "*.html": "@parcel/packager-html", "*.css": "@parcel/packager-css", - "*.js": "@parcel/packager-js", + "*.{js,mjs,cjs}": "@parcel/packager-js", "*.ts": "@parcel/packager-ts", "*.{jsonld,webmanifest}": "@parcel/packager-raw-url", "*": "@parcel/packager-raw" diff --git a/packages/core/core/src/requests/TargetRequest.js b/packages/core/core/src/requests/TargetRequest.js index 747959bb041..ab4a45e2a6a 100644 --- a/packages/core/core/src/requests/TargetRequest.js +++ b/packages/core/core/src/requests/TargetRequest.js @@ -9,6 +9,7 @@ import type { PackageJSON, PackageTargetDescriptor, TargetDescriptor, + OutputFormat, } from '@parcel/types'; import type {StaticRunOpts, RunAPI} from '../RequestTracker'; import type {Entry, ParcelOptions, Target} from '../types'; @@ -50,7 +51,27 @@ type RunOpts = {| |}; const DEFAULT_DIST_DIRNAME = 'dist'; -const COMMON_TARGETS = ['main', 'module', 'browser', 'types']; +const JS_RE = /\.[mc]?js$/; +const JS_EXTENSIONS = ['.js', '.mjs', '.cjs']; +const COMMON_TARGETS = { + main: { + match: JS_RE, + extensions: JS_EXTENSIONS, + }, + module: { + // module field is always ESM. Don't allow .cjs extension here. + match: /\.m?js$/, + extensions: ['.js', '.mjs'], + }, + browser: { + match: JS_RE, + extensions: JS_EXTENSIONS, + }, + types: { + match: /\.d\.ts$/, + extensions: ['.d.ts'], + }, +}; export type TargetRequest = {| id: string, @@ -438,7 +459,7 @@ export class TargetResolver { }; } - for (let targetName of COMMON_TARGETS) { + for (let targetName in COMMON_TARGETS) { let _targetDist; let pointer; if ( @@ -492,10 +513,126 @@ export class TargetResolver { continue; } - let isLibrary = - typeof distEntry === 'string' - ? path.extname(distEntry) === '.js' - : false; + if ( + distEntry != null && + !COMMON_TARGETS[targetName].match.test(distEntry) + ) { + let contents: string = + typeof pkgContents === 'string' + ? pkgContents + : // $FlowFixMe + JSON.stringify(pkgContents, null, '\t'); + // $FlowFixMe + let listFormat = new Intl.ListFormat('en-US', {type: 'disjunction'}); + let extensions = listFormat.format( + COMMON_TARGETS[targetName].extensions, + ); + let ext = path.extname(distEntry); + throw new ThrowableDiagnostic({ + diagnostic: { + message: md`Unexpected output file type ${ext} in target "${targetName}"`, + origin: '@parcel/core', + language: 'json', + filePath: pkgFilePath ?? undefined, + codeFrame: { + code: contents, + codeHighlights: generateJSONCodeHighlights(contents, [ + { + key: pointer, + type: 'value', + message: `File extension must be ${extensions}`, + }, + ]), + }, + hints: [ + `The "${targetName}" field is meant for libraries. If you meant to output a ${ext} file, either remove the "${targetName}" field or choose a different target name.`, + ], + }, + }); + } + + if (descriptor.outputFormat === 'global') { + let contents: string = + typeof pkgContents === 'string' + ? pkgContents + : // $FlowFixMe + JSON.stringify(pkgContents, null, '\t'); + throw new ThrowableDiagnostic({ + diagnostic: { + message: md`The "global" output format is not supported in the "${targetName}" target.`, + origin: '@parcel/core', + language: 'json', + filePath: pkgFilePath ?? undefined, + codeFrame: { + code: contents, + codeHighlights: generateJSONCodeHighlights(contents, [ + { + key: `/targets/${targetName}/outputFormat`, + type: 'value', + }, + ]), + }, + hints: [ + `The "${targetName}" field is meant for libraries. The outputFormat must be either "commonjs" or "esmodule". Either change or remove the declared outputFormat.`, + ], + }, + }); + } + + let inferredOutputFormat = this.inferOutputFormat( + distEntry, + descriptor, + targetName, + pkg, + pkgFilePath, + pkgContents, + ); + + let outputFormat = + descriptor.outputFormat ?? + inferredOutputFormat ?? + (targetName === 'module' ? 'esmodule' : 'commonjs'); + let isModule = outputFormat === 'esmodule'; + + if ( + targetName === 'main' && + outputFormat === 'esmodule' && + inferredOutputFormat !== 'esmodule' + ) { + let contents: string = + typeof pkgContents === 'string' + ? pkgContents + : // $FlowFixMe + JSON.stringify(pkgContents, null, '\t'); + throw new ThrowableDiagnostic({ + diagnostic: { + // prettier-ignore + message: md`Output format "esmodule" cannot be used in the "main" target without a .mjs extension or "type": "module" field.`, + origin: '@parcel/core', + language: 'json', + filePath: pkgFilePath ?? undefined, + codeFrame: { + code: contents, + codeHighlights: generateJSONCodeHighlights(contents, [ + { + key: `/targets/${targetName}/outputFormat`, + type: 'value', + message: 'Declared output format defined here', + }, + { + key: '/main', + type: 'value', + message: 'Inferred output format defined here', + }, + ]), + }, + hints: [ + `Either change the output file extension to .mjs, add "type": "module" to package.json, or remove the declared outputFormat.`, + ], + }, + }); + } + targets.set(targetName, { name: targetName, distDir, @@ -508,18 +645,12 @@ export class TargetResolver { descriptor.context ?? (targetName === 'browser' ? 'browser' - : targetName === 'module' + : isModule ? moduleContext : mainContext), - includeNodeModules: descriptor.includeNodeModules ?? !isLibrary, - outputFormat: - descriptor.outputFormat ?? - (isLibrary - ? targetName === 'module' - ? 'esmodule' - : 'commonjs' - : 'global'), - isLibrary: isLibrary, + includeNodeModules: descriptor.includeNodeModules ?? false, + outputFormat, + isLibrary: true, shouldOptimize: this.options.defaultTargetOptions.shouldOptimize && descriptor.optimize !== false, @@ -534,7 +665,7 @@ export class TargetResolver { } let customTargets = (Object.keys(pkgTargets): Array).filter( - targetName => !COMMON_TARGETS.includes(targetName), + targetName => !COMMON_TARGETS[targetName], ); // Custom targets @@ -598,6 +729,16 @@ export class TargetResolver { if (skipTarget(targetName, exclusiveTarget, descriptor.source)) { continue; } + + let inferredOutputFormat = this.inferOutputFormat( + distEntry, + descriptor, + targetName, + pkg, + pkgFilePath, + pkgContents, + ); + targets.set(targetName, { name: targetName, distDir: @@ -611,7 +752,8 @@ export class TargetResolver { engines: descriptor.engines ?? pkgEngines, context: descriptor.context, includeNodeModules: descriptor.includeNodeModules, - outputFormat: descriptor.outputFormat, + outputFormat: + descriptor.outputFormat ?? inferredOutputFormat ?? undefined, isLibrary: descriptor.isLibrary, shouldOptimize: this.options.defaultTargetOptions.shouldOptimize && @@ -650,6 +792,96 @@ export class TargetResolver { return targets; } + + inferOutputFormat( + distEntry: ?FilePath, + descriptor: PackageTargetDescriptor, + targetName: string, + pkg: PackageJSON, + pkgFilePath: ?FilePath, + pkgContents: ?string, + ): ?OutputFormat { + // Infer the outputFormat based on package.json properties. + // If the extension is .mjs it's always a module. + // If the extension is .cjs, it's always commonjs. + // If the "type" field is set to "module" and the extension is .js, it's a module. + let ext = distEntry != null ? path.extname(distEntry) : null; + let inferredOutputFormat, inferredOutputFormatField; + switch (ext) { + case '.mjs': + inferredOutputFormat = 'esmodule'; + inferredOutputFormatField = `/${targetName}`; + break; + case '.cjs': + inferredOutputFormat = 'commonjs'; + inferredOutputFormatField = `/${targetName}`; + break; + case '.js': + if (pkg.type === 'module') { + inferredOutputFormat = 'esmodule'; + inferredOutputFormatField = '/type'; + } + break; + } + + if ( + descriptor.outputFormat && + inferredOutputFormat && + descriptor.outputFormat !== inferredOutputFormat + ) { + let contents: string = + typeof pkgContents === 'string' + ? pkgContents + : // $FlowFixMe + JSON.stringify(pkgContents, null, '\t'); + let expectedExtensions; + switch (descriptor.outputFormat) { + case 'esmodule': + expectedExtensions = ['.mjs', '.js']; + break; + case 'commonjs': + expectedExtensions = ['.cjs', '.js']; + break; + case 'global': + expectedExtensions = ['.js']; + break; + } + // $FlowFixMe + let listFormat = new Intl.ListFormat('en-US', {type: 'disjunction'}); + throw new ThrowableDiagnostic({ + diagnostic: { + message: md`Declared output format "${descriptor.outputFormat}" does not match expected output format "${inferredOutputFormat}".`, + origin: '@parcel/core', + language: 'json', + filePath: pkgFilePath ?? undefined, + codeFrame: { + code: contents, + codeHighlights: generateJSONCodeHighlights(contents, [ + { + key: `/targets/${targetName}/outputFormat`, + type: 'value', + message: 'Declared output format defined here', + }, + { + key: nullthrows(inferredOutputFormatField), + type: 'value', + message: 'Inferred output format defined here', + }, + ]), + }, + hints: [ + inferredOutputFormatField === '/type' + ? 'Either remove the target\'s declared "outputFormat" or remove the "type" field.' + : `Either remove the target's declared "outputFormat" or change the extension to ${listFormat.format( + expectedExtensions, + )}.`, + ], + }, + }); + } + + return inferredOutputFormat; + } } function parseEngines( diff --git a/packages/core/core/test/TargetRequest.test.js b/packages/core/core/test/TargetRequest.test.js index 91abed62414..6dc6cd53b87 100644 --- a/packages/core/core/test/TargetRequest.test.js +++ b/packages/core/core/test/TargetRequest.test.js @@ -575,22 +575,245 @@ describe('TargetResolver', () => { ]); }); - it('resolves main target as an application when non-js file extension is used', async () => { + it('errors when the main target contains a non-js extension', async () => { let targetResolver = new TargetResolver(api, DEFAULT_OPTIONS); let fixture = path.join(__dirname, 'fixtures/application-targets'); + let code = await fs.readFile(path.join(fixture, 'package.json'), 'utf8'); + + // $FlowFixMe + await assert.rejects(() => targetResolver.resolve(fixture), { + diagnostics: [ + { + message: 'Unexpected output file type .html in target "main"', + origin: '@parcel/core', + filePath: path.join(fixture, 'package.json'), + language: 'json', + codeFrame: { + code, + codeHighlights: [ + { + end: { + column: 27, + line: 2, + }, + message: 'File extension must be .js, .mjs, or .cjs', + start: { + column: 11, + line: 2, + }, + }, + ], + }, + hints: [ + 'The "main" field is meant for libraries. If you meant to output a .html file, either remove the "main" field or choose a different target name.', + ], + }, + ], + }); + }); + + it('errors when the main target uses the global output format', async () => { + let targetResolver = new TargetResolver(api, DEFAULT_OPTIONS); + let fixture = path.join(__dirname, 'fixtures/main-global'); + let code = await fs.readFile(path.join(fixture, 'package.json'), 'utf8'); + + // $FlowFixMe + await assert.rejects(() => targetResolver.resolve(fixture), { + diagnostics: [ + { + message: + 'The "global" output format is not supported in the "main" target.', + origin: '@parcel/core', + filePath: path.join(fixture, 'package.json'), + language: 'json', + codeFrame: { + code, + codeHighlights: [ + { + message: undefined, + end: { + column: 30, + line: 5, + }, + start: { + column: 23, + line: 5, + }, + }, + ], + }, + hints: [ + 'The "main" field is meant for libraries. The outputFormat must be either "commonjs" or "esmodule". Either change or remove the declared outputFormat.', + ], + }, + ], + }); + }); + + it('errors when the main target uses the esmodule output format without a .mjs extension or "type": "module" field', async () => { + let targetResolver = new TargetResolver(api, DEFAULT_OPTIONS); + let fixture = path.join(__dirname, 'fixtures/main-mjs'); + let code = await fs.readFile(path.join(fixture, 'package.json'), 'utf8'); + + // $FlowFixMe + await assert.rejects(() => targetResolver.resolve(fixture), { + diagnostics: [ + { + message: + 'Output format "esmodule" cannot be used in the "main" target without a .mjs extension or "type": "module" field.', + origin: '@parcel/core', + filePath: path.join(fixture, 'package.json'), + language: 'json', + codeFrame: { + code, + codeHighlights: [ + { + message: 'Declared output format defined here', + end: { + column: 32, + line: 5, + }, + start: { + column: 23, + line: 5, + }, + }, + { + message: 'Inferred output format defined here', + end: { + column: 25, + line: 2, + }, + start: { + column: 11, + line: 2, + }, + }, + ], + }, + hints: [ + 'Either change the output file extension to .mjs, add "type": "module" to package.json, or remove the declared outputFormat.', + ], + }, + ], + }); + }); + + it('errors when the inferred output format does not match the declared one in common targets', async () => { + let targetResolver = new TargetResolver(api, DEFAULT_OPTIONS); + let fixture = path.join(__dirname, 'fixtures/main-format-mismatch'); + let code = await fs.readFile(path.join(fixture, 'package.json'), 'utf8'); + + // $FlowFixMe + await assert.rejects(() => targetResolver.resolve(fixture), { + diagnostics: [ + { + message: + 'Declared output format "esmodule" does not match expected output format "commonjs".', + origin: '@parcel/core', + filePath: path.join(fixture, 'package.json'), + language: 'json', + codeFrame: { + code, + codeHighlights: [ + { + message: 'Declared output format defined here', + end: { + column: 32, + line: 5, + }, + start: { + column: 23, + line: 5, + }, + }, + { + message: 'Inferred output format defined here', + end: { + column: 26, + line: 2, + }, + start: { + column: 11, + line: 2, + }, + }, + ], + }, + hints: [ + 'Either remove the target\'s declared "outputFormat" or change the extension to .mjs or .js.', + ], + }, + ], + }); + }); + + it('errors when the inferred output format does not match the declared one in custom targets', async () => { + let targetResolver = new TargetResolver(api, DEFAULT_OPTIONS); + let fixture = path.join(__dirname, 'fixtures/custom-format-mismatch'); + let code = await fs.readFile(path.join(fixture, 'package.json'), 'utf8'); + + // $FlowFixMe + await assert.rejects(() => targetResolver.resolve(fixture), { + diagnostics: [ + { + message: + 'Declared output format "commonjs" does not match expected output format "esmodule".', + origin: '@parcel/core', + filePath: path.join(fixture, 'package.json'), + language: 'json', + codeFrame: { + code, + codeHighlights: [ + { + message: 'Declared output format defined here', + end: { + column: 32, + line: 5, + }, + start: { + column: 23, + line: 5, + }, + }, + { + message: 'Inferred output format defined here', + end: { + column: 26, + line: 2, + }, + start: { + column: 11, + line: 2, + }, + }, + ], + }, + hints: [ + 'Either remove the target\'s declared "outputFormat" or change the extension to .cjs or .js.', + ], + }, + ], + }); + }); + + it('should infer output format for custom targets by extension', async () => { + let targetResolver = new TargetResolver(api, DEFAULT_OPTIONS); + let fixture = path.join(__dirname, 'fixtures/custom-format-infer-ext'); + assert.deepEqual(await targetResolver.resolve(fixture), [ { - name: 'main', + name: 'test', distDir: path.join(fixture, 'dist'), - distEntry: 'index.html', + distEntry: 'index.mjs', publicUrl: '/', env: { - id: 'c22175d22bace513', + id: 'f1b17591962458cc', context: 'browser', engines: {}, includeNodeModules: true, + outputFormat: 'esmodule', isLibrary: false, - outputFormat: 'global', shouldOptimize: false, shouldScopeHoist: false, sourceMap: {}, @@ -604,7 +827,7 @@ describe('TargetResolver', () => { line: 2, }, end: { - column: 27, + column: 26, line: 2, }, }, @@ -612,6 +835,44 @@ describe('TargetResolver', () => { ]); }); + it('should infer output format for custom targets by "type": "module" field', async () => { + let targetResolver = new TargetResolver(api, DEFAULT_OPTIONS); + let fixture = path.join(__dirname, 'fixtures/custom-format-infer-type'); + + assert.deepEqual(await targetResolver.resolve(fixture), [ + { + name: 'test', + distDir: path.join(fixture, 'dist'), + distEntry: 'index.js', + publicUrl: '/', + env: { + id: 'f1b17591962458cc', + context: 'browser', + engines: {}, + includeNodeModules: true, + outputFormat: 'esmodule', + isLibrary: false, + shouldOptimize: false, + shouldScopeHoist: false, + sourceMap: {}, + loc: undefined, + sourceType: 'module', + }, + loc: { + filePath: path.join(fixture, 'package.json'), + start: { + column: 11, + line: 3, + }, + end: { + column: 25, + line: 3, + }, + }, + }, + ]); + }); + it('resolves a subset of package.json targets when given a list of names', async () => { let targetResolver = new TargetResolver(api, { ...DEFAULT_OPTIONS, diff --git a/packages/core/core/test/fixtures/custom-format-infer-ext/package.json b/packages/core/core/test/fixtures/custom-format-infer-ext/package.json new file mode 100644 index 00000000000..dff284fc631 --- /dev/null +++ b/packages/core/core/test/fixtures/custom-format-infer-ext/package.json @@ -0,0 +1,6 @@ +{ + "test": "dist/index.mjs", + "targets": { + "test": {} + } +} diff --git a/packages/core/core/test/fixtures/custom-format-infer-type/package.json b/packages/core/core/test/fixtures/custom-format-infer-type/package.json new file mode 100644 index 00000000000..3728496066f --- /dev/null +++ b/packages/core/core/test/fixtures/custom-format-infer-type/package.json @@ -0,0 +1,7 @@ +{ + "type": "module", + "test": "dist/index.js", + "targets": { + "test": {} + } +} diff --git a/packages/core/core/test/fixtures/custom-format-mismatch/package.json b/packages/core/core/test/fixtures/custom-format-mismatch/package.json new file mode 100644 index 00000000000..ce3c27405d0 --- /dev/null +++ b/packages/core/core/test/fixtures/custom-format-mismatch/package.json @@ -0,0 +1,8 @@ +{ + "test": "dist/index.mjs", + "targets": { + "test": { + "outputFormat": "commonjs" + } + } +} diff --git a/packages/core/core/test/fixtures/main-format-mismatch/package.json b/packages/core/core/test/fixtures/main-format-mismatch/package.json new file mode 100644 index 00000000000..367c2c239e0 --- /dev/null +++ b/packages/core/core/test/fixtures/main-format-mismatch/package.json @@ -0,0 +1,8 @@ +{ + "main": "dist/index.cjs", + "targets": { + "main": { + "outputFormat": "esmodule" + } + } +} diff --git a/packages/core/core/test/fixtures/main-global/package.json b/packages/core/core/test/fixtures/main-global/package.json new file mode 100644 index 00000000000..61f90c95c13 --- /dev/null +++ b/packages/core/core/test/fixtures/main-global/package.json @@ -0,0 +1,8 @@ +{ + "main": "dist/index.js", + "targets": { + "main": { + "outputFormat": "global" + } + } +} diff --git a/packages/core/core/test/fixtures/main-mjs/package.json b/packages/core/core/test/fixtures/main-mjs/package.json new file mode 100644 index 00000000000..60a7ae98b9b --- /dev/null +++ b/packages/core/core/test/fixtures/main-mjs/package.json @@ -0,0 +1,8 @@ +{ + "main": "dist/index.js", + "targets": { + "main": { + "outputFormat": "esmodule" + } + } +} diff --git a/packages/core/integration-tests/test/integration/formats/cjs-type-module/index.js b/packages/core/integration-tests/test/integration/formats/cjs-type-module/index.js new file mode 100644 index 00000000000..788dd894a1c --- /dev/null +++ b/packages/core/integration-tests/test/integration/formats/cjs-type-module/index.js @@ -0,0 +1,3 @@ +import {noop} from 'lodash'; + +noop(); diff --git a/packages/core/integration-tests/test/integration/formats/cjs-type-module/package.json b/packages/core/integration-tests/test/integration/formats/cjs-type-module/package.json new file mode 100644 index 00000000000..6a7aa85d857 --- /dev/null +++ b/packages/core/integration-tests/test/integration/formats/cjs-type-module/package.json @@ -0,0 +1,6 @@ +{ + "name": "cjs-type-module", + "private": true, + "type": "module", + "main": "dist/index.cjs" +} diff --git a/packages/core/integration-tests/test/integration/formats/cjs-type-module/yarn.lock b/packages/core/integration-tests/test/integration/formats/cjs-type-module/yarn.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/core/integration-tests/test/integration/formats/esm-mjs/index.js b/packages/core/integration-tests/test/integration/formats/esm-mjs/index.js new file mode 100644 index 00000000000..788dd894a1c --- /dev/null +++ b/packages/core/integration-tests/test/integration/formats/esm-mjs/index.js @@ -0,0 +1,3 @@ +import {noop} from 'lodash'; + +noop(); diff --git a/packages/core/integration-tests/test/integration/formats/esm-mjs/package.json b/packages/core/integration-tests/test/integration/formats/esm-mjs/package.json new file mode 100644 index 00000000000..6832001f94b --- /dev/null +++ b/packages/core/integration-tests/test/integration/formats/esm-mjs/package.json @@ -0,0 +1,5 @@ +{ + "name": "esm-mjs", + "private": true, + "main": "dist/index.mjs" +} diff --git a/packages/core/integration-tests/test/integration/formats/esm-mjs/yarn.lock b/packages/core/integration-tests/test/integration/formats/esm-mjs/yarn.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/core/integration-tests/test/integration/formats/esm-type-module/index.js b/packages/core/integration-tests/test/integration/formats/esm-type-module/index.js new file mode 100644 index 00000000000..788dd894a1c --- /dev/null +++ b/packages/core/integration-tests/test/integration/formats/esm-type-module/index.js @@ -0,0 +1,3 @@ +import {noop} from 'lodash'; + +noop(); diff --git a/packages/core/integration-tests/test/integration/formats/esm-type-module/package.json b/packages/core/integration-tests/test/integration/formats/esm-type-module/package.json new file mode 100644 index 00000000000..29d27e04886 --- /dev/null +++ b/packages/core/integration-tests/test/integration/formats/esm-type-module/package.json @@ -0,0 +1,6 @@ +{ + "name": "esm-type-module", + "private": true, + "type": "module", + "main": "dist/index.js" +} diff --git a/packages/core/integration-tests/test/integration/formats/esm-type-module/yarn.lock b/packages/core/integration-tests/test/integration/formats/esm-type-module/yarn.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/core/integration-tests/test/output-formats.js b/packages/core/integration-tests/test/output-formats.js index 5e2bbbdca76..455cf0ce9f5 100644 --- a/packages/core/integration-tests/test/output-formats.js +++ b/packages/core/integration-tests/test/output-formats.js @@ -1118,6 +1118,40 @@ describe('output formats', function() { assert.deepEqual(ns.test, true); assert.deepEqual(ns.default, {test: true}); }); + + it('should support outputting .mjs files', async function() { + let b = await bundle( + path.join(__dirname, '/integration/formats/esm-mjs/index.js'), + ); + + let filePath = b.getBundles()[0].filePath; + assert(filePath.endsWith('.mjs')); + let output = await outputFS.readFile(filePath, 'utf8'); + assert(output.includes('import ')); + }); + + it('should support outputting ESM in .js files with "type": "module"', async function() { + let b = await bundle( + path.join(__dirname, '/integration/formats/esm-type-module/index.js'), + ); + + let filePath = b.getBundles()[0].filePath; + assert(filePath.endsWith('.js')); + let output = await outputFS.readFile(filePath, 'utf8'); + assert(output.includes('import ')); + }); + + it('.cjs extension should override "type": "module"', async function() { + let b = await bundle( + path.join(__dirname, '/integration/formats/cjs-type-module/index.js'), + ); + + let filePath = b.getBundles()[0].filePath; + assert(filePath.endsWith('.cjs')); + let output = await outputFS.readFile(filePath, 'utf8'); + assert(!output.includes('import ')); + assert(output.includes('require(')); + }); }); it('should support generating ESM from universal module wrappers', async function() { diff --git a/packages/core/types/index.js b/packages/core/types/index.js index 1d18e856268..4c231426e64 100644 --- a/packages/core/types/index.js +++ b/packages/core/types/index.js @@ -251,6 +251,7 @@ type PackageDependencies = {| export type PackageJSON = { name: PackageName, version: Semver, + type?: 'module', main?: FilePath, module?: FilePath, types?: FilePath, diff --git a/packages/namers/default/src/DefaultNamer.js b/packages/namers/default/src/DefaultNamer.js index b27cd01610d..0faceb2153b 100644 --- a/packages/namers/default/src/DefaultNamer.js +++ b/packages/namers/default/src/DefaultNamer.js @@ -9,6 +9,9 @@ import path from 'path'; import nullthrows from 'nullthrows'; const COMMON_NAMES = new Set(['index', 'src', 'lib']); +const ALLOWED_EXTENSIONS = { + js: ['js', 'mjs', 'cjs'], +}; export default (new Namer({ name({bundle, bundleGraph, options}) { @@ -40,10 +43,9 @@ export default (new Namer({ ) { let loc = bundle.target.loc; let distEntry = bundle.target.distEntry; - if ( - path.extname(bundle.target.distEntry).slice(1) !== bundle.type && - loc - ) { + let distExtension = path.extname(bundle.target.distEntry).slice(1); + let allowedExtensions = ALLOWED_EXTENSIONS[bundle.type] || [bundle.type]; + if (!allowedExtensions.includes(distExtension) && loc) { let fullName = path.relative( path.dirname(loc.filePath), path.join(bundle.target.distDir, distEntry), diff --git a/packages/reporters/cli/src/CLIReporter.js b/packages/reporters/cli/src/CLIReporter.js index bf37ae634dc..917c2e1be2a 100644 --- a/packages/reporters/cli/src/CLIReporter.js +++ b/packages/reporters/cli/src/CLIReporter.js @@ -49,7 +49,7 @@ export async function _report( if (options.serveOptions) { persistMessage( chalk.blue.bold( - `${emoji.info} Server running at ${ + `Server running at ${ options.serveOptions.https ? 'https' : 'http' }://${options.serveOptions.host ?? 'localhost'}:${ options.serveOptions.port @@ -187,9 +187,13 @@ async function writeDiagnostic( writeOut(codeframe, isError); } + if (hints.length > 0) { + writeOut(''); + } + // Write hints for (let hint of hints) { - writeOut(chalk.blue.bold(hint)); + writeOut(`${emoji.hint} ${chalk.blue.bold(hint)}`); } } }