Skip to content
Permalink
Browse files

New: Allow full package names in .hintrc

Allow full packages names to be specified in `.hintrc` files to
disambiguate between packages published by separate third parties
with a common suffix. Also allows third parties to use their own npm
scope when publishing packages, e.g. `@example/webhint-hint-foo`.
Full package names must still follow naming conventions.

```json
// Example .hintrc
{
    "extends": ["@example/webhint-configuration-foo"],
    "hints": {
        "axe": "error",
        "@example/webhint-hint-bar": "error"
    }
}
```

Also fix finding third-party resources loaded with a configuration.

- - - - - - - - - -

Fix #2654 
Close #2640
  • Loading branch information...
antross committed Aug 2, 2019
1 parent 68b632d commit f36055c685d7f7361dc84556e44a78ccbca246be
@@ -28,6 +28,7 @@
"devDependencies": {
"@types/async": "^3.0.1",
"@types/content-type": "^1.1.3",
"@types/cpx": "^1.5.0",
"@types/lodash": "^4.14.136",
"@types/node": "^12.6.8",
"@types/semver": "^6.0.1",
@@ -256,12 +256,31 @@ const getResource = (source: string, type: ResourceType, name: string) => {
return null;
};

/**
* Check if a name represents a full package name for the specified resource type.
* E.g. for `hint` allowed values would be `@example/webhint-hint-foo` or
* `webhint-hint-foo` (where `example` and `foo` are custom). Also allows internal
* resource references for multi-hint scenarios (e.g. `webhint-hint-foo/subhint`).
*/
const isFullPackageName = (packageName: string, type: ResourceType): boolean => {
const parts = packageName.split('/');
const name = parts.length >= 2 && parts[0].startsWith('@') ? parts[1] : packageName;

if (parts[0] === '@hint') {
return name.startsWith(`${type}-`);
}

return name.startsWith(`webhint-${type}-`);
};

/**
* Looks inside the configurations looking for resources.
*/
const generateConfigPathsToResources = (configurations: string[], name: string, type: ResourceType) => {
return configurations.reduce((total: string[], configuration: string) => {
const basePackagePaths = ['@hint/configuration-', 'hint-configuration-'];
const basePackagePaths = isFullPackageName(configuration, ResourceType.configuration) ?
[''] :
['@hint/configuration-', 'webhint-configuration-'];

let result = total;

@@ -270,8 +289,11 @@ const generateConfigPathsToResources = (configurations: string[], name: string,

try {
const packagePath = path.dirname(resolvePackage(packageName));
const resourceGlob = isFullPackageName(name, type) ?
name :
`{@hint/,webhint-}${type}-${name}`;

const resourcePackages = globby.sync(`node_modules/{@hint/,hint-}${type}-${name}/package.json`, { absolute: true, cwd: packagePath }).map((pkg) => {
const resourcePackages = globby.sync(`node_modules/${resourceGlob}/package.json`, { absolute: true, cwd: packagePath }).map((pkg) => {
return path.dirname(pkg);
});

@@ -293,21 +315,29 @@ const generateConfigPathsToResources = (configurations: string[], name: string,
*
* 1. core resource
* 2. `@hint/` scoped package
* 3. `hint-` prefixed package
* 3. `webhint-` prefixed package
* 4. external hints
*
*/
export const loadResource = (name: string, type: ResourceType, configurations: string[] = [], verifyVersion = false) => {
debug(`Searching ${name}…`);
const isSource = fs.existsSync(name); // eslint-disable-line no-sync
const nameSplitted = name.split('/');
const isPackage = isFullPackageName(name, type);
const nameParts = name.split('/');

let scope = '';
let unscopedNameParts = nameParts;

const packageName = nameSplitted[0];
if (isPackage && nameParts[0].startsWith('@')) {
scope = `${nameParts[0]}/`;
unscopedNameParts = nameParts.slice(1);
}

const packageName = `${scope}${unscopedNameParts[0]}`;
const resourceName = isSource ?
name :
nameSplitted[1] || packageName;
name : unscopedNameParts[1] || packageName;

const key: string = isSource ?
const key = isPackage || isSource ?
name :
`${type}-${name}`;

@@ -326,17 +356,22 @@ export const loadResource = (name: string, type: ResourceType, configurations: s
* i.e.
* if we want to load the hint `hint-typescript-config/is-valid` the key for the cache
* has to be `hint-typescript-config/is-valid`.
* But we need to load the package `typescript-config`.
* But we need to load the package `hint-typescript-config`.
*/
const sources: string[] = isSource ?
[path.resolve(currentProcessDir, name)] : // If the name is direct path to the source we should only check that
[
let sources: string[];

if (isSource) {
sources = [path.resolve(currentProcessDir, name)]; // If the name is direct path to the source we should only check that.
} else if (isPackage) {
sources = [packageName].concat(configPathsToResources); // If the name is a full package name we should only check that, but look for it in config paths as well.
} else {
sources = [
`@hint/${type}-${packageName}`, // Officially supported package
`webhint-${type}-${packageName}`, // Third party package
path.normalize(`${HINT_ROOT}/dist/src/lib/${type}s/${packageName}/${packageName}.js`), // Part of core. E.g.: built-in formatters, parsers, connectors
path.normalize(currentProcessDir) // External hints.
// path.normalize(`${path.resolve(HINT_ROOT, '..')}/${key}`) // Things under `/packages/` for when we are developing something official. E.g.: `/packages/hint-http-cache`
].concat(configPathsToResources);
}

let resource: any;
let loadedSource: string;
@@ -417,10 +452,12 @@ const loadListOfResources = (list: string[] | Object = [], type: ResourceType, c

loaded.push(resource);
} catch (e) {
const name = isFullPackageName(resourceId, type) ? resourceId : `${type}-${resourceId}`;

if (e.status === ResourceErrorStatus.NotCompatible) {
incompatible.push(`${type}-${resourceId}`);
incompatible.push(name);
} else if (e.status === ResourceErrorStatus.NotFound) {
missing.push(`${type}-${resourceId}`);
missing.push(name);
} else {
throw e;
}
@@ -0,0 +1,9 @@
{
"connector": "jsdom",
"formatters": ["json"],
"hints": {
"@example/webhint-hint-example": "error",
"@example2/webhint-hint-example2": "error",
"webhint-hint-example3": "error"
}
}
@@ -0,0 +1,6 @@
{
"main": "index.json",
"name": "@example/webhint-configuration-example",
"private": true,
"version": "1.0.0"
}
@@ -0,0 +1,3 @@
console.log('@example/webhint-hint-example');

module.exports = { meta: { id: 'example' } };
@@ -0,0 +1,6 @@
{
"main": "index.js",
"name": "@example/webhint-hint-example",
"private": true,
"version": "1.0.0"
}
@@ -0,0 +1,3 @@
console.log('@example2/webhint-hint-example2');

module.exports = { meta: { id: 'example2' } };
@@ -0,0 +1,6 @@
{
"main": "index.js",
"name": "@example2/webhint-hint-example2",
"private": true,
"version": "1.0.0"
}
@@ -0,0 +1,3 @@
console.log('webhint-hint-example3');

module.exports = { meta: { id: 'example3' } };
@@ -0,0 +1,6 @@
{
"main": "index.js",
"name": "webhint-hint-example3",
"private": true,
"version": "1.0.0"
}
@@ -3,8 +3,10 @@
* missing packages.
*/
import * as path from 'path';
import { promisify } from 'util';

import test from 'ava';
import * as cpx from 'cpx';
import * as sinon from 'sinon';
import * as globby from 'globby';
import * as proxyquire from 'proxyquire';
@@ -16,6 +18,8 @@ import { ResourceType } from '../../../src/lib/enums/resource-type';
import { ResourceError } from '../../../src/lib/types/resource-error';
import { ResourceErrorStatus } from '../../../src/lib/enums/error-status';

const copy = promisify(cpx.copy);

const cacheKey = path.resolve(__dirname, '../../../src/lib/utils/resource-loader.js');

const installedConnectors = [
@@ -69,31 +73,67 @@ test.serial('tryToLoadFrom does nothing if the package itself is missing', async
sandbox.restore();
});

// TODO: Add tests to verify the order of loading is the right one: core -> scoped -> prefixed. This only checks core resources
test('loadResource looks for resources in the right order (core > @hint > hint- ', async (t) => {
test('loadResource looks for resources in the right order (@hint > webhint- > core)', async (t) => {
cleanCache();

const resourceLoader = await import('../../../src/lib/utils/resource-loader');
const tryToLoadFromStub = sinon.stub(resourceLoader, 'tryToLoadFrom');
const resourceName = 'missing-hint';
const resourceType: ResourceType = ResourceType.hint;
const resourceType = ResourceType.hint;

tryToLoadFromStub.onFirstCall().returns(null);
tryToLoadFromStub.onSecondCall().returns(null);
tryToLoadFromStub.onThirdCall().returns(null);
tryToLoadFromStub.returns(null);

t.throws(() => {
resourceLoader.loadResource(resourceName, resourceType);
});

t.true((tryToLoadFromStub.firstCall.args[0] as string).endsWith(`${resourceName}`), 'Tries to load scoped package second');
t.true((tryToLoadFromStub.secondCall.args[0] as string).endsWith(`hint-${resourceName}`), 'Tries to load prefixed package third');
t.is(tryToLoadFromStub.firstCall.args[0], `@hint/${resourceType}-${resourceName}`, 'Tries to load scoped package second');
t.is(tryToLoadFromStub.secondCall.args[0], `webhint-${resourceType}-${resourceName}`, 'Tries to load prefixed package third');
t.true((tryToLoadFromStub.thirdCall.args[0] as string).endsWith(path.normalize(`/dist/src/lib/${resourceType}s/${resourceName}/${resourceName}.js`)), 'Tries to load core first');

tryToLoadFromStub.restore();
});

test('loadResource looks for resources with full package names by their full name only', async (t) => {
cleanCache();

const resourceLoader = await import('../../../src/lib/utils/resource-loader');
const tryToLoadFromStub = sinon.stub(resourceLoader, 'tryToLoadFrom');
const resourceName = '@example/webhint-hint-missing';
const resourceType = ResourceType.hint;

tryToLoadFromStub.returns(null);

t.throws(() => {
resourceLoader.loadResource(resourceName, resourceType);
});

t.true(tryToLoadFromStub.calledOnce);
t.is(tryToLoadFromStub.firstCall.args[0], resourceName);

tryToLoadFromStub.restore();
});

test('loadResource looks for first-party resources with full package names by their full name only', async (t) => {
cleanCache();

const resourceLoader = await import('../../../src/lib/utils/resource-loader');
const tryToLoadFromStub = sinon.stub(resourceLoader, 'tryToLoadFrom');
const resourceName = '@hint/hint-missing';
const resourceType = ResourceType.hint;

tryToLoadFromStub.returns(null);

t.throws(() => {
resourceLoader.loadResource(resourceName, resourceType);
});

t.true(tryToLoadFromStub.calledOnce);
t.is(tryToLoadFromStub.firstCall.args[0], resourceName);

tryToLoadFromStub.restore();
});

test('loadHint calls loadResource with the right parameters', (t) => {
cleanCache();

@@ -340,6 +380,42 @@ test('loadResources loads all the resources of a given config', async (t) => {

t.true(resources.missing.length > 0, `Found all resources`);
});

test('loadResources loads all the resources of a given config (full package name)', async (t) => {
cleanCache();

// Make hints under `test_modules` nested dependencies of `webhint-configuration-example`.
await copy('tests/lib/utils/fixtures/@example/webhint-configuration-example/test_modules/**', 'tests/lib/utils/fixtures/@example/webhint-configuration-example/node_modules');

// Put `webhint-configuration-example` in the `require` path.
await copy('tests/lib/utils/fixtures/@example/**', 'node_modules/@example');

const config: Configuration = {
browserslist: [],
connector: {
name: 'jsdom',
options: {}
},
extends: ['@example/webhint-configuration-example'],
formatters: ['json'],
hints: {
'@example/webhint-hint-example': 'error',
'@example2/webhint-hint-example2': 'error',
'webhint-hint-example3': 'error'
},
hintsTimeout: 1000,
ignoredUrls: new Map(),
language: '',
parsers: []
};
const resourceLoader = await import('../../../src/lib/utils/resource-loader');
const resources = resourceLoader.loadResources(config);

t.is(resources.hints.length, 3);
t.is(resources.incompatible.length, 0);
t.is(resources.missing.length, 0);
});

/**
* More tests:
*
@@ -661,6 +661,13 @@
resolved "https://registry.yarnpkg.com/@types/content-type/-/content-type-1.1.3.tgz#3688bd77fc12f935548eef102a4e34c512b03a07"
integrity sha512-pv8VcFrZ3fN93L4rTNIbbUzdkzjEyVMp5mPVjsFfOYTDOZMZiZ8P1dhu+kEv3faYyKzZgLlSvnyQNFg+p/v5ug==

"@types/cpx@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@types/cpx/-/cpx-1.5.0.tgz#d2a44b0ab5ec32bfdd743f64aae84847858bce0c"
integrity sha512-kuGK3lZqEvHTSDbJcaA6tcPoEXV4/e88YrltZMcQUewZhzYQwNSTMGIiPBqeeFd4LCBo1CX5U6CV6LaHG3wXSg==
dependencies:
"@types/node" "*"

"@types/css-modules-loader-core@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@types/css-modules-loader-core/-/css-modules-loader-core-1.1.0.tgz#67af15aa16603ac2ffc1d3a7f08547ac809c3005"

0 comments on commit f36055c

Please sign in to comment.
You can’t perform that action at this time.