Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(sys): make NodeLazyRequire complain if package versions aren't right #3346

Merged
merged 8 commits into from
Apr 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 62 additions & 10 deletions NOTICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1472,6 +1472,30 @@ Homepage: https://lodash.com/

--------

## `lru-cache`

License: ISC

Author: Isaac Z. Schlueter <i@izs.me>

> The ISC License
>
> Copyright (c) Isaac Z. Schlueter and Contributors
>
> Permission to use, copy, modify, and/or distribute this software for any
> purpose with or without fee is hereby granted, provided that the above
> copyright notice and this permission notice appear in all copies.
>
> THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
> WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
> MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
> ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
> WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
> ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
> IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

--------

## `magic-string`

License: MIT
Expand Down Expand Up @@ -2822,21 +2846,25 @@ Homepage: https://rollupjs.org/

--------

## `semiver`
## `semver`

License: MIT

Author: [Luke Edwards](lukeed.com)
License: ISC

> MIT License
>
> Copyright (c) Luke Edwards <luke.edwards05@gmail.com> (lukeed.com)
> The ISC License
>
> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
> Copyright (c) Isaac Z. Schlueter and Contributors
>
> The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
> Permission to use, copy, modify, and/or distribute this software for any
> purpose with or without fee is hereby granted, provided that the above
> copyright notice and this permission notice appear in all copies.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
> THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
> WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
> MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
> ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
> WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
> ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
> IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

--------

Expand Down Expand Up @@ -3263,3 +3291,27 @@ Homepage: https://github.com/websockets/ws
> SOFTWARE.

--------

## `yallist`

License: ISC

Author: Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)

> The ISC License
>
> Copyright (c) Isaac Z. Schlueter and Contributors
>
> Permission to use, copy, modify, and/or distribute this software for any
> purpose with or without fee is hereby granted, provided that the above
> copyright notice and this permission notice appear in all copies.
>
> THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
> WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
> MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
> ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
> WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
> ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
> IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

--------
16 changes: 0 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@
"puppeteer": "~10.0.0",
"rollup": "2.42.3",
"rollup-plugin-sourcemaps": "^0.6.3",
"semiver": "^1.1.0",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this as a dependency since we were only using it in a few places and already depending on semver.

I noticed references to this package in scripts/license.ts and NOTICE.md — is there any manual intervention needed to sort those out?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

possible nvm on this one - found scripts/license.ts and ran it, see 684fe50

"semver": "7.3.4",
"sizzle": "^2.3.6",
"terser": "5.6.1",
Expand Down
2 changes: 1 addition & 1 deletion scripts/license.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const entryDeps = [
'postcss',
'prompts',
'rollup',
'semiver',
'semver',
'sizzle',
'source-map',
'terser',
Expand Down
63 changes: 49 additions & 14 deletions src/sys/node/node-lazy-require.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,78 @@
import type * as d from '../../declarations';
import { buildError } from '@utils';
import { NodeResolveModule } from './node-resolve-module';
import semiver from 'semiver';
import fs from 'graceful-fs';
import path from 'path';
import satisfies from 'semver/functions/satisfies';
import major from 'semver/functions/major';

/**
* The version range that we support for a given package
* [0] is the lower end, while [1] is the higher end.
*
* These strings should be standard semver strings.
*/
type NodeVersionRange = [string, string];

/**
* A manifest for lazily-loaded dependencies, mapping dependency names
* to version ranges.
*/
type LazyDependencies = Record<string, NodeVersionRange>;

/**
* Lazy requirer for Node, with functionality for specifying version ranges
* and returning diagnostic errors if requirements aren't met.
*/
export class NodeLazyRequire implements d.LazyRequire {
private ensured = new Set<string>();

constructor(
private nodeResolveModule: NodeResolveModule,
private lazyDependencies: { [dep: string]: [string, string] }
) {}
/**
* Create a NodeLazyRequire instance
*
* @param nodeResolveModule an object which wraps up module resolution functionality
* @param lazyDependencies the dependency requirements we want to enforce here
*/
constructor(private nodeResolveModule: NodeResolveModule, private lazyDependencies: LazyDependencies) {}

async ensure(fromDir: string, ensureModuleIds: string[]) {
/**
* Ensure that a dependency within our supported range is installed in the current
* environment. This function will check all the dependency requirements passed in when
* the class is instantiated and return diagnostics if there are any issues.
*
* @param fromDir the directory from which we'll attempt to resolve the dependencies, typically
* this will be project's root directory.
* @param ensureModuleIds an array of module names whose versions we're going to check
* @returns a Promise holding diagnostics if any of the dependencies either were not
* resolved _or_ did not meet our version requirements.
*/
async ensure(fromDir: string, ensureModuleIds: string[]): Promise<d.Diagnostic[]> {
const diagnostics: d.Diagnostic[] = [];
const missingDeps: string[] = [];
const problemDeps: string[] = [];

ensureModuleIds.forEach((ensureModuleId) => {
if (!this.ensured.has(ensureModuleId)) {
const [minVersion, recommendedVersion] = this.lazyDependencies[ensureModuleId];
const [minVersion, maxVersion] = this.lazyDependencies[ensureModuleId];

try {
const pkgJsonPath = this.nodeResolveModule.resolveModule(fromDir, ensureModuleId);

const installedPkgJson: d.PackageJsonData = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));

if (semiver(installedPkgJson.version, minVersion) >= 0) {
if (satisfies(installedPkgJson.version, `${minVersion} - ${major(maxVersion)}.x`)) {
this.ensured.add(ensureModuleId);
return;
}
} catch (e) {}
missingDeps.push(`${ensureModuleId}@${recommendedVersion}`);
// if we get here we didn't get to the `return` above, so either 1) there was some error
// reading the package.json or 2) the version wasn't in our specified version range.
problemDeps.push(`${ensureModuleId}@${maxVersion}`);
}
});

if (missingDeps.length > 0) {
if (problemDeps.length > 0) {
const err = buildError(diagnostics);
err.header = `Please install missing dev dependencies with either npm or yarn.`;
err.messageText = `npm install --save-dev ${missingDeps.join(' ')}`;
err.header = `Please install supported versions of dev dependencies with either npm or yarn.`;
err.messageText = `npm install --save-dev ${problemDeps.join(' ')}`;
}

return diagnostics;
Expand Down
4 changes: 2 additions & 2 deletions src/sys/node/node-stencil-version-checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Logger, PackageJsonData } from '../../declarations';
import { isString, noop } from '@utils';
import fs from 'graceful-fs';
import path from 'path';
import semiver from 'semiver';
import semverLt from 'semver/functions/lt';
import { tmpdir } from 'os';

const REGISTRY_URL = `https://registry.npmjs.org/@stencil/core`;
Expand All @@ -14,7 +14,7 @@ export async function checkVersion(logger: Logger, currentVersion: string): Prom
const latestVersion = await getLatestCompilerVersion(logger);
if (latestVersion != null) {
return () => {
if (semiver(currentVersion, latestVersion) < 0) {
if (semverLt(currentVersion, latestVersion)) {
printUpdateMessage(logger, currentVersion, latestVersion);
} else {
console.debug(
Expand Down
68 changes: 68 additions & 0 deletions src/sys/node/test/node-lazy-require.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { NodeLazyRequire } from '../node-lazy-require';
import { buildError } from '@utils';
import { NodeResolveModule } from '../node-resolve-module';
import fs from 'graceful-fs';

const mockPackageJson = (version: string) =>
JSON.stringify({
version,
});

describe('node-lazy-require', () => {
describe('NodeLazyRequire', () => {
describe('ensure', () => {
let readFSMock: jest.SpyInstance<ReturnType<typeof fs.readFileSync>, Parameters<typeof fs.readFileSync>>;

beforeEach(() => {
readFSMock = jest.spyOn(fs, 'readFileSync').mockReturnValue(mockPackageJson('10.10.10'));
});

afterEach(() => {
readFSMock.mockClear();
});

function setup() {
const resolveModule = new NodeResolveModule();
const nodeLazyRequire = new NodeLazyRequire(resolveModule, {
jest: ['2.0.7', '38.0.1'],
});
return nodeLazyRequire;
}

it.each(['2.0.7', '10.10.10', '38.0.1', '38.0.2', '38.5.17'])(
'should not error if installed package has a suitable major version (%p)',
async (testVersion) => {
const nodeLazyRequire = setup();
readFSMock.mockReturnValue(mockPackageJson(testVersion));
let diagnostics = await nodeLazyRequire.ensure('.', ['jest']);
expect(diagnostics.length).toBe(0);
}
);

it('should error if the installed version of a package is too low', async () => {
const nodeLazyRequire = setup();
readFSMock.mockReturnValue(mockPackageJson('1.1.1'));
let [error] = await nodeLazyRequire.ensure('.', ['jest']);
expect(error).toEqual({
...buildError([]),
header: 'Please install supported versions of dev dependencies with either npm or yarn.',
messageText: 'npm install --save-dev jest@38.0.1',
});
});

it.each(['100.1.1', '38.0.1-alpha.0'])(
'should error if the installed version of a package is too high (%p)',
async (version) => {
const nodeLazyRequire = setup();
readFSMock.mockReturnValue(mockPackageJson(version));
let [error] = await nodeLazyRequire.ensure('.', ['jest']);
expect(error).toEqual({
...buildError([]),
header: 'Please install supported versions of dev dependencies with either npm or yarn.',
messageText: 'npm install --save-dev jest@38.0.1',
});
}
);
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add one more test here to verify that this returns an error for prereleases? E.g.

Suggested change
});
});
it('should error if the installed version of a package is too high (prereleases)', async () => {
const { nodeLazyRequire, readFSMock } = setup();
readFSMock.mockReturnValue(mockPackageJson('38.0.1-alpha.0'));
let [error] = await nodeLazyRequire.ensure('.', ['jest']);
expect(error).toEqual({
...buildError([]),
header: 'Please install supported versions of dev dependencies with either npm or yarn.',
messageText: 'npm install --save-dev jest@38.0.1',
});
});

Copy link
Contributor Author

@alicewriteswrongs alicewriteswrongs Apr 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense - I parameterized this test and added that version string, and then also added a few more to the other test which tests what should be accepted as valid versions — 3bd58e2

});
});