diff --git a/packages/core/core/src/Dependency.js b/packages/core/core/src/Dependency.js index fc44842482c..85f6fad0778 100644 --- a/packages/core/core/src/Dependency.js +++ b/packages/core/core/src/Dependency.js @@ -6,6 +6,7 @@ import type { SourceLocation, Symbol, BundleBehavior as IBundleBehavior, + SemverRange, } from '@parcel/types'; import type {Dependency, Environment, Target} from './types'; import {hashString} from '@parcel/hash'; @@ -29,6 +30,7 @@ type DependencyOpts = {| env: Environment, meta?: Meta, resolveFrom?: FilePath, + range?: SemverRange, target?: Target, symbols?: Map< Symbol, @@ -69,6 +71,7 @@ export function createDependency( isEntry: opts.isEntry ?? false, isOptional: opts.isOptional ?? false, meta: opts.meta || {}, + range: opts.range, symbols: opts.symbols && new Map( diff --git a/packages/core/core/src/public/Dependency.js b/packages/core/core/src/public/Dependency.js index 950bb54249a..bf22a85f5a2 100644 --- a/packages/core/core/src/public/Dependency.js +++ b/packages/core/core/src/public/Dependency.js @@ -135,6 +135,10 @@ export default class Dependency implements IDependency { ); } + get range(): ?string { + return this.#dep.range; + } + get pipeline(): ?string { return this.#dep.pipeline; } diff --git a/packages/core/core/src/types.js b/packages/core/core/src/types.js index 82fa1b3a01d..3acd4df21eb 100644 --- a/packages/core/core/src/types.js +++ b/packages/core/core/src/types.js @@ -131,6 +131,7 @@ export type Dependency = {| sourcePath: ?ProjectPath, sourceAssetType?: ?string, resolveFrom: ?ProjectPath, + range: ?SemverRange, symbols: ?Map< Symbol, {| diff --git a/packages/core/integration-tests/test/integration/swc-helpers-library/package.json b/packages/core/integration-tests/test/integration/swc-helpers-library/package.json index 86755921e24..43da441e86e 100644 --- a/packages/core/integration-tests/test/integration/swc-helpers-library/package.json +++ b/packages/core/integration-tests/test/integration/swc-helpers-library/package.json @@ -3,6 +3,6 @@ "module": "dist/module.js", "browserslist": "IE >= 11", "dependencies": { - "@swc/helpers": "*" + "@swc/helpers": "^0.4.2" } } diff --git a/packages/core/integration-tests/test/javascript.js b/packages/core/integration-tests/test/javascript.js index 4561a8e03ef..dfdbf601696 100644 --- a/packages/core/integration-tests/test/javascript.js +++ b/packages/core/integration-tests/test/javascript.js @@ -5639,6 +5639,96 @@ describe('javascript', function () { ); }); + it('should error on mismatched helpers version for libraries', async function () { + let fixture = path.join( + __dirname, + 'integration/undeclared-external/helpers.js', + ); + let pkg = path.join( + __dirname, + 'integration/undeclared-external/package.json', + ); + let pkgContents = JSON.stringify( + { + ...JSON.parse(await overlayFS.readFile(pkg, 'utf8')), + dependencies: { + '@swc/helpers': '^0.3.0', + }, + }, + false, + 2, + ); + await overlayFS.mkdirp(path.dirname(pkg)); + await overlayFS.writeFile(pkg, pkgContents); + await assert.rejects( + () => + bundle(fixture, { + mode: 'production', + inputFS: overlayFS, + defaultTargetOptions: { + shouldOptimize: false, + }, + }), + { + name: 'BuildError', + diagnostics: [ + { + message: md`Failed to resolve '${'@swc/helpers/lib/_class_call_check.js'}' from '${normalizePath( + require.resolve('@parcel/transformer-js/src/JSTransformer.js'), + )}'`, + origin: '@parcel/core', + codeFrames: [ + { + code: await inputFS.readFile(fixture, 'utf8'), + filePath: fixture, + codeHighlights: [ + { + start: { + line: 1, + column: 1, + }, + end: { + line: 1, + column: 1, + }, + }, + ], + }, + ], + }, + { + message: + 'External dependency "@swc/helpers" does not satisfy required semver range "^0.4.2".', + origin: '@parcel/resolver-default', + codeFrames: [ + { + code: pkgContents, + filePath: pkg, + language: 'json', + codeHighlights: [ + { + message: 'Found this conflicting requirement.', + start: { + line: 6, + column: 21, + }, + end: { + line: 6, + column: 28, + }, + }, + ], + }, + ], + hints: [ + 'Update the dependency on "@swc/helpers" to satisfy "^0.4.2".', + ], + }, + ], + }, + ); + }); + describe('multiple import types', function () { it('supports both static and dynamic imports to the same specifier in the same file', async function () { let b = await bundle( diff --git a/packages/core/types/index.js b/packages/core/types/index.js index 2c1ca94685e..29aa68ee9fd 100644 --- a/packages/core/types/index.js +++ b/packages/core/types/index.js @@ -525,6 +525,8 @@ export type DependencyOptions = {| * By default, this is the path of the source file where the dependency was specified. */ +resolveFrom?: FilePath, + /** The semver version range expected for the dependency. */ + +range?: SemverRange, /** The symbols within the resolved module that the source file depends on. */ +symbols?: $ReadOnlyMap< Symbol, @@ -601,6 +603,8 @@ export interface Dependency { * By default, this is the path of the source file where the dependency was specified. */ +resolveFrom: ?FilePath; + /** The semver version range expected for the dependency. */ + +range: ?SemverRange; /** The pipeline defined in .parcelrc that the dependency should be processed with. */ +pipeline: ?string; diff --git a/packages/resolvers/default/src/DefaultResolver.js b/packages/resolvers/default/src/DefaultResolver.js index 419cf59ad8f..835c881d4d1 100644 --- a/packages/resolvers/default/src/DefaultResolver.js +++ b/packages/resolvers/default/src/DefaultResolver.js @@ -34,6 +34,7 @@ export default (new Resolver({ return resolver.resolve({ filename: specifier, specifierType: dependency.specifierType, + range: dependency.range, parent: dependency.resolveFrom, env: dependency.env, sourcePath: dependency.sourcePath, diff --git a/packages/resolvers/glob/src/GlobResolver.js b/packages/resolvers/glob/src/GlobResolver.js index 2539a9becc6..e5b2d556087 100644 --- a/packages/resolvers/glob/src/GlobResolver.js +++ b/packages/resolvers/glob/src/GlobResolver.js @@ -99,6 +99,7 @@ export default (new Resolver({ invalidateOnFileChange, specifierType: dependency.specifierType, loc: dependency.loc, + range: dependency.range, }; let result; diff --git a/packages/transformers/js/src/JSTransformer.js b/packages/transformers/js/src/JSTransformer.js index a538480a775..8223ff49525 100644 --- a/packages/transformers/js/src/JSTransformer.js +++ b/packages/transformers/js/src/JSTransformer.js @@ -12,6 +12,7 @@ import nullthrows from 'nullthrows'; import ThrowableDiagnostic, {encodeJSONKeyComponent} from '@parcel/diagnostic'; import {validateSchema, remapSourceLocation, isGlobMatch} from '@parcel/utils'; import WorkerFarm from '@parcel/workers'; +import pkg from '../package.json'; const JSX_EXTENSIONS = { jsx: true, @@ -689,6 +690,17 @@ export default (new Transformer({ }; } + // Add required version range for helpers. + let range; + if (isHelper) { + let idx = dep.specifier.indexOf('/'); + if (dep.specifier[0] === '@') { + idx = dep.specifier.indexOf('/', idx + 1); + } + let module = idx >= 0 ? dep.specifier.slice(0, idx) : dep.specifier; + range = pkg.dependencies[module]; + } + asset.addDependency({ specifier: dep.specifier, specifierType: dep.kind === 'Require' ? 'commonjs' : 'esm', @@ -697,6 +709,7 @@ export default (new Transformer({ isOptional: dep.is_optional, meta, resolveFrom: isHelper ? __filename : undefined, + range, env, }); } diff --git a/packages/utils/node-resolver-core/package.json b/packages/utils/node-resolver-core/package.json index d29055ae3e6..36f5eac7a0f 100644 --- a/packages/utils/node-resolver-core/package.json +++ b/packages/utils/node-resolver-core/package.json @@ -21,7 +21,8 @@ "dependencies": { "@parcel/diagnostic": "2.6.0", "@parcel/utils": "2.6.0", - "nullthrows": "^1.1.1" + "nullthrows": "^1.1.1", + "semver": "^5.7.1" }, "devDependencies": { "assert": "^2.0.0", diff --git a/packages/utils/node-resolver-core/src/NodeResolver.js b/packages/utils/node-resolver-core/src/NodeResolver.js index 66d216e794a..7f8ddef69d8 100644 --- a/packages/utils/node-resolver-core/src/NodeResolver.js +++ b/packages/utils/node-resolver-core/src/NodeResolver.js @@ -8,6 +8,7 @@ import type { SpecifierType, PluginLogger, SourceLocation, + SemverRange, } from '@parcel/types'; import type {FileSystem} from '@parcel/fs'; import type {PackageManager} from '@parcel/package-manager'; @@ -27,11 +28,13 @@ import { import ThrowableDiagnostic, { generateJSONCodeHighlights, md, + encodeJSONKeyComponent, } from '@parcel/diagnostic'; import builtins, {empty} from './builtins'; import nullthrows from 'nullthrows'; import _Module from 'module'; import {fileURLToPath} from 'url'; +import semver from 'semver'; const EMPTY_SHIM = require.resolve('./_empty'); @@ -68,6 +71,7 @@ type ResolverContext = {| invalidateOnFileCreate: Array, invalidateOnFileChange: Set, specifierType: SpecifierType, + range: ?SemverRange, loc: ?SourceLocation, |}; @@ -111,6 +115,7 @@ export default class NodeResolver { filename, parent, specifierType, + range, env, sourcePath, loc, @@ -118,6 +123,7 @@ export default class NodeResolver { filename: FilePath, parent: ?FilePath, specifierType: SpecifierType, + range?: ?SemverRange, env: Environment, sourcePath?: ?FilePath, loc?: ?SourceLocation, @@ -126,6 +132,7 @@ export default class NodeResolver { invalidateOnFileCreate: [], invalidateOnFileChange: new Set(), specifierType, + range, loc, }; @@ -485,6 +492,40 @@ export default class NodeResolver { }, }); } + + if (ctx.range) { + let range = ctx.range; + let depRange = + pkg.dependencies?.[moduleName] || pkg.peerDependencies?.[moduleName]; + if (depRange && !semver.intersects(depRange, range)) { + let pkgContent = await this.fs.readFile(pkg.pkgfile, 'utf8'); + let field = pkg.dependencies?.[moduleName] + ? 'dependencies' + : 'peerDependencies'; + throw new ThrowableDiagnostic({ + diagnostic: { + message: md`External dependency "${moduleName}" does not satisfy required semver range "${range}".`, + codeFrames: [ + { + filePath: pkg.pkgfile, + language: 'json', + code: pkgContent, + codeHighlights: generateJSONCodeHighlights(pkgContent, [ + { + key: `/${field}/${encodeJSONKeyComponent(moduleName)}`, + type: 'value', + message: 'Found this conflicting requirement.', + }, + ]), + }, + ], + hints: [ + `Update the dependency on "${moduleName}" to satisfy "${range}".`, + ], + }, + }); + } + } } async resolveFilename( diff --git a/packages/utils/node-resolver-core/test/fixture/package.json b/packages/utils/node-resolver-core/test/fixture/package.json index 0e4d70aacbf..c8c96b71fcc 100755 --- a/packages/utils/node-resolver-core/test/fixture/package.json +++ b/packages/utils/node-resolver-core/test/fixture/package.json @@ -11,7 +11,7 @@ "glob/*/*": "./nested/$2" }, "dependencies": { - "foo": "*" + "foo": "^0.3.4" }, "peerDependencies": { "bar": "*" diff --git a/packages/utils/node-resolver-core/test/resolver.js b/packages/utils/node-resolver-core/test/resolver.js index 3b7e3e3fff8..e28f9bf5f7f 100644 --- a/packages/utils/node-resolver-core/test/resolver.js +++ b/packages/utils/node-resolver-core/test/resolver.js @@ -2679,6 +2679,29 @@ describe('resolver', function () { assert.deepEqual(result, {isExcluded: true}); }); + + it('should error when a library has an incorrect external dependency version', async function () { + let result = await resolver.resolve({ + env: new Environment( + createEnvironment({ + context: 'browser', + isLibrary: true, + includeNodeModules: false, + }), + DEFAULT_OPTIONS, + ), + filename: 'foo', + specifierType: 'esm', + range: '^0.4.0', + parent: path.join(rootDir, 'foo.js'), + sourcePath: path.join(rootDir, 'foo.js'), + }); + + assert.equal( + result?.diagnostics?.[0].message, + 'External dependency "foo" does not satisfy required semver range "^0.4.0".', + ); + }); }); describe('urls', function () {