1 |
|
2 |
|
3 | import type {
|
4 | Engines,
|
5 | File,
|
6 | FilePath,
|
7 | PackageJSON,
|
8 | PackageTargetDescriptor,
|
9 | TargetDescriptor,
|
10 | } from '@parcel/types';
|
11 | import type {FileSystem} from '@parcel/fs';
|
12 | import type {ParcelOptions, Target} from './types';
|
13 | import type {Diagnostic} from '@parcel/diagnostic';
|
14 |
|
15 | import ThrowableDiagnostic, {
|
16 | generateJSONCodeHighlights,
|
17 | getJSONSourceLocation,
|
18 | } from '@parcel/diagnostic';
|
19 | import {loadConfig, validateSchema} from '@parcel/utils';
|
20 | import {createEnvironment} from './Environment';
|
21 | import path from 'path';
|
22 | import browserslist from 'browserslist';
|
23 | import jsonMap from 'json-source-map';
|
24 | import invariant from 'assert';
|
25 | import {
|
26 | COMMON_TARGET_DESCRIPTOR_SCHEMA,
|
27 | DESCRIPTOR_SCHEMA,
|
28 | ENGINES_SCHEMA,
|
29 | } from './TargetDescriptor.schema';
|
30 |
|
31 | export type TargetResolveResult = {|
|
32 | targets: Array<Target>,
|
33 | files: Array<File>,
|
34 | |};
|
35 |
|
36 | const 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 |
|
46 | const DEFAULT_PRODUCTION_ENGINES = {
|
47 | browsers: ['>= 0.25%'],
|
48 | node: '8',
|
49 | };
|
50 |
|
51 | const DEFAULT_DIST_DIRNAME = 'dist';
|
52 | const COMMON_TARGETS = ['main', 'module', 'browser', 'types'];
|
53 |
|
54 | export 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 |
|
82 |
|
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 |
|
99 |
|
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 |
|
146 |
|
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 |
|
166 |
|
167 | if (this.options.serve) {
|
168 |
|
169 |
|
170 | let serveOptions = this.options.serve;
|
171 | targets = [
|
172 | {
|
173 | name: 'default',
|
174 |
|
175 |
|
176 |
|
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 |
|
248 |
|
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 |
|
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 |
|
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 | :
|
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 |
|
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 |
|
442 | function 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 |
|
463 | return engines;
|
464 | }
|
465 | }
|
466 |
|
467 | function 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 |
|
484 | return descriptor;
|
485 | }
|
486 |
|
487 | function 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 |
|
504 | return descriptor;
|
505 | }
|
506 |
|
507 | function assertNoDuplicateTargets(targets, pkgFilePath, pkgContents) {
|
508 |
|
509 |
|
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 |
|
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 | }
|