Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve library targets #6517

Merged
merged 9 commits into from
Jun 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/configs/default/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
268 changes: 250 additions & 18 deletions packages/core/core/src/requests/TargetRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -438,7 +459,7 @@ export class TargetResolver {
};
}

for (let targetName of COMMON_TARGETS) {
for (let targetName in COMMON_TARGETS) {
let _targetDist;
let pointer;
if (
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -534,7 +665,7 @@ export class TargetResolver {
}

let customTargets = (Object.keys(pkgTargets): Array<string>).filter(
targetName => !COMMON_TARGETS.includes(targetName),
targetName => !COMMON_TARGETS[targetName],
);

// Custom targets
Expand Down Expand Up @@ -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:
Expand All @@ -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 &&
Expand Down Expand Up @@ -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(
Expand Down