diff --git a/.changeset/quiet-links-sync.md b/.changeset/quiet-links-sync.md new file mode 100644 index 0000000..f388411 --- /dev/null +++ b/.changeset/quiet-links-sync.md @@ -0,0 +1,5 @@ +--- +"packref": patch +--- + +Fix `packref sync` in monorepos that have private workspace packages with no `version` field. Packref does not show a manifest parse error for these packages now. Packref also stops at the nearest installed package, and does not use a version from a different package with the same name in a parent `node_modules`. diff --git a/src/lib/manifests/__tests__/javascript.test.ts b/src/lib/manifests/__tests__/javascript.test.ts index 0cb1668..117ce31 100644 --- a/src/lib/manifests/__tests__/javascript.test.ts +++ b/src/lib/manifests/__tests__/javascript.test.ts @@ -1,6 +1,6 @@ import type * as FileSystem from "effect/FileSystem" import type * as Path from "effect/Path" -import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises" +import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises" import { tmpdir } from "node:os" import { join } from "node:path" import * as NodeServices from "@effect/platform-node/NodeServices" @@ -485,6 +485,69 @@ importers: expect(dependencies[0]?.exactVersion).toBe("4.0.0-beta.55") }) + it("stops at the nearest installed package when it has no version", async () => { + const workspaceRoot = await makeTempDirectory() + const projectPath = join(workspaceRoot, "apps", "web") + const workspacePackagePath = join(workspaceRoot, "tooling", "linting") + await mkdir(projectPath, { recursive: true }) + await mkdir(workspacePackagePath, { recursive: true }) + await writeFile( + join(projectPath, "package.json"), + JSON.stringify({ + dependencies: { + effect: "^4.0.0-beta.50", + }, + devDependencies: { + "@tooling/linting": "workspace:*", + }, + }) + ) + await writeFile( + join(workspacePackagePath, "package.json"), + JSON.stringify({ name: "@tooling/linting", private: true }) + ) + await mkdir(join(projectPath, "node_modules", "@tooling"), { recursive: true }) + await symlink(workspacePackagePath, join(projectPath, "node_modules", "@tooling", "linting")) + await mkdir(join(workspaceRoot, "node_modules", "@tooling", "linting"), { recursive: true }) + await writeFile( + join(workspaceRoot, "node_modules", "@tooling", "linting", "package.json"), + JSON.stringify({ name: "@tooling/linting", version: "2.0.0" }) + ) + await mkdir(join(projectPath, "node_modules", "effect"), { recursive: true }) + await writeFile( + join(projectPath, "node_modules", "effect", "package.json"), + JSON.stringify({ version: "4.0.0-beta.55" }) + ) + + const dependencies = await run(readJavascriptManifest(projectPath)) + + expect(dependencies.map(({ exactVersion, name }) => [name, exactVersion])).toEqual([ + ["effect", "4.0.0-beta.55"], + ["@tooling/linting", undefined], + ]) + }) + + it("keeps the installed version for a local path dependency", async () => { + const projectPath = await makeTempDirectory() + await writeFile( + join(projectPath, "package.json"), + JSON.stringify({ + dependencies: { + example: "file:../example", + }, + }) + ) + await mkdir(join(projectPath, "node_modules", "example"), { recursive: true }) + await writeFile( + join(projectPath, "node_modules", "example", "package.json"), + JSON.stringify({ name: "example", version: "1.0.0" }) + ) + + const dependencies = await run(readJavascriptManifest(projectPath)) + + expect(dependencies[0]?.exactVersion).toBe("1.0.0") + }) + it("reports no dependency for installed packages absent from the manifest", async () => { const projectPath = await makeTempDirectory() await writeFile( diff --git a/src/lib/manifests/javascript.ts b/src/lib/manifests/javascript.ts index 963e68d..bf2e4ee 100644 --- a/src/lib/manifests/javascript.ts +++ b/src/lib/manifests/javascript.ts @@ -34,7 +34,7 @@ export const JavascriptPackageManifestSchema = Schema.StructWithRest( const InstalledPackageSchema = Schema.StructWithRest( Schema.Struct({ - version: Schema.String, + version: Schema.optional(Schema.String), }), [Schema.Record(Schema.String, Schema.Unknown)] ) @@ -485,10 +485,15 @@ const readNodeModulesVersion = Effect.fn("readNodeModulesVersion")(function* ( toManifestParseError(packageJsonPath) ) - return valid(manifest.version) === null - ? Result.fail(void 0) - : Result.succeed(manifest.version) + // Like Node resolution, stop at the nearest installed package even if it has no version. + return Result.succeed(manifest.version) }) + ).pipe( + Effect.map( + Option.filter( + (version): version is string => version !== undefined && valid(version) !== null + ) + ) ) })