diff --git a/index.js b/index.js index eaf7000..3d975d4 100755 --- a/index.js +++ b/index.js @@ -34,7 +34,7 @@ function checkPkgMap () { try { const base = process.cwd() const lock = JSON.parse(stripBOM(fs.readFileSync(path.join(base, 'package-lock.json'), 'utf8'))) - const map = JSON.parse(stripBOM(fs.readFileSync(path.join(base, 'node_modules', '.package-map.json'), 'utf8'))) + const map = JSON.parse(stripBOM(fs.readFileSync(path.join(base, '.package-map.json'), 'utf8'))) require('ssri').checkData( JSON.stringify(lock), map.lockfile_integrity, {error: true} ) diff --git a/lib/installer.js b/lib/installer.js index f983748..aada6ab 100644 --- a/lib/installer.js +++ b/lib/installer.js @@ -117,7 +117,7 @@ class Installer { readJson(prefix, 'package.json'), readJson(prefix, 'package-lock.json', true), readJson(prefix, 'npm-shrinkwrap.json', true), - readJson(path.join(prefix, 'node_modules'), '.package-map.json', true), + readJson(prefix, '.package-map.json', true), (pkg, lock, shrink, map) => { if (shrink) { this.log('verbose', 'prepare', 'using npm-shrinkwrap.json') @@ -337,7 +337,7 @@ class Installer { const lockHash = ssri.fromData(lockStr, {algorithms: ['sha256']}) const pkgMap = { 'lockfile_integrity': lockHash.toString(), - 'path_prefix': '/.package-map.json' + 'path_prefix': '/node_modules' } tree.forEach((dep, next) => { if (dep.isRoot) { return next() } @@ -364,9 +364,7 @@ class Installer { } async writePackageMap (map) { - const nm = path.join(this.prefix, 'node_modules') - await mkdirp(nm) - await writeFileAsync(path.join(nm, '.package-map.json'), stringifyPkg(map)) + await writeFileAsync(path.join(this.prefix, '.package-map.json'), stringifyPkg(map)) } } module.exports = treeFrog diff --git a/lib/node/fs.js b/lib/node/fs.js index e050024..4172206 100644 --- a/lib/node/fs.js +++ b/lib/node/fs.js @@ -183,6 +183,7 @@ function overrideNode () { return stats } } + fs.statSync.orig = statSync const {stat} = fs fs.stat = function (p, callback) { diff --git a/lib/node/module.js b/lib/node/module.js index 42e9978..14010b7 100644 --- a/lib/node/module.js +++ b/lib/node/module.js @@ -23,8 +23,7 @@ // NOTE: The code here is almost all identical to the regular module.js. // It's reloaded here because the `process.binding('fs')` override -// can't retroactively affect module.js, and because `.package-map.json` -// needs to be added to the search path. All other functionality is +// can't retroactively affect module.js. All other functionality is // done by the fs override itself. module.exports.overrideNode = overrideNode function overrideNode () { @@ -137,10 +136,6 @@ function overrideNode () { let warned = false Module._findPath = function (request, paths, isMain) { - paths = paths.reduce((acc, p) => { - acc.push(p, path.join(p, '.package-map.json')) - return acc - }, []) if (path.isAbsolute(request)) { paths = [''] } else if (!paths || paths.length === 0) { diff --git a/lib/pkgmap.js b/lib/pkgmap.js index 6cb7493..9e8d352 100644 --- a/lib/pkgmap.js +++ b/lib/pkgmap.js @@ -8,11 +8,10 @@ const path = require('path') const ssri = require('ssri') const pkgMapName = '.package-map.json' -const mapNameLen = pkgMapName.length const pkgMapCache = new Map() const envNoPkgMap = process.env.FROG_NO_PKG_MAP -const isPkgMapDisabled = () => process.noPkgMap || envNoPkgMap +const isPkgMapDisabled = () => !process.tink || process.tink.noPkgMap || envNoPkgMap module.exports.resolve = resolve module.exports._clearCache = () => pkgMapCache.clear() @@ -23,11 +22,15 @@ function resolve (...p) { // expected package + file hash. if (isPkgMapDisabled()) { return null } const resolved = path.resolve(...p) + // If the file already exists in the filesystem, use the filesystem version + try { + (fs.statSync.orig || fs.statSync)(resolved, true) + return null + } catch (e) {} const result = readPkgMap(resolved) if (!result) { return result } - const {pkgMap, pkgMapIdx} = result + let {pkgMap, subPath} = result if (!pkgMap) { return false } - let subPath = resolved.substr(pkgMapIdx - 1) let pkgName, filePath let scope = pkgMap while (subPath) { @@ -78,7 +81,7 @@ function resolveEntity (scope, pkgName, filePath) { const next = split.shift() if (next === '.') { continue } location = location[next] - if (typeof location === 'string') { + if (typeof location === 'string' && !split.length) { return {hash: location, isFile: true} } else if (!location || typeof location !== 'object') { return false @@ -92,21 +95,31 @@ function resolveEntity (scope, pkgName, filePath) { module.exports.readPkgMap = readPkgMap function readPkgMap (...p) { const resolved = path.resolve(...p) - const pkgMapIdx = resolved.indexOf(pkgMapName) - if (pkgMapIdx === -1 || pkgMapIdx + mapNameLen + 1 >= resolved.length) { - // Not in a pkgmapped path, or reading a .package-map.json itself - return null - } - const pkgMapPath = resolved.substr(0, pkgMapIdx + mapNameLen) - let pkgMap - if (pkgMapCache.has(pkgMapPath)) { - pkgMap = pkgMapCache.get(pkgMapPath) - } else { - const p = path.toNamespacedPath ? path.toNamespacedPath(pkgMapPath) : pkgMapPath - pkgMap = JSON.parse(fs.readFileSync(p)) - pkgMapCache.set(pkgMapPath, pkgMap) + let modulesIdx = resolved.lastIndexOf('node_modules') + while (modulesIdx !== -1) { + let substr = resolved.substr(0, modulesIdx - 1) + const pkgMapPath = path.join(substr, pkgMapName) + let pkgMap + if (pkgMapCache.has(pkgMapPath)) { + pkgMap = pkgMapCache.get(pkgMapPath) + } else { + const p = path.toNamespacedPath(pkgMapPath) + try { + pkgMap = JSON.parse(fs.readFileSync(p)) + pkgMapCache.set(pkgMapPath, pkgMap) + } catch (e) { + if (e.code !== 'ENOENT') { + throw e + } + } + } + if (pkgMap) { + return {pkgMap, subPath: resolved.substr(modulesIdx - 1)} + } else { + modulesIdx = substr.lastIndexOf('node_modules') + } } - return {pkgMap, pkgMapIdx} + return null } module.exports.read = read @@ -143,15 +156,15 @@ function readSync ({cache, hash, pkg, resolvedPath, isFile}) { module.exports.stat = stat async function stat ({cache, hash, pkg, resolvedPath, isDir}, verify) { - if (!cache || !hash) { - throw new Error('stat() requires a fully-resolved pkgmap file address') - } if (isDir || path.basename(resolvedPath) === '.package-map.json') { return Object.assign(fs.lstatSync(process.tink.cache), { mode: 16676, // read-only size: 64 }) } + if (!cache || !hash) { + throw new Error('stat() requires a fully-resolved pkgmap file address') + } let info try { info = await ccRead.hasContent(cache, hash) @@ -188,15 +201,15 @@ async function stat ({cache, hash, pkg, resolvedPath, isDir}, verify) { module.exports.statSync = statSync function statSync ({cache, hash, pkg, resolvedPath, isDir}, verify) { - if (!cache || !hash) { - throw new Error('statSync() requires a fully-resolved pkgmap file address') - } if (isDir || path.basename(resolvedPath) === '.package-map.json') { return Object.assign(fs.lstatSync(process.tink.cache), { mode: 16676, // read-only size: 64 }) } + if (!cache || !hash) { + throw new Error('statSync() requires a fully-resolved pkgmap file address') + } let info try { info = ccRead.hasContent.sync(cache, hash) diff --git a/test/pkgmap.js b/test/pkgmap.js index 08d1164..126378d 100644 --- a/test/pkgmap.js +++ b/test/pkgmap.js @@ -11,13 +11,58 @@ const {File, Dir} = Tacks const pkgmap = require('../lib/pkgmap.js') +test('UNIT readPkgMap', t => { + process.tink = { + cache: './here' + } + const pkgMap = { + path_prefix: '/node_modules', + packages: { + 'eggplant': { + files: { + 'hello.js': 'sha1-deadbeef' + } + } + }, + scopes: { + 'eggplant': { + path_prefix: '/node_modules', + packages: { + 'aubergine': { + files: { + 'bonjour.js': 'sha1-badc0ffee' + } + }, + '@myscope/scoped': { + files: { + 'ohmy.js': 'sha1-abcdef', + 'lib': { + 'hithere.js': 'sha1-badbebe' + } + } + } + } + } + } + } + const fixture = new Tacks(Dir({ + '.package-map.json': File(pkgMap) + })) + fixture.create(testDir.path) + t.deepEqual(pkgmap.readPkgMap('node_modules/eggplant'), { + pkgMap, + subPath: '/node_modules/eggplant' + }) + t.done() +}) + test('resolve: finds an existing path into a .package-map.json', async t => { process.tink = { cache: './here' } const fixture = new Tacks(Dir({ '.package-map.json': File({ - path_prefix: '/.package-map.json', + path_prefix: '/node_modules', packages: { 'eggplant': { files: { @@ -48,7 +93,7 @@ test('resolve: finds an existing path into a .package-map.json', async t => { }) })) fixture.create(testDir.path) - const prefix = path.join(testDir.path, '.package-map.json', 'eggplant') + const prefix = path.join(testDir.path, 'node_modules', 'eggplant') t.similar(pkgmap.resolve(prefix, 'hello.js'), { cache: './here', hash: 'sha1-deadbeef', @@ -90,9 +135,7 @@ test('resolve: finds an existing path into a .package-map.json', async t => { } }, 'found file even though pkgmap deleted') pkgmap._clearCache() - t.throws(() => { - pkgmap.resolve(prefix, 'hello.js') - }, /ENOENT/, 'cache gone after clearing') + t.equal(pkgmap.resolve(prefix, 'hello.js'), null, 'cache gone') pkgmap._clearCache() }) @@ -104,7 +147,7 @@ test('read: reads a file defined in a package map', async t => { } const fixture = new Tacks(Dir({ '.package-map.json': File({ - path_prefix: '/.package-map.json', + path_prefix: '/node_modules', packages: { 'eggplant': { files: { @@ -116,7 +159,7 @@ test('read: reads a file defined in a package map', async t => { })) fixture.create(testDir.path) const p = pkgmap.resolve( - testDir.path, '.package-map.json', 'eggplant', 'hello.js' + testDir.path, 'node_modules', 'eggplant', 'hello.js' ) t.equal( (await pkgmap.read(p)).toString('utf8'), @@ -141,7 +184,7 @@ test('stat: get filesystem stats for a file', async t => { } const fixture = new Tacks(Dir({ '.package-map.json': File({ - path_prefix: '/.package-map.json', + path_prefix: '/node_modules', packages: { 'eggplant': { files: { @@ -153,7 +196,7 @@ test('stat: get filesystem stats for a file', async t => { })) fixture.create(testDir.path) const p = pkgmap.resolve( - testDir.path, '.package-map.json', 'eggplant', 'hello.js' + testDir.path, 'node_modules', 'eggplant', 'hello.js' ) const stat = await pkgmap.stat(p) t.ok(stat, 'got stat from cache file')