Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
19 changes: 14 additions & 5 deletions lib/commands/ci.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const fs = require('fs/promises')
const { log, time } = require('proc-log')
const validateLockfile = require('../utils/validate-lockfile.js')
const ArboristWorkspaceCmd = require('../arborist-cmd.js')
const getWorkspaces = require('../utils/get-workspaces.js')

class CI extends ArboristWorkspaceCmd {
static description = 'Clean install a project'
Expand Down Expand Up @@ -76,14 +77,22 @@ class CI extends ArboristWorkspaceCmd {

const dryRun = this.npm.config.get('dry-run')
if (!dryRun) {
const workspacePaths = await getWorkspaces([], {
path: this.npm.localPrefix,
includeWorkspaceRoot: true,
})

// Only remove node_modules after we've successfully loaded the virtual
// tree and validated the lockfile
await time.start('npm-ci:rm', async () => {
const path = `${where}/node_modules`
// get the list of entries so we can skip the glob for performance
const entries = await fs.readdir(path, null).catch(() => [])
return Promise.all(entries.map(f => fs.rm(`${path}/${f}`,
{ force: true, recursive: true })))
return await Promise.all([...workspacePaths.values()].map(async modulePath => {
const path = `${modulePath}/node_modules`
// get the list of entries so we can skip the glob for performance
const entries = await fs.readdir(path, null).catch(() => [])
return Promise.all(entries.map(f => {
return fs.rm(`${path}/${f}`, { force: true, recursive: true })
}))
}))
})
}

Expand Down
44 changes: 44 additions & 0 deletions mock-registry/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,50 @@ class MockRegistry {
...packument,
}
}

/**
* this is a simpler convience method for creating mockable registry with
* tarballs for specific versions
*/
async setup (packages) {
const format = Object.keys(packages).map(v => {
const [name, version] = v.split('@')
return { name, version }
}).reduce((acc, inc) => {
const exists = acc.find(pkg => pkg.name === inc.name)
if (exists) {
exists.tarballs = {
...exists.tarballs,
[inc.version]: packages[`${inc.name}@${inc.version}`],
}
} else {
acc.push({ name: inc.name,
tarballs: {
[inc.version]: packages[`${inc.name}@${inc.version}`],
},
})
}
return acc
}, [])
const registry = this
for (const pkg of format) {
const { name, tarballs } = pkg
const versions = Object.keys(tarballs)
const manifest = await registry.manifest({ name, versions })

for (const version of versions) {
const tarballPath = pkg.tarballs[version]
if (!tarballPath) {
throw new Error(`Tarball path not provided for version ${version}`)
}

await registry.tarball({
manifest: manifest.versions[version],
tarball: tarballPath,
})
}
}
}
}

module.exports = MockRegistry
161 changes: 161 additions & 0 deletions test/fixtures/mock-npm.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
const os = require('os')
const fs = require('fs').promises
const fsSync = require('fs')
const path = require('path')
const tap = require('tap')
const mockLogs = require('./mock-logs.js')
const mockGlobals = require('@npmcli/mock-globals')
const tmock = require('./tmock')
const MockRegistry = require('@npmcli/mock-registry')
const defExitCode = process.exitCode

const changeDir = (dir) => {
Expand Down Expand Up @@ -288,6 +290,165 @@ const setupMockNpm = async (t, {
}
}

const loadNpmWithRegistry = async (t, opts) => {
const mock = await setupMockNpm(t, opts)
const registry = new MockRegistry({
tap: t,
registry: mock.npm.config.get('registry'),
strict: true,
})

const fileShouldExist = (filePath) => {
t.equal(
fsSync.existsSync(path.join(mock.npm.prefix, filePath)), true, `${filePath} should exist`
)
}

const fileShouldNotExist = (filePath) => {
t.equal(
fsSync.existsSync(path.join(mock.npm.prefix, filePath)), false, `${filePath} should not exist`
)
}

const packageVersionMatches = (filePath, version) => {
t.equal(
JSON.parse(fsSync.readFileSync(path.join(mock.npm.prefix, filePath), 'utf8')).version, version
)
}

const assert = { fileShouldExist, fileShouldNotExist, packageVersionMatches }

return { registry, assert, ...mock }
}

/** breaks down a spec "[email protected]" into different parts for mocking */
function dep (spec, opt) {
const [name, version = '1.0.0'] = spec.split('@')
const lockPath = !opt.hoist && opt.parent ? `${opt.parent}/` : ''
const { definition } = opt

const depsMap = definition ? Object.entries(definition).map(([s, o]) => {
return dep(s, { ...o, parent: name })
}) : []
const dependencies = Object.assign({}, ...depsMap.map(d => d.asDependency))

const asPackageJSON = JSON.stringify({
name, version, ...(Object.keys(dependencies).length ? { dependencies } : {}),
})

const asDependency = {
[name]: version,
}

const asPackageLock = {
[`${lockPath}node_modules/${name}`]: {
version,
resolved: `https://registry.npmjs.org/${name}/-/${name}-${version}.tgz`,
},
}
const asPackage = {
'package.json': asPackageJSON,
'index.js': 'module.exports = "hello world"',
}

const asTarball = [`${name}@${version}`, asPackage]

const asDirtyModule = {
[name]: {
[`${name}@${version}.txt`]: '',
'package.json': asPackageJSON,
},
}

const asLockLink = {
[`node_modules/${name}`]: {
resolved: `${name}`,
link: true,
},
}

const asDepLock = depsMap.map(d => d.asPackageLock)
const asLocalLockEntry = { [name]: { version, dependencies } }

const asModule = {
[name]: {
node_modules: Object.assign({}, ...depsMap.map(d => d.hoist ? {} : d.asDirtyModule)),
...asPackage,
},
}

const asLocalizedDirty = lockPath ? {
...asDirtyModule,
} : {}

return {
...opt,
name,
version,
asTarball,
asPackage,
asLocalizedDirty,
asDirtyModule,
asPackageJSON,
asPackageLock,
asDependency,
asModule,
depsMap,
asLockLink,
asDepLock,
asLocalLockEntry,
}
}

function workspaceMock (t, opts) {
const { clean, workspaces } = { clean: true, ...opts }

const root = 'workspace-root'
const version = '1.0.0'
const names = Object.keys(workspaces)
const ws = Object.entries(workspaces).map(([name, definition]) => dep(name, { definition }))
const deps = ws.map(w => w.depsMap).flat()
const tarballs = Object.fromEntries(deps.map(flatDep => flatDep.asTarball))
const symlinks = Object.fromEntries(names.map((name) => {
return [name, t.fixture('symlink', `../${name}`)]
}))
const hoisted = Object.assign({}, ...deps.filter(d => d.hoist).map(d => d.asDirtyModule))
const workspaceFolders = Object.assign({}, ...ws.map(w => w.asModule))
const packageJSON = { name: root, version, workspaces: names }
const packageLockJSON = ({
name: root,
version,
lockfileVersion: 3,
requires: true,
packages: {
'': { name: root, version, workspaces: names },
...Object.assign({}, ...ws.map(d => d.asLockLink).flat()),
...Object.assign({}, ...ws.map(d => d.asDepLock).flat()),
...Object.assign({}, ...ws.map(d => d.asLocalLockEntry).flat()),
},
})

return {
tarballs,
node_modules: clean ? {} : {
...hoisted,
...symlinks,
},
'package-lock.json': JSON.stringify(packageLockJSON),
'package.json': JSON.stringify(packageJSON),
...Object.fromEntries(Object.entries(workspaceFolders).map(([_key, value]) => {
return [_key, Object.fromEntries(Object.entries(value).map(([key, valueTwo]) => {
if (key === 'node_modules' && clean) {
return [key, {}]
}
return [key, valueTwo]
}))]
})),
}
}

module.exports = setupMockNpm
module.exports.load = setupMockNpm
module.exports.loadNpmWithRegistry = loadNpmWithRegistry
module.exports.setGlobalNodeModules = setGlobalNodeModules
module.exports.workspaceMock = workspaceMock
File renamed without changes.
Loading