diff --git a/.flowconfig b/.flowconfig index 5c8a52c6058..0f356aceae5 100644 --- a/.flowconfig +++ b/.flowconfig @@ -36,4 +36,4 @@ untyped-import untyped-type-import [version] -0.151.0 +0.153.0 diff --git a/Cargo.lock b/Cargo.lock index 4843877b2a1..c81c09b5edc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,6 +21,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anyhow" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61" + [[package]] name = "arrayvec" version = "0.5.2" @@ -117,9 +123,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cpufeatures" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed00c67cb5d0a7d64a44f6ad2668db7e7530311dd53ea79bcd4fb022c64911c8" +checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" dependencies = [ "libc", ] @@ -280,9 +286,9 @@ checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" [[package]] name = "hermit-abi" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] @@ -415,9 +421,9 @@ checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" [[package]] name = "libmimalloc-sys" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2396cf99d2f58611cd69f0efeee4af3d2e2c7b61bed433515029163aa567e65c" +checksum = "1d1b8479c593dba88c2741fc50b92e13dbabbbe0bd504d979f244ccc1a5b1c01" dependencies = [ "cc", ] @@ -445,9 +451,9 @@ checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" [[package]] name = "mimalloc" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7c6b11afd1e5e689ac96b6d18b1fc763398fe3d7eed99e8773426bc2033dfb" +checksum = "fb74897ce508e6c49156fd1476fc5922cbc6e75183c65e399c765a09122e5130" dependencies = [ "libmimalloc-sys", ] @@ -493,6 +499,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "normpath" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a9da8c9922c35a1033d76f7272dfc2e7ee20392083d75aeea6ced23c6266578" +dependencies = [ + "winapi", +] + [[package]] name = "num-bigint" version = "0.2.6" @@ -591,6 +606,8 @@ dependencies = [ "data-encoding", "dunce", "indoc", + "path-slash", + "pathdiff", "serde", "serde_bytes", "sha-1", @@ -623,6 +640,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "path-slash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cacbb3c4ff353b534a67fb8d7524d00229da4cb1dc8c79f4db96e375ab5b619" + +[[package]] +name = "pathdiff" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877630b3de15c0b64cc52f659345724fbf6bdad9bd9566699fc53688f3c34a34" + [[package]] name = "percent-encoding" version = "2.1.0" @@ -1023,9 +1052,9 @@ dependencies = [ [[package]] name = "swc_common" -version = "0.10.20" +version = "0.10.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d02578ed17d73cb0233cf27aa6c59220a1910f5f335196dced75c9b2be2900fb" +checksum = "526ac4386aca6a792d75bb3bd5cb72eb170e2029f4ce0f6a9d280923da4b0ce8" dependencies = [ "ast_node", "atty", @@ -1049,9 +1078,9 @@ dependencies = [ [[package]] name = "swc_ecma_ast" -version = "0.46.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e64e7a5db16d1a3887291e149bfe0603cd8886c64ca0e210519a879eada926" +checksum = "ab98b7f6777222431bdf193bbfc6046af53cc105c8702390a9109d1a4511329b" dependencies = [ "is-macro", "num-bigint", @@ -1063,9 +1092,9 @@ dependencies = [ [[package]] name = "swc_ecma_codegen" -version = "0.58.0" +version = "0.60.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f463acd9d19f52b051dfd99179c9ee6d75298e3ed3064bc4e03996f88c656d42" +checksum = "0e84b5477973503f94c6a82f02bc6008c148dc3dd5b07bb81fee6f5f93e3a190" dependencies = [ "bitflags", "num-bigint", @@ -1090,11 +1119,27 @@ dependencies = [ "syn", ] +[[package]] +name = "swc_ecma_loader" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d234e6348de8d954334700f65a35485363de082fd58671c07f060f1fa499f59c" +dependencies = [ + "anyhow", + "normpath", + "serde", + "serde_json", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_visit", +] + [[package]] name = "swc_ecma_parser" -version = "0.60.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5118746b81e7b0cab2d9920383a384567647c623f8a7b73aa87a553fadc572e5" +checksum = "82ed9b5648252851ae2cfc066717fcae9c5561be74f496eda64afc791d9b17ed" dependencies = [ "either", "enum_kind", @@ -1113,9 +1158,9 @@ dependencies = [ [[package]] name = "swc_ecma_preset_env" -version = "0.23.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63920a34beb3cfd32153742fec900ed42f4a4a4543346135e86a9be490ed9a99" +checksum = "538902691c3a274a6e80ce7860c91b96977ebd977e1ca7fba77057e425267951" dependencies = [ "dashmap", "fxhash", @@ -1137,9 +1182,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms" -version = "0.53.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d112414d2ab2e46ca0a8bf2e09febe8bf2ecc9c4ca082b0a40894de16a59e261" +checksum = "7fd060899f0cb2994ad9d2c04475e30864549e80dde35a526acdf3a79ec8d722" dependencies = [ "swc_atoms", "swc_common", @@ -1159,9 +1204,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_base" -version = "0.18.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7320ce4841ee0f81341bd2534fb82389c9b0e4c7caad5a0b7e4b83fcc09e5da9" +checksum = "311b66839a94ea620382ecadb46c001132c21ad15b13f0e04ffe3ad115a8ce78" dependencies = [ "fxhash", "once_cell", @@ -1178,9 +1223,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_classes" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2d01cc813f01bf4a971df476d3e1def0008aa3d8a93e41c4099517167a1554c" +checksum = "f9f7417ff08dd6da5bf7ab96e96bb6ae43cdf484235ded9855d5260011cc0186" dependencies = [ "swc_atoms", "swc_common", @@ -1192,9 +1237,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_compat" -version = "0.20.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05a5c7c4d85cae6803d063d2e1bc41464c310e0dbc4f579824c0277fecd96f57" +checksum = "f464846be30234400767c1f7785bdfd2d27fef6f3cfb9f4e24a2044c9e8e76d2" dependencies = [ "arrayvec", "fxhash", @@ -1229,17 +1274,20 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_module" -version = "0.20.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec5cba3220efe5897cca93560ac24ff84c7badfb0df12f6b7497c1569af9eb88" +checksum = "22c79be4775f2f6f166c7901706e5cc64cd837e6a26b8f0d7ced224321a39dc2" dependencies = [ "Inflector", + "anyhow", "fxhash", "indexmap", + "pathdiff", "serde", "swc_atoms", "swc_common", "swc_ecma_ast", + "swc_ecma_loader", "swc_ecma_parser", "swc_ecma_transforms_base", "swc_ecma_utils", @@ -1248,9 +1296,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_optimization" -version = "0.23.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143301f9738d3bd909ac83c6fdf845bce1471faf8c91d986af8bf91f118f976f" +checksum = "422f1a381ee1782fa5b3d883fb01239e9bca26515932b93e4fa3e956275c684f" dependencies = [ "dashmap", "fxhash", @@ -1270,9 +1318,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_proposal" -version = "0.20.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f74a1fd2b73e7cc36cba6a178721e194363f9cd0b1be56177431bb4ff8f8be8" +checksum = "90a18d71bee10a061894ff92d36fcc54e43197c92f70cdb98309021bff282290" dependencies = [ "either", "fxhash", @@ -1290,9 +1338,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_react" -version = "0.21.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d165dc9ce3308168b0496e2717cbfbe2eff6b82cd88d0988ad1176ca53e8d22" +checksum = "39e2bf91bc9166722159d919cff5dc0276fd404031c53a0f5c6716fdcdcb3a0a" dependencies = [ "base64 0.13.0", "dashmap", @@ -1313,9 +1361,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_typescript" -version = "0.22.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff5f628a95bb61902da6956a89bd236c0106e8f283834bce3a842c9aa45db0e8" +checksum = "18d642e21c1088a040e680e81527a7d7528c3703070d15f93cc56cce7c8591f2" dependencies = [ "fxhash", "serde", @@ -1330,9 +1378,9 @@ dependencies = [ [[package]] name = "swc_ecma_utils" -version = "0.37.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4aead4054e4789500228dff53c4f776ea5ab2aae705955497520cb1e3a40940" +checksum = "db19d40a76ba6c01ebbbd73981645dd2552cdbfc14605481c7a9b7f6f26bd20e" dependencies = [ "once_cell", "scoped-tls", @@ -1345,9 +1393,9 @@ dependencies = [ [[package]] name = "swc_ecma_visit" -version = "0.32.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d52f36a7e74c2895c97c1d86e61fd74b50027ec838b5c6d0b44485f6cbde868" +checksum = "23ad83badbeda1123290a73ca6254158ed027f72f1b593f96acddcaed5d49c6a" dependencies = [ "num-bigint", "swc_atoms", @@ -1358,9 +1406,9 @@ dependencies = [ [[package]] name = "swc_ecmascript" -version = "0.39.0" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3366daf79ec1a0820dc307fd7cf50536106e2fab11cdeffdf84c0b2a9c9204bb" +checksum = "c55e1c5047fe3f5a6c301a312c278ac018789ba4a6e40e556612e82defda422c" dependencies = [ "swc_ecma_ast", "swc_ecma_codegen", diff --git a/flow-libs/commander.js.flow b/flow-libs/commander.js.flow index b31fa126028..0515f1c6afc 100644 --- a/flow-libs/commander.js.flow +++ b/flow-libs/commander.js.flow @@ -282,10 +282,7 @@ declare module 'commander' { // manually added command( nameAndArgs: string, opts?: commander$CommandOptions, - ): $Call< - ((...args: any[]) => R) => R, - $PropertyType, - >; + ): commander$Command; /** * Define a command, implemented in a separate executable file. diff --git a/flow-typed/npm/typescript_v3.3.x.js b/flow-typed/npm/typescript_v3.3.x.js index 678ca005a52..d32a41cac8c 100644 --- a/flow-typed/npm/typescript_v3.3.x.js +++ b/flow-typed/npm/typescript_v3.3.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: a73d5be820ce9376397c9110a4a02d9e -// flow-typed version: c6154227d1/typescript_v3.3.x/flow_>=v0.104.x +// flow-typed signature: 8ef93f972fa1514991dd699880b244bf +// flow-typed version: 80b11313ee/typescript_v3.3.x/flow_>=v0.104.x declare module "typescript" { declare var versionMajorMinor: "3.3"; // "3.3"; @@ -3300,7 +3300,7 @@ declare module "typescript" { ... }; - declare type ParseConfigHost = { + declare interface ParseConfigHost { useCaseSensitiveFileNames: boolean, readDirectory( rootDir: string, @@ -3311,9 +3311,8 @@ declare module "typescript" { ): $ReadOnlyArray, fileExists(path: string): boolean, readFile(path: string): string | void, - trace?: (s: string) => void, - ... - }; + trace?: (s: string) => void + } declare type ResolvedConfigFileName = string & { _isResolvedConfigFileName: empty, ... }; declare type WriteFileCallback = ( @@ -4903,11 +4902,10 @@ declare module "typescript" { ... }; - declare type GetEffectiveTypeRootsHost = { - directoryExists?: (directoryName: string) => boolean, - getCurrentDirectory?: () => string, - ... - }; + declare interface GetEffectiveTypeRootsHost { + +directoryExists?: (directoryName: string) => boolean, + +getCurrentDirectory?: () => string + } declare type TextSpan = { start: number, @@ -8435,59 +8433,57 @@ declare module "typescript" { ... }; - declare type LanguageServiceHost = { - ...$Exact, + declare interface LanguageServiceHost extends GetEffectiveTypeRootsHost { getCompilationSettings(): CompilerOptions, - getNewLine?: () => string, - getProjectVersion?: () => string, + +getNewLine?: () => string, + +getProjectVersion?: () => string, getScriptFileNames(): string[], - getScriptKind?: (fileName: string) => $Values, + +getScriptKind?: (fileName: string) => $Values, getScriptVersion(fileName: string): string, getScriptSnapshot(fileName: string): IScriptSnapshot | void, - getProjectReferences?: () => $ReadOnlyArray | void, - getLocalizedDiagnosticMessages?: () => any, - getCancellationToken?: () => HostCancellationToken, + +getProjectReferences?: () => $ReadOnlyArray | void, + +getLocalizedDiagnosticMessages?: () => any, + +getCancellationToken?: () => HostCancellationToken, getCurrentDirectory(): string, getDefaultLibFileName(options: CompilerOptions): string, - log?: (s: string) => void, - trace?: (s: string) => void, - error?: (s: string) => void, - useCaseSensitiveFileNames?: () => boolean, - readDirectory?: ( + +log?: (s: string) => void, + +trace?: (s: string) => void, + +error?: (s: string) => void, + +useCaseSensitiveFileNames?: () => boolean, + +readDirectory?: ( path: string, extensions?: $ReadOnlyArray, exclude?: $ReadOnlyArray, include?: $ReadOnlyArray, depth?: number ) => string[], - readFile?: (path: string, encoding?: string) => string | void, - realpath?: (path: string) => string, - fileExists?: (path: string) => boolean, - getTypeRootsVersion?: () => number, - resolveModuleNames?: ( + +readFile?: (path: string, encoding?: string) => string | void, + +realpath?: (path: string) => string, + +fileExists?: (path: string) => boolean, + +getTypeRootsVersion?: () => number, + +resolveModuleNames?: ( moduleNames: string[], containingFile: string, reusedNames?: string[], redirectedReference?: ResolvedProjectReference ) => (ResolvedModule | void)[], - getResolvedModuleWithFailedLookupLocationsFromCache?: ( + +getResolvedModuleWithFailedLookupLocationsFromCache?: ( modulename: string, containingFile: string ) => ResolvedModuleWithFailedLookupLocations | void, - resolveTypeReferenceDirectives?: ( + +resolveTypeReferenceDirectives?: ( typeDirectiveNames: string[], containingFile: string, redirectedReference?: ResolvedProjectReference ) => (ResolvedTypeReferenceDirective | void)[], - getDirectories?: (directoryName: string) => string[], - getCustomTransformers?: () => CustomTransformers | void, - isKnownTypesPackageName?: (name: string) => boolean, - installPackage?: ( + +getDirectories?: (directoryName: string) => string[], + +getCustomTransformers?: () => CustomTransformers | void, + +isKnownTypesPackageName?: (name: string) => boolean, + +installPackage?: ( options: InstallPackageOptions ) => Promise, - writeFile?: (fileName: string, content: string) => void, - ... - }; + +writeFile?: (fileName: string, content: string) => void + } declare type WithMetadata = T & { metadata?: mixed, ... }; declare type LanguageService = { diff --git a/package.json b/package.json index a84582e7a66..d38ce40c38f 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ ], "scripts": { "build": "cross-env NODE_ENV=production PARCEL_BUILD_ENV=production gulp", + "build-ts": "lerna run build-ts && lerna run check-ts", "build-native": "node scripts/build-native.js", "build-native-release": "node scripts/build-native.js --release", "clean-test": "rimraf packages/core/integration-tests/.parcel-cache && rimraf packages/core/integration-tests/dist", @@ -22,7 +23,7 @@ "check": "flow check", "lint": "eslint . && prettier \"./packages/*/*/{src,bin,test}/**/*.{js,json,md}\" --list-different && cargo fmt --all -- --check", "lint:readme": "node scripts/validate-readme-toc.js", - "prepublishOnly": "yarn build && node scripts/update-config-dependencies.js", + "prepublishOnly": "yarn build && yarn build-ts && node scripts/update-config-dependencies.js", "test:unit": "cross-env NODE_ENV=test mocha", "test:integration": "yarn workspace @parcel/integration-tests test", "test:integration-ci": "yarn workspace @parcel/integration-tests test-ci", @@ -36,11 +37,13 @@ }, "devDependencies": { "@babel/core": "^7.12.0", + "@khanacademy/flow-to-ts": "^0.5.2", "@napi-rs/cli": "1.0.4", + "@types/node": "^15.12.4", "cross-env": "^7.0.0", "doctoc": "^1.4.0", "eslint": "^7.20.0", - "flow-bin": "0.151.0", + "flow-bin": "0.153.0", "glob": "^7.1.6", "gulp": "^4.0.2", "gulp-babel": "^8.0.0", diff --git a/packages/bundlers/default/src/DefaultBundler.js b/packages/bundlers/default/src/DefaultBundler.js index 8f5a753c49c..16c706633b2 100644 --- a/packages/bundlers/default/src/DefaultBundler.js +++ b/packages/bundlers/default/src/DefaultBundler.js @@ -108,11 +108,12 @@ export default (new Bundler({ for (let asset of assets) { let bundle = bundleGraph.createBundle({ entryAsset: asset, - isEntry: + needsStableName: + dependency.bundleBehavior === 'inline' || asset.bundleBehavior === 'inline' ? false : dependency.isEntry || dependency.needsStableName, - isInline: asset.bundleBehavior === 'inline', + bundleBehavior: dependency.bundleBehavior ?? asset.bundleBehavior, target: bundleGroup.target, }); bundleByType.set(bundle.type, bundle); @@ -165,13 +166,14 @@ export default (new Bundler({ env: asset.env, type: asset.type, target: bundleGroup.target, - isEntry: + needsStableName: asset.bundleBehavior === 'inline' || + dependency.bundleBehavior === 'inline' || (dependency.priority === 'parallel' && !dependency.needsStableName) ? false - : parentBundle.isEntry, - isInline: asset.bundleBehavior === 'inline', + : parentBundle.needsStableName, + bundleBehavior: dependency.bundleBehavior ?? asset.bundleBehavior, isSplittable: asset.isBundleSplittable ?? true, pipeline: asset.pipeline, }); @@ -209,7 +211,12 @@ export default (new Bundler({ // Step 2: Remove asset graphs that begin with entries to other bundles. bundleGraph.traverseBundles(bundle => { - if (bundle.isInline || !bundle.isSplittable || bundle.env.isIsolated()) { + if ( + bundle.bundleBehavior === 'inline' || + bundle.bundleBehavior === 'isolated' || + !bundle.isSplittable || + bundle.env.isIsolated() + ) { return; } @@ -231,8 +238,10 @@ export default (new Bundler({ // Don't add to BundleGroups for entry bundles, as that would require // another entry bundle depending on these conditions, making it difficult // to predict and reference. - !containingBundle.isEntry && - !containingBundle.isInline && + // TODO: reconsider this. This is only true for the global output format. + !containingBundle.needsStableName && + containingBundle.bundleBehavior !== 'inline' && + containingBundle.bundleBehavior !== 'isolated' && containingBundle.isSplittable, ); @@ -245,7 +254,8 @@ export default (new Bundler({ group => bundleGraph .getBundlesInBundleGroup(group) - .filter(b => !b.isInline).length < config.maxParallelRequests, + .filter(b => b.bundleBehavior !== 'inline').length < + config.maxParallelRequests, ) ) { bundleGraph.createBundleReference(candidate, bundle); @@ -340,11 +350,13 @@ export default (new Bundler({ // Don't create shared bundles from entry bundles, as that would require // another entry bundle depending on these conditions, making it difficult // to predict and reference. + // TODO: reconsider this. This is only true for the global output format. + // This also currently affects other bundles with stable names, e.g. service workers. .filter(b => { let entries = b.getEntryAssets(); return ( - !b.isEntry && + !b.needsStableName && b.isSplittable && entries.every(entry => entry.id !== asset.id) ); @@ -386,31 +398,35 @@ export default (new Bundler({ .sort((a, b) => b.size - a.size); for (let {assets, sourceBundles} of sortedCandidates) { - // Find all bundle groups connected to the original bundles - let bundleGroups = new Set(); + let eligibleSourceBundles = new Set(); for (let bundle of sourceBundles) { - for (let bundleGroup of bundleGraph.getBundleGroupsContainingBundle( - bundle, - )) { - bundleGroups.add(bundleGroup); + // Find all bundle groups connected to the original bundles + let bundleGroups = bundleGraph.getBundleGroupsContainingBundle(bundle); + // Check if all bundle groups are within the parallel request limit + if ( + bundleGroups.every( + group => + bundleGraph + .getBundlesInBundleGroup(group) + .filter(b => b.bundleBehavior !== 'inline').length < + config.maxParallelRequests, + ) + ) { + eligibleSourceBundles.add(bundle); } } - // If all bundle groups have already met the max parallel request limit, then they cannot be split. - if ( - Array.from(bundleGroups).every( - group => - bundleGraph.getBundlesInBundleGroup(group).filter(b => !b.isInline) - .length >= config.maxParallelRequests, - ) - ) { + // Do not create a shared bundle unless there are at least 2 source bundles + if (eligibleSourceBundles.size < 2) { continue; } - let [firstBundle] = [...sourceBundles]; + let [firstBundle] = [...eligibleSourceBundles]; let sharedBundle = bundleGraph.createBundle({ - uniqueKey: hashString([...sourceBundles].map(b => b.id).join(':')), + uniqueKey: hashString( + [...eligibleSourceBundles].map(b => b.id).join(':'), + ), // Allow this bundle to be deduplicated. It shouldn't be further split. // TODO: Reconsider bundle/asset flags. isSplittable: true, @@ -424,20 +440,8 @@ export default (new Bundler({ for (let asset of assets) { bundleGraph.addAssetGraphToBundle(asset, sharedBundle); - for (let bundle of sourceBundles) { - // Remove the asset graph from the bundle if all bundle groups are - // within the parallel request limit and will include the shared bundle. - let bundleGroups = bundleGraph.getBundleGroupsContainingBundle( - bundle, - ); - if ( - bundleGroups.every( - bundleGroup => - bundleGraph - .getBundlesInBundleGroup(bundleGroup) - .filter(b => !b.isInline).length < config.maxParallelRequests, - ) - ) { + for (let bundle of eligibleSourceBundles) { + { bundleGraph.createBundleReference(bundle, sharedBundle); bundleGraph.removeAssetGraphFromBundle(asset, bundle); } diff --git a/packages/configs/default/index.json b/packages/configs/default/index.json index 61e26f590be..3a550dd44f6 100644 --- a/packages/configs/default/index.json +++ b/packages/configs/default/index.json @@ -4,6 +4,10 @@ "types:*.{ts,tsx}": ["@parcel/transformer-typescript-types"], "bundle-text:*": ["...", "@parcel/transformer-inline-string"], "data-url:*": ["...", "@parcel/transformer-inline-string"], + "worklet:*.{js,mjs,jsm,jsx,es6,cjs,ts,tsx}": [ + "@parcel/transformer-worklet", + "..." + ], "*.{js,mjs,jsm,jsx,es6,cjs,ts,tsx}": [ "@parcel/transformer-babel", "@parcel/transformer-js", @@ -34,8 +38,8 @@ "script:*.vue": ["@parcel/transformer-vue"], "style:*.vue": ["@parcel/transformer-vue"], "custom:*.vue": ["@parcel/transformer-vue"], - "url:*.{png,jpg,jpeg,webp}": ["@parcel/transformer-image"], - "url:*": ["@parcel/transformer-raw"] + "*.{png,jpg,jpeg,webp}": ["@parcel/transformer-image"], + "url:*": ["...", "@parcel/transformer-raw"] }, "namers": ["@parcel/namer-default"], "runtimes": [ @@ -46,13 +50,14 @@ "optimizers": { "data-url:*": ["...", "@parcel/optimizer-data-url"], "*.css": ["@parcel/optimizer-cssnano"], - "*.js": ["@parcel/optimizer-terser"], - "*.html": ["@parcel/optimizer-htmlnano"] + "*.html": ["@parcel/optimizer-htmlnano"], + "*.{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/configs/default/package.json b/packages/configs/default/package.json index 4f03c0d0fd6..50b68b3d1c0 100644 --- a/packages/configs/default/package.json +++ b/packages/configs/default/package.json @@ -23,6 +23,7 @@ "@parcel/optimizer-cssnano": "2.0.0-beta.3.1", "@parcel/optimizer-htmlnano": "2.0.0-beta.3.1", "@parcel/optimizer-terser": "2.0.0-beta.3.1", + "@parcel/optimizer-svgo": "2.0.0-beta.3.1", "@parcel/packager-css": "2.0.0-beta.3.1", "@parcel/packager-html": "2.0.0-beta.3.1", "@parcel/packager-js": "2.0.0-beta.3.1", @@ -63,6 +64,7 @@ "@parcel/transformer-typescript-types": "2.0.0-beta.3.1", "@parcel/transformer-vue": "2.0.0-beta.3.1", "@parcel/transformer-webmanifest": "2.0.0-beta.3.1", + "@parcel/transformer-worklet": "2.0.0-beta.3.1", "@parcel/transformer-yaml": "2.0.0-beta.3.1" }, "peerDependencies": { diff --git a/packages/configs/webextension/index.json b/packages/configs/webextension/index.json index c611a871f31..519213b14bf 100644 --- a/packages/configs/webextension/index.json +++ b/packages/configs/webextension/index.json @@ -1,7 +1,8 @@ { "extends": "@parcel/config-default", "transformers": { - "manifest.json": ["@parcel/transformer-webextension"] + "manifest.json": ["@parcel/transformer-webextension"], + "raw:*": ["@parcel/transformer-raw"] }, "packagers": { "manifest.json": "@parcel/packager-raw-url" diff --git a/packages/core/cache/index.d.ts b/packages/core/cache/index.d.ts new file mode 100644 index 00000000000..2f403ca7c16 --- /dev/null +++ b/packages/core/cache/index.d.ts @@ -0,0 +1,10 @@ +import type {FilePath} from '@parcel/types'; + +export type {Cache} from './lib/types'; +export const FSCache: { + new (cacheDir: FilePath): Cache +}; + +export const LMDBCache: { + new (cacheDir: FilePath): Cache +}; diff --git a/packages/core/cache/package.json b/packages/core/cache/package.json index b1ad1cb44f6..cda21b596c9 100644 --- a/packages/core/cache/package.json +++ b/packages/core/cache/package.json @@ -15,13 +15,18 @@ }, "main": "lib/index.js", "source": "src/index.js", + "types": "index.d.ts", "engines": { "node": ">= 12.0.0" }, + "scripts": { + "build-ts": "mkdir -p lib && flow-to-ts src/types.js > lib/types.d.ts", + "check-ts": "tsc --noEmit index.d.ts" + }, "dependencies": { "@parcel/logger": "2.0.0-beta.3.1", "@parcel/utils": "2.0.0-beta.3.1", - "lmdb-store": "^1.5.4" + "lmdb-store": "^1.5.5" }, "peerDependencies": { "@parcel/core": "^2.0.0-alpha.3.1" diff --git a/packages/core/core/index.d.ts b/packages/core/core/index.d.ts new file mode 100644 index 00000000000..b900d395030 --- /dev/null +++ b/packages/core/core/index.d.ts @@ -0,0 +1,13 @@ +import type {InitialParcelOptions, BuildEvent, BuildSuccessEvent, AsyncSubscription} from '@parcel/types'; +import type {FarmOptions} from '@parcel/workers'; +import type WorkerFarm from '@parcel/workers'; + +declare class Parcel { + constructor(options: InitialParcelOptions); + run(): Promise; + watch( + cb?: (err: Error | null | undefined, buildEvent?: BuildEvent) => unknown, + ): Promise +} + +export declare function createWorkerFarm(options?: Partial): WorkerFarm; diff --git a/packages/core/core/package.json b/packages/core/core/package.json index f3d0f5f8c1e..127a452a261 100644 --- a/packages/core/core/package.json +++ b/packages/core/core/package.json @@ -20,7 +20,8 @@ }, "scripts": { "test": "mocha", - "test-ci": "mocha" + "test-ci": "mocha", + "check-ts": "tsc --noEmit index.d.ts" }, "dependencies": { "@parcel/cache": "2.0.0-beta.3.1", @@ -31,7 +32,7 @@ "@parcel/logger": "2.0.0-beta.3.1", "@parcel/package-manager": "2.0.0-beta.3.1", "@parcel/plugin": "2.0.0-beta.3.1", - "@parcel/source-map": "2.0.0-rc.4", + "@parcel/source-map": "2.0.0-rc.5", "@parcel/types": "2.0.0-beta.3.1", "@parcel/utils": "2.0.0-beta.3.1", "@parcel/workers": "2.0.0-beta.3.1", diff --git a/packages/core/core/src/AssetGraph.js b/packages/core/core/src/AssetGraph.js index ee381caa127..048ed8badc0 100644 --- a/packages/core/core/src/AssetGraph.js +++ b/packages/core/core/src/AssetGraph.js @@ -27,9 +27,10 @@ import ContentGraph, { type ContentGraphOpts, } from './ContentGraph'; import {createDependency} from './Dependency'; +import {type ProjectPath, fromProjectPathRelative} from './projectPath'; type InitOpts = {| - entries?: Array, + entries?: Array, targets?: Array, assetGroups?: Array, |}; @@ -62,7 +63,7 @@ export function nodeFromDep(dep: Dependency): DependencyNode { export function nodeFromAssetGroup(assetGroup: AssetGroup): AssetGroupNode { return { id: hashString( - assetGroup.filePath + + fromProjectPathRelative(assetGroup.filePath) + assetGroup.env.id + String(assetGroup.isSource) + String(assetGroup.sideEffects) + @@ -91,9 +92,9 @@ export function nodeFromAsset(asset: Asset): AssetNode { }; } -export function nodeFromEntrySpecifier(entry: string): EntrySpecifierNode { +export function nodeFromEntrySpecifier(entry: ProjectPath): EntrySpecifierNode { return { - id: 'entry_specifier:' + entry, + id: 'entry_specifier:' + fromProjectPathRelative(entry), type: 'entry_specifier', value: entry, }; @@ -176,6 +177,16 @@ export default class AssetGraph extends ContentGraph { addNode(node: AssetGraphNode): NodeId { this.hash = null; + let existing = this.getNodeByContentKey(node.id); + if (existing != null) { + invariant(existing.type === node.type); + // $FlowFixMe[incompatible-type] Checked above + // $FlowFixMe[prop-missing] + existing.value = node.value; + let existingId = this.getNodeIdByContentKey(node.id); + this.updateNode(existingId, existing); + return existingId; + } return super.addNodeByContentKey(node.id, node); } @@ -199,7 +210,7 @@ export default class AssetGraph extends ContentGraph { } resolveEntry( - entry: string, + entry: ProjectPath, resolved: Array, correspondingRequest: ContentKey, ) { @@ -223,8 +234,9 @@ export default class AssetGraph extends ContentGraph { ) { let depNodes = targets.map(target => { let node = nodeFromDep( - createDependency({ - specifier: entry.filePath, + // The passed project path is ignored in this case, because there is no `loc` + createDependency('', { + specifier: fromProjectPathRelative(entry.filePath), specifierType: 'esm', // ??? pipeline: target.pipeline, target: target, @@ -473,9 +485,14 @@ export default class AssetGraph extends ContentGraph { this.normalizeEnvironment(dep); let depNode = nodeFromDep(dep); let existing = this.getNodeByContentKey(depNode.id); - if (existing) { - invariant(existing.type === 'dependency'); - depNode.value.meta = existing.value.meta; + if ( + existing?.type === 'dependency' && + existing.value.resolverMeta != null + ) { + depNode.value.meta = { + ...depNode.value.meta, + ...existing.value.resolverMeta, + }; } let dependentAsset = dependentAssets.find( a => a.uniqueKey === dep.specifier, diff --git a/packages/core/core/src/BundleGraph.js b/packages/core/core/src/BundleGraph.js index 70bce0a4c96..68d017e1282 100644 --- a/packages/core/core/src/BundleGraph.js +++ b/packages/core/core/src/BundleGraph.js @@ -3,7 +3,6 @@ import type { BundleGroup, GraphVisitor, - SourceLocation, Symbol, TraversalActions, } from '@parcel/types'; @@ -17,6 +16,7 @@ import type { Dependency, DependencyNode, NodeId, + InternalSourceLocation, } from './types'; import type AssetGraph from './AssetGraph'; @@ -25,7 +25,7 @@ import invariant from 'assert'; import nullthrows from 'nullthrows'; import {objectSortedEntriesDeep} from '@parcel/utils'; import {Hash, hashString} from '@parcel/hash'; -import {Priority} from './types'; +import {Priority, BundleBehavior} from './types'; import {getBundleGroupId, getPublicId} from './utils'; import {ALL_EDGE_TYPES, mapVisitor} from './Graph'; @@ -33,7 +33,7 @@ import ContentGraph, { type SerializedContentGraph, type ContentGraphOpts, } from './ContentGraph'; -import Environment from './public/Environment'; +import {ISOLATED_ENVS} from './public/Environment'; export const bundleGraphEdgeTypes = { // A lack of an edge type indicates to follow the edge while traversing @@ -66,7 +66,7 @@ type InternalSymbolResolution = {| asset: Asset, exportSymbol: string, symbol: ?Symbol | false, - loc: ?SourceLocation, + loc: ?InternalSourceLocation, |}; type InternalExportSymbolResolution = {| @@ -849,9 +849,10 @@ export default class BundleGraph { // If a bundle's environment is isolated, it can't access assets present // in any ancestor bundles. Don't consider any assets reachable. if ( - new Environment(bundle.env).isIsolated() || + ISOLATED_ENVS.has(bundle.env.context) || !bundle.isSplittable || - bundle.isInline + bundle.bundleBehavior === BundleBehavior.isolated || + bundle.bundleBehavior === BundleBehavior.inline ) { return false; } @@ -877,7 +878,10 @@ export default class BundleGraph { // Check that every parent bundle has a bundle group in its ancestry that contains the asset. return parentBundleNodes.every(bundleNodeId => { let bundleNode = nullthrows(this._graph.getNode(bundleNodeId)); - if (bundleNode.type === 'root') { + if ( + bundleNode.type !== 'bundle' || + bundleNode.value.bundleBehavior === BundleBehavior.isolated + ) { return false; } @@ -902,7 +906,10 @@ export default class BundleGraph { let childBundles = this.getBundlesInBundleGroup(node.value); if ( childBundles.some( - b => b.id !== bundle.id && this.bundleHasAsset(b, asset), + b => + b.id !== bundle.id && + b.bundleBehavior !== BundleBehavior.isolated && + this.bundleHasAsset(b, asset), ) ) { actions.skipChildren(); @@ -1522,7 +1529,7 @@ export default class BundleGraph { let referencedBundles = this.getReferencedBundles(bundle); for (let referenced of referencedBundles) { - if (referenced.isInline) { + if (referenced.bundleBehavior === BundleBehavior.inline) { bundles.push(referenced); addReferencedBundles(referenced); } @@ -1532,7 +1539,7 @@ export default class BundleGraph { addReferencedBundles(bundle); this.traverseBundles((childBundle, _, traversal) => { - if (childBundle.isInline) { + if (childBundle.bundleBehavior === BundleBehavior.inline) { bundles.push(childBundle); } else if (childBundle.id !== bundle.id) { traversal.skipChildren(); @@ -1554,7 +1561,7 @@ export default class BundleGraph { } for (let referencedBundle of this.getReferencedBundles(bundle)) { - if (!referencedBundle.isInline) { + if (referencedBundle.bundleBehavior !== BundleBehavior.inline) { hash.writeString(referencedBundle.id); } } diff --git a/packages/core/core/src/CommittedAsset.js b/packages/core/core/src/CommittedAsset.js index 6561b6e8958..8d97839b068 100644 --- a/packages/core/core/src/CommittedAsset.js +++ b/packages/core/core/src/CommittedAsset.js @@ -109,7 +109,7 @@ export default class CommittedAsset { let mapBuffer = await this.getMapBuffer(); if (mapBuffer) { // Get sourcemap from flatbuffer - return new SourceMap(mapBuffer); + return new SourceMap(this.options.projectRoot, mapBuffer); } })(); } diff --git a/packages/core/core/src/ContentGraph.js b/packages/core/core/src/ContentGraph.js index 3166ece7f5e..bbf788e86d1 100644 --- a/packages/core/core/src/ContentGraph.js +++ b/packages/core/core/src/ContentGraph.js @@ -1,12 +1,13 @@ // @flow strict-local +import type {ContentKey, NodeId} from './types'; import Graph, {type SerializedGraph, type GraphOpts} from './Graph'; -import type {ContentKey, Node, NodeId} from './types'; import nullthrows from 'nullthrows'; export type ContentGraphOpts = {| ...GraphOpts, _contentKeyToNodeId: Map, + _nodeIdToContentKey: Map, |}; export type SerializedContentGraph = {| ...SerializedGraph, @@ -18,15 +19,18 @@ export default class ContentGraph< TEdgeType: number = 1, > extends Graph { _contentKeyToNodeId: Map; + _nodeIdToContentKey: Map; constructor(opts: ?ContentGraphOpts) { if (opts) { - let {_contentKeyToNodeId, ...rest} = opts; + let {_contentKeyToNodeId, _nodeIdToContentKey, ...rest} = opts; super(rest); this._contentKeyToNodeId = _contentKeyToNodeId; + this._nodeIdToContentKey = _nodeIdToContentKey; } else { super(); this._contentKeyToNodeId = new Map(); + this._nodeIdToContentKey = new Map(); } } @@ -42,21 +46,19 @@ export default class ContentGraph< return { ...super.serialize(), _contentKeyToNodeId: this._contentKeyToNodeId, + _nodeIdToContentKey: this._nodeIdToContentKey, }; } addNodeByContentKey(contentKey: ContentKey, node: TNode): NodeId { - if (!this.hasContentKey(contentKey)) { - let nodeId = super.addNode(node); - this._contentKeyToNodeId.set(contentKey, nodeId); - return nodeId; - } else { - let existingNodeId = this.getNodeIdByContentKey(contentKey); - let existingNode = nullthrows(this.getNodeByContentKey(contentKey)); - existingNode.value = node.value; - this.updateNode(existingNodeId, existingNode); - return existingNodeId; + if (this.hasContentKey(contentKey)) { + throw new Error('Graph already has content key ' + contentKey); } + + let nodeId = super.addNode(node); + this._contentKeyToNodeId.set(contentKey, nodeId); + this._nodeIdToContentKey.set(nodeId, contentKey); + return nodeId; } getNodeByContentKey(contentKey: ContentKey): ?TNode { @@ -69,7 +71,7 @@ export default class ContentGraph< getNodeIdByContentKey(contentKey: ContentKey): NodeId { return nullthrows( this._contentKeyToNodeId.get(contentKey), - 'Expected content key to exist', + `Expected content key ${contentKey} to exist`, ); } @@ -77,9 +79,11 @@ export default class ContentGraph< return this._contentKeyToNodeId.has(contentKey); } - removeNode(nodeId: NodeId) { + removeNode(nodeId: NodeId): void { this._assertHasNodeId(nodeId); - this._contentKeyToNodeId.delete(nullthrows(this.getNode(nodeId)).id); + let contentKey = nullthrows(this._nodeIdToContentKey.get(nodeId)); + this._contentKeyToNodeId.delete(contentKey); + this._nodeIdToContentKey.delete(nodeId); super.removeNode(nodeId); } } diff --git a/packages/core/core/src/Dependency.js b/packages/core/core/src/Dependency.js index cbc54150814..438693a2948 100644 --- a/packages/core/core/src/Dependency.js +++ b/packages/core/core/src/Dependency.js @@ -1,28 +1,34 @@ // @flow import type { - SourceLocation, + FilePath, Meta, DependencySpecifier, + SourceLocation, Symbol, + BundleBehavior as IBundleBehavior, } from '@parcel/types'; import type {Dependency, Environment, Target} from './types'; import {hashString} from '@parcel/hash'; -import {SpecifierType, Priority} from './types'; +import {SpecifierType, Priority, BundleBehavior} from './types'; + +import {toInternalSourceLocation} from './utils'; +import {toProjectPath} from './projectPath'; type DependencyOpts = {| id?: string, - sourcePath?: string, + sourcePath?: FilePath, sourceAssetId?: string, specifier: DependencySpecifier, specifierType: $Keys, priority?: $Keys, needsStableName?: boolean, + bundleBehavior?: ?IBundleBehavior, isEntry?: boolean, isOptional?: boolean, loc?: SourceLocation, env: Environment, meta?: Meta, - resolveFrom?: string, + resolveFrom?: FilePath, target?: Target, symbols?: Map< Symbol, @@ -31,7 +37,10 @@ type DependencyOpts = {| pipeline?: ?string, |}; -export function createDependency(opts: DependencyOpts): Dependency { +export function createDependency( + projectRoot: FilePath, + opts: DependencyOpts, +): Dependency { let id = opts.id || hashString( @@ -44,14 +53,32 @@ export function createDependency(opts: DependencyOpts): Dependency { return { ...opts, + resolveFrom: toProjectPath(projectRoot, opts.resolveFrom), + sourcePath: toProjectPath(projectRoot, opts.sourcePath), id, + loc: toInternalSourceLocation(projectRoot, opts.loc), specifierType: SpecifierType[opts.specifierType], priority: Priority[opts.priority ?? 'sync'], needsStableName: opts.needsStableName ?? false, + bundleBehavior: opts.bundleBehavior + ? BundleBehavior[opts.bundleBehavior] + : null, isEntry: opts.isEntry ?? false, isOptional: opts.isOptional ?? false, meta: opts.meta || {}, - symbols: opts.symbols, + symbols: + opts.symbols && + new Map( + [...opts.symbols].map(([k, v]) => [ + k, + { + local: v.local, + meta: v.meta, + isWeak: v.isWeak, + loc: toInternalSourceLocation(projectRoot, v.loc), + }, + ]), + ), }; } diff --git a/packages/core/core/src/Environment.js b/packages/core/core/src/Environment.js index 9271ad19b12..ca7c9408233 100644 --- a/packages/core/core/src/Environment.js +++ b/packages/core/core/src/Environment.js @@ -1,13 +1,19 @@ // @flow -import type {EnvironmentOptions} from '@parcel/types'; -import type {Environment} from './types'; +import type {EnvironmentOptions, FilePath} from '@parcel/types'; +import type {Environment, InternalSourceLocation} from './types'; import {hashString} from '@parcel/hash'; +import {toInternalSourceLocation} from './utils'; const DEFAULT_ENGINES = { browsers: ['> 0.25%'], node: '>= 8.0.0', }; +type EnvironmentOpts = {| + ...EnvironmentOptions, + loc?: ?InternalSourceLocation, +|}; + export function createEnvironment({ context, engines, @@ -19,7 +25,7 @@ export function createEnvironment({ shouldScopeHoist = false, sourceMap, loc, -}: EnvironmentOptions = {}): Environment { +}: EnvironmentOpts = {}): Environment { if (context == null) { if (engines?.node) { context = 'node'; @@ -99,6 +105,7 @@ export function createEnvironment({ } export function mergeEnvironments( + projectRoot: FilePath, a: Environment, b: ?EnvironmentOptions, ): Environment { @@ -111,6 +118,7 @@ export function mergeEnvironments( return createEnvironment({ ...a, ...b, + loc: b.loc ? toInternalSourceLocation(projectRoot, b.loc) : a.loc, }); } diff --git a/packages/core/core/src/Graph.js b/packages/core/core/src/Graph.js index ff15b895328..b14fc41e010 100644 --- a/packages/core/core/src/Graph.js +++ b/packages/core/core/src/Graph.js @@ -2,7 +2,7 @@ import {fromNodeId} from './types'; import AdjacencyList, {type SerializedAdjacencyList} from './AdjacencyList'; -import type {Edge, Node, NodeId} from './types'; +import type {Edge, NodeId} from './types'; import type {TraversalActions, GraphVisitor} from '@parcel/types'; import assert from 'assert'; @@ -24,7 +24,7 @@ export type SerializedGraph = {| export type AllEdgeTypes = '@@all_edge_types'; export const ALL_EDGE_TYPES: AllEdgeTypes = '@@all_edge_types'; -export default class Graph { +export default class Graph { nodes: Map; adjacencyList: AdjacencyList; rootNodeId: ?NodeId; diff --git a/packages/core/core/src/InternalConfig.js b/packages/core/core/src/InternalConfig.js index b1158d81d3c..a8f819e11f7 100644 --- a/packages/core/core/src/InternalConfig.js +++ b/packages/core/core/src/InternalConfig.js @@ -1,27 +1,29 @@ // @flow strict-local +import type {PackageName, ConfigResult} from '@parcel/types'; import type { - FileCreateInvalidation, - FilePath, - PackageName, - ConfigResult, - DevDepOptions, -} from '@parcel/types'; -import type {Config, Environment} from './types'; + Config, + Environment, + InternalFileCreateInvalidation, + InternalDevDepOptions, +} from './types'; +import type {ProjectPath} from './projectPath'; + +import {fromProjectPathRelative} from './projectPath'; import {createEnvironment} from './Environment'; import {hashString} from '@parcel/hash'; type ConfigOpts = {| plugin: PackageName, - searchPath: FilePath, + searchPath: ProjectPath, isSource?: boolean, env?: Environment, result?: ConfigResult, - invalidateOnFileChange?: Set, - invalidateOnFileCreate?: Array, + invalidateOnFileChange?: Set, + invalidateOnFileCreate?: Array, invalidateOnEnvChange?: Set, invalidateOnOptionChange?: Set, - devDeps?: Array, + devDeps?: Array, invalidateOnStartup?: boolean, |}; @@ -40,7 +42,12 @@ export function createConfig({ }: ConfigOpts): Config { let environment = env ?? createEnvironment(); return { - id: hashString(plugin + searchPath + environment.id + String(isSource)), + id: hashString( + plugin + + fromProjectPathRelative(searchPath) + + environment.id + + String(isSource), + ), isSource: isSource ?? false, searchPath, env: environment, diff --git a/packages/core/core/src/PackagerRunner.js b/packages/core/core/src/PackagerRunner.js index 580830fc5cc..1504cb36f7d 100644 --- a/packages/core/core/src/PackagerRunner.js +++ b/packages/core/core/src/PackagerRunner.js @@ -39,6 +39,12 @@ import BundleGraph, { } from './public/BundleGraph'; import PluginOptions from './public/PluginOptions'; import {PARCEL_VERSION, HASH_REF_PREFIX, HASH_REF_REGEX} from './constants'; +import { + fromProjectPath, + toProjectPathUnsafe, + fromProjectPathRelative, + joinProjectPath, +} from './projectPath'; import {createConfig} from './InternalConfig'; import { loadPluginConfig, @@ -201,7 +207,7 @@ export default class PackagerRunner { if (plugin.plugin.loadConfig != null) { let config = createConfig({ plugin: plugin.name, - searchPath: path.join(this.options.projectRoot, 'index'), + searchPath: toProjectPathUnsafe('index'), }); await loadPluginConfig(plugin, config, this.options); @@ -213,7 +219,10 @@ export default class PackagerRunner { this.previousDevDeps, this.options, ); - let key = `${devDep.specifier}:${devDep.resolveFrom}`; + let key = `${devDep.specifier}:${fromProjectPath( + this.options.projectRoot, + devDep.resolveFrom, + )}`; this.devDepRequests.set(key, devDepRequest); } @@ -295,7 +304,7 @@ export default class PackagerRunner { } getSourceMapReference(bundle: NamedBundle, map: ?SourceMap): Async { - if (map && bundle.env.sourceMap && !bundle.isInline) { + if (map && bundle.env.sourceMap && bundle.bundleBehavior !== 'inline') { if (bundle.env.sourceMap && bundle.env.sourceMap.inline) { return this.generateSourceMap(bundleToInternalBundle(bundle), map); } else { @@ -338,7 +347,7 @@ export default class PackagerRunner { bundle: BundleType, bundleGraph: BundleGraphType, ) => { - if (!bundle.isInline) { + if (bundle.bundleBehavior !== 'inline') { throw new Error( 'Bundle is not inline and unable to retrieve contents', ); @@ -373,7 +382,10 @@ export default class PackagerRunner { this.previousDevDeps, this.options, ); - this.devDepRequests.set(`${name}:${resolveFrom}`, devDepRequest); + this.devDepRequests.set( + `${name}:${fromProjectPathRelative(resolveFrom)}`, + devDepRequest, + ); } } @@ -453,7 +465,7 @@ export default class PackagerRunner { this.options, ); this.devDepRequests.set( - `${optimizer.name}:${optimizer.resolveFrom}`, + `${optimizer.name}:${fromProjectPathRelative(optimizer.resolveFrom)}`, devDepRequest, ); } @@ -467,9 +479,13 @@ export default class PackagerRunner { map: SourceMap, ): Promise { // sourceRoot should be a relative path between outDir and rootDir for node.js targets - let filePath = path.join(bundle.target.distDir, nullthrows(bundle.name)); + let filePath = joinProjectPath( + bundle.target.distDir, + nullthrows(bundle.name), + ); + let fullPath = fromProjectPath(this.options.projectRoot, filePath); let sourceRoot: string = path.relative( - path.dirname(filePath), + path.dirname(fullPath), this.options.projectRoot, ); let inlineSources = false; @@ -498,7 +514,7 @@ export default class PackagerRunner { } } - let mapFilename = filePath + '.map'; + let mapFilename = fullPath + '.map'; let isInlineMap = bundle.env.sourceMap && bundle.env.sourceMap.inline; let stringified = await map.stringify({ @@ -559,11 +575,13 @@ export default class PackagerRunner { let packager = await this.config.getPackager(name); let optimizers = await this.config.getOptimizers(name); - let key = `${packager.name}:${packager.resolveFrom}`; + let key = `${packager.name}:${fromProjectPathRelative( + packager.resolveFrom, + )}`; let devDepHashes = this.devDepRequests.get(key)?.hash ?? this.previousDevDeps.get(key) ?? ''; for (let {name, resolveFrom} of optimizers) { - let key = `${name}:${resolveFrom}`; + let key = `${name}:${fromProjectPathRelative(resolveFrom)}`; devDepHashes += this.devDepRequests.get(key)?.hash ?? this.previousDevDeps.get(key) ?? diff --git a/packages/core/core/src/Parcel.js b/packages/core/core/src/Parcel.js index 147b0fd8f46..adcd1628907 100644 --- a/packages/core/core/src/Parcel.js +++ b/packages/core/core/src/Parcel.js @@ -41,6 +41,7 @@ import createParcelBuildRequest from './requests/ParcelBuildRequest'; import {Disposable} from '@parcel/events'; import {init as initSourcemaps} from '@parcel/source-map'; import {init as initHash} from '@parcel/hash'; +import {toProjectPath} from './projectPath'; registerCoreWithSerializer(); @@ -366,12 +367,12 @@ export default class Parcel { } } - _getWatcherSubscription(): Promise { + async _getWatcherSubscription(): Promise { invariant(this.#watcherSubscription == null); let resolvedOptions = nullthrows(this.#resolvedOptions); let opts = getWatcherOptions(resolvedOptions); - return resolvedOptions.inputFS.watch( + let sub = await resolvedOptions.inputFS.watch( resolvedOptions.projectRoot, (err, events) => { if (err) { @@ -379,7 +380,12 @@ export default class Parcel { return; } - let isInvalid = this.#requestTracker.respondToFSEvents(events); + let isInvalid = this.#requestTracker.respondToFSEvents( + events.map(e => ({ + type: e.type, + path: toProjectPath(resolvedOptions.projectRoot, e.path), + })), + ); if (isInvalid && this.#watchQueue.getNumWaiting() === 0) { if (this.#watchAbortController) { this.#watchAbortController.abort(); @@ -391,6 +397,7 @@ export default class Parcel { }, opts, ); + return {unsubscribe: () => sub.unsubscribe()}; } // This is mainly for integration tests and it not public api! diff --git a/packages/core/core/src/ParcelConfig.js b/packages/core/core/src/ParcelConfig.js index 22272f91d9d..f5b4021c242 100644 --- a/packages/core/core/src/ParcelConfig.js +++ b/packages/core/core/src/ParcelConfig.js @@ -1,6 +1,5 @@ // @flow import type { - FilePath, Glob, Transformer, Resolver, @@ -14,6 +13,7 @@ import type { Semver, SemverRange, Validator, + FilePath, } from '@parcel/types'; import type { ProcessedParcelConfig, @@ -22,9 +22,16 @@ import type { ExtendableParcelConfigPipeline, ParcelOptions, } from './types'; + import {makeRe} from 'micromatch'; import {basename} from 'path'; import loadPlugin from './loadParcelPlugin'; +import { + type ProjectPath, + fromProjectPath, + fromProjectPathRelative, + toProjectPathUnsafe, +} from './projectPath'; type GlobMap = {[Glob]: T, ...}; type SerializedParcelConfig = {| @@ -37,14 +44,14 @@ export type LoadedPlugin = {| name: string, version: Semver, plugin: T, - resolveFrom: FilePath, + resolveFrom: ProjectPath, keyPath?: string, range?: ?SemverRange, |}; export default class ParcelConfig { options: ParcelOptions; - filePath: FilePath; + filePath: ProjectPath; resolvers: PureParcelConfigPipeline; transformers: GlobMap; bundler: ?ParcelPluginNode; @@ -105,7 +112,7 @@ export default class ParcelConfig { ): Promise<{| plugin: T, version: Semver, - resolveFrom: FilePath, + resolveFrom: ProjectPath, range: ?SemverRange, |}> { let plugin = this.pluginCache.get(node.packageName); @@ -115,7 +122,7 @@ export default class ParcelConfig { plugin = loadPlugin( node.packageName, - node.resolveFrom, + fromProjectPath(this.options.projectRoot, node.resolveFrom), node.keyPath, this.options, ); @@ -159,21 +166,23 @@ export default class ParcelConfig { return this.loadPlugins(this._getResolverNodes()); } - _getValidatorNodes(filePath: FilePath): $ReadOnlyArray { + _getValidatorNodes(filePath: ProjectPath): $ReadOnlyArray { let validators: PureParcelConfigPipeline = this.matchGlobMapPipelines(filePath, this.validators) || []; return validators; } - getValidatorNames(filePath: FilePath): Array { + getValidatorNames(filePath: ProjectPath): Array { let validators: PureParcelConfigPipeline = this._getValidatorNodes( filePath, ); return validators.map(v => v.packageName); } - getValidators(filePath: FilePath): Promise>> { + getValidators( + filePath: ProjectPath, + ): Promise>> { let validators = this._getValidatorNodes(filePath); return this.loadPlugins(validators); } @@ -185,7 +194,7 @@ export default class ParcelConfig { } _getTransformerNodes( - filePath: FilePath, + filePath: ProjectPath, pipeline?: ?string, allowEmpty?: boolean, ): $ReadOnlyArray { @@ -200,7 +209,7 @@ export default class ParcelConfig { } throw new Error( - `No transformers found for ${filePath}` + + `No transformers found for ${fromProjectPathRelative(filePath)}` + (pipeline != null ? ` with pipeline: '${pipeline}'` : '') + '.', ); @@ -210,7 +219,7 @@ export default class ParcelConfig { } getTransformerNames( - filePath: FilePath, + filePath: ProjectPath, pipeline?: ?string, allowEmpty?: boolean, ): Array { @@ -223,7 +232,7 @@ export default class ParcelConfig { } getTransformers( - filePath: FilePath, + filePath: ProjectPath, pipeline?: ?string, allowEmpty?: boolean, ): Promise>>> { @@ -265,7 +274,10 @@ export default class ParcelConfig { } _getPackagerNode(filePath: FilePath): ParcelPluginNode { - let packagerName = this.matchGlobMap(filePath, this.packagers); + let packagerName = this.matchGlobMap( + toProjectPathUnsafe(filePath), + this.packagers, + ); if (!packagerName) { throw new Error(`No packager found for "${filePath}".`); } @@ -295,7 +307,11 @@ export default class ParcelConfig { } return ( - this.matchGlobMapPipelines(filePath, this.optimizers, pipeline) ?? [] + this.matchGlobMapPipelines( + toProjectPathUnsafe(filePath), + this.optimizers, + pipeline, + ) ?? [] ); } @@ -320,7 +336,14 @@ export default class ParcelConfig { return this.loadPlugins(this.reporters); } - isGlobMatch(filePath: FilePath, pattern: Glob, pipeline?: ?string): boolean { + isGlobMatch( + projectPath: ProjectPath, + pattern: Glob, + pipeline?: ?string, + ): boolean { + // glob's shouldn't be dependant on absolute paths anyway + let filePath = fromProjectPathRelative(projectPath); + let [patternPipeline, patternGlob] = pattern.split(':'); if (!patternGlob) { patternGlob = patternPipeline; @@ -329,7 +352,7 @@ export default class ParcelConfig { let re = this.regexCache.get(patternGlob); if (!re) { - re = makeRe(patternGlob, {dot: true}); + re = makeRe(patternGlob, {dot: true, nocase: true}); this.regexCache.set(patternGlob, re); } @@ -339,7 +362,7 @@ export default class ParcelConfig { ); } - matchGlobMap(filePath: FilePath, globMap: {|[Glob]: T|}): ?T { + matchGlobMap(filePath: ProjectPath, globMap: {|[Glob]: T|}): ?T { for (let pattern in globMap) { if (this.isGlobMatch(filePath, pattern)) { return globMap[pattern]; @@ -350,7 +373,7 @@ export default class ParcelConfig { } matchGlobMapPipelines( - filePath: FilePath, + filePath: ProjectPath, globMap: {|[Glob]: ExtendableParcelConfigPipeline|}, pipeline?: ?string, ): PureParcelConfigPipeline { diff --git a/packages/core/core/src/RequestTracker.js b/packages/core/core/src/RequestTracker.js index f132bf6c908..b413c9c3d8d 100644 --- a/packages/core/core/src/RequestTracker.js +++ b/packages/core/core/src/RequestTracker.js @@ -1,21 +1,17 @@ // @flow strict-local import type {AbortSignal} from 'abortcontroller-polyfill/dist/cjs-ponyfill'; -import type { - Async, - File, - FilePath, - FileCreateInvalidation, - Glob, - EnvMap, -} from '@parcel/types'; -import type {Event, Options as WatcherOptions} from '@parcel/watcher'; +import type {Async, EnvMap} from '@parcel/types'; +import type {EventType, Options as WatcherOptions} from '@parcel/watcher'; import type WorkerFarm from '@parcel/workers'; import type { ContentKey, NodeId, ParcelOptions, RequestInvalidation, + InternalFile, + InternalFileCreateInvalidation, + InternalGlob, } from './types'; import type {Deferred} from '@parcel/utils'; @@ -33,6 +29,13 @@ import ContentGraph, { type ContentGraphOpts, } from './ContentGraph'; import {assertSignalNotAborted, hashFromOption} from './utils'; +import { + type ProjectPath, + fromProjectPathRelative, + toProjectPathUnsafe, + toProjectPath, +} from './projectPath'; + import { PARCEL_VERSION, VALID, @@ -76,8 +79,8 @@ type SerializedRequestGraph = {| unpredicatableNodeIds: Set, |}; -type FileNode = {|id: ContentKey, +type: 'file', value: File|}; -type GlobNode = {|id: ContentKey, +type: 'glob', value: Glob|}; +type FileNode = {|id: ContentKey, +type: 'file', value: InternalFile|}; +type GlobNode = {|id: ContentKey, +type: 'glob', value: InternalGlob|}; type FileNameNode = {| id: ContentKey, +type: 'file_name', @@ -125,9 +128,9 @@ type RequestGraphNode = | OptionNode; export type RunAPI = {| - invalidateOnFileCreate: FileCreateInvalidation => void, - invalidateOnFileDelete: FilePath => void, - invalidateOnFileUpdate: FilePath => void, + invalidateOnFileCreate: InternalFileCreateInvalidation => void, + invalidateOnFileDelete: ProjectPath => void, + invalidateOnFileUpdate: ProjectPath => void, invalidateOnStartup: () => void, invalidateOnEnvChange: string => void, invalidateOnOptionChange: string => void, @@ -154,32 +157,32 @@ export type StaticRunOpts = {| invalidateReason: InvalidateReason, |}; -const nodeFromFilePath = (filePath: string) => ({ - id: filePath, +const nodeFromFilePath = (filePath: ProjectPath): RequestGraphNode => ({ + id: fromProjectPathRelative(filePath), type: 'file', value: {filePath}, }); -const nodeFromGlob = (glob: Glob) => ({ - id: glob, +const nodeFromGlob = (glob: InternalGlob): RequestGraphNode => ({ + id: fromProjectPathRelative(glob), type: 'glob', value: glob, }); -const nodeFromFileName = (fileName: string) => ({ +const nodeFromFileName = (fileName: string): RequestGraphNode => ({ id: 'file_name:' + fileName, type: 'file_name', value: fileName, }); -const nodeFromRequest = (request: StoredRequest) => ({ +const nodeFromRequest = (request: StoredRequest): RequestGraphNode => ({ id: request.id, type: 'request', value: request, invalidateReason: INITIAL_BUILD, }); -const nodeFromEnv = (env: string, value: string | void) => ({ +const nodeFromEnv = (env: string, value: string | void): RequestGraphNode => ({ id: 'env:' + env, type: 'env', value: { @@ -188,7 +191,7 @@ const nodeFromEnv = (env: string, value: string | void) => ({ }, }); -const nodeFromOption = (option: string, value: mixed) => ({ +const nodeFromOption = (option: string, value: mixed): RequestGraphNode => ({ id: 'option:' + option, type: 'option', value: { @@ -260,6 +263,7 @@ export class RequestGraph extends ContentGraph< this.invalidNodeIds.delete(nodeId); this.incompleteNodeIds.delete(nodeId); this.incompleteNodePromises.delete(nodeId); + this.unpredicatableNodeIds.delete(nodeId); let node = nullthrows(this.getNode(nodeId)); if (node.type === 'glob') { this.globNodeIds.delete(nodeId); @@ -351,7 +355,7 @@ export class RequestGraph extends ContentGraph< } } - invalidateOnFileUpdate(requestNodeId: NodeId, filePath: FilePath) { + invalidateOnFileUpdate(requestNodeId: NodeId, filePath: ProjectPath) { let fileNodeId = this.addNode(nodeFromFilePath(filePath)); if ( @@ -369,7 +373,7 @@ export class RequestGraph extends ContentGraph< } } - invalidateOnFileDelete(requestNodeId: NodeId, filePath: FilePath) { + invalidateOnFileDelete(requestNodeId: NodeId, filePath: ProjectPath) { let fileNodeId = this.addNode(nodeFromFilePath(filePath)); if ( @@ -387,7 +391,10 @@ export class RequestGraph extends ContentGraph< } } - invalidateOnFileCreate(requestNodeId: NodeId, input: FileCreateInvalidation) { + invalidateOnFileCreate( + requestNodeId: NodeId, + input: InternalFileCreateInvalidation, + ) { let node; if (input.glob != null) { node = nodeFromGlob(input.glob); @@ -606,13 +613,13 @@ export class RequestGraph extends ContentGraph< invalidateFileNameNode( node: FileNameNode, - filePath: FilePath, + filePath: ProjectPath, matchNodes: Array, ) { // If there is an edge between this file_name node and one of the original file nodes pointed to // by the original file_name node, and the matched node is inside the current directory, invalidate // all connected requests pointed to by the file node. - let dirname = path.dirname(filePath); + let dirname = path.dirname(fromProjectPathRelative(filePath)); let nodeId = this.getNodeIdByContentKey(node.id); for (let matchNode of matchNodes) { @@ -623,7 +630,10 @@ export class RequestGraph extends ContentGraph< matchNodeId, requestGraphEdgeTypes.invalidated_by_create_above, ) && - isDirectoryInside(path.dirname(matchNode.value.filePath), dirname) + isDirectoryInside( + path.dirname(fromProjectPathRelative(matchNode.value.filePath)), + dirname, + ) ) { let connectedNodes = this.getNodeIdsConnectedTo( matchNodeId, @@ -649,16 +659,35 @@ export class RequestGraph extends ContentGraph< ) { let parent = nullthrows(this.getNodeByContentKey(contentKey)); invariant(parent.type === 'file_name'); - this.invalidateFileNameNode(parent, dirname, matchNodes); + this.invalidateFileNameNode( + parent, + toProjectPathUnsafe(dirname), + matchNodes, + ); } } } - respondToFSEvents(events: Array): boolean { + respondToFSEvents( + events: Array<{|path: ProjectPath, type: EventType|}>, + ): boolean { let didInvalidate = false; - for (let {path: filePath, type} of events) { + for (let {path: _filePath, type} of events) { + let filePath = fromProjectPathRelative(_filePath); let hasFileRequest = this.hasContentKey(filePath); + // If we see a 'create' event for the project root itself, + // this means the project root was moved and we need to + // re-run all requests. + if (type === 'create' && filePath === '') { + for (let [id, node] of this.nodes) { + if (node.type === 'request') { + this.invalidNodeIds.add(id); + } + } + return true; + } + // sometimes mac os reports update events as create events. // if it was a create event, but the file already exists in the graph, // then also invalidate nodes connected by invalidated_by_update edges. @@ -703,7 +732,7 @@ export class RequestGraph extends ContentGraph< if (above.length > 0) { didInvalidate = true; - this.invalidateFileNameNode(fileNameNode, filePath, above); + this.invalidateFileNameNode(fileNameNode, _filePath, above); } } @@ -711,7 +740,7 @@ export class RequestGraph extends ContentGraph< let globNode = this.getNode(globeNodeId); invariant(globNode && globNode.type === 'glob'); - if (isGlobMatch(filePath, globNode.value)) { + if (isGlobMatch(filePath, fromProjectPathRelative(globNode.value))) { let connectedNodes = this.getNodeIdsConnectedTo( globeNodeId, requestGraphEdgeTypes.invalidated_by_create, @@ -731,6 +760,11 @@ export class RequestGraph extends ContentGraph< didInvalidate = true; this.invalidateNode(connectedNode, FILE_DELETE); } + + // Delete the file node since it doesn't exist anymore. + // This ensures that files that don't exist aren't sent + // to requests as invalidations for future requests. + this.removeNode(nodeId); } } @@ -848,7 +882,9 @@ export default class RequestTracker { } } - respondToFSEvents(events: Array): boolean { + respondToFSEvents( + events: Array<{|path: ProjectPath, type: EventType|}>, + ): boolean { return this.graph.respondToFSEvents(events); } @@ -902,12 +938,17 @@ export default class RequestTracker { } } + let previousInvalidations = + requestId != null ? this.graph.getInvalidations(requestId) : []; let {requestNodeId, deferred} = this.startRequest({ id: request.id, type: request.type, }); - let {api, subRequestContentKeys} = this.createAPI(requestNodeId); + let {api, subRequestContentKeys} = this.createAPI( + requestNodeId, + previousInvalidations, + ); try { let node = this.graph.getRequestNode(requestNodeId); @@ -936,9 +977,9 @@ export default class RequestTracker { createAPI( requestId: NodeId, + previousInvalidations: Array, ): {|api: RunAPI, subRequestContentKeys: Set|} { let subRequestContentKeys = new Set(); - let invalidations = this.graph.getInvalidations(requestId); let api: RunAPI = { invalidateOnFileCreate: input => this.graph.invalidateOnFileCreate(requestId, input), @@ -955,7 +996,7 @@ export default class RequestTracker { option, this.options[option], ), - getInvalidations: () => invalidations, + getInvalidations: () => previousInvalidations, storeResult: (result, cacheKey) => { this.storeResult(requestId, result, cacheKey); }, @@ -1066,7 +1107,12 @@ async function loadRequestGraph(options): Async { requestGraph.invalidateUnpredictableNodes(); requestGraph.invalidateEnvNodes(options.env); requestGraph.invalidateOptionNodes(options); - requestGraph.respondToFSEvents(events); + requestGraph.respondToFSEvents( + events.map(e => ({ + type: e.type, + path: toProjectPath(options.projectRoot, e.path), + })), + ); return requestGraph; } diff --git a/packages/core/core/src/Transformation.js b/packages/core/core/src/Transformation.js index 0358165f957..63f3b4732bd 100644 --- a/packages/core/core/src/Transformation.js +++ b/packages/core/core/src/Transformation.js @@ -2,12 +2,10 @@ import type { FilePath, - FileCreateInvalidation, GenerateOutput, Transformer, TransformerResult, PackageName, - DevDepOptions, SemverRange, } from '@parcel/types'; import type {WorkerApi} from '@parcel/workers'; @@ -18,12 +16,15 @@ import type { Config, DevDepRequest, ParcelOptions, + InternalFileCreateInvalidation, + InternalDevDepOptions, } from './types'; import type {LoadedPlugin} from './ParcelConfig'; import path from 'path'; +import {Readable} from 'stream'; import nullthrows from 'nullthrows'; -import {normalizeSeparators, objectSortedEntries} from '@parcel/utils'; +import {objectSortedEntries} from '@parcel/utils'; import logger, {PluginLogger} from '@parcel/logger'; import ThrowableDiagnostic, { errorToDiagnostic, @@ -64,6 +65,14 @@ import { invalidateDevDeps, getWorkerDevDepRequests, } from './requests/DevDepRequest'; +import { + type ProjectPath, + fromProjectPath, + fromProjectPathRelative, + toProjectPathUnsafe, + toProjectPath, +} from './projectPath'; +import {invalidateOnFileCreateToInternal} from './utils'; type GenerateFunc = (input: UncommittedAsset) => Promise; @@ -78,7 +87,7 @@ export type TransformationResult = {| assets: Array, configRequests: Array, invalidations: Array, - invalidateOnFileCreate: Array, + invalidateOnFileCreate: Array, devDepRequests: Array, |}; @@ -91,7 +100,7 @@ export default class Transformation { workerApi: WorkerApi; parcelConfig: ParcelConfig; invalidations: Map; - invalidateOnFileCreate: Array; + invalidateOnFileCreate: Array; constructor({request, options, config, workerApi}: TransformationOpts) { this.configs = new Map(); @@ -117,36 +126,38 @@ export default class Transformation { async run(): Promise { let asset = await this.loadAsset(); + let existing; if (!asset.mapBuffer && SOURCEMAP_EXTENSIONS.has(asset.value.type)) { // Load existing sourcemaps, this automatically runs the source contents extraction - let existing; try { existing = await asset.loadExistingSourcemap(); } catch (err) { logger.verbose([ { origin: '@parcel/core', - message: md`Could not load existing source map for ${path.relative( - this.options.projectRoot, + message: md`Could not load existing source map for ${fromProjectPathRelative( asset.value.filePath, )}`, - filePath: asset.value.filePath, }, { origin: '@parcel/core', message: escapeMarkdown(err.message), - filePath: asset.value.filePath, }, ]); } + } - if (existing == null) { - // If no existing sourcemap was found, initialize asset.sourceContent - // with the original contents. This will be used when the transformer - // calls setMap to ensure the source content is in the sourcemap. - asset.sourceContent = await asset.getCode(); - } + if ( + existing == null && + // Don't buffer an entire stream into memory since it may not need sourceContent, + // e.g. large binary files + !(asset.content instanceof Readable) + ) { + // If no existing sourcemap was found, initialize asset.sourceContent + // with the original contents. This will be used when the transformer + // calls setMap to ensure the source content is in the sourcemap. + asset.sourceContent = await asset.getCode(); } invalidateDevDeps( @@ -194,26 +205,27 @@ export default class Transformation { size, hash, isSource: summarizedIsSource, - } = await summarizeRequest(this.options.inputFS, {filePath, code}); + } = await summarizeRequest(this.options.inputFS, { + filePath: fromProjectPath(this.options.projectRoot, filePath), + code, + }); // Prefer `isSource` originating from the AssetRequest. let isSource = isSourceOverride ?? summarizedIsSource; // If the transformer request passed code, use a hash in addition // to the filename as the base for the id to ensure it is unique. - let idBase = normalizeSeparators( - path.relative(this.options.projectRoot, filePath), - ); + let idBase = fromProjectPathRelative(filePath); if (code != null) { idBase += hash; } return new UncommittedAsset({ idBase, - value: createAsset({ + value: createAsset(this.options.projectRoot, { idBase, filePath, isSource, - type: path.extname(filePath).slice(1), + type: path.extname(fromProjectPathRelative(filePath)).slice(1), hash, pipeline, env, @@ -304,7 +316,9 @@ export default class Transformation { async getPipelineHash(pipeline: Pipeline): Promise { let hashes = ''; for (let transformer of pipeline.transformers) { - let key = `${transformer.name}:${transformer.resolveFrom}`; + let key = `${transformer.name}:${fromProjectPathRelative( + transformer.resolveFrom, + )}`; hashes += this.request.devDeps.get(key) ?? this.devDepRequests.get(key)?.hash ?? @@ -315,7 +329,9 @@ export default class Transformation { hashes += await getConfigHash(config, transformer.name, this.options); for (let devDep of config.devDeps) { - let key = `${devDep.specifier}:${devDep.resolveFrom}`; + let key = `${devDep.specifier}:${fromProjectPathRelative( + devDep.resolveFrom, + )}`; hashes += nullthrows(this.devDepRequests.get(key)).hash; } } @@ -325,21 +341,25 @@ export default class Transformation { } async addDevDependency( - opts: DevDepOptions, + opts: InternalDevDepOptions, transformer: | LoadedPlugin> | TransformerWithNameAndConfig, ): Promise { let {specifier, resolveFrom, range} = opts; - let key = `${specifier}:${resolveFrom}`; + let key = `${specifier}:${fromProjectPathRelative(resolveFrom)}`; if (this.devDepRequests.has(key)) { return; } // Ensure that the package manager has an entry for this resolution. - await this.options.packageManager.resolve(specifier, resolveFrom, { - range, - }); + await this.options.packageManager.resolve( + specifier, + fromProjectPath(this.options.projectRoot, opts.resolveFrom), + { + range, + }, + ); let devDepRequest = await createDevDependency( opts, @@ -406,7 +426,10 @@ export default class Transformation { throw new ThrowableDiagnostic({ diagnostic: errorToDiagnostic(e, { origin: transformer.name, - filePath: asset.value.filePath, + filePath: fromProjectPath( + this.options.projectRoot, + asset.value.filePath, + ), }), }); } @@ -526,7 +549,7 @@ export default class Transformation { } async loadPipeline( - filePath: FilePath, + filePath: ProjectPath, isSource: boolean, pipeline: ?string, ): Promise { @@ -574,14 +597,18 @@ export default class Transformation { newPipeline, currentPipeline, }: {| - filePath: string, + filePath: ProjectPath, isSource: boolean, newType: string, newPipeline: ?string, currentPipeline: Pipeline, |}): Promise { - let nextFilePath = - filePath.slice(0, -path.extname(filePath).length) + '.' + newType; + let filePathRelative = fromProjectPathRelative(filePath); + let nextFilePath = toProjectPathUnsafe( + filePathRelative.slice(0, -path.extname(filePathRelative).length) + + '.' + + newType, + ); let nextPipeline = await this.loadPipeline( nextFilePath, isSource, @@ -596,7 +623,7 @@ export default class Transformation { } async loadTransformerConfig( - filePath: FilePath, + filePath: ProjectPath, transformer: LoadedPlugin>, isSource: boolean, ): Promise { @@ -627,37 +654,46 @@ export default class Transformation { transformer: Transformer, transformerName: string, preloadedConfig: ?Config, - ): Promise> { + ): Promise<$ReadOnlyArray> { const logger = new PluginLogger({origin: transformerName}); const resolve = async (from: FilePath, to: string): Promise => { - let result = nullthrows( - await pipeline.resolverRunner.resolve( - createDependency({ - env: asset.value.env, - specifier: to, - specifierType: 'esm', // ??? - sourcePath: from, - }), - ), + let result = await pipeline.resolverRunner.resolve( + createDependency(this.options.projectRoot, { + env: asset.value.env, + specifier: to, + specifierType: 'esm', // ??? + sourcePath: from, + }), ); if (result.invalidateOnFileCreate) { - this.invalidateOnFileCreate.push(...result.invalidateOnFileCreate); + this.invalidateOnFileCreate.push( + ...result.invalidateOnFileCreate.map(i => + invalidateOnFileCreateToInternal(this.options.projectRoot, i), + ), + ); } if (result.invalidateOnFileChange) { for (let filePath of result.invalidateOnFileChange) { let invalidation = { type: 'file', - filePath, + filePath: toProjectPath(this.options.projectRoot, filePath), }; this.invalidations.set(getInvalidationId(invalidation), invalidation); } } - return result.assetGroup.filePath; + if (result.diagnostics && result.diagnostics.length > 0) { + throw new ThrowableDiagnostic({diagnostic: result.diagnostics}); + } + + return fromProjectPath( + this.options.projectRoot, + nullthrows(result.assetGroup).filePath, + ); }; // If an ast exists on the asset, but we cannot reuse it, @@ -698,17 +734,16 @@ export default class Transformation { } // Transform. - let results = await normalizeAssets( - // $FlowFixMe + let transfomerResult: Array = + // $FlowFixMe the returned IMutableAsset really is a MutableAsset await transformer.transform({ asset: new MutableAsset(asset), - ast: asset.ast, config, options: pipeline.pluginOptions, resolve, logger, - }), - ); + }); + let results = await normalizeAssets(this.options, transfomerResult); // Create generate function that can be called later asset.generate = (): Promise => { @@ -748,18 +783,19 @@ type TransformerWithNameAndConfig = {| plugin: Transformer, config: ?Config, configKeyPath?: string, - resolveFrom: FilePath, + resolveFrom: ProjectPath, range?: ?SemverRange, |}; -function normalizeAssets(results: Array) { - return Promise.all( - results.map(result => { - if (result instanceof MutableAsset) { - return mutableAssetToUncommittedAsset(result); - } +function normalizeAssets( + options, + results: Array, +): Array { + return results.map(result => { + if (result instanceof MutableAsset) { + return mutableAssetToUncommittedAsset(result); + } - return result; - }), - ); + return result; + }); } diff --git a/packages/core/core/src/UncommittedAsset.js b/packages/core/core/src/UncommittedAsset.js index e11cb706afe..ec199e6ca68 100644 --- a/packages/core/core/src/UncommittedAsset.js +++ b/packages/core/core/src/UncommittedAsset.js @@ -4,7 +4,6 @@ import type { AST, Blob, DependencyOptions, - FilePath, FileCreateInvalidation, GenerateOutput, PackageName, @@ -15,14 +14,15 @@ import type { RequestInvalidation, Dependency, ParcelOptions, + InternalFileCreateInvalidation, } from './types'; import invariant from 'assert'; import {Readable} from 'stream'; import SourceMap from '@parcel/source-map'; import { - bufferStream, blobToStream, + bufferStream, streamFromPromise, TapStream, loadSourceMap, @@ -40,6 +40,8 @@ import { getInvalidationHash, } from './assetUtils'; import {BundleBehaviorNames} from './types'; +import {invalidateOnFileCreateToInternal} from './utils'; +import {type ProjectPath, fromProjectPath} from './projectPath'; type UncommittedAssetOptions = {| value: Asset, @@ -50,7 +52,7 @@ type UncommittedAssetOptions = {| isASTDirty?: ?boolean, idBase?: ?string, invalidations?: Map, - fileCreateInvalidations?: Array, + fileCreateInvalidations?: Array, |}; export default class UncommittedAsset { @@ -64,7 +66,7 @@ export default class UncommittedAsset { isASTDirty: boolean; idBase: ?string; invalidations: Map; - fileCreateInvalidations: Array; + fileCreateInvalidations: Array; generate: ?() => Promise; constructor({ @@ -234,10 +236,14 @@ export default class UncommittedAsset { } let code = await this.getCode(); - let map = await loadSourceMap(this.value.filePath, code, { - fs: this.options.inputFS, - projectRoot: this.options.projectRoot, - }); + let map = await loadSourceMap( + fromProjectPath(this.options.projectRoot, this.value.filePath), + code, + { + fs: this.options.inputFS, + projectRoot: this.options.projectRoot, + }, + ); if (map) { this.map = map; @@ -257,7 +263,7 @@ export default class UncommittedAsset { let mapBuffer = this.mapBuffer ?? (await this.getMapBuffer()); if (mapBuffer) { // Get sourcemap from flatbuffer - this.map = new SourceMap(mapBuffer); + this.map = new SourceMap(this.options.projectRoot, mapBuffer); } } @@ -268,8 +274,13 @@ export default class UncommittedAsset { // If we have sourceContent available, it means this asset is source code without // a previous source map. Ensure that the map set by the transformer has the original // source content available. - if (map && this.sourceContent != null) { - map.setSourceContent(this.value.filePath, this.sourceContent); + if (map != null && this.sourceContent != null) { + map.setSourceContent( + fromProjectPath(this.options.projectRoot, this.value.filePath), + // $FlowFixMe + this.sourceContent, + ); + this.sourceContent = null; } this.map = map; @@ -304,13 +315,16 @@ export default class UncommittedAsset { addDependency(opts: DependencyOptions): string { // eslint-disable-next-line no-unused-vars let {env, symbols, ...rest} = opts; - let dep = createDependency({ + let dep = createDependency(this.options.projectRoot, { ...rest, // $FlowFixMe "convert" the $ReadOnlyMaps to the interal mutable one symbols, - env: mergeEnvironments(this.value.env, env), + env: mergeEnvironments(this.options.projectRoot, this.value.env, env), sourceAssetId: this.value.id, - sourcePath: this.value.filePath, + sourcePath: fromProjectPath( + this.options.projectRoot, + this.value.filePath, + ), }); let existing = this.value.dependencies.get(dep.id); if (existing) { @@ -321,7 +335,7 @@ export default class UncommittedAsset { return dep.id; } - invalidateOnFileChange(filePath: FilePath) { + invalidateOnFileChange(filePath: ProjectPath) { let invalidation: RequestInvalidation = { type: 'file', filePath, @@ -331,7 +345,9 @@ export default class UncommittedAsset { } invalidateOnFileCreate(invalidation: FileCreateInvalidation) { - this.fileCreateInvalidations.push(invalidation); + this.fileCreateInvalidations.push( + invalidateOnFileCreateToInternal(this.options.projectRoot, invalidation), + ); } invalidateOnEnvChange(key: string) { @@ -354,13 +370,13 @@ export default class UncommittedAsset { createChildAsset( result: TransformerResult, plugin: PackageName, - configPath: FilePath, + configPath: ProjectPath, configKeyPath?: string, ): UncommittedAsset { let content = result.content ?? null; let asset = new UncommittedAsset({ - value: createAsset({ + value: createAsset(this.options.projectRoot, { idBase: this.idBase, hash: this.value.hash, filePath: this.value.filePath, @@ -373,7 +389,11 @@ export default class UncommittedAsset { isBundleSplittable: result.isBundleSplittable ?? this.value.isBundleSplittable, isSource: this.value.isSource, - env: mergeEnvironments(this.value.env, result.env), + env: mergeEnvironments( + this.options.projectRoot, + this.value.env, + result.env, + ), dependencies: this.value.type === result.type ? new Map(this.value.dependencies) diff --git a/packages/core/core/src/Validation.js b/packages/core/core/src/Validation.js index 61bbe31391d..3dcdcbcb29e 100644 --- a/packages/core/core/src/Validation.js +++ b/packages/core/core/src/Validation.js @@ -6,7 +6,7 @@ import type {Validator, ValidateResult} from '@parcel/types'; import type {Diagnostic} from '@parcel/diagnostic'; import path from 'path'; -import {resolveConfig, normalizeSeparators} from '@parcel/utils'; +import {resolveConfig} from '@parcel/utils'; import logger, {PluginLogger} from '@parcel/logger'; import ThrowableDiagnostic, {errorToDiagnostic} from '@parcel/diagnostic'; import ParcelConfig from './ParcelConfig'; @@ -15,6 +15,7 @@ import {createAsset} from './assetUtils'; import {Asset} from './public/Asset'; import PluginOptions from './public/PluginOptions'; import summarizeRequest from './summarizeRequest'; +import {fromProjectPath, fromProjectPathRelative} from './projectPath'; export type ValidationOpts = {| config: ParcelConfig, @@ -91,16 +92,16 @@ export default class Validation { await Promise.all( assets.map(async input => { let config = null; - let asset = new Asset(input); + let publicAsset = new Asset(input); if (plugin.getConfig) { config = await plugin.getConfig({ - asset, + asset: publicAsset, options: pluginOptions, logger: validatorLogger, resolveConfig: (configNames: Array) => resolveConfig( this.options.inputFS, - input.value.filePath, + publicAsset.filePath, configNames, this.options.projectRoot, ), @@ -108,7 +109,7 @@ export default class Validation { } let validatorResult = await plugin.validate({ - asset, + asset: publicAsset, options: pluginOptions, config, logger: validatorLogger, @@ -136,7 +137,7 @@ export default class Validation { this.requests.map(async request => { this.report({ type: 'validation', - filePath: request.filePath, + filePath: fromProjectPath(this.options.projectRoot, request.filePath), }); let asset = await this.loadAsset(request); @@ -182,24 +183,21 @@ export default class Validation { let {filePath, env, code, sideEffects, query} = request; let {content, size, hash, isSource} = await summarizeRequest( this.options.inputFS, - {filePath: request.filePath}, + { + filePath: fromProjectPath(this.options.projectRoot, request.filePath), + }, ); // If the transformer request passed code rather than a filename, // use a hash as the base for the id to ensure it is unique. - let idBase = - code != null - ? hash - : normalizeSeparators( - path.relative(this.options.projectRoot, filePath), - ); + let idBase = code != null ? hash : fromProjectPathRelative(filePath); return new UncommittedAsset({ idBase, - value: createAsset({ + value: createAsset(this.options.projectRoot, { idBase, filePath: filePath, isSource, - type: path.extname(filePath).slice(1), + type: path.extname(fromProjectPathRelative(filePath)).slice(1), hash, query, env: env, diff --git a/packages/core/core/src/applyRuntimes.js b/packages/core/core/src/applyRuntimes.js index 90167f77105..6bc956b8bb9 100644 --- a/packages/core/core/src/applyRuntimes.js +++ b/packages/core/core/src/applyRuntimes.js @@ -29,6 +29,7 @@ import {dependencyToInternalDependency} from './public/Dependency'; import {mergeEnvironments} from './Environment'; import createAssetGraphRequest from './requests/AssetGraphRequest'; import {createDevDependency, runDevDepRequest} from './requests/DevDepRequest'; +import {toProjectPath, fromProjectPathRelative} from './projectPath'; type RuntimeConnection = {| bundle: InternalBundle, @@ -92,8 +93,8 @@ export default async function applyRuntimes({ let assetGroup = { code, - filePath: sourceName, - env: mergeEnvironments(bundle.env, env), + filePath: toProjectPath(options.projectRoot, sourceName), + env: mergeEnvironments(options.projectRoot, bundle.env, env), // Runtime assets should be considered source, as they should be // e.g. compiled to run in the target environment isSource: true, @@ -129,7 +130,9 @@ export default async function applyRuntimes({ options, ); devDepRequests.set( - `${devDepRequest.specifier}:${devDepRequest.resolveFrom}`, + `${devDepRequest.specifier}:${fromProjectPathRelative( + devDepRequest.resolveFrom, + )}`, devDepRequest, ); await runDevDepRequest(api, devDepRequest); diff --git a/packages/core/core/src/assetUtils.js b/packages/core/core/src/assetUtils.js index 2d69bbf030f..4ab3be355fc 100644 --- a/packages/core/core/src/assetUtils.js +++ b/packages/core/core/src/assetUtils.js @@ -31,8 +31,13 @@ import loadPlugin from './loadParcelPlugin'; import {Asset as PublicAsset} from './public/Asset'; import PluginOptions from './public/PluginOptions'; import {blobToStream, hashFile} from '@parcel/utils'; -import {hashFromOption} from './utils'; +import {hashFromOption, toInternalSourceLocation} from './utils'; import {createBuildCache} from './buildCache'; +import { + type ProjectPath, + fromProjectPath, + fromProjectPathRelative, +} from './projectPath'; import {hashString} from '@parcel/hash'; import {BundleBehavior as BundleBehaviorMap} from './types'; @@ -41,7 +46,7 @@ type AssetOptions = {| committed?: boolean, hash?: ?string, idBase?: ?string, - filePath: FilePath, + filePath: ProjectPath, query?: ?QueryParameters, type: string, contentKey?: ?string, @@ -61,13 +66,16 @@ type AssetOptions = {| sideEffects?: boolean, uniqueKey?: ?string, plugin?: PackageName, - configPath?: FilePath, + configPath?: ProjectPath, configKeyPath?: string, |}; export function createAssetIdFromOptions(options: AssetOptions): string { let uniqueKey = options.uniqueKey ?? ''; - let idBase = options.idBase != null ? options.idBase : options.filePath; + let idBase = + options.idBase != null + ? options.idBase + : fromProjectPathRelative(options.filePath); let queryString = options.query ? JSON.stringify(objectSortedEntries(options.query)) : ''; @@ -84,7 +92,10 @@ export function createAssetIdFromOptions(options: AssetOptions): string { ); } -export function createAsset(options: AssetOptions): Asset { +export function createAsset( + projectRoot: FilePath, + options: AssetOptions, +): Asset { return { id: options.id != null ? options.id : createAssetIdFromOptions(options), committed: options.committed ?? false, @@ -107,7 +118,18 @@ export function createAsset(options: AssetOptions): Asset { env: options.env, meta: options.meta || {}, stats: options.stats, - symbols: options.symbols, + symbols: + options.symbols && + new Map( + [...options.symbols].map(([k, v]) => [ + k, + { + local: v.local, + meta: v.meta, + loc: toInternalSourceLocation(projectRoot, v.loc), + }, + ]), + ), sideEffects: options.sideEffects ?? true, uniqueKey: options.uniqueKey ?? '', plugin: options.plugin, @@ -138,7 +160,10 @@ async function _generateFromAST(asset: CommittedAsset | UncommittedAsset) { let pluginName = nullthrows(asset.value.plugin); let {plugin} = await loadPlugin>( pluginName, - nullthrows(asset.value.configPath), + fromProjectPath( + asset.options.projectRoot, + nullthrows(asset.value.configPath), + ), nullthrows(asset.value.configKeyPath), asset.options, ); @@ -177,7 +202,7 @@ async function _generateFromAST(asset: CommittedAsset | UncommittedAsset) { export function getInvalidationId(invalidation: RequestInvalidation): string { switch (invalidation.type) { case 'file': - return 'file:' + invalidation.filePath; + return 'file:' + fromProjectPathRelative(invalidation.filePath); case 'env': return 'env:' + invalidation.key; case 'option': @@ -208,7 +233,10 @@ export async function getInvalidationHash( // Only recompute the hash of this file if we haven't seen it already during this build. let fileHash = hashCache.get(invalidation.filePath); if (fileHash == null) { - fileHash = hashFile(options.inputFS, invalidation.filePath); + fileHash = hashFile( + options.inputFS, + fromProjectPath(options.projectRoot, invalidation.filePath), + ); hashCache.set(invalidation.filePath, fileHash); } hashes += await fileHash; diff --git a/packages/core/core/src/dumpGraphToGraphViz.js b/packages/core/core/src/dumpGraphToGraphViz.js index 904476b057a..3c649816855 100644 --- a/packages/core/core/src/dumpGraphToGraphViz.js +++ b/packages/core/core/src/dumpGraphToGraphViz.js @@ -1,12 +1,11 @@ // @flow -import type {Environment} from './types'; - +import type {AssetGraphNode, BundleGraphNode, Environment} from './types'; import type Graph from './Graph'; -import type {AssetGraphNode, BundleGraphNode} from './types'; import {SpecifierType, Priority} from './types'; import path from 'path'; +import {fromProjectPathRelative} from './projectPath'; const COLORS = { root: 'gray', @@ -92,7 +91,10 @@ export default async function dumpGraphToGraphViz( } } } else if (node.type === 'asset') { - label += path.basename(node.value.filePath) + '#' + node.value.type; + label += + path.basename(fromProjectPathRelative(node.value.filePath)) + + '#' + + node.value.type; if (detailedSymbols) { if (!node.value.symbols) { label += '\\nsymbols: cleared'; @@ -118,8 +120,8 @@ export default async function dumpGraphToGraphViz( // $FlowFixMe } else if (node.type === 'bundle') { let parts = []; - if (node.value.isEntry) parts.push('entry'); - if (node.value.isInline) parts.push('inline'); + if (node.value.needsStableName) parts.push('stable name'); + if (node.value.bundleBehavior) parts.push(node.value.bundleBehavior); if (parts.length) label += ' (' + parts.join(', ') + ')'; if (node.value.env) label += ` (${getEnvDescription(node.value.env)})`; // $FlowFixMe diff --git a/packages/core/core/src/loadParcelPlugin.js b/packages/core/core/src/loadParcelPlugin.js index 6fb5b739345..8143a5450bf 100644 --- a/packages/core/core/src/loadParcelPlugin.js +++ b/packages/core/core/src/loadParcelPlugin.js @@ -2,9 +2,9 @@ import type {FilePath, PackageName, Semver, SemverRange} from '@parcel/types'; import type {ParcelOptions} from './types'; +import path from 'path'; import semver from 'semver'; import logger from '@parcel/logger'; -import {CONFIG} from '@parcel/plugin'; import nullthrows from 'nullthrows'; import ThrowableDiagnostic, { generateJSONCodeHighlights, @@ -12,13 +12,14 @@ import ThrowableDiagnostic, { } from '@parcel/diagnostic'; import { findAlternativeNodeModules, - resolveConfig, loadConfig, + resolveConfig, } from '@parcel/utils'; -import path from 'path'; +import {type ProjectPath, toProjectPath} from './projectPath'; import {version as PARCEL_VERSION} from '../package.json'; const NODE_MODULES = `${path.sep}node_modules${path.sep}`; +const CONFIG = Symbol.for('parcel-plugin-config'); export default async function loadPlugin( pluginName: PackageName, @@ -28,7 +29,7 @@ export default async function loadPlugin( ): Promise<{| plugin: T, version: Semver, - resolveFrom: FilePath, + resolveFrom: ProjectPath, range: ?SemverRange, |}> { let resolveFrom = configPath; @@ -60,22 +61,24 @@ export default async function loadPlugin( resolveFrom, )}. Either include it in "dependencies" or "parcelDependencies".`, origin: '@parcel/core', - filePath: configPkg.files[0].filePath, - language: 'json5', - codeFrame: + codeFrames: configPkg.config.dependencies || configPkg.config.parcelDependencies - ? { - code: contents, - codeHighlights: generateJSONCodeHighlights(contents, [ - { - key: configPkg.config.parcelDependencies - ? '/parcelDependencies' - : '/dependencies', - type: 'key', - }, - ]), - } + ? [ + { + filePath: configPkg.files[0].filePath, + language: 'json5', + code: contents, + codeHighlights: generateJSONCodeHighlights(contents, [ + { + key: configPkg.config.parcelDependencies + ? '/parcelDependencies' + : '/dependencies', + type: 'key', + }, + ]), + }, + ] : undefined, }, }); @@ -111,23 +114,25 @@ export default async function loadPlugin( diagnostic: { message: md`Cannot find Parcel plugin "${pluginName}"`, origin: '@parcel/core', - filePath: configPath, - language: 'json5', - codeFrame: keyPath - ? { - code: configContents, - codeHighlights: generateJSONCodeHighlights(configContents, [ - { - key: keyPath, - type: 'value', - message: md`Cannot find module "${pluginName}"${ - alternatives[0] - ? `, did you mean "${alternatives[0]}"?` - : '' - }`, - }, - ]), - } + codeFrames: keyPath + ? [ + { + filePath: configPath, + language: 'json5', + code: configContents, + codeHighlights: generateJSONCodeHighlights(configContents, [ + { + key: keyPath, + type: 'value', + message: md`Cannot find module "${pluginName}"${ + alternatives[0] + ? `, did you mean "${alternatives[0]}"?` + : '' + }`, + }, + ]), + }, + ] : undefined, }, }); @@ -159,16 +164,18 @@ export default async function loadPlugin( diagnostic: { message: md`The plugin "${pluginName}" is not compatible with the current version of Parcel. Requires "${parcelVersionRange}" but the current version is "${PARCEL_VERSION}".`, origin: '@parcel/core', - filePath: pkgFile, - language: 'json5', - codeFrame: { - code: pkgContents, - codeHighlights: generateJSONCodeHighlights(pkgContents, [ - { - key: '/engines/parcel', - }, - ]), - }, + codeFrames: [ + { + filePath: pkgFile, + language: 'json5', + code: pkgContents, + codeHighlights: generateJSONCodeHighlights(pkgContents, [ + { + key: '/engines/parcel', + }, + ]), + }, + ], }, }); } @@ -186,5 +193,10 @@ export default async function loadPlugin( `Plugin ${pluginName} is not a valid Parcel plugin, should export an instance of a Parcel plugin ex. "export default new Reporter({ ... })".`, ); } - return {plugin, version: nullthrows(pkg).version, resolveFrom, range}; + return { + plugin, + version: nullthrows(pkg).version, + resolveFrom: toProjectPath(options.projectRoot, resolveFrom), + range, + }; } diff --git a/packages/core/core/src/projectPath.js b/packages/core/core/src/projectPath.js new file mode 100644 index 00000000000..c6e1a2b85a9 --- /dev/null +++ b/packages/core/core/src/projectPath.js @@ -0,0 +1,83 @@ +// @flow strict-local +import type {FilePath} from '@parcel/types'; +import path from 'path'; +import {relativePath} from '@parcel/utils'; + +/** + * A path that's relative to the project root. + */ +export opaque type ProjectPath = string; + +function toProjectPath_(projectRoot: FilePath, p: FilePath): ProjectPath { + if (p == null) { + return p; + } + + // If the file is outside the project root, store an absolute path rather + // than a relative one. This way if the project root is moved, the file + // references still work. Accessing files outside the project root is not + // portable anyway. + let relative = relativePath(projectRoot, p, false); + return relative.startsWith('..') ? p : relative; +} + +export const toProjectPath: (( + projectRoot: FilePath, + p: FilePath, +) => ProjectPath) & + ((projectRoot: FilePath, p: FilePath | void) => ProjectPath | void) & + // $FlowFixMe Not sure how to type properly + ((projectRoot: FilePath, p: ?FilePath) => ?ProjectPath) = toProjectPath_; + +function fromProjectPath_(projectRoot: FilePath, p: ?ProjectPath): ?FilePath { + if (p == null) { + return null; + } + + // If the path is absolute (e.g. outside the project root), just return it. + if (path.isAbsolute(p)) { + return p; + } + + // Project paths use normalized unix separators, so we only need to + // convert them on Windows. + let projectPath = process.platform === 'win32' ? path.normalize(p) : p; + + // Add separator if needed. Doing this manunally is much faster than path.join. + if (projectRoot[projectRoot.length - 1] !== path.sep) { + return projectRoot + path.sep + projectPath; + } + + return projectRoot + projectPath; +} + +export const fromProjectPath: (( + projectRoot: FilePath, + p: ProjectPath, +) => FilePath) & + // $FlowFixMe Not sure how to type properly + ((projectRoot: FilePath, p: ?ProjectPath) => ?FilePath) = fromProjectPath_; + +/** + * Returns a path relative to the project root. This should be used when computing cache keys + */ +export function fromProjectPathRelative(p: ProjectPath): FilePath { + return p; +} + +/** + * This function should be avoided, it doesn't change the actual value. + */ +export function toProjectPathUnsafe(p: FilePath): ProjectPath { + return p; +} + +/** + * Joins a project root with relative paths (similar to `path.join`) + */ +export function joinProjectPath( + a: ProjectPath, + ...b: Array +): ProjectPath { + return path.join(a, ...b); +} diff --git a/packages/core/core/src/public/Asset.js b/packages/core/core/src/public/Asset.js index 9c9dbda23dc..d9bb86bec7d 100644 --- a/packages/core/core/src/public/Asset.js +++ b/packages/core/core/src/public/Asset.js @@ -31,10 +31,12 @@ import {AssetSymbols, MutableAssetSymbols} from './Symbols'; import UncommittedAsset from '../UncommittedAsset'; import CommittedAsset from '../CommittedAsset'; import {createEnvironment} from '../Environment'; +import {fromProjectPath, toProjectPath} from '../projectPath'; import { BundleBehavior as BundleBehaviorMap, BundleBehaviorNames, } from '../types'; +import {toInternalSourceLocation} from '../utils'; const inspect = Symbol.for('nodejs.util.inspect.custom'); @@ -101,7 +103,7 @@ class BaseAsset { } get env(): IEnvironment { - return new Environment(this.#asset.value.env); + return new Environment(this.#asset.value.env, this.#asset.options); } get fs(): FileSystem { @@ -109,7 +111,10 @@ class BaseAsset { } get filePath(): FilePath { - return this.#asset.value.filePath; + return fromProjectPath( + this.#asset.options.projectRoot, + this.#asset.value.filePath, + ); } get query(): QueryParameters { @@ -138,7 +143,7 @@ class BaseAsset { } get symbols(): IAssetSymbols { - return new AssetSymbols(this.#asset.value); + return new AssetSymbols(this.#asset.options, this.#asset.value); } get uniqueKey(): ?string { @@ -154,7 +159,9 @@ class BaseAsset { } getDependencies(): $ReadOnlyArray { - return this.#asset.getDependencies().map(dep => new Dependency(dep)); + return this.#asset + .getDependencies() + .map(dep => new Dependency(dep, this.#asset.options)); } getCode(): Promise { @@ -264,7 +271,7 @@ export class MutableAsset extends BaseAsset implements IMutableAsset { } get symbols(): IMutableAssetSymbols { - return new MutableAssetSymbols(this.#asset.value); + return new MutableAssetSymbols(this.#asset.options, this.#asset.value); } addDependency(dep: DependencyOptions): string { @@ -272,7 +279,9 @@ export class MutableAsset extends BaseAsset implements IMutableAsset { } invalidateOnFileChange(filePath: FilePath): void { - this.#asset.invalidateOnFileChange(filePath); + this.#asset.invalidateOnFileChange( + toProjectPath(this.#asset.options.projectRoot, filePath), + ); } invalidateOnFileCreate(invalidation: FileCreateInvalidation): void { @@ -313,7 +322,10 @@ export class MutableAsset extends BaseAsset implements IMutableAsset { } setEnvironment(env: EnvironmentOptions): void { - this.#asset.value.env = createEnvironment(env); + this.#asset.value.env = createEnvironment({ + ...env, + loc: toInternalSourceLocation(this.#asset.options.projectRoot, env.loc), + }); this.#asset.updateId(); } } diff --git a/packages/core/core/src/public/Bundle.js b/packages/core/core/src/public/Bundle.js index 8039c454718..c7a5f539e5f 100644 --- a/packages/core/core/src/public/Bundle.js +++ b/packages/core/core/src/public/Bundle.js @@ -11,11 +11,12 @@ import type { BundleTraversable, Dependency as IDependency, Environment as IEnvironment, + GraphVisitor, NamedBundle as INamedBundle, PackagedBundle as IPackagedBundle, Stats, Target as ITarget, - GraphVisitor, + BundleBehavior, } from '@parcel/types'; import type BundleGraph from '../BundleGraph'; @@ -28,6 +29,8 @@ import {mapVisitor} from '../Graph'; import Environment from './Environment'; import Dependency, {dependencyToInternalDependency} from './Dependency'; import Target from './Target'; +import {BundleBehaviorNames} from '../types'; +import {fromProjectPath} from '../projectPath'; const internalBundleToBundle: DefaultWeakMap< ParcelOptions, @@ -112,15 +115,16 @@ export class Bundle implements IBundle { } get env(): IEnvironment { - return new Environment(this.#bundle.env); + return new Environment(this.#bundle.env, this.#options); } - get isEntry(): ?boolean { - return this.#bundle.isEntry; + get needsStableName(): ?boolean { + return this.#bundle.needsStableName; } - get isInline(): ?boolean { - return this.#bundle.isInline; + get bundleBehavior(): ?BundleBehavior { + let bundleBehavior = this.#bundle.bundleBehavior; + return bundleBehavior != null ? BundleBehaviorNames[bundleBehavior] : null; } get isSplittable(): ?boolean { @@ -128,7 +132,7 @@ export class Bundle implements IBundle { } get target(): ITarget { - return new Target(this.#bundle.target); + return new Target(this.#bundle.target, this.#options); } hasAsset(asset: IAsset): boolean { @@ -175,7 +179,10 @@ export class Bundle implements IBundle { value: assetFromValue(node.value, this.#options), }; } else if (node.type === 'dependency') { - return {type: 'dependency', value: new Dependency(node.value)}; + return { + type: 'dependency', + value: new Dependency(node.value, this.#options), + }; } }, visit), ); @@ -192,6 +199,7 @@ export class Bundle implements IBundle { export class NamedBundle extends Bundle implements INamedBundle { #bundle /*: InternalBundle */; #bundleGraph /*: BundleGraph */; + #options /*: ParcelOptions */; constructor( sentinel: mixed, @@ -202,6 +210,7 @@ export class NamedBundle extends Bundle implements INamedBundle { super(sentinel, bundle, bundleGraph, options); this.#bundle = bundle; // Repeating for flow this.#bundleGraph = bundleGraph; // Repeating for flow + this.#options = options; } static get( @@ -244,6 +253,7 @@ export class NamedBundle extends Bundle implements INamedBundle { export class PackagedBundle extends NamedBundle implements IPackagedBundle { #bundle /*: InternalBundle */; #bundleGraph /*: BundleGraph */; + #options /*: ParcelOptions */; #bundleInfo /*: ?PackagedBundleInfo */; constructor( @@ -255,6 +265,7 @@ export class PackagedBundle extends NamedBundle implements IPackagedBundle { super(sentinel, bundle, bundleGraph, options); this.#bundle = bundle; // Repeating for flow this.#bundleGraph = bundleGraph; // Repeating for flow + this.#options = options; // Repeating for flow } static get( @@ -299,7 +310,10 @@ export class PackagedBundle extends NamedBundle implements IPackagedBundle { } get filePath(): string { - return nullthrows(this.#bundleInfo).filePath; + return fromProjectPath( + this.#options.projectRoot, + nullthrows(this.#bundleInfo).filePath, + ); } get stats(): Stats { diff --git a/packages/core/core/src/public/BundleGraph.js b/packages/core/core/src/public/BundleGraph.js index c561ab17a74..cd405e9e23a 100644 --- a/packages/core/core/src/public/BundleGraph.js +++ b/packages/core/core/src/public/BundleGraph.js @@ -22,6 +22,7 @@ import {assetFromValue, assetToAssetValue, Asset} from './Asset'; import {bundleToInternalBundle} from './Bundle'; import Dependency, {dependencyToInternalDependency} from './Dependency'; import {mapVisitor} from '../Graph'; +import {fromInternalSourceLocation} from '../utils'; // Friendly access for other modules within this package that need access // to the internal bundle. @@ -88,7 +89,7 @@ export default class BundleGraph getIncomingDependencies(asset: IAsset): Array { return this.#graph .getIncomingDependencies(assetToAssetValue(asset)) - .map(dep => new Dependency(dep)); + .map(dep => new Dependency(dep, this.#options)); } getAssetWithDependency(dep: IDependency): ?IAsset { @@ -163,7 +164,7 @@ export default class BundleGraph getDependencies(asset: IAsset): Array { return this.#graph .getDependencies(assetToAssetValue(asset)) - .map(dep => new Dependency(dep)); + .map(dep => new Dependency(dep, this.#options)); } isAssetReachableFromBundle(asset: IAsset, bundle: IBundle): boolean { @@ -250,7 +251,7 @@ export default class BundleGraph asset: assetFromValue(res.asset, this.#options), exportSymbol: res.exportSymbol, symbol: res.symbol, - loc: res.loc, + loc: fromInternalSourceLocation(this.#options.projectRoot, res.loc), }; } @@ -266,7 +267,7 @@ export default class BundleGraph asset: assetFromValue(e.asset, this.#options), exportSymbol: e.exportSymbol, symbol: e.symbol, - loc: e.loc, + loc: fromInternalSourceLocation(this.#options.projectRoot, e.loc), exportAs: e.exportAs, })); } @@ -281,7 +282,7 @@ export default class BundleGraph ? {type: 'asset', value: assetFromValue(node.value, this.#options)} : { type: 'dependency', - value: new Dependency(node.value), + value: new Dependency(node.value, this.#options), }, visit, ), diff --git a/packages/core/core/src/public/Config.js b/packages/core/core/src/public/Config.js index f803f8ed9d3..1b2eba4edb8 100644 --- a/packages/core/core/src/public/Config.js +++ b/packages/core/core/src/public/Config.js @@ -10,8 +10,10 @@ import type { } from '@parcel/types'; import type {Config, ParcelOptions} from '../types'; +import invariant from 'assert'; import {DefaultWeakMap, loadConfig} from '@parcel/utils'; import Environment from './Environment'; +import {fromProjectPath, toProjectPath} from '../projectPath'; const internalConfigToConfig: DefaultWeakMap< ParcelOptions, @@ -37,11 +39,11 @@ export default class PublicConfig implements IConfig { } get env(): Environment { - return new Environment(this.#config.env); + return new Environment(this.#config.env, this.#options); } get searchPath(): FilePath { - return this.#config.searchPath; + return fromProjectPath(this.#options.projectRoot, this.#config.searchPath); } get result(): ConfigResult { @@ -62,15 +64,44 @@ export default class PublicConfig implements IConfig { } invalidateOnFileChange(filePath: FilePath) { - this.#config.invalidateOnFileChange.add(filePath); + this.#config.invalidateOnFileChange.add( + toProjectPath(this.#options.projectRoot, filePath), + ); } addDevDependency(devDep: DevDepOptions) { - this.#config.devDeps.push(devDep); + this.#config.devDeps.push({ + ...devDep, + resolveFrom: toProjectPath(this.#options.projectRoot, devDep.resolveFrom), + additionalInvalidations: devDep.additionalInvalidations?.map(i => ({ + ...i, + resolveFrom: toProjectPath(this.#options.projectRoot, i.resolveFrom), + })), + }); } invalidateOnFileCreate(invalidation: FileCreateInvalidation) { - this.#config.invalidateOnFileCreate.push(invalidation); + if (invalidation.glob != null) { + // $FlowFixMe + this.#config.invalidateOnFileCreate.push(invalidation); + } else if (invalidation.filePath != null) { + this.#config.invalidateOnFileCreate.push({ + filePath: toProjectPath( + this.#options.projectRoot, + invalidation.filePath, + ), + }); + } else { + invariant(invalidation.aboveFilePath != null); + this.#config.invalidateOnFileCreate.push({ + // $FlowFixMe + fileName: invalidation.fileName, + aboveFilePath: toProjectPath( + this.#options.projectRoot, + invalidation.aboveFilePath, + ), + }); + } } invalidateOnEnvChange(env: string) { diff --git a/packages/core/core/src/public/Dependency.js b/packages/core/core/src/public/Dependency.js index 97f64142252..60cc46042d1 100644 --- a/packages/core/core/src/public/Dependency.js +++ b/packages/core/core/src/public/Dependency.js @@ -2,19 +2,24 @@ import type { Dependency as IDependency, Environment as IEnvironment, - SourceLocation, + FilePath, Meta, MutableDependencySymbols as IMutableDependencySymbols, + SourceLocation, SpecifierType, DependencyPriority, + BundleBehavior, } from '@parcel/types'; -import type {Dependency as InternalDependency} from '../types'; +import type {Dependency as InternalDependency, ParcelOptions} from '../types'; +import {BundleBehaviorNames} from '../types'; +import nullthrows from 'nullthrows'; import Environment from './Environment'; import Target from './Target'; import {MutableDependencySymbols} from './Symbols'; -import nullthrows from 'nullthrows'; import {SpecifierType as SpecifierTypeMap, Priority} from '../types'; +import {fromProjectPath} from '../projectPath'; +import {fromInternalSourceLocation} from '../utils'; const SpecifierTypeNames = Object.keys(SpecifierTypeMap); const PriorityNames = Object.keys(Priority); @@ -37,14 +42,16 @@ export function dependencyToInternalDependency( export default class Dependency implements IDependency { #dep /*: InternalDependency */; + #options /*: ParcelOptions */; - constructor(dep: InternalDependency): Dependency { + constructor(dep: InternalDependency, options: ParcelOptions): Dependency { let existing = internalDependencyToDependency.get(dep); if (existing != null) { return existing; } this.#dep = dep; + this.#options = options; _dependencyToInternalDependency.set(this, dep); internalDependencyToDependency.set(dep, this); return this; @@ -75,6 +82,11 @@ export default class Dependency implements IDependency { return this.#dep.needsStableName; } + get bundleBehavior(): ?BundleBehavior { + let bundleBehavior = this.#dep.bundleBehavior; + return bundleBehavior == null ? null : BundleBehaviorNames[bundleBehavior]; + } + get isEntry(): boolean { return this.#dep.isEntry; } @@ -84,11 +96,11 @@ export default class Dependency implements IDependency { } get loc(): ?SourceLocation { - return this.#dep.loc; + return fromInternalSourceLocation(this.#options.projectRoot, this.#dep.loc); } get env(): IEnvironment { - return new Environment(this.#dep.env); + return new Environment(this.#dep.env, this.#options); } get meta(): Meta { @@ -96,12 +108,12 @@ export default class Dependency implements IDependency { } get symbols(): IMutableDependencySymbols { - return new MutableDependencySymbols(this.#dep); + return new MutableDependencySymbols(this.#options, this.#dep); } get target(): ?Target { let target = this.#dep.target; - return target ? new Target(target) : null; + return target ? new Target(target, this.#options) : null; } get sourceAssetId(): ?string { @@ -109,9 +121,9 @@ export default class Dependency implements IDependency { return this.#dep.sourceAssetId; } - get sourcePath(): ?string { + get sourcePath(): ?FilePath { // TODO: does this need to be public? - return this.#dep.sourcePath; + return fromProjectPath(this.#options.projectRoot, this.#dep.sourcePath); } get sourceAssetType(): ?string { @@ -119,7 +131,10 @@ export default class Dependency implements IDependency { } get resolveFrom(): ?string { - return this.#dep.resolveFrom ?? this.#dep.sourcePath; + return fromProjectPath( + this.#options.projectRoot, + this.#dep.resolveFrom ?? this.#dep.sourcePath, + ); } get pipeline(): ?string { diff --git a/packages/core/core/src/public/Environment.js b/packages/core/core/src/public/Environment.js index 0338059fdca..d40f277f10b 100644 --- a/packages/core/core/src/public/Environment.js +++ b/packages/core/core/src/public/Environment.js @@ -11,21 +11,23 @@ import type { SourceType, TargetSourceMapOptions, } from '@parcel/types'; -import type {Environment as InternalEnvironment} from '../types'; +import type {Environment as InternalEnvironment, ParcelOptions} from '../types'; import nullthrows from 'nullthrows'; import browserslist from 'browserslist'; import semver from 'semver'; +import {fromInternalSourceLocation} from '../utils'; export const BROWSER_ENVS: Set = new Set([ 'browser', 'web-worker', 'service-worker', + 'worklet', 'electron-renderer', ]); const ELECTRON_ENVS = new Set(['electron-main', 'electron-renderer']); const NODE_ENVS = new Set(['node', ...ELECTRON_ENVS]); const WORKER_ENVS = new Set(['web-worker', 'service-worker']); -const ISOLATED_ENVS = WORKER_ENVS; +export const ISOLATED_ENVS: Set = new Set([...WORKER_ENVS, 'worklet']); const ALL_BROWSERS = [ 'chrome', @@ -83,6 +85,18 @@ const supportData = { 'service-worker-module': { // TODO: Safari 14.1?? }, + 'import-meta-url': { + edge: '79', + firefox: '62', + chrome: '64', + safari: '11.1', + opera: '51', + ios: '12', + android: '64', + and_chr: '64', + and_ff: '62', + samsung: '9.2', + }, }; const internalEnvironmentToEnvironment: WeakMap< @@ -101,14 +115,16 @@ export function environmentToInternalEnvironment( export default class Environment implements IEnvironment { #environment /*: InternalEnvironment */; + #options /*: ParcelOptions */; - constructor(env: InternalEnvironment): Environment { + constructor(env: InternalEnvironment, options: ParcelOptions): Environment { let existing = internalEnvironmentToEnvironment.get(env); if (existing != null) { return existing; } this.#environment = env; + this.#options = options; _environmentToInternalEnvironment.set(this, env); internalEnvironmentToEnvironment.set(env, this); return this; @@ -158,7 +174,10 @@ export default class Environment implements IEnvironment { } get loc(): ?SourceLocation { - return this.#environment.loc; + return fromInternalSourceLocation( + this.#options.projectRoot, + this.#environment.loc, + ); } isBrowser(): boolean { @@ -181,6 +200,10 @@ export default class Environment implements IEnvironment { return WORKER_ENVS.has(this.#environment.context); } + isWorklet(): boolean { + return this.#environment.context === 'worklet'; + } + matchesEngines( minVersions: VersionMap, defaultValue?: boolean = false, diff --git a/packages/core/core/src/public/MutableBundleGraph.js b/packages/core/core/src/public/MutableBundleGraph.js index 051d43869c9..af3d5d10351 100644 --- a/packages/core/core/src/public/MutableBundleGraph.js +++ b/packages/core/core/src/public/MutableBundleGraph.js @@ -12,7 +12,6 @@ import type { import type {ParcelOptions} from '../types'; import invariant from 'assert'; -import path from 'path'; import nullthrows from 'nullthrows'; import {hashString} from '@parcel/hash'; import BundleGraph from './BundleGraph'; @@ -24,6 +23,7 @@ import Dependency, {dependencyToInternalDependency} from './Dependency'; import {environmentToInternalEnvironment} from './Environment'; import {targetToInternalTarget} from './Target'; import {HASH_REF_PREFIX} from '../constants'; +import {fromProjectPathRelative} from '../projectPath'; import {BundleBehavior} from '../types'; export default class MutableBundleGraph extends BundleGraph @@ -47,7 +47,7 @@ export default class MutableBundleGraph extends BundleGraph assetToAssetValue(asset), bundleToInternalBundle(bundle), shouldSkipDependency - ? d => shouldSkipDependency(new Dependency(d)) + ? d => shouldSkipDependency(new Dependency(d, this.#options)) : undefined, ); } @@ -61,7 +61,7 @@ export default class MutableBundleGraph extends BundleGraph assetToAssetValue(asset), bundleToInternalBundle(bundle), shouldSkipDependency - ? d => shouldSkipDependency(new Dependency(d)) + ? d => shouldSkipDependency(new Dependency(d, this.#options)) : undefined, ); } @@ -72,6 +72,8 @@ export default class MutableBundleGraph extends BundleGraph throw new Error('Dependency not found'); } + invariant(dependencyNode.type === 'dependency'); + let resolved = this.#graph.getDependencyResolution( dependencyToInternalDependency(dependency), ); @@ -86,16 +88,15 @@ export default class MutableBundleGraph extends BundleGraph entryAssetId: resolved.id, }; - let bundleGroupNode = { - id: getBundleGroupId(bundleGroup), - type: 'bundle_group', - value: bundleGroup, - }; + let bundleGroupKey = getBundleGroupId(bundleGroup); + let bundleGroupNodeId = this.#graph._graph.hasContentKey(bundleGroupKey) + ? this.#graph._graph.getNodeIdByContentKey(bundleGroupKey) + : this.#graph._graph.addNodeByContentKey(bundleGroupKey, { + id: bundleGroupKey, + type: 'bundle_group', + value: bundleGroup, + }); - let bundleGroupNodeId = this.#graph._graph.addNodeByContentKey( - bundleGroupNode.id, - bundleGroupNode, - ); let dependencyNodeId = this.#graph._graph.getNodeIdByContentKey( dependencyNode.id, ); @@ -112,10 +113,7 @@ export default class MutableBundleGraph extends BundleGraph ); this.#graph._graph.removeEdge(dependencyNodeId, resolvedNodeId); - if ( - dependency.isEntry || - resolved.bundleBehavior === BundleBehavior.isolated - ) { + if (dependency.isEntry) { this.#graph._graph.addEdge( nullthrows(this.#graph._graph.rootNodeId), bundleGroupNodeId, @@ -160,8 +158,8 @@ export default class MutableBundleGraph extends BundleGraph let target = targetToInternalTarget(opts.target); let bundleId = hashString( 'bundle:' + - (opts.uniqueKey ?? nullthrows(entryAsset?.id)) + - path.relative(this.#options.projectRoot, target.distDir), + (opts.entryAsset ? opts.entryAsset.id : opts.uniqueKey) + + fromProjectPathRelative(target.distDir), ); let existing = this.#graph._graph.getNodeByContentKey(bundleId); @@ -190,16 +188,21 @@ export default class MutableBundleGraph extends BundleGraph value: { id: bundleId, hashReference: HASH_REF_PREFIX + bundleId, - type: opts.type ?? nullthrows(entryAsset).type, + type: opts.entryAsset ? opts.entryAsset.type : opts.type, env: opts.env ? environmentToInternalEnvironment(opts.env) : nullthrows(entryAsset).env, entryAssetIds: entryAsset ? [entryAsset.id] : [], mainEntryId: entryAsset?.id, - pipeline: opts.pipeline ?? entryAsset?.pipeline, - isEntry: opts.isEntry, - isInline: opts.isInline, - isSplittable: opts.isSplittable ?? entryAsset?.isBundleSplittable, + pipeline: opts.entryAsset ? opts.entryAsset.pipeline : opts.pipeline, + needsStableName: opts.needsStableName, + bundleBehavior: + opts.bundleBehavior != null + ? BundleBehavior[opts.bundleBehavior] + : null, + isSplittable: opts.entryAsset + ? opts.entryAsset.isBundleSplittable + : opts.isSplittable, isPlaceholder, target, name: null, diff --git a/packages/core/core/src/public/PluginOptions.js b/packages/core/core/src/public/PluginOptions.js index 61e21b3f205..eae70e13a59 100644 --- a/packages/core/core/src/public/PluginOptions.js +++ b/packages/core/core/src/public/PluginOptions.js @@ -13,6 +13,8 @@ import type {FileSystem} from '@parcel/fs'; import type {PackageManager} from '@parcel/package-manager'; import type {ParcelOptions} from '../types'; +import {fromProjectPath} from '../projectPath'; + let parcelOptionsToPluginOptions: WeakMap< ParcelOptions, PluginOptions, @@ -65,7 +67,7 @@ export default class PluginOptions implements IPluginOptions { } get entryRoot(): FilePath { - return this.#options.entryRoot; + return fromProjectPath(this.#options.projectRoot, this.#options.entryRoot); } get cacheDir(): FilePath { diff --git a/packages/core/core/src/public/Symbols.js b/packages/core/core/src/public/Symbols.js index 315f82b96b4..32ae9ca92be 100644 --- a/packages/core/core/src/public/Symbols.js +++ b/packages/core/core/src/public/Symbols.js @@ -7,9 +7,10 @@ import type { SourceLocation, Meta, } from '@parcel/types'; -import type {Asset, Dependency} from '../types'; +import type {Asset, Dependency, ParcelOptions} from '../types'; import nullthrows from 'nullthrows'; +import {fromInternalSourceLocation, toInternalSourceLocation} from '../utils'; const EMPTY_ITERABLE = { [Symbol.iterator]() { @@ -32,14 +33,16 @@ export class AssetSymbols implements IAssetSymbols { @@iterator(): Iterator<[ISymbol, {|local: ISymbol, loc: ?SourceLocation, meta?: ?Meta|}]> { return ({}: any); } */ #value: Asset; + #options: ParcelOptions; - constructor(asset: Asset): AssetSymbols { + constructor(options: ParcelOptions, asset: Asset): AssetSymbols { let existing = valueToSymbols.get(asset); if (existing != null) { return existing; } this.#value = asset; + this.#options = options; valueToSymbols.set(asset, this); return this; } @@ -61,7 +64,10 @@ export class AssetSymbols implements IAssetSymbols { get( exportSymbol: ISymbol, ): ?{|local: ISymbol, loc: ?SourceLocation, meta?: ?Meta|} { - return this.#value.symbols?.get(exportSymbol); + return fromInternalAssetSymbol( + this.#options.projectRoot, + this.#value.symbols?.get(exportSymbol), + ); } get isCleared(): boolean { @@ -100,13 +106,15 @@ export class MutableAssetSymbols implements IMutableAssetSymbols { @@iterator(): Iterator<[ISymbol, {|local: ISymbol, loc: ?SourceLocation, meta?: ?Meta|}]> { return ({}: any); } */ #value: Asset; + #options: ParcelOptions; - constructor(asset: Asset): MutableAssetSymbols { + constructor(options: ParcelOptions, asset: Asset): MutableAssetSymbols { let existing = valueToMutableAssetSymbols.get(asset); if (existing != null) { return existing; } this.#value = asset; + this.#options = options; return this; } @@ -129,7 +137,10 @@ export class MutableAssetSymbols implements IMutableAssetSymbols { get( exportSymbol: ISymbol, ): ?{|local: ISymbol, loc: ?SourceLocation, meta?: ?Meta|} { - return nullthrows(this.#value.symbols).get(exportSymbol); + return fromInternalAssetSymbol( + this.#options.projectRoot, + this.#value.symbols?.get(exportSymbol), + ); } get isCleared(): boolean { @@ -172,7 +183,11 @@ export class MutableAssetSymbols implements IMutableAssetSymbols { loc: ?SourceLocation, meta: ?Meta, ) { - nullthrows(this.#value.symbols).set(exportSymbol, {local, loc, meta}); + nullthrows(this.#value.symbols).set(exportSymbol, { + local, + loc: toInternalSourceLocation(this.#options.projectRoot, loc), + meta, + }); } delete(exportSymbol: ISymbol) { @@ -189,13 +204,18 @@ export class MutableDependencySymbols implements IMutableDependencySymbols { @@iterator(): Iterator<[ISymbol, {|local: ISymbol, loc: ?SourceLocation, isWeak: boolean, meta?: ?Meta|}]> { return ({}: any); } */ #value: Dependency; + #options: ParcelOptions; - constructor(dep: Dependency): MutableDependencySymbols { + constructor( + options: ParcelOptions, + dep: Dependency, + ): MutableDependencySymbols { let existing = valueToMutableDependencySymbols.get(dep); if (existing != null) { return existing; } this.#value = dep; + this.#options = options; return this; } @@ -217,7 +237,10 @@ export class MutableDependencySymbols implements IMutableDependencySymbols { get( exportSymbol: ISymbol, ): ?{|local: ISymbol, loc: ?SourceLocation, isWeak: boolean, meta?: ?Meta|} { - return nullthrows(this.#value.symbols).get(exportSymbol); + return fromInternalDependencySymbol( + this.#options.projectRoot, + nullthrows(this.#value.symbols).get(exportSymbol), + ); } get isCleared(): boolean { @@ -264,7 +287,7 @@ export class MutableDependencySymbols implements IMutableDependencySymbols { let symbols = nullthrows(this.#value.symbols); symbols.set(exportSymbol, { local, - loc, + loc: toInternalSourceLocation(this.#options.projectRoot, loc), isWeak: (symbols.get(exportSymbol)?.isWeak ?? true) && (isWeak ?? false), }); } @@ -273,3 +296,24 @@ export class MutableDependencySymbols implements IMutableDependencySymbols { nullthrows(this.#value.symbols).delete(exportSymbol); } } + +function fromInternalAssetSymbol(projectRoot: string, value) { + return ( + value && { + local: value.local, + meta: value.meta, + loc: fromInternalSourceLocation(projectRoot, value.loc), + } + ); +} + +function fromInternalDependencySymbol(projectRoot: string, value) { + return ( + value && { + local: value.local, + meta: value.meta, + isWeak: value.isWeak, + loc: fromInternalSourceLocation(projectRoot, value.loc), + } + ); +} diff --git a/packages/core/core/src/public/Target.js b/packages/core/core/src/public/Target.js index a04635737c5..18ab4e7c5e7 100644 --- a/packages/core/core/src/public/Target.js +++ b/packages/core/core/src/public/Target.js @@ -5,9 +5,12 @@ import type { Environment as IEnvironment, SourceLocation, } from '@parcel/types'; -import type {Target as TargetValue} from '../types'; -import Environment from './Environment'; +import type {Target as TargetValue, ParcelOptions} from '../types'; + import nullthrows from 'nullthrows'; +import Environment from './Environment'; +import {fromProjectPath} from '../projectPath'; +import {fromInternalSourceLocation} from '../utils'; const internalTargetToTarget: WeakMap = new WeakMap(); const _targetToInternalTarget: WeakMap = new WeakMap(); @@ -17,14 +20,16 @@ export function targetToInternalTarget(target: ITarget): TargetValue { export default class Target implements ITarget { #target /*: TargetValue */; + #options /*: ParcelOptions */; - constructor(target: TargetValue): Target { + constructor(target: TargetValue, options: ParcelOptions): Target { let existing = internalTargetToTarget.get(target); if (existing != null) { return existing; } this.#target = target; + this.#options = options; _targetToInternalTarget.set(this, target); internalTargetToTarget.set(target, this); return this; @@ -35,11 +40,11 @@ export default class Target implements ITarget { } get distDir(): FilePath { - return this.#target.distDir; + return fromProjectPath(this.#options.projectRoot, this.#target.distDir); } get env(): IEnvironment { - return new Environment(this.#target.env); + return new Environment(this.#target.env, this.#options); } get name(): string { @@ -51,6 +56,9 @@ export default class Target implements ITarget { } get loc(): ?SourceLocation { - return this.#target.loc; + return fromInternalSourceLocation( + this.#options.projectRoot, + this.#target.loc, + ); } } diff --git a/packages/core/core/src/requests/AssetGraphRequest.js b/packages/core/core/src/requests/AssetGraphRequest.js index 656fbda2fe5..82b17ce4a2f 100644 --- a/packages/core/core/src/requests/AssetGraphRequest.js +++ b/packages/core/core/src/requests/AssetGraphRequest.js @@ -1,13 +1,6 @@ // @flow strict-local -import type { - Async, - FilePath, - DependencySpecifier, - Symbol, - SourceLocation, - Meta, -} from '@parcel/types'; +import type {Async, Symbol, Meta} from '@parcel/types'; import type {SharedReference} from '@parcel/workers'; import type {Diagnostic} from '@parcel/diagnostic'; import type { @@ -18,6 +11,7 @@ import type { Dependency, DependencyNode, Entry, + InternalSourceLocation, NodeId, ParcelOptions, Target, @@ -28,7 +22,6 @@ import type {PathRequestInput} from './PathRequest'; import invariant from 'assert'; import nullthrows from 'nullthrows'; -import path from 'path'; import {PromiseQueue} from '@parcel/utils'; import {hashString} from '@parcel/hash'; import ThrowableDiagnostic, {md} from '@parcel/diagnostic'; @@ -39,11 +32,16 @@ import createEntryRequest from './EntryRequest'; import createTargetRequest from './TargetRequest'; import createAssetRequest from './AssetRequest'; import createPathRequest from './PathRequest'; +import { + type ProjectPath, + fromProjectPathRelative, + fromProjectPath, +} from '../projectPath'; import dumpToGraphViz from '../dumpGraphToGraphViz'; type AssetGraphRequestInput = {| - entries?: Array, + entries?: Array, assetGroups?: Array, optionsRef: SharedReference, name: string, @@ -264,7 +262,7 @@ export class AssetGraphBuilder { // exportSymbol -> identifier let assetSymbols: $ReadOnlyMap< Symbol, - {|local: Symbol, loc: ?SourceLocation, meta?: ?Meta|}, + {|local: Symbol, loc: ?InternalSourceLocation, meta?: ?Meta|}, > = assetNode.value.symbols; // identifier -> exportSymbol let assetSymbolsInverse; @@ -406,7 +404,7 @@ export class AssetGraphBuilder { let assetSymbols: ?$ReadOnlyMap< Symbol, - {|local: Symbol, loc: ?SourceLocation, meta?: ?Meta|}, + {|local: Symbol, loc: ?InternalSourceLocation, meta?: ?Meta|}, > = assetNode.value.symbols; let assetSymbolsInverse = null; @@ -461,7 +459,7 @@ export class AssetGraphBuilder { } } - let errors = []; + let errors: Array = []; for (let incomingDep of incomingDeps) { let incomingDepUsedSymbolsUpOld = incomingDep.usedSymbolsUp; @@ -489,22 +487,27 @@ export class AssetGraphBuilder { invariant(resolution && resolution.type === 'asset_group'); errors.push({ - message: md`${path.relative( - this.options.projectRoot, + message: md`${fromProjectPathRelative( resolution.value.filePath, )} does not export '${s}'`, origin: '@parcel/core', - filePath: loc?.filePath, - language: assetNode.value.type, - codeFrame: loc - ? { - codeHighlights: [ - { - start: loc.start, - end: loc.end, - }, - ], - } + codeFrames: loc + ? [ + { + filePath: + fromProjectPath( + this.options.projectRoot, + loc?.filePath, + ) ?? undefined, + language: assetNode.value.type, + codeHighlights: [ + { + start: loc.start, + end: loc.end, + }, + ], + }, + ] : undefined, }); } @@ -777,9 +780,9 @@ export class AssetGraphBuilder { ); } - async runEntryRequest(input: DependencySpecifier) { + async runEntryRequest(input: ProjectPath) { let request = createEntryRequest(input); - let result = await this.api.runRequest(request, { + let result = await this.api.runRequest(request, { force: true, }); this.assetGraph.resolveEntry(request.input, result.entries, request.id); diff --git a/packages/core/core/src/requests/AssetRequest.js b/packages/core/core/src/requests/AssetRequest.js index 5b251117ef4..76d2323c6dc 100644 --- a/packages/core/core/src/requests/AssetRequest.js +++ b/packages/core/core/src/requests/AssetRequest.js @@ -18,6 +18,7 @@ import {hashString} from '@parcel/hash'; import createParcelConfigRequest from './ParcelConfigRequest'; import {runDevDepRequest} from './DevDepRequest'; import {runConfigRequest} from './ConfigRequest'; +import {fromProjectPath, fromProjectPathRelative} from '../projectPath'; import {report} from '../ReporterRunner'; type RunInput = {| @@ -50,7 +51,7 @@ function getId(input: AssetRequestInput) { let {optionsRef, ...hashInput} = input; return hashString( type + - input.filePath + + fromProjectPathRelative(input.filePath) + input.env.id + String(input.isSource) + String(input.sideEffects) + @@ -62,11 +63,11 @@ function getId(input: AssetRequestInput) { ); } -async function run({input, api, farm, invalidateReason}: RunInput) { +async function run({input, api, farm, invalidateReason, options}: RunInput) { report({ type: 'buildProgress', phase: 'transforming', - filePath: input.filePath, + filePath: fromProjectPath(options.projectRoot, input.filePath), }); api.invalidateOnFileUpdate(input.filePath); @@ -102,7 +103,10 @@ async function run({input, api, farm, invalidateReason}: RunInput) { devDeps: new Map( [...previousDevDepRequests.entries()] .filter(([id]) => api.canSkipSubrequest(id)) - .map(([, req]) => [`${req.specifier}:${req.resolveFrom}`, req.hash]), + .map(([, req]) => [ + `${req.specifier}:${fromProjectPathRelative(req.resolveFrom)}`, + req.hash, + ]), ), invalidDevDeps: await Promise.all( [...previousDevDepRequests.entries()] @@ -113,7 +117,10 @@ async function run({input, api, farm, invalidateReason}: RunInput) { specifier: req.specifier, resolveFrom: req.resolveFrom, }, - ...(req.additionalInvalidations ?? []), + ...(req.additionalInvalidations ?? []).map(i => ({ + specifier: i.specifier, + resolveFrom: i.resolveFrom, + })), ]; }), ), diff --git a/packages/core/core/src/requests/BundleGraphRequest.js b/packages/core/core/src/requests/BundleGraphRequest.js index 676d85a1ded..f9f15d656ad 100644 --- a/packages/core/core/src/requests/BundleGraphRequest.js +++ b/packages/core/core/src/requests/BundleGraphRequest.js @@ -13,7 +13,6 @@ import type { import type {ConfigAndCachePath} from './ParcelConfigRequest'; import assert from 'assert'; -import path from 'path'; import nullthrows from 'nullthrows'; import {PluginLogger} from '@parcel/logger'; import ThrowableDiagnostic, {errorToDiagnostic} from '@parcel/diagnostic'; @@ -48,6 +47,11 @@ import { type PluginWithLoadConfig, } from './ConfigRequest'; import {cacheSerializedObject, deserializeToCache} from '../serializer'; +import { + joinProjectPath, + fromProjectPathRelative, + toProjectPathUnsafe, +} from '../projectPath'; type BundleGraphRequestInput = {| assetGraph: AssetGraph, @@ -136,7 +140,7 @@ class BundlerRunner { async loadConfig(plugin: LoadedPlugin) { let config = createConfig({ plugin: plugin.name, - searchPath: path.join(this.options.projectRoot, 'index'), + searchPath: toProjectPathUnsafe('index'), }); await loadPluginConfig(plugin, config, this.options); @@ -156,7 +160,7 @@ class BundlerRunner { async runDevDepRequest(devDepRequest: DevDepRequest) { let {specifier, resolveFrom} = devDepRequest; - let key = `${specifier}:${resolveFrom}`; + let key = `${specifier}:${fromProjectPathRelative(resolveFrom)}`; this.devDepRequests.set(key, devDepRequest); await runDevDepRequest(this.api, devDepRequest); } @@ -347,7 +351,7 @@ class BundlerRunner { } let bundleNames = bundles.map(b => - path.join(b.target.distDir, nullthrows(b.name)), + joinProjectPath(b.target.distDir, nullthrows(b.name)), ); assert.deepEqual( bundleNames, diff --git a/packages/core/core/src/requests/ConfigRequest.js b/packages/core/core/src/requests/ConfigRequest.js index c398df9bf8b..86861d2c7bd 100644 --- a/packages/core/core/src/requests/ConfigRequest.js +++ b/packages/core/core/src/requests/ConfigRequest.js @@ -2,13 +2,17 @@ import type { Async, Config as IConfig, - FilePath, - FileCreateInvalidation, PluginOptions as IPluginOptions, + PluginLogger as IPluginLogger, } from '@parcel/types'; -import type {Config, ParcelOptions} from '../types'; +import type { + Config, + ParcelOptions, + InternalFileCreateInvalidation, +} from '../types'; import type {LoadedPlugin} from '../ParcelConfig'; import type {RunAPI} from '../RequestTracker'; +import type {ProjectPath} from '../projectPath'; import {serializeRaw} from '../serializer.js'; import {PluginLogger} from '@parcel/logger'; @@ -23,15 +27,15 @@ export type PluginWithLoadConfig = { loadConfig?: ({| config: IConfig, options: IPluginOptions, - logger: PluginLogger, + logger: IPluginLogger, |}) => Async, ... }; export type ConfigRequest = { id: string, - invalidateOnFileChange: Set, - invalidateOnFileCreate: Array, + invalidateOnFileChange: Set, + invalidateOnFileCreate: Array, invalidateOnEnvChange: Set, invalidateOnOptionChange: Set, invalidateOnStartup: boolean, diff --git a/packages/core/core/src/requests/DevDepRequest.js b/packages/core/core/src/requests/DevDepRequest.js index db9793dcec0..cd1e63b306f 100644 --- a/packages/core/core/src/requests/DevDepRequest.js +++ b/packages/core/core/src/requests/DevDepRequest.js @@ -1,21 +1,38 @@ // @flow -import type {DevDepOptions, DependencySpecifier, FilePath} from '@parcel/types'; +import type {DependencySpecifier} from '@parcel/types'; import type ParcelConfig from '../ParcelConfig'; -import type {DevDepRequest, ParcelOptions} from '../types'; +import type { + DevDepRequest, + ParcelOptions, + InternalDevDepOptions, +} from '../types'; import type {RunAPI} from '../RequestTracker'; +import type {ProjectPath} from '../projectPath'; import nullthrows from 'nullthrows'; import {getInvalidationHash} from '../assetUtils'; import {createBuildCache} from '../buildCache'; +import {invalidateOnFileCreateToInternal} from '../utils'; +import { + fromProjectPath, + fromProjectPathRelative, + toProjectPath, +} from '../projectPath'; + +// A cache of dev dep requests keyed by invalidations. +// If the package manager returns the same invalidation object, then +// we can reuse the dev dep request rather than recomputing the project +// paths and hashes. +const devDepRequestCache = new WeakMap(); export async function createDevDependency( - opts: DevDepOptions, - plugin: {name: DependencySpecifier, resolveFrom: FilePath, ...}, + opts: InternalDevDepOptions, + plugin: {name: DependencySpecifier, resolveFrom: ProjectPath, ...}, requestDevDeps: Map, options: ParcelOptions, ): Promise { - let {specifier, resolveFrom, invalidateParcelPlugin} = opts; - let key = `${specifier}:${resolveFrom}`; + let {specifier, resolveFrom, additionalInvalidations} = opts; + let key = `${specifier}:${fromProjectPathRelative(resolveFrom)}`; // If the request sent us a hash, we know the dev dep and all of its dependencies didn't change. // Reuse the same hash in the response. No need to send back invalidations as the request won't @@ -29,19 +46,30 @@ export async function createDevDependency( }; } + let resolveFromAbsolute = fromProjectPath(options.projectRoot, resolveFrom); + // Ensure that the package manager has an entry for this resolution. - await options.packageManager.resolve(specifier, resolveFrom); + await options.packageManager.resolve(specifier, resolveFromAbsolute); let invalidations = options.packageManager.getInvalidations( specifier, - resolveFrom, + resolveFromAbsolute, ); + let cached = devDepRequestCache.get(invalidations); + if (cached != null) { + return cached; + } + + let invalidateOnFileChangeProject = [ + ...invalidations.invalidateOnFileChange, + ].map(f => toProjectPath(options.projectRoot, f)); + // It is possible for a transformer to have multiple different hashes due to // different dependencies (e.g. conditional requires) so we must always // recompute the hash and compare rather than only sending a transformer // dev dependency once. hash = await getInvalidationHash( - [...invalidations.invalidateOnFileChange].map(f => ({ + invalidateOnFileChangeProject.map(f => ({ type: 'file', filePath: f, })), @@ -52,27 +80,20 @@ export async function createDevDependency( specifier, resolveFrom, hash, - invalidateOnFileCreate: invalidations.invalidateOnFileCreate, - invalidateOnFileChange: invalidations.invalidateOnFileChange, + invalidateOnFileCreate: invalidations.invalidateOnFileCreate.map(i => + invalidateOnFileCreateToInternal(options.projectRoot, i), + ), + invalidateOnFileChange: new Set(invalidateOnFileChangeProject), + additionalInvalidations, }; - // Optionally also invalidate the parcel plugin that is loading the config - // when this dev dep changes (e.g. to invalidate local caches). - if (invalidateParcelPlugin) { - devDepRequest.additionalInvalidations = [ - { - specifier: plugin.name, - resolveFrom: plugin.resolveFrom, - }, - ]; - } - + devDepRequestCache.set(invalidations, devDepRequest); return devDepRequest; } export type DevDepSpecifier = {| specifier: DependencySpecifier, - resolveFrom: FilePath, + resolveFrom: ProjectPath, |}; type DevDepRequests = {| @@ -97,7 +118,10 @@ export async function getDevDepRequests(api: RunAPI): Promise { devDeps: new Map( [...previousDevDepRequests.entries()] .filter(([id]) => api.canSkipSubrequest(id)) - .map(([, req]) => [`${req.specifier}:${req.resolveFrom}`, req.hash]), + .map(([, req]) => [ + `${req.specifier}:${fromProjectPathRelative(req.resolveFrom)}`, + req.hash, + ]), ), invalidDevDeps: await Promise.all( [...previousDevDepRequests.entries()] @@ -108,7 +132,10 @@ export async function getDevDepRequests(api: RunAPI): Promise { specifier: req.specifier, resolveFrom: req.resolveFrom, }, - ...(req.additionalInvalidations ?? []), + ...(req.additionalInvalidations ?? []).map(i => ({ + specifier: i.specifier, + resolveFrom: i.resolveFrom, + })), ]; }), ), @@ -125,10 +152,13 @@ export function invalidateDevDeps( config: ParcelConfig, ) { for (let {specifier, resolveFrom} of invalidDevDeps) { - let key = `${specifier}:${resolveFrom}`; + let key = `${specifier}:${fromProjectPathRelative(resolveFrom)}`; if (!invalidatedDevDeps.has(key)) { config.invalidatePlugin(specifier); - options.packageManager.invalidate(specifier, resolveFrom); + options.packageManager.invalidate( + specifier, + fromProjectPath(options.projectRoot, resolveFrom), + ); invalidatedDevDeps.set(key, true); } } diff --git a/packages/core/core/src/requests/EntryRequest.js b/packages/core/core/src/requests/EntryRequest.js index 405548fa788..aa6d59b3139 100644 --- a/packages/core/core/src/requests/EntryRequest.js +++ b/packages/core/core/src/requests/EntryRequest.js @@ -1,16 +1,30 @@ // @flow strict-local -import type {Async, FilePath, File, PackageJSON} from '@parcel/types'; +import type {Async, FilePath, PackageJSON} from '@parcel/types'; import type {StaticRunOpts} from '../RequestTracker'; -import type {Entry, ParcelOptions} from '../types'; +import type {Entry, InternalFile, ParcelOptions} from '../types'; import type {FileSystem} from '@parcel/fs'; -import {isDirectoryInside, isGlob, glob} from '@parcel/utils'; -import ThrowableDiagnostic, {md} from '@parcel/diagnostic'; +import { + isDirectoryInside, + isGlob, + glob, + findAlternativeFiles, +} from '@parcel/utils'; +import ThrowableDiagnostic, { + md, + generateJSONCodeHighlights, +} from '@parcel/diagnostic'; import path from 'path'; +import { + type ProjectPath, + fromProjectPath, + fromProjectPathRelative, + toProjectPath, +} from '../projectPath'; type RunOpts = {| - input: FilePath, + input: ProjectPath, ...StaticRunOpts, |}; @@ -18,19 +32,19 @@ export type EntryRequest = {| id: string, +type: 'entry_request', run: RunOpts => Async, - input: FilePath, + input: ProjectPath, |}; export type EntryResult = {| entries: Array, - files: Array, + files: Array, |}; const type = 'entry_request'; -export default function createEntryRequest(input: FilePath): EntryRequest { +export default function createEntryRequest(input: ProjectPath): EntryRequest { return { - id: `${type}:${input}`, + id: `${type}:${fromProjectPathRelative(input)}`, type, run, input, @@ -39,7 +53,8 @@ export default function createEntryRequest(input: FilePath): EntryRequest { async function run({input, api, options}: RunOpts): Promise { let entryResolver = new EntryResolver(options); - let result = await entryResolver.resolveEntry(input); + let filePath = fromProjectPath(options.projectRoot, input); + let result = await entryResolver.resolveEntry(filePath); // Connect files like package.json that affect the entry // resolution so we invalidate when they change. @@ -50,7 +65,7 @@ async function run({input, api, options}: RunOpts): Promise { // If the entry specifier is a glob, add a glob node so // we invalidate when a new file matches. - if (isGlob(input)) { + if (isGlob(filePath)) { api.invalidateOnFileCreate({glob: input}); } @@ -66,26 +81,64 @@ async function run({input, api, options}: RunOpts): Promise { async function assertFile( fs: FileSystem, - source: string, - diagnosticPath: string, + entry: FilePath, + relativeSource: FilePath, + pkgFilePath: FilePath, + keyPath: string, + options: ParcelOptions, ) { + let source = path.join(entry, relativeSource); let stat; try { stat = await fs.stat(source); } catch (err) { + let contents = await fs.readFile(pkgFilePath, 'utf8'); + let alternatives = await findAlternativeFiles( + fs, + relativeSource, + entry, + options.projectRoot, + false, + ); throw new ThrowableDiagnostic({ diagnostic: { - message: `${diagnosticPath} does not exist`, - filePath: source, + origin: '@parcel/core', + message: `${path.relative(process.cwd(), source)} does not exist.`, + codeFrames: [ + { + filePath: pkgFilePath, + codeHighlights: generateJSONCodeHighlights(contents, [ + { + key: keyPath, + type: 'value', + }, + ]), + }, + ], + hints: alternatives.map(r => { + return `Did you mean '__${r}__'?`; + }), }, }); } if (!stat.isFile()) { + let contents = await fs.readFile(pkgFilePath, 'utf8'); throw new ThrowableDiagnostic({ diagnostic: { - message: `${diagnosticPath} is not a file`, - filePath: source, + origin: '@parcel/core', + message: `${path.relative(process.cwd(), source)} is not a file.`, + codeFrames: [ + { + filePath: pkgFilePath, + codeHighlights: generateJSONCodeHighlights(contents, [ + { + key: keyPath, + type: 'value', + }, + ]), + }, + ], }, }); } @@ -123,18 +176,25 @@ export class EntryResolver { throw new ThrowableDiagnostic({ diagnostic: { message: md`Entry ${entry} does not exist`, - filePath: entry, }, }); } if (stat.isDirectory()) { - let pkg = await this.readPackage(entry); + let pkg: ?{ + ...PackageJSON, + filePath: FilePath, + ... + } = await this.readPackage(entry); if (pkg) { let {filePath} = pkg; let entries = []; - let files = [{filePath}]; + let files = [ + { + filePath: toProjectPath(this.options.projectRoot, filePath), + }, + ]; let targetsWithSources = 0; if (pkg.targets) { @@ -145,19 +205,26 @@ export class EntryResolver { let targetSources = Array.isArray(target.source) ? target.source : [target.source]; + let i = 0; for (let relativeSource of targetSources) { let source = path.join(entry, relativeSource); - let diagnosticPath = md`${relativeSource} in ${path.relative( - this.options.inputFS.cwd(), + await assertFile( + this.options.inputFS, + entry, + relativeSource, filePath, - )}#targets["${targetName}"].source`; - await assertFile(this.options.inputFS, source, diagnosticPath); + `/targets/${targetName}/source${ + Array.isArray(target.source) ? `/${i}` : '' + }`, + this.options, + ); entries.push({ - filePath: source, - packagePath: entry, + filePath: toProjectPath(this.options.projectRoot, source), + packagePath: toProjectPath(this.options.projectRoot, entry), target: targetName, }); + i++; } } } @@ -173,14 +240,22 @@ export class EntryResolver { let pkgSources = Array.isArray(pkg.source) ? pkg.source : [pkg.source]; + let i = 0; for (let pkgSource of pkgSources) { let source = path.join(path.dirname(filePath), pkgSource); - let diagnosticPath = md`${pkgSource} in ${path.relative( - this.options.inputFS.cwd(), + await assertFile( + this.options.inputFS, + entry, + pkgSource, filePath, - )}#source`; - await assertFile(this.options.inputFS, source, diagnosticPath); - entries.push({filePath: source, packagePath: entry}); + `/source${Array.isArray(pkg.source) ? `/${i}` : ''}`, + this.options, + ); + entries.push({ + filePath: toProjectPath(this.options.projectRoot, source), + packagePath: toProjectPath(this.options.projectRoot, entry), + }); + i++; } } @@ -196,7 +271,6 @@ export class EntryResolver { throw new ThrowableDiagnostic({ diagnostic: { message: md`Could not find entry: ${entry}`, - filePath: entry, }, }); } else if (stat.isFile()) { @@ -209,7 +283,12 @@ export class EntryResolver { : projectRoot; return { - entries: [{filePath: entry, packagePath: packagePath}], + entries: [ + { + filePath: toProjectPath(this.options.projectRoot, entry), + packagePath: toProjectPath(this.options.projectRoot, packagePath), + }, + ], files: [], }; } @@ -217,7 +296,6 @@ export class EntryResolver { throw new ThrowableDiagnostic({ diagnostic: { message: `Unknown entry: ${entry}`, - filePath: entry, }, }); } @@ -236,13 +314,13 @@ export class EntryResolver { try { pkg = JSON.parse(content); } catch (err) { + // TODO: code frame? throw new ThrowableDiagnostic({ diagnostic: { message: md`Error parsing ${path.relative( this.options.inputFS.cwd(), pkgFile, )}: ${err.message}`, - filePath: pkgFile, }, }); } diff --git a/packages/core/core/src/requests/ParcelConfigRequest.js b/packages/core/core/src/requests/ParcelConfigRequest.js index 5c624e23891..b2562c93855 100644 --- a/packages/core/core/src/requests/ParcelConfigRequest.js +++ b/packages/core/core/src/requests/ParcelConfigRequest.js @@ -6,6 +6,7 @@ import type { RawParcelConfig, ResolvedParcelConfigFile, } from '@parcel/types'; +import type {FileSystem} from '@parcel/fs'; import type {StaticRunOpts} from '../RequestTracker'; import type { ExtendableParcelConfigPipeline, @@ -35,6 +36,7 @@ import ParcelConfigSchema from '../ParcelConfig.schema'; import {optionsProxy} from '../utils'; import ParcelConfig from '../ParcelConfig'; import {createBuildCache} from '../buildCache'; +import {toProjectPath} from '../projectPath'; type ConfigMap = {[K]: V, ...}; @@ -67,7 +69,14 @@ export default function createParcelConfigRequest(): ParcelConfigRequest { id: type, type, async run({api, options}: RunOpts): Promise { - let {config, extendedFiles, usedDefault} = await loadParcelConfig( + let { + config, + extendedFiles, + usedDefault, + }: {| + ...ParcelConfigChain, + usedDefault: boolean, + |} = await loadParcelConfig( optionsProxy(options, api.invalidateOnOptionChange), ); @@ -75,15 +84,16 @@ export default function createParcelConfigRequest(): ParcelConfigRequest { api.invalidateOnFileDelete(config.filePath); for (let filePath of extendedFiles) { - api.invalidateOnFileUpdate(filePath); - api.invalidateOnFileDelete(filePath); + let pp = toProjectPath(options.projectRoot, filePath); + api.invalidateOnFileUpdate(pp); + api.invalidateOnFileDelete(pp); } if (usedDefault) { - let resolveFrom = getResolveFrom(options); + let resolveFrom = getResolveFrom(options.inputFS, options.projectRoot); api.invalidateOnFileCreate({ fileName: '.parcelrc', - aboveFilePath: resolveFrom, + aboveFilePath: toProjectPath(options.projectRoot, resolveFrom), }); } @@ -130,7 +140,7 @@ export async function loadParcelConfig( export async function resolveParcelConfig( options: ParcelOptions, ): Promise { - let resolveFrom = getResolveFrom(options); + let resolveFrom = getResolveFrom(options.inputFS, options.projectRoot); let configPath = options.config != null ? (await options.packageManager.resolve(options.config, resolveFrom)) @@ -169,7 +179,7 @@ export async function resolveParcelConfig( }); } - let {config, extendedFiles} = await parseAndProcessConfig( + let {config, extendedFiles}: ParcelConfigChain = await parseAndProcessConfig( configPath, contents, options, @@ -214,18 +224,20 @@ export async function parseAndProcessConfig( message: `Failed to parse .parcelrc`, origin: '@parcel/core', - filePath: configPath, - language: 'json5', - codeFrame: { - code: contents, - codeHighlights: [ - { - start: pos, - end: pos, - message: escapeMarkdown(e.message), - }, - ], - }, + codeFrames: [ + { + filePath: configPath, + language: 'json5', + code: contents, + codeHighlights: [ + { + start: pos, + end: pos, + message: escapeMarkdown(e.message), + }, + ], + }, + ], }, }); } @@ -233,100 +245,148 @@ export async function parseAndProcessConfig( } function processPipeline( + options: ParcelOptions, pipeline: ?Array, keyPath: string, filePath: FilePath, - //$FlowFixMe -): any { +) { if (pipeline) { return pipeline.map((pkg, i) => { + // $FlowFixMe if (pkg === '...') return pkg; return { packageName: pkg, - resolveFrom: filePath, + resolveFrom: toProjectPath(options.projectRoot, filePath), keyPath: `${keyPath}/${i}`, }; }); } } -function processMap( +async function processMap( // $FlowFixMe map: ?ConfigMap, keyPath: string, filePath: FilePath, + options: ParcelOptions, // $FlowFixMe -): ConfigMap | typeof undefined { +): Promise | typeof undefined> { if (!map) return undefined; // $FlowFixMe let res: ConfigMap = {}; for (let k in map) { + if (k.startsWith('node:')) { + let code = await options.inputFS.readFile(filePath, 'utf8'); + throw new ThrowableDiagnostic({ + diagnostic: { + message: + 'Named pipeline `node:` is reserved for builtin Node.js libraries', + origin: '@parcel/core', + codeFrames: [ + { + filePath: filePath, + language: 'json5', + code, + codeHighlights: generateJSONCodeHighlights(code, [ + { + key: `${keyPath}/${k}`, + type: 'key', + }, + ]), + }, + ], + }, + }); + } + if (typeof map[k] === 'string') { res[k] = { packageName: map[k], - resolveFrom: filePath, + resolveFrom: toProjectPath(options.projectRoot, filePath), keyPath: `${keyPath}/${k}`, }; } else { - res[k] = processPipeline(map[k], `${keyPath}/${k}`, filePath); + res[k] = processPipeline(options, map[k], `${keyPath}/${k}`, filePath); } } return res; } -export function processConfig( +export async function processConfig( configFile: ResolvedParcelConfigFile, -): ProcessedParcelConfig { + options: ParcelOptions, +): Promise { return { - extends: configFile.extends, - filePath: configFile.filePath, - resolveFrom: configFile.resolveFrom, + filePath: toProjectPath(options.projectRoot, configFile.filePath), + ...(configFile.resolveFrom != null + ? { + resolveFrom: toProjectPath( + options.projectRoot, + configFile.resolveFrom, + ), + } + : {...null}), resolvers: processPipeline( + options, configFile.resolvers, '/resolvers', configFile.filePath, ), - transformers: processMap( + transformers: await processMap( configFile.transformers, '/transformers', configFile.filePath, + options, ), bundler: configFile.bundler != null ? { packageName: configFile.bundler, - resolveFrom: configFile.filePath, + resolveFrom: toProjectPath( + options.projectRoot, + configFile.filePath, + ), keyPath: '/bundler', } : undefined, - namers: processPipeline(configFile.namers, '/namers', configFile.filePath), + namers: processPipeline( + options, + configFile.namers, + '/namers', + configFile.filePath, + ), runtimes: processPipeline( + options, configFile.runtimes, '/runtimes', configFile.filePath, ), - packagers: processMap( + packagers: await processMap( configFile.packagers, '/packagers', configFile.filePath, + options, ), - optimizers: processMap( + optimizers: await processMap( configFile.optimizers, '/optimizers', configFile.filePath, + options, ), reporters: processPipeline( + options, configFile.reporters, '/reporters', configFile.filePath, ), - validators: processMap( + validators: await processMap( configFile.validators, '/validators', configFile.filePath, + options, ), }; } @@ -341,10 +401,13 @@ export async function processConfigChain( validateConfigFile(configFile, relativePath); // Process config... - let config: ProcessedParcelConfig = processConfig({ - filePath, - ...configFile, - }); + let config: ProcessedParcelConfig = await processConfig( + { + filePath, + ...configFile, + }, + options, + ); let extendedFiles: Array = []; if (configFile.extends != null) { @@ -422,20 +485,24 @@ export async function resolveExtends( diagnostic: { message: `Cannot find extended parcel config`, origin: '@parcel/core', - filePath: configPath, - language: 'json5', - codeFrame: { - code: parentContents, - codeHighlights: generateJSONCodeHighlights(parentContents, [ - { - key: extendsKey, - type: 'value', - message: md`Cannot find module "${ext}"${ - alternatives[0] ? `, did you mean "${alternatives[0]}"?` : '' - }`, - }, - ]), - }, + codeFrames: [ + { + filePath: configPath, + language: 'json5', + code: parentContents, + codeHighlights: generateJSONCodeHighlights(parentContents, [ + { + key: extendsKey, + type: 'value', + message: md`Cannot find module "${ext}"${ + alternatives[0] + ? `, did you mean "${alternatives[0]}"?` + : '' + }`, + }, + ]), + }, + ], }, }); } @@ -467,20 +534,22 @@ async function processExtendedConfig( diagnostic: { message: 'Cannot find extended parcel config', origin: '@parcel/core', - filePath: configPath, - language: 'json5', - codeFrame: { - code: parentContents, - codeHighlights: generateJSONCodeHighlights(parentContents, [ - { - key: extendsKey, - type: 'value', - message: md`"${extendsSpecifier}" does not exist${ - alternatives[0] ? `, did you mean "./${alternatives[0]}"?` : '' - }`, - }, - ]), - }, + codeFrames: [ + { + filePath: configPath, + language: 'json5', + code: parentContents, + codeHighlights: generateJSONCodeHighlights(parentContents, [ + { + key: extendsKey, + type: 'value', + message: md`"${extendsSpecifier}" does not exist${ + alternatives[0] ? `, did you mean "${alternatives[0]}"?` : '' + }`, + }, + ]), + }, + ], }, }); } @@ -535,11 +604,12 @@ export function mergeConfigs( }; } -function getResolveFrom(options: ParcelOptions) { - let cwd = options.inputFS.cwd(); - let dir = isDirectoryInside(cwd, options.projectRoot) - ? cwd - : options.projectRoot; +export function getResolveFrom( + fs: FileSystem, + projectRoot: FilePath, +): FilePath { + let cwd = fs.cwd(); + let dir = isDirectoryInside(cwd, projectRoot) ? cwd : projectRoot; return path.join(dir, 'index'); } diff --git a/packages/core/core/src/requests/PathRequest.js b/packages/core/core/src/requests/PathRequest.js index 2cf2af2b74d..9ec81c8bb3a 100644 --- a/packages/core/core/src/requests/PathRequest.js +++ b/packages/core/core/src/requests/PathRequest.js @@ -12,10 +12,10 @@ import type {ConfigAndCachePath} from './ParcelConfigRequest'; import ThrowableDiagnostic, {errorToDiagnostic, md} from '@parcel/diagnostic'; import {PluginLogger} from '@parcel/logger'; -import {relativePath} from '@parcel/utils'; import nullthrows from 'nullthrows'; import path from 'path'; import URL from 'url'; +import {normalizePath} from '@parcel/utils'; import querystring from 'querystring'; import {report} from '../ReporterRunner'; import PublicDependency from '../public/Dependency'; @@ -24,6 +24,12 @@ import ParcelConfig from '../ParcelConfig'; import createParcelConfigRequest, { getCachedParcelConfig, } from './ParcelConfigRequest'; +import {invalidateOnFileCreateToInternal} from '../utils'; +import { + fromProjectPath, + fromProjectPathRelative, + toProjectPath, +} from '../projectPath'; import {Priority} from '../types'; export type PathRequest = {| @@ -66,25 +72,35 @@ async function run({input, api, options}: RunOpts) { options, config, }); - let result = await resolverRunner.resolve(input.dependency); + let result: ResolverResult = await resolverRunner.resolve(input.dependency); - if (result != null) { - if (result.invalidateOnFileCreate) { - for (let file of result.invalidateOnFileCreate) { - api.invalidateOnFileCreate(file); - } + if (result.invalidateOnFileCreate) { + for (let file of result.invalidateOnFileCreate) { + api.invalidateOnFileCreate( + invalidateOnFileCreateToInternal(options.projectRoot, file), + ); } + } - if (result.invalidateOnFileChange) { - for (let filePath of result.invalidateOnFileChange) { - api.invalidateOnFileUpdate(filePath); - api.invalidateOnFileDelete(filePath); - } + if (result.invalidateOnFileChange) { + for (let filePath of result.invalidateOnFileChange) { + let pp = toProjectPath(options.projectRoot, filePath); + api.invalidateOnFileUpdate(pp); + api.invalidateOnFileDelete(pp); } + } + if (result.assetGroup) { api.invalidateOnFileDelete(result.assetGroup.filePath); return result.assetGroup; } + + if (result.diagnostics && result.diagnostics.length > 0) { + let err = new ThrowableDiagnostic({diagnostic: result.diagnostics}); + // $FlowFixMe[prop-missing] + err.code = 'MODULE_NOT_FOUND'; + throw err; + } } type ResolverRunnerOpts = {| @@ -93,9 +109,10 @@ type ResolverRunnerOpts = {| |}; type ResolverResult = {| - assetGroup: AssetGroup, + assetGroup: ?AssetGroup, invalidateOnFileCreate?: Array, invalidateOnFileChange?: Array, + diagnostics?: Array, |}; export class ResolverRunner { @@ -109,33 +126,36 @@ export class ResolverRunner { this.pluginOptions = new PluginOptions(this.options); } - async getThrowableDiagnostic( + async getDiagnostic( dependency: Dependency, message: string, - ): Async { + ): Async { let diagnostic: Diagnostic = { message, origin: '@parcel/core', }; if (dependency.loc && dependency.sourcePath != null) { - diagnostic.filePath = dependency.sourcePath; - diagnostic.codeFrame = { - code: await this.options.inputFS.readFile( - dependency.sourcePath, - 'utf8', - ), - codeHighlights: dependency.loc - ? [{start: dependency.loc.start, end: dependency.loc.end}] - : [], - }; + let filePath = fromProjectPath( + this.options.projectRoot, + dependency.sourcePath, + ); + diagnostic.codeFrames = [ + { + filePath, + code: await this.options.inputFS.readFile(filePath, 'utf8'), + codeHighlights: dependency.loc + ? [{start: dependency.loc.start, end: dependency.loc.end}] + : [], + }, + ]; } - return new ThrowableDiagnostic({diagnostic}); + return diagnostic; } - async resolve(dependency: Dependency): Promise { - let dep = new PublicDependency(dependency); + async resolve(dependency: Dependency): Promise { + let dep = new PublicDependency(dependency, this.options); report({ type: 'buildProgress', phase: 'resolving', @@ -154,26 +174,38 @@ export class ResolverRunner { !path.isAbsolute(dependency.specifier) && dependency.specifier.includes(':') ) { - [pipeline, filePath] = dependency.specifier.split(':'); - if (!validPipelines.has(pipeline)) { - if (dep.specifierType === 'url') { - // This may be a url protocol or scheme rather than a pipeline, such as - // `url('http://example.com/foo.png')` - return null; - } else { - throw await this.getThrowableDiagnostic( - dependency, - md`Unknown pipeline: ${pipeline}.`, - ); + if (dependency.specifier.startsWith('node:')) { + filePath = dependency.specifier; + } else { + [pipeline, filePath] = dependency.specifier.split(':'); + if (!validPipelines.has(pipeline)) { + if (dep.specifierType === 'url') { + // This may be a url protocol or scheme rather than a pipeline, such as + // `url('http://example.com/foo.png')` + return {assetGroup: null}; + } else { + return { + assetGroup: null, + diagnostics: [ + await this.getDiagnostic( + dependency, + md`Unknown pipeline: ${pipeline}.`, + ), + ], + }; + } } } } else { - if ( - dep.specifierType === 'url' && - dependency.specifier.startsWith('//') - ) { - // A protocol-relative URL, e.g `url('//example.com/foo.png')` - return null; + if (dep.specifierType === 'url') { + if (dependency.specifier.startsWith('//')) { + // A protocol-relative URL, e.g `url('//example.com/foo.png')` + return {assetGroup: null}; + } + if (dependency.specifier.startsWith('#')) { + // An ID-only URL, e.g. `url(#clip-path)` for CSS rules + return {assetGroup: null}; + } } filePath = dependency.specifier; } @@ -182,10 +214,15 @@ export class ResolverRunner { if (dep.specifierType === 'url') { let parsed = URL.parse(filePath); if (typeof parsed.pathname !== 'string') { - throw await this.getThrowableDiagnostic( - dependency, - md`Received URL without a pathname ${filePath}.`, - ); + return { + assetGroup: null, + diagnostics: [ + await this.getDiagnostic( + dependency, + md`Received URL without a pathname ${filePath}.`, + ), + ], + }; } filePath = decodeURIComponent(parsed.pathname); if (parsed.query != null) { @@ -202,7 +239,13 @@ export class ResolverRunner { query = querystring.parse(queryPart); } + // Entrypoints, convert ProjectPath in module specifier to absolute path + if (dep.resolveFrom == null) { + filePath = path.join(this.options.projectRoot, filePath); + } let diagnostics: Array = []; + let invalidateOnFileCreate = []; + let invalidateOnFileChange = []; for (let resolver of resolvers) { try { let result = await resolver.plugin.resolve({ @@ -215,6 +258,7 @@ export class ResolverRunner { if (result) { if (result.meta) { + dependency.resolverMeta = result.meta; dependency.meta = { ...dependency.meta, ...result.meta, @@ -225,15 +269,37 @@ export class ResolverRunner { dependency.priority = Priority[result.priority]; } + if (result.invalidateOnFileCreate) { + invalidateOnFileCreate.push(...result.invalidateOnFileCreate); + } + + if (result.invalidateOnFileChange) { + invalidateOnFileChange.push(...result.invalidateOnFileChange); + } + if (result.isExcluded) { - return null; + return { + assetGroup: null, + invalidateOnFileCreate, + invalidateOnFileChange, + }; } if (result.filePath != null) { + let resultFilePath = result.filePath; + if (!path.isAbsolute(resultFilePath)) { + throw new Error( + md`Resolvers must return an absolute path, ${resolver.name} returned: ${resultFilePath}`, + ); + } + return { assetGroup: { canDefer: result.canDefer, - filePath: result.filePath, + filePath: toProjectPath( + this.options.projectRoot, + resultFilePath, + ), query, sideEffects: result.sideEffects, code: result.code, @@ -244,8 +310,8 @@ export class ResolverRunner { : result.pipeline, isURL: dep.specifierType === 'url', }, - invalidateOnFileCreate: result.invalidateOnFileCreate, - invalidateOnFileChange: result.invalidateOnFileChange, + invalidateOnFileCreate, + invalidateOnFileChange, }; } @@ -277,27 +343,33 @@ export class ResolverRunner { } if (dep.isOptional) { - return null; + return { + assetGroup: null, + invalidateOnFileCreate, + invalidateOnFileChange, + }; } let resolveFrom = dependency.resolveFrom ?? dependency.sourcePath; let dir = resolveFrom != null - ? relativePath(this.options.projectRoot, resolveFrom) + ? normalizePath(fromProjectPathRelative(resolveFrom)) : ''; - // $FlowFixMe because of the err.code assignment - let err = await this.getThrowableDiagnostic( + let diagnostic = await this.getDiagnostic( dependency, md`Failed to resolve '${dependency.specifier}' ${ dir ? `from '${dir}'` : '' }`, ); - // Merge diagnostics - err.diagnostics.push(...diagnostics); - err.code = 'MODULE_NOT_FOUND'; + diagnostics.unshift(diagnostic); - throw err; + return { + assetGroup: null, + invalidateOnFileCreate, + invalidateOnFileChange, + diagnostics, + }; } } diff --git a/packages/core/core/src/requests/TargetRequest.js b/packages/core/core/src/requests/TargetRequest.js index 747959bb041..a47928e37ce 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'; @@ -42,7 +43,8 @@ import { ENGINES_SCHEMA, } from '../TargetDescriptor.schema'; import {BROWSER_ENVS} from '../public/Environment'; -import {optionsProxy} from '../utils'; +import {optionsProxy, toInternalSourceLocation} from '../utils'; +import {fromProjectPath, toProjectPath} from '../projectPath'; type RunOpts = {| input: Entry, @@ -50,7 +52,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, @@ -89,7 +111,10 @@ async function run({input, api, options}: RunOpts) { api, optionsProxy(options, api.invalidateOnOptionChange), ); - let targets = await targetResolver.resolve(input.packagePath, input.target); + let targets = await targetResolver.resolve( + fromProjectPath(options.projectRoot, input.packagePath), + input.target, + ); let configResult = nullthrows( await api.runRequest(createParcelConfigRequest()), @@ -178,33 +203,42 @@ export class TargetResolver { diagnostic: { message: md`Missing distDir for target "${name}"`, origin: '@parcel/core', - codeFrame: { - code: optionTargetsString, - codeHighlights: generateJSONCodeHighlights( - optionTargetsString || '', - [ - { - key: `/${name}`, - type: 'value', - }, - ], - ), - }, + codeFrames: [ + { + code: optionTargetsString, + codeHighlights: generateJSONCodeHighlights( + optionTargetsString || '', + [ + { + key: `/${name}`, + type: 'value', + }, + ], + ), + }, + ], }, }); } let target: Target = { name, - distDir: path.resolve(this.fs.cwd(), distDir), + distDir: toProjectPath( + this.options.projectRoot, + path.resolve(this.fs.cwd(), distDir), + ), publicUrl: descriptor.publicUrl ?? this.options.defaultTargetOptions.publicUrl, env: createEnvironment({ engines: descriptor.engines, context: descriptor.context, - isLibrary: descriptor.isLibrary, + isLibrary: + descriptor.isLibrary ?? + this.options.defaultTargetOptions.isLibrary, includeNodeModules: descriptor.includeNodeModules, - outputFormat: descriptor.outputFormat, + outputFormat: + descriptor.outputFormat ?? + this.options.defaultTargetOptions.outputFormat, shouldOptimize: this.options.defaultTargetOptions.shouldOptimize && descriptor.optimize !== false, @@ -253,7 +287,10 @@ export class TargetResolver { }, }); } - targets[0].distDir = serve.distDir; + targets[0].distDir = toProjectPath( + this.options.projectRoot, + serve.distDir, + ); } } else { // Explicit targets were not provided. Either use a modern target for server @@ -264,12 +301,16 @@ export class TargetResolver { targets = [ { name: 'default', - distDir: this.options.serveOptions.distDir, + distDir: toProjectPath( + this.options.projectRoot, + this.options.serveOptions.distDir, + ), publicUrl: this.options.defaultTargetOptions.publicUrl ?? '/', env: createEnvironment({ context: 'browser', engines: {}, shouldOptimize: this.options.defaultTargetOptions.shouldOptimize, + outputFormat: this.options.defaultTargetOptions.outputFormat, shouldScopeHoist: this.options.defaultTargetOptions .shouldScopeHoist, sourceMap: this.options.defaultTargetOptions.sourceMaps @@ -304,10 +345,12 @@ export class TargetResolver { this.options.projectRoot, ); + let rootFileProject = toProjectPath(this.options.projectRoot, rootFile); + // Invalidate whenever a package.json file is added. this.api.invalidateOnFileCreate({ fileName: 'package.json', - aboveFilePath: rootFile, + aboveFilePath: rootFileProject, }); let pkg; @@ -331,8 +374,9 @@ export class TargetResolver { pkgContents = await this.fs.readFile(_pkgFilePath, 'utf8'); pkgMap = jsonMap.parse(pkgContents.replace(/\t/g, ' ')); - this.api.invalidateOnFileUpdate(_pkgFilePath); - this.api.invalidateOnFileDelete(_pkgFilePath); + let pp = toProjectPath(this.options.projectRoot, _pkgFilePath); + this.api.invalidateOnFileUpdate(pp); + this.api.invalidateOnFileDelete(pp); } else { pkg = {}; pkgDir = this.fs.cwd(); @@ -374,12 +418,12 @@ export class TargetResolver { this.api.invalidateOnFileCreate({ fileName: 'browserslist', - aboveFilePath: rootFile, + aboveFilePath: rootFileProject, }); this.api.invalidateOnFileCreate({ fileName: '.browserslistrc', - aboveFilePath: rootFile, + aboveFilePath: rootFileProject, }); if (browserslistConfig != null) { @@ -395,8 +439,9 @@ export class TargetResolver { } // Invalidate whenever browserslist config file or relevant environment variables change - this.api.invalidateOnFileUpdate(browserslistConfig); - this.api.invalidateOnFileDelete(browserslistConfig); + let pp = toProjectPath(this.options.projectRoot, browserslistConfig); + this.api.invalidateOnFileUpdate(pp); + this.api.invalidateOnFileDelete(pp); this.api.invalidateOnEnvChange('BROWSERSLIST_ENV'); this.api.invalidateOnEnvChange('NODE_ENV'); } @@ -438,7 +483,7 @@ export class TargetResolver { }; } - for (let targetName of COMMON_TARGETS) { + for (let targetName in COMMON_TARGETS) { let _targetDist; let pointer; if ( @@ -465,7 +510,10 @@ export class TargetResolver { let _descriptor: mixed = pkgTargets[targetName] ?? {}; if (typeof targetDist === 'string') { - distDir = path.resolve(pkgDir, path.dirname(targetDist)); + distDir = toProjectPath( + this.options.projectRoot, + path.resolve(pkgDir, path.dirname(targetDist)), + ); distEntry = path.basename(targetDist); loc = { filePath: nullthrows(pkgFilePath), @@ -474,7 +522,10 @@ export class TargetResolver { } else { distDir = this.options.defaultTargetOptions.distDir ?? - path.join(pkgDir, DEFAULT_DIST_DIRNAME, targetName); + toProjectPath( + this.options.projectRoot, + path.join(pkgDir, DEFAULT_DIST_DIRNAME, targetName), + ); } if (_descriptor == false) { @@ -492,10 +543,163 @@ 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', + codeFrames: [ + { + language: 'json', + filePath: pkgFilePath ?? undefined, + 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', + codeFrames: [ + { + language: 'json', + filePath: pkgFilePath ?? undefined, + 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 ?? + this.options.defaultTargetOptions.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', + codeFrames: [ + { + language: 'json', + filePath: pkgFilePath ?? undefined, + 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.`, + ], + }, + }); + } + + if (descriptor.scopeHoist === false) { + let contents: string = + typeof pkgContents === 'string' + ? pkgContents + : // $FlowFixMe + JSON.stringify(pkgContents, null, '\t'); + throw new ThrowableDiagnostic({ + diagnostic: { + message: 'Scope hoisting cannot be disabled for library targets.', + origin: '@parcel/core', + codeFrames: [ + { + language: 'json', + filePath: pkgFilePath ?? undefined, + code: contents, + codeHighlights: generateJSONCodeHighlights(contents, [ + { + key: `/targets/${targetName}/scopeHoist`, + type: 'value', + }, + ]), + }, + ], + hints: [ + `The "${targetName}" target is meant for libraries. Either remove the "scopeHoist" option, or use a different target name.`, + ], + }, + }); + } + targets.set(targetName, { name: targetName, distDir, @@ -508,33 +712,25 @@ 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, - shouldScopeHoist: - this.options.defaultTargetOptions.shouldScopeHoist && - descriptor.scopeHoist !== false, + descriptor.optimize === true, + shouldScopeHoist: true, sourceMap: normalizeSourceMap(this.options, descriptor.sourceMap), }), - loc, + loc: toInternalSourceLocation(this.options.projectRoot, loc), }); } } let customTargets = (Object.keys(pkgTargets): Array).filter( - targetName => !COMMON_TARGETS.includes(targetName), + targetName => !COMMON_TARGETS[targetName], ); // Custom targets @@ -545,8 +741,10 @@ export class TargetResolver { let loc; if (distPath == null) { distDir = - this.options.defaultTargetOptions.distDir ?? - path.join(pkgDir, DEFAULT_DIST_DIRNAME); + fromProjectPath( + this.options.projectRoot, + this.options.defaultTargetOptions.distDir, + ) ?? path.join(pkgDir, DEFAULT_DIST_DIRNAME); if (customTargets.length >= 2) { distDir = path.join(distDir, targetName); } @@ -561,18 +759,20 @@ export class TargetResolver { diagnostic: { message: md`Invalid distPath for target "${targetName}"`, origin: '@parcel/core', - language: 'json', - filePath: pkgFilePath ?? undefined, - codeFrame: { - code: contents, - codeHighlights: generateJSONCodeHighlights(contents, [ - { - key: `/${targetName}`, - type: 'value', - message: 'Expected type string', - }, - ]), - }, + codeFrames: [ + { + language: 'json', + filePath: pkgFilePath ?? undefined, + code: contents, + codeHighlights: generateJSONCodeHighlights(contents, [ + { + key: `/${targetName}`, + type: 'value', + message: 'Expected type string', + }, + ]), + }, + ], }, }); } @@ -598,12 +798,64 @@ export class TargetResolver { if (skipTarget(targetName, exclusiveTarget, descriptor.source)) { continue; } + + let inferredOutputFormat = this.inferOutputFormat( + distEntry, + descriptor, + targetName, + pkg, + pkgFilePath, + pkgContents, + ); + + if (descriptor.scopeHoist === false && descriptor.isLibrary) { + let contents: string = + typeof pkgContents === 'string' + ? pkgContents + : // $FlowFixMe + JSON.stringify(pkgContents, null, '\t'); + throw new ThrowableDiagnostic({ + diagnostic: { + message: 'Scope hoisting cannot be disabled for library targets.', + origin: '@parcel/core', + codeFrames: [ + { + language: 'json', + filePath: pkgFilePath ?? undefined, + code: contents, + codeHighlights: generateJSONCodeHighlights(contents, [ + { + key: `/targets/${targetName}/scopeHoist`, + type: 'value', + }, + { + key: `/targets/${targetName}/isLibrary`, + type: 'value', + }, + ]), + }, + ], + hints: [`Either remove the "scopeHoist" or "isLibrary" option.`], + }, + }); + } + + let isLibrary = + descriptor.isLibrary ?? + this.options.defaultTargetOptions.isLibrary ?? + false; + let shouldScopeHoist = isLibrary + ? true + : this.options.defaultTargetOptions.shouldScopeHoist; + targets.set(targetName, { name: targetName, - distDir: + distDir: toProjectPath( + this.options.projectRoot, descriptor.distDir != null ? path.resolve(pkgDir, descriptor.distDir) : distDir, + ), distEntry, publicUrl: descriptor.publicUrl ?? this.options.defaultTargetOptions.publicUrl, @@ -611,17 +863,23 @@ export class TargetResolver { engines: descriptor.engines ?? pkgEngines, context: descriptor.context, includeNodeModules: descriptor.includeNodeModules, - outputFormat: descriptor.outputFormat, - isLibrary: descriptor.isLibrary, + outputFormat: + descriptor.outputFormat ?? + this.options.defaultTargetOptions.outputFormat ?? + inferredOutputFormat ?? + undefined, + isLibrary, shouldOptimize: this.options.defaultTargetOptions.shouldOptimize && - descriptor.optimize !== false, + // Libraries are not optimized by default, users must explicitly configure this. + (isLibrary + ? descriptor.optimize === true + : descriptor.optimize !== false), shouldScopeHoist: - this.options.defaultTargetOptions.shouldScopeHoist && - descriptor.scopeHoist !== false, + shouldScopeHoist && descriptor.scopeHoist !== false, sourceMap: normalizeSourceMap(this.options, descriptor.sourceMap), }), - loc, + loc: toInternalSourceLocation(this.options.projectRoot, loc), }); } } @@ -632,13 +890,21 @@ export class TargetResolver { name: 'default', distDir: this.options.defaultTargetOptions.distDir ?? - path.join(pkgDir, DEFAULT_DIST_DIRNAME), + toProjectPath( + this.options.projectRoot, + path.join(pkgDir, DEFAULT_DIST_DIRNAME), + ), publicUrl: this.options.defaultTargetOptions.publicUrl, env: createEnvironment({ engines: pkgEngines, context, + outputFormat: this.options.defaultTargetOptions.outputFormat, + isLibrary: this.options.defaultTargetOptions.isLibrary, shouldOptimize: this.options.defaultTargetOptions.shouldOptimize, - shouldScopeHoist: this.options.defaultTargetOptions.shouldScopeHoist, + shouldScopeHoist: + this.options.defaultTargetOptions.shouldScopeHoist ?? + (this.options.mode === 'production' && + !this.options.defaultTargetOptions.isLibrary), sourceMap: this.options.defaultTargetOptions.sourceMaps ? {} : undefined, @@ -646,10 +912,102 @@ export class TargetResolver { }); } - assertNoDuplicateTargets(targets, pkgFilePath, pkgContents); + assertNoDuplicateTargets(this.options, targets, pkgFilePath, pkgContents); 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', + codeFrames: [ + { + language: 'json', + filePath: pkgFilePath ?? undefined, + 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( @@ -738,13 +1096,17 @@ function parseCommonTargetDescriptor( return descriptor; } -function assertNoDuplicateTargets(targets, pkgFilePath, pkgContents) { +function assertNoDuplicateTargets(options, targets, pkgFilePath, pkgContents) { // Detect duplicate targets by destination path and provide a nice error. // Without this, an assertion is thrown much later after naming the bundles and finding duplicates. let targetsByPath: Map> = new Map(); for (let target of targets.values()) { - if (target.distEntry != null) { - let distPath = path.join(target.distDir, target.distEntry); + let {distEntry} = target; + if (distEntry != null) { + let distPath = path.join( + fromProjectPath(options.projectRoot, target.distDir), + distEntry, + ); if (!targetsByPath.has(distPath)) { targetsByPath.set(distPath, []); } @@ -761,18 +1123,20 @@ function assertNoDuplicateTargets(targets, pkgFilePath, pkgContents) { targetPath, )}"`, origin: '@parcel/core', - language: 'json', - filePath: pkgFilePath || undefined, - codeFrame: { - code: pkgContents, - codeHighlights: generateJSONCodeHighlights( - pkgContents, - targetNames.map(t => ({ - key: `/${t}`, - type: 'value', - })), - ), - }, + codeFrames: [ + { + language: 'json', + filePath: pkgFilePath || undefined, + code: pkgContents, + codeHighlights: generateJSONCodeHighlights( + pkgContents, + targetNames.map(t => ({ + key: `/${t}`, + type: 'value', + })), + ), + }, + ], }); } } diff --git a/packages/core/core/src/requests/WriteBundleRequest.js b/packages/core/core/src/requests/WriteBundleRequest.js index 7fedee42a69..634a81b7938 100644 --- a/packages/core/core/src/requests/WriteBundleRequest.js +++ b/packages/core/core/src/requests/WriteBundleRequest.js @@ -13,6 +13,7 @@ import path from 'path'; import {NamedBundle} from '../public/Bundle'; import {TapStream} from '@parcel/utils'; import {Readable, Transform} from 'stream'; +import {fromProjectPath, toProjectPath, joinProjectPath} from '../projectPath'; const BOUNDARY_LENGTH = HASH_REF_PREFIX.length + 32 - 1; @@ -67,7 +68,7 @@ async function run({input, options, api}: RunInput) { name = name.replace(thisHashReference, thisNameHash); } - let filePath = path.join(bundle.target.distDir, name); + let filePath = joinProjectPath(bundle.target.distDir, name); // Watch the bundle and source map for deletion. // Also watch the dist dir because invalidateOnFileDelete does not currently @@ -78,11 +79,14 @@ async function run({input, options, api}: RunInput) { let cacheKeys = info.cacheKeys; let mapKey = cacheKeys.map; + let fullPath = fromProjectPath(options.projectRoot, filePath); if (mapKey && bundle.env.sourceMap && !bundle.env.sourceMap.inline) { - api.invalidateOnFileDelete(filePath + '.map'); + api.invalidateOnFileDelete( + toProjectPath(options.projectRoot, fullPath + '.map'), + ); } - let dir = path.dirname(filePath); + let dir = path.dirname(fullPath); await outputFS.mkdirp(dir); // ? Got rid of dist exists, is this an expensive operation // Use the file mode from the entry asset as the file mode for the bundle. @@ -98,7 +102,7 @@ async function run({input, options, api}: RunInput) { let contentStream = options.cache.getStream(cacheKeys.content); let size = await writeFileStream( outputFS, - filePath, + fullPath, contentStream, info.hashReferences, hashRefToNameHash, @@ -114,7 +118,7 @@ async function run({input, options, api}: RunInput) { let mapStream = options.cache.getStream(mapKey); await writeFileStream( outputFS, - filePath + '.map', + fullPath + '.map', mapStream, info.hashReferences, hashRefToNameHash, diff --git a/packages/core/core/src/requests/WriteBundlesRequest.js b/packages/core/core/src/requests/WriteBundlesRequest.js index ef394768469..d673c6d4aad 100644 --- a/packages/core/core/src/requests/WriteBundlesRequest.js +++ b/packages/core/core/src/requests/WriteBundlesRequest.js @@ -9,11 +9,12 @@ import type {BundleInfo} from '../PackagerRunner'; import {HASH_REF_PREFIX} from '../constants'; import {serialize} from '../serializer'; +import {joinProjectPath} from '../projectPath'; import nullthrows from 'nullthrows'; -import path from 'path'; import {hashString} from '@parcel/hash'; import {createPackageRequest} from './PackageRequest'; import createWriteBundleRequest from './WriteBundleRequest'; +import {BundleBehavior} from '../types'; type WriteBundlesRequestInput = {| bundleGraph: BundleGraph, @@ -69,7 +70,7 @@ async function run({input, api, farm, options}: RunInput) { hashRefToNameHash.set(bundle.hashReference, hash); let name = nullthrows(bundle.name).replace(bundle.hashReference, hash); res.set(bundle.id, { - filePath: path.join(bundle.target.distDir, name), + filePath: joinProjectPath(bundle.target.distDir, name), stats: { time: 0, size: 0, @@ -79,7 +80,7 @@ async function run({input, api, farm, options}: RunInput) { } // skip inline bundles, they will be processed via the parent bundle - return !bundle.isInline; + return bundle.bundleBehavior !== BundleBehavior.inline; }); try { diff --git a/packages/core/core/src/resolveOptions.js b/packages/core/core/src/resolveOptions.js index 5cefcebe3f7..0ae84630aaa 100644 --- a/packages/core/core/src/resolveOptions.js +++ b/packages/core/core/src/resolveOptions.js @@ -1,6 +1,11 @@ // @flow strict-local -import type {FilePath, InitialParcelOptions} from '@parcel/types'; +import type { + FilePath, + InitialParcelOptions, + DependencySpecifier, +} from '@parcel/types'; +import type {FileSystem} from '@parcel/fs'; import type {ParcelOptions} from './types'; import path from 'path'; @@ -8,8 +13,10 @@ import {hashString} from '@parcel/hash'; import {NodeFS} from '@parcel/fs'; import {LMDBCache, FSCache} from '@parcel/cache'; import {NodePackageManager} from '@parcel/package-manager'; -import {getRootDir, resolveConfig} from '@parcel/utils'; +import {getRootDir, relativePath, resolveConfig} from '@parcel/utils'; import loadDotEnv from './loadDotEnv'; +import {toProjectPath} from './projectPath'; +import {getResolveFrom} from './requests/ParcelConfigRequest'; // Default cache directory name const DEFAULT_CACHE_DIRNAME = '.parcel-cache'; @@ -55,11 +62,6 @@ export default async function resolveOptions( path.parse(entryRoot).root, )) || path.join(inputCwd, 'index'); // ? Should this just be rootDir - let lockFile = null; - let rootFileName = path.basename(projectRootFile); - if (LOCK_FILE_NAMES.includes(rootFileName)) { - lockFile = projectRootFile; - } let projectRoot = path.dirname(projectRootFile); let packageManager = @@ -98,8 +100,16 @@ export default async function resolveOptions( } return { - config: initialOptions.config, - defaultConfig: initialOptions.defaultConfig, + config: getRelativeConfigSpecifier( + inputFS, + projectRoot, + initialOptions.config, + ), + defaultConfig: getRelativeConfigSpecifier( + inputFS, + projectRoot, + initialOptions.defaultConfig, + ), shouldPatchConsole: initialOptions.shouldPatchConsole ?? process.env.NODE_ENV !== 'test', env: { @@ -126,28 +136,51 @@ export default async function resolveOptions( shouldDisableCache: initialOptions.shouldDisableCache ?? false, shouldProfile: initialOptions.shouldProfile ?? false, cacheDir, - entries, - entryRoot, + entries: entries.map(e => toProjectPath(projectRoot, e)), + entryRoot: toProjectPath(projectRoot, entryRoot), targets: initialOptions.targets, logLevel: initialOptions.logLevel ?? 'info', projectRoot, - lockFile, inputFS, outputFS, cache, packageManager, - additionalReporters: initialOptions.additionalReporters ?? [], + additionalReporters: + initialOptions.additionalReporters?.map(({packageName, resolveFrom}) => ({ + packageName, + resolveFrom: toProjectPath(projectRoot, resolveFrom), + })) ?? [], instanceId: generateInstanceId(entries), detailedReport: initialOptions.detailedReport, defaultTargetOptions: { shouldOptimize, - shouldScopeHoist: - initialOptions?.defaultTargetOptions?.shouldScopeHoist ?? - initialOptions.mode === 'production', + shouldScopeHoist: initialOptions?.defaultTargetOptions?.shouldScopeHoist, sourceMaps: initialOptions?.defaultTargetOptions?.sourceMaps ?? true, publicUrl, - distDir, + ...(distDir != null + ? {distDir: toProjectPath(projectRoot, distDir)} + : {...null}), engines: initialOptions?.defaultTargetOptions?.engines, + outputFormat: initialOptions?.defaultTargetOptions?.outputFormat, + isLibrary: initialOptions?.defaultTargetOptions?.isLibrary, }, }; } + +function getRelativeConfigSpecifier( + fs: FileSystem, + projectRoot: FilePath, + specifier: ?DependencySpecifier, +) { + if (specifier == null) { + return undefined; + } else if (path.isAbsolute(specifier)) { + let resolveFrom = getResolveFrom(fs, projectRoot); + let relative = relativePath(path.dirname(resolveFrom), specifier); + // If the config is outside the project root, use an absolute path so that if the project root + // moves the path still works. Otherwise, use a relative path so that the cache is portable. + return relative.startsWith('..') ? specifier : relative; + } else { + return specifier; + } +} diff --git a/packages/core/core/src/serializer.js b/packages/core/core/src/serializer.js index c9ca80678b3..ab078a82e4f 100644 --- a/packages/core/core/src/serializer.js +++ b/packages/core/core/src/serializer.js @@ -11,10 +11,6 @@ const nameToCtor: Map> = new Map(); const ctorToName: Map, string> = new Map(); export function registerSerializableClass(name: string, ctor: Class<*>) { - if (nameToCtor.has(name)) { - throw new Error('Name already registered with serializer'); - } - if (ctorToName.has(ctor)) { throw new Error('Class already registered with serializer'); } diff --git a/packages/core/core/src/types.js b/packages/core/core/src/types.js index 574a4575fa1..74f19b08cae 100644 --- a/packages/core/core/src/types.js +++ b/packages/core/core/src/types.js @@ -7,7 +7,6 @@ import type { Engines, EnvironmentContext, EnvMap, - FileCreateInvalidation, FilePath, Glob, LogLevel, @@ -15,8 +14,8 @@ import type { DependencySpecifier, PackageName, ReporterEvent, + SemverRange, ServerOptions, - SourceLocation, SourceType, Stats, Symbol, @@ -27,16 +26,16 @@ import type { HMROptions, QueryParameters, DetailedReportOptions, - DevDepOptions, } from '@parcel/types'; import type {SharedReference} from '@parcel/workers'; import type {FileSystem} from '@parcel/fs'; import type {Cache} from '@parcel/cache'; import type {PackageManager} from '@parcel/package-manager'; +import type {ProjectPath} from './projectPath'; export type ParcelPluginNode = {| packageName: PackageName, - resolveFrom: FilePath, + resolveFrom: ProjectPath, keyPath?: string, |}; @@ -46,7 +45,6 @@ export type ExtendableParcelConfigPipeline = $ReadOnlyArray< >; export type ProcessedParcelConfig = {| - extends?: PackageName | FilePath | Array, resolvers?: PureParcelConfigPipeline, transformers?: {[Glob]: ExtendableParcelConfigPipeline, ...}, bundler: ?ParcelPluginNode, @@ -56,8 +54,8 @@ export type ProcessedParcelConfig = {| optimizers?: {[Glob]: ExtendableParcelConfigPipeline, ...}, reporters?: PureParcelConfigPipeline, validators?: {[Glob]: ExtendableParcelConfigPipeline, ...}, - filePath: FilePath, - resolveFrom?: FilePath, + filePath: ProjectPath, + resolveFrom?: ProjectPath, |}; export type Environment = {| @@ -74,16 +72,30 @@ export type Environment = {| shouldOptimize: boolean, shouldScopeHoist: boolean, sourceMap: ?TargetSourceMapOptions, - loc: ?SourceLocation, + loc: ?InternalSourceLocation, +|}; + +export type InternalSourceLocation = {| + +filePath: ProjectPath, + /** inclusive */ + +start: {| + +line: number, + +column: number, + |}, + /** exclusive */ + +end: {| + +line: number, + +column: number, + |}, |}; export type Target = {| distEntry?: ?FilePath, - distDir: FilePath, + distDir: ProjectPath, env: Environment, name: string, publicUrl: string, - loc?: ?SourceLocation, + loc?: ?InternalSourceLocation, pipeline?: string, source?: FilePath | Array, |}; @@ -107,19 +119,26 @@ export type Dependency = {| specifierType: $Values, priority: $Values, needsStableName: boolean, + bundleBehavior: ?$Values, isEntry: boolean, isOptional: boolean, - loc: ?SourceLocation, + loc: ?InternalSourceLocation, env: Environment, meta: Meta, + resolverMeta?: ?Meta, target: ?Target, sourceAssetId: ?string, - sourcePath: ?string, + sourcePath: ?ProjectPath, sourceAssetType?: ?string, - resolveFrom: ?string, + resolveFrom: ?ProjectPath, symbols: ?Map< Symbol, - {|local: Symbol, loc: ?SourceLocation, isWeak: boolean, meta?: ?Meta|}, + {| + local: Symbol, + loc: ?InternalSourceLocation, + isWeak: boolean, + meta?: ?Meta, + |}, >, pipeline?: ?string, |}; @@ -137,7 +156,7 @@ export type Asset = {| id: ContentKey, committed: boolean, hash: ?string, - filePath: FilePath, + filePath: ProjectPath, query: ?QueryParameters, type: string, dependencies: Map, @@ -153,17 +172,27 @@ export type Asset = {| pipeline: ?string, astKey: ?string, astGenerator: ?ASTGenerator, - symbols: ?Map, + symbols: ?Map< + Symbol, + {|local: Symbol, loc: ?InternalSourceLocation, meta?: ?Meta|}, + >, sideEffects: boolean, uniqueKey: ?string, - configPath?: FilePath, + configPath?: ProjectPath, plugin: ?PackageName, configKeyPath?: string, |}; +export type InternalGlob = ProjectPath; + +export type InternalFile = {| + +filePath: ProjectPath, + +hash?: string, +|}; + export type FileInvalidation = {| type: 'file', - filePath: FilePath, + filePath: ProjectPath, |}; export type EnvInvalidation = {| @@ -181,21 +210,40 @@ export type RequestInvalidation = | EnvInvalidation | OptionInvalidation; +export type InternalFileInvalidation = {| + filePath: ProjectPath, +|}; + +export type InternalGlobInvalidation = {| + glob: InternalGlob, +|}; + +export type InternalFileAboveInvalidation = {| + fileName: string, + aboveFilePath: ProjectPath, +|}; + +export type InternalFileCreateInvalidation = + | InternalFileInvalidation + | InternalGlobInvalidation + | InternalFileAboveInvalidation; + export type DevDepRequest = {| specifier: DependencySpecifier, - resolveFrom: FilePath, + resolveFrom: ProjectPath, hash: string, - invalidateOnFileCreate?: Array, - invalidateOnFileChange?: Set, + invalidateOnFileCreate?: Array, + invalidateOnFileChange?: Set, additionalInvalidations?: Array<{| specifier: DependencySpecifier, - resolveFrom: FilePath, + resolveFrom: ProjectPath, + range?: ?SemverRange, |}>, |}; export type ParcelOptions = {| - entries: Array, - entryRoot: FilePath, + entries: Array, + entryRoot: ProjectPath, config?: DependencySpecifier, defaultConfig?: DependencySpecifier, env: EnvMap, @@ -211,7 +259,6 @@ export type ParcelOptions = {| shouldAutoInstall: boolean, logLevel: LogLevel, projectRoot: FilePath, - lockFile: ?FilePath, shouldProfile: boolean, shouldPatchConsole: boolean, detailedReport?: ?DetailedReportOptions, @@ -222,18 +269,20 @@ export type ParcelOptions = {| packageManager: PackageManager, additionalReporters: Array<{| packageName: DependencySpecifier, - resolveFrom: FilePath, + resolveFrom: ProjectPath, |}>, instanceId: string, +defaultTargetOptions: {| +shouldOptimize: boolean, - +shouldScopeHoist: boolean, + +shouldScopeHoist?: boolean, +sourceMaps: boolean, +publicUrl: string, - +distDir?: FilePath, + +distDir?: ProjectPath, +engines?: Engines, + +outputFormat?: OutputFormat, + +isLibrary?: boolean, |}, |}; @@ -254,13 +303,6 @@ export type Edge = {| type: TEdgeType, |}; -export interface Node { - id: ContentKey; - +type: string; - // $FlowFixMe - value: any; -} - export type AssetNode = {| id: ContentKey, +type: 'asset', @@ -296,7 +338,7 @@ export type RootNode = {|id: ContentKey, +type: 'root', value: string | null|}; export type AssetRequestInput = {| name?: string, // AssetGraph name, needed so that different graphs can isolated requests since the results are not stored - filePath: FilePath, + filePath: ProjectPath, env: Environment, isSource?: boolean, canDefer?: boolean, @@ -332,7 +374,7 @@ export type TransformationRequest = {| devDeps: Map, invalidDevDeps: Array<{| specifier: DependencySpecifier, - resolveFrom: FilePath, + resolveFrom: ProjectPath, |}>, |}; @@ -351,13 +393,13 @@ export type AssetRequestNode = {| export type EntrySpecifierNode = {| id: ContentKey, +type: 'entry_specifier', - value: DependencySpecifier, + value: ProjectPath, correspondingRequest?: string, |}; export type Entry = {| - filePath: FilePath, - packagePath: FilePath, + filePath: ProjectPath, + packagePath: ProjectPath, target?: string, |}; @@ -385,24 +427,35 @@ export type BundleGraphNode = | BundleGroupNode | BundleNode; +export type InternalDevDepOptions = {| + specifier: DependencySpecifier, + resolveFrom: ProjectPath, + range?: ?SemverRange, + additionalInvalidations?: Array<{| + specifier: DependencySpecifier, + resolveFrom: ProjectPath, + range?: ?SemverRange, + |}>, +|}; + export type Config = {| id: string, isSource: boolean, - searchPath: FilePath, + searchPath: ProjectPath, env: Environment, cacheKey: ?string, result: ConfigResult, - invalidateOnFileChange: Set, - invalidateOnFileCreate: Array, + invalidateOnFileChange: Set, + invalidateOnFileCreate: Array, invalidateOnEnvChange: Set, invalidateOnOptionChange: Set, - devDeps: Array, + devDeps: Array, invalidateOnStartup: boolean, |}; export type EntryRequest = {| specifier: DependencySpecifier, - result?: FilePath, + result?: ProjectPath, |}; export type EntryRequestNode = {| @@ -414,11 +467,11 @@ export type EntryRequestNode = {| export type TargetRequestNode = {| id: ContentKey, +type: 'target_request', - value: FilePath, + value: ProjectPath, |}; export type CacheEntry = {| - filePath: FilePath, + filePath: ProjectPath, env: Environment, hash: string, assets: Array, @@ -434,8 +487,8 @@ export type Bundle = {| env: Environment, entryAssetIds: Array, mainEntryId: ?ContentKey, - isEntry: ?boolean, - isInline: ?boolean, + needsStableName: ?boolean, + bundleBehavior: ?$Values, isSplittable: ?boolean, isPlaceholder?: boolean, target: Target, @@ -457,7 +510,7 @@ export type BundleGroupNode = {| |}; export type PackagedBundleInfo = {| - filePath: FilePath, + filePath: ProjectPath, stats: Stats, |}; diff --git a/packages/core/core/src/utils.js b/packages/core/core/src/utils.js index 619436c659c..22facc66606 100644 --- a/packages/core/core/src/utils.js +++ b/packages/core/core/src/utils.js @@ -1,9 +1,19 @@ // @flow strict-local import type {AbortSignal} from 'abortcontroller-polyfill/dist/cjs-ponyfill'; -import type {BundleGroup} from '@parcel/types'; -import type {ParcelOptions} from './types'; - +import type { + BundleGroup, + FilePath, + FileCreateInvalidation, + SourceLocation, +} from '@parcel/types'; +import type { + ParcelOptions, + InternalFileCreateInvalidation, + InternalSourceLocation, +} from './types'; + +import invariant from 'assert'; import baseX from 'base-x'; import {hashObject} from '@parcel/utils'; import {registerSerializableClass} from './serializer'; @@ -13,6 +23,7 @@ import Graph from './Graph'; import ParcelConfig from './ParcelConfig'; import {RequestGraph} from './RequestTracker'; import Config from './public/Config'; +import {fromProjectPath, toProjectPath} from './projectPath'; // flowlint-next-line untyped-import:off import packageJson from '../package.json'; @@ -45,7 +56,7 @@ export function registerCoreWithSerializer() { throw new Error('Expected package version to be a string'); } - // $FlowFixMe + // $FlowFixMe[incompatible-cast] for (let [name, ctor] of (Object.entries({ AssetGraph, Config, @@ -115,3 +126,69 @@ export function hashFromOption(value: mixed): string { return String(value); } + +export function invalidateOnFileCreateToInternal( + projectRoot: FilePath, + invalidation: FileCreateInvalidation, +): InternalFileCreateInvalidation { + if (invalidation.glob != null) { + return {glob: toProjectPath(projectRoot, invalidation.glob)}; + } else if (invalidation.filePath != null) { + return { + filePath: toProjectPath(projectRoot, invalidation.filePath), + }; + } else { + invariant( + invalidation.aboveFilePath != null && invalidation.fileName != null, + ); + return { + fileName: invalidation.fileName, + aboveFilePath: toProjectPath(projectRoot, invalidation.aboveFilePath), + }; + } +} + +export function fromInternalSourceLocation( + projectRoot: FilePath, + loc: ?InternalSourceLocation, +): ?SourceLocation { + if (!loc) return loc; + + return { + filePath: fromProjectPath(projectRoot, loc.filePath), + start: loc.start, + end: loc.end, + }; +} + +export function toInternalSourceLocation( + projectRoot: FilePath, + loc: ?SourceLocation, +): ?InternalSourceLocation { + if (!loc) return loc; + + return { + filePath: toProjectPath(projectRoot, loc.filePath), + start: loc.start, + end: loc.end, + }; +} +export function toInternalSymbols( + projectRoot: FilePath, + symbols: ?Map, +): ?Map< + Symbol, + {|loc: ?InternalSourceLocation, ...$Rest|}, +> { + if (!symbols) return symbols; + + return new Map( + [...symbols].map(([k, {loc, ...v}]) => [ + k, + { + ...v, + loc: toInternalSourceLocation(projectRoot, loc), + }, + ]), + ); +} diff --git a/packages/core/core/src/worker.js b/packages/core/core/src/worker.js index 224f76f91dd..6fdb6db46e6 100644 --- a/packages/core/core/src/worker.js +++ b/packages/core/core/src/worker.js @@ -63,12 +63,10 @@ async function loadConfig(cachePath, options) { return config; } - let processedConfig = nullthrows(await options.cache.get(cachePath)); - config = new ParcelConfig( - // $FlowFixMe - ((processedConfig: any): ProcessedParcelConfig), - options, + let processedConfig = nullthrows( + await options.cache.get(cachePath), ); + config = new ParcelConfig(processedConfig, options); parcelConfigCache.set(cachePath, config); return config; } diff --git a/packages/core/core/test/AssetGraph.test.js b/packages/core/core/test/AssetGraph.test.js index 4ca1805e42d..0741771adf7 100644 --- a/packages/core/core/test/AssetGraph.test.js +++ b/packages/core/core/test/AssetGraph.test.js @@ -8,36 +8,55 @@ import AssetGraph, { nodeFromEntryFile, nodeFromAsset, } from '../src/AssetGraph'; -import {createDependency} from '../src/Dependency'; -import {createAsset} from '../src/assetUtils'; +import {createDependency as _createDependency} from '../src/Dependency'; +import {createAsset as _createAsset} from '../src/assetUtils'; import {DEFAULT_ENV, DEFAULT_TARGETS} from './test-utils'; +import {toProjectPath as _toProjectPath} from '../src/projectPath'; const stats = {size: 0, time: 0}; +function createAsset(opts) { + return _createAsset('/', opts); +} + +function createDependency(opts) { + return _createDependency('/', opts); +} + +function toProjectPath(p) { + return _toProjectPath('/', p); +} + describe('AssetGraph', () => { it('initialization should create one root node with edges to entry_specifier nodes for each entry', () => { let graph = new AssetGraph(); graph.setRootConnections({ - entries: ['/path/to/index1', '/path/to/index2'], + entries: [ + toProjectPath('/path/to/index1'), + toProjectPath('/path/to/index2'), + ], }); assert(graph.hasNode(nullthrows(graph.rootNodeId))); - assert(graph.hasContentKey('entry_specifier:/path/to/index1')); - assert(graph.hasContentKey('entry_specifier:/path/to/index2')); + assert(graph.hasContentKey('entry_specifier:path/to/index1')); + assert(graph.hasContentKey('entry_specifier:path/to/index2')); }); it('resolveEntry should connect an entry_specifier node to entry_file nodes', () => { let graph = new AssetGraph(); graph.setRootConnections({ - entries: ['/path/to/index1', '/path/to/index2'], + entries: [ + toProjectPath('/path/to/index1'), + toProjectPath('/path/to/index2'), + ], }); graph.resolveEntry( - '/path/to/index1', + toProjectPath('/path/to/index1'), [ { - filePath: '/path/to/index1/src/main.js', - packagePath: '/path/to/index1', + filePath: toProjectPath('/path/to/index1/src/main.js'), + packagePath: toProjectPath('/path/to/index1'), }, ], '123', @@ -46,18 +65,18 @@ describe('AssetGraph', () => { assert( graph.hasContentKey( nodeFromEntryFile({ - filePath: '/path/to/index1/src/main.js', - packagePath: '/path/to/index1', + filePath: toProjectPath('/path/to/index1/src/main.js'), + packagePath: toProjectPath('/path/to/index1'), }).id, ), ); assert( graph.hasEdge( - graph.getNodeIdByContentKey('entry_specifier:/path/to/index1'), + graph.getNodeIdByContentKey('entry_specifier:path/to/index1'), graph.getNodeIdByContentKey( nodeFromEntryFile({ - filePath: '/path/to/index1/src/main.js', - packagePath: '/path/to/index1', + filePath: toProjectPath('/path/to/index1/src/main.js'), + packagePath: toProjectPath('/path/to/index1'), }).id, ), ), @@ -67,37 +86,46 @@ describe('AssetGraph', () => { it('resolveTargets should connect an entry_file node to dependencies for each target', () => { let graph = new AssetGraph(); graph.setRootConnections({ - entries: ['/path/to/index1', '/path/to/index2'], + entries: [ + toProjectPath('/path/to/index1'), + toProjectPath('/path/to/index2'), + ], }); graph.resolveEntry( - '/path/to/index1', + toProjectPath('/path/to/index1'), [ { - filePath: '/path/to/index1/src/main.js', - packagePath: '/path/to/index1', + filePath: toProjectPath('/path/to/index1/src/main.js'), + packagePath: toProjectPath('/path/to/index1'), }, ], '1', ); graph.resolveEntry( - '/path/to/index2', + toProjectPath('/path/to/index2'), [ { - filePath: '/path/to/index2/src/main.js', - packagePath: '/path/to/index2', + filePath: toProjectPath('/path/to/index2/src/main.js'), + packagePath: toProjectPath('/path/to/index2'), }, ], '2', ); graph.resolveTargets( - {filePath: '/path/to/index1/src/main.js', packagePath: '/path/to/index1'}, + { + filePath: toProjectPath('/path/to/index1/src/main.js'), + packagePath: toProjectPath('/path/to/index1'), + }, DEFAULT_TARGETS, '3', ); graph.resolveTargets( - {filePath: '/path/to/index2/src/main.js', packagePath: '/path/to/index2'}, + { + filePath: toProjectPath('/path/to/index2/src/main.js'), + packagePath: toProjectPath('/path/to/index2'), + }, DEFAULT_TARGETS, '4', ); @@ -105,7 +133,7 @@ describe('AssetGraph', () => { assert( graph.hasContentKey( createDependency({ - specifier: '/path/to/index1/src/main.js', + specifier: 'path/to/index1/src/main.js', specifierType: 'esm', target: DEFAULT_TARGETS[0], env: DEFAULT_ENV, @@ -115,7 +143,7 @@ describe('AssetGraph', () => { assert( graph.hasContentKey( createDependency({ - specifier: '/path/to/index2/src/main.js', + specifier: 'path/to/index2/src/main.js', specifierType: 'esm', target: DEFAULT_TARGETS[0], env: DEFAULT_ENV, @@ -125,30 +153,30 @@ describe('AssetGraph', () => { assert.deepEqual(Array.from(graph.getAllEdges()), [ { from: graph.rootNodeId, - to: graph.getNodeIdByContentKey('entry_specifier:/path/to/index1'), + to: graph.getNodeIdByContentKey('entry_specifier:path/to/index1'), type: 1, }, { from: graph.rootNodeId, - to: graph.getNodeIdByContentKey('entry_specifier:/path/to/index2'), + to: graph.getNodeIdByContentKey('entry_specifier:path/to/index2'), type: 1, }, { - from: graph.getNodeIdByContentKey('entry_specifier:/path/to/index1'), + from: graph.getNodeIdByContentKey('entry_specifier:path/to/index1'), to: graph.getNodeIdByContentKey( nodeFromEntryFile({ - filePath: '/path/to/index1/src/main.js', - packagePath: '/path/to/index1', + filePath: toProjectPath('/path/to/index1/src/main.js'), + packagePath: toProjectPath('/path/to/index1'), }).id, ), type: 1, }, { - from: graph.getNodeIdByContentKey('entry_specifier:/path/to/index2'), + from: graph.getNodeIdByContentKey('entry_specifier:path/to/index2'), to: graph.getNodeIdByContentKey( nodeFromEntryFile({ - filePath: '/path/to/index2/src/main.js', - packagePath: '/path/to/index2', + filePath: toProjectPath('/path/to/index2/src/main.js'), + packagePath: toProjectPath('/path/to/index2'), }).id, ), type: 1, @@ -156,13 +184,13 @@ describe('AssetGraph', () => { { from: graph.getNodeIdByContentKey( nodeFromEntryFile({ - filePath: '/path/to/index1/src/main.js', - packagePath: '/path/to/index1', + filePath: toProjectPath('/path/to/index1/src/main.js'), + packagePath: toProjectPath('/path/to/index1'), }).id, ), to: graph.getNodeIdByContentKey( createDependency({ - specifier: '/path/to/index1/src/main.js', + specifier: 'path/to/index1/src/main.js', specifierType: 'esm', target: DEFAULT_TARGETS[0], env: DEFAULT_ENV, @@ -173,13 +201,13 @@ describe('AssetGraph', () => { { from: graph.getNodeIdByContentKey( nodeFromEntryFile({ - filePath: '/path/to/index2/src/main.js', - packagePath: '/path/to/index2', + filePath: toProjectPath('/path/to/index2/src/main.js'), + packagePath: toProjectPath('/path/to/index2'), }).id, ), to: graph.getNodeIdByContentKey( createDependency({ - specifier: '/path/to/index2/src/main.js', + specifier: 'path/to/index2/src/main.js', specifierType: 'esm', target: DEFAULT_TARGETS[0], env: DEFAULT_ENV, @@ -194,27 +222,39 @@ describe('AssetGraph', () => { let graph = new AssetGraph(); graph.setRootConnections({ targets: DEFAULT_TARGETS, - entries: ['/path/to/index'], + entries: [toProjectPath('/path/to/index')], }); graph.resolveEntry( - '/path/to/index', - [{filePath: '/path/to/index/src/main.js', packagePath: '/path/to/index'}], + toProjectPath('/path/to/index'), + [ + { + filePath: toProjectPath('/path/to/index/src/main.js'), + packagePath: toProjectPath('/path/to/index'), + }, + ], '1', ); graph.resolveTargets( - {filePath: '/path/to/index/src/main.js', packagePath: '/path/to/index'}, + { + filePath: toProjectPath('/path/to/index/src/main.js'), + packagePath: toProjectPath('/path/to/index'), + }, DEFAULT_TARGETS, '2', ); let dep = createDependency({ - specifier: '/path/to/index/src/main.js', + specifier: 'path/to/index/src/main.js', specifierType: 'esm', target: DEFAULT_TARGETS[0], env: DEFAULT_ENV, }); - let req = {filePath: '/index.js', env: DEFAULT_ENV, query: {}}; + let req = { + filePath: toProjectPath('/index.js'), + env: DEFAULT_ENV, + query: {}, + }; graph.resolveDependency(dep, req, '3'); let assetGroupNodeId = graph.getNodeIdByContentKey( @@ -224,7 +264,11 @@ describe('AssetGraph', () => { assert(graph.nodes.has(assetGroupNodeId)); assert(graph.hasEdge(dependencyNodeId, assetGroupNodeId)); - let req2 = {filePath: '/index.jsx', env: DEFAULT_ENV, query: {}}; + let req2 = { + filePath: toProjectPath('/index.jsx'), + env: DEFAULT_ENV, + query: {}, + }; graph.resolveDependency(dep, req2, '4'); let assetGroupNodeId2 = graph.getNodeIdByContentKey( @@ -244,31 +288,39 @@ describe('AssetGraph', () => { let graph = new AssetGraph(); graph.setRootConnections({ targets: DEFAULT_TARGETS, - entries: ['/path/to/index'], + entries: [toProjectPath('/path/to/index')], }); graph.resolveEntry( - '/path/to/index', - [{filePath: '/path/to/index/src/main.js', packagePath: '/path/to/index'}], + toProjectPath('/path/to/index'), + [ + { + filePath: toProjectPath('/path/to/index/src/main.js'), + packagePath: toProjectPath('/path/to/index'), + }, + ], '1', ); graph.resolveTargets( - {filePath: '/path/to/index/src/main.js', packagePath: '/path/to/index'}, + { + filePath: toProjectPath('/path/to/index/src/main.js'), + packagePath: toProjectPath('/path/to/index'), + }, DEFAULT_TARGETS, '2', ); let dep = createDependency({ - specifier: '/path/to/index/src/main.js', + specifier: 'path/to/index/src/main.js', specifierType: 'esm', target: DEFAULT_TARGETS[0], env: DEFAULT_ENV, sourcePath: '', }); - let filePath = '/index.js'; + let sourcePath = '/index.js'; + let filePath = toProjectPath(sourcePath); let req = {filePath, env: DEFAULT_ENV, query: {}}; graph.resolveDependency(dep, req, '3'); - let sourcePath = filePath; let assets = [ createAsset({ id: '1', @@ -402,29 +454,40 @@ describe('AssetGraph', () => { // to the asset's dependency instead of the asset group. it('resolveAssetGroup should handle dependent assets in asset groups', () => { let graph = new AssetGraph(); - graph.setRootConnections({targets: DEFAULT_TARGETS, entries: ['./index']}); + graph.setRootConnections({ + targets: DEFAULT_TARGETS, + entries: [toProjectPath('/index')], + }); graph.resolveEntry( - './index', - [{filePath: '/path/to/index/src/main.js', packagePath: '/path/to/index'}], + toProjectPath('/index'), + [ + { + filePath: toProjectPath('/path/to/index/src/main.js'), + packagePath: toProjectPath('/path/to/index'), + }, + ], '1', ); graph.resolveTargets( - {filePath: '/path/to/index/src/main.js', packagePath: '/path/to/index'}, + { + filePath: toProjectPath('/path/to/index/src/main.js'), + packagePath: toProjectPath('/path/to/index'), + }, DEFAULT_TARGETS, '2', ); let dep = createDependency({ - specifier: '/path/to/index/src/main.js', + specifier: 'path/to/index/src/main.js', specifierType: 'esm', env: DEFAULT_ENV, target: DEFAULT_TARGETS[0], }); - let filePath = '/index.js'; + let sourcePath = '/index.js'; + let filePath = toProjectPath(sourcePath); let req = {filePath, env: DEFAULT_ENV, query: {}}; graph.resolveDependency(dep, req, '123'); - let sourcePath = filePath; let dep1 = createDependency({ specifier: 'dependent-asset-1', specifierType: 'esm', @@ -500,7 +563,11 @@ describe('AssetGraph', () => { let graph = new AssetGraph(); // index - let indexAssetGroup = {filePath: '/index.js', env: DEFAULT_ENV, query: {}}; + let indexAssetGroup = { + filePath: toProjectPath('/index.js'), + env: DEFAULT_ENV, + query: {}, + }; graph.setRootConnections({assetGroups: [indexAssetGroup]}); let indexFooDep = createDependency({ specifier: './foo', @@ -516,7 +583,7 @@ describe('AssetGraph', () => { }); let indexAsset = createAsset({ id: 'assetIndex', - filePath: '/index.js', + filePath: toProjectPath('/index.js'), type: 'js', isSource: true, hash: '#4', @@ -530,7 +597,11 @@ describe('AssetGraph', () => { graph.resolveAssetGroup(indexAssetGroup, [indexAsset], '0'); // index imports foo - let fooAssetGroup = {filePath: '/foo.js', env: DEFAULT_ENV, query: {}}; + let fooAssetGroup = { + filePath: toProjectPath('/foo.js'), + env: DEFAULT_ENV, + query: {}, + }; graph.resolveDependency(indexFooDep, fooAssetGroup, '0'); let fooAssetGroupNode = nodeFromAssetGroup(fooAssetGroup); let fooUtilsDep = createDependency({ @@ -542,7 +613,7 @@ describe('AssetGraph', () => { let fooUtilsDepNode = nodeFromDep(fooUtilsDep); let fooAsset = createAsset({ id: 'assetFoo', - filePath: '/foo.js', + filePath: toProjectPath('/foo.js'), type: 'js', isSource: true, hash: '#1', @@ -552,7 +623,11 @@ describe('AssetGraph', () => { }); let fooAssetNode = nodeFromAsset(fooAsset); graph.resolveAssetGroup(fooAssetGroup, [fooAsset], '0'); - let utilsAssetGroup = {filePath: '/utils.js', env: DEFAULT_ENV, query: {}}; + let utilsAssetGroup = { + filePath: toProjectPath('/utils.js'), + env: DEFAULT_ENV, + query: {}, + }; let utilsAssetGroupNode = nodeFromAssetGroup(utilsAssetGroup); graph.resolveDependency(fooUtilsDep, utilsAssetGroup, '0'); @@ -568,7 +643,11 @@ describe('AssetGraph', () => { assert(node.hasDeferred); // index also imports bar - let barAssetGroup = {filePath: '/bar.js', env: DEFAULT_ENV, query: {}}; + let barAssetGroup = { + filePath: toProjectPath('/bar.js'), + env: DEFAULT_ENV, + query: {}, + }; graph.resolveDependency(indexBarDep, barAssetGroup, '0'); let barAssetGroupNode = nodeFromAssetGroup(barAssetGroup); let barUtilsDep = createDependency({ @@ -579,7 +658,7 @@ describe('AssetGraph', () => { }); let barAsset = createAsset({ id: 'assetBar', - filePath: '/bar.js', + filePath: toProjectPath('/bar.js'), type: 'js', isSource: true, hash: '#2', diff --git a/packages/core/core/test/BundleGraph.test.js b/packages/core/core/test/BundleGraph.test.js index af38cdc3100..6df5aeef958 100644 --- a/packages/core/core/test/BundleGraph.test.js +++ b/packages/core/core/test/BundleGraph.test.js @@ -4,8 +4,17 @@ import assert from 'assert'; import BundleGraph from '../src/BundleGraph'; import {DEFAULT_ENV, DEFAULT_TARGETS} from './test-utils'; import AssetGraph, {nodeFromAssetGroup} from '../src/AssetGraph'; -import {createAsset} from '../src/assetUtils'; -import {createDependency} from '../src/Dependency'; +import {createAsset as _createAsset} from '../src/assetUtils'; +import {createDependency as _createDependency} from '../src/Dependency'; +import {toProjectPath} from '../src/projectPath'; + +function createAsset(opts) { + return _createAsset('/', opts); +} + +function createDependency(opts) { + return _createDependency('/', opts); +} const id1 = '0123456789abcdef0123456789abcdef'; const id2 = '9876543210fedcba9876543210fedcba'; @@ -45,26 +54,35 @@ function getAssets(bundleGraph) { const stats = {size: 0, time: 0}; function createMockAssetGraph(ids: [string, string]) { let graph = new AssetGraph(); - graph.setRootConnections({entries: ['./index']}); + graph.setRootConnections({entries: [toProjectPath('/', '/index')]}); graph.resolveEntry( - './index', - [{filePath: '/path/to/index/src/main.js', packagePath: '/path/to/index'}], + toProjectPath('/', '/index'), + [ + { + filePath: toProjectPath('/', '/path/to/index/src/main.js'), + packagePath: toProjectPath('/', '/path/to/index'), + }, + ], '1', ); graph.resolveTargets( - {filePath: '/path/to/index/src/main.js', packagePath: '/path/to/index'}, + { + filePath: toProjectPath('/', '/path/to/index/src/main.js'), + packagePath: toProjectPath('/', '/path/to/index'), + }, DEFAULT_TARGETS, '2', ); let dep = createDependency({ - specifier: '/path/to/index/src/main.js', + specifier: 'path/to/index/src/main.js', specifierType: 'esm', env: DEFAULT_ENV, target: DEFAULT_TARGETS[0], }); - let filePath = '/index.js'; + let sourcePath = '/index.js'; + let filePath = toProjectPath('/', sourcePath); let req = {filePath, env: DEFAULT_ENV, query: {}}; graph.resolveDependency(dep, nodeFromAssetGroup(req).value, '3'); @@ -72,7 +90,7 @@ function createMockAssetGraph(ids: [string, string]) { specifier: 'dependent-asset-1', specifierType: 'esm', env: DEFAULT_ENV, - sourcePath: filePath, + sourcePath, }); let assets = [ diff --git a/packages/core/core/test/ContentGraph.test.js b/packages/core/core/test/ContentGraph.test.js index 046500c480e..3cb0e66d95d 100644 --- a/packages/core/core/test/ContentGraph.test.js +++ b/packages/core/core/test/ContentGraph.test.js @@ -7,7 +7,7 @@ describe('ContentGraph', () => { it('should addNodeByContentKey if no node exists with the content key', () => { let graph = new ContentGraph(); - const node = {id: 'contentKey', type: 'mynode', value: ' 1'}; + const node = {}; const nodeId1 = graph.addNodeByContentKey('contentKey', node); @@ -16,29 +16,23 @@ describe('ContentGraph', () => { assert.deepEqual(graph.getNodeByContentKey('contentKey'), node); }); - it('should update the node through addNodeByContentKey if a node with the content key exists', () => { + it('should throw if a node with the content key already exists', () => { let graph = new ContentGraph(); - const node1 = {id: 'contentKey', value: '1', type: 'mynode'}; - const node2 = {id: 'contentKey', value: '2', type: 'mynode'}; + graph.addNodeByContentKey('contentKey', {}); - const nodeId1 = graph.addNodeByContentKey('contentKey', node1); - const nodeId2 = graph.addNodeByContentKey('contentKey', node2); - - assert.deepEqual(graph.getNode(nodeId1), node1); - assert(graph.hasContentKey('contentKey')); - - assert.equal(nodeId1, nodeId2); - assert.deepEqual(graph.getNode(nodeId2), node2); + assert.throws(() => { + graph.addNodeByContentKey('contentKey', {}); + }, /already has content key/); }); it('should remove the content key from graph when node is removed', () => { let graph = new ContentGraph(); - const node1 = {id: 'contentKey', value: '1', type: 'mynode'}; + const node1 = {}; const nodeId1 = graph.addNodeByContentKey('contentKey', node1); - assert.deepEqual(graph.getNode(nodeId1), node1); + assert.equal(graph.getNode(nodeId1), node1); assert(graph.hasContentKey('contentKey')); graph.removeNode(nodeId1); diff --git a/packages/core/core/test/EntryRequest.test.js b/packages/core/core/test/EntryRequest.test.js index 7a1bd7ed9bc..ccc870a7a02 100644 --- a/packages/core/core/test/EntryRequest.test.js +++ b/packages/core/core/test/EntryRequest.test.js @@ -4,7 +4,6 @@ import path from 'path'; import {inputFS as fs} from '@parcel/test-utils'; import {EntryResolver} from '../src/requests/EntryRequest'; import {DEFAULT_OPTIONS as _DEFAULT_OPTIONS} from './test-utils'; -import {md} from '@parcel/diagnostic'; const DEFAULT_OPTIONS = { ..._DEFAULT_OPTIONS, @@ -34,10 +33,6 @@ const INVALID_TARGET_SOURCE_NOT_FILE_FIXTURE_PATH = path.join( 'fixtures/invalid-target-source-not-file', ); -function packagePath(fixturePath) { - return path.join(path.relative(fs.cwd(), fixturePath), '/package.json'); -} - describe('EntryResolver', function() { let entryResolver = new EntryResolver({...DEFAULT_OPTIONS}); @@ -47,9 +42,37 @@ describe('EntryResolver', function() { await assert.rejects( () => entryResolver.resolveEntry(INVALID_SOURCE_MISSING_FIXTURE_PATH), { - message: md`missing.js in ${packagePath( - INVALID_SOURCE_MISSING_FIXTURE_PATH, - )}#source does not exist`, + diagnostics: [ + { + origin: '@parcel/core', + message: `${path.join( + path.relative(fs.cwd(), INVALID_SOURCE_MISSING_FIXTURE_PATH), + 'missing.js', + )} does not exist.`, + codeFrames: [ + { + filePath: path.join( + INVALID_SOURCE_MISSING_FIXTURE_PATH, + 'package.json', + ), + codeHighlights: [ + { + message: undefined, + start: { + line: 4, + column: 13, + }, + end: { + line: 4, + column: 24, + }, + }, + ], + }, + ], + hints: [], + }, + ], }, ); }); @@ -59,9 +82,36 @@ describe('EntryResolver', function() { await assert.rejects( () => entryResolver.resolveEntry(INVALID_SOURCE_NOT_FILE_FIXTURE_PATH), { - message: md`src in ${packagePath( - INVALID_SOURCE_NOT_FILE_FIXTURE_PATH, - )}#source is not a file`, + diagnostics: [ + { + origin: '@parcel/core', + message: `${path.join( + path.relative(fs.cwd(), INVALID_SOURCE_NOT_FILE_FIXTURE_PATH), + 'src', + )} is not a file.`, + codeFrames: [ + { + filePath: path.join( + INVALID_SOURCE_NOT_FILE_FIXTURE_PATH, + 'package.json', + ), + codeHighlights: [ + { + message: undefined, + start: { + line: 4, + column: 13, + }, + end: { + line: 4, + column: 17, + }, + }, + ], + }, + ], + }, + ], }, ); }); @@ -72,9 +122,40 @@ describe('EntryResolver', function() { () => entryResolver.resolveEntry(INVALID_TARGET_SOURCE_MISSING_FIXTURE_PATH), { - message: md`missing.js in ${packagePath( - INVALID_TARGET_SOURCE_MISSING_FIXTURE_PATH, - )}#targets["a"].source does not exist`, + diagnostics: [ + { + origin: '@parcel/core', + message: `${path.join( + path.relative( + fs.cwd(), + INVALID_TARGET_SOURCE_MISSING_FIXTURE_PATH, + ), + 'missing.js', + )} does not exist.`, + codeFrames: [ + { + filePath: path.join( + INVALID_TARGET_SOURCE_MISSING_FIXTURE_PATH, + 'package.json', + ), + codeHighlights: [ + { + message: undefined, + start: { + line: 6, + column: 17, + }, + end: { + line: 6, + column: 28, + }, + }, + ], + }, + ], + hints: [], + }, + ], }, ); }); @@ -85,9 +166,39 @@ describe('EntryResolver', function() { () => entryResolver.resolveEntry(INVALID_TARGET_SOURCE_NOT_FILE_FIXTURE_PATH), { - message: md`src in ${packagePath( - INVALID_TARGET_SOURCE_NOT_FILE_FIXTURE_PATH, - )}#targets["a"].source is not a file`, + diagnostics: [ + { + origin: '@parcel/core', + message: `${path.join( + path.relative( + fs.cwd(), + INVALID_TARGET_SOURCE_NOT_FILE_FIXTURE_PATH, + ), + 'src', + )} is not a file.`, + codeFrames: [ + { + filePath: path.join( + INVALID_TARGET_SOURCE_NOT_FILE_FIXTURE_PATH, + 'package.json', + ), + codeHighlights: [ + { + message: undefined, + start: { + line: 6, + column: 17, + }, + end: { + line: 6, + column: 21, + }, + }, + ], + }, + ], + }, + ], }, ); }); diff --git a/packages/core/core/test/Graph.test.js b/packages/core/core/test/Graph.test.js index 84b1154806e..fb0849966da 100644 --- a/packages/core/core/test/Graph.test.js +++ b/packages/core/core/test/Graph.test.js @@ -15,7 +15,7 @@ describe('Graph', () => { it('addNode should add a node to the graph', () => { let graph = new Graph(); - let node = {id: 'do not use', type: 'mynode', value: 'a'}; + let node = {}; let id = graph.addNode(node); assert.equal(graph.nodes.get(id), node); }); @@ -52,7 +52,7 @@ describe('Graph', () => { it("errors when adding an edge to a node that doesn't exist", () => { let graph = new Graph(); - let node = graph.addNode({id: 'foo', type: 'mynode', value: null}); + let node = graph.addNode({}); assert.throws(() => { graph.addEdge(node, toNodeId(-1)); }, /"to" node '-1' not found/); @@ -60,7 +60,7 @@ describe('Graph', () => { it("errors when adding an edge from a node that doesn't exist", () => { let graph = new Graph(); - let node = graph.addNode({id: 'foo', type: 'mynode', value: null}); + let node = graph.addNode({}); assert.throws(() => { graph.addEdge(toNodeId(-1), node); }, /"from" node '-1' not found/); @@ -68,24 +68,24 @@ describe('Graph', () => { it('hasNode should return a boolean based on whether the node exists in the graph', () => { let graph = new Graph(); - let node = graph.addNode({id: 'a', type: 'mynode', value: 'a'}); + let node = graph.addNode({}); assert(graph.hasNode(node)); assert(!graph.hasNode(toNodeId(-1))); }); it('addEdge should add an edge to the graph', () => { let graph = new Graph(); - let nodeA = graph.addNode({id: 'a', type: 'mynode', value: null}); - let nodeB = graph.addNode({id: 'b', type: 'mynode', value: null}); + let nodeA = graph.addNode('a'); + let nodeB = graph.addNode('b'); graph.addEdge(nodeA, nodeB); assert(graph.hasEdge(nodeA, nodeB)); }); it('isOrphanedNode should return true or false if the node is orphaned or not', () => { let graph = new Graph(); - let nodeA = graph.addNode({id: 'a', type: 'mynode', value: 'a'}); - let nodeB = graph.addNode({id: 'b', type: 'mynode', value: 'b'}); - let nodeC = graph.addNode({id: 'c', type: 'mynode', value: 'c'}); + let nodeA = graph.addNode('a'); + let nodeB = graph.addNode('b'); + let nodeC = graph.addNode('c'); graph.addEdge(nodeA, nodeB); graph.addEdge(nodeA, nodeC, 1); assert(graph.isOrphanedNode(nodeA)); @@ -100,10 +100,10 @@ describe('Graph', () => { // / // c let graph = new Graph(); - let nodeA = graph.addNode({id: 'a', type: 'mynode', value: 'a'}); - let nodeB = graph.addNode({id: 'b', type: 'mynode', value: 'b'}); - let nodeC = graph.addNode({id: 'c', type: 'mynode', value: 'c'}); - let nodeD = graph.addNode({id: 'd', type: 'mynode', value: 'd'}); + let nodeA = graph.addNode('a'); + let nodeB = graph.addNode('b'); + let nodeC = graph.addNode('c'); + let nodeD = graph.addNode('d'); graph.addEdge(nodeA, nodeB); graph.addEdge(nodeA, nodeD); graph.addEdge(nodeB, nodeC); @@ -139,13 +139,13 @@ describe('Graph', () => { // f let graph = new Graph(); - let nodeA = graph.addNode({id: 'a', type: 'mynode', value: 'a'}); - let nodeB = graph.addNode({id: 'b', type: 'mynode', value: 'b'}); - let nodeC = graph.addNode({id: 'c', type: 'mynode', value: 'c'}); - let nodeD = graph.addNode({id: 'd', type: 'mynode', value: 'd'}); - let nodeE = graph.addNode({id: 'e', type: 'mynode', value: 'e'}); - let nodeF = graph.addNode({id: 'f', type: 'mynode', value: 'f'}); - let nodeG = graph.addNode({id: 'g', type: 'mynode', value: 'g'}); + let nodeA = graph.addNode('a'); + let nodeB = graph.addNode('b'); + let nodeC = graph.addNode('c'); + let nodeD = graph.addNode('d'); + let nodeE = graph.addNode('e'); + let nodeF = graph.addNode('f'); + let nodeG = graph.addNode('g'); graph.addEdge(nodeA, nodeB); graph.addEdge(nodeA, nodeC); @@ -182,13 +182,13 @@ describe('Graph', () => { // f let graph = new Graph(); - let nodeA = graph.addNode({id: 'a', type: 'mynode', value: 'a'}); - let nodeB = graph.addNode({id: 'b', type: 'mynode', value: 'b'}); - let nodeC = graph.addNode({id: 'c', type: 'mynode', value: 'c'}); - let nodeD = graph.addNode({id: 'd', type: 'mynode', value: 'd'}); - let nodeE = graph.addNode({id: 'e', type: 'mynode', value: 'e'}); - let nodeF = graph.addNode({id: 'f', type: 'mynode', value: 'f'}); - let nodeG = graph.addNode({id: 'g', type: 'mynode', value: 'g'}); + let nodeA = graph.addNode('a'); + let nodeB = graph.addNode('b'); + let nodeC = graph.addNode('c'); + let nodeD = graph.addNode('d'); + let nodeE = graph.addNode('e'); + let nodeF = graph.addNode('f'); + let nodeG = graph.addNode('g'); graph.setRootNodeId(nodeA); graph.addEdge(nodeA, nodeB); @@ -217,11 +217,11 @@ describe('Graph', () => { // \ / | // e ----- let graph = new Graph(); - let nodeA = graph.addNode({id: 'a', type: 'mynode', value: 'a'}); - let nodeB = graph.addNode({id: 'b', type: 'mynode', value: 'b'}); - let nodeC = graph.addNode({id: 'c', type: 'mynode', value: 'c'}); - let nodeD = graph.addNode({id: 'd', type: 'mynode', value: 'd'}); - let nodeE = graph.addNode({id: 'e', type: 'mynode', value: 'e'}); + let nodeA = graph.addNode('a'); + let nodeB = graph.addNode('b'); + let nodeC = graph.addNode('c'); + let nodeD = graph.addNode('d'); + let nodeE = graph.addNode('e'); graph.setRootNodeId(nodeA); graph.addEdge(nodeA, nodeB); @@ -249,8 +249,8 @@ describe('Graph', () => { it('removing a node with only one inbound edge does not cause it to be removed as an orphan', () => { let graph = new Graph(); - let nodeA = graph.addNode({id: 'a', type: 'mynode', value: 'a'}); - let nodeB = graph.addNode({id: 'b', type: 'mynode', value: 'b'}); + let nodeA = graph.addNode('a'); + let nodeB = graph.addNode('b'); graph.setRootNodeId(nodeA); graph.addEdge(nodeA, nodeB); @@ -267,13 +267,13 @@ describe('Graph', () => { it("replaceNodeIdsConnectedTo should update a node's downstream nodes", () => { let graph = new Graph(); - let nodeA = graph.addNode({id: 'a', type: 'mynode', value: 'a'}); - let nodeB = graph.addNode({id: 'b', type: 'mynode', value: 'b'}); - let nodeC = graph.addNode({id: 'c', type: 'mynode', value: 'c'}); + let nodeA = graph.addNode('a'); + let nodeB = graph.addNode('b'); + let nodeC = graph.addNode('c'); graph.addEdge(nodeA, nodeB); graph.addEdge(nodeA, nodeC); - let nodeD = graph.addNode({id: 'd', type: 'mynode', value: 'd'}); + let nodeD = graph.addNode('d'); graph.replaceNodeIdsConnectedTo(nodeA, [nodeB, nodeD]); assert(graph.hasNode(nodeA)); @@ -288,10 +288,10 @@ describe('Graph', () => { it('traverses along edge types if a filter is given', () => { let graph = new Graph(); - let nodeA = graph.addNode({id: 'a', type: 'mynode', value: 'a'}); - let nodeB = graph.addNode({id: 'b', type: 'mynode', value: 'b'}); - let nodeC = graph.addNode({id: 'c', type: 'mynode', value: 'c'}); - let nodeD = graph.addNode({id: 'd', type: 'mynode', value: 'd'}); + let nodeA = graph.addNode('a'); + let nodeB = graph.addNode('b'); + let nodeC = graph.addNode('c'); + let nodeD = graph.addNode('d'); graph.addEdge(nodeA, nodeB, 2); graph.addEdge(nodeA, nodeD); diff --git a/packages/core/core/test/InternalAsset.test.js b/packages/core/core/test/InternalAsset.test.js index 1dbb732b393..a29a2befc29 100644 --- a/packages/core/core/test/InternalAsset.test.js +++ b/packages/core/core/test/InternalAsset.test.js @@ -2,9 +2,14 @@ import assert from 'assert'; import UncommittedAsset from '../src/UncommittedAsset'; -import {createAsset} from '../src/assetUtils'; +import {createAsset as _createAsset} from '../src/assetUtils'; import {createEnvironment} from '../src/Environment'; import {DEFAULT_OPTIONS} from './test-utils'; +import {toProjectPath} from '../src/projectPath'; + +function createAsset(opts) { + return _createAsset('/', opts); +} const stats = {time: 0, size: 0}; @@ -12,7 +17,7 @@ describe('InternalAsset', () => { it('only includes connected files once per filePath', () => { let asset = new UncommittedAsset({ value: createAsset({ - filePath: '/foo/asset.js', + filePath: toProjectPath('/', '/foo/asset.js'), env: createEnvironment(), stats, type: 'js', @@ -20,12 +25,12 @@ describe('InternalAsset', () => { }), options: DEFAULT_OPTIONS, }); - asset.invalidateOnFileChange('/foo/file'); - asset.invalidateOnFileChange('/foo/file'); + asset.invalidateOnFileChange(toProjectPath('/', '/foo/file')); + asset.invalidateOnFileChange(toProjectPath('/', '/foo/file')); assert.deepEqual(asset.getInvalidations(), [ { type: 'file', - filePath: '/foo/file', + filePath: 'foo/file', }, ]); }); @@ -33,7 +38,7 @@ describe('InternalAsset', () => { it('only includes dependencies once per id', () => { let asset = new UncommittedAsset({ value: createAsset({ - filePath: '/foo/asset.js', + filePath: toProjectPath('/', '/foo/asset.js'), env: createEnvironment(), stats, type: 'js', @@ -52,7 +57,7 @@ describe('InternalAsset', () => { it('includes different dependencies if their id differs', () => { let asset = new UncommittedAsset({ value: createAsset({ - filePath: '/foo/asset.js', + filePath: toProjectPath('/', '/foo/asset.js'), env: createEnvironment(), stats, type: 'js', diff --git a/packages/core/core/test/Parcel.test.js b/packages/core/core/test/Parcel.test.js index 60f1be5b7da..ac3025d671b 100644 --- a/packages/core/core/test/Parcel.test.js +++ b/packages/core/core/test/Parcel.test.js @@ -83,6 +83,7 @@ function createParcel(opts?: InitialParcelOptions) { path.dirname(require.resolve('@parcel/test-utils')), '.parcelrc-no-reporters', ), + shouldDisableCache: true, ...opts, }); } diff --git a/packages/core/core/test/ParcelConfig.test.js b/packages/core/core/test/ParcelConfig.test.js index 83587ad8920..e58cfb8190a 100644 --- a/packages/core/core/test/ParcelConfig.test.js +++ b/packages/core/core/test/ParcelConfig.test.js @@ -8,22 +8,25 @@ import logger from '@parcel/logger'; import {inputFS} from '@parcel/test-utils'; import {parseAndProcessConfig} from '../src/requests/ParcelConfigRequest'; import {DEFAULT_OPTIONS} from './test-utils'; +import {toProjectPath} from '../src/projectPath'; + +const PARCELRC_PATH = toProjectPath('/', '/.parcelrc'); describe('ParcelConfig', () => { describe('matchGlobMap', () => { let config = new ParcelConfig( { - filePath: '.parcelrc', + filePath: PARCELRC_PATH, bundler: undefined, packagers: { '*.css': { packageName: 'parcel-packager-css', - resolveFrom: '.parcelrc', + resolveFrom: PARCELRC_PATH, keyPath: '/packagers/*.css', }, '*.js': { packageName: 'parcel-packager-js', - resolveFrom: '.parcelrc', + resolveFrom: PARCELRC_PATH, keyPath: '/packagers/*.js', }, }, @@ -32,15 +35,21 @@ describe('ParcelConfig', () => { ); it('should return null array if no glob matches', () => { - let result = config.matchGlobMap('foo.wasm', config.packagers); + let result = config.matchGlobMap( + toProjectPath('/', '/foo.wasm'), + config.packagers, + ); assert.deepEqual(result, null); }); it('should return a matching pipeline', () => { - let result = config.matchGlobMap('foo.js', config.packagers); + let result = config.matchGlobMap( + toProjectPath('/', '/foo.js'), + config.packagers, + ); assert.deepEqual(result, { packageName: 'parcel-packager-js', - resolveFrom: '.parcelrc', + resolveFrom: PARCELRC_PATH, keyPath: '/packagers/*.js', }); }); @@ -49,13 +58,13 @@ describe('ParcelConfig', () => { describe('matchGlobMapPipelines', () => { let config = new ParcelConfig( { - filePath: '.parcelrc', + filePath: PARCELRC_PATH, bundler: undefined, transformers: { '*.jsx': [ { packageName: 'parcel-transform-jsx', - resolveFrom: '.parcelrc', + resolveFrom: PARCELRC_PATH, keyPath: '/transformers/*.jsx/0', }, '...', @@ -63,7 +72,7 @@ describe('ParcelConfig', () => { '*.{js,jsx}': [ { packageName: 'parcel-transform-js', - resolveFrom: '.parcelrc', + resolveFrom: PARCELRC_PATH, keyPath: '/transformers/*.{js,jsx}/0', }, ], @@ -74,7 +83,7 @@ describe('ParcelConfig', () => { it('should return an empty array if no pipeline matches', () => { let pipeline = config.matchGlobMapPipelines( - 'foo.css', + toProjectPath('/', '/foo.css'), config.transformers, ); assert.deepEqual(pipeline, []); @@ -82,13 +91,13 @@ describe('ParcelConfig', () => { it('should return a matching pipeline', () => { let pipeline = config.matchGlobMapPipelines( - 'foo.js', + toProjectPath('/', '/foo.js'), config.transformers, ); assert.deepEqual(pipeline, [ { packageName: 'parcel-transform-js', - resolveFrom: '.parcelrc', + resolveFrom: PARCELRC_PATH, keyPath: '/transformers/*.{js,jsx}/0', }, ]); @@ -96,18 +105,18 @@ describe('ParcelConfig', () => { it('should merge pipelines with spread elements', () => { let pipeline = config.matchGlobMapPipelines( - 'foo.jsx', + toProjectPath('/', '/foo.jsx'), config.transformers, ); assert.deepEqual(pipeline, [ { packageName: 'parcel-transform-jsx', - resolveFrom: '.parcelrc', + resolveFrom: PARCELRC_PATH, keyPath: '/transformers/*.jsx/0', }, { packageName: 'parcel-transform-js', - resolveFrom: '.parcelrc', + resolveFrom: PARCELRC_PATH, keyPath: '/transformers/*.{js,jsx}/0', }, ]); @@ -116,11 +125,10 @@ describe('ParcelConfig', () => { describe('loadPlugin', () => { it('should warn if a plugin needs to specify an engines.parcel field in package.json', async () => { - let configFilePath = path.join( - __dirname, - 'fixtures', - 'plugins', - '.parcelrc', + let projectRoot = path.join(__dirname, 'fixtures', 'plugins'); + let configFilePath = toProjectPath( + projectRoot, + path.join(__dirname, 'fixtures', 'plugins', '.parcelrc'), ); let config = new ParcelConfig( { @@ -136,10 +144,10 @@ describe('ParcelConfig', () => { ], }, }, - DEFAULT_OPTIONS, + {...DEFAULT_OPTIONS, projectRoot}, ); - sinon.stub(logger, 'warn'); + let warnStub = sinon.stub(logger, 'warn'); let {plugin} = await config.loadPlugin({ packageName: 'parcel-transformer-no-engines', resolveFrom: configFilePath, @@ -147,21 +155,20 @@ describe('ParcelConfig', () => { }); assert(plugin); assert.equal(typeof plugin.transform, 'function'); - assert(logger.warn.calledOnce); - assert.deepEqual(logger.warn.getCall(0).args[0], { + assert(warnStub.calledOnce); + assert.deepEqual(warnStub.getCall(0).args[0], { origin: '@parcel/core', message: 'The plugin "parcel-transformer-no-engines" needs to specify a `package.json#engines.parcel` field with the supported Parcel version range.', }); - logger.warn.restore(); + warnStub.restore(); }); it('should error if a plugin specifies an invalid engines.parcel field in package.json', async () => { - let configFilePath = path.join( - __dirname, - 'fixtures', - 'plugins', - '.parcelrc', + let projectRoot = path.join(__dirname, 'fixtures', 'plugins'); + let configFilePath = toProjectPath( + projectRoot, + path.join(__dirname, 'fixtures', 'plugins', '.parcelrc'), ); let config = new ParcelConfig( { @@ -177,9 +184,9 @@ describe('ParcelConfig', () => { ], }, }, - DEFAULT_OPTIONS, + {...DEFAULT_OPTIONS, projectRoot}, ); - // $FlowFixMe + // $FlowFixMe[untyped-import] let parcelVersion = require('../package.json').version; let pkgJSON = path.join( __dirname, @@ -205,18 +212,20 @@ describe('ParcelConfig', () => { { message: `The plugin "parcel-transformer-bad-engines" is not compatible with the current version of Parcel. Requires "5.x" but the current version is "${parcelVersion}".`, origin: '@parcel/core', - filePath: pkgJSON, - language: 'json5', - codeFrame: { - code, - codeHighlights: [ - { - start: {line: 5, column: 5}, - end: {line: 5, column: 19}, - message: undefined, - }, - ], - }, + codeFrames: [ + { + filePath: pkgJSON, + language: 'json5', + code, + codeHighlights: [ + { + start: {line: 5, column: 5}, + end: {line: 5, column: 19}, + message: undefined, + }, + ], + }, + ], }, ], }, @@ -245,21 +254,68 @@ describe('ParcelConfig', () => { { message: 'Cannot find Parcel plugin "@parcel/transformer-jj"', origin: '@parcel/core', - filePath: configFilePath, - language: 'json5', - codeFrame: { - code, - codeHighlights: [ + codeFrames: [ + { + filePath: configFilePath, + language: 'json5', + code, + codeHighlights: [ + { + start: {line: 4, column: 14}, + end: {line: 4, column: 37}, + message: `Cannot find module "@parcel/transformer-jj", did you mean "@parcel/transformer-js"?`, + }, + ], + }, + ], + }, + ], + }); + }); + + it('should error when using a reserved pipeline name "node:*"', async () => { + let configFilePath = path.join( + __dirname, + 'fixtures', + 'config-node-pipeline', + '.parcelrc', + ); + let code = await DEFAULT_OPTIONS.inputFS.readFile(configFilePath, 'utf8'); + + // $FlowFixMe + await assert.rejects( + () => parseAndProcessConfig(configFilePath, code, DEFAULT_OPTIONS), + { + name: 'Error', + diagnostics: [ + { + message: + 'Named pipeline `node:` is reserved for builtin Node.js libraries', + origin: '@parcel/core', + codeFrames: [ { - start: {line: 4, column: 14}, - end: {line: 4, column: 37}, - message: `Cannot find module "@parcel/transformer-jj", did you mean "@parcel/transformer-js"?`, + filePath: configFilePath, + language: 'json5', + code, + codeHighlights: [ + { + message: undefined, + start: { + line: 4, + column: 5, + }, + end: { + line: 4, + column: 15, + }, + }, + ], }, ], }, - }, - ], - }); + ], + }, + ); }); }); }); diff --git a/packages/core/core/test/ParcelConfigRequest.test.js b/packages/core/core/test/ParcelConfigRequest.test.js index ecac0f09e75..c656ba6bcdb 100644 --- a/packages/core/core/test/ParcelConfigRequest.test.js +++ b/packages/core/core/test/ParcelConfigRequest.test.js @@ -14,9 +14,10 @@ import { processConfig, } from '../src/requests/ParcelConfigRequest'; import {validatePackageName} from '../src/ParcelConfig.schema'; -import {DEFAULT_OPTIONS} from './test-utils'; +import {DEFAULT_OPTIONS, relative} from './test-utils'; +import {toProjectPath} from '../src/projectPath'; -describe('loadParcelConfig', () => { +describe('ParcelConfigRequest', () => { describe('validatePackageName', () => { it('should error on an invalid official package', () => { assert.throws(() => { @@ -277,7 +278,7 @@ describe('loadParcelConfig', () => { }, e => { assert.strictEqual( - e.diagnostics[0].codeFrame.codeHighlights[0].message, + e.diagnostics[0].codeFrames[0].codeHighlights[0].message, `Did you mean "transformers"?`, ); return true; @@ -316,7 +317,7 @@ describe('loadParcelConfig', () => { [ { packageName: 'parcel-transform-foo', - resolveFrom: '.parcelrc', + resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/0', }, ], @@ -337,7 +338,7 @@ describe('loadParcelConfig', () => { mergePipelines(null, [ { packageName: 'parcel-transform-bar', - resolveFrom: '.parcelrc', + resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/0', }, ]), @@ -357,14 +358,14 @@ describe('loadParcelConfig', () => { [ { packageName: 'parcel-transform-foo', - resolveFrom: '.parcelrc', + resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/0', }, ], [ { packageName: 'parcel-transform-bar', - resolveFrom: '.parcelrc', + resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/0', }, ], @@ -385,20 +386,20 @@ describe('loadParcelConfig', () => { [ { packageName: 'parcel-transform-foo', - resolveFrom: '.parcelrc', + resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/0', }, ], [ { packageName: 'parcel-transform-bar', - resolveFrom: '.parcelrc', + resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/0', }, '...', { packageName: 'parcel-transform-baz', - resolveFrom: '.parcelrc', + resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/2', }, ], @@ -429,20 +430,20 @@ describe('loadParcelConfig', () => { [ { packageName: 'parcel-transform-foo', - resolveFrom: '.parcelrc', + resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/0', }, ], [ { packageName: 'parcel-transform-bar', - resolveFrom: '.parcelrc', + resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/0', }, '...', { packageName: 'parcel-transform-baz', - resolveFrom: '.parcelrc', + resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/2', }, '...', @@ -456,13 +457,13 @@ describe('loadParcelConfig', () => { mergePipelines(null, [ { packageName: 'parcel-transform-bar', - resolveFrom: '.parcelrc', + resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/0', }, '...', { packageName: 'parcel-transform-baz', - resolveFrom: '.parcelrc', + resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/2', }, ]), @@ -486,13 +487,13 @@ describe('loadParcelConfig', () => { mergePipelines(null, [ { packageName: 'parcel-transform-bar', - resolveFrom: '.parcelrc', + resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/0', }, '...', { packageName: 'parcel-transform-baz', - resolveFrom: '.parcelrc', + resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/2', }, '...', @@ -544,11 +545,11 @@ describe('loadParcelConfig', () => { it('should merge configs', () => { let base = new ParcelConfig( { - filePath: '.parcelrc', + filePath: toProjectPath('/', '/.parcelrc'), resolvers: [ { packageName: 'parcel-resolver-base', - resolveFrom: '.parcelrc', + resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/resolvers/0', }, ], @@ -556,21 +557,21 @@ describe('loadParcelConfig', () => { '*.js': [ { packageName: 'parcel-transform-base', - resolveFrom: '.parcelrc', + resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.js/0', }, ], '*.css': [ { packageName: 'parcel-transform-css', - resolveFrom: '.parcelrc', + resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/transformers/*.css/0', }, ], }, bundler: { packageName: 'parcel-bundler-base', - resolveFrom: '.parcelrc', + resolveFrom: toProjectPath('/', '/.parcelrc'), keyPath: '/bundler', }, }, @@ -680,10 +681,13 @@ describe('loadParcelConfig', () => { describe('parseAndProcessConfig', () => { it('should load and merge configs', async () => { let defaultConfigPath = require.resolve('@parcel/config-default'); - let defaultConfig = processConfig({ - ...require('@parcel/config-default'), - filePath: defaultConfigPath, - }); + let defaultConfig = await processConfig( + { + ...require('@parcel/config-default'), + filePath: defaultConfigPath, + }, + DEFAULT_OPTIONS, + ); let configFilePath = path.join( __dirname, 'fixtures', @@ -707,12 +711,12 @@ describe('loadParcelConfig', () => { assert.deepEqual(transformers['*.js'], [ { packageName: 'parcel-transformer-sub', - resolveFrom: subConfigFilePath, + resolveFrom: relative(subConfigFilePath), keyPath: '/transformers/*.js/0', }, { packageName: 'parcel-transformer-base', - resolveFrom: configFilePath, + resolveFrom: relative(configFilePath), keyPath: '/transformers/*.js/0', }, '...', @@ -749,18 +753,20 @@ describe('loadParcelConfig', () => { { message: 'Failed to parse .parcelrc', origin: '@parcel/core', - filePath: configFilePath, - language: 'json5', - codeFrame: { - code, - codeHighlights: [ - { - message: "JSON5: invalid character 'b' at 2:14", - start: pos, - end: pos, - }, - ], - }, + codeFrames: [ + { + filePath: configFilePath, + language: 'json5', + code, + codeHighlights: [ + { + message: "JSON5: invalid character 'b' at 2:14", + start: pos, + end: pos, + }, + ], + }, + ], }, ], }, @@ -785,19 +791,21 @@ describe('loadParcelConfig', () => { { message: 'Cannot find extended parcel config', origin: '@parcel/core', - filePath: configFilePath, - language: 'json5', - codeFrame: { - code, - codeHighlights: [ - { - message: - '"./.parclrc-node-modules" does not exist, did you mean "./.parcelrc-node-modules"?', - start: {line: 2, column: 14}, - end: {line: 2, column: 38}, - }, - ], - }, + codeFrames: [ + { + filePath: configFilePath, + language: 'json5', + code, + codeHighlights: [ + { + message: + '"./.parclrc-node-modules" does not exist, did you mean "./.parcelrc-node-modules"?', + start: {line: 2, column: 14}, + end: {line: 2, column: 38}, + }, + ], + }, + ], }, ], }, @@ -822,19 +830,21 @@ describe('loadParcelConfig', () => { { message: 'Cannot find extended parcel config', origin: '@parcel/core', - filePath: configFilePath, - language: 'json5', - codeFrame: { - code, - codeHighlights: [ - { - message: - 'Cannot find module "@parcel/config-deflt", did you mean "@parcel/config-default"?', - start: {line: 2, column: 14}, - end: {line: 2, column: 35}, - }, - ], - }, + codeFrames: [ + { + filePath: configFilePath, + language: 'json5', + code, + codeHighlights: [ + { + message: + 'Cannot find module "@parcel/config-deflt", did you mean "@parcel/config-default"?', + start: {line: 2, column: 14}, + end: {line: 2, column: 35}, + }, + ], + }, + ], }, ], }, @@ -859,36 +869,40 @@ describe('loadParcelConfig', () => { { message: 'Cannot find extended parcel config', origin: '@parcel/core', - filePath: configFilePath, - language: 'json5', - codeFrame: { - code, - codeHighlights: [ - { - message: - 'Cannot find module "@parcel/config-deflt", did you mean "@parcel/config-default"?', - start: {line: 2, column: 15}, - end: {line: 2, column: 36}, - }, - ], - }, + codeFrames: [ + { + filePath: configFilePath, + language: 'json5', + code, + codeHighlights: [ + { + message: + 'Cannot find module "@parcel/config-deflt", did you mean "@parcel/config-default"?', + start: {line: 2, column: 15}, + end: {line: 2, column: 36}, + }, + ], + }, + ], }, { message: 'Cannot find extended parcel config', origin: '@parcel/core', - filePath: configFilePath, - language: 'json5', - codeFrame: { - code, - codeHighlights: [ - { - message: - '"./.parclrc" does not exist, did you mean "./.parcelrc"?', - start: {line: 2, column: 39}, - end: {line: 2, column: 50}, - }, - ], - }, + codeFrames: [ + { + filePath: configFilePath, + language: 'json5', + code, + codeHighlights: [ + { + message: + '"./.parclrc" does not exist, did you mean "./.parcelrc"?', + start: {line: 2, column: 39}, + end: {line: 2, column: 50}, + }, + ], + }, + ], }, ], }, diff --git a/packages/core/core/test/PublicAsset.test.js b/packages/core/core/test/PublicAsset.test.js index aaed37ad964..82be3beab48 100644 --- a/packages/core/core/test/PublicAsset.test.js +++ b/packages/core/core/test/PublicAsset.test.js @@ -3,9 +3,14 @@ import assert from 'assert'; import {Asset, MutableAsset} from '../src/public/Asset'; import UncommittedAsset from '../src/UncommittedAsset'; -import {createAsset} from '../src/assetUtils'; +import {createAsset as _createAsset} from '../src/assetUtils'; import {createEnvironment} from '../src/Environment'; import {DEFAULT_OPTIONS} from './test-utils'; +import {toProjectPath} from '../src/projectPath'; + +function createAsset(opts) { + return _createAsset('/', opts); +} describe('Public Asset', () => { let internalAsset; @@ -13,7 +18,7 @@ describe('Public Asset', () => { internalAsset = new UncommittedAsset({ options: DEFAULT_OPTIONS, value: createAsset({ - filePath: '/does/not/exist', + filePath: toProjectPath('/', '/does/not/exist'), type: 'js', env: createEnvironment({}), isSource: true, diff --git a/packages/core/core/test/PublicBundle.test.js b/packages/core/core/test/PublicBundle.test.js index 8fe446b494b..231b537f687 100644 --- a/packages/core/core/test/PublicBundle.test.js +++ b/packages/core/core/test/PublicBundle.test.js @@ -1,4 +1,5 @@ // @flow strict-local +import type {Bundle as InternalBundle} from '../src/types'; import assert from 'assert'; import {Bundle, NamedBundle, PackagedBundle} from '../src/public/Bundle'; @@ -6,9 +7,10 @@ import BundleGraph from '../src/BundleGraph'; import {createEnvironment} from '../src/Environment'; import {DEFAULT_OPTIONS} from './test-utils'; import ContentGraph from '../src/ContentGraph'; +import {toProjectPath} from '../src/projectPath'; describe('Public Bundle', () => { - let internalBundle; + let internalBundle: InternalBundle; let bundleGraph; beforeEach(() => { let env = createEnvironment({}); @@ -23,12 +25,12 @@ describe('Public Bundle', () => { displayName: null, publicId: null, pipeline: null, - isEntry: null, - isInline: null, + needsStableName: null, + bundleBehavior: null, isSplittable: true, target: { env, - distDir: '', + distDir: toProjectPath('/', '/'), name: '', publicUrl: '', }, diff --git a/packages/core/core/test/PublicDependency.test.js b/packages/core/core/test/PublicDependency.test.js index c5fd688af4c..914e48968e9 100644 --- a/packages/core/core/test/PublicDependency.test.js +++ b/packages/core/core/test/PublicDependency.test.js @@ -4,18 +4,19 @@ import assert from 'assert'; import {createEnvironment} from '../src/Environment'; import {createDependency} from '../src/Dependency'; import Dependency from '../src/public/Dependency'; +import {DEFAULT_OPTIONS} from './test-utils'; describe('Public Dependency', () => { it('returns the same public Dependency given an internal dependency', () => { - let internalDependency = createDependency({ + let internalDependency = createDependency('/', { specifier: 'foo', specifierType: 'esm', env: createEnvironment({}), }); assert.equal( - new Dependency(internalDependency), - new Dependency(internalDependency), + new Dependency(internalDependency, DEFAULT_OPTIONS), + new Dependency(internalDependency, DEFAULT_OPTIONS), ); }); }); diff --git a/packages/core/core/test/PublicMutableBundleGraph.test.js b/packages/core/core/test/PublicMutableBundleGraph.test.js index e5f9b017209..d5d04b693f3 100644 --- a/packages/core/core/test/PublicMutableBundleGraph.test.js +++ b/packages/core/core/test/PublicMutableBundleGraph.test.js @@ -8,14 +8,23 @@ import InternalBundleGraph from '../src/BundleGraph'; import MutableBundleGraph from '../src/public/MutableBundleGraph'; import {DEFAULT_ENV, DEFAULT_TARGETS, DEFAULT_OPTIONS} from './test-utils'; import AssetGraph, {nodeFromAssetGroup} from '../src/AssetGraph'; -import {createAsset} from '../src/assetUtils'; -import {createDependency} from '../src/Dependency'; +import {createAsset as _createAsset} from '../src/assetUtils'; +import {createDependency as _createDependency} from '../src/Dependency'; import nullthrows from 'nullthrows'; +import {toProjectPath} from '../src/projectPath'; + +function createAsset(opts) { + return _createAsset('/', opts); +} + +function createDependency(opts) { + return _createDependency('/', opts); +} const id1 = '0123456789abcdef0123456789abcdef'; const id2 = '9876543210fedcba9876543210fedcba'; -describe('MutableBundleGraph', () => { +describe('PublicMutableBundleGraph', () => { it('creates publicIds for bundles', () => { let internalBundleGraph = InternalBundleGraph.fromAssetGraph( createMockAssetGraph(), @@ -88,46 +97,62 @@ const stats = {size: 0, time: 0}; function createMockAssetGraph() { let graph = new AssetGraph(); graph.setRootConnections({ - entries: ['./index', './index2'], + entries: [toProjectPath('/', '/index'), toProjectPath('/', '/index2')], }); graph.resolveEntry( - './index', - [{filePath: '/path/to/index/src/main.js', packagePath: '/path/to/index'}], + toProjectPath('/', '/index'), + [ + { + filePath: toProjectPath('/', '/path/to/index/src/main.js'), + packagePath: toProjectPath('/', '/path/to/index'), + }, + ], '1', ); graph.resolveEntry( - './index2', - [{filePath: '/path/to/index/src/main2.js', packagePath: '/path/to/index'}], + toProjectPath('/', '/index2'), + [ + { + filePath: toProjectPath('/', '/path/to/index/src/main2.js'), + packagePath: toProjectPath('/', '/path/to/index'), + }, + ], '2', ); graph.resolveTargets( - {filePath: '/path/to/index/src/main.js', packagePath: '/path/to/index'}, + { + filePath: toProjectPath('/', '/path/to/index/src/main.js'), + packagePath: toProjectPath('/', '/path/to/index'), + }, DEFAULT_TARGETS, '3', ); graph.resolveTargets( - {filePath: '/path/to/index/src/main2.js', packagePath: '/path/to/index'}, + { + filePath: toProjectPath('/', '/path/to/index/src/main2.js'), + packagePath: toProjectPath('/', '/path/to/index'), + }, DEFAULT_TARGETS, '4', ); let dep1 = createDependency({ - specifier: '/path/to/index/src/main.js', + specifier: 'path/to/index/src/main.js', specifierType: 'esm', needsStableName: true, env: DEFAULT_ENV, target: DEFAULT_TARGETS[0], }); let dep2 = createDependency({ - specifier: '/path/to/index/src/main2.js', + specifier: 'path/to/index/src/main2.js', specifierType: 'esm', needsStableName: true, env: DEFAULT_ENV, target: DEFAULT_TARGETS[0], }); - let filePath = '/index.js'; + let filePath = toProjectPath('/', '/index.js'); let req1 = {filePath, env: DEFAULT_ENV, query: {}}; graph.resolveDependency(dep1, nodeFromAssetGroup(req1).value, '5'); graph.resolveAssetGroup( @@ -146,7 +171,7 @@ function createMockAssetGraph() { '6', ); - filePath = '/index2.js'; + filePath = toProjectPath('/', '/index2.js'); let req2 = {filePath, env: DEFAULT_ENV, query: {}}; graph.resolveDependency(dep2, nodeFromAssetGroup(req2).value, '7'); graph.resolveAssetGroup( diff --git a/packages/core/core/test/TargetRequest.test.js b/packages/core/core/test/TargetRequest.test.js index 91abed62414..960e0a3f1c4 100644 --- a/packages/core/core/test/TargetRequest.test.js +++ b/packages/core/core/test/TargetRequest.test.js @@ -6,7 +6,7 @@ import tempy from 'tempy'; import {inputFS as fs} from '@parcel/test-utils'; import {md} from '@parcel/diagnostic'; import {TargetResolver} from '../src/requests/TargetRequest'; -import {DEFAULT_OPTIONS as _DEFAULT_OPTIONS} from './test-utils'; +import {DEFAULT_OPTIONS as _DEFAULT_OPTIONS, relative} from './test-utils'; const DEFAULT_OPTIONS = { ..._DEFAULT_OPTIONS, @@ -167,11 +167,11 @@ describe('TargetResolver', () => { [ { name: 'main', - distDir: path.join(__dirname, 'fixtures/common-targets/dist/main'), + distDir: 'fixtures/common-targets/dist/main', distEntry: 'index.js', publicUrl: '/', env: { - id: 'c76b62332705945c', + id: '09b930c9a214dbbb', context: 'node', engines: { node: '>= 8.0.0', @@ -180,13 +180,15 @@ describe('TargetResolver', () => { outputFormat: 'commonjs', isLibrary: true, shouldOptimize: false, - shouldScopeHoist: false, + shouldScopeHoist: true, sourceMap: {}, loc: undefined, sourceType: 'module', }, loc: { - filePath: path.join(COMMON_TARGETS_FIXTURE_PATH, 'package.json'), + filePath: relative( + path.join(COMMON_TARGETS_FIXTURE_PATH, 'package.json'), + ), start: { column: 11, line: 2, @@ -199,11 +201,11 @@ describe('TargetResolver', () => { }, { name: 'module', - distDir: path.join(__dirname, 'fixtures/common-targets/dist/module'), + distDir: 'fixtures/common-targets/dist/module', distEntry: 'index.js', publicUrl: '/', env: { - id: 'df6fdb273f632b46', + id: '02a05911c4b149ef', context: 'browser', engines: { browsers: ['last 1 version'], @@ -212,7 +214,7 @@ describe('TargetResolver', () => { outputFormat: 'esmodule', isLibrary: true, shouldOptimize: false, - shouldScopeHoist: false, + shouldScopeHoist: true, sourceMap: { inlineSources: true, }, @@ -220,7 +222,9 @@ describe('TargetResolver', () => { sourceType: 'module', }, loc: { - filePath: path.join(COMMON_TARGETS_FIXTURE_PATH, 'package.json'), + filePath: relative( + path.join(COMMON_TARGETS_FIXTURE_PATH, 'package.json'), + ), start: { column: 13, line: 3, @@ -233,11 +237,11 @@ describe('TargetResolver', () => { }, { name: 'browser', - distDir: path.join(__dirname, 'fixtures/common-targets/dist/browser'), + distDir: 'fixtures/common-targets/dist/browser', distEntry: 'index.js', publicUrl: '/assets', env: { - id: 'd827cb8315171b3b', + id: 'f17b51a5bc6afeb9', context: 'browser', engines: { browsers: ['last 1 version'], @@ -246,13 +250,15 @@ describe('TargetResolver', () => { outputFormat: 'commonjs', isLibrary: true, shouldOptimize: false, - shouldScopeHoist: false, + shouldScopeHoist: true, sourceMap: {}, loc: undefined, sourceType: 'module', }, loc: { - filePath: path.join(COMMON_TARGETS_FIXTURE_PATH, 'package.json'), + filePath: relative( + path.join(COMMON_TARGETS_FIXTURE_PATH, 'package.json'), + ), start: { column: 14, line: 4, @@ -275,7 +281,9 @@ describe('TargetResolver', () => { [ { name: 'app', - distDir: path.join(COMMON_TARGETS_IGNORE_FIXTURE_PATH, 'dist'), + distDir: relative( + path.join(COMMON_TARGETS_IGNORE_FIXTURE_PATH, 'dist'), + ), distEntry: 'index.js', publicUrl: '/', env: { @@ -294,9 +302,8 @@ describe('TargetResolver', () => { sourceType: 'module', }, loc: { - filePath: path.join( - COMMON_TARGETS_IGNORE_FIXTURE_PATH, - 'package.json', + filePath: relative( + path.join(COMMON_TARGETS_IGNORE_FIXTURE_PATH, 'package.json'), ), start: { column: 10, @@ -319,11 +326,11 @@ describe('TargetResolver', () => { [ { name: 'main', - distDir: path.join(__dirname, 'fixtures/custom-targets/dist/main'), + distDir: 'fixtures/custom-targets/dist/main', distEntry: 'index.js', publicUrl: '/', env: { - id: 'c76b62332705945c', + id: '09b930c9a214dbbb', context: 'node', engines: { node: '>= 8.0.0', @@ -332,13 +339,15 @@ describe('TargetResolver', () => { outputFormat: 'commonjs', isLibrary: true, shouldOptimize: false, - shouldScopeHoist: false, + shouldScopeHoist: true, sourceMap: {}, loc: undefined, sourceType: 'module', }, loc: { - filePath: path.join(CUSTOM_TARGETS_FIXTURE_PATH, 'package.json'), + filePath: relative( + path.join(CUSTOM_TARGETS_FIXTURE_PATH, 'package.json'), + ), start: { column: 11, line: 2, @@ -351,10 +360,7 @@ describe('TargetResolver', () => { }, { name: 'browserModern', - distDir: path.join( - __dirname, - 'fixtures/custom-targets/dist/browserModern', - ), + distDir: 'fixtures/custom-targets/dist/browserModern', distEntry: 'index.js', publicUrl: '/', env: { @@ -373,7 +379,9 @@ describe('TargetResolver', () => { sourceType: 'module', }, loc: { - filePath: path.join(CUSTOM_TARGETS_FIXTURE_PATH, 'package.json'), + filePath: relative( + path.join(CUSTOM_TARGETS_FIXTURE_PATH, 'package.json'), + ), start: { column: 20, line: 3, @@ -386,10 +394,7 @@ describe('TargetResolver', () => { }, { name: 'browserLegacy', - distDir: path.join( - __dirname, - 'fixtures/custom-targets/dist/browserLegacy', - ), + distDir: 'fixtures/custom-targets/dist/browserLegacy', distEntry: 'index.js', publicUrl: '/', env: { @@ -408,7 +413,128 @@ describe('TargetResolver', () => { sourceType: 'module', }, loc: { - filePath: path.join(CUSTOM_TARGETS_FIXTURE_PATH, 'package.json'), + filePath: relative( + path.join(CUSTOM_TARGETS_FIXTURE_PATH, 'package.json'), + ), + start: { + column: 20, + line: 4, + }, + end: { + column: 48, + line: 4, + }, + }, + }, + ], + ); + }); + + it('should not optimize libraries by default', async () => { + let targetResolver = new TargetResolver(api, { + ...DEFAULT_OPTIONS, + mode: 'production', + defaultTargetOptions: { + ...DEFAULT_OPTIONS.defaultTargetOptions, + shouldOptimize: true, + }, + }); + + assert.deepEqual( + await targetResolver.resolve(CUSTOM_TARGETS_FIXTURE_PATH), + [ + { + name: 'main', + distDir: 'fixtures/custom-targets/dist/main', + distEntry: 'index.js', + publicUrl: '/', + env: { + id: '09b930c9a214dbbb', + context: 'node', + engines: { + node: '>= 8.0.0', + }, + includeNodeModules: false, + outputFormat: 'commonjs', + isLibrary: true, + shouldOptimize: false, + shouldScopeHoist: true, + sourceMap: {}, + loc: undefined, + sourceType: 'module', + }, + loc: { + filePath: relative( + path.join(CUSTOM_TARGETS_FIXTURE_PATH, 'package.json'), + ), + start: { + column: 11, + line: 2, + }, + end: { + column: 30, + line: 2, + }, + }, + }, + { + name: 'browserModern', + distDir: 'fixtures/custom-targets/dist/browserModern', + distEntry: 'index.js', + publicUrl: '/', + env: { + id: 'a44a40bf101f18ec', + context: 'browser', + engines: { + browsers: ['last 1 version'], + }, + includeNodeModules: true, + outputFormat: 'global', + isLibrary: false, + shouldOptimize: true, + shouldScopeHoist: false, + sourceMap: {}, + loc: undefined, + sourceType: 'module', + }, + loc: { + filePath: relative( + path.join(CUSTOM_TARGETS_FIXTURE_PATH, 'package.json'), + ), + start: { + column: 20, + line: 3, + }, + end: { + column: 48, + line: 3, + }, + }, + }, + { + name: 'browserLegacy', + distDir: 'fixtures/custom-targets/dist/browserLegacy', + distEntry: 'index.js', + publicUrl: '/', + env: { + id: 'a188d65cbb275231', + context: 'browser', + engines: { + browsers: ['ie11'], + }, + includeNodeModules: true, + outputFormat: 'global', + isLibrary: false, + shouldOptimize: true, + shouldScopeHoist: false, + sourceMap: {}, + loc: undefined, + sourceType: 'module', + }, + loc: { + filePath: relative( + path.join(CUSTOM_TARGETS_FIXTURE_PATH, 'package.json'), + ), start: { column: 20, line: 4, @@ -430,7 +556,7 @@ describe('TargetResolver', () => { [ { name: 'app', - distDir: path.join(__dirname, 'fixtures/custom-targets-distdir/www'), + distDir: 'fixtures/custom-targets-distdir/www', distEntry: undefined, publicUrl: 'www', env: { @@ -544,24 +670,24 @@ describe('TargetResolver', () => { assert.deepEqual(await targetResolver.resolve(CONTEXT_FIXTURE_PATH), [ { name: 'main', - distDir: path.join(__dirname, 'fixtures/context/dist/main'), + distDir: 'fixtures/context/dist/main', distEntry: 'index.js', publicUrl: '/', env: { - id: 'fb21597610528ace', + id: '48f9c3826a31634f', context: 'node', engines: {}, includeNodeModules: false, isLibrary: true, outputFormat: 'commonjs', shouldOptimize: false, - shouldScopeHoist: false, + shouldScopeHoist: true, sourceMap: {}, loc: undefined, sourceType: 'module', }, loc: { - filePath: path.join(CONTEXT_FIXTURE_PATH, 'package.json'), + filePath: relative(path.join(CONTEXT_FIXTURE_PATH, 'package.json')), start: { column: 11, line: 2, @@ -575,22 +701,342 @@ 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', + codeFrames: [ + { + filePath: path.join(fixture, 'package.json'), + language: 'json', + 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', + codeFrames: [ + { + filePath: path.join(fixture, 'package.json'), + language: 'json', + 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', + codeFrames: [ + { + filePath: path.join(fixture, 'package.json'), + language: 'json', + 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', + codeFrames: [ + { + filePath: path.join(fixture, 'package.json'), + language: 'json', + 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', + codeFrames: [ + { + filePath: path.join(fixture, 'package.json'), + language: 'json', + 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('errors when a common library target turns scope hoisting off', async () => { + let targetResolver = new TargetResolver(api, DEFAULT_OPTIONS); + let fixture = path.join(__dirname, 'fixtures/library-scopehoist'); + let code = await fs.readFile(path.join(fixture, 'package.json'), 'utf8'); + + // $FlowFixMe + await assert.rejects(() => targetResolver.resolve(fixture), { + diagnostics: [ + { + message: 'Scope hoisting cannot be disabled for library targets.', + origin: '@parcel/core', + codeFrames: [ + { + filePath: path.join(fixture, 'package.json'), + language: 'json', + code, + codeHighlights: [ + { + message: undefined, + end: { + column: 25, + line: 5, + }, + start: { + column: 21, + line: 5, + }, + }, + ], + }, + ], + hints: [ + 'The "main" target is meant for libraries. Either remove the "scopeHoist" option, or use a different target name.', + ], + }, + ], + }); + }); + + it('errors when a custom library target turns scope hoisting off', async () => { + let targetResolver = new TargetResolver(api, DEFAULT_OPTIONS); + let fixture = path.join(__dirname, 'fixtures/library-custom-scopehoist'); + let code = await fs.readFile(path.join(fixture, 'package.json'), 'utf8'); + + // $FlowFixMe + await assert.rejects(() => targetResolver.resolve(fixture), { + diagnostics: [ + { + message: 'Scope hoisting cannot be disabled for library targets.', + origin: '@parcel/core', + codeFrames: [ + { + filePath: path.join(fixture, 'package.json'), + language: 'json', + code, + codeHighlights: [ + { + message: undefined, + end: { + column: 25, + line: 6, + }, + start: { + column: 21, + line: 6, + }, + }, + { + message: undefined, + end: { + column: 23, + line: 5, + }, + start: { + column: 20, + line: 5, + }, + }, + ], + }, + ], + hints: ['Either remove the "scopeHoist" or "isLibrary" option.'], + }, + ], + }); + }); + + 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', - distDir: path.join(fixture, 'dist'), - distEntry: 'index.html', + name: 'test', + distDir: relative(path.join(fixture, 'dist')), + 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: {}, @@ -598,13 +1044,13 @@ describe('TargetResolver', () => { sourceType: 'module', }, loc: { - filePath: path.join(fixture, 'package.json'), + filePath: relative(path.join(fixture, 'package.json')), start: { column: 11, line: 2, }, end: { - column: 27, + column: 26, line: 2, }, }, @@ -612,6 +1058,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: relative(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: relative(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, @@ -623,11 +1107,11 @@ describe('TargetResolver', () => { [ { name: 'main', - distDir: path.join(__dirname, 'fixtures/common-targets/dist/main'), + distDir: 'fixtures/common-targets/dist/main', distEntry: 'index.js', publicUrl: '/', env: { - id: 'c76b62332705945c', + id: '09b930c9a214dbbb', context: 'node', engines: { node: '>= 8.0.0', @@ -636,13 +1120,15 @@ describe('TargetResolver', () => { outputFormat: 'commonjs', isLibrary: true, shouldOptimize: false, - shouldScopeHoist: false, + shouldScopeHoist: true, sourceMap: {}, loc: undefined, sourceType: 'module', }, loc: { - filePath: path.join(COMMON_TARGETS_FIXTURE_PATH, 'package.json'), + filePath: relative( + path.join(COMMON_TARGETS_FIXTURE_PATH, 'package.json'), + ), start: { column: 11, line: 2, @@ -655,11 +1141,11 @@ describe('TargetResolver', () => { }, { name: 'browser', - distDir: path.join(__dirname, 'fixtures/common-targets/dist/browser'), + distDir: 'fixtures/common-targets/dist/browser', distEntry: 'index.js', publicUrl: '/assets', env: { - id: 'd827cb8315171b3b', + id: 'f17b51a5bc6afeb9', context: 'browser', engines: { browsers: ['last 1 version'], @@ -668,13 +1154,15 @@ describe('TargetResolver', () => { outputFormat: 'commonjs', isLibrary: true, shouldOptimize: false, - shouldScopeHoist: false, + shouldScopeHoist: true, sourceMap: {}, loc: undefined, sourceType: 'module', }, loc: { - filePath: path.join(COMMON_TARGETS_FIXTURE_PATH, 'package.json'), + filePath: relative( + path.join(COMMON_TARGETS_FIXTURE_PATH, 'package.json'), + ), start: { column: 14, line: 4, @@ -702,7 +1190,7 @@ describe('TargetResolver', () => { [ { name: 'default', - distDir: serveDistDir, + distDir: '.parcel-cache/dist', publicUrl: '/', env: { id: 'c22175d22bace513', @@ -730,7 +1218,9 @@ describe('TargetResolver', () => { [ { name: 'default', - distDir: path.join(DEFAULT_DISTPATH_FIXTURE_PATHS.none, 'dist'), + distDir: relative( + path.join(DEFAULT_DISTPATH_FIXTURE_PATHS.none, 'dist'), + ), publicUrl: '/', env: { id: '45ce37e4d0180a7f', @@ -760,7 +1250,9 @@ describe('TargetResolver', () => { [ { name: 'browserModern', - distDir: path.join(DEFAULT_DISTPATH_FIXTURE_PATHS.one, 'dist'), + distDir: relative( + path.join(DEFAULT_DISTPATH_FIXTURE_PATHS.one, 'dist'), + ), distEntry: undefined, publicUrl: '/', env: { @@ -792,10 +1284,12 @@ describe('TargetResolver', () => { [ { name: 'browserModern', - distDir: path.join( - DEFAULT_DISTPATH_FIXTURE_PATHS.two, - 'dist', - 'browserModern', + distDir: relative( + path.join( + DEFAULT_DISTPATH_FIXTURE_PATHS.two, + 'dist', + 'browserModern', + ), ), distEntry: undefined, publicUrl: '/', @@ -818,10 +1312,12 @@ describe('TargetResolver', () => { }, { name: 'browserLegacy', - distDir: path.join( - DEFAULT_DISTPATH_FIXTURE_PATHS.two, - 'dist', - 'browserLegacy', + distDir: relative( + path.join( + DEFAULT_DISTPATH_FIXTURE_PATHS.two, + 'dist', + 'browserLegacy', + ), ), distEntry: undefined, publicUrl: '/', @@ -883,43 +1379,45 @@ describe('TargetResolver', () => { { message: 'Invalid target descriptor for target "main"', origin: '@parcel/core', - filePath: undefined, - language: 'json', - codeFrame: { - code, - codeHighlights: [ - { - start: {line: 6, column: 5}, - end: {line: 6, column: 8}, - message: 'Expected a wildcard or filepath', - }, - { - start: {line: 8, column: 15}, - end: {line: 8, column: 21}, - message: 'Did you mean "node"?', - }, - { - start: {line: 9, column: 20}, - end: {line: 9, column: 27}, - message: 'Did you mean "esmodule"?', - }, - { - start: {line: 12, column: 15}, - end: {line: 12, column: 21}, - message: 'Expected type boolean', - }, - { - start: {line: 13, column: 5}, - end: {line: 13, column: 13}, - message: 'Possible values: "inlineSources"', - }, - { - start: {line: 17, column: 5}, - end: {line: 17, column: 13}, - message: 'Did you mean "browsers"?', - }, - ], - }, + codeFrames: [ + { + filePath: undefined, + language: 'json', + code, + codeHighlights: [ + { + start: {line: 6, column: 5}, + end: {line: 6, column: 8}, + message: 'Expected a wildcard or filepath', + }, + { + start: {line: 8, column: 15}, + end: {line: 8, column: 21}, + message: 'Did you mean "node"?', + }, + { + start: {line: 9, column: 20}, + end: {line: 9, column: 27}, + message: 'Did you mean "esmodule"?', + }, + { + start: {line: 12, column: 15}, + end: {line: 12, column: 21}, + message: 'Expected type boolean', + }, + { + start: {line: 13, column: 5}, + end: {line: 13, column: 13}, + message: 'Possible values: "inlineSources"', + }, + { + start: {line: 17, column: 5}, + end: {line: 17, column: 13}, + message: 'Did you mean "browsers"?', + }, + ], + }, + ], }, ], }, @@ -940,23 +1438,28 @@ describe('TargetResolver', () => { { message: 'Invalid target descriptor for target "module"', origin: '@parcel/core', - filePath: path.join(INVALID_TARGETS_FIXTURE_PATH, 'package.json'), - language: 'json', - codeFrame: { - code, - codeHighlights: [ - { - start: {line: 9, column: 29}, - end: {line: 9, column: 35}, - message: 'Expected type boolean', - }, - { - start: {line: 11, column: 7}, - end: {line: 11, column: 17}, - message: 'Did you mean "publicUrl"?', - }, - ], - }, + codeFrames: [ + { + filePath: path.join( + INVALID_TARGETS_FIXTURE_PATH, + 'package.json', + ), + language: 'json', + code, + codeHighlights: [ + { + start: {line: 9, column: 29}, + end: {line: 9, column: 35}, + message: 'Expected type boolean', + }, + { + start: {line: 11, column: 7}, + end: {line: 11, column: 17}, + message: 'Did you mean "publicUrl"?', + }, + ], + }, + ], }, ], }, @@ -977,35 +1480,40 @@ describe('TargetResolver', () => { { message: 'Invalid engines in package.json', origin: '@parcel/core', - filePath: path.join(INVALID_ENGINES_FIXTURE_PATH, 'package.json'), - language: 'json', - codeFrame: { - code, - codeHighlights: [ - { - end: { - column: 13, - line: 8, - }, - message: 'Did you mean "browsers"?', - start: { - column: 5, - line: 8, + codeFrames: [ + { + filePath: path.join( + INVALID_ENGINES_FIXTURE_PATH, + 'package.json', + ), + language: 'json', + code, + codeHighlights: [ + { + end: { + column: 13, + line: 8, + }, + message: 'Did you mean "browsers"?', + start: { + column: 5, + line: 8, + }, }, - }, - { - end: { - column: 5, - line: 7, + { + end: { + column: 5, + line: 7, + }, + message: 'Expected type string', + start: { + column: 13, + line: 5, + }, }, - message: 'Expected type string', - start: { - column: 13, - line: 5, - }, - }, - ], - }, + ], + }, + ], }, ], }, @@ -1026,24 +1534,29 @@ describe('TargetResolver', () => { { message: 'Invalid distPath for target "legacy"', origin: '@parcel/core', - filePath: path.join(INVALID_DISTPATH_FIXTURE_PATH, 'package.json'), - language: 'json', - codeFrame: { - code, - codeHighlights: [ - { - end: { - column: 13, - line: 2, - }, - message: 'Expected type string', - start: { - column: 13, - line: 2, + codeFrames: [ + { + filePath: path.join( + INVALID_DISTPATH_FIXTURE_PATH, + 'package.json', + ), + language: 'json', + code, + codeHighlights: [ + { + end: { + column: 13, + line: 2, + }, + message: 'Expected type string', + start: { + column: 13, + line: 2, + }, }, - }, - ], - }, + ], + }, + ], }, ], }, @@ -1065,35 +1578,37 @@ describe('TargetResolver', () => { 'dist/index.js', )}"`, origin: '@parcel/core', - filePath: path.join(fixture, 'package.json'), - language: 'json', - codeFrame: { - code, - codeHighlights: [ - { - end: { - column: 25, - line: 2, - }, - message: undefined, - start: { - column: 11, - line: 2, - }, - }, - { - end: { - column: 27, - line: 3, + codeFrames: [ + { + filePath: path.join(fixture, 'package.json'), + language: 'json', + code, + codeHighlights: [ + { + end: { + column: 25, + line: 2, + }, + message: undefined, + start: { + column: 11, + line: 2, + }, }, - message: undefined, - start: { - column: 13, - line: 3, + { + end: { + column: 27, + line: 3, + }, + message: undefined, + start: { + column: 13, + line: 3, + }, }, - }, - ], - }, + ], + }, + ], hints: [ 'Try removing the duplicate targets, or changing the destination paths.', ], diff --git a/packages/core/core/test/fixtures/config-node-pipeline/.parcelrc b/packages/core/core/test/fixtures/config-node-pipeline/.parcelrc new file mode 100644 index 00000000000..b8397a0af09 --- /dev/null +++ b/packages/core/core/test/fixtures/config-node-pipeline/.parcelrc @@ -0,0 +1,6 @@ +{ + "extends": "@parcel/config-default", + "transformers": { + "node:*.js": ["@parcel/transformer-js"] + } +} 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/library-custom-scopehoist/package.json b/packages/core/core/test/fixtures/library-custom-scopehoist/package.json new file mode 100644 index 00000000000..91bb58dcbaa --- /dev/null +++ b/packages/core/core/test/fixtures/library-custom-scopehoist/package.json @@ -0,0 +1,9 @@ +{ + "test": "dist/index.js", + "targets": { + "test": { + "isLibrary": true, + "scopeHoist": false + } + } +} diff --git a/packages/core/core/test/fixtures/library-scopehoist/package.json b/packages/core/core/test/fixtures/library-scopehoist/package.json new file mode 100644 index 00000000000..6f92c075125 --- /dev/null +++ b/packages/core/core/test/fixtures/library-scopehoist/package.json @@ -0,0 +1,8 @@ +{ + "main": "dist/index.js", + "targets": { + "main": { + "scopeHoist": false + } + } +} 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/core/test/test-utils.js b/packages/core/core/test/test-utils.js index 421696f1d9d..b8c398c454b 100644 --- a/packages/core/core/test/test-utils.js +++ b/packages/core/core/test/test-utils.js @@ -1,25 +1,27 @@ // @flow strict-local -import type {Environment, ParcelOptions} from '../src/types'; +import type {Environment, ParcelOptions, Target} from '../src/types'; import {FSCache} from '@parcel/cache'; import tempy from 'tempy'; +import path from 'path'; import {inputFS, outputFS} from '@parcel/test-utils'; +import {relativePath} from '@parcel/utils'; import {NodePackageManager} from '@parcel/package-manager'; import {createEnvironment} from '../src/Environment'; +import {toProjectPath} from '../src/projectPath'; let cacheDir = tempy.directory(); export let cache: FSCache = new FSCache(outputFS, cacheDir); cache.ensure(); export const DEFAULT_OPTIONS: ParcelOptions = { - cacheDir: '.parcel-cache', + cacheDir: path.join(__dirname, '.parcel-cache'), entries: [], logLevel: 'info', - entryRoot: __dirname, + entryRoot: toProjectPath('/', __dirname), targets: undefined, - projectRoot: '', - lockFile: undefined, + projectRoot: __dirname, shouldAutoInstall: false, hmrOptions: undefined, shouldContentHash: true, @@ -52,12 +54,16 @@ export const DEFAULT_ENV: Environment = createEnvironment({ }, }); -export const DEFAULT_TARGETS = [ +export const DEFAULT_TARGETS: Array = [ { name: 'test', - distDir: 'dist', + distDir: toProjectPath('/', '/dist'), distEntry: 'out.js', env: DEFAULT_ENV, publicUrl: '/', }, ]; + +export function relative(f: string): string { + return relativePath(__dirname, f, false); +} diff --git a/packages/core/diagnostic/package.json b/packages/core/diagnostic/package.json index ae39dc3c24b..f1e4dfcdb3e 100644 --- a/packages/core/diagnostic/package.json +++ b/packages/core/diagnostic/package.json @@ -15,9 +15,14 @@ }, "main": "lib/diagnostic.js", "source": "src/diagnostic.js", + "types": "lib/diagnostic.d.ts", "engines": { "node": ">= 12.0.0" }, + "scripts": { + "build-ts": "flow-to-ts src/*.js --write && tsc --emitDeclarationOnly --declaration --esModuleInterop src/*.ts && mkdir -p lib && mv src/*.d.ts lib/. && rm src/*.ts", + "check-ts": "tsc --noEmit lib/diagnostic.d.ts" + }, "dependencies": { "json-source-map": "^0.6.1", "nullthrows": "^1.1.1" diff --git a/packages/core/diagnostic/src/diagnostic.js b/packages/core/diagnostic/src/diagnostic.js index bdeb56d5854..428578c8ab0 100644 --- a/packages/core/diagnostic/src/diagnostic.js +++ b/packages/core/diagnostic/src/diagnostic.js @@ -1,5 +1,4 @@ // @flow strict-local -import type {FilePath} from '@parcel/types'; import invariant from 'assert'; import nullthrows from 'nullthrows'; @@ -35,11 +34,15 @@ export type DiagnosticCodeFrame = {| /** * The contents of the source file. * - * If no code is passed, it will be read in from Diagnostic#filePath, remember that + * If no code is passed, it will be read in from filePath, remember that * the asset's current code could be different from the input contents. * */ code?: string, + /** Path to the file this code frame is about (optional, absolute or relative to the project root) */ + filePath?: string, + /** Language of the file this code frame is about (optional) */ + language?: string, codeHighlights: Array, |}; @@ -58,13 +61,8 @@ export type Diagnostic = {| /** Name of the error (optional) */ name?: string, - /** Path to the file this diagnostic is about (optional, absolute or relative to the project root) */ - filePath?: FilePath, - /** Language of the file this diagnostic is about (optional) */ - language?: string, - /** A code frame points to a certain location(s) in the file this diagnostic is linked to (optional) */ - codeFrame?: DiagnosticCodeFrame, + codeFrames?: ?Array, /** An optional list of strings that suggest ways to resolve this issue */ hints?: Array, @@ -74,19 +72,18 @@ export type Diagnostic = {| |}; // This type should represent all error formats Parcel can encounter... -export type PrintableError = Error & { - fileName?: string, - filePath?: string, - codeFrame?: string, - highlightedCodeFrame?: string, +export interface PrintableError extends Error { + fileName?: string; + filePath?: string; + codeFrame?: string; + highlightedCodeFrame?: string; loc?: ?{ column: number, line: number, ... - }, - source?: string, - ... -}; + }; + source?: string; +} export type DiagnosticWithoutOrigin = {| ...Diagnostic, @@ -99,38 +96,41 @@ export type Diagnostifiable = | Array | ThrowableDiagnostic | PrintableError + | Error | string; /** Normalize the given value into a diagnostic. */ export function anyToDiagnostic(input: Diagnostifiable): Array { - // $FlowFixMe - let diagnostic: Array = input; - - if (input instanceof ThrowableDiagnostic) { - diagnostic = input.diagnostics; + if (Array.isArray(input)) { + return input; + } else if (input instanceof ThrowableDiagnostic) { + return input.diagnostics; } else if (input instanceof Error) { - diagnostic = errorToDiagnostic(input); + return errorToDiagnostic(input); + } else if (typeof input === 'string') { + return [{message: input}]; + } else if (typeof input === 'object') { + return [input]; + } else { + return errorToDiagnostic(input); } - - return Array.isArray(diagnostic) ? diagnostic : [diagnostic]; } /** Normalize the given error into a diagnostic. */ export function errorToDiagnostic( error: ThrowableDiagnostic | PrintableError | string, - defaultValues: {| + defaultValues?: {| origin?: ?string, filePath?: ?string, - |} = {...null}, + |}, ): Array { - let codeFrame: DiagnosticCodeFrame | void = undefined; + let codeFrames: ?Array = undefined; if (typeof error === 'string') { return [ { origin: defaultValues?.origin ?? 'Error', message: escapeMarkdown(error), - codeFrame, }, ]; } @@ -145,21 +145,28 @@ export function errorToDiagnostic( } if (error.loc && error.source != null) { - codeFrame = { - code: error.source, - codeHighlights: [ - { - start: { - line: error.loc.line, - column: error.loc.column, - }, - end: { - line: error.loc.line, - column: error.loc.column, + codeFrames = [ + { + filePath: + error.filePath ?? + error.fileName ?? + defaultValues?.filePath ?? + undefined, + code: error.source, + codeHighlights: [ + { + start: { + line: error.loc.line, + column: error.loc.column, + }, + end: { + line: error.loc.line, + column: error.loc.column, + }, }, - }, - ], - }; + ], + }, + ]; } return [ @@ -167,13 +174,8 @@ export function errorToDiagnostic( origin: defaultValues?.origin ?? 'Error', message: escapeMarkdown(error.message), name: error.name, - filePath: - error.filePath ?? - error.fileName ?? - defaultValues?.filePath ?? - undefined, stack: error.highlightedCodeFrame ?? error.codeFrame ?? error.stack, - codeFrame, + codeFrames, }, ]; } @@ -197,7 +199,9 @@ export default class ThrowableDiagnostic extends Error { // Construct error from diagnostics super(diagnostics[0].message); + // @ts-ignore this.stack = diagnostics[0].stack ?? super.stack; + // @ts-ignore this.name = diagnostics[0].name ?? super.name; this.diagnostics = diagnostics; @@ -280,8 +284,7 @@ export function escapeMarkdown(s: string): string { return result; } -// $FlowFixMe[unclear-type] -type TemplateInput = any; +type TemplateInput = $FlowFixMe; const mdVerbatim = Symbol(); export function md( @@ -291,30 +294,27 @@ export function md( let result = []; for (let i = 0; i < params.length; i++) { let param = params[i]; - result.push( - strings[i], - param?.[mdVerbatim] ? param.value : escapeMarkdown(`${param}`), - ); + result.push(strings[i], param?.[mdVerbatim] ?? escapeMarkdown(`${param}`)); } return result.join('') + strings[strings.length - 1]; } -md.bold = function(s: TemplateInput): {|value: string|} { +md.bold = function(s: TemplateInput): TemplateInput { // $FlowFixMe[invalid-computed-prop] - return {[mdVerbatim]: true, value: '**' + escapeMarkdown(`${s}`) + '**'}; + return {[mdVerbatim]: '**' + escapeMarkdown(`${s}`) + '**'}; }; -md.italic = function(s: TemplateInput): {|value: string|} { +md.italic = function(s: TemplateInput): TemplateInput { // $FlowFixMe[invalid-computed-prop] - return {[mdVerbatim]: true, value: '_' + escapeMarkdown(`${s}`) + '_'}; + return {[mdVerbatim]: '_' + escapeMarkdown(`${s}`) + '_'}; }; -md.underline = function(s: TemplateInput): {|value: string|} { +md.underline = function(s: TemplateInput): TemplateInput { // $FlowFixMe[invalid-computed-prop] - return {[mdVerbatim]: true, value: '__' + escapeMarkdown(`${s}`) + '__'}; + return {[mdVerbatim]: '__' + escapeMarkdown(`${s}`) + '__'}; }; -md.strikethrough = function(s: TemplateInput): {|value: string|} { +md.strikethrough = function(s: TemplateInput): TemplateInput { // $FlowFixMe[invalid-computed-prop] - return {[mdVerbatim]: true, value: '~~' + escapeMarkdown(`${s}`) + '~~'}; + return {[mdVerbatim]: '~~' + escapeMarkdown(`${s}`) + '~~'}; }; diff --git a/packages/core/fs/index.d.ts b/packages/core/fs/index.d.ts new file mode 100644 index 00000000000..afed1f43db7 --- /dev/null +++ b/packages/core/fs/index.d.ts @@ -0,0 +1,16 @@ +import type {FileSystem} from './lib/types'; +import type WorkerFarm from '@parcel/workers'; + +export * from './lib/types'; + +export const NodeFS: { + new (): FileSystem; +}; + +export const MemoryFS: { + new (farm: WorkerFarm): FileSystem; +}; + +export const OverlayFS: { + new (writable: FileSystem, readable: FileSystem): FileSystem; +}; diff --git a/packages/core/fs/package.json b/packages/core/fs/package.json index 996a6cb4c65..0f6f1465760 100644 --- a/packages/core/fs/package.json +++ b/packages/core/fs/package.json @@ -16,12 +16,18 @@ }, "main": "lib/index.js", "source": "src/index.js", + "types": "index.d.ts", "engines": { "node": ">= 12.0.0" }, + "scripts": { + "build-ts": "mkdir -p lib && flow-to-ts src/types.js > lib/types.d.ts", + "check-ts": "tsc --noEmit index.d.ts" + }, "dependencies": { "@parcel/fs-search": "2.0.0-beta.3.1", "@parcel/fs-write-stream-atomic": "2.0.0-beta.3.1", + "@parcel/types": "2.0.0-beta.3.1", "@parcel/utils": "2.0.0-beta.3.1", "@parcel/watcher": "2.0.0-alpha.10", "@parcel/workers": "2.0.0-beta.3.1", @@ -29,7 +35,8 @@ "mkdirp": "^0.5.1", "ncp": "^2.0.0", "nullthrows": "^1.1.1", - "rimraf": "^3.0.2" + "rimraf": "^3.0.2", + "utility-types": "^3.10.0" }, "peerDependencies": { "@parcel/core": "^2.0.0-alpha.3.1" diff --git a/packages/core/fs/src/MemoryFS.js b/packages/core/fs/src/MemoryFS.js index 2b80d5dda60..890ff79d23d 100644 --- a/packages/core/fs/src/MemoryFS.js +++ b/packages/core/fs/src/MemoryFS.js @@ -1,6 +1,6 @@ // @flow -import type {FileSystem, FileOptions, ReaddirOptions} from './types'; +import type {FileSystem, FileOptions, ReaddirOptions, Encoding} from './types'; import type {FilePath} from '@parcel/types'; import type { Event, @@ -160,7 +160,7 @@ export class MemoryFS implements FileSystem { async writeFile( filePath: FilePath, contents: Buffer | string, - options: ?FileOptions, + options?: ?FileOptions, ) { filePath = this._normalizePath(filePath); if (this.dirs.has(filePath)) { @@ -195,11 +195,11 @@ export class MemoryFS implements FileSystem { } // eslint-disable-next-line require-await - async readFile(filePath: FilePath, encoding?: buffer$Encoding): Promise { + async readFile(filePath: FilePath, encoding?: Encoding): Promise { return this.readFileSync(filePath, encoding); } - readFileSync(filePath: FilePath, encoding?: buffer$Encoding): any { + readFileSync(filePath: FilePath, encoding?: Encoding): any { filePath = this._normalizePath(filePath); let file = this.files.get(filePath); if (file == null) { @@ -844,7 +844,7 @@ class Dirent { name: string; #mode: number; - constructor(name: string, entry: {mode: number, ...}) { + constructor(name: string, entry: interface {mode: number}) { this.name = name; this.#mode = entry.mode; } diff --git a/packages/core/fs/src/NodeFS.js b/packages/core/fs/src/NodeFS.js index 13935c30f83..a6e9a428336 100644 --- a/packages/core/fs/src/NodeFS.js +++ b/packages/core/fs/src/NodeFS.js @@ -1,7 +1,7 @@ // @flow import type {ReadStream, Stats} from 'fs'; import type {Writable} from 'stream'; -import type {FileOptions, FileSystem} from './types'; +import type {FileOptions, FileSystem, Encoding} from './types'; import type {FilePath} from '@parcel/types'; import type { Event, @@ -42,10 +42,10 @@ export class NodeFS implements FileSystem { ncp: any = promisify(ncp); createReadStream: (path: string, options?: any) => ReadStream = fs.createReadStream; - cwd: () => string = process.cwd; - chdir: (directory: string) => void = process.chdir; + cwd: () => string = () => process.cwd(); + chdir: (directory: string) => void = directory => process.chdir(directory); - statSync: (path: string) => Stats = fs.statSync; + statSync: (path: string) => Stats = path => fs.statSync(path); realpathSync: (path: string, cache?: any) => string = process.platform === 'win32' ? fs.realpathSync : fs.realpathSync.native; existsSync: (path: string) => boolean = fs.existsSync; @@ -79,7 +79,7 @@ export class NodeFS implements FileSystem { await fs.promises.rename(tmpFilePath, filePath); } - readFileSync(filePath: FilePath, encoding?: buffer$Encoding): any { + readFileSync(filePath: FilePath, encoding?: Encoding): any { if (encoding != null) { return fs.readFileSync(filePath, encoding); } diff --git a/packages/core/fs/src/types.js b/packages/core/fs/src/types.js index e90b1575cf1..6463e31b3d0 100644 --- a/packages/core/fs/src/types.js +++ b/packages/core/fs/src/types.js @@ -1,6 +1,5 @@ // @flow import type {FilePath} from '@parcel/types'; -import type {Stats} from 'fs'; import type {Readable, Writable} from 'stream'; import type { Event, @@ -13,11 +12,52 @@ export type ReaddirOptions = | {withFileTypes?: false, ...} | {withFileTypes: true, ...}; +export interface Stats { + dev: number; + ino: number; + mode: number; + nlink: number; + uid: number; + gid: number; + rdev: number; + size: number; + blksize: number; + blocks: number; + atimeMs: number; + mtimeMs: number; + ctimeMs: number; + birthtimeMs: number; + atime: Date; + mtime: Date; + ctime: Date; + birthtime: Date; + + isFile(): boolean; + isDirectory(): boolean; + isBlockDevice(): boolean; + isCharacterDevice(): boolean; + isSymbolicLink(): boolean; + isFIFO(): boolean; + isSocket(): boolean; +} + +export type Encoding = + | 'hex' + | 'utf8' + | 'utf-8' + | 'ascii' + | 'binary' + | 'base64' + | 'ucs2' + | 'ucs-2' + | 'utf16le' + | 'latin1'; + export interface FileSystem { readFile(filePath: FilePath): Promise; - readFile(filePath: FilePath, encoding: buffer$Encoding): Promise; + readFile(filePath: FilePath, encoding: Encoding): Promise; readFileSync(filePath: FilePath): Buffer; - readFileSync(filePath: FilePath, encoding: buffer$Encoding): string; + readFileSync(filePath: FilePath, encoding: Encoding): string; writeFile( filePath: FilePath, contents: Buffer | string, @@ -28,8 +68,8 @@ export interface FileSystem { destination: FilePath, flags?: number, ): Promise; - stat(filePath: FilePath): Promise<$Shape>; - statSync(filePath: FilePath): $Shape; + stat(filePath: FilePath): Promise; + statSync(filePath: FilePath): Stats; readdir( path: FilePath, opts?: {withFileTypes?: false, ...}, @@ -45,8 +85,8 @@ export interface FileSystem { mkdirp(path: FilePath): Promise; rimraf(path: FilePath): Promise; ncp(source: FilePath, destination: FilePath): Promise; - createReadStream(path: FilePath, options: ?FileOptions): Readable; - createWriteStream(path: FilePath, options: ?FileOptions): Writable; + createReadStream(path: FilePath, options?: ?FileOptions): Readable; + createWriteStream(path: FilePath, options?: ?FileOptions): Writable; cwd(): FilePath; chdir(dir: FilePath): void; watch( diff --git a/packages/core/integration-tests/package.json b/packages/core/integration-tests/package.json index f1cd8a076cb..c921bb98328 100644 --- a/packages/core/integration-tests/package.json +++ b/packages/core/integration-tests/package.json @@ -18,6 +18,7 @@ "@babel/plugin-syntax-export-namespace-from": "^7.2.0", "@babel/plugin-syntax-module-attributes": "^7.10.4", "@babel/preset-env": "^7.12.11", + "@babel/preset-typescript": "^7.14.5", "@jetbrains/kotlinc-js-api": "^1.2.12", "@mdx-js/react": "^1.5.3", "chalk": "^4.1.0", diff --git a/packages/core/integration-tests/test/cache.js b/packages/core/integration-tests/test/cache.js index 25c9ce295d0..4be0663e313 100644 --- a/packages/core/integration-tests/test/cache.js +++ b/packages/core/integration-tests/test/cache.js @@ -15,30 +15,36 @@ import { sleep, getNextBuild, distDir, + getParcelOptions, + assertNoFilePathInCache, } from '@parcel/test-utils'; import {md} from '@parcel/diagnostic'; import fs from 'fs'; import {NodePackageManager} from '@parcel/package-manager'; import {createWorkerFarm} from '@parcel/core'; +import resolveOptions from '@parcel/core/src/resolveOptions'; let inputDir: string; let packageManager = new NodePackageManager(inputFS, '/'); -function runBundle(entries = 'src/index.js', opts) { - entries = (Array.isArray(entries) ? entries : [entries]).map(entry => +function getEntries(entries = 'src/index.js') { + return (Array.isArray(entries) ? entries : [entries]).map(entry => path.resolve(inputDir, entry), ); +} - return bundler( - entries, - mergeParcelOptions( - { - inputFS: overlayFS, - shouldDisableCache: false, - }, - opts, - ), - ).run(); +function getOptions(opts) { + return mergeParcelOptions( + { + inputFS: overlayFS, + shouldDisableCache: false, + }, + opts, + ); +} + +function runBundle(entries = 'src/index.js', opts) { + return bundler(getEntries(entries), getOptions(opts)).run(); } type UpdateFn = BuildSuccessEvent => @@ -69,13 +75,33 @@ async function testCache(update: UpdateFn | TestConfig, integration) { } } + let resolvedOptions = await resolveOptions( + getParcelOptions(getEntries(entries), getOptions(options)), + ); + let b = await runBundle(entries, options); + await assertNoFilePathInCache( + resolvedOptions.outputFS, + resolvedOptions.cacheDir, + resolvedOptions.projectRoot, + ); + // update let newOptions = await update(b); + options = mergeParcelOptions(options || {}, newOptions); // Run cached build - b = await runBundle(entries, mergeParcelOptions(options || {}, newOptions)); + b = await runBundle(entries, options); + + resolvedOptions = await resolveOptions( + getParcelOptions(getEntries(entries), getOptions(options)), + ); + await assertNoFilePathInCache( + resolvedOptions.outputFS, + resolvedOptions.cacheDir, + resolvedOptions.projectRoot, + ); return b; } @@ -1401,6 +1427,7 @@ describe('cache', function() { targets: { esmodule: { outputFormat: 'esmodule', + isLibrary: true, }, }, }), @@ -1563,6 +1590,7 @@ describe('cache', function() { targets: { esmodule: { outputFormat: 'esmodule', + isLibrary: true, }, }, }), @@ -1679,6 +1707,7 @@ describe('cache', function() { targets: { modern: { outputFormat: 'esmodule', + isLibrary: true, }, legacy: { outputFormat: 'commonjs', @@ -1883,6 +1912,7 @@ describe('cache', function() { targets: { modern: { outputFormat: 'esmodule', + isLibrary: true, }, }, }), @@ -1912,6 +1942,7 @@ describe('cache', function() { targets: { modern: { outputFormat: 'esmodule', + isLibrary: true, }, }, }), @@ -2305,7 +2336,7 @@ describe('cache', function() { return { defaultTargetOptions: { - distDir: 'dist/test', + distDir: path.join(__dirname, 'integration/cache/dist/test'), }, }; }, @@ -3460,7 +3491,7 @@ describe('cache', function() { { entries: ['index.sass'], env: { - SASS_PATH: path.join(inputDir, 'include-path'), + SASS_PATH: 'include-path', }, async setup() { await overlayFS.mkdirp(path.join(inputDir, 'include2')); @@ -3481,7 +3512,7 @@ describe('cache', function() { return { env: { - SASS_PATH: path.join(inputDir, 'include2'), + SASS_PATH: 'include2', }, }; }, @@ -5271,6 +5302,75 @@ describe('cache', function() { let res = await run(b.bundleGraph); assert.equal(res, 4); }); + + it('should invalidate when a terser config is modified', async function() { + let b = await testCache({ + mode: 'production', + async setup() { + await overlayFS.writeFile( + path.join(inputDir, '.terserrc'), + JSON.stringify({ + mangle: false, + }), + ); + }, + async update(b) { + let contents = await overlayFS.readFile( + b.bundleGraph.getBundles()[0].filePath, + 'utf8', + ); + assert(contents.includes('$parcel$interopDefault')); + + await overlayFS.writeFile( + path.join(inputDir, '.terserrc'), + JSON.stringify({ + mangle: true, + }), + ); + }, + }); + + let contents = await overlayFS.readFile( + b.bundleGraph.getBundles()[0].filePath, + 'utf8', + ); + assert(!contents.includes('$parcel$interopDefault')); + }); + + it('should invalidate when an htmlnano config is modified', async function() { + let b = await testCache({ + mode: 'production', + entries: ['src/index.html'], + async setup() { + await overlayFS.writeFile( + path.join(inputDir, '.htmlnanorc'), + JSON.stringify({ + removeAttributeQuotes: true, + }), + ); + }, + async update(b) { + let contents = await overlayFS.readFile( + b.bundleGraph.getBundles()[0].filePath, + 'utf8', + ); + assert(contents.includes('type=module')); + + await overlayFS.writeFile( + path.join(inputDir, '.htmlnanorc'), + JSON.stringify({ + removeAttributeQuotes: false, + }), + ); + }, + }); + + let contents = await overlayFS.readFile( + b.bundleGraph.getBundles()[0].filePath, + 'utf8', + ); + assert(contents.includes('type="module"')); + }); }); describe('scope hoisting', function() { @@ -5279,6 +5379,58 @@ describe('cache', function() { it('should support updating sideEffects config', function() {}); it('should support removing sideEffects config', function() {}); + + it('should wrap modules when they become conditional', async function() { + let b = await testCache( + { + defaultTargetOptions: { + shouldScopeHoist: true, + }, + entries: ['a.js'], + async setup() { + let contents = await overlayFS.readFile( + path.join(inputDir, 'a.js'), + 'utf8', + ); + await overlayFS.writeFile( + path.join(inputDir, 'a.js'), + contents.replace(/if \(b\) \{((?:.|\n)+)\}/, '$1'), + ); + }, + async update(b) { + let out = []; + await run(b.bundleGraph, { + b: false, + output(o) { + out.push(o); + }, + }); + + assert.deepEqual(out, ['a', 'b', 'c', 'd']); + + let contents = await overlayFS.readFile( + path.join( + __dirname, + 'integration/scope-hoisting/commonjs/require-conditional/a.js', + ), + 'utf8', + ); + await overlayFS.writeFile(path.join(inputDir, 'a.js'), contents); + }, + }, + 'scope-hoisting/commonjs/require-conditional', + ); + + let out = []; + await run(b.bundleGraph, { + b: false, + output(o) { + out.push(o); + }, + }); + + assert.deepEqual(out, ['a', 'd']); + }); }); describe('runtime', () => { @@ -5498,4 +5650,34 @@ describe('cache', function() { } } }); + + it('should support moving the project root', async function() { + // This test relies on the real filesystem because the memory fs doesn't support renames. + // But renameSync is broken on windows in CI with EPERM errors. Just skip this test for now. + if (process.platform === 'win32') { + return; + } + + let b = await testCache({ + inputFS, + outputFS: inputFS, + async setup() { + await inputFS.mkdirp(inputDir); + await inputFS.ncp(path.join(__dirname, '/integration/cache'), inputDir); + }, + update: async b => { + assert.equal(await run(b.bundleGraph), 4); + + await inputFS.writeFile( + path.join(inputDir, 'src/nested/test.js'), + 'export default 4', + ); + + fs.renameSync(inputDir, (inputDir += '_2')); + await sleep(100); + }, + }); + + assert.equal(await run(b.bundleGraph), 6); + }); }); diff --git a/packages/core/integration-tests/test/css.js b/packages/core/integration-tests/test/css.js index 4e853d69774..a9b46c6484f 100644 --- a/packages/core/integration-tests/test/css.js +++ b/packages/core/integration-tests/test/css.js @@ -353,22 +353,24 @@ describe('css', () => { { message: "Failed to resolve 'x.png' from './index.scss'", origin: '@parcel/core', - filePath: fixture, - codeFrame: { - code, - codeHighlights: [ - { - start: { - line: 5, - column: 3, + codeFrames: [ + { + filePath: fixture, + code, + codeHighlights: [ + { + start: { + line: 5, + column: 3, + }, + end: { + line: 5, + column: 3, + }, }, - end: { - line: 5, - column: 3, - }, - }, - ], - }, + ], + }, + ], }, { message: "Cannot load file './x.png' in './'.", diff --git a/packages/core/integration-tests/test/eslint-validation.js b/packages/core/integration-tests/test/eslint-validation.js index c714c5ad503..5379dfa7659 100644 --- a/packages/core/integration-tests/test/eslint-validation.js +++ b/packages/core/integration-tests/test/eslint-validation.js @@ -19,16 +19,16 @@ describe('eslint-validator', function() { } catch (e) { assert.equal(e.name, 'BuildError'); assert(Array.isArray(e.diagnostics)); - assert(e.diagnostics[0].codeFrame); + assert(e.diagnostics[0].codeFrames); assert.equal(e.diagnostics[0].origin, '@parcel/validator-eslint'); assert.equal( e.diagnostics[0].message, 'ESLint found **1** __errors__ and **1** __warnings__.', ); - assert.equal(e.diagnostics[0].filePath, entry); - let codeframe = e.diagnostics[0].codeFrame; + let codeframe = e.diagnostics[0].codeFrames[0]; assert(codeframe); + assert.equal(codeframe.filePath, entry); assert.equal(codeframe.codeHighlights.length, 2); codeframe.codeHighlights.sort( ({start: {line: a}}, {start: {line: b}}) => a - b, @@ -65,10 +65,10 @@ describe('eslint-validator', function() { e.diagnostics[0].message, 'ESLint found **1** __errors__ and **0** __warnings__.', ); - assert.equal(e.diagnostics[0].filePath, entry); - let codeframe = e.diagnostics[0].codeFrame; + let codeframe = e.diagnostics[0].codeFrames[0]; assert(codeframe); + assert.equal(codeframe.filePath, entry); assert.equal(codeframe.codeHighlights.length, 1); assert(codeframe.codeHighlights[0].start.line != null); assert(codeframe.codeHighlights[0].start.column != null); diff --git a/packages/core/integration-tests/test/glob.js b/packages/core/integration-tests/test/glob.js index 3c02b1b00ba..c7b80c018dd 100644 --- a/packages/core/integration-tests/test/glob.js +++ b/packages/core/integration-tests/test/glob.js @@ -146,7 +146,7 @@ describe('glob', function() { { message: 'Glob imports are not supported in html files.', origin: '@parcel/resolver-glob', - codeFrame: undefined, + codeFrames: undefined, }, ], }); @@ -161,40 +161,44 @@ describe('glob', function() { { message: "Failed to resolve 'images/\\*.jpg' from './index.css'", origin: '@parcel/core', - filePath, - codeFrame: { - code: await inputFS.readFile(filePath, 'utf8'), - codeHighlights: [ - { - start: { - column: 7, - line: 2, + codeFrames: [ + { + filePath, + code: await inputFS.readFile(filePath, 'utf8'), + codeHighlights: [ + { + start: { + column: 7, + line: 2, + }, + end: { + column: 20, + line: 2, + }, }, - end: { - column: 20, - line: 2, - }, - }, - ], - }, + ], + }, + ], }, { message: 'Glob imports are not supported in URL dependencies.', origin: '@parcel/resolver-glob', - codeFrame: { - codeHighlights: [ - { - start: { - column: 7, - line: 2, - }, - end: { - column: 20, - line: 2, + codeFrames: [ + { + codeHighlights: [ + { + start: { + column: 7, + line: 2, + }, + end: { + column: 20, + line: 2, + }, }, - }, - ], - }, + ], + }, + ], }, ], }); diff --git a/packages/core/integration-tests/test/html.js b/packages/core/integration-tests/test/html.js index c3ec4884c59..7c96e7c1d7b 100644 --- a/packages/core/integration-tests/test/html.js +++ b/packages/core/integration-tests/test/html.js @@ -961,7 +961,9 @@ describe('html', function() { for (let url of urls) { assert( bundles.find( - bundle => !bundle.isInline && path.basename(bundle.filePath) === url, + bundle => + bundle.bundleBehavior !== 'inline' && + path.basename(bundle.filePath) === url, ), ); } @@ -1206,31 +1208,36 @@ describe('html', function() { } catch (err) { assert.equal( err.message, - 'Browser scripts cannot have imports or exports. Use a