Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
77 changes: 63 additions & 14 deletions src/providers/base_javascript.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,18 +112,70 @@
}

/**
* Checks if a required lock file exists in the manifest directory or at the workspace root.
* When TRUSTIFY_DA_WORKSPACE_DIR is provided (via env var or opts),
* checks only that directory for the lock file.
* Walks up the directory tree from manifestDir looking for the lock file.
* Stops when the lock file is found, when a package.json with a "workspaces"
* field is encountered without a lock file (workspace root boundary), or
* when the filesystem root is reached.
*
* When TRUSTIFY_DA_WORKSPACE_DIR is set, checks only that directory (no walk-up).
*
* @param {string} manifestDir - The directory to start searching from
* @param {Object} [opts={}] - optional; may contain TRUSTIFY_DA_WORKSPACE_DIR
* @returns {string|null} The directory containing the lock file, or null
* @protected
*/
_isWorkspaceRoot(dir) {
if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml'))) {
return true
}
const pkgJsonPath = path.join(dir, 'package.json')
if (fs.existsSync(pkgJsonPath)) {
try {
const content = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'))
if (content.workspaces) {
return true
}
} catch (_) {
// ignore parse errors
}
}
return false
}

_findLockFileDir(manifestDir, opts = {}) {
const workspaceDir = getCustom('TRUSTIFY_DA_WORKSPACE_DIR', null, opts)
if (workspaceDir) {
const dir = path.resolve(workspaceDir)
return fs.existsSync(path.join(dir, this._lockFileName())) ? dir : null
}

let dir = path.resolve(manifestDir)
let parent = dir

do {
dir = parent

if (fs.existsSync(path.join(dir, this._lockFileName()))) {
return dir
}

if (this._isWorkspaceRoot(dir)) {
return null
}

parent = path.dirname(dir)
} while (parent !== dir)

return null
}

/**
* @param {string} manifestDir - The base directory where the manifest is located
* @param {{TRUSTIFY_DA_WORKSPACE_DIR?: string}} [opts={}] - optional workspace root
* @param {Object} [opts={}] - optional; may contain TRUSTIFY_DA_WORKSPACE_DIR
* @returns {boolean} True if the lock file exists
*/
validateLockFile(manifestDir, opts = {}) {
const workspaceDir = getCustom('TRUSTIFY_DA_WORKSPACE_DIR', null, opts)
const dirToCheck = workspaceDir ? path.resolve(workspaceDir) : manifestDir
const lock = path.join(dirToCheck, this._lockFileName())
return fs.existsSync(lock)
return this._findLockFileDir(manifestDir, opts) !== null

Check warning on line 178 in src/providers/base_javascript.js

View workflow job for this annotation

GitHub Actions / Lint and test project (24)

Expected '!=' and instead saw '!=='

Check warning on line 178 in src/providers/base_javascript.js

View workflow job for this annotation

GitHub Actions / Lint and test project (22)

Expected '!=' and instead saw '!=='
}

/**
Expand Down Expand Up @@ -188,8 +240,7 @@
_buildDependencyTree(includeTransitive, opts = {}) {
this._version();
const manifestDir = path.dirname(this.#manifest.manifestPath);
const workspaceDir = getCustom('TRUSTIFY_DA_WORKSPACE_DIR', null, opts)
const cmdDir = workspaceDir ? path.resolve(workspaceDir) : manifestDir;
const cmdDir = this._findLockFileDir(manifestDir, opts) || manifestDir;
this.#createLockFile(cmdDir);

let output = this.#executeListCmd(includeTransitive, cmdDir);
Expand Down Expand Up @@ -310,7 +361,7 @@
*/
#executeListCmd(includeTransitive, manifestDir) {
const listArgs = this._listCmdArgs(includeTransitive, manifestDir);
return this.#invokeCommand(listArgs);
return this.#invokeCommand(listArgs, { cwd: manifestDir });
}

/**
Expand All @@ -332,14 +383,12 @@
const isWindows = os.platform() === 'win32';

if (isWindows) {
// On Windows, --prefix flag doesn't work as expected
// Instead of installing from the prefix folder, it installs from current working directory
process.chdir(manifestDir);
}

try {
const args = this._updateLockFileCmdArgs(manifestDir);
this.#invokeCommand(args);
this.#invokeCommand(args, { cwd: manifestDir });
} finally {
if (isWindows) {
process.chdir(originalDir);
Expand Down
36 changes: 36 additions & 0 deletions test/providers/javascript.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ suite('testing the javascript-npm data provider', async () => {
[
{ name: 'npm/with_lock_file', validation: true },
{ name: 'npm/without_lock_file', validation: false },
{ name: 'npm/workspace_member_with_lock/packages/module-a', validation: true },
{ name: 'npm/workspace_member_without_lock/packages/module-a', validation: false },
{ name: 'pnpm/with_lock_file', validation: true },
{ name: 'pnpm/without_lock_file', validation: false },
{ name: 'yarn-classic/with_lock_file', validation: true },
Expand Down Expand Up @@ -168,4 +170,38 @@ suite('testing the javascript-npm data provider', async () => {
expect(provider.isSupported('package.json')).to.be.true
})

test('verify workspace member walks up and finds lock file at workspace root', () => {
const manifest = 'test/providers/provider_manifests/npm/workspace_member_with_lock/packages/module-a/package.json'
const provider = match(manifest, availableProviders)
expect(provider).to.not.be.null
expect(provider.isSupported('package.json')).to.be.true
})

test('verify workspace member throws when workspace root has no lock file', () => {
const manifest = 'test/providers/provider_manifests/npm/workspace_member_without_lock/packages/module-a/package.json'
expect(() => match(manifest, availableProviders))
.to.throw('package.json requires a lock file')
})

test('verify match with opts.TRUSTIFY_DA_WORKSPACE_DIR overrides walk-up for workspace member', () => {
const manifest = 'test/providers/provider_manifests/npm/workspace_member_with_lock/packages/module-a/package.json'
const opts = { TRUSTIFY_DA_WORKSPACE_DIR: 'test/providers/provider_manifests/npm/workspace_member_with_lock' }
const provider = match(manifest, availableProviders, opts)
expect(provider).to.not.be.null
expect(provider.isSupported('package.json')).to.be.true
})

test('verify pnpm workspace member stops at pnpm-workspace.yaml boundary', () => {
const manifest = 'test/providers/provider_manifests/pnpm/workspace_member_without_lock/packages/module-a/package.json'
expect(() => match(manifest, availableProviders))
.to.throw('package.json requires a lock file')
})

test('verify match with wrong TRUSTIFY_DA_WORKSPACE_DIR fails even when walk-up would succeed', () => {
const manifest = 'test/providers/provider_manifests/npm/workspace_member_with_lock/packages/module-a/package.json'
const opts = { TRUSTIFY_DA_WORKSPACE_DIR: 'test/providers/provider_manifests/npm/workspace_member_without_lock' }
expect(() => match(manifest, availableProviders, opts))
.to.throw('package.json requires a lock file')
})

}).beforeAll(() => clock = useFakeTimers(new Date('2023-08-07T00:00:00.000Z'))).afterAll(() => clock.restore());

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "test-workspace-root",
"private": true,
"workspaces": ["packages/*"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "module-a",
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "test-workspace-root",
"private": true,
"workspaces": ["packages/*"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "module-a",
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "test-pnpm-workspace-root",
"private": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "module-a",
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
packages:
- 'packages/*'
Loading