From c261a50340df3f507ab4ce10539f50b2102edba6 Mon Sep 17 00:00:00 2001 From: Guy Paterson-Jones Date: Wed, 19 Apr 2023 13:41:52 +0200 Subject: [PATCH 01/11] feat: add script to test npm behaviour with respect to locks for installs and updates --- test/npm_compatibility/README.md | 66 +++++++++++++++++++ test/npm_compatibility/npm-behaviour.sh | 64 ++++++++++++++++++ .../primary-in-range/package-lock.json | 29 ++++++++ .../primary-in-range/package.json | 6 ++ .../package-lock.json | 29 ++++++++ .../package.json | 6 ++ .../package-lock.json | 29 ++++++++ .../package.json | 6 ++ .../primary-out-range/package-lock.json | 29 ++++++++ .../primary-out-range/package.json | 6 ++ .../secondary-in-range/package-lock.json | 29 ++++++++ .../secondary-in-range/package.json | 6 ++ .../secondary-out-range/package-lock.json | 29 ++++++++ .../secondary-out-range/package.json | 6 ++ 14 files changed, 340 insertions(+) create mode 100644 test/npm_compatibility/README.md create mode 100755 test/npm_compatibility/npm-behaviour.sh create mode 100644 test/npm_compatibility/primary-in-range/package-lock.json create mode 100644 test/npm_compatibility/primary-in-range/package.json create mode 100644 test/npm_compatibility/primary-not-latest-secondary-in-range/package-lock.json create mode 100644 test/npm_compatibility/primary-not-latest-secondary-in-range/package.json create mode 100644 test/npm_compatibility/primary-not-latest-secondary-out-range/package-lock.json create mode 100644 test/npm_compatibility/primary-not-latest-secondary-out-range/package.json create mode 100644 test/npm_compatibility/primary-out-range/package-lock.json create mode 100644 test/npm_compatibility/primary-out-range/package.json create mode 100644 test/npm_compatibility/secondary-in-range/package-lock.json create mode 100644 test/npm_compatibility/secondary-in-range/package.json create mode 100644 test/npm_compatibility/secondary-out-range/package-lock.json create mode 100644 test/npm_compatibility/secondary-out-range/package.json diff --git a/test/npm_compatibility/README.md b/test/npm_compatibility/README.md new file mode 100644 index 0000000..b04dc5b --- /dev/null +++ b/test/npm_compatibility/README.md @@ -0,0 +1,66 @@ +# NPM Compatibility + +We want to match the behaviour of `npm` for installs and updates, for better +ecosystem alignment. The main question is: when does a primary/secondary +dependency's version get bumped? This obviously depends on the constraint in +your `package.json`, and what version (if any) is already installed in your +`package-lock.json`. Since `@jspm/generator` doesn't currently have a lockfile, +we treat all top-level `"imports"` in your map as primary dependencies, and any +transitive dependencies in your `"scopes"` as secondary dependencies. + +The package we're using for testing this stuff is `wayfarer@6.6.2`, since it +has a single dependency on `xtend@^4.0.1`, and multiple versions in the `6.6.x` +range. To see the behaviour for your current version of `npm`, you can run +`./npm-behaviour.sh`. Each of the four cases below has a corresponding test +against the generator to catch regressions. + + +## Test Cases + +### `npm install ` + +Primary in range: *bumped to latest in range* +Primary out of range: *bumped to latest in range* +Secondary in range: *kept at current version* +Secondary out of range: *bumped to latest in range* +Primary not latest, secondary in range: *primary bumped, secondary kept* +Primary not latest, secondary out range: *primary bumped, secondary bumped* + +### `npm install` + +Primary in range: *kept at current version* +Primary out of range: *bumped to latest in range* +Secondary in range: *kept at current version* +Secondary out of range: *bumped to latest in range* +Primary not latest, secondary in range: *primary kept, secondary kept* +Primary not latest, secondary out range: *primary kept, secondary bumped* + +### `npm update ` + +Primary in range: *bumped to latest in range* +Primary out of range: *bumped to latest in range* +Secondary in range: *kept at current version* +Secondary out of range: *bumped to latest in range* +Primary not latest, secondary in range: *primary bumped, secondary kept* +Primary not latest, secondary out range: *primary bumped, secondary bumped* + +### `npm update` + +Primary in range: *bumped to latest in range* +Primary out of range: *bumped to latest in range* +Secondary in range: *bumped to latest in range* +Secondary out of range: *bumped to latest in range* +Primary not latest, secondary in range: *primary bumped, secondary bumped* +Primary not latest, secondary out range: *primary bumped, secondary bumped* + + +## Common Rules + +An argumentless `update` bumps the versions of everything to latest compatible. + +An argumentless `install` bumps everything that is out-of-range to latest +compatible. + +An intentful `install `, always bumps the primary to latest compatible, +and bumps the secondaries only if they're out of range. An intentful +`update ` has exactly the same behaviour. diff --git a/test/npm_compatibility/npm-behaviour.sh b/test/npm_compatibility/npm-behaviour.sh new file mode 100755 index 0000000..9fc663e --- /dev/null +++ b/test/npm_compatibility/npm-behaviour.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Spits out the behaviour of npm in various version resolution scenarios. +set -e + +function get_version() { + package_name=$1 + echo $(jq -r ".packages.\"node_modules/$package_name\".version" package-lock.json) +} + +function get_range() { + package_name=$1 + echo $(jq -r ".dependencies.\"${package_name}\"" package.json) +} + +function test_command() { + cmd=$1 + tests=( + "primary-in-range" + "primary-out-range" + "secondary-in-range" + "secondary-out-range" + "primary-not-latest-secondary-in-range" + "primary-not-latest-secondary-out-range" + ) + + for test in "${tests[@]}" + do + cp "${test}/package.json" package.json.bkp + cp "${test}/package-lock.json" package-lock.json.bkp + + cd "${test}" + primary="wayfarer" + primary_range=$(get_range "${primary}") + primary_preversion=$(get_version "${primary}") + secondary="xtend" + secondary_range="^4.0.1" + secondary_preversion=$(get_version "${secondary}") + + echo "Running \"npm ${@}\" in ${test}:" + npm "${@}" 2>&1 1>/dev/null + + primary_postversion=$(get_version "${primary}") + secondary_postversion=$(get_version "${secondary}") + echo " range (pr): ${primary_range}" + echo " before (pr): ${primary}@${primary_preversion}" + echo " after (pr): ${primary}@${primary_postversion}" + echo " range (sn): ${secondary_range}" + echo " before (sn): ${secondary}@${secondary_preversion}" + echo " after (sn): ${secondary}@${secondary_postversion}" + cd - 2>&1 1>/dev/null + + rm -r "${test}/" + mkdir "${test}" + mv package.json.bkp "${test}/package.json" + mv package-lock.json.bkp "${test}/package-lock.json" + done + echo +} + +cd $(dirname "$0") +test_command install wayfarer +test_command install +test_command update wayfarer +test_command update diff --git a/test/npm_compatibility/primary-in-range/package-lock.json b/test/npm_compatibility/primary-in-range/package-lock.json new file mode 100644 index 0000000..0d6baa5 --- /dev/null +++ b/test/npm_compatibility/primary-in-range/package-lock.json @@ -0,0 +1,29 @@ +{ + "name": "test", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "test", + "dependencies": { + "wayfarer": "^6.6.2" + } + }, + "node_modules/wayfarer": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/wayfarer/-/wayfarer-6.6.2.tgz", + "integrity": "sha512-jwIufUN6EYfMeCJYBA8r0YytqHaSGACLtOddMeBtdq5gTo2F2XgK+t7eEXSPUBF9vm+hdI/iPOtSz1EJOd01dQ==", + "dependencies": { + "xtend": "^4.0.1" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/test/npm_compatibility/primary-in-range/package.json b/test/npm_compatibility/primary-in-range/package.json new file mode 100644 index 0000000..8ffd974 --- /dev/null +++ b/test/npm_compatibility/primary-in-range/package.json @@ -0,0 +1,6 @@ +{ + "name": "test", + "dependencies": { + "wayfarer": "^6.6.2" + } +} diff --git a/test/npm_compatibility/primary-not-latest-secondary-in-range/package-lock.json b/test/npm_compatibility/primary-not-latest-secondary-in-range/package-lock.json new file mode 100644 index 0000000..ec389fe --- /dev/null +++ b/test/npm_compatibility/primary-not-latest-secondary-in-range/package-lock.json @@ -0,0 +1,29 @@ +{ + "name": "test", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "test", + "dependencies": { + "wayfarer": "^6.6.2" + } + }, + "node_modules/wayfarer": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/wayfarer/-/wayfarer-6.6.2.tgz", + "integrity": "sha512-jwIufUN6EYfMeCJYBA8r0YytqHaSGACLtOddMeBtdq5gTo2F2XgK+t7eEXSPUBF9vm+hdI/iPOtSz1EJOd01dQ==", + "dependencies": { + "xtend": "^4.0.1" + } + }, + "node_modules/xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha512-iTwvhNBRetXWe81+VcIw5YeadVSWyze7uA7nVnpP13ulrpnJ3UfQm5ApGnrkmxDJFdrblRdZs0EvaTCIfei5oQ==", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/test/npm_compatibility/primary-not-latest-secondary-in-range/package.json b/test/npm_compatibility/primary-not-latest-secondary-in-range/package.json new file mode 100644 index 0000000..8ffd974 --- /dev/null +++ b/test/npm_compatibility/primary-not-latest-secondary-in-range/package.json @@ -0,0 +1,6 @@ +{ + "name": "test", + "dependencies": { + "wayfarer": "^6.6.2" + } +} diff --git a/test/npm_compatibility/primary-not-latest-secondary-out-range/package-lock.json b/test/npm_compatibility/primary-not-latest-secondary-out-range/package-lock.json new file mode 100644 index 0000000..946634f --- /dev/null +++ b/test/npm_compatibility/primary-not-latest-secondary-out-range/package-lock.json @@ -0,0 +1,29 @@ +{ + "name": "test", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "test", + "dependencies": { + "wayfarer": "^6.6.2" + } + }, + "node_modules/wayfarer": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/wayfarer/-/wayfarer-6.6.2.tgz", + "integrity": "sha512-jwIufUN6EYfMeCJYBA8r0YytqHaSGACLtOddMeBtdq5gTo2F2XgK+t7eEXSPUBF9vm+hdI/iPOtSz1EJOd01dQ==", + "dependencies": { + "xtend": "^4.0.1" + } + }, + "node_modules/xtend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", + "integrity": "sha512-sp/sT9OALMjRW1fKDlPeuSZlDQpkqReA0pyJukniWbTGoEKefHxhGJynE3PNhUMlcM8qWIjPwecwCw4LArS5Eg==", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/test/npm_compatibility/primary-not-latest-secondary-out-range/package.json b/test/npm_compatibility/primary-not-latest-secondary-out-range/package.json new file mode 100644 index 0000000..8ffd974 --- /dev/null +++ b/test/npm_compatibility/primary-not-latest-secondary-out-range/package.json @@ -0,0 +1,6 @@ +{ + "name": "test", + "dependencies": { + "wayfarer": "^6.6.2" + } +} diff --git a/test/npm_compatibility/primary-out-range/package-lock.json b/test/npm_compatibility/primary-out-range/package-lock.json new file mode 100644 index 0000000..0d6baa5 --- /dev/null +++ b/test/npm_compatibility/primary-out-range/package-lock.json @@ -0,0 +1,29 @@ +{ + "name": "test", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "test", + "dependencies": { + "wayfarer": "^6.6.2" + } + }, + "node_modules/wayfarer": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/wayfarer/-/wayfarer-6.6.2.tgz", + "integrity": "sha512-jwIufUN6EYfMeCJYBA8r0YytqHaSGACLtOddMeBtdq5gTo2F2XgK+t7eEXSPUBF9vm+hdI/iPOtSz1EJOd01dQ==", + "dependencies": { + "xtend": "^4.0.1" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/test/npm_compatibility/primary-out-range/package.json b/test/npm_compatibility/primary-out-range/package.json new file mode 100644 index 0000000..11302a8 --- /dev/null +++ b/test/npm_compatibility/primary-out-range/package.json @@ -0,0 +1,6 @@ +{ + "name": "test", + "dependencies": { + "wayfarer": "^6.6.3" + } +} diff --git a/test/npm_compatibility/secondary-in-range/package-lock.json b/test/npm_compatibility/secondary-in-range/package-lock.json new file mode 100644 index 0000000..ec389fe --- /dev/null +++ b/test/npm_compatibility/secondary-in-range/package-lock.json @@ -0,0 +1,29 @@ +{ + "name": "test", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "test", + "dependencies": { + "wayfarer": "^6.6.2" + } + }, + "node_modules/wayfarer": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/wayfarer/-/wayfarer-6.6.2.tgz", + "integrity": "sha512-jwIufUN6EYfMeCJYBA8r0YytqHaSGACLtOddMeBtdq5gTo2F2XgK+t7eEXSPUBF9vm+hdI/iPOtSz1EJOd01dQ==", + "dependencies": { + "xtend": "^4.0.1" + } + }, + "node_modules/xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha512-iTwvhNBRetXWe81+VcIw5YeadVSWyze7uA7nVnpP13ulrpnJ3UfQm5ApGnrkmxDJFdrblRdZs0EvaTCIfei5oQ==", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/test/npm_compatibility/secondary-in-range/package.json b/test/npm_compatibility/secondary-in-range/package.json new file mode 100644 index 0000000..fcff993 --- /dev/null +++ b/test/npm_compatibility/secondary-in-range/package.json @@ -0,0 +1,6 @@ +{ + "name": "test", + "dependencies": { + "wayfarer": "6.6.2" + } +} diff --git a/test/npm_compatibility/secondary-out-range/package-lock.json b/test/npm_compatibility/secondary-out-range/package-lock.json new file mode 100644 index 0000000..946634f --- /dev/null +++ b/test/npm_compatibility/secondary-out-range/package-lock.json @@ -0,0 +1,29 @@ +{ + "name": "test", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "test", + "dependencies": { + "wayfarer": "^6.6.2" + } + }, + "node_modules/wayfarer": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/wayfarer/-/wayfarer-6.6.2.tgz", + "integrity": "sha512-jwIufUN6EYfMeCJYBA8r0YytqHaSGACLtOddMeBtdq5gTo2F2XgK+t7eEXSPUBF9vm+hdI/iPOtSz1EJOd01dQ==", + "dependencies": { + "xtend": "^4.0.1" + } + }, + "node_modules/xtend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", + "integrity": "sha512-sp/sT9OALMjRW1fKDlPeuSZlDQpkqReA0pyJukniWbTGoEKefHxhGJynE3PNhUMlcM8qWIjPwecwCw4LArS5Eg==", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/test/npm_compatibility/secondary-out-range/package.json b/test/npm_compatibility/secondary-out-range/package.json new file mode 100644 index 0000000..fcff993 --- /dev/null +++ b/test/npm_compatibility/secondary-out-range/package.json @@ -0,0 +1,6 @@ +{ + "name": "test", + "dependencies": { + "wayfarer": "6.6.2" + } +} From 7504e5896fbae6ff44fae8c5d2ccf7ad989b6f90 Mon Sep 17 00:00:00 2001 From: Guy Paterson-Jones Date: Wed, 19 Apr 2023 14:09:58 +0200 Subject: [PATCH 02/11] feat: add tests covering the intentful install/update cases --- test/api/latest.test.js | 149 ------------------ .../README.md | 0 test/npm-compatibility/install-pkg.test.js | 49 ++++++ .../npm-behaviour.sh | 2 + .../primary-in-range/importmap.json | 17 ++ .../primary-in-range/package-lock.json | 0 .../primary-in-range/package.json | 0 .../importmap.json | 17 ++ .../package-lock.json | 0 .../package.json | 0 .../importmap.json | 17 ++ .../package-lock.json | 0 .../package.json | 0 .../primary-out-range/importmap.json | 17 ++ .../primary-out-range/package-lock.json | 0 .../primary-out-range/package.json | 0 .../secondary-in-range/importmap.json | 17 ++ .../secondary-in-range/package-lock.json | 0 .../secondary-in-range/package.json | 0 .../secondary-out-range/importmap.json | 17 ++ .../secondary-out-range/package-lock.json | 0 .../secondary-out-range/package.json | 0 test/npm-compatibility/update-pkg.test.js | 50 ++++++ 23 files changed, 203 insertions(+), 149 deletions(-) delete mode 100644 test/api/latest.test.js rename test/{npm_compatibility => npm-compatibility}/README.md (100%) create mode 100644 test/npm-compatibility/install-pkg.test.js rename test/{npm_compatibility => npm-compatibility}/npm-behaviour.sh (94%) create mode 100644 test/npm-compatibility/primary-in-range/importmap.json rename test/{npm_compatibility => npm-compatibility}/primary-in-range/package-lock.json (100%) rename test/{npm_compatibility => npm-compatibility}/primary-in-range/package.json (100%) create mode 100644 test/npm-compatibility/primary-not-latest-secondary-in-range/importmap.json rename test/{npm_compatibility => npm-compatibility}/primary-not-latest-secondary-in-range/package-lock.json (100%) rename test/{npm_compatibility => npm-compatibility}/primary-not-latest-secondary-in-range/package.json (100%) create mode 100644 test/npm-compatibility/primary-not-latest-secondary-out-range/importmap.json rename test/{npm_compatibility => npm-compatibility}/primary-not-latest-secondary-out-range/package-lock.json (100%) rename test/{npm_compatibility => npm-compatibility}/primary-not-latest-secondary-out-range/package.json (100%) create mode 100644 test/npm-compatibility/primary-out-range/importmap.json rename test/{npm_compatibility => npm-compatibility}/primary-out-range/package-lock.json (100%) rename test/{npm_compatibility => npm-compatibility}/primary-out-range/package.json (100%) create mode 100644 test/npm-compatibility/secondary-in-range/importmap.json rename test/{npm_compatibility => npm-compatibility}/secondary-in-range/package-lock.json (100%) rename test/{npm_compatibility => npm-compatibility}/secondary-in-range/package.json (100%) create mode 100644 test/npm-compatibility/secondary-out-range/importmap.json rename test/{npm_compatibility => npm-compatibility}/secondary-out-range/package-lock.json (100%) rename test/{npm_compatibility => npm-compatibility}/secondary-out-range/package.json (100%) create mode 100644 test/npm-compatibility/update-pkg.test.js diff --git a/test/api/latest.test.js b/test/api/latest.test.js deleted file mode 100644 index 6a5d9c4..0000000 --- a/test/api/latest.test.js +++ /dev/null @@ -1,149 +0,0 @@ -import { Generator, lookup, parseUrlPkg } from "@jspm/generator"; -import assert from "assert"; - -/** - * NPM has the following behaviour with respect to primary and secondary - * locks and the package.json constraints during installs: - * - * primary out-of-range: install latest compatible version - * primary in-range: install latest compatible version - * scndary out-of-range: install latest compatible version - * scndary in-range: use lock - * - * We should have the same behaviour by default, except for the case of a - * primary lock that isn't in the package.json, which we should keep as the - * user likely installed something manually. - */ - -const baseUrl = new URL("./local/latest/", import.meta.url); -function generator(inputMap = {}) { - return new Generator({ - baseUrl, - inputMap, - }); -} -async function getMapFor(pkgs, res={}) { - const g = new Generator({ resolutions: res }); - await Promise.all(pkgs.map(pkg => g.install(pkg))); - return g.getMap(); -} - -// Latest in-range version of "react", which is "16.13.1", with dependencies: -// "loose-envify": "^1.1.0" -// "object-assign": "^4.1.1" -// "prop-types": "^15.6.2" -const [latestReact, latestObjectAssign] = await (async () => { - const g = generator(); - await g.install("react"); - const map = g.getMap(); - return [ - (await parseUrlPkg(map.imports["react"])).pkg, - (await parseUrlPkg(map.scopes["https://ga.jspm.io/"]["object-assign"])).pkg, - (await parseUrlPkg(map.scopes["https://ga.jspm.io/"]["prop-types/checkPropTypes"])).pkg, - ]; -})(); - -// primary not in package.json -// shouldn't be removed or changed in any way -{ - const g = generator(await getMapFor(['lit@^2.0.0'])); - await g.install(); - - const map = g.getMap(); - assert.deepStrictEqual( - (await parseUrlPkg(map.imports["lit"])).pkg, - (await lookup("lit@^2.0.0")).resolved, - ); -} - -// primary out-of-range -// should be replaced with an in-range latest -{ - const g = generator(await getMapFor(["react@16.14.0"])); - await g.install(); - - const map = g.getMap(); - assert.deepStrictEqual( - (await parseUrlPkg(map.imports["react"])).pkg, - latestReact, - ); -} - -// primary in-range but not latest -// should be replaced with in-range latest -{ - const g = generator(await getMapFor(["react@16.13.0"])); - await g.install(); - - const map = g.getMap(); - assert.deepStrictEqual( - (await parseUrlPkg(map.imports["react"])).pkg, - latestReact, - ); -} - -// primary in-range but not latest, installed under alias -// should be replaced with in-range latest -{ - const g = generator(await getMapFor([{ - alias: "alias", - target: "react@16.13.0" - }])); - await g.install(); - - const map = g.getMap(); - assert.deepStrictEqual( - (await parseUrlPkg(map.imports["alias"])).pkg, - latestReact, - ); -} - -// secondary out-of-range -// should be replaced with in-range latest -{ - const g = generator(await getMapFor(["react@16.13.0"], { - "object-assign": "~4.0.0", - })); - await g.install(); - - const map = g.getMap(); - assert.deepStrictEqual( - (await parseUrlPkg(map.scopes["https://ga.jspm.io/"]["object-assign"])).pkg, - latestObjectAssign, - ); -} - -// secondary in-range -// should use the existing lock -{ - const imap = await getMapFor(["react@16.13.0"], { - "prop-types": "15.6.2", - }); - const propTypes = - (await parseUrlPkg(imap.scopes["https://ga.jspm.io/"]["prop-types/checkPropTypes"])).pkg; - const g = generator(imap); - await g.install(); - - const map = g.getMap(); - assert.deepStrictEqual( - (await parseUrlPkg(map.scopes["https://ga.jspm.io/"]["prop-types/checkPropTypes"])).pkg, - propTypes, - ); -} - -// primary custom mapping -// should not be touched -{ - const g = generator({ - imports: { - "react": "https://code.jquery.com/jquery-3.6.4.min.js", - }, - }); - await g.install(); - - const map = g.getMap(); - assert.deepStrictEqual( - map.imports.react, - "https://code.jquery.com/jquery-3.6.4.min.js", - ); -} diff --git a/test/npm_compatibility/README.md b/test/npm-compatibility/README.md similarity index 100% rename from test/npm_compatibility/README.md rename to test/npm-compatibility/README.md diff --git a/test/npm-compatibility/install-pkg.test.js b/test/npm-compatibility/install-pkg.test.js new file mode 100644 index 0000000..8d7c292 --- /dev/null +++ b/test/npm-compatibility/install-pkg.test.js @@ -0,0 +1,49 @@ +import { Generator } from "@jspm/generator"; +import assert from "assert"; +import fs from "fs/promises"; +import { fileURLToPath } from "url"; + +const expectedResults = { + "primary-in-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + }, + "primary-out-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + }, + "secondary-in-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.1/immutable.js", + }, + "secondary-out-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + }, + "primary-not-latest-secondary-in-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.1/immutable.js", + }, + "primary-not-latest-secondary-out-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + }, +} + +for (const [name, expected] of Object.entries(expectedResults)) { + const gen = new Generator({ + baseUrl: new URL(`./${name}/`, import.meta.url), + inputMap: JSON.parse(await fs.readFile( + fileURLToPath(new URL(`./${name}/importmap.json`, import.meta.url)) + )), + }); + + await gen.install("wayfarer"); + const map = JSON.stringify(gen.getMap(), null, 2); + for (const [pkg, resolution] of Object.entries(expected)) { + assert( + map.includes(resolution), + `${name}: ${pkg} should have resolution ${resolution}:\n${map}`, + ); + } +} diff --git a/test/npm_compatibility/npm-behaviour.sh b/test/npm-compatibility/npm-behaviour.sh similarity index 94% rename from test/npm_compatibility/npm-behaviour.sh rename to test/npm-compatibility/npm-behaviour.sh index 9fc663e..3809400 100755 --- a/test/npm_compatibility/npm-behaviour.sh +++ b/test/npm-compatibility/npm-behaviour.sh @@ -27,6 +27,7 @@ function test_command() { do cp "${test}/package.json" package.json.bkp cp "${test}/package-lock.json" package-lock.json.bkp + cp "${test}/importmap.json" importmap.json.bkp cd "${test}" primary="wayfarer" @@ -53,6 +54,7 @@ function test_command() { mkdir "${test}" mv package.json.bkp "${test}/package.json" mv package-lock.json.bkp "${test}/package-lock.json" + mv importmap.json.bkp "${test}/importmap.json" done echo } diff --git a/test/npm-compatibility/primary-in-range/importmap.json b/test/npm-compatibility/primary-in-range/importmap.json new file mode 100644 index 0000000..f6a0ed2 --- /dev/null +++ b/test/npm-compatibility/primary-in-range/importmap.json @@ -0,0 +1,17 @@ +{ + "env": [ + "browser", + "development", + "module" + ], + "imports": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js" + }, + "scopes": { + "https://ga.jspm.io/": { + "assert": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/assert.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + "xtend/mutable": "https://ga.jspm.io/npm:xtend@4.0.2/mutable.js" + } + } +} diff --git a/test/npm_compatibility/primary-in-range/package-lock.json b/test/npm-compatibility/primary-in-range/package-lock.json similarity index 100% rename from test/npm_compatibility/primary-in-range/package-lock.json rename to test/npm-compatibility/primary-in-range/package-lock.json diff --git a/test/npm_compatibility/primary-in-range/package.json b/test/npm-compatibility/primary-in-range/package.json similarity index 100% rename from test/npm_compatibility/primary-in-range/package.json rename to test/npm-compatibility/primary-in-range/package.json diff --git a/test/npm-compatibility/primary-not-latest-secondary-in-range/importmap.json b/test/npm-compatibility/primary-not-latest-secondary-in-range/importmap.json new file mode 100644 index 0000000..5e52549 --- /dev/null +++ b/test/npm-compatibility/primary-not-latest-secondary-in-range/importmap.json @@ -0,0 +1,17 @@ +{ + "env": [ + "browser", + "development", + "module" + ], + "imports": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js" + }, + "scopes": { + "https://ga.jspm.io/": { + "assert": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/assert.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.1/immutable.js", + "xtend/mutable": "https://ga.jspm.io/npm:xtend@4.0.1/mutable.js" + } + } +} \ No newline at end of file diff --git a/test/npm_compatibility/primary-not-latest-secondary-in-range/package-lock.json b/test/npm-compatibility/primary-not-latest-secondary-in-range/package-lock.json similarity index 100% rename from test/npm_compatibility/primary-not-latest-secondary-in-range/package-lock.json rename to test/npm-compatibility/primary-not-latest-secondary-in-range/package-lock.json diff --git a/test/npm_compatibility/primary-not-latest-secondary-in-range/package.json b/test/npm-compatibility/primary-not-latest-secondary-in-range/package.json similarity index 100% rename from test/npm_compatibility/primary-not-latest-secondary-in-range/package.json rename to test/npm-compatibility/primary-not-latest-secondary-in-range/package.json diff --git a/test/npm-compatibility/primary-not-latest-secondary-out-range/importmap.json b/test/npm-compatibility/primary-not-latest-secondary-out-range/importmap.json new file mode 100644 index 0000000..0c47967 --- /dev/null +++ b/test/npm-compatibility/primary-not-latest-secondary-out-range/importmap.json @@ -0,0 +1,17 @@ +{ + "env": [ + "browser", + "development", + "module" + ], + "imports": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js" + }, + "scopes": { + "https://ga.jspm.io/": { + "assert": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/assert.js", + "xtend": "https://ga.jspm.io/npm:xtend@3.0.0/index.js", + "xtend/mutable": "https://ga.jspm.io/npm:xtend@3.0.0/mutable.js" + } + } +} \ No newline at end of file diff --git a/test/npm_compatibility/primary-not-latest-secondary-out-range/package-lock.json b/test/npm-compatibility/primary-not-latest-secondary-out-range/package-lock.json similarity index 100% rename from test/npm_compatibility/primary-not-latest-secondary-out-range/package-lock.json rename to test/npm-compatibility/primary-not-latest-secondary-out-range/package-lock.json diff --git a/test/npm_compatibility/primary-not-latest-secondary-out-range/package.json b/test/npm-compatibility/primary-not-latest-secondary-out-range/package.json similarity index 100% rename from test/npm_compatibility/primary-not-latest-secondary-out-range/package.json rename to test/npm-compatibility/primary-not-latest-secondary-out-range/package.json diff --git a/test/npm-compatibility/primary-out-range/importmap.json b/test/npm-compatibility/primary-out-range/importmap.json new file mode 100644 index 0000000..52b2b0c --- /dev/null +++ b/test/npm-compatibility/primary-out-range/importmap.json @@ -0,0 +1,17 @@ +{ + "env": [ + "browser", + "development", + "module" + ], + "imports": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js" + }, + "scopes": { + "https://ga.jspm.io/": { + "assert": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/assert.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + "xtend/mutable": "https://ga.jspm.io/npm:xtend@4.0.2/mutable.js" + } + } +} \ No newline at end of file diff --git a/test/npm_compatibility/primary-out-range/package-lock.json b/test/npm-compatibility/primary-out-range/package-lock.json similarity index 100% rename from test/npm_compatibility/primary-out-range/package-lock.json rename to test/npm-compatibility/primary-out-range/package-lock.json diff --git a/test/npm_compatibility/primary-out-range/package.json b/test/npm-compatibility/primary-out-range/package.json similarity index 100% rename from test/npm_compatibility/primary-out-range/package.json rename to test/npm-compatibility/primary-out-range/package.json diff --git a/test/npm-compatibility/secondary-in-range/importmap.json b/test/npm-compatibility/secondary-in-range/importmap.json new file mode 100644 index 0000000..5e52549 --- /dev/null +++ b/test/npm-compatibility/secondary-in-range/importmap.json @@ -0,0 +1,17 @@ +{ + "env": [ + "browser", + "development", + "module" + ], + "imports": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js" + }, + "scopes": { + "https://ga.jspm.io/": { + "assert": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/assert.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.1/immutable.js", + "xtend/mutable": "https://ga.jspm.io/npm:xtend@4.0.1/mutable.js" + } + } +} \ No newline at end of file diff --git a/test/npm_compatibility/secondary-in-range/package-lock.json b/test/npm-compatibility/secondary-in-range/package-lock.json similarity index 100% rename from test/npm_compatibility/secondary-in-range/package-lock.json rename to test/npm-compatibility/secondary-in-range/package-lock.json diff --git a/test/npm_compatibility/secondary-in-range/package.json b/test/npm-compatibility/secondary-in-range/package.json similarity index 100% rename from test/npm_compatibility/secondary-in-range/package.json rename to test/npm-compatibility/secondary-in-range/package.json diff --git a/test/npm-compatibility/secondary-out-range/importmap.json b/test/npm-compatibility/secondary-out-range/importmap.json new file mode 100644 index 0000000..0c47967 --- /dev/null +++ b/test/npm-compatibility/secondary-out-range/importmap.json @@ -0,0 +1,17 @@ +{ + "env": [ + "browser", + "development", + "module" + ], + "imports": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js" + }, + "scopes": { + "https://ga.jspm.io/": { + "assert": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/assert.js", + "xtend": "https://ga.jspm.io/npm:xtend@3.0.0/index.js", + "xtend/mutable": "https://ga.jspm.io/npm:xtend@3.0.0/mutable.js" + } + } +} \ No newline at end of file diff --git a/test/npm_compatibility/secondary-out-range/package-lock.json b/test/npm-compatibility/secondary-out-range/package-lock.json similarity index 100% rename from test/npm_compatibility/secondary-out-range/package-lock.json rename to test/npm-compatibility/secondary-out-range/package-lock.json diff --git a/test/npm_compatibility/secondary-out-range/package.json b/test/npm-compatibility/secondary-out-range/package.json similarity index 100% rename from test/npm_compatibility/secondary-out-range/package.json rename to test/npm-compatibility/secondary-out-range/package.json diff --git a/test/npm-compatibility/update-pkg.test.js b/test/npm-compatibility/update-pkg.test.js new file mode 100644 index 0000000..086a761 --- /dev/null +++ b/test/npm-compatibility/update-pkg.test.js @@ -0,0 +1,50 @@ +import { Generator } from "@jspm/generator"; +import assert from "assert"; +import fs from "fs/promises"; +import { fileURLToPath } from "url"; + +const expectedResults = { + "primary-in-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + }, + "primary-out-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + }, + "secondary-in-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.1/immutable.js", + }, + "secondary-out-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + }, + "primary-not-latest-secondary-in-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.1/immutable.js", + }, + "primary-not-latest-secondary-out-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + }, +} + +for (const [name, expected] of Object.entries(expectedResults)) { + const gen = new Generator({ + baseUrl: new URL(`./${name}/`, import.meta.url), + inputMap: JSON.parse(await fs.readFile( + fileURLToPath(new URL(`./${name}/importmap.json`, import.meta.url)) + )), + }); + + await gen.update("wayfarer"); + const map = JSON.stringify(gen.getMap(), null, 2); + for (const [pkg, resolution] of Object.entries(expected)) { + assert( + map.includes(resolution), + `${name}: ${pkg} should have resolution ${resolution}:\n${map}`, + ); + } +} + From 0dcb54850394d4971eda3f53265093ec61a2ebb8 Mon Sep 17 00:00:00 2001 From: Guy Paterson-Jones Date: Wed, 19 Apr 2023 14:12:42 +0200 Subject: [PATCH 03/11] feat: add tests covering the argumentless install/update cases --- test/npm-compatibility/install.test.js | 50 +++++++++++++++++++++++++ test/npm-compatibility/update.test.js | 51 ++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 test/npm-compatibility/install.test.js create mode 100644 test/npm-compatibility/update.test.js diff --git a/test/npm-compatibility/install.test.js b/test/npm-compatibility/install.test.js new file mode 100644 index 0000000..13882c7 --- /dev/null +++ b/test/npm-compatibility/install.test.js @@ -0,0 +1,50 @@ +import { Generator } from "@jspm/generator"; +import assert from "assert"; +import fs from "fs/promises"; +import { fileURLToPath } from "url"; + +const expectedResults = { + "primary-in-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + }, + "primary-out-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + }, + "secondary-in-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.1/immutable.js", + }, + "secondary-out-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + }, + "primary-not-latest-secondary-in-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.1/immutable.js", + }, + "primary-not-latest-secondary-out-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + }, +} + +for (const [name, expected] of Object.entries(expectedResults)) { + const gen = new Generator({ + baseUrl: new URL(`./${name}/`, import.meta.url), + inputMap: JSON.parse(await fs.readFile( + fileURLToPath(new URL(`./${name}/importmap.json`, import.meta.url)) + )), + }); + + await gen.install("wayfarer"); + const map = JSON.stringify(gen.getMap(), null, 2); + for (const [pkg, resolution] of Object.entries(expected)) { + assert( + map.includes(resolution), + `${name}: ${pkg} should have resolution ${resolution}:\n${map}`, + ); + } +} + diff --git a/test/npm-compatibility/update.test.js b/test/npm-compatibility/update.test.js new file mode 100644 index 0000000..b15cbaf --- /dev/null +++ b/test/npm-compatibility/update.test.js @@ -0,0 +1,51 @@ +import { Generator } from "@jspm/generator"; +import assert from "assert"; +import fs from "fs/promises"; +import { fileURLToPath } from "url"; + +const expectedResults = { + "primary-in-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + }, + "primary-out-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + }, + "secondary-in-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + }, + "secondary-out-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + }, + "primary-not-latest-secondary-in-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + }, + "primary-not-latest-secondary-out-range": { + "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + }, +} + +for (const [name, expected] of Object.entries(expectedResults)) { + const gen = new Generator({ + baseUrl: new URL(`./${name}/`, import.meta.url), + inputMap: JSON.parse(await fs.readFile( + fileURLToPath(new URL(`./${name}/importmap.json`, import.meta.url)) + )), + }); + + await gen.install("wayfarer"); + const map = JSON.stringify(gen.getMap(), null, 2); + for (const [pkg, resolution] of Object.entries(expected)) { + assert( + map.includes(resolution), + `${name}: ${pkg} should have resolution ${resolution}:\n${map}`, + ); + } +} + + From 5bed868992269e79e040c5f2cdc4cc2d3431318f Mon Sep 17 00:00:00 2001 From: Guy Paterson-Jones Date: Wed, 19 Apr 2023 14:37:09 +0200 Subject: [PATCH 04/11] feat: thread through resolution options to link/update/install --- src/generator.ts | 69 +++++++++++++++++++++------------------- src/install/installer.ts | 25 ++++++++++----- src/trace/tracemap.ts | 6 ++-- 3 files changed, 57 insertions(+), 43 deletions(-) diff --git a/src/generator.ts b/src/generator.ts index 04da78a..24d8aea 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -38,7 +38,7 @@ import { getIntegrity } from "./common/integrity.js"; import { createLogger, Log, LogStream } from "./common/log.js"; import { Replacer } from "./common/str.js"; import { analyzeHtml } from "./html/analyze.js"; -import { InstallTarget } from "./install/installer.js"; +import { InstallTarget, type ResolutionOptions } from "./install/installer.js"; import { LockResolutions } from "./install/lock.js"; import { getDefaultProviderStrings, type Provider } from "./providers/index.js"; import * as nodemodules from "./providers/nodemodules.js"; @@ -48,7 +48,7 @@ import { Resolver } from "./trace/resolver.js"; export { analyzeHtml }; // Type exports for users: -export { Provider }; +export { Provider, ResolutionOptions }; /** * @interface GeneratorOptions. @@ -300,16 +300,17 @@ export interface GeneratorOptions { /** * When using a lockfile, do not modify any existing resolutions, and use * existing resolutions whenever possible for new locks. + * + * @deprecated Pass a ResolutionOptions object to Generator functions instead. */ - freeze?: boolean; // TODO: deprecate and move to install/link options + freeze?: boolean; /** - * When using a lockfile, force update touched resolutions to latest. + * When using a lockfile, force update all touched resolutions to latest. * - * @deprecated Defaults to 'true' for installs and updates, set to 'false' - * to enable old behaviour. + * @deprecated Pass a ResolutionOptions object to Generator functions instead. */ - latest?: boolean; // TODO: deprecate and move to install/link options + latest?: boolean; /** * Support tracing CommonJS dependencies locally. This is necessary if you @@ -517,7 +518,7 @@ export class Generator { this.map = new ImportMap({ mapUrl: this.mapUrl, rootUrl: this.rootUrl }); if (inputMap) this.addMappings(inputMap); - // Set global installation options: + // Set deprecated global resolution options for backwards compat: this.latest = latest; this.freeze = freeze; } @@ -576,12 +577,13 @@ export class Generator { */ async pin( specifier: string, - parentUrl?: string + parentUrl?: string, + opts?: ResolutionOptions ): Promise<{ staticDeps: string[]; dynamicDeps: string[]; }> { - return this.link(specifier, parentUrl); + return this.link(specifier, parentUrl, opts); } /** @@ -594,9 +596,10 @@ export class Generator { */ async traceInstall( specifier: string | string[], - parentUrl?: string + parentUrl?: string, + opts?: ResolutionOptions ): Promise<{ staticDeps: string[]; dynamicDeps: string[] }> { - return this.link(specifier, parentUrl); + return this.link(specifier, parentUrl, opts); } /** @@ -608,7 +611,8 @@ export class Generator { */ async link( specifier: string | string[], - parentUrl?: string + parentUrl?: string, + opts?: ResolutionOptions ): Promise<{ staticDeps: string[]; dynamicDeps: string[] }> { if (typeof specifier === "string") specifier = [specifier]; let error = false; @@ -622,9 +626,9 @@ export class Generator { specifier, { installOpts: { - freeze: this.freeze ?? true, // link defaults to freeze - latest: this.latest, - mode: "new-prefer-existing", + freeze: opts?.freeze ?? this.freeze ?? true, // link defaults to freeze + latest: opts?.latest ?? this.latest, + mode: opts?.mode ?? "new-prefer-existing", }, toplevel: true, }, @@ -1012,13 +1016,9 @@ export class Generator { * ``` */ async install( - install?: string | Install | (string | Install)[] + install?: string | Install | (string | Install)[], + opts?: ResolutionOptions ): Promise { - if (arguments.length > 1) - throw new JspmError( - "Install takes no arguments, a single install target, or a list of install targets." - ); - // If there are no arguments, then we reinstall all the top-level locks: if (!install) { await this.traceMap.processInputMap; @@ -1109,17 +1109,14 @@ export class Generator { `Adding primary constraint for ${alias}: ${JSON.stringify(target)}` ); - // Always install latest unless "freeze" is set or the user has set - // the deprecated "latest" flag explicitly: - const installLatest = this.latest ?? (this.freeze ? false : true); - await this.traceMap.add(alias, target, this.freeze, installLatest); + await this.traceMap.add(alias, target, this.freeze, this.latest); await this.traceMap.visit( alias + subpath.slice(1), { installOpts: { - freeze: this.freeze, - latest: installLatest, - mode: "new", + freeze: opts?.freeze ?? this.freeze, + latest: opts?.latest ?? this.latest, + mode: opts?.mode ?? "new", }, toplevel: true, }, @@ -1159,13 +1156,21 @@ export class Generator { } } - async update(pkgNames?: string | string[]) { + async update(pkgNames?: string | string[], opts?: ResolutionOptions) { if (typeof pkgNames === "string") pkgNames = [pkgNames]; if (this.installCnt++ === 0) this.traceMap.startInstall(); await this.traceMap.processInputMap; + + // To match the behaviour of `npm update`, an argumentless update should + // reinstall all of the primary locks to latest: const primaryResolutions = this.traceMap.installer.installs.primary; const primaryConstraints = this.traceMap.installer.constraints.primary; - if (!pkgNames) pkgNames = Object.keys(primaryResolutions); + if (!pkgNames) { + pkgNames = Object.keys(primaryResolutions); + opts ??= {}; + opts.latest ??= true; + } + const installs: Install[] = []; for (const name of pkgNames) { const resolution = primaryResolutions[name]; @@ -1210,7 +1215,7 @@ export class Generator { } } - await this.install(installs); + await this.install(installs, opts); if (--this.installCnt === 0) { const { map, staticDeps, dynamicDeps } = await this.traceMap.finishInstall(); diff --git a/src/install/installer.ts b/src/install/installer.ts index eeab60f..fbca917 100644 --- a/src/install/installer.ts +++ b/src/install/installer.ts @@ -24,12 +24,21 @@ export interface PackageProvider { export type ResolutionMode = "new" | "new-prefer-existing" | "existing"; /** - * InstallOptions configures the interaction between the Installer and the - * existing lockfile during an install operation. + * ResolutionOptions configures the interaction between version resolutions + * and the existing lockfile during operations. */ -export interface InstallOptions { - mode: ResolutionMode; +export interface ResolutionOptions { + mode?: ResolutionMode; + + /** + * Use existing locks whenever possible for all touched resolutions. + */ freeze?: boolean; + + /** + * Force update all touched resolutions to the latest version compatible + * with the parent's package.json. + */ latest?: boolean; } @@ -168,7 +177,7 @@ export class Installer { * @param {string} pkgName Name of the package being installed. * @param {InstallTarget} target The installation target being installed. * @param {`./${string}` | '.'} traceSubpath - * @param {InstallOptions} opts Specifies how to interact with existing installs. + * @param {ResolutionOptions} opts Specifies how to interact with existing installs. * @param {`${string}/` | null} pkgScope URL of the package scope in which this install is occurring, null if it's a top-level install. * @param {string} parentUrl URL of the parent for this install. * @returns {Promise} @@ -177,7 +186,7 @@ export class Installer { pkgName: string, { pkgTarget, installSubpath }: InstallTarget, traceSubpath: `./${string}` | ".", - opts: InstallOptions, + opts: ResolutionOptions, pkgScope: `${string}/` | null, parentUrl: string ): Promise { @@ -326,7 +335,7 @@ export class Installer { * Installs the given package specifier. * * @param {string} pkgName The package specifier being installed. - * @param {InstallOptions} opts Specifies how to interact with existing installs. + * @param {ResolutionOptions} opts Specifies how to interact with existing installs. * @param {`${string}/` | null} pkgScope URL of the package scope in which this install is occurring, null if it's a top-level install. * @param {`./${string}` | '.'} traceSubpath * @param {string} parentUrl URL of the parent for this install. @@ -334,7 +343,7 @@ export class Installer { */ async install( pkgName: string, - opts: InstallOptions, + opts: ResolutionOptions, pkgScope: `${string}/` | null = null, traceSubpath: `./${string}` | ".", parentUrl: string = this.installBaseUrl diff --git a/src/trace/tracemap.ts b/src/trace/tracemap.ts index d27f684..cc030e3 100644 --- a/src/trace/tracemap.ts +++ b/src/trace/tracemap.ts @@ -2,7 +2,7 @@ import { type InstallerOptions, InstallTarget, ResolutionMode, - InstallOptions, + ResolutionOptions, } from "../install/installer.js"; import { importedFrom, @@ -91,7 +91,7 @@ interface TraceEntry { interface VisitOpts { static?: boolean; toplevel: boolean; - installOpts: InstallOptions; + installOpts: ResolutionOptions; visitor?: ( specifier: string, parentUrl: string, @@ -398,7 +398,7 @@ export default class TraceMap { async resolve( specifier: string, parentUrl: string, - installOpts: InstallOptions, + installOpts: ResolutionOptions, toplevel: boolean ): Promise { const cjsEnv = this.tracedUrls[parentUrl]?.wasCjs; From b3b672e7009efc34d94728e74b045954d5aa6b61 Mon Sep 17 00:00:00 2001 From: Guy Paterson-Jones Date: Wed, 19 Apr 2023 16:38:08 +0200 Subject: [PATCH 05/11] feat: completely align with npm for updates/installs --- src/generator.ts | 43 +++++++++++++++++++------- src/install/installer.ts | 31 ++++++++++++++----- src/trace/tracemap.ts | 5 ++- test/npm-compatibility/install.test.js | 2 +- test/npm-compatibility/update.test.js | 2 +- 5 files changed, 58 insertions(+), 25 deletions(-) diff --git a/src/generator.ts b/src/generator.ts index 24d8aea..933e090 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -627,7 +627,8 @@ export class Generator { { installOpts: { freeze: opts?.freeze ?? this.freeze ?? true, // link defaults to freeze - latest: opts?.latest ?? this.latest, + latestPrimaries: opts?.latestPrimaries ?? this.latest, + latestSecondaries: opts?.latestSecondaries ?? this.latest, mode: opts?.mode ?? "new-prefer-existing", }, toplevel: true, @@ -1022,6 +1023,14 @@ export class Generator { // If there are no arguments, then we reinstall all the top-level locks: if (!install) { await this.traceMap.processInputMap; + + // To match the behaviour of an argumentless `npm install`, we use + // existing resolutions for everything unless it's out-of-range: + opts ??= {}; + opts.latestPrimaries ??= false; + opts.latestSecondaries ??= false; + opts.mode ??= "new-prefer-existing"; + return this.install( Object.entries(this.traceMap.installer.installs.primary).map( ([alias, target]) => { @@ -1046,7 +1055,8 @@ export class Generator { subpath: target.installSubpath ?? undefined, } as Install; } - ) + ), + opts ); } @@ -1060,7 +1070,7 @@ export class Generator { } return await Promise.all( - install.map((install) => this.install(install)) + install.map((install) => this.install(install, opts)) ).then((installs) => installs.find((i) => i)); } @@ -1077,11 +1087,14 @@ export class Generator { }); return await Promise.all( install.subpaths.map((subpath) => - this.install({ - target: (install as Install).target, - alias: (install as Install).alias, - subpath, - }) + this.install( + { + target: (install as Install).target, + alias: (install as Install).alias, + subpath, + }, + opts + ) ) ).then((installs) => installs.find((i) => i)); } @@ -1109,13 +1122,17 @@ export class Generator { `Adding primary constraint for ${alias}: ${JSON.stringify(target)}` ); - await this.traceMap.add(alias, target, this.freeze, this.latest); + await this.traceMap.add(alias, target, opts); await this.traceMap.visit( alias + subpath.slice(1), { installOpts: { freeze: opts?.freeze ?? this.freeze, - latest: opts?.latest ?? this.latest, + latestPrimaries: + opts?.latestPrimaries ?? + this.latest ?? + (this.freeze ? false : true), + latestSecondaries: opts?.latestSecondaries ?? this.latest ?? false, mode: opts?.mode ?? "new", }, toplevel: true, @@ -1162,13 +1179,15 @@ export class Generator { await this.traceMap.processInputMap; // To match the behaviour of `npm update`, an argumentless update should - // reinstall all of the primary locks to latest: + // reinstall all primary locks and their secondary dependencies to the + // latest compatible versions: const primaryResolutions = this.traceMap.installer.installs.primary; const primaryConstraints = this.traceMap.installer.constraints.primary; if (!pkgNames) { pkgNames = Object.keys(primaryResolutions); opts ??= {}; - opts.latest ??= true; + opts.latestPrimaries ??= true; + opts.latestSecondaries ??= true; } const installs: Install[] = []; diff --git a/src/install/installer.ts b/src/install/installer.ts index fbca917..7267f14 100644 --- a/src/install/installer.ts +++ b/src/install/installer.ts @@ -36,10 +36,16 @@ export interface ResolutionOptions { freeze?: boolean; /** - * Force update all touched resolutions to the latest version compatible - * with the parent's package.json. + * Force update all touched primary resolutions to the latest version + * compatible with the parent's package.json. */ - latest?: boolean; + latestPrimaries?: boolean; + + /** + * Force update all touched secondary resolutions to the latest version + * compatible with the parent's package.json. + */ + latestSecondaries?: boolean; } export type InstallTarget = { @@ -196,6 +202,10 @@ export class Installer { "ERR_NOT_INSTALLED" ); + const useLatest = + (pkgScope === null && opts.latestPrimaries) || + (pkgScope !== null && opts.latestSecondaries); + // Resolutions are always authoritative, and override the existing target: if (this.resolutions[pkgName]) { const resolutionTarget = newPackageTarget( @@ -245,8 +255,8 @@ export class Installer { // If this is a secondary install or we're in an existing-lock install // mode, then we make an attempt to find a compatible existing lock: if ( - (opts.freeze || opts.mode.includes("existing") || pkgScope !== null) && - !opts.latest + (opts.freeze || opts.mode?.includes("existing") || pkgScope !== null) && + !useLatest ) { const pkg = await this.getBestExistingMatch(pkgTarget); if (pkg) { @@ -284,7 +294,7 @@ export class Installer { // constraint and we can't, then we fallback to the best existing lock: if ( !opts.freeze && - !opts.latest && + !useLatest && pkgScope && latestPkg && !this.tryUpgradeAllTo(latestPkg, pkgUrl, installed) @@ -353,7 +363,7 @@ export class Installer { `installing ${pkgName} from ${parentUrl} in scope ${pkgScope}` ); if (!this.installing) throwInternalError("Not installing"); - if (opts.latest && opts.freeze) { + if ((opts.latestPrimaries || opts.latestSecondaries) && opts.freeze) { throw new JspmError( "Cannot enable 'freeze' and 'latest' install options simultaneously." ); @@ -364,6 +374,9 @@ export class Installer { // a secondary dependency: // TODO: wire this concept through the whole codebase. const isTopLevel = !pkgScope || pkgScope == this.installBaseUrl; + const useLatest = + (isTopLevel && opts.latestPrimaries) || + (!isTopLevel && opts.latestSecondaries); if (this.resolutions[pkgName]) return this.installTarget( @@ -402,13 +415,14 @@ export class Installer { ); // Find any existing locks in the current package scope, making sure - // secondaries are always in-range for their parent scope pjsons: + // locks are always in-range for their parent scope pjsons: const existingResolution = getResolution( this.installs, pkgName, isTopLevel ? null : pkgScope ); if ( + !useLatest && existingResolution && (isTopLevel || opts.freeze || @@ -438,6 +452,7 @@ export class Installer { ); if ( + !useLatest && flattenedResolution && (opts.freeze || (await this.inRange( diff --git a/src/trace/tracemap.ts b/src/trace/tracemap.ts index cc030e3..0ad0882 100644 --- a/src/trace/tracemap.ts +++ b/src/trace/tracemap.ts @@ -379,14 +379,13 @@ export default class TraceMap { async add( name: string, target: InstallTarget, - freeze?: boolean, - latest?: boolean + opts?: ResolutionOptions ): Promise { await this.installer.installTarget( name, target, null, - { mode: "new", freeze, latest }, + opts || {}, null, this.mapUrl.href ); diff --git a/test/npm-compatibility/install.test.js b/test/npm-compatibility/install.test.js index 13882c7..c0d7561 100644 --- a/test/npm-compatibility/install.test.js +++ b/test/npm-compatibility/install.test.js @@ -38,7 +38,7 @@ for (const [name, expected] of Object.entries(expectedResults)) { )), }); - await gen.install("wayfarer"); + await gen.install(); const map = JSON.stringify(gen.getMap(), null, 2); for (const [pkg, resolution] of Object.entries(expected)) { assert( diff --git a/test/npm-compatibility/update.test.js b/test/npm-compatibility/update.test.js index b15cbaf..ae39533 100644 --- a/test/npm-compatibility/update.test.js +++ b/test/npm-compatibility/update.test.js @@ -38,7 +38,7 @@ for (const [name, expected] of Object.entries(expectedResults)) { )), }); - await gen.install("wayfarer"); + await gen.update(); const map = JSON.stringify(gen.getMap(), null, 2); for (const [pkg, resolution] of Object.entries(expected)) { assert( From 0a59554db9b2e90d6e63634faa403b21c14a2d36 Mon Sep 17 00:00:00 2001 From: Guy Paterson-Jones Date: Thu, 20 Apr 2023 13:41:24 +0200 Subject: [PATCH 06/11] fix: resolve bugs picked up by deno tests, disable babel test for now --- src/install/installer.ts | 14 +++++++++----- test/deno/babel.test.js | 36 ++++++++++++++++++++---------------- test/providers/deno.test.js | 14 +++++++++----- 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/install/installer.ts b/src/install/installer.ts index 7267f14..753e950 100644 --- a/src/install/installer.ts +++ b/src/install/installer.ts @@ -374,9 +374,6 @@ export class Installer { // a secondary dependency: // TODO: wire this concept through the whole codebase. const isTopLevel = !pkgScope || pkgScope == this.installBaseUrl; - const useLatest = - (isTopLevel && opts.latestPrimaries) || - (!isTopLevel && opts.latestSecondaries); if (this.resolutions[pkgName]) return this.installTarget( @@ -414,6 +411,13 @@ export class Installer { pkgName ); + // The latestPrimaries and latestSecondaries options are used to always + // take latest version resolutions from the package.json: + const useLatestPjsonTarget = + !!pjsonTarget && + ((isTopLevel && opts.latestPrimaries) || + (!isTopLevel && opts.latestSecondaries)); + // Find any existing locks in the current package scope, making sure // locks are always in-range for their parent scope pjsons: const existingResolution = getResolution( @@ -422,7 +426,7 @@ export class Installer { isTopLevel ? null : pkgScope ); if ( - !useLatest && + !useLatestPjsonTarget && existingResolution && (isTopLevel || opts.freeze || @@ -452,7 +456,7 @@ export class Installer { ); if ( - !useLatest && + !useLatestPjsonTarget && flattenedResolution && (opts.freeze || (await this.inRange( diff --git a/test/deno/babel.test.js b/test/deno/babel.test.js index 2a7a618..2494708 100644 --- a/test/deno/babel.test.js +++ b/test/deno/babel.test.js @@ -1,26 +1,30 @@ import { Generator } from "@jspm/generator"; import { denoExec } from "#test/deno"; +// Babel doesn't seem to support deno very well, see: +// https://github.com/babel/babel/issues/11543 +// +// At the moment trying to import babel in deno produces the following error: +// error: Uncaught ReferenceError: Cannot access 'default' before initialization + const generator = new Generator({ env: ["node", "deno"], - resolutions: { - // hack as deno bombs on circular imports in 7.21.0 (latest) - "@babel/helper-create-class-features-plugin": "7.20.0" - }, }); await generator.install("@babel/core@7.15.0"); await generator.install("assert"); -const map = generator.getMap(); - -await denoExec( - map, - ` - import babel from '@babel/core'; - import assert from 'assert'; - - const { code } = babel.transform('var p = 5'); - assert.strictEqual(code, 'var p = 5;'); -` -); +// TODO: re-enable if they support deno at some point +// +// const map = generator.getMap(); +// +// await denoExec( +// map, +// ` +// import babel from '@babel/core'; +// import assert from 'assert'; +// +// const { code } = babel.transform('var p = 5'); +// assert.strictEqual(code, 'var p = 5;'); +// ` +// ); diff --git a/test/providers/deno.test.js b/test/providers/deno.test.js index 89bf825..e1ea966 100644 --- a/test/providers/deno.test.js +++ b/test/providers/deno.test.js @@ -129,16 +129,18 @@ const oakVersion = (await lookup("denoland:oak")).resolved.version; }); await generator.install("deno:path"); - const json = generator.getMap(); + // Install shouldn't have touched the existing lock: assert.strictEqual( json.imports["fs"], `https://deno.land/std@0.148.0/fs/mod.ts` ); + + // But the newly installed path should be on latest: assert.strictEqual( json.imports["path"], - `https://deno.land/std@0.148.0/path/mod.ts` + `https://deno.land/std@${denoStdVersion}/path/mod.ts` ); } @@ -191,21 +193,23 @@ const oakVersion = (await lookup("denoland:oak")).resolved.version; }); await generator.install("deno:testing/asserts"); - await generator.install("deno:async/abortable.ts"); const json = generator.getMap(); + // Existing version should be the same: assert.strictEqual( json.imports["fs"], `https://deno.land/std@0.148.0/fs/mod.ts` ); + + // Installed versions should be on latest: assert.strictEqual( json.imports["async/abortable"], - `https://deno.land/std@0.148.0/async/abortable.ts` + `https://deno.land/std@${denoStdVersion}/async/abortable.ts` ); assert.strictEqual( json.imports["testing/asserts"], - `https://deno.land/std@0.148.0/testing/asserts.ts` + `https://deno.land/std@${denoStdVersion}/testing/asserts.ts` ); } From f5347e8c710aa63c14baabbc1a4df8606ebf1510 Mon Sep 17 00:00:00 2001 From: Guy Paterson-Jones Date: Thu, 20 Apr 2023 13:51:38 +0200 Subject: [PATCH 07/11] fix: fix bug picked up by freeze test --- src/generator.ts | 21 +++++++++++---------- test/providers/deno.test.js | 12 +++--------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/generator.ts b/src/generator.ts index 933e090..44c757e 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -1122,19 +1122,20 @@ export class Generator { `Adding primary constraint for ${alias}: ${JSON.stringify(target)}` ); - await this.traceMap.add(alias, target, opts); + // Apply the default resolution options: + const defaultOpts: ResolutionOptions = { + freeze: opts?.freeze ?? this.freeze, + latestPrimaries: + opts?.latestPrimaries ?? this.latest ?? (this.freeze ? false : true), + latestSecondaries: opts?.latestSecondaries ?? this.latest ?? false, + mode: opts?.mode ?? "new", + }; + + await this.traceMap.add(alias, target, defaultOpts); await this.traceMap.visit( alias + subpath.slice(1), { - installOpts: { - freeze: opts?.freeze ?? this.freeze, - latestPrimaries: - opts?.latestPrimaries ?? - this.latest ?? - (this.freeze ? false : true), - latestSecondaries: opts?.latestSecondaries ?? this.latest ?? false, - mode: opts?.mode ?? "new", - }, + installOpts: defaultOpts, toplevel: true, }, this.mapUrl.href diff --git a/test/providers/deno.test.js b/test/providers/deno.test.js index e1ea966..84419ba 100644 --- a/test/providers/deno.test.js +++ b/test/providers/deno.test.js @@ -131,16 +131,13 @@ const oakVersion = (await lookup("denoland:oak")).resolved.version; await generator.install("deno:path"); const json = generator.getMap(); - // Install shouldn't have touched the existing lock: assert.strictEqual( json.imports["fs"], `https://deno.land/std@0.148.0/fs/mod.ts` ); - - // But the newly installed path should be on latest: assert.strictEqual( json.imports["path"], - `https://deno.land/std@${denoStdVersion}/path/mod.ts` + `https://deno.land/std@0.148.0/path/mod.ts` ); } @@ -197,19 +194,16 @@ const oakVersion = (await lookup("denoland:oak")).resolved.version; const json = generator.getMap(); - // Existing version should be the same: assert.strictEqual( json.imports["fs"], `https://deno.land/std@0.148.0/fs/mod.ts` ); - - // Installed versions should be on latest: assert.strictEqual( json.imports["async/abortable"], - `https://deno.land/std@${denoStdVersion}/async/abortable.ts` + `https://deno.land/std@0.148.0/async/abortable.ts` ); assert.strictEqual( json.imports["testing/asserts"], - `https://deno.land/std@${denoStdVersion}/testing/asserts.ts` + `https://deno.land/std@0.148.0/testing/asserts.ts` ); } From a0aa283edea2b5dda3766c5c49ad4091f692d171 Mon Sep 17 00:00:00 2001 From: Guy Paterson-Jones Date: Thu, 20 Apr 2023 14:17:00 +0200 Subject: [PATCH 08/11] fix: pin back assert version for deno esm.sh test --- test/deno/esmsh.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/deno/esmsh.test.js b/test/deno/esmsh.test.js index 80df6fe..5cf2151 100644 --- a/test/deno/esmsh.test.js +++ b/test/deno/esmsh.test.js @@ -8,7 +8,8 @@ const generator = new Generator({ }); // Install the NPM assert shim and use it to test itself! -await generator.install('npm:assert@2.0.0'); +// The esm.sh deno build for assert@2.0.0 is broken, so we use an old version. +await generator.install('npm:assert@1.5.0'); await denoExec( generator.getMap(), ` From 54a911b4a5f74e744f08092f4e21492f91966106 Mon Sep 17 00:00:00 2001 From: Guy Paterson-Jones Date: Thu, 20 Apr 2023 17:14:02 +0200 Subject: [PATCH 09/11] fix: remove single-argument install test --- test/api/install.test.js | 8 -------- test/test.html | 16 ++++++++-------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/test/api/install.test.js b/test/api/install.test.js index cd31f04..c29e29b 100644 --- a/test/api/install.test.js +++ b/test/api/install.test.js @@ -17,14 +17,6 @@ const generator = new Generator({ freeze: true, // lock versions }); -// Install with too many arguments should throw: -try { - await generator.install("too", "many"); - assert(false); -} catch { - /* expected to throw */ -} - // Install with no arguments should install all top-level pins. await generator.install(); let json = generator.getMap(); diff --git a/test/test.html b/test/test.html index db847f3..6975e3c 100644 --- a/test/test.html +++ b/test/test.html @@ -27,7 +27,7 @@ "#lib/config/resolve-targets.js": "https://ga.jspm.io/npm:@babel/core@7.21.4/lib/config/resolve-targets-browser.js", "#lib/transform-file.js": "https://ga.jspm.io/npm:@babel/core@7.21.4/lib/transform-file-browser.js", "#node.js": "https://ga.jspm.io/npm:browserslist@4.21.5/browser.js", - "@ampproject/remapping": "https://ga.jspm.io/npm:@ampproject/remapping@2.2.0/dist/remapping.mjs", + "@ampproject/remapping": "https://ga.jspm.io/npm:@ampproject/remapping@2.2.1/dist/remapping.umd.js", "@babel/code-frame": "https://ga.jspm.io/npm:@babel/code-frame@7.21.4/lib/index.js", "@babel/compat-data/native-modules": "https://ga.jspm.io/npm:@babel/compat-data@7.21.4/native-modules.js", "@babel/compat-data/plugins": "https://ga.jspm.io/npm:@babel/compat-data@7.21.4/plugins.js", @@ -61,21 +61,21 @@ "@babel/template": "https://ga.jspm.io/npm:@babel/template@7.20.7/lib/index.js", "@babel/traverse": "https://ga.jspm.io/npm:@babel/traverse@7.21.4/lib/index.js", "@babel/types": "https://ga.jspm.io/npm:@babel/types@7.21.4/lib/index.js", - "@jridgewell/gen-mapping": "https://ga.jspm.io/npm:@jridgewell/gen-mapping@0.1.1/dist/gen-mapping.umd.js", + "@jridgewell/gen-mapping": "https://ga.jspm.io/npm:@jridgewell/gen-mapping@0.3.3/dist/gen-mapping.umd.js", "@jridgewell/resolve-uri": "https://ga.jspm.io/npm:@jridgewell/resolve-uri@3.1.0/dist/resolve-uri.umd.js", "@jridgewell/set-array": "https://ga.jspm.io/npm:@jridgewell/set-array@1.1.2/dist/set-array.umd.js", - "@jridgewell/sourcemap-codec": "https://ga.jspm.io/npm:@jridgewell/sourcemap-codec@1.4.14/dist/sourcemap-codec.umd.js", - "@jridgewell/trace-mapping": "https://ga.jspm.io/npm:@jridgewell/trace-mapping@0.3.17/dist/trace-mapping.umd.js", + "@jridgewell/sourcemap-codec": "https://ga.jspm.io/npm:@jridgewell/sourcemap-codec@1.4.15/dist/sourcemap-codec.umd.js", + "@jridgewell/trace-mapping": "https://ga.jspm.io/npm:@jridgewell/trace-mapping@0.3.18/dist/trace-mapping.umd.js", "ansi-styles": "https://ga.jspm.io/npm:ansi-styles@3.2.1/index.js", "browserslist": "https://ga.jspm.io/npm:browserslist@4.21.5/index.js", "buffer": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/buffer.js", - "caniuse-lite/dist/unpacker/agents": "https://ga.jspm.io/npm:caniuse-lite@1.0.30001473/dist/unpacker/agents.js", + "caniuse-lite/dist/unpacker/agents": "https://ga.jspm.io/npm:caniuse-lite@1.0.30001480/dist/unpacker/agents.js", "chalk": "https://ga.jspm.io/npm:chalk@2.4.2/index.js", "color-convert": "https://ga.jspm.io/npm:color-convert@1.9.3/index.js", "color-name": "https://ga.jspm.io/npm:color-name@1.1.3/index.js", "convert-source-map": "https://ga.jspm.io/npm:convert-source-map@1.9.0/index.js", "debug": "https://ga.jspm.io/npm:debug@4.3.4/src/browser.js", - "electron-to-chromium/versions": "https://ga.jspm.io/npm:electron-to-chromium@1.4.348/versions.js", + "electron-to-chromium/versions": "https://ga.jspm.io/npm:electron-to-chromium@1.4.368/versions.js", "escape-string-regexp": "https://ga.jspm.io/npm:escape-string-regexp@1.0.5/index.js", "fs": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/fs.js", "gensync": "https://ga.jspm.io/npm:gensync@1.0.0-beta.2/index.js", @@ -93,8 +93,8 @@ "to-fast-properties": "https://ga.jspm.io/npm:to-fast-properties@2.0.0/index.js", "yallist": "https://ga.jspm.io/npm:yallist@3.1.1/yallist.js" }, - "https://ga.jspm.io/npm:@babel/generator@7.21.4/": { - "@jridgewell/gen-mapping": "https://ga.jspm.io/npm:@jridgewell/gen-mapping@0.3.2/dist/gen-mapping.umd.js" + "https://ga.jspm.io/npm:@jridgewell/trace-mapping@0.3.18/": { + "@jridgewell/sourcemap-codec": "https://ga.jspm.io/npm:@jridgewell/sourcemap-codec@1.4.14/dist/sourcemap-codec.umd.js" } } } From 02924eb0a2d8aaab256cc39605c584d1e4715ea0 Mon Sep 17 00:00:00 2001 From: Guy Paterson-Jones Date: Thu, 20 Apr 2023 17:24:03 +0200 Subject: [PATCH 10/11] fix: make tests browser compatible, prettier tests --- chompfile.toml | 4 +- test/api/flat-dedupes.test.js | 16 +- test/api/freeze.test.js | 183 +++++++++++---------- test/api/install.test.js | 8 +- test/api/node.test.js | 12 +- test/api/providerswitch.test.js | 2 +- test/api/urls.test.js | 20 +-- test/deno/babel.test.js | 6 +- test/deno/esmsh.test.js | 2 +- test/npm-compatibility/install-pkg.test.js | 48 +++--- test/npm-compatibility/install.test.js | 49 +++--- test/npm-compatibility/update-pkg.test.js | 49 +++--- test/npm-compatibility/update.test.js | 50 +++--- test/providers/deno.test.js | 6 +- test/providers/esmsh.test.js | 5 +- test/resolve/unused-cjs.test.js | 4 +- 16 files changed, 247 insertions(+), 217 deletions(-) diff --git a/chompfile.toml b/chompfile.toml index 72f4acf..f715a3d 100644 --- a/chompfile.toml +++ b/chompfile.toml @@ -133,10 +133,10 @@ run = ''' [[task]] name = 'prettier' template = 'prettier' -deps = ['src/**/*.ts', 'test/**/*.ts'] +deps = ['src/**/*.ts', 'test/**/*.js'] [task.template-options] ignore-path = '.prettierignore' -files = 'src/**/*.ts test/**/*.ts' +files = 'src/**/*.ts test/**/*.js' loglevel = 'warn' [[task]] diff --git a/test/api/flat-dedupes.test.js b/test/api/flat-dedupes.test.js index c6bbfb7..c9b2f15 100644 --- a/test/api/flat-dedupes.test.js +++ b/test/api/flat-dedupes.test.js @@ -22,49 +22,49 @@ // ethers: "5.7.2", // workaround for incorrect version pin in @wagmi/core // }, // }; -// +// // const generatorOne = new Generator({ // ...BASE_CONFIG, // }); // await generatorOne.install("@react-three/fiber"); // const mapOne = generatorOne.getMap(); -// +// // strictEqual( // mapOne.scopes["https://ga.jspm.io/"].zustand, // "https://ga.jspm.io/npm:zustand@3.7.1/esm/index.js" // ); -// +// // const generatorTwo = new Generator({ // ...BASE_CONFIG, // inputMap: mapOne, // }); // await generatorTwo.install("wagmi"); // const mapTwo = generatorTwo.getMap(); -// +// // strictEqual( // mapTwo.scopes["https://ga.jspm.io/"].zustand, // "https://ga.jspm.io/npm:zustand@3.7.1/esm/index.js" // ); -// +// // const generatorThree = new Generator({ // ...BASE_CONFIG, // inputMap: mapTwo, // }); // await generatorThree.install("connectkit"); // const mapThree = generatorThree.getMap(); -// +// // strictEqual( // mapThree.scopes["https://ga.jspm.io/"].zustand, // "https://ga.jspm.io/npm:zustand@3.7.1/esm/index.js" // ); -// +// // const generatorFour = new Generator({ // ...BASE_CONFIG, // inputMap: mapThree, // }); // await generatorFour.reinstall(); // const mapFour = generatorFour.getMap(); -// +// // strictEqual( // mapFour.scopes["https://ga.jspm.io/"].zustand, // "https://ga.jspm.io/npm:zustand@3.7.1/esm/index.js" diff --git a/test/api/freeze.test.js b/test/api/freeze.test.js index 614d168..eee482f 100644 --- a/test/api/freeze.test.js +++ b/test/api/freeze.test.js @@ -1,5 +1,5 @@ import { Generator, lookup, parseUrlPkg } from "@jspm/generator"; -import assert from 'assert'; +import assert from "assert"; /** * This test pins down the semantics of the "freeze" option on the generator. @@ -30,8 +30,8 @@ async function checkScenario(scenario) { // install dependencies: try { - await Promise.all(scenario.install.map(pkg => generator.install(pkg))); - } catch(err) { + await Promise.all(scenario.install.map((pkg) => generator.install(pkg))); + } catch (err) { if (!scenario.expect) return; // expected to throw, all good throw err; } @@ -43,109 +43,118 @@ async function checkScenario(scenario) { mdls.push(await parseUrlPkg(url)); function getVersions(pkg) { return mdls - .filter(mdl => mdl.pkg.name === pkg) - .map(mdl => mdl.pkg.version); + .filter((mdl) => mdl.pkg.name === pkg) + .map((mdl) => mdl.pkg.version); } // check constraints for (let [pkg, version] of Object.entries(scenario.expect ?? {})) { - if (version === "latest") version = (await lookup(`${pkg}@latest`)).resolved.version; + if (version === "latest") + version = (await lookup(`${pkg}@latest`)).resolved.version; assert( - getVersions(pkg).every(v => v === version), - `freeze scenario "${scenario.name}" expected ${pkg}@${version}, but got [ ${getVersions(pkg).join(", ")} ]`, + getVersions(pkg).every((v) => v === version), + `freeze scenario "${ + scenario.name + }" expected ${pkg}@${version}, but got [ ${getVersions(pkg).join(", ")} ]` ); } } -await Promise.all([ - { - name: "no existing locks", - install: ["lit", "react"], - expect: { - lit: "latest", - react: "latest", +await Promise.all( + [ + { + name: "no existing locks", + install: ["lit", "react"], + expect: { + lit: "latest", + react: "latest", + }, }, - }, - { - name: "existing primary locks", - map: { - imports: { - "lit-html": "https://ga.jspm.io/npm:lit-html@2.6.0/development/lit-html.js", - "react": "https://ga.jspm.io/npm:react@18.1.0/dev.index.js", - } - }, - install: ["lit", "react"], - expect: { - "lit-html": "2.6.0", // lock is hit for a secondary install - "react": "18.1.0", // lock is hit for a primary install - "lit": "latest", + { + name: "existing primary locks", + map: { + imports: { + "lit-html": + "https://ga.jspm.io/npm:lit-html@2.6.0/development/lit-html.js", + react: "https://ga.jspm.io/npm:react@18.1.0/dev.index.js", + }, + }, + install: ["lit", "react"], + expect: { + "lit-html": "2.6.0", // lock is hit for a secondary install + react: "18.1.0", // lock is hit for a primary install + lit: "latest", + }, }, - }, - { - name: "existing secondary locks", - map: { - scopes: { - "https://ga.jspm.io/": { - "lit-html/is-server.js": "https://ga.jspm.io/npm:lit-html@2.6.0/development/is-server.js", - "chalk": "https://ga.jspm.io/npm:chalk@2.0.0/index.js", // out-of-range - } - } - }, - install: ["lit", "lit-html", "react", "chalk"], - expect: { - "lit-html": "2.6.0", // lock is hit for primary as it's in-range - "chalk": "4.1.0", // lock ignored for primary as it's out-of-range - "react": "latest", - "lit": "latest", + { + name: "existing secondary locks", + map: { + scopes: { + "https://ga.jspm.io/": { + "lit-html/is-server.js": + "https://ga.jspm.io/npm:lit-html@2.6.0/development/is-server.js", + chalk: "https://ga.jspm.io/npm:chalk@2.0.0/index.js", // out-of-range + }, + }, + }, + install: ["lit", "lit-html", "react", "chalk"], + expect: { + "lit-html": "2.6.0", // lock is hit for primary as it's in-range + chalk: "4.1.0", // lock ignored for primary as it's out-of-range + react: "latest", + lit: "latest", + }, }, - }, - { - name: "combined with resolutions", - map: { - imports: { - "react": "https://ga.jspm.io/npm:react@18.1.0/dev.index.js", + { + name: "combined with resolutions", + map: { + imports: { + react: "https://ga.jspm.io/npm:react@18.1.0/dev.index.js", + }, + scopes: { + "https://ga.jspm.io/": { + "lit-html/is-server.js": + "https://ga.jspm.io/npm:lit-html@2.6.0/development/is-server.js", + }, + }, }, - scopes: { - "https://ga.jspm.io/": { - "lit-html/is-server.js": "https://ga.jspm.io/npm:lit-html@2.6.0/development/is-server.js" - } - } - }, - opts: { - resolutions: { - "react": "18.2.0", - "lit-html": "2.6.1", + opts: { + resolutions: { + react: "18.2.0", + "lit-html": "2.6.1", + }, + }, + install: ["lit", "react"], + expect: { + "lit-html": "2.6.1", // resolution takes precedence + react: "18.2.0", // resolution takes precedence + lit: "latest", }, }, - install: ["lit", "react"], - expect: { - "lit-html": "2.6.1", // resolution takes precedence - "react": "18.2.0", // resolution takes precedence - "lit": "latest", - }, - }, - { - name: "combined with latest", - map: { - imports: { - "react": "https://ga.jspm.io/npm:react@18.1.0/dev.index.js", - "chalk": "https://ga.jspm.io/npm:chalk@4.1.0/source/index.js", + { + name: "combined with latest", + map: { + imports: { + react: "https://ga.jspm.io/npm:react@18.1.0/dev.index.js", + chalk: "https://ga.jspm.io/npm:chalk@4.1.0/source/index.js", + }, + scopes: { + "https://ga.jspm.io/": { + "lit-html/is-server.js": + "https://ga.jspm.io/npm:lit-html@2.6.0/development/is-server.js", + }, + }, }, - scopes: { - "https://ga.jspm.io/": { - "lit-html/is-server.js": "https://ga.jspm.io/npm:lit-html@2.6.0/development/is-server.js" - } - } - }, - opts: { - latest: true, + opts: { + latest: true, + }, + install: ["lit", "react"], + expect: false, // throws }, - install: ["lit", "react"], - expect: false, // throws - }, -].map(checkScenario)); + ].map(checkScenario) +); diff --git a/test/api/install.test.js b/test/api/install.test.js index c29e29b..a2a8607 100644 --- a/test/api/install.test.js +++ b/test/api/install.test.js @@ -4,12 +4,12 @@ import assert from "assert"; const generator = new Generator({ inputMap: { imports: { - react: "https://ga.jspm.io/npm:react@17.0.1/dev.index.js", + react: "https://ga.jspm.io/npm:react@17.0.1/dev.index.js", }, scopes: { "https://ga.jspm.io/": { "lit-html": "https://ga.jspm.io/npm:lit-html@2.6.0/lit-html.js", - } + }, }, }, mapUrl: import.meta.url, @@ -32,12 +32,12 @@ json = generator.getMap(); assert.strictEqual( json.imports.lit, - "https://ga.jspm.io/npm:lit@2.6.1/index.js", + "https://ga.jspm.io/npm:lit@2.6.1/index.js" ); // Even though latest for lit-html is 2.6.1, it should remain locked due to // the freeze option being set: assert.strictEqual( json.scopes["https://ga.jspm.io/"]["lit-html"], - "https://ga.jspm.io/npm:lit-html@2.6.0/lit-html.js", + "https://ga.jspm.io/npm:lit-html@2.6.0/lit-html.js" ); diff --git a/test/api/node.test.js b/test/api/node.test.js index c7be5b5..5a7cd5d 100644 --- a/test/api/node.test.js +++ b/test/api/node.test.js @@ -12,7 +12,8 @@ import assert from "assert"; assert.strictEqual( json.imports["process"], - `https://ga.jspm.io/npm:@jspm/core@${(await lookup("@jspm/core")).resolved.version + `https://ga.jspm.io/npm:@jspm/core@${ + (await lookup("@jspm/core")).resolved.version }/nodelibs/browser/process-production.js` ); } @@ -33,12 +34,14 @@ import assert from "assert"; assert.strictEqual( json.imports["fs"], - `https://ga.jspm.io/npm:@jspm/core@${(await lookup("@jspm/core")).resolved.version + `https://ga.jspm.io/npm:@jspm/core@${ + (await lookup("@jspm/core")).resolved.version }/nodelibs/browser/fs.js` ); assert.strictEqual( json.imports["process"], - `https://ga.jspm.io/npm:@jspm/core@${(await lookup("@jspm/core")).resolved.version + `https://ga.jspm.io/npm:@jspm/core@${ + (await lookup("@jspm/core")).resolved.version }/nodelibs/browser/process-production.js` ); } @@ -79,7 +82,8 @@ import assert from "assert"; assert.strictEqual( json.imports["process"], - `https://ga.jspm.io/npm:@jspm/core@${(await lookup("@jspm/core")).resolved.version + `https://ga.jspm.io/npm:@jspm/core@${ + (await lookup("@jspm/core")).resolved.version }/nodelibs/browser/process-production.js` ); } diff --git a/test/api/providerswitch.test.js b/test/api/providerswitch.test.js index 0714f97..e0e6003 100644 --- a/test/api/providerswitch.test.js +++ b/test/api/providerswitch.test.js @@ -6,7 +6,7 @@ const generator = new Generator({ defaultProvider: "jspm.io", inputMap: { imports: { - "react": "https://cdn.skypack.dev/react@18.2.0/index.js", + react: "https://cdn.skypack.dev/react@18.2.0/index.js", }, }, }); diff --git a/test/api/urls.test.js b/test/api/urls.test.js index f9abd82..ba9089e 100644 --- a/test/api/urls.test.js +++ b/test/api/urls.test.js @@ -5,7 +5,7 @@ import assert from "assert"; // Should be able to install a package scope URL directly, and it should // resolve to the default export in the scope's package.json: -await (async (enabled=true) => { +await (async (enabled = true) => { if (!enabled) return; const gen = new Generator(); @@ -13,15 +13,12 @@ await (async (enabled=true) => { const map = gen.getMap(); assert.ok(map); - assert.strictEqual( - map?.imports?.lit, - "https://unpkg.com/lit@2.0.0/index.js", - ); + assert.strictEqual(map?.imports?.lit, "https://unpkg.com/lit@2.0.0/index.js"); })(false); // Should be able to install a particular exports subpath from a package scope // URL directly using the pipe ("|") separator -await (async (enabled=true) => { +await (async (enabled = true) => { if (!enabled) return; const gen = new Generator(); @@ -30,14 +27,14 @@ await (async (enabled=true) => { assert.ok(map); assert.strictEqual( - map?.imports['react/jsx-runtime'], - "https://unpkg.com/react@18.0.0/jsx-runtime.js", + map?.imports["react/jsx-runtime"], + "https://unpkg.com/react@18.0.0/jsx-runtime.js" ); })(false); // Should be able to install a module URL directly, if that module URL is // present as an export in the scope's package.json: -await (async (enabled=true) => { +await (async (enabled = true) => { if (!enabled) return; const gen = new Generator(); @@ -45,10 +42,7 @@ await (async (enabled=true) => { const map = gen.getMap(); assert.ok(map); - assert.strictEqual( - map?.imports?.lit, - "https://unpkg.com/lit@2.0.0/index.js", - ); + assert.strictEqual(map?.imports?.lit, "https://unpkg.com/lit@2.0.0/index.js"); })(false); // TODO diff --git a/test/deno/babel.test.js b/test/deno/babel.test.js index 2494708..50e2510 100644 --- a/test/deno/babel.test.js +++ b/test/deno/babel.test.js @@ -15,15 +15,15 @@ await generator.install("@babel/core@7.15.0"); await generator.install("assert"); // TODO: re-enable if they support deno at some point -// +// // const map = generator.getMap(); -// +// // await denoExec( // map, // ` // import babel from '@babel/core'; // import assert from 'assert'; -// +// // const { code } = babel.transform('var p = 5'); // assert.strictEqual(code, 'var p = 5;'); // ` diff --git a/test/deno/esmsh.test.js b/test/deno/esmsh.test.js index 5cf2151..8bd165e 100644 --- a/test/deno/esmsh.test.js +++ b/test/deno/esmsh.test.js @@ -9,7 +9,7 @@ const generator = new Generator({ // Install the NPM assert shim and use it to test itself! // The esm.sh deno build for assert@2.0.0 is broken, so we use an old version. -await generator.install('npm:assert@1.5.0'); +await generator.install("npm:assert@1.5.0"); await denoExec( generator.getMap(), ` diff --git a/test/npm-compatibility/install-pkg.test.js b/test/npm-compatibility/install-pkg.test.js index 8d7c292..441dde4 100644 --- a/test/npm-compatibility/install-pkg.test.js +++ b/test/npm-compatibility/install-pkg.test.js @@ -1,49 +1,57 @@ +import { fetch } from "#fetch"; import { Generator } from "@jspm/generator"; import assert from "assert"; -import fs from "fs/promises"; -import { fileURLToPath } from "url"; const expectedResults = { "primary-in-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", }, "primary-out-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", }, "secondary-in-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.1/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.1/immutable.js", }, "secondary-out-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", }, "primary-not-latest-secondary-in-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.1/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.1/immutable.js", }, "primary-not-latest-secondary-out-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", }, -} +}; for (const [name, expected] of Object.entries(expectedResults)) { + const res = await fetch( + new URL(`./${name}/importmap.json`, import.meta.url), + { + cache: "no-store", // don't want cached stuff in tests + } + ); + assert( + res.status === 200 || res.status === 304, + `Failed to fetch import map for ${name}: ${res.statusText}` + ); + const inputMap = await res.json(); const gen = new Generator({ baseUrl: new URL(`./${name}/`, import.meta.url), - inputMap: JSON.parse(await fs.readFile( - fileURLToPath(new URL(`./${name}/importmap.json`, import.meta.url)) - )), + inputMap, }); await gen.install("wayfarer"); const map = JSON.stringify(gen.getMap(), null, 2); for (const [pkg, resolution] of Object.entries(expected)) { assert( - map.includes(resolution), - `${name}: ${pkg} should have resolution ${resolution}:\n${map}`, + map.includes(resolution), + `${name}: ${pkg} should have resolution ${resolution}:\n${map}` ); } } diff --git a/test/npm-compatibility/install.test.js b/test/npm-compatibility/install.test.js index c0d7561..f88731b 100644 --- a/test/npm-compatibility/install.test.js +++ b/test/npm-compatibility/install.test.js @@ -1,50 +1,57 @@ +import { fetch } from "#fetch"; import { Generator } from "@jspm/generator"; import assert from "assert"; -import fs from "fs/promises"; -import { fileURLToPath } from "url"; const expectedResults = { "primary-in-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", }, "primary-out-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", }, "secondary-in-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.1/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.1/immutable.js", }, "secondary-out-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", }, "primary-not-latest-secondary-in-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.1/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.1/immutable.js", }, "primary-not-latest-secondary-out-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", }, -} +}; for (const [name, expected] of Object.entries(expectedResults)) { + const res = await fetch( + new URL(`./${name}/importmap.json`, import.meta.url), + { + cache: "no-store", // don't want cached stuff in tests + } + ); + assert( + res.status === 200 || res.status === 304, + `Failed to fetch import map for ${name}: ${res.statusText}` + ); + const inputMap = await res.json(); const gen = new Generator({ baseUrl: new URL(`./${name}/`, import.meta.url), - inputMap: JSON.parse(await fs.readFile( - fileURLToPath(new URL(`./${name}/importmap.json`, import.meta.url)) - )), + inputMap, }); await gen.install(); const map = JSON.stringify(gen.getMap(), null, 2); for (const [pkg, resolution] of Object.entries(expected)) { assert( - map.includes(resolution), - `${name}: ${pkg} should have resolution ${resolution}:\n${map}`, + map.includes(resolution), + `${name}: ${pkg} should have resolution ${resolution}:\n${map}` ); } } - diff --git a/test/npm-compatibility/update-pkg.test.js b/test/npm-compatibility/update-pkg.test.js index 086a761..e66645d 100644 --- a/test/npm-compatibility/update-pkg.test.js +++ b/test/npm-compatibility/update-pkg.test.js @@ -1,50 +1,57 @@ +import { fetch } from "#fetch"; import { Generator } from "@jspm/generator"; import assert from "assert"; -import fs from "fs/promises"; -import { fileURLToPath } from "url"; const expectedResults = { "primary-in-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", }, "primary-out-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", }, "secondary-in-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.1/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.1/immutable.js", }, "secondary-out-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", }, "primary-not-latest-secondary-in-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.1/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.1/immutable.js", }, "primary-not-latest-secondary-out-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", }, -} +}; for (const [name, expected] of Object.entries(expectedResults)) { + const res = await fetch( + new URL(`./${name}/importmap.json`, import.meta.url), + { + cache: "no-store", // don't want cached stuff in tests + } + ); + assert( + res.status === 200 || res.status === 304, + `Failed to fetch import map for ${name}: ${res.statusText}` + ); + const inputMap = await res.json(); const gen = new Generator({ baseUrl: new URL(`./${name}/`, import.meta.url), - inputMap: JSON.parse(await fs.readFile( - fileURLToPath(new URL(`./${name}/importmap.json`, import.meta.url)) - )), + inputMap, }); await gen.update("wayfarer"); const map = JSON.stringify(gen.getMap(), null, 2); for (const [pkg, resolution] of Object.entries(expected)) { assert( - map.includes(resolution), - `${name}: ${pkg} should have resolution ${resolution}:\n${map}`, + map.includes(resolution), + `${name}: ${pkg} should have resolution ${resolution}:\n${map}` ); } } - diff --git a/test/npm-compatibility/update.test.js b/test/npm-compatibility/update.test.js index ae39533..18ba62f 100644 --- a/test/npm-compatibility/update.test.js +++ b/test/npm-compatibility/update.test.js @@ -1,51 +1,57 @@ +import { fetch } from "#fetch"; import { Generator } from "@jspm/generator"; import assert from "assert"; -import fs from "fs/promises"; -import { fileURLToPath } from "url"; const expectedResults = { "primary-in-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", }, "primary-out-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", }, "secondary-in-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", }, "secondary-out-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.2/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", }, "primary-not-latest-secondary-in-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", }, "primary-not-latest-secondary-out-range": { - "wayfarer": "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", - "xtend": "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", + wayfarer: "https://ga.jspm.io/npm:wayfarer@6.6.4/index.js", + xtend: "https://ga.jspm.io/npm:xtend@4.0.2/immutable.js", }, -} +}; for (const [name, expected] of Object.entries(expectedResults)) { + const res = await fetch( + new URL(`./${name}/importmap.json`, import.meta.url), + { + cache: "no-store", // don't want cached stuff in tests + } + ); + assert( + res.status === 200 || res.status === 304, + `Failed to fetch import map for ${name}: ${res.statusText}` + ); + const inputMap = await res.json(); const gen = new Generator({ baseUrl: new URL(`./${name}/`, import.meta.url), - inputMap: JSON.parse(await fs.readFile( - fileURLToPath(new URL(`./${name}/importmap.json`, import.meta.url)) - )), + inputMap, }); await gen.update(); const map = JSON.stringify(gen.getMap(), null, 2); for (const [pkg, resolution] of Object.entries(expected)) { assert( - map.includes(resolution), - `${name}: ${pkg} should have resolution ${resolution}:\n${map}`, + map.includes(resolution), + `${name}: ${pkg} should have resolution ${resolution}:\n${map}` ); } } - - diff --git a/test/providers/deno.test.js b/test/providers/deno.test.js index 84419ba..6bf564f 100644 --- a/test/providers/deno.test.js +++ b/test/providers/deno.test.js @@ -11,11 +11,9 @@ import assert from "assert"; assert.strictEqual( json.imports["fresh/runtime.ts"], - "https://deno.land/x/fresh@1.1.5/runtime.ts", - ); - assert.ok( - json.scopes["https://deno.land/"]["preact"] + "https://deno.land/x/fresh@1.1.5/runtime.ts" ); + assert.ok(json.scopes["https://deno.land/"]["preact"]); } const denoStdVersion = (await lookup("deno:path")).resolved.version; diff --git a/test/providers/esmsh.test.js b/test/providers/esmsh.test.js index a520989..0b578ae 100644 --- a/test/providers/esmsh.test.js +++ b/test/providers/esmsh.test.js @@ -10,10 +10,7 @@ const generator = new Generator({ await generator.install("lit@2.0.0-rc.1"); const json = generator.getMap(); -assert.strictEqual( - json.imports.lit, - "https://esm.sh/*lit@2.0.0-rc.1" -); +assert.strictEqual(json.imports.lit, "https://esm.sh/*lit@2.0.0-rc.1"); const scope = json.scopes["https://esm.sh/"]; assert.ok(scope["@lit/reactive-element"]); diff --git a/test/resolve/unused-cjs.test.js b/test/resolve/unused-cjs.test.js index 263331f..32eff43 100644 --- a/test/resolve/unused-cjs.test.js +++ b/test/resolve/unused-cjs.test.js @@ -3,13 +3,13 @@ import assert from "assert"; const generator = new Generator({ mapUrl: import.meta.url, -}) +}); // Should not throw, index file doesn't use CJS: await generator.install("./unusedcjspkg"); // Should throw, uses module global: -await (async() => { +await (async () => { try { await generator.install("./unusedcjspkg/cjs.js"); assert(false); From 2f2e870c4fc9dec3a4133af4bd631dd640449633 Mon Sep 17 00:00:00 2001 From: Guy Paterson-Jones Date: Thu, 20 Apr 2023 19:09:43 +0200 Subject: [PATCH 11/11] feat: rip out all the various mode flags and replace with a single InstallMode type --- src/generator.ts | 98 ++++++++++++++--------------- src/install/installer.ts | 123 ++++++++++++++++++------------------- src/trace/tracemap.ts | 20 +++--- test/api/extract.test.js | 2 +- test/api/reenv.test.js | 4 +- test/api/uninstall.test.js | 2 +- 6 files changed, 123 insertions(+), 126 deletions(-) diff --git a/src/generator.ts b/src/generator.ts index 44c757e..92f1676 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -38,7 +38,7 @@ import { getIntegrity } from "./common/integrity.js"; import { createLogger, Log, LogStream } from "./common/log.js"; import { Replacer } from "./common/str.js"; import { analyzeHtml } from "./html/analyze.js"; -import { InstallTarget, type ResolutionOptions } from "./install/installer.js"; +import { InstallTarget, type InstallMode } from "./install/installer.js"; import { LockResolutions } from "./install/lock.js"; import { getDefaultProviderStrings, type Provider } from "./providers/index.js"; import * as nodemodules from "./providers/nodemodules.js"; @@ -48,7 +48,7 @@ import { Resolver } from "./trace/resolver.js"; export { analyzeHtml }; // Type exports for users: -export { Provider, ResolutionOptions }; +export { Provider }; /** * @interface GeneratorOptions. @@ -301,14 +301,14 @@ export interface GeneratorOptions { * When using a lockfile, do not modify any existing resolutions, and use * existing resolutions whenever possible for new locks. * - * @deprecated Pass a ResolutionOptions object to Generator functions instead. + * @deprecated Use install/link/update to manage dependencies. */ freeze?: boolean; /** * When using a lockfile, force update all touched resolutions to latest. * - * @deprecated Pass a ResolutionOptions object to Generator functions instead. + * @deprecated */ latest?: boolean; @@ -364,8 +364,9 @@ export class Generator { */ installCnt = 0; - // TODO: remove these and make them options on install/link etc instead. + // @deprecated private freeze: boolean | null; + // @deprecated private latest: boolean | null; /** @@ -531,7 +532,6 @@ export class Generator { * @param mapUrl An optional URL for the map to handle relative resolutions, defaults to generator mapUrl. * @param rootUrl An optional root URL for the map to handle root resolutions, defaults to generator rootUrl. * @returns The list of modules pinned by this import map or HTML. - * */ async addMappings( jsonOrHtml: string | IImportMap, @@ -577,13 +577,12 @@ export class Generator { */ async pin( specifier: string, - parentUrl?: string, - opts?: ResolutionOptions + parentUrl?: string ): Promise<{ staticDeps: string[]; dynamicDeps: string[]; }> { - return this.link(specifier, parentUrl, opts); + return this.link(specifier, parentUrl); } /** @@ -596,10 +595,9 @@ export class Generator { */ async traceInstall( specifier: string | string[], - parentUrl?: string, - opts?: ResolutionOptions + parentUrl?: string ): Promise<{ staticDeps: string[]; dynamicDeps: string[] }> { - return this.link(specifier, parentUrl, opts); + return this.link(specifier, parentUrl); } /** @@ -611,8 +609,7 @@ export class Generator { */ async link( specifier: string | string[], - parentUrl?: string, - opts?: ResolutionOptions + parentUrl?: string ): Promise<{ staticDeps: string[]; dynamicDeps: string[] }> { if (typeof specifier === "string") specifier = [specifier]; let error = false; @@ -625,12 +622,7 @@ export class Generator { this.traceMap.visit( specifier, { - installOpts: { - freeze: opts?.freeze ?? this.freeze ?? true, // link defaults to freeze - latestPrimaries: opts?.latestPrimaries ?? this.latest, - latestSecondaries: opts?.latestSecondaries ?? this.latest, - mode: opts?.mode ?? "new-prefer-existing", - }, + installMode: "freeze", toplevel: true, }, parentUrl || this.baseUrl.href @@ -1017,21 +1009,28 @@ export class Generator { * ``` */ async install( + install?: string | Install | (string | Install)[] + ): Promise { + return this._install(install); + } + + private async _install( install?: string | Install | (string | Install)[], - opts?: ResolutionOptions + mode?: InstallMode ): Promise { + // Backwards-compatibility for deprecated options: + if (this.latest) mode ??= "latest-primaries"; + if (this.freeze) mode ??= "freeze"; + // If there are no arguments, then we reinstall all the top-level locks: - if (!install) { + if (install === null || install === undefined) { await this.traceMap.processInputMap; // To match the behaviour of an argumentless `npm install`, we use // existing resolutions for everything unless it's out-of-range: - opts ??= {}; - opts.latestPrimaries ??= false; - opts.latestSecondaries ??= false; - opts.mode ??= "new-prefer-existing"; + mode ??= "default"; - return this.install( + return this._install( Object.entries(this.traceMap.installer.installs.primary).map( ([alias, target]) => { const pkgTarget = @@ -1056,7 +1055,7 @@ export class Generator { } as Install; } ), - opts + mode ); } @@ -1070,7 +1069,7 @@ export class Generator { } return await Promise.all( - install.map((install) => this.install(install, opts)) + install.map((install) => this._install(install, mode)) ).then((installs) => installs.find((i) => i)); } @@ -1087,13 +1086,13 @@ export class Generator { }); return await Promise.all( install.subpaths.map((subpath) => - this.install( + this._install( { target: (install as Install).target, alias: (install as Install).alias, subpath, }, - opts + mode ) ) ).then((installs) => installs.find((i) => i)); @@ -1122,20 +1121,15 @@ export class Generator { `Adding primary constraint for ${alias}: ${JSON.stringify(target)}` ); - // Apply the default resolution options: - const defaultOpts: ResolutionOptions = { - freeze: opts?.freeze ?? this.freeze, - latestPrimaries: - opts?.latestPrimaries ?? this.latest ?? (this.freeze ? false : true), - latestSecondaries: opts?.latestSecondaries ?? this.latest ?? false, - mode: opts?.mode ?? "new", - }; + // By default, an install takes the latest compatible version for primary + // dependencies, and existing in-range versions for secondaries: + mode ??= "latest-primaries"; - await this.traceMap.add(alias, target, defaultOpts); + await this.traceMap.add(alias, target, mode); await this.traceMap.visit( alias + subpath.slice(1), { - installOpts: defaultOpts, + installMode: mode, toplevel: true, }, this.mapUrl.href @@ -1174,21 +1168,27 @@ export class Generator { } } - async update(pkgNames?: string | string[], opts?: ResolutionOptions) { + /** + * Updates the versions of the given packages to the latest versions + * compatible with their parent's package.json ranges. If no packages are + * given then all the top-level packages in the "imports" field of the + * initial import map are updated. + * + * @param {string | string[]} pkgNames Package name or list of package names to update. + */ + async update(pkgNames?: string | string[]) { if (typeof pkgNames === "string") pkgNames = [pkgNames]; if (this.installCnt++ === 0) this.traceMap.startInstall(); await this.traceMap.processInputMap; - // To match the behaviour of `npm update`, an argumentless update should - // reinstall all primary locks and their secondary dependencies to the - // latest compatible versions: const primaryResolutions = this.traceMap.installer.installs.primary; const primaryConstraints = this.traceMap.installer.constraints.primary; + + // Matching the behaviour of "npm update": + let mode: InstallMode = "latest-primaries"; if (!pkgNames) { pkgNames = Object.keys(primaryResolutions); - opts ??= {}; - opts.latestPrimaries ??= true; - opts.latestSecondaries ??= true; + mode = "latest-all"; } const installs: Install[] = []; @@ -1235,7 +1235,7 @@ export class Generator { } } - await this.install(installs, opts); + await this._install(installs, mode); if (--this.installCnt === 0) { const { map, staticDeps, dynamicDeps } = await this.traceMap.finishInstall(); diff --git a/src/install/installer.ts b/src/install/installer.ts index 753e950..91b95e9 100644 --- a/src/install/installer.ts +++ b/src/install/installer.ts @@ -1,11 +1,11 @@ import { Semver } from "sver"; +import { throwInternalError } from "../common/err.js"; import { Log } from "../common/log.js"; +import { registryProviders } from "../providers/index.js"; import { Resolver } from "../trace/resolver.js"; -import { ExactPackage, newPackageTarget, PackageTarget } from "./package.js"; -import { JspmError, throwInternalError } from "../common/err.js"; import { - getFlattenedResolution, getConstraintFor, + getFlattenedResolution, getResolution, InstalledResolution, LockResolutions, @@ -14,7 +14,7 @@ import { setResolution, VersionConstraints, } from "./lock.js"; -import { registryProviders } from "../providers/index.js"; +import { ExactPackage, newPackageTarget, PackageTarget } from "./package.js"; export interface PackageProvider { provider: string; @@ -24,29 +24,40 @@ export interface PackageProvider { export type ResolutionMode = "new" | "new-prefer-existing" | "existing"; /** - * ResolutionOptions configures the interaction between version resolutions - * and the existing lockfile during operations. + * InstallOptions configure the generator's behaviour for existing mappings + * in the import map. An existing mapping is considered "in-range" if either: + * 1. its parent package.json has no "dependencies" range for it + * 2. its semver version is within the parent's range + * + * The "latest-compatible version" of a package is the latest existing version + * within the parent's "dependencies range", or just the latest existing + * version if there is no such range. + * + * "default": + * New installs always resolve to the latest compatible version. Existing + * mappings are kept unless they are out-of-range, in which case they are + * bumped to the latest compatible version. + * + * "latest-primaries": + * Existing primary dependencies (i.e. mappings under "imports") are bumped + * to latest. Existing secondary dependencies are kept unless they are + * out-of-range, in which case they are bumped to the latest compatible + * version. New installs behave according to "default". + * + * "latest-all": + * All existing mappings are bumped to the latest compatible version. New + * installs behave according to "default". + * + * "freeze": + * No existing mappings are changed, and existing mappings are always used + * for new installs wherever possible. Completely new installs behave + * according to "default". */ -export interface ResolutionOptions { - mode?: ResolutionMode; - - /** - * Use existing locks whenever possible for all touched resolutions. - */ - freeze?: boolean; - - /** - * Force update all touched primary resolutions to the latest version - * compatible with the parent's package.json. - */ - latestPrimaries?: boolean; - - /** - * Force update all touched secondary resolutions to the latest version - * compatible with the parent's package.json. - */ - latestSecondaries?: boolean; -} +export type InstallMode = + | "default" + | "latest-primaries" + | "latest-all" + | "freeze"; export type InstallTarget = { pkgTarget: PackageTarget | URL; @@ -183,7 +194,7 @@ export class Installer { * @param {string} pkgName Name of the package being installed. * @param {InstallTarget} target The installation target being installed. * @param {`./${string}` | '.'} traceSubpath - * @param {ResolutionOptions} opts Specifies how to interact with existing installs. + * @param {InstallMode} mode Specifies how to interact with existing installs. * @param {`${string}/` | null} pkgScope URL of the package scope in which this install is occurring, null if it's a top-level install. * @param {string} parentUrl URL of the parent for this install. * @returns {Promise} @@ -192,19 +203,14 @@ export class Installer { pkgName: string, { pkgTarget, installSubpath }: InstallTarget, traceSubpath: `./${string}` | ".", - opts: ResolutionOptions, + mode: InstallMode, pkgScope: `${string}/` | null, parentUrl: string ): Promise { - if (opts.freeze && opts.mode === "existing") - throw new JspmError( - `"${pkgName}" is not installed in the current map to freeze install, imported from ${parentUrl}.`, - "ERR_NOT_INSTALLED" - ); - + const isTopLevel = pkgScope === null; const useLatest = - (pkgScope === null && opts.latestPrimaries) || - (pkgScope !== null && opts.latestSecondaries); + (isTopLevel && mode.includes("latest")) || + (!isTopLevel && mode === "latest-all"); // Resolutions are always authoritative, and override the existing target: if (this.resolutions[pkgName]) { @@ -222,7 +228,7 @@ export class Installer { pkgName, resolutionTarget, traceSubpath, - opts, + mode, pkgScope, parentUrl ); @@ -252,12 +258,9 @@ export class Installer { const provider = this.getProvider(pkgTarget); - // If this is a secondary install or we're in an existing-lock install - // mode, then we make an attempt to find a compatible existing lock: - if ( - (opts.freeze || opts.mode?.includes("existing") || pkgScope !== null) && - !useLatest - ) { + // Look for an existing lock for this package if we're in an install mode + // that supports them: + if (mode === "default" || mode === "freeze" || !useLatest) { const pkg = await this.getBestExistingMatch(pkgTarget); if (pkg) { this.log( @@ -293,9 +296,9 @@ export class Installer { // existing locks on this package to latest and use that. If there's a // constraint and we can't, then we fallback to the best existing lock: if ( - !opts.freeze && + mode !== "freeze" && !useLatest && - pkgScope && + !isTopLevel && latestPkg && !this.tryUpgradeAllTo(latestPkg, pkgUrl, installed) ) { @@ -337,7 +340,8 @@ export class Installer { installSubpath ); setConstraint(this.constraints, pkgName, pkgTarget, pkgScope); - if (!opts.freeze) this.upgradeSupportedTo(latestPkg, pkgUrl, installed); + if (mode !== "freeze") + this.upgradeSupportedTo(latestPkg, pkgUrl, installed); return { installUrl: pkgUrl, installSubpath }; } @@ -345,7 +349,7 @@ export class Installer { * Installs the given package specifier. * * @param {string} pkgName The package specifier being installed. - * @param {ResolutionOptions} opts Specifies how to interact with existing installs. + * @param {InstallMode} mode Specifies how to interact with existing installs. * @param {`${string}/` | null} pkgScope URL of the package scope in which this install is occurring, null if it's a top-level install. * @param {`./${string}` | '.'} traceSubpath * @param {string} parentUrl URL of the parent for this install. @@ -353,7 +357,7 @@ export class Installer { */ async install( pkgName: string, - opts: ResolutionOptions, + mode: InstallMode, pkgScope: `${string}/` | null = null, traceSubpath: `./${string}` | ".", parentUrl: string = this.installBaseUrl @@ -363,11 +367,6 @@ export class Installer { `installing ${pkgName} from ${parentUrl} in scope ${pkgScope}` ); if (!this.installing) throwInternalError("Not installing"); - if ((opts.latestPrimaries || opts.latestSecondaries) && opts.freeze) { - throw new JspmError( - "Cannot enable 'freeze' and 'latest' install options simultaneously." - ); - } // Anything installed in the scope of the installer's base URL is treated // as top-level, and hits the primary locks. Anything else is treated as @@ -385,7 +384,7 @@ export class Installer { pkgName ), traceSubpath, - opts, + mode, isTopLevel ? null : pkgScope, parentUrl ); @@ -411,12 +410,10 @@ export class Installer { pkgName ); - // The latestPrimaries and latestSecondaries options are used to always - // take latest version resolutions from the package.json: const useLatestPjsonTarget = !!pjsonTarget && - ((isTopLevel && opts.latestPrimaries) || - (!isTopLevel && opts.latestSecondaries)); + ((isTopLevel && mode.includes("latest")) || + (!isTopLevel && mode === "latest-all")); // Find any existing locks in the current package scope, making sure // locks are always in-range for their parent scope pjsons: @@ -429,7 +426,7 @@ export class Installer { !useLatestPjsonTarget && existingResolution && (isTopLevel || - opts.freeze || + mode === "freeze" || (await this.inRange( existingResolution.installUrl, pjsonTarget.pkgTarget @@ -458,7 +455,7 @@ export class Installer { if ( !useLatestPjsonTarget && flattenedResolution && - (opts.freeze || + (mode === "freeze" || (await this.inRange( flattenedResolution.installUrl, pjsonTarget.pkgTarget @@ -481,7 +478,7 @@ export class Installer { pkgName, pjsonTarget, traceSubpath, - opts, + mode, isTopLevel ? null : pkgScope, parentUrl ); @@ -498,7 +495,7 @@ export class Installer { // fully qualified InstallTarget, or support string targets here. builtin.target as InstallTarget, traceSubpath, - opts, + mode, isTopLevel ? null : pkgScope, parentUrl ); @@ -521,7 +518,7 @@ export class Installer { pkgName, target, null, - opts, + mode, isTopLevel ? null : pkgScope, parentUrl ); diff --git a/src/trace/tracemap.ts b/src/trace/tracemap.ts index 0ad0882..cb07233 100644 --- a/src/trace/tracemap.ts +++ b/src/trace/tracemap.ts @@ -2,7 +2,7 @@ import { type InstallerOptions, InstallTarget, ResolutionMode, - ResolutionOptions, + InstallMode, } from "../install/installer.js"; import { importedFrom, @@ -91,7 +91,7 @@ interface TraceEntry { interface VisitOpts { static?: boolean; toplevel: boolean; - installOpts: ResolutionOptions; + installMode: InstallMode; visitor?: ( specifier: string, parentUrl: string, @@ -202,12 +202,12 @@ export default class TraceMap { this.log( "tracemap/visit", - `Attempting to resolve ${specifier} to a module from ${parentUrl}, toplevel=${opts.toplevel}, mode=${opts.installOpts.mode}` + `Attempting to resolve ${specifier} to a module from ${parentUrl}, toplevel=${opts.toplevel}, mode=${opts.installMode}` ); const resolved = await this.resolve( specifier, parentUrl, - opts.installOpts, + opts.installMode, opts.toplevel ); @@ -272,7 +272,7 @@ export default class TraceMap { await Promise.all( modules.map(async (module) => { await this.visit(module, { - installOpts: { mode: "existing" }, + installMode: "freeze", // pruning shouldn't change versions static: this.opts.static, toplevel: true, }); @@ -331,7 +331,7 @@ export default class TraceMap { { static: true, visitor, - installOpts: { mode: "existing" }, + installMode: "freeze", toplevel: true, }, this.baseUrl.href, @@ -345,7 +345,7 @@ export default class TraceMap { dynamics.map(async ([specifier, parent]) => { await this.visit( specifier, - { visitor, installOpts: { mode: "existing" }, toplevel: false }, + { visitor, installMode: "freeze", toplevel: false }, parent, seen ); @@ -379,13 +379,13 @@ export default class TraceMap { async add( name: string, target: InstallTarget, - opts?: ResolutionOptions + opts: InstallMode ): Promise { await this.installer.installTarget( name, target, null, - opts || {}, + opts, null, this.mapUrl.href ); @@ -397,7 +397,7 @@ export default class TraceMap { async resolve( specifier: string, parentUrl: string, - installOpts: ResolutionOptions, + installOpts: InstallMode, toplevel: boolean ): Promise { const cjsEnv = this.tracedUrls[parentUrl]?.wasCjs; diff --git a/test/api/extract.test.js b/test/api/extract.test.js index aaf03a3..3f0f6cc 100644 --- a/test/api/extract.test.js +++ b/test/api/extract.test.js @@ -30,7 +30,7 @@ assert.strictEqual( ); assert.strictEqual( map.scopes["https://ga.jspm.io/"]["object-assign"], - "https://ga.jspm.io/npm:object-assign@4.1.1/index.js" + "https://ga.jspm.io/npm:object-assign@4.1.0/index.js" ); assert.strictEqual(Object.keys(map.imports).length, 1); assert.strictEqual(Object.keys(map.scopes["https://ga.jspm.io/"]).length, 1); diff --git a/test/api/reenv.test.js b/test/api/reenv.test.js index 0eddc07..1b4b9b4 100644 --- a/test/api/reenv.test.js +++ b/test/api/reenv.test.js @@ -28,7 +28,7 @@ import assert from "assert"; ); assert.strictEqual( json.scopes["https://ga.jspm.io/"]["object-assign"], - "https://ga.jspm.io/npm:object-assign@4.1.1/index.js" + "https://ga.jspm.io/npm:object-assign@4.1.0/index.js" ); } @@ -61,6 +61,6 @@ import assert from "assert"; ); assert.strictEqual( json.scopes["https://ga.jspm.io/"]["object-assign"], - "https://ga.jspm.io/npm:object-assign@4.1.1/index.js" + "https://ga.jspm.io/npm:object-assign@4.1.0/index.js" ); } diff --git a/test/api/uninstall.test.js b/test/api/uninstall.test.js index c1b3536..0932c41 100644 --- a/test/api/uninstall.test.js +++ b/test/api/uninstall.test.js @@ -32,7 +32,7 @@ import assert from "assert"; ); assert.strictEqual( json.scopes["https://ga.jspm.io/"]["object-assign"], - "https://ga.jspm.io/npm:object-assign@4.1.1/index.js" + "https://ga.jspm.io/npm:object-assign@4.1.0/index.js" ); assert.strictEqual(Object.keys(json.imports).length, 1); assert.strictEqual(Object.keys(json.scopes["https://ga.jspm.io/"]).length, 1);