trigger.dev
Version:
A Command-Line Interface for Trigger.dev (v3) projects
308 lines • 12 kB
JavaScript
import { makeRe } from "minimatch";
import { mkdir, symlink } from "node:fs/promises";
import { dirname, join } from "node:path";
import { readPackageJSON, resolvePackageJSON } from "pkg-types";
import nodeResolve from "resolve";
import { alwaysExternal, } from "@trigger.dev/core/v3/build";
import { logger } from "../utilities/logger.js";
import { resolvePathSync as esmResolveSync } from "mlly";
/**
* externals in dev might not be resolvable from the worker directory
* for example, if the external is not an immediate dependency of the project
* and the project is not hoisting the dependency (e.g. pnpm, npm with nested)
*
* This function will create a symbolic link from a place where the external is resolvable
* to the actual resolved external path
*/
async function linkUnresolvableExternals(externals, resolveDir, logger) {
for (const external of externals) {
if (!(await isExternalResolvable(external, resolveDir, logger))) {
await linkExternal(external, resolveDir, logger);
}
}
}
async function linkExternal(external, resolveDir, logger) {
const destinationPath = join(resolveDir, "node_modules");
await mkdir(destinationPath, { recursive: true });
logger.debug("[externals] Make a symbolic link", {
fromPath: external.path,
destinationPath,
external,
});
// For scoped packages, we need to ensure the scope directory exists
if (external.name.startsWith("@")) {
// Get the scope part (e.g., '@huggingface')
const scopeDir = external.name.split("/")[0];
if (scopeDir) {
const scopePath = join(destinationPath, scopeDir);
logger.debug("[externals] Ensure scope directory exists", {
scopeDir,
scopePath,
});
await mkdir(scopePath, { recursive: true });
}
else {
logger.debug("[externals] Unable to get the scope directory", {
external,
});
}
}
const symbolicLinkPath = join(destinationPath, external.name);
// Make sure the symbolic link does not exist
try {
await symlink(external.path, symbolicLinkPath, "dir");
}
catch (e) {
logger.debug("[externals] Unable to create symbolic link", {
error: e,
fromPath: external.path,
destinationPath,
external,
});
}
}
async function isExternalResolvable(external, resolveDir, logger) {
try {
const resolvedPath = resolveSync(external.name, resolveDir);
logger.debug("[externals][isExternalResolvable] Resolved external", {
resolveDir,
external,
resolvedPath,
});
if (!resolvedPath.includes(external.path)) {
logger.debug("[externals][isExternalResolvable] resolvedPath does not match the external.path", {
resolveDir,
external,
resolvedPath,
});
return false;
}
return true;
}
catch (e) {
logger.debug("[externals][isExternalResolvable] Unable to resolve external", {
resolveDir,
external,
error: e,
});
return false;
}
}
function createExternalsCollector(target, resolvedConfig, forcedExternal = []) {
const externals = [];
const maybeExternals = discoverMaybeExternals(target, resolvedConfig, forcedExternal);
return {
externals,
plugin: {
name: "externals",
setup: (build) => {
build.onStart(async () => {
externals.splice(0);
});
build.onEnd(async () => {
logger.debug("[externals][onEnd] Collected externals", { externals });
});
maybeExternals.forEach((external) => {
build.onResolve({ filter: external.filter, namespace: "file" }, async (args) => {
// Check if the external is already in the externals collection
if (externals.find((e) => e.name === external.raw)) {
return {
external: true,
};
}
const packageName = packageNameForImportPath(args.path);
try {
const resolvedPath = resolveSync(packageName, args.resolveDir);
logger.debug("[externals][onResolve] Resolved external", {
external,
resolvedPath,
args,
packageName,
});
const packageJsonPath = await resolvePackageJSON(dirname(resolvedPath));
if (!packageJsonPath) {
return undefined;
}
logger.debug("[externals][onResolve] Found package.json", {
packageJsonPath,
external,
resolvedPath,
args,
packageName,
});
const packageJson = await readPackageJSON(packageJsonPath);
if (!packageJson || !packageJson.name) {
return undefined;
}
if (!external.filter.test(packageJson.name)) {
logger.debug("[externals][onResolve] Package name does not match", {
external,
packageJson,
resolvedPath,
packageName,
});
return undefined;
}
if (!packageJson.version) {
logger.debug("[externals][onResolve] No version found in package.json", {
external,
packageJson,
resolvedPath,
});
return undefined;
}
externals.push({
name: packageName,
path: dirname(packageJsonPath),
version: packageJson.version,
});
logger.debug("[externals][onResolve] adding external to the externals collection", {
external,
resolvedPath,
args,
packageName,
resolvedExternal: {
name: packageJson.name,
path: dirname(packageJsonPath),
version: packageJson.version,
},
});
return {
external: true,
};
}
catch (error) {
logger.debug("[externals][onResolve] Unable to resolve external", {
external,
error,
args,
packageName,
});
return undefined;
}
});
});
},
},
};
}
function discoverMaybeExternals(target, config, forcedExternal = []) {
const external = [];
for (const externalName of forcedExternal) {
const externalRegex = makeRe(externalName);
if (!externalRegex) {
continue;
}
external.push({
raw: externalName,
filter: new RegExp(`^${externalName}$|${externalRegex.source}`),
});
}
if (config.build?.external) {
for (const externalName of config.build?.external) {
const externalRegex = makeExternalRegexp(externalName);
if (!externalRegex) {
continue;
}
external.push({
raw: externalName,
filter: externalRegex,
});
}
}
for (const externalName of config.instrumentedPackageNames ?? []) {
const externalRegex = makeExternalRegexp(externalName);
if (!externalRegex) {
continue;
}
external.push({
raw: externalName,
filter: externalRegex,
});
}
for (const buildExtension of config.build?.extensions ?? []) {
const moduleExternals = buildExtension.externalsForTarget?.(target);
for (const externalName of moduleExternals ?? []) {
const externalRegex = makeExternalRegexp(externalName);
if (!externalRegex) {
continue;
}
external.push({
raw: externalName,
filter: externalRegex,
});
}
}
return external;
}
export function createExternalsBuildExtension(target, config, forcedExternal = []) {
const { externals, plugin } = createExternalsCollector(target, config, forcedExternal);
return {
name: "externals",
onBuildStart(context) {
context.registerPlugin(plugin, {
target,
// @ts-expect-error
placement: "$head", // cheat to get to the front of the plugins
});
},
onBuildComplete: async (context, manifest) => {
if (context.target === "dev") {
await linkUnresolvableExternals(externals, manifest.outputPath, context.logger);
}
context.addLayer({
id: "externals",
dependencies: externals.reduce((acc, external) => {
acc[external.name] = external.version;
return acc;
}, {}),
});
},
};
}
function makeExternalRegexp(packageName) {
// Escape special regex characters in the package name
const escapedPkg = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
// Create the regex pattern
const pattern = `^${escapedPkg}(?:/[^'"]*)?$`;
return new RegExp(pattern);
}
function packageNameForImportPath(importPath) {
// Remove any leading '@' to handle it separately
const withoutAtSign = importPath.replace(/^@/, "");
// Split the path by '/'
const parts = withoutAtSign.split("/");
// Handle scoped packages
if (importPath.startsWith("@")) {
// Return '@org/package' for scoped packages
return "@" + parts.slice(0, 2).join("/");
}
else {
// Return just the first part for non-scoped packages
return parts[0];
}
}
export async function resolveAlwaysExternal(client) {
try {
const response = await client.retrieveExternals();
if (response.success) {
return response.data.externals;
}
return alwaysExternal;
}
catch (error) {
logger.debug("[externals][resolveAlwaysExternal] Unable to retrieve externals", {
error,
});
return alwaysExternal;
}
}
function resolveSync(id, resolveDir) {
try {
return nodeResolve.sync(id, { basedir: resolveDir });
}
catch (error) {
return esmResolveSync(id, { url: resolveDir });
}
}
//# sourceMappingURL=externals.js.map