mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
bf9662172a
* fix: resolve extensionless bun relative imports on windows loader * fix: resolve local module probe against importer dir not job root * fix: keep node_modules-internal relative imports out of windmill resolver on windows
180 lines
8.2 KiB
JavaScript
180 lines
8.2 KiB
JavaScript
// Injected by backend: maps normalized paths to temp storage hashes (or null)
|
|
const TEMP_SCRIPT_REFS = TEMP_SCRIPT_REFS_PLACEHOLDER;
|
|
|
|
// Windows-specific bun loader that uses a virtual "windmill-url" namespace instead
|
|
// of writing .url files to disk. This avoids Windows path issues (backslashes in
|
|
// resolve(), 8.3 short filenames, drive letter prefixes). The virtual namespace
|
|
// approach is likely better on all fronts but we keep the original .url-file loader
|
|
// on Linux to avoid breaking back-compat.
|
|
const p = {
|
|
name: "windmill-relative-resolver",
|
|
async setup(build) {
|
|
const { readFileSync } = await import("fs");
|
|
const { resolve, dirname } = await import("node:path");
|
|
|
|
const base_internal_url = "BASE_INTERNAL_URL".replace(
|
|
"localhost",
|
|
"127.0.0.1"
|
|
);
|
|
|
|
const w_id = "W_ID";
|
|
const current_path = "CURRENT_PATH";
|
|
const token = "TOKEN";
|
|
|
|
const cdir = resolve("./");
|
|
const cdirNoPrivate = cdir.replace(/^\/private/, ""); // for macos
|
|
// Normalize path to forward slashes to match Bun's resolver output on Windows
|
|
const cdirFwd = cdir.replace(/\\/g, "/");
|
|
const cdirPosix = cdirFwd.replace(/^[a-zA-Z]:/, "");
|
|
// Match either an already-normalized `.ts` specifier OR an *extensionless*
|
|
// windmill relative/workspace import (`./`, `../`, `f/`, `/f/`, `u/`, `/u/`).
|
|
// The extensionless branch is essential on Windows: the `main.ts` onLoad that
|
|
// would rewrite `./mid` → `./mid.ts` only fires when its path filter matches
|
|
// bun's resolver output, and the 8.3 short-name (`RUNNER~1`) vs canonical-path
|
|
// mismatch makes that unreliable — so bare imports must be resolvable here
|
|
// directly rather than depending on the rewrite. The `(?!.*\.[A-Za-z0-9]+$)`
|
|
// guard keeps extension-bearing relative imports (a package's internal
|
|
// `./foo.js`/`./x.json` requires) OUT of this resolver so they fall through
|
|
// to bun's default resolver — a windmill script import is always `.ts` or
|
|
// extensionless. Bare npm specifiers (no `./` prefix, not `f/`/`u/`) never
|
|
// match either branch.
|
|
const filterResolve = new RegExp(
|
|
`^(?!\\.\/main\\.ts)(?!\\.\/_wm_)(?!${cdirFwd}\/main\\.ts)(?!${cdirFwd}\/_wm_)(?!${cdirPosix}\/main\\.ts)(?!${cdirPosix}\/_wm_)(?!(?:/private)?${cdirNoPrivate}\/wrapper\\.mjs)(?:.*\\.ts|(?:\\.\\.?\/|\/?f\/|\/?u\/)(?!.*\\.[A-Za-z0-9]+$).*)$`
|
|
);
|
|
|
|
// Match the entry main.ts against bun's forward-slash resolver output on
|
|
// Windows — raw `cdir` carries backslashes that corrupt the regex, so the
|
|
// onLoad below would never fire and extensionless relative imports in
|
|
// main.ts would slip past filterResolve unrewritten.
|
|
const filterLoad = new RegExp(`^(?:${cdirFwd}|${cdirPosix})\/main\\.ts$`);
|
|
const transpiler = new Bun.Transpiler({
|
|
loader: "ts",
|
|
});
|
|
|
|
function replaceRelativeImports(code) {
|
|
const imports = transpiler.scanImports(code);
|
|
for (const imp of imports) {
|
|
if (imp.kind == "import-statement") {
|
|
if (
|
|
(imp.path.startsWith(".") ||
|
|
imp.path.startsWith("/u/") ||
|
|
imp.path.startsWith("/f/")) &&
|
|
!imp.path.endsWith(".ts")
|
|
) {
|
|
code = code.replaceAll(imp.path, imp.path + ".ts");
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
contents: code,
|
|
};
|
|
}
|
|
|
|
function normalizePath(rawPath) {
|
|
return rawPath.split("/").reduce((acc, seg) => {
|
|
if (seg === "..") acc.pop();
|
|
else if (seg !== "." && seg !== "") acc.push(seg);
|
|
return acc;
|
|
}, []).join("/");
|
|
}
|
|
|
|
// Resolve a windmill script import path relative to an importer path.
|
|
// Bun on Windows may prefix args with "windmill-url:" or strip leading "/".
|
|
function resolveWindmillImport(importerPath, importPath) {
|
|
const path = importPath.replace(/^windmill-url:/, "").replace(/^\//, "");
|
|
const isAbsolute = path.startsWith("f/") || path.startsWith("u/");
|
|
const endExt = path.endsWith(".ts") ? "" : ".ts";
|
|
const rawScriptPath = isAbsolute
|
|
? `${path}${endExt}`
|
|
: `${importerPath}/../${path}${endExt}`;
|
|
const normalized = normalizePath(rawScriptPath);
|
|
// Look up temp script hash (keys are extensionless paths)
|
|
const lookupPath = normalized.replace(/\.ts$/, "");
|
|
const hash = TEMP_SCRIPT_REFS?.[lookupPath];
|
|
// Encode hash in the path so onLoad can extract it and append to fetch URL
|
|
const resolvedPath = hash ? `${normalized}?temp_script_hash=${hash}` : normalized;
|
|
return { path: resolvedPath, namespace: "windmill-url" };
|
|
}
|
|
|
|
build.onLoad({ filter: filterLoad }, async (args) => {
|
|
const code = readFileSync(args.path, "utf8");
|
|
return replaceRelativeImports(code);
|
|
});
|
|
|
|
// Load windmill scripts by fetching from the API
|
|
build.onLoad({ filter: /.*/, namespace: "windmill-url" }, async (args) => {
|
|
// Extract temp_script_hash if embedded in the path by resolveWindmillImport
|
|
const [scriptPath, queryString] = args.path.replace(/^windmill-url:/, "").split("?");
|
|
const hashParam = queryString?.startsWith("temp_script_hash=")
|
|
? queryString.replace("temp_script_hash=", "")
|
|
: undefined;
|
|
const url = `${base_internal_url}/api/w/${w_id}/scripts/RAW_GET_ENDPOINT/p/${scriptPath}`
|
|
+ (hashParam ? `?temp_script_hash=${hashParam}` : "");
|
|
const req = await fetch(url, {
|
|
method: "GET",
|
|
headers: {
|
|
Authorization: "Bearer " + token,
|
|
},
|
|
});
|
|
if (!req.ok) {
|
|
throw new Error(
|
|
`Failed to find relative import at ${url} (status ${req.status})`
|
|
);
|
|
}
|
|
const contents = await req.text();
|
|
return {
|
|
contents: replaceRelativeImports(contents).contents,
|
|
loader: "tsx",
|
|
};
|
|
});
|
|
|
|
// Resolve windmill script imports from the file namespace (e.g. from main.ts)
|
|
build.onResolve({ filter: filterResolve }, (args) => {
|
|
const importerFwd = args.importer?.replace(/\\/g, "/") ?? "";
|
|
// Let bun natively resolve any import originating INSIDE a dependency, so a
|
|
// package's own relative requires (`./string.js`, extensionless `./foo`)
|
|
// are never sent to the windmill resolver. Match `/node_modules/` anywhere
|
|
// rather than a `cdir`-anchored prefix: on Windows `cdir` (canonical, e.g.
|
|
// `runneradmin`) and the importer path (8.3 short name, e.g. `RUNNER~1`)
|
|
// disagree, so a prefix check silently fails and dependency imports 404.
|
|
if (importerFwd.includes("/node_modules/")) {
|
|
return undefined;
|
|
}
|
|
// Check if the import resolves to a local module file (written by
|
|
// write_module_files, which can nest files in subdirectories). Resolve
|
|
// candidates against the importer's own directory — not the job root — so
|
|
// a subdir module importing a sibling (`dir/a.ts` → `./b`) finds
|
|
// `dir/b.ts` rather than a phantom `job_dir/b.ts`. For imports from
|
|
// `main.ts` the importer dir IS the job root, so behavior is unchanged.
|
|
// Module files carry a `.ts` extension, so also try the `.ts` variant of
|
|
// a bare relative import before falling through to the remote resolver.
|
|
if (args.path.startsWith(".")) {
|
|
const importerDir = args.importer ? dirname(args.importer) : cdir;
|
|
const candidates = args.path.endsWith(".ts")
|
|
? [args.path]
|
|
: [args.path, args.path + ".ts"];
|
|
for (const candidate of candidates) {
|
|
const cwdPath = resolve(importerDir, candidate);
|
|
try {
|
|
readFileSync(cwdPath);
|
|
return { path: cwdPath };
|
|
} catch {}
|
|
}
|
|
}
|
|
const isMainTs =
|
|
args.importer == "./main.ts" || importerFwd.endsWith("/main.ts");
|
|
const file_path = isMainTs
|
|
? current_path
|
|
: importerFwd.replace(cdirFwd + "/", "");
|
|
return resolveWindmillImport(file_path, args.path);
|
|
});
|
|
|
|
// Resolve nested imports from within windmill-url modules
|
|
build.onResolve({ filter: /\.ts$/, namespace: "windmill-url" }, (args) => {
|
|
// Strip any query string from the importer path before resolving
|
|
const importer = args.importer.replace(/^windmill-url:/, "").split("?")[0];
|
|
return resolveWindmillImport(importer, args.path);
|
|
});
|
|
},
|
|
};
|