UNPKG

16.8 kBJavaScriptView Raw
1// @flow
2
3import type {
4 Engines,
5 File,
6 FilePath,
7 PackageJSON,
8 PackageTargetDescriptor,
9 TargetDescriptor,
10} from '@parcel/types';
11import type {FileSystem} from '@parcel/fs';
12import type {ParcelOptions, Target} from './types';
13import type {Diagnostic} from '@parcel/diagnostic';
14
15import ThrowableDiagnostic, {
16 generateJSONCodeHighlights,
17 getJSONSourceLocation,
18} from '@parcel/diagnostic';
19import {loadConfig, validateSchema} from '@parcel/utils';
20import {createEnvironment} from './Environment';
21import path from 'path';
22import browserslist from 'browserslist';
23import jsonMap from 'json-source-map';
24import invariant from 'assert';
25import {
26 COMMON_TARGET_DESCRIPTOR_SCHEMA,
27 DESCRIPTOR_SCHEMA,
28 ENGINES_SCHEMA,
29} from './TargetDescriptor.schema';
30
31export type TargetResolveResult = {|
32 targets: Array<Target>,
33 files: Array<File>,
34|};
35
36const DEFAULT_DEVELOPMENT_ENGINES = {
37 node: 'current',
38 browsers: [
39 'last 1 Chrome version',
40 'last 1 Safari version',
41 'last 1 Firefox version',
42 'last 1 Edge version',
43 ],
44};
45
46const DEFAULT_PRODUCTION_ENGINES = {
47 browsers: ['>= 0.25%'],
48 node: '8',
49};
50
51const DEFAULT_DIST_DIRNAME = 'dist';
52const COMMON_TARGETS = ['main', 'module', 'browser', 'types'];
53
54export default class TargetResolver {
55 fs: FileSystem;
56 options: ParcelOptions;
57
58 constructor(options: ParcelOptions) {
59 this.fs = options.inputFS;
60 this.options = options;
61 }
62
63 async resolve(
64 rootDir?: FilePath = this.options.projectRoot,
65 ): Promise<TargetResolveResult> {
66 let optionTargets = this.options.targets;
67
68 let targets: Array<Target>;
69 let files: Array<File> = [];
70 if (optionTargets) {
71 if (Array.isArray(optionTargets)) {
72 if (optionTargets.length === 0) {
73 throw new ThrowableDiagnostic({
74 diagnostic: {
75 message: `Targets option is an empty array`,
76 origin: '@parcel/core',
77 },
78 });
79 }
80
81 // If an array of strings is passed, it's a filter on the resolved package
82 // targets. Load them, and find the matching targets.
83 let packageTargets = await this.resolvePackageTargets(rootDir);
84 targets = optionTargets.map(target => {
85 let matchingTarget = packageTargets.targets.get(target);
86 if (!matchingTarget) {
87 throw new ThrowableDiagnostic({
88 diagnostic: {
89 message: `Could not find target with name "${target}"`,
90 origin: '@parcel/core',
91 },
92 });
93 }
94 return matchingTarget;
95 });
96 files = packageTargets.files;
97 } else {
98 // Otherwise, it's an object map of target descriptors (similar to those
99 // in package.json). Adapt them to native targets.
100 targets = Object.entries(optionTargets).map(([name, _descriptor]) => {
101 let {distDir, ...descriptor} = parseDescriptor(
102 name,
103 _descriptor,
104 null,
105 {targets: optionTargets},
106 );
107 if (!distDir) {
108 let optionTargetsString = JSON.stringify(optionTargets, null, '\t');
109 throw new ThrowableDiagnostic({
110 diagnostic: {
111 message: `Missing distDir for target "${name}"`,
112 origin: '@parcel/core',
113 codeFrame: {
114 code: optionTargetsString,
115 codeHighlights: generateJSONCodeHighlights(
116 optionTargetsString,
117 [
118 {
119 key: `/${name}`,
120 type: 'value',
121 },
122 ],
123 ),
124 },
125 },
126 });
127 }
128 return {
129 name,
130 distDir: path.resolve(this.fs.cwd(), distDir),
131 publicUrl: descriptor.publicUrl,
132 env: createEnvironment({
133 engines: descriptor.engines,
134 context: descriptor.context,
135 isLibrary: descriptor.isLibrary,
136 includeNodeModules: descriptor.includeNodeModules,
137 outputFormat: descriptor.outputFormat,
138 }),
139 sourceMap: descriptor.sourceMap,
140 };
141 });
142 }
143
144 if (this.options.serve) {
145 // In serve mode, we only support a single browser target. If the user
146 // provided more than one, or the matching target is not a browser, throw.
147 if (targets.length > 1) {
148 throw new ThrowableDiagnostic({
149 diagnostic: {
150 message: `More than one target is not supported in serve mode`,
151 origin: '@parcel/core',
152 },
153 });
154 }
155 if (targets[0].env.context !== 'browser') {
156 throw new ThrowableDiagnostic({
157 diagnostic: {
158 message: `Only browser targets are supported in serve mode`,
159 origin: '@parcel/core',
160 },
161 });
162 }
163 }
164 } else {
165 // Explicit targets were not provided. Either use a modern target for server
166 // mode, or simply use the package.json targets.
167 if (this.options.serve) {
168 // In serve mode, we only support a single browser target. Since the user
169 // hasn't specified a target, use one targeting modern browsers for development
170 let serveOptions = this.options.serve;
171 targets = [
172 {
173 name: 'default',
174 // For serve, write the `dist` to inside the parcel cache, which is
175 // temporary, likely in a .gitignore or similar, but still readily
176 // available for introspection by the user if necessary.
177 distDir: path.resolve(this.options.cacheDir, DEFAULT_DIST_DIRNAME),
178 publicUrl: serveOptions.publicUrl ?? '/',
179 env: createEnvironment({
180 context: 'browser',
181 engines: {
182 browsers: DEFAULT_DEVELOPMENT_ENGINES.browsers,
183 },
184 }),
185 },
186 ];
187 } else {
188 let packageTargets = await this.resolvePackageTargets(rootDir);
189 targets = Array.from(packageTargets.targets.values());
190 files = packageTargets.files;
191 }
192 }
193
194 return {targets, files};
195 }
196
197 async resolvePackageTargets(rootDir: FilePath) {
198 let conf = await loadConfig(this.fs, path.join(rootDir, 'index'), [
199 'package.json',
200 ]);
201
202 let pkg;
203 let pkgContents;
204 let pkgFilePath: ?FilePath;
205 let pkgDir: FilePath;
206 let pkgMap;
207 if (conf) {
208 pkg = (conf.config: PackageJSON);
209 let pkgFile = conf.files[0];
210 if (pkgFile == null) {
211 throw new ThrowableDiagnostic({
212 diagnostic: {
213 message: `Expected package.json file in ${rootDir}`,
214 origin: '@parcel/core',
215 },
216 });
217 }
218 pkgFilePath = pkgFile.filePath;
219 pkgDir = path.dirname(pkgFilePath);
220 pkgContents = await this.fs.readFile(pkgFilePath, 'utf8');
221 pkgMap = jsonMap.parse(pkgContents.replace(/\t/g, ' '));
222 } else {
223 pkg = {};
224 pkgDir = this.fs.cwd();
225 }
226
227 let pkgTargets = pkg.targets || {};
228 let pkgEngines: Engines =
229 parseEngines(
230 pkg.engines,
231 pkgFilePath,
232 pkgContents,
233 '/engines',
234 'Invalid engines in package.json',
235 ) || {};
236 if (!pkgEngines.browsers) {
237 let browserslistBrowsers = browserslist.loadConfig({path: rootDir});
238 if (browserslistBrowsers) {
239 pkgEngines.browsers = browserslistBrowsers;
240 }
241 }
242
243 let targets: Map<string, Target> = new Map();
244 let node = pkgEngines.node;
245 let browsers = pkgEngines.browsers;
246
247 // If there is a separate `browser` target, or an `engines.node` field but no browser targets, then
248 // the `main` and `module` targets refer to node, otherwise browser.
249 let mainContext =
250 pkg.browser || pkgTargets.browser || (node && !browsers)
251 ? 'node'
252 : 'browser';
253 let moduleContext =
254 pkg.browser || pkgTargets.browser ? 'browser' : mainContext;
255
256 let defaultEngines =
257 this.options.defaultEngines ??
258 (this.options.mode === 'production'
259 ? DEFAULT_PRODUCTION_ENGINES
260 : DEFAULT_DEVELOPMENT_ENGINES);
261 let context = browsers || !node ? 'browser' : 'node';
262 if (context === 'browser' && pkgEngines.browsers == null) {
263 pkgEngines.browsers = defaultEngines.browsers;
264 } else if (context === 'node' && pkgEngines.node == null) {
265 pkgEngines.node = defaultEngines.node;
266 }
267
268 for (let targetName of COMMON_TARGETS) {
269 let targetDist;
270 let pointer;
271 if (
272 targetName === 'browser' &&
273 pkg[targetName] != null &&
274 typeof pkg[targetName] === 'object'
275 ) {
276 // The `browser` field can be a file path or an alias map.
277 targetDist = pkg[targetName][pkg.name];
278 pointer = `/${targetName}/${pkg.name}`;
279 } else {
280 targetDist = pkg[targetName];
281 pointer = `/${targetName}`;
282 }
283
284 if (typeof targetDist === 'string' || pkgTargets[targetName]) {
285 let distDir;
286 let distEntry;
287 let loc;
288
289 invariant(typeof pkgFilePath === 'string');
290 invariant(pkgMap != null);
291
292 let _descriptor: mixed = pkgTargets[targetName] ?? {};
293 if (typeof targetDist === 'string') {
294 distDir = path.resolve(pkgDir, path.dirname(targetDist));
295 distEntry = path.basename(targetDist);
296 loc = {
297 filePath: pkgFilePath,
298 ...getJSONSourceLocation(pkgMap.pointers[pointer], 'value'),
299 };
300 } else {
301 distDir = path.resolve(pkgDir, DEFAULT_DIST_DIRNAME, targetName);
302 }
303
304 let descriptor = parseCommonTargetDescriptor(
305 targetName,
306 _descriptor,
307 pkgFilePath,
308 pkgContents,
309 );
310 if (!descriptor) continue;
311
312 let isLibrary =
313 typeof distEntry === 'string'
314 ? path.extname(distEntry) === '.js'
315 : false;
316 targets.set(targetName, {
317 name: targetName,
318 distDir,
319 distEntry,
320 publicUrl: descriptor.publicUrl ?? '/',
321 env: createEnvironment({
322 engines: descriptor.engines ?? pkgEngines,
323 context:
324 descriptor.context ??
325 (targetName === 'browser'
326 ? 'browser'
327 : targetName === 'module'
328 ? moduleContext
329 : mainContext),
330 includeNodeModules: descriptor.includeNodeModules ?? !isLibrary,
331 outputFormat:
332 descriptor.outputFormat ??
333 (isLibrary
334 ? targetName === 'module'
335 ? 'esmodule'
336 : 'commonjs'
337 : 'global'),
338 isLibrary: isLibrary,
339 }),
340 sourceMap: descriptor.sourceMap,
341 loc,
342 });
343 }
344 }
345
346 // Custom targets
347 for (let targetName in pkgTargets) {
348 if (COMMON_TARGETS.includes(targetName)) {
349 continue;
350 }
351
352 let distPath: mixed = pkg[targetName];
353 let distDir;
354 let distEntry;
355 let loc;
356 if (distPath == null) {
357 distDir = path.resolve(pkgDir, DEFAULT_DIST_DIRNAME, targetName);
358 } else {
359 if (typeof distPath !== 'string') {
360 let contents: string =
361 typeof pkgContents === 'string'
362 ? pkgContents
363 : // $FlowFixMe
364 JSON.stringify(pkgContents, null, '\t');
365 throw new ThrowableDiagnostic({
366 diagnostic: {
367 message: `Invalid distPath for target "${targetName}"`,
368 origin: '@parcel/core',
369 language: 'json',
370 filePath: pkgFilePath || undefined,
371 codeFrame: {
372 code: contents,
373 codeHighlights: generateJSONCodeHighlights(contents, [
374 {
375 key: `/${targetName}`,
376 type: 'value',
377 message: 'Expected type string',
378 },
379 ]),
380 },
381 },
382 });
383 }
384 distDir = path.resolve(pkgDir, path.dirname(distPath));
385 distEntry = path.basename(distPath);
386
387 invariant(typeof pkgFilePath === 'string');
388 invariant(pkgMap != null);
389 loc = {
390 filePath: pkgFilePath,
391 ...getJSONSourceLocation(pkgMap.pointers[`/${targetName}`], 'value'),
392 };
393 }
394
395 if (targetName in pkgTargets) {
396 let descriptor = parseDescriptor(
397 targetName,
398 pkgTargets[targetName],
399 pkgFilePath,
400 pkgContents,
401 );
402 targets.set(targetName, {
403 name: targetName,
404 distDir,
405 distEntry,
406 publicUrl: descriptor.publicUrl ?? '/',
407 env: createEnvironment({
408 engines: descriptor.engines ?? pkgEngines,
409 context: descriptor.context,
410 includeNodeModules: descriptor.includeNodeModules,
411 outputFormat: descriptor.outputFormat,
412 isLibrary: descriptor.isLibrary,
413 }),
414 sourceMap: descriptor.sourceMap,
415 loc,
416 });
417 }
418 }
419
420 // If no explicit targets were defined, add a default.
421 if (targets.size === 0) {
422 targets.set('default', {
423 name: 'default',
424 distDir: path.resolve(this.fs.cwd(), DEFAULT_DIST_DIRNAME),
425 publicUrl: '/',
426 env: createEnvironment({
427 engines: pkgEngines,
428 context,
429 }),
430 });
431 }
432
433 assertNoDuplicateTargets(targets, pkgFilePath, pkgContents);
434
435 return {
436 targets,
437 files: conf ? conf.files : [],
438 };
439 }
440}
441
442function parseEngines(
443 engines: mixed,
444 pkgPath: ?FilePath,
445 pkgContents: string | mixed,
446 prependKey: string,
447 message: string,
448): Engines | typeof undefined {
449 if (engines === undefined) {
450 return engines;
451 } else {
452 validateSchema.diagnostic(
453 ENGINES_SCHEMA,
454 engines,
455 pkgPath,
456 pkgContents,
457 '@parcel/core',
458 prependKey,
459 message,
460 );
461
462 // $FlowFixMe we just verified this
463 return engines;
464 }
465}
466
467function parseDescriptor(
468 targetName: string,
469 descriptor: mixed,
470 pkgPath: ?FilePath,
471 pkgContents: string | mixed,
472): TargetDescriptor | PackageTargetDescriptor {
473 validateSchema.diagnostic(
474 DESCRIPTOR_SCHEMA,
475 descriptor,
476 pkgPath,
477 pkgContents,
478 '@parcel/core',
479 `/targets/${targetName}`,
480 `Invalid target descriptor for target "${targetName}"`,
481 );
482
483 // $FlowFixMe we just verified this
484 return descriptor;
485}
486
487function parseCommonTargetDescriptor(
488 targetName: string,
489 descriptor: mixed,
490 pkgPath: ?FilePath,
491 pkgContents: string | mixed,
492): TargetDescriptor | PackageTargetDescriptor | false {
493 validateSchema.diagnostic(
494 COMMON_TARGET_DESCRIPTOR_SCHEMA,
495 descriptor,
496 pkgPath,
497 pkgContents,
498 '@parcel/core',
499 `/targets/${targetName}`,
500 `Invalid target descriptor for target "${targetName}"`,
501 );
502
503 // $FlowFixMe we just verified this
504 return descriptor;
505}
506
507function assertNoDuplicateTargets(targets, pkgFilePath, pkgContents) {
508 // Detect duplicate targets by destination path and provide a nice error.
509 // Without this, an assertion is thrown much later after naming the bundles and finding duplicates.
510 let targetsByPath: Map<string, Array<string>> = new Map();
511 for (let target of targets.values()) {
512 if (target.distEntry) {
513 let distPath = path.join(target.distDir, target.distEntry);
514 if (!targetsByPath.has(distPath)) {
515 targetsByPath.set(distPath, []);
516 }
517 targetsByPath.get(distPath)?.push(target.name);
518 }
519 }
520
521 let diagnostics: Array<Diagnostic> = [];
522 for (let [targetPath, targetNames] of targetsByPath) {
523 if (targetNames.length > 1 && pkgContents && pkgFilePath) {
524 diagnostics.push({
525 message: `Multiple targets have the same destination path "${path.relative(
526 path.dirname(pkgFilePath),
527 targetPath,
528 )}"`,
529 origin: '@parcel/core',
530 language: 'json',
531 filePath: pkgFilePath || undefined,
532 codeFrame: {
533 code: pkgContents,
534 codeHighlights: generateJSONCodeHighlights(
535 pkgContents,
536 targetNames.map(t => ({
537 key: `/${t}`,
538 type: 'value',
539 })),
540 ),
541 },
542 });
543 }
544 }
545
546 if (diagnostics.length > 0) {
547 // Only add hints to the last diagnostic so it isn't duplicated on each one
548 diagnostics[diagnostics.length - 1].hints = [
549 'Try removing the duplicate targets, or changing the destination paths.',
550 ];
551
552 throw new ThrowableDiagnostic({
553 diagnostic: diagnostics,
554 });
555 }
556}