Monorepo packages automatic handling (#1208)

* Add fixture

* Update fixture name

* Update fixture to work with npm & pnpm

* Update setup/teardown and README

* Add support for monorepo to e2e suite utilities

* Use direct dependencies info in esbuild plugin

* Renaming

* Add support for yarn

* Add fixture for npm

* Remove console.log call

* Edge cases

* Remove unused options

* Fix error unknown yarn in CI

* Fix logger debug call's error field

* Add changeset

* Fix missing span end
This commit is contained in:
Émile Ré
2024-07-23 14:02:48 +04:00
committed by GitHub
parent 6e65591e87
commit ae9a8b0ceb
28 changed files with 5338 additions and 69 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Automatically bundle internal packages that use file, link or workspace protocl
+3
View File
@@ -42,6 +42,9 @@ jobs:
- name: 🔧 Build worker template files
run: pnpm --filter trigger.dev run build:workers
- name: Enable corepack
run: corepack enable
- name: Run E2E Tests
run: |
PM=${{ matrix.package-manager }} pnpm --filter trigger.dev run test:e2e
+2 -2
View File
@@ -117,9 +117,9 @@ This will test your fixture project, and generate outputs in the `packages/cli-v
```sh
cd packages/cli-v3/e2e/fixtures/<fixture-name>
rm -rf node_modules
rm -rf **/node_modules
npm install
rm -rf node_modules
rm -rf **/node_modules
corepack use yarn@4.2.2 # will update the yarn lockfile
```
+27 -5
View File
@@ -2,7 +2,7 @@ import { esbuildDecorators } from "@anatine/esbuild-decorators";
import { build } from "esbuild";
import { readFileSync } from "node:fs";
import { writeFile } from "node:fs/promises";
import { basename, join, posix, resolve } from "node:path";
import { basename, join, posix, relative, resolve, sep } from "node:path";
import invariant from "tiny-invariant";
import {
@@ -15,9 +15,12 @@ import { writeJSONFile } from "../src/utilities/fileSystem.js";
import { logger } from "../src/utilities/logger.js";
import { createTaskFileImports, gatherTaskFiles } from "../src/utilities/taskFiles.js";
import { escapeImportPath } from "../src/utilities/windows.js";
import { E2EJavascriptProject } from "./javascriptProject.js";
import { PackageManager } from "../src/utilities/getUserPackageManager.js";
type CompileOptions = {
outputMetafile?: string;
packageManager: PackageManager;
resolvedConfig: ReadConfigResult;
tempDir: string;
};
@@ -28,6 +31,7 @@ export async function compile(options: CompileOptions) {
}
const {
packageManager,
tempDir,
resolvedConfig: { config },
} = options;
@@ -61,6 +65,9 @@ export async function compile(options: CompileOptions) {
);
}
const e2eJsProject = new E2EJavascriptProject(config.projectDir, packageManager);
const directDependenciesMeta = await e2eJsProject.extractDirectDependenciesMeta();
const result = await build({
stdin: {
contents: workerContents,
@@ -86,7 +93,12 @@ export async function compile(options: CompileOptions) {
},
plugins: [
mockServerOnlyPlugin(),
bundleDependenciesPlugin("workerFacade", config.dependenciesToBundle, config.tsconfigPath),
bundleDependenciesPlugin(
"workerFacade",
directDependenciesMeta,
config.dependenciesToBundle,
config.tsconfigPath
),
workerSetupImportConfigPlugin(configPath),
esbuildDecorators({
tsconfig: config.tsconfigPath,
@@ -127,7 +139,12 @@ export async function compile(options: CompileOptions) {
__PROJECT_CONFIG__: JSON.stringify(config),
},
plugins: [
bundleDependenciesPlugin("entryPoint.ts", config.dependenciesToBundle, config.tsconfigPath),
bundleDependenciesPlugin(
"entryPoint.ts",
directDependenciesMeta,
config.dependenciesToBundle,
config.tsconfigPath
),
],
});
@@ -145,9 +162,14 @@ export async function compile(options: CompileOptions) {
logger.debug(`Writing compiled files to ${tempDir}`);
// Get the metaOutput for the result build
const pathsToProjectDir = relative(
join(process.cwd(), "e2e", "fixtures"),
config.projectDir
).split(sep);
const metaOutput =
result.metafile!.outputs[
posix.join("e2e", "fixtures", basename(config.projectDir), "out", "stdin.js")
posix.join("e2e", "fixtures", ...pathsToProjectDir, "out", "stdin.js")
];
invariant(metaOutput, "Meta output for the result build is missing");
@@ -155,7 +177,7 @@ export async function compile(options: CompileOptions) {
// Get the metaOutput for the entryPoint build
const entryPointMetaOutput =
entryPointResult.metafile!.outputs[
posix.join("e2e", "fixtures", basename(config.projectDir), "out", "stdin.js")
posix.join("e2e", "fixtures", ...pathsToProjectDir, "out", "stdin.js")
];
invariant(entryPointMetaOutput, "Meta output for the entryPoint build is missing");
+11
View File
@@ -1,6 +1,7 @@
export interface TestCase {
resolveEnv?: { [key: string]: string };
id: string;
workspaceRelativeDir?: string;
skipTypecheck?: boolean;
wantConfigNotFoundError?: boolean;
wantConfigInvalidError?: boolean;
@@ -11,6 +12,16 @@ export interface TestCase {
}
export const fixturesConfig: TestCase[] = [
{
id: "compile-monorepo-packages",
skipTypecheck: true,
workspaceRelativeDir: "packages/trigger",
},
{
id: "compile-monorepo-packages-npm",
skipTypecheck: true,
workspaceRelativeDir: "packages/trigger",
},
{
id: "config-infisical-sdk",
skipTypecheck: true,
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,12 @@
{
"name": "compile-monorepo-packages-npm",
"private": true,
"packageManager": "yarn@4.2.2+sha256.1aa43a5304405be7a7cb9cb5de7b97de9c4e8ddd3273e4dad00d6ae3eb39f0ef",
"engines": {
"pnpm": "8.15.5",
"yarn": "4.2.2"
},
"workspaces": [
"packages/*"
]
}
@@ -0,0 +1,3 @@
module.exports = {
MESSAGE: "Hello, World!",
};
@@ -0,0 +1,4 @@
{
"name": "@compile-monorepo-packages-npm/message",
"private": true
}
@@ -0,0 +1,8 @@
{
"name": "@compile-monorepo-packages-npm/trigger",
"private": true,
"dependencies": {
"@compile-monorepo-packages-npm/message": "*",
"@trigger.dev/sdk": "3.0.0-beta.40"
}
}
@@ -0,0 +1,9 @@
import { task } from "@trigger.dev/sdk/v3";
import { MESSAGE } from "@compile-monorepo-packages-npm/message";
export const helloWorldTask = task({
id: "hello-world",
run: async (payload) => {
console.log(MESSAGE, payload);
},
});
@@ -0,0 +1,4 @@
export const config = {
project: "compile-monorepo-packages",
triggerDirectories: ["./src"],
};
@@ -0,0 +1 @@
nodeLinker: node-modules
@@ -0,0 +1,12 @@
{
"name": "compile-monorepo-packages",
"private": true,
"packageManager": "pnpm@8.15.5+sha256.4b4efa12490e5055d59b9b9fc9438b7d581a6b7af3b5675eb5c5f447cee1a589",
"engines": {
"pnpm": "8.15.5",
"yarn": "4.2.2"
},
"workspaces": [
"packages/*"
]
}
@@ -0,0 +1,3 @@
module.exports = {
MESSAGE: "Hello, World!",
};
@@ -0,0 +1,4 @@
{
"name": "@compile-monorepo-packages/message",
"private": true
}
@@ -0,0 +1,8 @@
{
"name": "@compile-monorepo-packages/trigger",
"private": true,
"dependencies": {
"@compile-monorepo-packages/message": "workspace:*",
"@trigger.dev/sdk": "3.0.0-beta.40"
}
}
@@ -0,0 +1,9 @@
import { task } from "@trigger.dev/sdk/v3";
import { MESSAGE } from "@compile-monorepo-packages/message";
export const helloWorldTask = task({
id: "hello-world",
run: async (payload) => {
console.log(MESSAGE, payload);
},
});
@@ -0,0 +1,4 @@
export const config = {
project: "compile-monorepo-packages",
triggerDirectories: ["./src"],
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,3 @@
# https://github.com/pnpm/pnpm/issues/2412
packages:
- "packages/*"
File diff suppressed because it is too large Load Diff
+2 -15
View File
@@ -13,9 +13,9 @@ import {
import { ReadConfigResult } from "../src/utilities/configFiles.js";
import { writeJSONFile } from "../src/utilities/fileSystem.js";
import { PackageManager } from "../src/utilities/getUserPackageManager.js";
import { JavascriptProject } from "../src/utilities/javascriptProject.js";
import { logger } from "../src/utilities/logger.js";
import { cliLink } from "../src/utilities/cliOutput.js";
import { E2EJavascriptProject } from "./javascriptProject.js";
type HandleDependenciesOptions = {
entryPointMetaOutput: Metafile["outputs"]["out/stdin.js"];
@@ -25,19 +25,6 @@ type HandleDependenciesOptions = {
tempDir: string;
};
class JavascriptProjectLocal extends JavascriptProject {
constructor(
projectPath: string,
private overridenPackageManager: PackageManager
) {
super(projectPath);
}
async getPackageManager(): Promise<PackageManager> {
return Promise.resolve(this.overridenPackageManager);
}
}
export async function handleDependencies(options: HandleDependenciesOptions) {
if (options.resolvedConfig.status === "error") {
throw new Error("cannot resolve config");
@@ -58,7 +45,7 @@ export async function handleDependencies(options: HandleDependenciesOptions) {
// Get all the required dependencies from the metaOutputs and save them to /tmp/dir/package.json
const allImports = [...metaOutput.imports, ...entryPointMetaOutput.imports];
const javascriptProject = new JavascriptProjectLocal(config.projectDir, packageManager);
const javascriptProject = new E2EJavascriptProject(config.projectDir, packageManager);
const dependencies = await resolveRequiredDependencies(allImports, config, javascriptProject);
+66 -26
View File
@@ -19,11 +19,13 @@ import { handleDependencies } from "./handleDependencies";
import { E2EOptions, E2EOptionsSchema } from "./schemas";
import { fixturesConfig, TestCase } from "./fixtures.config";
import { Metafile, OutputFile } from "esbuild";
import { findUpMultiple } from "find-up";
interface E2EFixtureTest extends TestCase {
dir: string;
fixtureDir: string;
packageManager: PackageManager;
tempDir: string;
workspaceDir: string;
}
const TIMEOUT = 120_000;
@@ -49,35 +51,47 @@ logger.loggerLevel = options.logLevel;
if (testCases.length > 0) {
describe.concurrent("bundling", async () => {
beforeEach<E2EFixtureTest>(async ({ dir, packageManager, skip }) => {
await rimraf(join(dir, "**/node_modules/**"), {
beforeEach<E2EFixtureTest>(async ({ fixtureDir, packageManager, skip, workspaceDir }) => {
await rimraf(join(workspaceDir, "**/node_modules"), {
glob: true,
});
await rimraf(join(dir, ".yarn"), { glob: true });
await rimraf(join(workspaceDir, ".yarn"), { glob: true });
if (
packageManager === "npm" &&
(existsSync(resolve(join(dir, "yarn.lock"))) ||
existsSync(resolve(join(dir, "yarn.lock.copy"))))
(existsSync(resolve(join(workspaceDir, "yarn.lock"))) ||
existsSync(resolve(join(workspaceDir, "yarn.lock.copy"))))
) {
// `npm ci` & `npm install` will update an existing yarn.lock
try {
await rename(resolve(join(dir, "yarn.lock")), resolve(join(dir, "yarn.lock.copy")));
await rename(
resolve(join(workspaceDir, "yarn.lock")),
resolve(join(workspaceDir, "yarn.lock.copy"))
);
} catch (e) {
await rename(resolve(join(dir, "yarn.lock.copy")), resolve(join(dir, "yarn.lock")));
await rename(
resolve(join(workspaceDir, "yarn.lock.copy")),
resolve(join(workspaceDir, "yarn.lock"))
);
}
}
if (options.packageManager && !existsSync(resolve(dir, LOCKFILES[options.packageManager]))) {
if (
options.packageManager &&
!existsSync(resolve(fixtureDir, LOCKFILES[options.packageManager]))
) {
skip();
}
await installFixtureDeps(dir, packageManager);
await installFixtureDeps({ fixtureDir, packageManager, workspaceDir });
}, TIMEOUT);
afterEach<E2EFixtureTest>(async ({ dir, packageManager }) => {
afterEach<E2EFixtureTest>(async ({ packageManager, workspaceDir }) => {
if (packageManager === "npm") {
try {
await rename(resolve(join(dir, "yarn.lock.copy")), resolve(join(dir, "yarn.lock")));
await rename(
resolve(join(workspaceDir, "yarn.lock.copy")),
resolve(join(workspaceDir, "yarn.lock"))
);
} catch {}
}
@@ -87,21 +101,24 @@ if (testCases.length > 0) {
for (let testCase of testCases) {
test.extend<E2EFixtureTest>({
...testCase,
dir: async ({ id }, use) => await use(resolve(join(process.cwd(), "e2e/fixtures", id))),
packageManager: async ({ dir }, use) => await use(await parsePackageManager(options, dir)),
tempDir: async ({ dir }, use) => {
const existingTempDir = resolve(join(dir, ".trigger"));
fixtureDir: async ({ id }, use) =>
await use(resolve(join(process.cwd(), "e2e/fixtures", id))),
workspaceDir: async ({ fixtureDir, workspaceRelativeDir = "" }, use) =>
await use(resolve(join(fixtureDir, workspaceRelativeDir))),
packageManager: async ({ workspaceDir }, use) =>
await use(await parsePackageManager(options, workspaceDir)),
tempDir: async ({ workspaceDir }, use) => {
const existingTempDir = resolve(join(workspaceDir, ".trigger"));
if (existsSync(existingTempDir)) {
await rm(existingTempDir, { force: true, recursive: true });
}
await use((await mkdir(join(dir, ".trigger"), { recursive: true })) as string);
await use((await mkdir(join(workspaceDir, ".trigger"), { recursive: true })) as string);
},
})(
`fixture '${testCase.id}'`,
{ timeout: TIMEOUT },
async ({
dir,
packageManager,
resolveEnv,
skipTypecheck,
@@ -112,11 +129,12 @@ if (testCases.length > 0) {
wantDependenciesError,
wantInstallationError,
wantWorkerError,
workspaceDir,
}) => {
let resolvedConfig: ReadConfigResult;
const configExpect = expect(
(async () => {
resolvedConfig = await readConfig(dir, { cwd: dir });
resolvedConfig = await readConfig(workspaceDir, { cwd: workspaceDir });
})(),
wantConfigNotFoundError || wantConfigInvalidError
? "does not resolve config"
@@ -153,6 +171,7 @@ if (testCases.length > 0) {
const compileExpect = expect(
(async () => {
const compilationResult = await compile({
packageManager,
resolvedConfig: resolvedConfig!,
tempDir,
});
@@ -284,27 +303,48 @@ function debug(message: string) {
}
}
async function installFixtureDeps(dir: string, packageManager: PackageManager) {
async function installFixtureDeps(options: {
fixtureDir: string;
packageManager: PackageManager;
workspaceDir: string;
}) {
const { packageManager, workspaceDir } = options;
if (["pnpm", "yarn"].includes(packageManager)) {
const buffer = readFileSync(resolve(join(dir, "package.json")), "utf8");
const pkgJSON = JSON.parse(buffer.toString());
const version = pkgJSON.engines[packageManager];
const version = await detectPackageManagerVersion(options);
debug(`Detected ${packageManager}@${version} from package.json 'engines' field`);
const { stdout, stderr } = await execa("corepack", ["use", `${packageManager}@${version}`], {
cwd: dir,
cwd: workspaceDir,
});
debug(stdout);
if (stderr) console.error(stderr);
} else {
const { stdout, stderr } = await execa(packageManager, installArgs(packageManager), {
cwd: dir,
NODE_PATH: resolve(join(dir, "node_modules")),
cwd: workspaceDir,
NODE_PATH: resolve(join(workspaceDir, "node_modules")),
});
debug(stdout);
if (stderr) console.error(stderr);
}
}
async function detectPackageManagerVersion(options: {
fixtureDir: string;
packageManager: PackageManager;
workspaceDir: string;
}): Promise<string> {
const { fixtureDir, packageManager, workspaceDir } = options;
const pkgPaths = await findUpMultiple("package.json", { cwd: workspaceDir, stopAt: fixtureDir });
for (let pkgPath of pkgPaths) {
const buffer = readFileSync(pkgPath, "utf8");
const pkgJSON = JSON.parse(buffer.toString());
if (!pkgJSON.engines) continue;
const version = pkgJSON.engines[packageManager];
if (version) return version;
}
throw new Error(`No version found for package manager ${packageManager}`);
}
function installArgs(packageManager: string) {
switch (packageManager) {
case "bun":
+15
View File
@@ -0,0 +1,15 @@
import { PackageManager } from "../src/utilities/getUserPackageManager.js";
import { JavascriptProject } from "../src/utilities/javascriptProject.js";
export class E2EJavascriptProject extends JavascriptProject {
constructor(
projectPath: string,
private overridenPackageManager: PackageManager
) {
super(projectPath);
}
async getPackageManager(): Promise<PackageManager> {
return Promise.resolve(this.overridenPackageManager);
}
}
+5
View File
@@ -1170,6 +1170,9 @@ async function compileProject(
);
}
const jsProject = new JavascriptProject(config.projectDir);
const directDependenciesMeta = await jsProject.extractDirectDependenciesMeta();
const result = await build({
stdin: {
contents: workerContents,
@@ -1197,6 +1200,7 @@ async function compileProject(
mockServerOnlyPlugin(),
bundleDependenciesPlugin(
"workerFacade",
directDependenciesMeta,
config.dependenciesToBundle,
config.tsconfigPath
),
@@ -1253,6 +1257,7 @@ async function compileProject(
plugins: [
bundleDependenciesPlugin(
"entryPoint.ts",
directDependenciesMeta,
config.dependenciesToBundle,
config.tsconfigPath
),
+6
View File
@@ -5,6 +5,7 @@ import { extname, isAbsolute } from "node:path";
import tsConfigPaths from "tsconfig-paths";
import { logger } from "./logger";
import { escapeImportPath } from "./windows";
import { DependencyMeta } from "./javascriptProject";
export function mockServerOnlyPlugin(): Plugin {
return {
@@ -106,6 +107,7 @@ export function workerSetupImportConfigPlugin(configPath?: string): Plugin {
export function bundleDependenciesPlugin(
buildIdentifier: string,
dependencies: Record<string, DependencyMeta>,
dependenciesToBundle?: Array<string | RegExp>,
tsconfigPath?: string
): Plugin {
@@ -149,6 +151,10 @@ export function bundleDependenciesPlugin(
}
}
if (dependencies[args.path] && !dependencies[args.path]!.external) {
return undefined; // let esbuild bundle it
}
logger.debug(`[${buildIdentifier}] Externalizing ${args.path}`, {
...args,
});
@@ -11,6 +11,7 @@ import { recordSpanException } from "@trigger.dev/core/v3/otel";
import { flattenAttributes } from "@trigger.dev/core/v3";
export type ResolveOptions = { allowDev: boolean };
export type DependencyMeta = { version: string; external: boolean };
export class JavascriptProject {
private _packageJson?: PackageJson;
@@ -94,15 +95,39 @@ export class JavascriptProject {
}
}
async resolveAll(
packageNames: string[],
options?: ResolveOptions
): Promise<Record<string, string>> {
async extractDirectDependenciesMeta(): Promise<Record<string, DependencyMeta>> {
return tracer.startActiveSpan(
"JavascriptProject.extractDirectDependenciesMeta",
async (span) => {
const command = await this.#getCommand();
span.setAttributes({
packageManager: command.name,
});
try {
span.end();
return await command.extractDirectDependenciesMeta({
cwd: this.projectPath,
});
} catch (error) {
recordSpanException(span, error);
span.end();
logger.debug(`Failed to resolve internal dependencies using ${command.name}`, {
error,
});
throw error;
}
}
);
}
async resolveAll(packageNames: string[]): Promise<Record<string, string>> {
return tracer.startActiveSpan("JavascriptProject.resolveAll", async (span) => {
const externalPackages = packageNames.filter((packageName) => !isBuiltInModule(packageName));
const opts = { allowDev: false, ...options };
const command = await this.#getCommand();
span.setAttributes({
@@ -139,18 +164,6 @@ export class JavascriptProject {
missingPackageVersions[packageName] = packageJsonVersion;
}
if (opts.allowDev) {
const devPackageJsonVersion = this.packageJson.devDependencies?.[packageName];
if (typeof devPackageJsonVersion === "string") {
logger.debug(`Resolved ${packageName} version using devDependencies`, {
devPackageJsonVersion,
});
missingPackageVersions[packageName] = devPackageJsonVersion;
}
}
}
span.setAttributes({
@@ -271,6 +284,10 @@ interface PackageManagerCommands {
installDependencies(options: PackageManagerOptions): Promise<void>;
extractDirectDependenciesMeta(
options: PackageManagerOptions
): Promise<Record<string, DependencyMeta>>;
resolveDependencyVersion(
packageName: string,
options: PackageManagerOptions
@@ -337,6 +354,46 @@ class PNPMCommands implements PackageManagerCommands {
return results;
}
async extractDirectDependenciesMeta(options: PackageManagerOptions) {
const result = await this.#listDirectDependencies(options);
logger.debug(`Extracting direct dependencies metadata using ${this.name}`);
const results: Record<string, DependencyMeta> = {};
for (const projectPkg of result) {
results[projectPkg.name] = { version: projectPkg.version, external: false };
if (projectPkg.dependencies) {
for (const [name, dep] of Object.entries(projectPkg.dependencies)) {
const { version } = dep;
results[name] = {
version,
external: !version.startsWith("link:"),
};
}
}
}
return results;
}
async #listDirectDependencies(options: PackageManagerOptions) {
const childProcess = await $({
cwd: options.cwd,
reject: false,
})`${this.cmd} list --recursive --json`;
if (childProcess.failed) {
logger.debug("Failed to list dependencies, using stdout anyway...", {
error: childProcess,
});
}
return JSON.parse(childProcess.stdout) as PnpmList;
}
async #listDependencies(packageNames: string[], options: PackageManagerOptions) {
const childProcess = await $({
cwd: options.cwd,
@@ -345,7 +402,7 @@ class PNPMCommands implements PackageManagerCommands {
if (childProcess.failed) {
logger.debug("Failed to list dependencies, using stdout anyway...", {
error: childProcess.stderr,
error: childProcess,
});
}
@@ -410,6 +467,31 @@ class NPMCommands implements PackageManagerCommands {
return results;
}
async extractDirectDependenciesMeta(
options: PackageManagerOptions
): Promise<Record<string, DependencyMeta>> {
const result = await this.#listDirectDependencies(options);
logger.debug(`Extracting direct dependencies metadata using ${this.name}`);
return result.dependencies ? this.#flattenDependenciesMeta(result.dependencies) : {};
}
async #listDirectDependencies(options: PackageManagerOptions) {
const childProcess = await $({
cwd: options.cwd,
reject: false,
})`${this.cmd} list --json`;
if (childProcess.failed) {
logger.debug("Failed to list dependencies, using stdout anyway...", {
error: childProcess,
});
}
return JSON.parse(childProcess.stdout) as NpmListOutput;
}
async #listDependencies(packageNames: string[], options: PackageManagerOptions) {
const childProcess = await $({
cwd: options.cwd,
@@ -418,7 +500,7 @@ class NPMCommands implements PackageManagerCommands {
if (childProcess.failed) {
logger.debug("Failed to list dependencies, using stdout anyway...", {
error: childProcess.stderr,
error: childProcess,
});
}
@@ -443,6 +525,23 @@ class NPMCommands implements PackageManagerCommands {
}
}
}
#flattenDependenciesMeta(
dependencies: Record<string, NpmDependency>
): Record<string, DependencyMeta> {
let results: Record<string, DependencyMeta> = {};
for (const [name, dep] of Object.entries(dependencies)) {
const { version, resolved, dependencies: children } = dep;
results[name] = { version, external: !!resolved && !resolved.startsWith("file:") };
if (children) {
results = { ...results, ...this.#flattenDependenciesMeta(children) };
}
}
return results;
}
}
class YarnCommands implements PackageManagerCommands {
@@ -501,6 +600,39 @@ class YarnCommands implements PackageManagerCommands {
return results;
}
async extractDirectDependenciesMeta(options: PackageManagerOptions) {
const result = await this.#listDirectDependencies(options);
const rawPackagesData = result.split("\n");
logger.debug(`Extracting direct dependencies metadata using ${this.name}`);
const results: Record<string, DependencyMeta> = {};
for (const rawPackageData of rawPackagesData) {
const packageData = JSON.parse(rawPackageData);
const [name, dependencyMeta] = this.#parseYarnValueIntoDependencyMeta(packageData.value);
results[name] = dependencyMeta;
}
return results;
}
async #listDirectDependencies(options: PackageManagerOptions) {
const childProcess = await $({
cwd: options.cwd,
reject: false,
})`${this.cmd} info --all --json`;
if (childProcess.failed) {
logger.debug("Failed to list dependencies, using stdout anyway...", {
error: childProcess,
});
}
return childProcess.stdout;
}
async #listDependencies(packageNames: string[], options: PackageManagerOptions) {
const childProcess = await $({
cwd: options.cwd,
@@ -509,7 +641,7 @@ class YarnCommands implements PackageManagerCommands {
if (childProcess.failed) {
logger.debug("Failed to list dependencies, using stdout anyway...", {
error: childProcess.stderr,
error: childProcess,
});
}
@@ -531,6 +663,31 @@ class YarnCommands implements PackageManagerCommands {
// If the value contains an "@" symbol, then the package name is the first part
return parts[0] as string;
}
#parseYarnValueIntoDependencyMeta(value: string): [string, DependencyMeta] {
const parts = value.split("@");
let name: string, protocol: string, version: string;
if (parts.length === 3) {
// e.g. @<scope>/<package>@<protocol>:<version> -> ["", "<scope>/<package>"", "<protocol>:<version>""]
name = `@${parts[1]}`;
[protocol = "", version = ""] = parts[2]!.split(":");
} else if (parts.length === 2) {
// e.g. <package>@<protocol>:<version> -> ["<package>"", "<protocol>:<version>""]
name = parts[0]!.toString();
[protocol = "", version = ""] = parts[1]!.split(":");
} else {
throw new Error("Failed parsing ${value} into dependency meta");
}
return [
name,
{
version,
external: protocol !== "workspace" && protocol !== "file",
},
];
}
}
function isBuiltInModule(module: string): boolean {