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/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([]); +}); 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"] } }