From 1da28449f258e647738fb0ae25a05b29f3f63abe Mon Sep 17 00:00:00 2001 From: sidgaikwad Date: Fri, 4 Sep 2026 12:52:41 +0530 Subject: [PATCH 1/2] test: trip when the mirrored editor types move upstream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/types.ts hand-copies MountOptions, ImageEditorInstance, ImageEditorEmbed and ImageEditorSaveResult from the private @unlayer/image-editor package, with a comment saying it does so "until they move into @unlayer/types". Nothing enforced the "until". Once the move happens the local copies silently become a second, diverging source of truth — which is how a missing dock/corners reached users in #25. Add a tripwire that scans the installed @unlayer/types declarations and fails the moment any mirrored name appears there, with a message saying to delete the local copy and re-export instead. This is deliberately not a full drift check. Detecting the private package changing underneath us needs access this repository does not have, so that half stays a human responsibility — now at least written down next to the declarations. --- src/types.ts | 5 +++++ test/typesDrift.test.ts | 49 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 test/typesDrift.test.ts diff --git a/src/types.ts b/src/types.ts index 7e7b4f4..bf04d69 100644 --- a/src/types.ts +++ b/src/types.ts @@ -18,6 +18,11 @@ export interface ImageEditorSaveResult { // KEEP IN SYNC WITH packages/image-editor/src/index.ts in the unlayer // monorepo — these mirror the private @unlayer/image-editor package's // public API until they move into @unlayer/types. +// +// test/typesDrift.test.ts enforces the "until": it fails as soon as +// @unlayer/types exports any of these names, so the local copy gets deleted +// rather than left to diverge. It cannot detect the private package +// changing underneath us — that still needs a human. /** * Options for mounting the image editor. diff --git a/test/typesDrift.test.ts b/test/typesDrift.test.ts new file mode 100644 index 0000000..e3a4060 --- /dev/null +++ b/test/typesDrift.test.ts @@ -0,0 +1,49 @@ +import { readFileSync, readdirSync } from 'node:fs'; +import { join } from 'node:path'; + +// src/types.ts hand-mirrors the private @unlayer/image-editor public API, +// with a comment saying it does so "until they move into @unlayer/types". +// Nothing enforced the "until": once the move happens, the local copies +// silently become a second, diverging source of truth — which is how #25 +// (a missing `dock`/`corners`) reached users. +// +// This is a tripwire, not a full drift check: a real one needs access to the +// private package. It fails the moment @unlayer/types starts exporting any +// mirrored name, so the local copy gets deleted rather than left to rot. +const MIRRORED = [ + 'MountOptions', + 'ImageEditorInstance', + 'ImageEditorEmbed', + 'ImageEditorSaveResult', +]; + +const collectDeclarations = (dir: string): string => { + let out = ''; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const path = join(dir, entry.name); + if (entry.isDirectory()) out += collectDeclarations(path); + else if (entry.name.endsWith('.d.ts')) out += readFileSync(path, 'utf8'); + } + return out; +}; + +it('still needs its local copy of the image editor types', () => { + // Read the package directory directly: @unlayer/types restricts its + // "exports" field, so require.resolve cannot reach its package.json. + const declarations = collectDeclarations( + join(process.cwd(), 'node_modules', '@unlayer', 'types') + ); + + const moved = MIRRORED.filter((name) => + new RegExp(`\\b(interface|type)\\s+${name}\\b`).test(declarations) + ); + + expect( + moved, + moved.length + ? `@unlayer/types now exports ${moved.join(', ')}. Delete the mirrored ` + + `declaration(s) from src/types.ts and re-export from @unlayer/types ` + + `instead, then drop the name(s) from this test.` + : '' + ).toEqual([]); +}); From 90294bd3b4d41300106bd9593a6b4bf70255f735 Mon Sep 17 00:00:00 2001 From: sidgaikwad Date: Tue, 8 Sep 2026 10:26:14 +0530 Subject: [PATCH 2/2] build: add the Node type configuration the drift test needs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test/typesDrift.test.ts reads the installed @unlayer/types declarations, so it imports node:fs and node:path and calls process.cwd(). None of that typechecked: @types/node was never a root dependency, and tsconfig's "types" array listed only vitest/globals, which excludes the Node globals. Add @types/node (pinned exact, 24.10.9 — matching the demo's pin) and add "node" to the types array. Both halves are required: with the package installed but "node" absent from the array, tsc still reports TS2591 for all three. This passed locally for me because a stray @types/node in a parent directory of my checkout was being picked up by TypeScript's upward resolution. Verified in a clean tree outside that path. --- package-lock.json | 18 ++++++++++++++++++ package.json | 1 + tsconfig.json | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 5927564..31d8258 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ }, "devDependencies": { "@testing-library/react": "16.3.2", + "@types/node": "24.10.9", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", "@vitest/coverage-v8": "4.1.10", @@ -1654,6 +1655,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.10.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", + "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, "node_modules/@types/react": { "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", @@ -3456,6 +3467,13 @@ "node": ">=20.18.1" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "8.1.3", "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", diff --git a/package.json b/package.json index 0226bc9..5942eb4 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ }, "devDependencies": { "@testing-library/react": "16.3.2", + "@types/node": "24.10.9", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", "@vitest/coverage-v8": "4.1.10", diff --git a/tsconfig.json b/tsconfig.json index 0bc2c11..9ed99bf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,6 @@ "forceConsistentCasingInFileNames": true, "noEmit": true, "resolveJsonModule": true, - "types": ["vitest/globals"] + "types": ["vitest/globals", "node"] } }