@homeflare/alchemy 0.60.0 → 0.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/scope/duplicate-id.d.ts +16 -1
- package/dist/scope/duplicate-id.d.ts.map +1 -1
- package/dist/scope/index.d.ts +4 -3
- package/dist/scope/index.d.ts.map +1 -1
- package/dist/scope/index.js +13 -6
- package/dist/scope/index.js.map +4 -4
- package/dist/scope/patch-check.d.ts +6 -3
- package/dist/scope/patch-check.d.ts.map +1 -1
- package/docs/duplicate-resource-id.md +39 -6
- package/package.json +1 -1
- package/patches/alchemy@2.0.0-beta.83.patch +149 -0
- package/src/scope/duplicate-id.ts +27 -2
- package/src/scope/index.ts +8 -3
- package/src/scope/patch-check.test.ts +50 -22
- package/src/scope/patch-check.ts +31 -10
- package/src/scope/patch-coverage.test.ts +50 -0
|
@@ -24,9 +24,24 @@ export declare class DuplicateResourceIdError extends DuplicateResourceIdError_b
|
|
|
24
24
|
}> {
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
* The exact `alchemy` version `patches/alchemy@2.0.0-beta.81.patch` was cut against
|
|
27
|
+
* The exact `alchemy` version `patches/alchemy@2.0.0-beta.81.patch` was cut against — this
|
|
28
|
+
* package's own peer pin, and the version `duplicate-id.test.ts` drives a real `Stack` over.
|
|
28
29
|
* `duplicate-id.test.ts` keeps this honest against `package.json`'s own `peerDependencies`.
|
|
30
|
+
*
|
|
31
|
+
* ⛔ STAYS, MEANING BETA.81 — do not repoint this at beta.83. A consumer that calls
|
|
32
|
+
* `checkAlchemyPatch` with an explicit `alchemyVersion` (or reads this constant directly)
|
|
33
|
+
* must keep getting the same answer it always has; {@link PATCHED_ALCHEMY_VERSIONS} is the
|
|
34
|
+
* list a caller walks to find ALL supported pins, this one constant is not retargeted to mean
|
|
35
|
+
* "whichever is newest."
|
|
29
36
|
*/
|
|
30
37
|
export declare const PATCHED_ALCHEMY_VERSION: string;
|
|
38
|
+
/**
|
|
39
|
+
* Every alchemy version `@homeflare/alchemy` ships a cut `patches/alchemy@<version>.patch`
|
|
40
|
+
* for, oldest first. `checkAlchemyPatch` uses this to pick the shipped patch matching a
|
|
41
|
+
* consumer's ACTUAL installed version instead of only ever checking against
|
|
42
|
+
* {@link PATCHED_ALCHEMY_VERSION}. `patch-check.test.ts` and the static patch-coverage test
|
|
43
|
+
* both assert every entry here has a real `patches/alchemy@<version>.patch` file.
|
|
44
|
+
*/
|
|
45
|
+
export declare const PATCHED_ALCHEMY_VERSIONS: readonly string[];
|
|
31
46
|
export {};
|
|
32
47
|
//# sourceMappingURL=duplicate-id.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"duplicate-id.d.ts","sourceRoot":"","sources":["../../src/scope/duplicate-id.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"duplicate-id.d.ts","sourceRoot":"","sources":["../../src/scope/duplicate-id.ts"],"names":[],"mappings":";;;AAqCA;;;;;;;;;;;;GAYG;AACH,qBAAa,wBAAyB,SAAQ,8BAA6C;IACzF,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,yCAAyC;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;CAAG;AAEL;;;;;;;;;;GAUG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAwB,CAAC;AAE/D;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,EAAE,SAAS,MAAM,EAAuC,CAAC"}
|
package/dist/scope/index.d.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* `@homeflare/alchemy/scope` — a consuming stack's own check that it carries the
|
|
3
3
|
* duplicate-resource-id patch, and the error shape that patch throws.
|
|
4
4
|
*
|
|
5
|
-
* The fix itself is a patch to `alchemy`'s own `Resource.ts
|
|
6
|
-
* (`../../patches/alchemy
|
|
5
|
+
* The fix itself is a patch to `alchemy`'s own `Resource.ts`, one per supported alchemy version
|
|
6
|
+
* (`../../patches/alchemy@<version>.patch`, listed in `PATCHED_ALCHEMY_VERSIONS`: beta.81 and
|
|
7
|
+
* beta.83), applied at EACH consuming stack's own root
|
|
7
8
|
* (`alchemy` is a peer dependency here, so `patchedDependencies` in this package's manifest
|
|
8
9
|
* only reaches this package's own install, never a consumer's). docs/duplicate-resource-id.md
|
|
9
10
|
* has the full guide, the exact `bun patch` workflow, and what this does and does not cover.
|
|
@@ -18,6 +19,6 @@
|
|
|
18
19
|
* }
|
|
19
20
|
* ```
|
|
20
21
|
*/
|
|
21
|
-
export { DuplicateResourceIdError, PATCHED_ALCHEMY_VERSION } from './duplicate-id.ts';
|
|
22
|
+
export { DuplicateResourceIdError, PATCHED_ALCHEMY_VERSION, PATCHED_ALCHEMY_VERSIONS, } from './duplicate-id.ts';
|
|
22
23
|
export { checkAlchemyPatch, type PatchProblem } from './patch-check.ts';
|
|
23
24
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scope/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scope/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EACL,wBAAwB,EACxB,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,iBAAiB,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/scope/index.js
CHANGED
|
@@ -4,20 +4,26 @@ import * as Data from "effect/Data";
|
|
|
4
4
|
class DuplicateResourceIdError extends Data.TaggedError("DuplicateResourceIdError") {
|
|
5
5
|
}
|
|
6
6
|
var PATCHED_ALCHEMY_VERSION = "2.0.0-beta.81";
|
|
7
|
+
var PATCHED_ALCHEMY_VERSIONS = ["2.0.0-beta.81", "2.0.0-beta.83"];
|
|
7
8
|
// src/scope/patch-check.ts
|
|
8
9
|
var patchKey = (version) => `alchemy@${version}`;
|
|
9
10
|
var shippedPatchPath = (version) => new URL(`../../patches/${patchKey(version)}.patch`, import.meta.url).pathname;
|
|
10
|
-
var checkAlchemyPatch = async (projectDir, alchemyVersion
|
|
11
|
+
var checkAlchemyPatch = async (projectDir, alchemyVersion) => {
|
|
11
12
|
const problems = [];
|
|
12
|
-
const key = patchKey(alchemyVersion);
|
|
13
13
|
const installedPkg = Bun.file(`${projectDir}/node_modules/alchemy/package.json`);
|
|
14
14
|
if (!await installedPkg.exists()) {
|
|
15
15
|
problems.push("node_modules/alchemy: not installed — nothing to check the patch against");
|
|
16
16
|
return problems;
|
|
17
17
|
}
|
|
18
18
|
const installedVersion = (await installedPkg.json()).version;
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const targetVersion = alchemyVersion ?? installedVersion ?? PATCHED_ALCHEMY_VERSION;
|
|
20
|
+
const key = patchKey(targetVersion);
|
|
21
|
+
if (!PATCHED_ALCHEMY_VERSIONS.includes(targetVersion)) {
|
|
22
|
+
problems.push(`alchemy ${targetVersion} (installed: ${installedVersion ?? "unknown"}) is a version ` + `@homeflare/alchemy has ` + `no duplicate-resource-id patch for — supported versions are ` + `${PATCHED_ALCHEMY_VERSIONS.join(", ")}; cut a new patches/${key}.patch ` + "(@homeflare/alchemy's own patches/) before bumping alchemy, or this install has no " + "protection at all");
|
|
23
|
+
return problems;
|
|
24
|
+
}
|
|
25
|
+
if (installedVersion !== targetVersion) {
|
|
26
|
+
problems.push(`node_modules/alchemy is ${installedVersion ?? "unknown"}, but the duplicate-resource-id ` + `check was asked for ${targetVersion} — pass the installed version explicitly, or omit ` + "alchemyVersion to check against whatever is actually installed");
|
|
21
27
|
return problems;
|
|
22
28
|
}
|
|
23
29
|
const pkgFile = Bun.file(`${projectDir}/package.json`);
|
|
@@ -36,7 +42,7 @@ var checkAlchemyPatch = async (projectDir, alchemyVersion = PATCHED_ALCHEMY_VERS
|
|
|
36
42
|
problems.push(`package.json declares "${key}" at ${declaredPath}, but that file is missing`);
|
|
37
43
|
return problems;
|
|
38
44
|
}
|
|
39
|
-
const ours = Bun.file(shippedPatchPath(
|
|
45
|
+
const ours = Bun.file(shippedPatchPath(targetVersion));
|
|
40
46
|
if (!await ours.exists()) {
|
|
41
47
|
problems.push(`@homeflare/alchemy ships no patches/${key}.patch to compare against`);
|
|
42
48
|
return problems;
|
|
@@ -59,8 +65,9 @@ var checkAlchemyPatch = async (projectDir, alchemyVersion = PATCHED_ALCHEMY_VERS
|
|
|
59
65
|
export {
|
|
60
66
|
DuplicateResourceIdError,
|
|
61
67
|
PATCHED_ALCHEMY_VERSION,
|
|
68
|
+
PATCHED_ALCHEMY_VERSIONS,
|
|
62
69
|
checkAlchemyPatch
|
|
63
70
|
};
|
|
64
71
|
|
|
65
|
-
//# debugId=
|
|
72
|
+
//# debugId=461D34CD68FB220064756E2164756E21
|
|
66
73
|
//# sourceMappingURL=index.js.map
|
package/dist/scope/index.js.map
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/scope/duplicate-id.ts", "../src/scope/patch-check.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * `DuplicateResourceIdError` and the pinned alchemy version the kit's patch targets.\n *\n * ★ THE FIX ITSELF IS NOT HERE. A second resource of a different type claiming an id already\n * taken in one stack scope is refused by a PATCH to `alchemy`'s own `Resource.ts` — see\n * `../../patches/alchemy@2.0.0-beta.81.patch` and docs/duplicate-resource-id.md for why a\n * kit-side `Proxy` wrapper (PR 468's first version, replaced by this PR) cannot actually\n * catch the incident it was built for: wrapping `@homeflare/alchemy`'s OWN resource exports\n * never reaches `NixJobRetirement`, a resource homeflare-mini declares directly against\n * `alchemy`'s `Resource()`, so the measured collision still succeeded silently in BOTH\n * orderings. This file now holds only:\n *\n * 1. `DuplicateResourceIdError` — the exact shape the patched `alchemy` throws, duplicated\n * here (not imported — `alchemy` cannot depend on `@homeflare/alchemy`) purely so a\n * consumer can write `Effect.catchTag(\"DuplicateResourceIdError\", …)` against a type this\n * package exports, without needing to know alchemy carries its own copy of the same tag.\n * Effect dispatches `catchTag` on the `_tag` STRING, not `instanceof`, so either class\n * works identically as a catch target.\n * 2. `PATCHED_ALCHEMY_VERSION` — the exact alchemy version the
|
|
6
|
-
"/**\n * A consuming stack's own `check` calls `checkAlchemyPatch` to assert it actually carries the\n * duplicate-resource-id fix — not just that SOME version of `alchemy` is installed, but that\n * THIS install carries the patch, for the alchemy version it was cut against.\n *\n * ★ WHY A BUMP CAN OTHERWISE DROP IT SILENTLY. `patchedDependencies` keys a patch to an EXACT\n * version string (`alchemy@2.0.0-beta.81`). `bun add alchemy@2.0.0-beta.82` leaves that key\n * pointing at a version nothing installs anymore — `bun install` neither errors nor warns,\n * it just stops applying the patch, and the stack is back to the original silent collision\n * with every other gate still green. This check is what turns that bump into a failing\n * `check`, the same way `tests/dist.test.ts` turns a build regression into a failing test\n * instead of a surprise three repos downstream (this package's own AGENTS.md, \"the smoke\n * test is not optional\").\n * ★ A STATIC CHECK, mirroring `@homeflare/config/check`'s `checkCopy`/`checkOxfmt` — not a\n * functional probe that spins up a `Stack`/`Resource`/`Effect` harness on every `check` run.\n * The functional proof that the patch's LOGIC works lives in this package's own\n * `duplicate-id.test.ts`, against the exact pinned version; a consumer's `check` only needs\n * to know whether ITS install carries that same patch, applied — three file reads, no\n * `effect` runtime pulled into every repo's check step.\n * ⛔ IT REPORTS, IT DOES NOT REPAIR, same as `checkProject` in `@homeflare/config/check` — this\n * never edits `package.json` or re-runs `bun install` for the caller.\n */\nimport { PATCHED_ALCHEMY_VERSION } from './duplicate-id.ts';\n\n/** One thing wrong with a consuming stack's alchemy patch, in the imperative. */\nexport type PatchProblem = string;\n\nconst patchKey = (version: string) => `alchemy@${version}`;\n\n/** Where THIS package's own shipped copy of the patch lives, however it was installed. */\nconst shippedPatchPath = (version: string): string =>\n new URL(`../../patches/${patchKey(version)}.patch`, import.meta.url).pathname;\n\ntype PackageJson = {\n readonly patchedDependencies?: Readonly<Record<string, string>>;\n};\n\n/**\n * @param projectDir The consuming stack's OWN repo root (where its `package.json` and\n * `patches/` directory live) — never this kit's own.\n * @param alchemyVersion The version to check for
|
|
5
|
+
"/**\n * `DuplicateResourceIdError` and the pinned alchemy version the kit's patch targets.\n *\n * ★ THE FIX ITSELF IS NOT HERE. A second resource of a different type claiming an id already\n * taken in one stack scope is refused by a PATCH to `alchemy`'s own `Resource.ts` — see\n * `../../patches/alchemy@2.0.0-beta.81.patch` and docs/duplicate-resource-id.md for why a\n * kit-side `Proxy` wrapper (PR 468's first version, replaced by this PR) cannot actually\n * catch the incident it was built for: wrapping `@homeflare/alchemy`'s OWN resource exports\n * never reaches `NixJobRetirement`, a resource homeflare-mini declares directly against\n * `alchemy`'s `Resource()`, so the measured collision still succeeded silently in BOTH\n * orderings. This file now holds only:\n *\n * 1. `DuplicateResourceIdError` — the exact shape the patched `alchemy` throws, duplicated\n * here (not imported — `alchemy` cannot depend on `@homeflare/alchemy`) purely so a\n * consumer can write `Effect.catchTag(\"DuplicateResourceIdError\", …)` against a type this\n * package exports, without needing to know alchemy carries its own copy of the same tag.\n * Effect dispatches `catchTag` on the `_tag` STRING, not `instanceof`, so either class\n * works identically as a catch target.\n * 2. `PATCHED_ALCHEMY_VERSION` — the exact alchemy version THIS package's own peer pin and\n * devDependency sit at, and the one `duplicate-id.test.ts` drives its real `Stack` against.\n * Hardcoded, like `ESTATE_VERSIONS` in `@homeflare/config/versions` (the same reasoning:\n * a second source of truth derived by import would cross `tsconfig.build.json`'s\n * `rootDir: \"src\"` boundary to reach `package.json`). `duplicate-id.test.ts` asserts it\n * still matches this package's own `peerDependencies.alchemy`, so a pin bump that forgets\n * to re-cut the patch fails a test here instead of silently shipping a patch filename that\n * no longer matches what consumers install.\n * 3. `PATCHED_ALCHEMY_VERSIONS` — every alchemy version this package ships a CUT patch for\n * (currently beta.81 and beta.83, kit PR shipping beta.83's — a consumer on beta.82 or\n * beta.84 is NOT covered merely by being \"close\"; `bun`'s patch format keys to an exact\n * version string, and a patch cut against one version is not guaranteed to even apply to\n * another — see docs/duplicate-resource-id.md for why a diff was taken between the two\n * tarballs rather than assumed). `checkAlchemyPatch` (`patch-check.ts`) walks this list to\n * find the shipped patch matching a consumer's ACTUAL install, so a stack on beta.83 is not\n * forced to pin the single oldest supported version just to pass its own check.\n */\nimport * as Data from 'effect/Data';\n\n/**\n * Mirrors the error the patched `alchemy@2.0.0-beta.81` throws at registration — same `_tag`,\n * same fields — for a consumer that wants to `catchTag` it without importing from `alchemy`'s\n * own patched internals.\n *\n * ⚠️ ONLY `Type` IS COMPARED, NEVER `Props`. A platform-typed resource's tagged pattern\n * (`alchemy`'s own `Platform.ts` ~581) rewrites `Props` on a resource already registered, and a\n * bare-tag forward reference legitimately registers with `Props: undefined` until its\n * `.make(...)` Layer builds (`MissingImplementationError`'s own doc comment in `Resource.ts`).\n * Comparing props as well as type would make both of those die. `Type` is stable from a\n * resource's very first `yield*` and is exactly what distinguishes \"the same resource,\n * referenced twice\" (fine) from \"two different resources that picked the same id\" (refused).\n */\nexport class DuplicateResourceIdError extends Data.TaggedError('DuplicateResourceIdError')<{\n message: string;\n fqn: string;\n /** The type that first claimed `fqn`. */\n firstType: string;\n /** The different type that tried to claim it again. */\n secondType: string;\n}> {}\n\n/**\n * The exact `alchemy` version `patches/alchemy@2.0.0-beta.81.patch` was cut against — this\n * package's own peer pin, and the version `duplicate-id.test.ts` drives a real `Stack` over.\n * `duplicate-id.test.ts` keeps this honest against `package.json`'s own `peerDependencies`.\n *\n * ⛔ STAYS, MEANING BETA.81 — do not repoint this at beta.83. A consumer that calls\n * `checkAlchemyPatch` with an explicit `alchemyVersion` (or reads this constant directly)\n * must keep getting the same answer it always has; {@link PATCHED_ALCHEMY_VERSIONS} is the\n * list a caller walks to find ALL supported pins, this one constant is not retargeted to mean\n * \"whichever is newest.\"\n */\nexport const PATCHED_ALCHEMY_VERSION: string = '2.0.0-beta.81';\n\n/**\n * Every alchemy version `@homeflare/alchemy` ships a cut `patches/alchemy@<version>.patch`\n * for, oldest first. `checkAlchemyPatch` uses this to pick the shipped patch matching a\n * consumer's ACTUAL installed version instead of only ever checking against\n * {@link PATCHED_ALCHEMY_VERSION}. `patch-check.test.ts` and the static patch-coverage test\n * both assert every entry here has a real `patches/alchemy@<version>.patch` file.\n */\nexport const PATCHED_ALCHEMY_VERSIONS: readonly string[] = ['2.0.0-beta.81', '2.0.0-beta.83'];\n",
|
|
6
|
+
"/**\n * A consuming stack's own `check` calls `checkAlchemyPatch` to assert it actually carries the\n * duplicate-resource-id fix — not just that SOME version of `alchemy` is installed, but that\n * THIS install carries the patch, for the alchemy version it was cut against.\n *\n * ★ WHY A BUMP CAN OTHERWISE DROP IT SILENTLY. `patchedDependencies` keys a patch to an EXACT\n * version string (`alchemy@2.0.0-beta.81`). `bun add alchemy@2.0.0-beta.82` leaves that key\n * pointing at a version nothing installs anymore — `bun install` neither errors nor warns,\n * it just stops applying the patch, and the stack is back to the original silent collision\n * with every other gate still green. This check is what turns that bump into a failing\n * `check`, the same way `tests/dist.test.ts` turns a build regression into a failing test\n * instead of a surprise three repos downstream (this package's own AGENTS.md, \"the smoke\n * test is not optional\").\n * ★ A STATIC CHECK, mirroring `@homeflare/config/check`'s `checkCopy`/`checkOxfmt` — not a\n * functional probe that spins up a `Stack`/`Resource`/`Effect` harness on every `check` run.\n * The functional proof that the patch's LOGIC works lives in this package's own\n * `duplicate-id.test.ts`, against the exact pinned version; a consumer's `check` only needs\n * to know whether ITS install carries that same patch, applied — three file reads, no\n * `effect` runtime pulled into every repo's check step.\n * ⛔ IT REPORTS, IT DOES NOT REPAIR, same as `checkProject` in `@homeflare/config/check` — this\n * never edits `package.json` or re-runs `bun install` for the caller.\n */\nimport { PATCHED_ALCHEMY_VERSION, PATCHED_ALCHEMY_VERSIONS } from './duplicate-id.ts';\n\n/** One thing wrong with a consuming stack's alchemy patch, in the imperative. */\nexport type PatchProblem = string;\n\nconst patchKey = (version: string) => `alchemy@${version}`;\n\n/** Where THIS package's own shipped copy of the patch lives, however it was installed. */\nconst shippedPatchPath = (version: string): string =>\n new URL(`../../patches/${patchKey(version)}.patch`, import.meta.url).pathname;\n\ntype PackageJson = {\n readonly patchedDependencies?: Readonly<Record<string, string>>;\n};\n\n/**\n * @param projectDir The consuming stack's OWN repo root (where its `package.json` and\n * `patches/` directory live) — never this kit's own.\n * @param alchemyVersion The version to check for. Omitted (the normal case): the version\n * actually installed at `projectDir` is used, so a consumer on ANY version this package\n * ships a patch for ({@link PATCHED_ALCHEMY_VERSIONS}) passes without having to pin\n * {@link PATCHED_ALCHEMY_VERSION} specifically — a stack on beta.83 is checked against the\n * beta.83 patch, one on beta.81 against the beta.81 patch. A caller passes this explicitly\n * only to assert a SPECIFIC pin regardless of what happens to be installed (as the tests do).\n */\nexport const checkAlchemyPatch = async (\n projectDir: string,\n alchemyVersion?: string,\n): Promise<readonly PatchProblem[]> => {\n const problems: PatchProblem[] = [];\n\n const installedPkg = Bun.file(`${projectDir}/node_modules/alchemy/package.json`);\n if (!(await installedPkg.exists())) {\n problems.push('node_modules/alchemy: not installed — nothing to check the patch against');\n return problems;\n }\n const installedVersion = ((await installedPkg.json()) as { version?: string }).version;\n\n // No explicit ask: check against whatever is ACTUALLY installed, not a single hardcoded\n // pin — that is what lets a beta.83 consumer pass without also being on beta.81.\n const targetVersion = alchemyVersion ?? installedVersion ?? PATCHED_ALCHEMY_VERSION;\n const key = patchKey(targetVersion);\n\n if (!PATCHED_ALCHEMY_VERSIONS.includes(targetVersion)) {\n problems.push(\n // ★ Name the version being CHECKED: with an explicit `alchemyVersion` it can differ from\n // the installed one, and naming the installed version then blames a supported one.\n `alchemy ${targetVersion} (installed: ${installedVersion ?? 'unknown'}) is a version ` +\n `@homeflare/alchemy has ` +\n `no duplicate-resource-id patch for — supported versions are ` +\n `${PATCHED_ALCHEMY_VERSIONS.join(', ')}; cut a new patches/${key}.patch ` +\n \"(@homeflare/alchemy's own patches/) before bumping alchemy, or this install has no \" +\n 'protection at all',\n );\n return problems;\n }\n if (installedVersion !== targetVersion) {\n problems.push(\n `node_modules/alchemy is ${installedVersion ?? 'unknown'}, but the duplicate-resource-id ` +\n `check was asked for ${targetVersion} — pass the installed version explicitly, or omit ` +\n 'alchemyVersion to check against whatever is actually installed',\n );\n return problems;\n }\n\n const pkgFile = Bun.file(`${projectDir}/package.json`);\n if (!(await pkgFile.exists())) {\n problems.push('package.json: missing');\n return problems;\n }\n const pkg = (await pkgFile.json()) as PackageJson;\n const declaredPath = pkg.patchedDependencies?.[key];\n if (declaredPath === undefined) {\n problems.push(\n `package.json: patchedDependencies has no entry for \"${key}\" — this stack has no ` +\n 'duplicate-resource-id protection at all; see @homeflare/alchemy/docs/duplicate-resource-id.md',\n );\n return problems;\n }\n\n const theirs = Bun.file(`${projectDir}/${declaredPath}`);\n if (!(await theirs.exists())) {\n problems.push(`package.json declares \"${key}\" at ${declaredPath}, but that file is missing`);\n return problems;\n }\n const ours = Bun.file(shippedPatchPath(targetVersion));\n if (!(await ours.exists())) {\n // The kit's own tarball is missing its patch — a packaging bug, not the consumer's.\n problems.push(`@homeflare/alchemy ships no patches/${key}.patch to compare against`);\n return problems;\n }\n if ((await theirs.text()) !== (await ours.text())) {\n problems.push(\n `${declaredPath}: differs from @homeflare/alchemy's shipped patch — re-copy it, ` +\n 'or say why it differs',\n );\n }\n\n // Declared and byte-identical is not yet APPLIED: a hand-edited patchedDependencies entry,\n // or an install under a package manager that does not support bun's patch format, leaves\n // node_modules unpatched with nothing above noticing. One cheap textual proof it landed.\n const resourceSrc = Bun.file(`${projectDir}/node_modules/alchemy/src/Resource.ts`);\n const resourceLib = Bun.file(`${projectDir}/node_modules/alchemy/lib/Resource.js`);\n const marker = 'DuplicateResourceIdError';\n const [srcText, libText] = await Promise.all([\n resourceSrc.exists().then((ok) => (ok ? resourceSrc.text() : '')),\n resourceLib.exists().then((ok) => (ok ? resourceLib.text() : '')),\n ]);\n if (!srcText.includes(marker) && !libText.includes(marker)) {\n problems.push(\n 'node_modules/alchemy/{src/Resource.ts,lib/Resource.js}: neither contains the patch — ' +\n 'declared in package.json but not applied; re-run `bun install`',\n );\n }\n\n return problems;\n};\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": ";
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": ";AAmCA;AAAA;AAeO,MAAM,iCAAsC,iBAAY,0BAA0B,EAOtF;AAAC;AAaG,IAAM,0BAAkC;AASxC,IAAM,2BAA8C,CAAC,iBAAiB,eAAe;;ACpD5F,IAAM,WAAW,CAAC,YAAoB,WAAW;AAGjD,IAAM,mBAAmB,CAAC,YACxB,IAAI,IAAI,iBAAiB,SAAS,OAAO,WAAW,YAAY,GAAG,EAAE;AAgBhE,IAAM,oBAAoB,OAC/B,YACA,mBACqC;AAAA,EACrC,MAAM,WAA2B,CAAC;AAAA,EAElC,MAAM,eAAe,IAAI,KAAK,GAAG,8CAA8C;AAAA,EAC/E,IAAI,CAAE,MAAM,aAAa,OAAO,GAAI;AAAA,IAClC,SAAS,KAAK,0EAA0E;AAAA,IACxF,OAAO;AAAA,EACT;AAAA,EACA,MAAM,oBAAqB,MAAM,aAAa,KAAK,GAA4B;AAAA,EAI/E,MAAM,gBAAgB,kBAAkB,oBAAoB;AAAA,EAC5D,MAAM,MAAM,SAAS,aAAa;AAAA,EAElC,IAAI,CAAC,yBAAyB,SAAS,aAAa,GAAG;AAAA,IACrD,SAAS,KAGP,WAAW,6BAA6B,oBAAoB,6BAC1D,4BACA,iEACA,GAAG,yBAAyB,KAAK,IAAI,wBAAwB,eAC7D,wFACA,mBACJ;AAAA,IACA,OAAO;AAAA,EACT;AAAA,EACA,IAAI,qBAAqB,eAAe;AAAA,IACtC,SAAS,KACP,2BAA2B,oBAAoB,8CAC7C,uBAAuB,oEACvB,gEACJ;AAAA,IACA,OAAO;AAAA,EACT;AAAA,EAEA,MAAM,UAAU,IAAI,KAAK,GAAG,yBAAyB;AAAA,EACrD,IAAI,CAAE,MAAM,QAAQ,OAAO,GAAI;AAAA,IAC7B,SAAS,KAAK,uBAAuB;AAAA,IACrC,OAAO;AAAA,EACT;AAAA,EACA,MAAM,MAAO,MAAM,QAAQ,KAAK;AAAA,EAChC,MAAM,eAAe,IAAI,sBAAsB;AAAA,EAC/C,IAAI,iBAAiB,WAAW;AAAA,IAC9B,SAAS,KACP,uDAAuD,8BACrD,+FACJ;AAAA,IACA,OAAO;AAAA,EACT;AAAA,EAEA,MAAM,SAAS,IAAI,KAAK,GAAG,cAAc,cAAc;AAAA,EACvD,IAAI,CAAE,MAAM,OAAO,OAAO,GAAI;AAAA,IAC5B,SAAS,KAAK,0BAA0B,WAAW,wCAAwC;AAAA,IAC3F,OAAO;AAAA,EACT;AAAA,EACA,MAAM,OAAO,IAAI,KAAK,iBAAiB,aAAa,CAAC;AAAA,EACrD,IAAI,CAAE,MAAM,KAAK,OAAO,GAAI;AAAA,IAE1B,SAAS,KAAK,uCAAuC,8BAA8B;AAAA,IACnF,OAAO;AAAA,EACT;AAAA,EACA,IAAK,MAAM,OAAO,KAAK,MAAQ,MAAM,KAAK,KAAK,GAAI;AAAA,IACjD,SAAS,KACP,GAAG,iFACD,uBACJ;AAAA,EACF;AAAA,EAKA,MAAM,cAAc,IAAI,KAAK,GAAG,iDAAiD;AAAA,EACjF,MAAM,cAAc,IAAI,KAAK,GAAG,iDAAiD;AAAA,EACjF,MAAM,SAAS;AAAA,EACf,OAAO,SAAS,WAAW,MAAM,QAAQ,IAAI;AAAA,IAC3C,YAAY,OAAO,EAAE,KAAK,CAAC,OAAQ,KAAK,YAAY,KAAK,IAAI,EAAG;AAAA,IAChE,YAAY,OAAO,EAAE,KAAK,CAAC,OAAQ,KAAK,YAAY,KAAK,IAAI,EAAG;AAAA,EAClE,CAAC;AAAA,EACD,IAAI,CAAC,QAAQ,SAAS,MAAM,KAAK,CAAC,QAAQ,SAAS,MAAM,GAAG;AAAA,IAC1D,SAAS,KACP,0FACE,gEACJ;AAAA,EACF;AAAA,EAEA,OAAO;AAAA;",
|
|
9
|
+
"debugId": "461D34CD68FB220064756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -3,9 +3,12 @@ export type PatchProblem = string;
|
|
|
3
3
|
/**
|
|
4
4
|
* @param projectDir The consuming stack's OWN repo root (where its `package.json` and
|
|
5
5
|
* `patches/` directory live) — never this kit's own.
|
|
6
|
-
* @param alchemyVersion The version to check for
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* @param alchemyVersion The version to check for. Omitted (the normal case): the version
|
|
7
|
+
* actually installed at `projectDir` is used, so a consumer on ANY version this package
|
|
8
|
+
* ships a patch for ({@link PATCHED_ALCHEMY_VERSIONS}) passes without having to pin
|
|
9
|
+
* {@link PATCHED_ALCHEMY_VERSION} specifically — a stack on beta.83 is checked against the
|
|
10
|
+
* beta.83 patch, one on beta.81 against the beta.81 patch. A caller passes this explicitly
|
|
11
|
+
* only to assert a SPECIFIC pin regardless of what happens to be installed (as the tests do).
|
|
9
12
|
*/
|
|
10
13
|
export declare const checkAlchemyPatch: (projectDir: string, alchemyVersion?: string) => Promise<readonly PatchProblem[]>;
|
|
11
14
|
//# sourceMappingURL=patch-check.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"patch-check.d.ts","sourceRoot":"","sources":["../../src/scope/patch-check.ts"],"names":[],"mappings":"AAwBA,iFAAiF;AACjF,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAYlC
|
|
1
|
+
{"version":3,"file":"patch-check.d.ts","sourceRoot":"","sources":["../../src/scope/patch-check.ts"],"names":[],"mappings":"AAwBA,iFAAiF;AACjF,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAYlC;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,eAChB,MAAM,mBACD,MAAM,KACtB,OAAO,CAAC,SAAS,YAAY,EAAE,CAwFjC,CAAC"}
|
|
@@ -66,6 +66,29 @@ It touches three files so every consumer resolution path is covered: `src/Resour
|
|
|
66
66
|
registering the same collision under `bun test` and under plain `node` both die with the
|
|
67
67
|
identical message.
|
|
68
68
|
|
|
69
|
+
### Supported versions
|
|
70
|
+
|
|
71
|
+
`patchedDependencies` keys to an EXACT version string, and `bun`'s patch format is not
|
|
72
|
+
guaranteed to apply across versions even when the surrounding file is otherwise unchanged — so
|
|
73
|
+
each alchemy version this covers gets its OWN cut, byte-diffed against that version's real
|
|
74
|
+
tarball, not a renamed copy:
|
|
75
|
+
|
|
76
|
+
| alchemy version | patch file |
|
|
77
|
+
| --------------- | ------------------------------------- |
|
|
78
|
+
| `2.0.0-beta.81` | `patches/alchemy@2.0.0-beta.81.patch` |
|
|
79
|
+
| `2.0.0-beta.83` | `patches/alchemy@2.0.0-beta.83.patch` |
|
|
80
|
+
|
|
81
|
+
Between beta.81 and beta.83, upstream's only change touching these three files was an
|
|
82
|
+
unrelated, cosmetic rewrite of `isResource` (a function body collapsed to an arrow
|
|
83
|
+
expression) earlier in `src/Resource.ts` / `lib/Resource.js` — `lib/Resource.d.ts` is
|
|
84
|
+
byte-identical between the two. The patched spot itself (the `TODO(sam)` branch and its
|
|
85
|
+
surrounding context) is unchanged content, shifted by the line count that earlier rewrite
|
|
86
|
+
added or removed. `PATCHED_ALCHEMY_VERSIONS` (`@homeflare/alchemy/scope`) is the authoritative
|
|
87
|
+
list; `checkAlchemyPatch` walks it to find the entry matching what is actually installed.
|
|
88
|
+
A version not in that list — beta.82 included — has no patch cut for it; adopt the nearest
|
|
89
|
+
supported version's workflow below and open an issue, rather than assuming an untested patch
|
|
90
|
+
applies.
|
|
91
|
+
|
|
69
92
|
**Only `Type` is compared, never `Props`.** Comparing props as well would be wrong: a
|
|
70
93
|
platform-typed resource's tagged pattern (`alchemy`'s own `Platform.ts` ~581) rewrites `Props`
|
|
71
94
|
on a resource already registered, and a bare-tag forward reference legitimately registers with
|
|
@@ -74,15 +97,18 @@ first `yield*`; `Props` is not.
|
|
|
74
97
|
|
|
75
98
|
## Adopting it in a consuming stack
|
|
76
99
|
|
|
100
|
+
Pick the file matching the stack's OWN installed `alchemy` version from the table above —
|
|
101
|
+
here shown for beta.83; beta.81 is identical with the version string swapped:
|
|
102
|
+
|
|
77
103
|
```sh
|
|
78
|
-
cp node_modules/@homeflare/alchemy/patches/alchemy@2.0.0-beta.
|
|
104
|
+
cp node_modules/@homeflare/alchemy/patches/alchemy@2.0.0-beta.83.patch patches/
|
|
79
105
|
```
|
|
80
106
|
|
|
81
107
|
then add, in the stack's own root `package.json`:
|
|
82
108
|
|
|
83
109
|
```json
|
|
84
110
|
"patchedDependencies": {
|
|
85
|
-
"alchemy@2.0.0-beta.
|
|
111
|
+
"alchemy@2.0.0-beta.83": "patches/alchemy@2.0.0-beta.83.patch"
|
|
86
112
|
}
|
|
87
113
|
```
|
|
88
114
|
|
|
@@ -107,11 +133,18 @@ if (problems.length > 0) {
|
|
|
107
133
|
}
|
|
108
134
|
```
|
|
109
135
|
|
|
136
|
+
With no second argument, it checks against whatever `alchemy` version is ACTUALLY installed
|
|
137
|
+
at that stack's root — a stack on beta.83 is checked against the beta.83 patch, one on beta.81
|
|
138
|
+
against the beta.81 patch, neither has to pin `PATCHED_ALCHEMY_VERSION` (which stays beta.81,
|
|
139
|
+
for the consumer that already reads it directly) to pass. Pass a version explicitly only to
|
|
140
|
+
assert a SPECIFIC pin regardless of what is installed.
|
|
141
|
+
|
|
110
142
|
It is a static check — three file reads, no `effect` runtime spun up — covering: alchemy
|
|
111
|
-
installed at
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
143
|
+
installed at a version `@homeflare/alchemy` has no shipped patch for at all (`PATCHED_ALCHEMY_VERSIONS`
|
|
144
|
+
names every version it DOES cover), an explicit ask that disagrees with what is installed, no
|
|
145
|
+
`patchedDependencies` entry at all, the referenced file missing or differing from this
|
|
146
|
+
package's own shipped copy, and (the one failure mode none of those other checks would catch)
|
|
147
|
+
declared and byte-identical but never actually applied.
|
|
115
148
|
|
|
116
149
|
## Upstream
|
|
117
150
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homeflare/alchemy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.61.0",
|
|
4
4
|
"description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos, launchd and Caddy, and a verifier that proves adoptions are no-ops.",
|
|
5
5
|
"homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
|
|
6
6
|
"bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
diff --git a/lib/Resource.d.ts b/lib/Resource.d.ts
|
|
2
|
+
index 07b92ae..bfd1a13 100644
|
|
3
|
+
--- a/lib/Resource.d.ts
|
|
4
|
+
+++ b/lib/Resource.d.ts
|
|
5
|
+
@@ -228,6 +228,24 @@ export declare class MissingImplementationError extends MissingImplementationErr
|
|
6
|
+
id: string;
|
|
7
|
+
}> {
|
|
8
|
+
}
|
|
9
|
+
+declare const DuplicateResourceIdError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
10
|
+
+ readonly _tag: "DuplicateResourceIdError";
|
|
11
|
+
+} & Readonly<A>;
|
|
12
|
+
+/**
|
|
13
|
+
+ * HOMEFLARE PATCH (homeflare-kit PR superseding #468; re-cut for alchemy@2.0.0-beta.83) — see
|
|
14
|
+
+ * Resource.ts for the full comment.
|
|
15
|
+
+ * A second resource of a DIFFERENT type registered at a logical id an earlier resource
|
|
16
|
+
+ * already claimed in the same scope (namespace + id, i.e. the same FQN).
|
|
17
|
+
+ */
|
|
18
|
+
+export declare class DuplicateResourceIdError extends DuplicateResourceIdError_base<{
|
|
19
|
+
+ message: string;
|
|
20
|
+
+ fqn: string;
|
|
21
|
+
+ /** The type that first claimed `fqn`. */
|
|
22
|
+
+ firstType: string;
|
|
23
|
+
+ /** The different type that tried to claim it again. */
|
|
24
|
+
+ secondType: string;
|
|
25
|
+
+}> {
|
|
26
|
+
+}
|
|
27
|
+
export declare const missingImplementation: (type: string, id: string) => MissingImplementationError;
|
|
28
|
+
/**
|
|
29
|
+
* Creates a resource constructor for a concrete resource type.
|
|
30
|
+
diff --git a/lib/Resource.js b/lib/Resource.js
|
|
31
|
+
index cbc4f09..b57d530 100644
|
|
32
|
+
--- a/lib/Resource.js
|
|
33
|
+
+++ b/lib/Resource.js
|
|
34
|
+
@@ -4,7 +4,7 @@ import * as Effectable from "effect/Effectable";
|
|
35
|
+
import * as Layer from "effect/Layer";
|
|
36
|
+
import * as Option from "effect/Option";
|
|
37
|
+
import { AdoptPolicy } from "./AdoptPolicy.js";
|
|
38
|
+
-import { toFqn } from "./FQN.js";
|
|
39
|
+
+import { toFqn, toPath } from "./FQN.js";
|
|
40
|
+
import { CurrentNamespace } from "./Namespace.js";
|
|
41
|
+
import * as Output from "./Output.js";
|
|
42
|
+
import { Provider } from "./Provider.js";
|
|
43
|
+
@@ -58,6 +58,14 @@ export const isResourceOfType = (value, type) => (typeof value === "object" || t
|
|
44
|
+
*/
|
|
45
|
+
export class MissingImplementationError extends Data.TaggedError("MissingImplementationError") {
|
|
46
|
+
}
|
|
47
|
+
+/**
|
|
48
|
+
+ * HOMEFLARE PATCH (homeflare-kit PR superseding #468; re-cut for alchemy@2.0.0-beta.83) — see
|
|
49
|
+
+ * Resource.ts for the full comment.
|
|
50
|
+
+ * A second resource of a DIFFERENT type registered at a logical id an earlier resource
|
|
51
|
+
+ * already claimed in the same scope (namespace + id, i.e. the same FQN).
|
|
52
|
+
+ */
|
|
53
|
+
+export class DuplicateResourceIdError extends Data.TaggedError("DuplicateResourceIdError") {
|
|
54
|
+
+}
|
|
55
|
+
export const missingImplementation = (type, id) => new MissingImplementationError({
|
|
56
|
+
type,
|
|
57
|
+
id,
|
|
58
|
+
@@ -126,7 +134,17 @@ export function Resource(type, options) {
|
|
59
|
+
conflictingMode: ambientMode,
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
- // // TODO(sam): check if props are different and die
|
|
63
|
+
+ // HOMEFLARE PATCH: Type, not Props — see Resource.ts for why.
|
|
64
|
+
+ if (existing.Type !== type) {
|
|
65
|
+
+ return yield* Effect.die(new DuplicateResourceIdError({
|
|
66
|
+
+ message: `REFUSING: duplicate resource id "${fqn}" in scope ` +
|
|
67
|
+
+ `${toPath(namespace).join("/") || "(root)"}: first declared by ` +
|
|
68
|
+
+ `${existing.Type}, again by ${type}`,
|
|
69
|
+
+ fqn,
|
|
70
|
+
+ firstType: existing.Type,
|
|
71
|
+
+ secondType: type,
|
|
72
|
+
+ }));
|
|
73
|
+
+ }
|
|
74
|
+
return existing;
|
|
75
|
+
}
|
|
76
|
+
const bind = (...args) => typeof args[0] === "string"
|
|
77
|
+
diff --git a/src/Resource.ts b/src/Resource.ts
|
|
78
|
+
index c4d0a36..29e5ca7 100644
|
|
79
|
+
--- a/src/Resource.ts
|
|
80
|
+
+++ b/src/Resource.ts
|
|
81
|
+
@@ -5,7 +5,7 @@ import * as Layer from "effect/Layer";
|
|
82
|
+
import * as Option from "effect/Option";
|
|
83
|
+
import type { Pipeable } from "effect/Pipeable";
|
|
84
|
+
import { AdoptPolicy } from "./AdoptPolicy.ts";
|
|
85
|
+
-import { toFqn } from "./FQN.ts";
|
|
86
|
+
+import { toFqn, toPath } from "./FQN.ts";
|
|
87
|
+
import type { Input, InputProps, PropsInput } from "./Input.ts";
|
|
88
|
+
import { CurrentNamespace, type NamespaceNode } from "./Namespace.ts";
|
|
89
|
+
import * as Output from "./Output.ts";
|
|
90
|
+
@@ -292,6 +292,30 @@ export class MissingImplementationError extends Data.TaggedError("MissingImpleme
|
|
91
|
+
id: string;
|
|
92
|
+
}> {}
|
|
93
|
+
|
|
94
|
+
+/**
|
|
95
|
+
+ * A second resource of a DIFFERENT type registered (`yield*`ed) at a logical id an earlier
|
|
96
|
+
+ * resource already claimed in the same scope (namespace + id, i.e. the same FQN).
|
|
97
|
+
+ *
|
|
98
|
+
+ * ★ HOMEFLARE PATCH (homeflare-kit PR superseding #468, 2026-10-19; re-cut for
|
|
99
|
+
+ * alchemy@2.0.0-beta.83). Upstream's own idempotent-registration branch below used to
|
|
100
|
+
+ * return the FIRST-registered resource for ANY second `yield*` at an already-claimed FQN —
|
|
101
|
+
+ * correct for "the same resource read from two call sites," but it never compared `Type`,
|
|
102
|
+
+ * so a second, DIFFERENT resource that merely picked the same id silently lost: its props
|
|
103
|
+
+ * were never written to `stack.resources`, so neither `Plan.make` nor any later inspection
|
|
104
|
+
+ * could tell two different resources ever registered. Measured: `NixJobRetirement` and
|
|
105
|
+
+ * `Caddy.Config` both declaring id `caddy` in one stack planned `[caddy] noop` and dropped
|
|
106
|
+
+ * the retirement entirely. This error, and the `Type` check below, close that gap. Only
|
|
107
|
+
+ * `Type` is compared, not `Props` — see the check's own comment for why.
|
|
108
|
+
+ */
|
|
109
|
+
+export class DuplicateResourceIdError extends Data.TaggedError("DuplicateResourceIdError")<{
|
|
110
|
+
+ message: string;
|
|
111
|
+
+ fqn: string;
|
|
112
|
+
+ /** The type that first claimed `fqn`. */
|
|
113
|
+
+ firstType: string;
|
|
114
|
+
+ /** The different type that tried to claim it again. */
|
|
115
|
+
+ secondType: string;
|
|
116
|
+
+}> {}
|
|
117
|
+
+
|
|
118
|
+
export const missingImplementation = (type: string, id: string) =>
|
|
119
|
+
new MissingImplementationError({
|
|
120
|
+
type,
|
|
121
|
+
@@ -372,7 +396,27 @@ export function Resource<R extends ResourceLike>(
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
- // // TODO(sam): check if props are different and die
|
|
126
|
+
+ // HOMEFLARE PATCH: the TODO above asked whether PROPS differ; this checks TYPE
|
|
127
|
+
+ // instead, which is the question that actually matters. Comparing props would be
|
|
128
|
+
+ // wrong — `Platform.ts`'s own tagged resources rewrite Props after first registration
|
|
129
|
+
+ // (Platform.ts ~581), and a bare-tag forward reference legitimately registers with
|
|
130
|
+
+ // `Props: undefined` until its `.make(...)` Layer builds (MissingImplementationError's
|
|
131
|
+
+ // own doc comment above). Type is stable from the resource's very first `yield*` and is
|
|
132
|
+
+ // exactly what distinguishes "the same resource, referenced twice" from "two different
|
|
133
|
+
+ // resources that picked the same id."
|
|
134
|
+
+ if (existing.Type !== type) {
|
|
135
|
+
+ return yield* Effect.die(
|
|
136
|
+
+ new DuplicateResourceIdError({
|
|
137
|
+
+ message:
|
|
138
|
+
+ `REFUSING: duplicate resource id "${fqn}" in scope ` +
|
|
139
|
+
+ `${toPath(namespace).join("/") || "(root)"}: first declared by ` +
|
|
140
|
+
+ `${existing.Type}, again by ${type}`,
|
|
141
|
+
+ fqn,
|
|
142
|
+
+ firstType: existing.Type,
|
|
143
|
+
+ secondType: type,
|
|
144
|
+
+ }),
|
|
145
|
+
+ );
|
|
146
|
+
+ }
|
|
147
|
+
return existing;
|
|
148
|
+
}
|
|
149
|
+
const bind = (
|
|
@@ -16,13 +16,22 @@
|
|
|
16
16
|
* package exports, without needing to know alchemy carries its own copy of the same tag.
|
|
17
17
|
* Effect dispatches `catchTag` on the `_tag` STRING, not `instanceof`, so either class
|
|
18
18
|
* works identically as a catch target.
|
|
19
|
-
* 2. `PATCHED_ALCHEMY_VERSION` — the exact alchemy version
|
|
19
|
+
* 2. `PATCHED_ALCHEMY_VERSION` — the exact alchemy version THIS package's own peer pin and
|
|
20
|
+
* devDependency sit at, and the one `duplicate-id.test.ts` drives its real `Stack` against.
|
|
20
21
|
* Hardcoded, like `ESTATE_VERSIONS` in `@homeflare/config/versions` (the same reasoning:
|
|
21
22
|
* a second source of truth derived by import would cross `tsconfig.build.json`'s
|
|
22
23
|
* `rootDir: "src"` boundary to reach `package.json`). `duplicate-id.test.ts` asserts it
|
|
23
24
|
* still matches this package's own `peerDependencies.alchemy`, so a pin bump that forgets
|
|
24
25
|
* to re-cut the patch fails a test here instead of silently shipping a patch filename that
|
|
25
26
|
* no longer matches what consumers install.
|
|
27
|
+
* 3. `PATCHED_ALCHEMY_VERSIONS` — every alchemy version this package ships a CUT patch for
|
|
28
|
+
* (currently beta.81 and beta.83, kit PR shipping beta.83's — a consumer on beta.82 or
|
|
29
|
+
* beta.84 is NOT covered merely by being "close"; `bun`'s patch format keys to an exact
|
|
30
|
+
* version string, and a patch cut against one version is not guaranteed to even apply to
|
|
31
|
+
* another — see docs/duplicate-resource-id.md for why a diff was taken between the two
|
|
32
|
+
* tarballs rather than assumed). `checkAlchemyPatch` (`patch-check.ts`) walks this list to
|
|
33
|
+
* find the shipped patch matching a consumer's ACTUAL install, so a stack on beta.83 is not
|
|
34
|
+
* forced to pin the single oldest supported version just to pass its own check.
|
|
26
35
|
*/
|
|
27
36
|
import * as Data from 'effect/Data';
|
|
28
37
|
|
|
@@ -49,7 +58,23 @@ export class DuplicateResourceIdError extends Data.TaggedError('DuplicateResourc
|
|
|
49
58
|
}> {}
|
|
50
59
|
|
|
51
60
|
/**
|
|
52
|
-
* The exact `alchemy` version `patches/alchemy@2.0.0-beta.81.patch` was cut against
|
|
61
|
+
* The exact `alchemy` version `patches/alchemy@2.0.0-beta.81.patch` was cut against — this
|
|
62
|
+
* package's own peer pin, and the version `duplicate-id.test.ts` drives a real `Stack` over.
|
|
53
63
|
* `duplicate-id.test.ts` keeps this honest against `package.json`'s own `peerDependencies`.
|
|
64
|
+
*
|
|
65
|
+
* ⛔ STAYS, MEANING BETA.81 — do not repoint this at beta.83. A consumer that calls
|
|
66
|
+
* `checkAlchemyPatch` with an explicit `alchemyVersion` (or reads this constant directly)
|
|
67
|
+
* must keep getting the same answer it always has; {@link PATCHED_ALCHEMY_VERSIONS} is the
|
|
68
|
+
* list a caller walks to find ALL supported pins, this one constant is not retargeted to mean
|
|
69
|
+
* "whichever is newest."
|
|
54
70
|
*/
|
|
55
71
|
export const PATCHED_ALCHEMY_VERSION: string = '2.0.0-beta.81';
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Every alchemy version `@homeflare/alchemy` ships a cut `patches/alchemy@<version>.patch`
|
|
75
|
+
* for, oldest first. `checkAlchemyPatch` uses this to pick the shipped patch matching a
|
|
76
|
+
* consumer's ACTUAL installed version instead of only ever checking against
|
|
77
|
+
* {@link PATCHED_ALCHEMY_VERSION}. `patch-check.test.ts` and the static patch-coverage test
|
|
78
|
+
* both assert every entry here has a real `patches/alchemy@<version>.patch` file.
|
|
79
|
+
*/
|
|
80
|
+
export const PATCHED_ALCHEMY_VERSIONS: readonly string[] = ['2.0.0-beta.81', '2.0.0-beta.83'];
|
package/src/scope/index.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* `@homeflare/alchemy/scope` — a consuming stack's own check that it carries the
|
|
3
3
|
* duplicate-resource-id patch, and the error shape that patch throws.
|
|
4
4
|
*
|
|
5
|
-
* The fix itself is a patch to `alchemy`'s own `Resource.ts
|
|
6
|
-
* (`../../patches/alchemy
|
|
5
|
+
* The fix itself is a patch to `alchemy`'s own `Resource.ts`, one per supported alchemy version
|
|
6
|
+
* (`../../patches/alchemy@<version>.patch`, listed in `PATCHED_ALCHEMY_VERSIONS`: beta.81 and
|
|
7
|
+
* beta.83), applied at EACH consuming stack's own root
|
|
7
8
|
* (`alchemy` is a peer dependency here, so `patchedDependencies` in this package's manifest
|
|
8
9
|
* only reaches this package's own install, never a consumer's). docs/duplicate-resource-id.md
|
|
9
10
|
* has the full guide, the exact `bun patch` workflow, and what this does and does not cover.
|
|
@@ -18,5 +19,9 @@
|
|
|
18
19
|
* }
|
|
19
20
|
* ```
|
|
20
21
|
*/
|
|
21
|
-
export {
|
|
22
|
+
export {
|
|
23
|
+
DuplicateResourceIdError,
|
|
24
|
+
PATCHED_ALCHEMY_VERSION,
|
|
25
|
+
PATCHED_ALCHEMY_VERSIONS,
|
|
26
|
+
} from './duplicate-id.ts';
|
|
22
27
|
export { checkAlchemyPatch, type PatchProblem } from './patch-check.ts';
|
|
@@ -2,18 +2,27 @@
|
|
|
2
2
|
* `checkAlchemyPatch` against a fixture consumer directory — every failure mode a consuming
|
|
3
3
|
* stack's own `check` should catch, built one `mkdtemp` at a time rather than against this
|
|
4
4
|
* repo's own tree, so each case controls exactly what is "installed".
|
|
5
|
+
*
|
|
6
|
+
* ★ TWO SUPPORTED VERSIONS, NOT ONE. `@homeflare/alchemy` ships a cut patch for both
|
|
7
|
+
* `2.0.0-beta.81` ({@link PATCHED_ALCHEMY_VERSION}) and `2.0.0-beta.83` — a consumer on
|
|
8
|
+
* either passes `checkAlchemyPatch` without pinning a specific version; one on neither is
|
|
9
|
+
* the "unlisted version" case below.
|
|
5
10
|
*/
|
|
6
11
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
7
12
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
8
13
|
import { tmpdir } from 'node:os';
|
|
9
14
|
import { join } from 'node:path';
|
|
10
15
|
import { afterEach, describe, expect, test } from 'bun:test';
|
|
11
|
-
import { PATCHED_ALCHEMY_VERSION } from './duplicate-id.ts';
|
|
16
|
+
import { PATCHED_ALCHEMY_VERSION, PATCHED_ALCHEMY_VERSIONS } from './duplicate-id.ts';
|
|
12
17
|
import { checkAlchemyPatch } from './patch-check.ts';
|
|
13
18
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
const BETA_83 = '2.0.0-beta.83';
|
|
20
|
+
|
|
21
|
+
const shippedPatch = (version: string) =>
|
|
22
|
+
Bun.file(new URL(`../../patches/alchemy@${version}.patch`, import.meta.url)).text();
|
|
23
|
+
|
|
24
|
+
const OUR_PATCH_81 = await shippedPatch(PATCHED_ALCHEMY_VERSION);
|
|
25
|
+
const OUR_PATCH_83 = await shippedPatch(BETA_83);
|
|
17
26
|
|
|
18
27
|
let dir: string | undefined;
|
|
19
28
|
afterEach(() => {
|
|
@@ -44,19 +53,31 @@ const installAlchemy = async (
|
|
|
44
53
|
await writeFile(join(alchemyDir, 'lib', 'Resource.js'), marker);
|
|
45
54
|
};
|
|
46
55
|
|
|
56
|
+
/** Declares `patchedDependencies` for `version`, pointing at a copy of `patchContent`. */
|
|
57
|
+
const declarePatch = async (root: string, version: string, patchContent: string) => {
|
|
58
|
+
await mkdir(join(root, 'patches'), { recursive: true });
|
|
59
|
+
await writeFile(join(root, 'patches', 'alchemy.patch'), patchContent);
|
|
60
|
+
await writePkg(root, {
|
|
61
|
+
patchedDependencies: { [`alchemy@${version}`]: 'patches/alchemy.patch' },
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
47
65
|
describe('checkAlchemyPatch', () => {
|
|
48
66
|
test('alchemy not installed at all', async () => {
|
|
49
67
|
const root = scratch();
|
|
50
68
|
expect(await checkAlchemyPatch(root)).toEqual([expect.stringContaining('not installed')]);
|
|
51
69
|
});
|
|
52
70
|
|
|
53
|
-
test('installed alchemy is a
|
|
71
|
+
test('installed alchemy is a version this package has no shipped patch for', async () => {
|
|
54
72
|
const root = scratch();
|
|
55
73
|
await installAlchemy(root, { version: '2.0.0-beta.999' });
|
|
56
74
|
const problems = await checkAlchemyPatch(root);
|
|
57
75
|
expect(problems).toHaveLength(1);
|
|
58
76
|
expect(problems[0]).toContain('2.0.0-beta.999');
|
|
59
|
-
|
|
77
|
+
// Every supported version is named, so the caller knows what to re-cut against.
|
|
78
|
+
for (const supported of PATCHED_ALCHEMY_VERSIONS) {
|
|
79
|
+
expect(problems[0]).toContain(supported);
|
|
80
|
+
}
|
|
60
81
|
});
|
|
61
82
|
|
|
62
83
|
test('right version installed, but package.json has no patchedDependencies entry', async () => {
|
|
@@ -80,11 +101,7 @@ describe('checkAlchemyPatch', () => {
|
|
|
80
101
|
test('the referenced patch file differs from what @homeflare/alchemy ships', async () => {
|
|
81
102
|
const root = scratch();
|
|
82
103
|
await installAlchemy(root);
|
|
83
|
-
await
|
|
84
|
-
await writeFile(join(root, 'patches', 'alchemy.patch'), 'not the real patch');
|
|
85
|
-
await writePkg(root, {
|
|
86
|
-
patchedDependencies: { [`alchemy@${PATCHED_ALCHEMY_VERSION}`]: 'patches/alchemy.patch' },
|
|
87
|
-
});
|
|
104
|
+
await declarePatch(root, PATCHED_ALCHEMY_VERSION, 'not the real patch');
|
|
88
105
|
const problems = await checkAlchemyPatch(root);
|
|
89
106
|
expect(problems).toEqual([expect.stringContaining('differs')]);
|
|
90
107
|
});
|
|
@@ -92,23 +109,34 @@ describe('checkAlchemyPatch', () => {
|
|
|
92
109
|
test('declared and byte-identical, but never actually applied', async () => {
|
|
93
110
|
const root = scratch();
|
|
94
111
|
await installAlchemy(root, { applied: false });
|
|
95
|
-
await
|
|
96
|
-
await writeFile(join(root, 'patches', 'alchemy.patch'), OUR_PATCH);
|
|
97
|
-
await writePkg(root, {
|
|
98
|
-
patchedDependencies: { [`alchemy@${PATCHED_ALCHEMY_VERSION}`]: 'patches/alchemy.patch' },
|
|
99
|
-
});
|
|
112
|
+
await declarePatch(root, PATCHED_ALCHEMY_VERSION, OUR_PATCH_81);
|
|
100
113
|
const problems = await checkAlchemyPatch(root);
|
|
101
114
|
expect(problems).toEqual([expect.stringContaining('not applied')]);
|
|
102
115
|
});
|
|
103
116
|
|
|
104
|
-
test('declared, byte-identical, and applied: no problems', async () => {
|
|
117
|
+
test('beta.81, declared, byte-identical, and applied: no problems', async () => {
|
|
105
118
|
const root = scratch();
|
|
106
119
|
await installAlchemy(root);
|
|
107
|
-
await
|
|
108
|
-
await
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
await declarePatch(root, PATCHED_ALCHEMY_VERSION, OUR_PATCH_81);
|
|
121
|
+
expect(await checkAlchemyPatch(root)).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('beta.83, declared, byte-identical, and applied: no problems, no version argument needed', async () => {
|
|
125
|
+
const root = scratch();
|
|
126
|
+
await installAlchemy(root, { version: BETA_83 });
|
|
127
|
+
await declarePatch(root, BETA_83, OUR_PATCH_83);
|
|
128
|
+
// No explicit alchemyVersion — a beta.83 consumer must pass by what is ACTUALLY
|
|
129
|
+
// installed, not by also being pinned to PATCHED_ALCHEMY_VERSION (beta.81).
|
|
112
130
|
expect(await checkAlchemyPatch(root)).toEqual([]);
|
|
113
131
|
});
|
|
132
|
+
|
|
133
|
+
test('an explicit alchemyVersion that does not match what is installed is still a problem', async () => {
|
|
134
|
+
const root = scratch();
|
|
135
|
+
await installAlchemy(root, { version: BETA_83 });
|
|
136
|
+
await declarePatch(root, BETA_83, OUR_PATCH_83);
|
|
137
|
+
const problems = await checkAlchemyPatch(root, PATCHED_ALCHEMY_VERSION);
|
|
138
|
+
expect(problems).toHaveLength(1);
|
|
139
|
+
expect(problems[0]).toContain(BETA_83);
|
|
140
|
+
expect(problems[0]).toContain(PATCHED_ALCHEMY_VERSION);
|
|
141
|
+
});
|
|
114
142
|
});
|
package/src/scope/patch-check.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* ⛔ IT REPORTS, IT DOES NOT REPAIR, same as `checkProject` in `@homeflare/config/check` — this
|
|
21
21
|
* never edits `package.json` or re-runs `bun install` for the caller.
|
|
22
22
|
*/
|
|
23
|
-
import { PATCHED_ALCHEMY_VERSION } from './duplicate-id.ts';
|
|
23
|
+
import { PATCHED_ALCHEMY_VERSION, PATCHED_ALCHEMY_VERSIONS } from './duplicate-id.ts';
|
|
24
24
|
|
|
25
25
|
/** One thing wrong with a consuming stack's alchemy patch, in the imperative. */
|
|
26
26
|
export type PatchProblem = string;
|
|
@@ -38,16 +38,18 @@ type PackageJson = {
|
|
|
38
38
|
/**
|
|
39
39
|
* @param projectDir The consuming stack's OWN repo root (where its `package.json` and
|
|
40
40
|
* `patches/` directory live) — never this kit's own.
|
|
41
|
-
* @param alchemyVersion The version to check for
|
|
42
|
-
*
|
|
43
|
-
*
|
|
41
|
+
* @param alchemyVersion The version to check for. Omitted (the normal case): the version
|
|
42
|
+
* actually installed at `projectDir` is used, so a consumer on ANY version this package
|
|
43
|
+
* ships a patch for ({@link PATCHED_ALCHEMY_VERSIONS}) passes without having to pin
|
|
44
|
+
* {@link PATCHED_ALCHEMY_VERSION} specifically — a stack on beta.83 is checked against the
|
|
45
|
+
* beta.83 patch, one on beta.81 against the beta.81 patch. A caller passes this explicitly
|
|
46
|
+
* only to assert a SPECIFIC pin regardless of what happens to be installed (as the tests do).
|
|
44
47
|
*/
|
|
45
48
|
export const checkAlchemyPatch = async (
|
|
46
49
|
projectDir: string,
|
|
47
|
-
alchemyVersion
|
|
50
|
+
alchemyVersion?: string,
|
|
48
51
|
): Promise<readonly PatchProblem[]> => {
|
|
49
52
|
const problems: PatchProblem[] = [];
|
|
50
|
-
const key = patchKey(alchemyVersion);
|
|
51
53
|
|
|
52
54
|
const installedPkg = Bun.file(`${projectDir}/node_modules/alchemy/package.json`);
|
|
53
55
|
if (!(await installedPkg.exists())) {
|
|
@@ -55,11 +57,30 @@ export const checkAlchemyPatch = async (
|
|
|
55
57
|
return problems;
|
|
56
58
|
}
|
|
57
59
|
const installedVersion = ((await installedPkg.json()) as { version?: string }).version;
|
|
58
|
-
|
|
60
|
+
|
|
61
|
+
// No explicit ask: check against whatever is ACTUALLY installed, not a single hardcoded
|
|
62
|
+
// pin — that is what lets a beta.83 consumer pass without also being on beta.81.
|
|
63
|
+
const targetVersion = alchemyVersion ?? installedVersion ?? PATCHED_ALCHEMY_VERSION;
|
|
64
|
+
const key = patchKey(targetVersion);
|
|
65
|
+
|
|
66
|
+
if (!PATCHED_ALCHEMY_VERSIONS.includes(targetVersion)) {
|
|
67
|
+
problems.push(
|
|
68
|
+
// ★ Name the version being CHECKED: with an explicit `alchemyVersion` it can differ from
|
|
69
|
+
// the installed one, and naming the installed version then blames a supported one.
|
|
70
|
+
`alchemy ${targetVersion} (installed: ${installedVersion ?? 'unknown'}) is a version ` +
|
|
71
|
+
`@homeflare/alchemy has ` +
|
|
72
|
+
`no duplicate-resource-id patch for — supported versions are ` +
|
|
73
|
+
`${PATCHED_ALCHEMY_VERSIONS.join(', ')}; cut a new patches/${key}.patch ` +
|
|
74
|
+
"(@homeflare/alchemy's own patches/) before bumping alchemy, or this install has no " +
|
|
75
|
+
'protection at all',
|
|
76
|
+
);
|
|
77
|
+
return problems;
|
|
78
|
+
}
|
|
79
|
+
if (installedVersion !== targetVersion) {
|
|
59
80
|
problems.push(
|
|
60
81
|
`node_modules/alchemy is ${installedVersion ?? 'unknown'}, but the duplicate-resource-id ` +
|
|
61
|
-
`
|
|
62
|
-
'
|
|
82
|
+
`check was asked for ${targetVersion} — pass the installed version explicitly, or omit ` +
|
|
83
|
+
'alchemyVersion to check against whatever is actually installed',
|
|
63
84
|
);
|
|
64
85
|
return problems;
|
|
65
86
|
}
|
|
@@ -84,7 +105,7 @@ export const checkAlchemyPatch = async (
|
|
|
84
105
|
problems.push(`package.json declares "${key}" at ${declaredPath}, but that file is missing`);
|
|
85
106
|
return problems;
|
|
86
107
|
}
|
|
87
|
-
const ours = Bun.file(shippedPatchPath(
|
|
108
|
+
const ours = Bun.file(shippedPatchPath(targetVersion));
|
|
88
109
|
if (!(await ours.exists())) {
|
|
89
110
|
// The kit's own tarball is missing its patch — a packaging bug, not the consumer's.
|
|
90
111
|
problems.push(`@homeflare/alchemy ships no patches/${key}.patch to compare against`);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static coverage: every version `PATCHED_ALCHEMY_VERSIONS` claims to support actually has a
|
|
3
|
+
* shipped `patches/alchemy@<version>.patch` carrying the `DuplicateResourceIdError` fix.
|
|
4
|
+
*
|
|
5
|
+
* ★ STATIC, NOT FUNCTIONAL — no network. Proving a patch APPLIES to the real upstream tarball
|
|
6
|
+
* needs `npm pack alchemy@<version>` pulled live and diffed, which this suite's own CI lane
|
|
7
|
+
* cannot assume has network access (and must not: patch-check.ts's own header explains why a
|
|
8
|
+
* consumer's `check` stays a static file read rather than a network-dependent probe). The
|
|
9
|
+
* patch for {@link PATCHED_ALCHEMY_VERSION} (beta.81) already has that proof the hard way —
|
|
10
|
+
* `duplicate-id.test.ts` drives a REAL patched `Stack` over the devDependency `alchemy`
|
|
11
|
+
* install, which only passes because `bun install`'s `patchedDependencies` actually applied
|
|
12
|
+
* it. This test is the one that would catch a version ADDED to the list here without a
|
|
13
|
+
* patch file ever being cut for it, or a patch file that silently regressed back to the
|
|
14
|
+
* upstream TODO.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, expect, test } from 'bun:test';
|
|
17
|
+
import { PATCHED_ALCHEMY_VERSIONS } from './duplicate-id.ts';
|
|
18
|
+
|
|
19
|
+
/** `test.each` wants a plain mutable array of a known element type, not `readonly string[]`. */
|
|
20
|
+
const versions: string[] = [...PATCHED_ALCHEMY_VERSIONS];
|
|
21
|
+
|
|
22
|
+
const patchFile = (version: string) =>
|
|
23
|
+
Bun.file(new URL(`../../patches/alchemy@${version}.patch`, import.meta.url));
|
|
24
|
+
|
|
25
|
+
describe('every supported alchemy version ships a cut patch', () => {
|
|
26
|
+
test.each(versions)('alchemy@%s has a patches/ file', async (version: string) => {
|
|
27
|
+
expect(await patchFile(version).exists()).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test.each(versions)(
|
|
31
|
+
'alchemy@%s patch carries the DuplicateResourceIdError fix',
|
|
32
|
+
async (version: string) => {
|
|
33
|
+
const text = await patchFile(version).text();
|
|
34
|
+
expect(text).toContain('DuplicateResourceIdError');
|
|
35
|
+
// The TODO it replaces must be gone from the ADDED ("+") side of the diff — present
|
|
36
|
+
// only as part of a removed ("-") line, never re-introduced unpatched.
|
|
37
|
+
const addedLines = text.split('\n').filter((line) => line.startsWith('+'));
|
|
38
|
+
expect(addedLines.some((line) => line.includes('TODO(sam)'))).toBe(false);
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
test('touches the three files every consumer resolution path needs', async () => {
|
|
43
|
+
for (const version of PATCHED_ALCHEMY_VERSIONS) {
|
|
44
|
+
const text = await patchFile(version).text();
|
|
45
|
+
expect(text).toContain('a/src/Resource.ts');
|
|
46
|
+
expect(text).toContain('a/lib/Resource.js');
|
|
47
|
+
expect(text).toContain('a/lib/Resource.d.ts');
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|