diff --git a/cli/src/commands/script/script.ts b/cli/src/commands/script/script.ts index 1f8bdd90a2..c699e3d58f 100644 --- a/cli/src/commands/script/script.ts +++ b/cli/src/commands/script/script.ts @@ -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") diff --git a/cli/test/script_modules_unit.test.ts b/cli/test/script_modules_unit.test.ts index da6b97d8c2..501d384141 100644 --- a/cli/test/script_modules_unit.test.ts +++ b/cli/test/script_modules_unit.test.ts @@ -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. diff --git a/cli/test/sync_push_script_metadata_only.test.ts b/cli/test/sync_push_script_metadata_only.test.ts index 40e65efd5f..5ef01ae049 100644 --- a/cli/test/sync_push_script_metadata_only.test.ts +++ b/cli/test/sync_push_script_metadata_only.test.ts @@ -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", );