Skip to content

Commit

Permalink
Improve library targets (#6517)
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett committed Jun 29, 2021
1 parent 56eb17d commit 187ddfd
Show file tree
Hide file tree
Showing 22 changed files with 637 additions and 32 deletions.
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
Loading

0 comments on commit 187ddfd

Please sign in to comment.