fix(resolve): support relative node paths (#521)

This commit is contained in:
Thomas Allmer
2019-06-29 21:55:20 +02:00
committed by Lars den Bakker
parent 1e019466ae
commit e4c51b7c66
6 changed files with 151 additions and 9 deletions

View File

@@ -17,16 +17,22 @@ import { parseFromString, resolve } from '@import-maps/resolve';
// you probably want to cache the map processing and not redo it for every resolve
// a simple example
const mapCache = null;
const importMapCache = null;
function myResolve(specifier) {
const currentDir = process.cwd();
if (!mapCache) {
const mapString = fs.readFileSync(path.join(currentDir, 'import-map.json'), 'utf-8');
mapCache = parseFromString(mapString, currentDir);
const rootDir = process.cwd();
const basePath = importer ? importer.replace(rootDir, `${rootDir}::`) : `${rootDir}::`;
if (!importMapCache) {
const mapString = fs.readFileSync(path.join(rootDir, 'import-map.json'), 'utf-8');
mapCache = parseFromString(mapString, basePath);
}
return specifier => resolve(specifier, mapCache, path.join(`${currentDir}::`, 'index.html');
const relativeSource = source.replace(rootDir, '');
const resolvedPath = resolve(relativeSource, importMapCache, basePath);
if (resolvedPath) {
return resolvedPath;
}
}
```

View File

@@ -1,2 +1,3 @@
export { parseFromString } from './parser.js';
export { resolve } from './resolver.js';
export { mergeImportMaps } from './utils.js';

View File

@@ -1,5 +1,6 @@
/* eslint-disable no-restricted-syntax */
import { URL } from 'url';
import path from 'path';
import {
tryURLLikeSpecifierParse,
BUILT_IN_MODULE_SCHEME,
@@ -89,14 +90,25 @@ function resolveImportsMatch(normalizedSpecifier, specifierMap) {
export function resolve(specifier, parsedImportMap, scriptURL) {
const asURL = tryURLLikeSpecifierParse(specifier, scriptURL);
const normalizedSpecifier = asURL ? asURL.href : specifier;
const scriptURLString = scriptURL;
let nodeSpecifier = null;
if (scriptURL.includes('::')) {
const [rootPath, basePath] = scriptURL.split('::');
const dirPath = specifier.startsWith('/') ? '' : path.dirname(basePath);
nodeSpecifier = path.normalize(path.join(rootPath, dirPath, specifier));
}
const scriptURLString = scriptURL.split('::').join('');
for (const [scopePrefix, scopeImports] of Object.entries(parsedImportMap.scopes)) {
if (
scopePrefix === scriptURLString ||
(scopePrefix.endsWith('/') && scriptURLString.startsWith(scopePrefix))
) {
const scopeImportsMatch = resolveImportsMatch(normalizedSpecifier, scopeImports);
const scopeImportsMatch = resolveImportsMatch(
nodeSpecifier || normalizedSpecifier,
scopeImports,
);
if (scopeImportsMatch) {
return scopeImportsMatch;
}
@@ -116,5 +128,9 @@ export function resolve(specifier, parsedImportMap, scriptURL) {
return asURL.href;
}
if (nodeSpecifier && (specifier.startsWith('/') || specifier.startsWith('.'))) {
return nodeSpecifier;
}
throw new TypeError(`Unmapped bare specifier "${specifier}"`);
}

View File

@@ -56,3 +56,21 @@ export function tryURLLikeSpecifierParse(specifier, baseURL) {
return null;
}
export function mergeImportMaps(mapA, mapB) {
const mapAImports = mapA && mapA.imports ? mapA.imports : {};
const mapBImports = mapB && mapB.imports ? mapB.imports : {};
const mapAScopes = mapA && mapA.scopes ? mapA.scopes : {};
const mapBScopes = mapB && mapB.scopes ? mapB.scopes : {};
return {
imports: {
...mapAImports,
...mapBImports,
},
scopes: {
...mapAScopes,
...mapBScopes,
},
};
}

View File

@@ -0,0 +1,75 @@
import chai from 'chai';
import { mergeImportMaps } from '../src';
const { expect } = chai;
describe('mergeImportMaps', () => {
it('always has at least imports and scopes', () => {
expect(mergeImportMaps({}, null)).to.deep.equal({
imports: {},
scopes: {},
});
});
it('merges imports', () => {
const mapA = { imports: { foo: '/to/foo.js' } };
const mapB = { imports: { bar: '/to/bar.js' } };
expect(mergeImportMaps(mapA, mapB)).to.deep.equal({
imports: {
foo: '/to/foo.js',
bar: '/to/bar.js',
},
scopes: {},
});
});
it('merges scopes', () => {
const mapA = { scopes: { '/path/to/foo/': { foo: '/to/foo.js' } } };
const mapB = { scopes: { '/path/to/bar/': { foo: '/to/bar.js' } } };
expect(mergeImportMaps(mapA, mapB)).to.deep.equal({
imports: {},
scopes: {
'/path/to/foo/': { foo: '/to/foo.js' },
'/path/to/bar/': { foo: '/to/bar.js' },
},
});
});
it('removes unknown keys', () => {
const mapA = { imports: { foo: '/to/foo.js' } };
const mapB = { imp: { bar: '/to/bar.js' } };
expect(mergeImportMaps(mapA, mapB)).to.deep.equal({
imports: {
foo: '/to/foo.js',
},
scopes: {},
});
});
it('overrides keys of imports and scopes of first map with second', () => {
const mapA = { imports: { foo: '/to/foo.js' } };
const mapB = { imports: { foo: '/to/fooOverride.js' } };
expect(mergeImportMaps(mapA, mapB)).to.deep.equal({
imports: {
foo: '/to/fooOverride.js',
},
scopes: {},
});
});
it('does not introspect the scopes so with a conflict the last one wins', () => {
const mapA = { scopes: { '/path/to/foo/': { foo: '/to/foo.js' } } };
const mapB = { scopes: { '/path/to/foo/': { foo: '/to/fooOverride.js' } } };
expect(mergeImportMaps(mapA, mapB)).to.deep.equal({
imports: {},
scopes: {
'/path/to/foo/': { foo: '/to/fooOverride.js' },
},
});
});
});

View File

@@ -5,7 +5,7 @@ import { resolve } from '../src/resolver.js';
const { expect } = chai;
const mapBaseURL = '/home/foo/project-a::/app/index.js';
const scriptURL = '/home/foo/project-a/app/node_modules/foo/foo.js';
const scriptURL = '/home/foo/project-a::/js/app.js';
function makeResolveUnderTest(mapString) {
const map = parseFromString(mapString, mapBaseURL);
@@ -77,3 +77,29 @@ describe('Mapped using the "imports" key only (no scopes)', () => {
});
});
});
describe('Unmapped', () => {
const resolveUnderTest = makeResolveUnderTest(`{}`);
it('should resolve ./ specifiers as URLs', () => {
expect(resolveUnderTest('./foo')).to.equal('/home/foo/project-a/js/foo');
expect(resolveUnderTest('./foo/bar')).to.equal('/home/foo/project-a/js/foo/bar');
expect(resolveUnderTest('./foo/../bar')).to.equal('/home/foo/project-a/js/bar');
expect(resolveUnderTest('./foo/../../bar')).to.equal('/home/foo/project-a/bar');
});
it('should resolve ../ specifiers as URLs', () => {
expect(resolveUnderTest('../foo')).to.equal('/home/foo/project-a/foo');
expect(resolveUnderTest('../foo/bar')).to.equal('/home/foo/project-a/foo/bar');
// TODO: do not allow to go up higher then root path
// expect(resolveUnderTest('../../../foo/bar')).to.equal('/home/foo/project-a/foo/bar');
});
it('should resolve / specifiers as URLs', () => {
expect(resolveUnderTest('/foo')).to.equal('/home/foo/project-a/foo');
expect(resolveUnderTest('/foo/bar')).to.equal('/home/foo/project-a/foo/bar');
// TODO: do not allow to go up higher then root path
// expect(resolveUnderTest('/../../foo/bar')).to.equal('/home/foo/project-a/foo/bar');
// expect(resolveUnderTest('/../foo/../bar')).to.equal('/home/foo/project-a/bar');
});
});