fix(cli): resolve module script metadata on windows path separators (#10358)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-07-27 16:00:08 +02:00
committed by GitHub
parent ebf68d7970
commit b9960267bb
3 changed files with 32 additions and 13 deletions
+17 -12
View File
@@ -262,6 +262,20 @@ export async function findResourceFile(path: string) {
return validCandidates[0];
}
// The separator is whatever the local filesystem uses, so both are accepted:
// on Windows these paths reach us as `my_script__mod\script.yaml`.
const MODULE_ENTRY_META_RE = /([\\/])script\.(yaml|json|lock)$/;
/**
* Whether a path is a module folder's own metadata file (`__mod/script.yaml`).
* `isModuleEntryPoint` already pins the file to `script.*` directly under
* `__mod/`, so this only narrows it to the metadata extensions: a `script.yaml`
* nested deeper in the module tree is a module file, not the script's metadata.
*/
function isModuleEntryMetadata(p: string): boolean {
return isModuleEntryPoint(p) && MODULE_ENTRY_META_RE.test(p);
}
export async function handleScriptMetadata(
path: string,
workspace: Workspace,
@@ -276,14 +290,7 @@ export async function handleScriptMetadata(
const isFlatMeta = path.endsWith(".script.json") ||
path.endsWith(".script.yaml") ||
path.endsWith(".script.lock");
// Folder layout: my_script__mod/script.yaml. Only the entry point counts —
// a script.yaml nested deeper in the module tree is a module file, not the
// script's metadata.
const isFolderMeta = !isFlatMeta && isModuleEntryPoint(path) && (
path.endsWith("/script.yaml") ||
path.endsWith("/script.json") ||
path.endsWith("/script.lock")
);
const isFolderMeta = !isFlatMeta && isModuleEntryMetadata(path);
if (isFlatMeta || isFolderMeta) {
const contentPath = await findContentFile(path);
return handleFile(
@@ -884,12 +891,10 @@ export class UnresolvableScriptContentFileError extends Error {}
export async function findContentFile(filePath: string) {
// Folder layout: __mod/script.yaml -> __mod/script.ts
const isModuleFolderMeta =
isModuleEntryPoint(filePath) &&
(filePath.endsWith("/script.yaml") || filePath.endsWith("/script.json") || filePath.endsWith("/script.lock"));
const isModuleFolderMeta = isModuleEntryMetadata(filePath);
const toCandidate = (ext: string) =>
isModuleFolderMeta
? filePath.replace(/\/script\.(yaml|json|lock)$/, "/script" + ext)
? filePath.replace(MODULE_ENTRY_META_RE, "$1script" + ext)
: filePath.endsWith("script.json")
? filePath.replace(".script.json", ext)
: filePath.endsWith("script.lock")
+13
View File
@@ -350,6 +350,19 @@ describe("findContentFile", () => {
);
});
// `isModuleEntryPoint` treats `\` as a separator on every platform, so
// findContentFile must too or it rejects every Windows module path. Runnable
// on POSIX because there the backslash is merely part of the file name.
test("resolves a module entry point written with a backslash separator", async () => {
const meta = path.join(tempDir, "my_script__mod") + "\\script.yaml";
const content = path.join(tempDir, "my_script__mod") + "\\script.ts";
fs.mkdirSync(path.dirname(meta), { recursive: true });
fs.writeFileSync(content, "export async function main() {}\n");
fs.writeFileSync(meta, "summary: ''\n");
expect(await findContentFile(meta)).toBe(content);
});
// sync push catches UnresolvableScriptContentFileError to record the change as
// failed and carry on; any other error aborts the whole push, so every branch
// that simply cannot pair a metadata file with one script file must use it.
@@ -74,7 +74,8 @@ test(
tempDir,
);
expect(result.code).toBe(1);
expect(result.stdout + result.stderr).toContain(
// The push echoes local paths with the platform separator (`\` on Windows).
expect((result.stdout + result.stderr).replaceAll("\\", "/")).toContain(
"f/test/orphan.script.yaml",
);