From e4c51b7c664cfd888667f8ad07331e3e0d4fde47 Mon Sep 17 00:00:00 2001 From: Thomas Allmer Date: Sat, 29 Jun 2019 21:55:20 +0200 Subject: [PATCH] fix(resolve): support relative node paths (#521) --- packages/import-maps-resolve/README.md | 18 +++-- packages/import-maps-resolve/src/index.js | 1 + packages/import-maps-resolve/src/resolver.js | 20 ++++- packages/import-maps-resolve/src/utils.js | 18 +++++ .../test/mergeImportMaps.test.js | 75 +++++++++++++++++++ .../test/resolving-node-adoption.test.js | 28 ++++++- 6 files changed, 151 insertions(+), 9 deletions(-) create mode 100644 packages/import-maps-resolve/test/mergeImportMaps.test.js diff --git a/packages/import-maps-resolve/README.md b/packages/import-maps-resolve/README.md index c5c95ea2..beb6118c 100644 --- a/packages/import-maps-resolve/README.md +++ b/packages/import-maps-resolve/README.md @@ -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; + } } ``` diff --git a/packages/import-maps-resolve/src/index.js b/packages/import-maps-resolve/src/index.js index 1e17cfea..53a5573c 100644 --- a/packages/import-maps-resolve/src/index.js +++ b/packages/import-maps-resolve/src/index.js @@ -1,2 +1,3 @@ export { parseFromString } from './parser.js'; export { resolve } from './resolver.js'; +export { mergeImportMaps } from './utils.js'; diff --git a/packages/import-maps-resolve/src/resolver.js b/packages/import-maps-resolve/src/resolver.js index c9a56e85..ca1ce0f2 100644 --- a/packages/import-maps-resolve/src/resolver.js +++ b/packages/import-maps-resolve/src/resolver.js @@ -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}"`); } diff --git a/packages/import-maps-resolve/src/utils.js b/packages/import-maps-resolve/src/utils.js index 49c0344c..768ed7e8 100644 --- a/packages/import-maps-resolve/src/utils.js +++ b/packages/import-maps-resolve/src/utils.js @@ -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, + }, + }; +} diff --git a/packages/import-maps-resolve/test/mergeImportMaps.test.js b/packages/import-maps-resolve/test/mergeImportMaps.test.js new file mode 100644 index 00000000..ae4c1ea6 --- /dev/null +++ b/packages/import-maps-resolve/test/mergeImportMaps.test.js @@ -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' }, + }, + }); + }); +}); diff --git a/packages/import-maps-resolve/test/resolving-node-adoption.test.js b/packages/import-maps-resolve/test/resolving-node-adoption.test.js index bf64cf19..2b279a50 100644 --- a/packages/import-maps-resolve/test/resolving-node-adoption.test.js +++ b/packages/import-maps-resolve/test/resolving-node-adoption.test.js @@ -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'); + }); +});