Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
49 changes: 49 additions & 0 deletions test/typesDrift.test.ts
Original file line number Diff line number Diff line change
@@ -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([]);
});
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"resolveJsonModule": true,
"types": ["vitest/globals"]
"types": ["vitest/globals", "node"]
}
}
Loading