UNPKG

trigger.dev

Version:

A Command-Line Interface for Trigger.dev (v3) projects

308 lines • 12 kB
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