Skip to content

Commit ca4b015

Browse files
SamTV12345claude
andcommitted
fix(vitest): alias ep_etherpad-lite/* to source to avoid double-loading
When internal code imported via the exports map (ep_etherpad-lite/node/x) AND via a relative path (../../node/x), vite-node resolved two distinct module instances. Prom-client top-level Counter() calls ran twice and threw "metric already registered", cascading ~35 test failures. Fix adds a resolve.alias in vitest.config.ts that rewrites ep_etherpad-lite/<subpath> to the .ts source, so the two spellings collapse to one module instance. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ac98496 commit ca4b015

20 files changed

Lines changed: 51 additions & 30 deletions

bin/tsconfig.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@
4040
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
4141
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
4242
"resolveJsonModule": true, /* Enable importing .json files. */
43+
"baseUrl": ".",
44+
"paths": {
45+
"ep_etherpad-lite/*.js": ["../src/*.ts"],
46+
"ep_etherpad-lite/*": ["../src/*.ts", "../src/*"]
47+
},
4348
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
4449
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
4550

src/node/db/API.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {deserializeOps} from '../../static/js/Changeset.js';
2424
import ChatMessage from '../../static/js/ChatMessage.js';
2525
import {Builder} from "../../static/js/Builder.js";
2626
import {Attribute} from "../../static/js/types/Attribute.js";
27+
import AttributeMap from "../../static/js/AttributeMap.js";
2728
import settings from '../utils/Settings.js';
2829
import CustomError from '../utils/customError.js';
2930
import * as padManager from './PadManager.js';

src/node/hooks/express/specialpages.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ let ioI: { sockets: { sockets: any[]; }; } | null = null
2424
// Shared sanitizer for the `x-proxy-path` header. See the helper for the
2525
// allowed character class and the protocol-relative / traversal rejection
2626
// rules. Reused by admin.ts so both call sites share one definition.
27-
import {sanitizeProxyPath} from '../../utils/sanitizeProxyPath';
27+
import {sanitizeProxyPath} from '../../utils/sanitizeProxyPath.js';
2828

2929

3030
export const socketio = (hookName: string, {io}: any) => {

src/node/hooks/express/updateStatus.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export const resolveRequestAuthor = async (req: any): Promise<string | null> =>
4949
const cookiePrefix = (settings as any).cookie?.prefix ?? '';
5050
const token = req?.cookies?.[`${cookiePrefix}token`];
5151
if (typeof token !== 'string' || token === '') return null;
52-
const authorManagerMod: any = await import('../../db/AuthorManager');
52+
const authorManagerMod: any = await import('../../db/AuthorManager.js');
5353
const authorManager = authorManagerMod.default ?? authorManagerMod;
5454
if (typeof authorManager.getAuthorId !== 'function') return null;
5555
const authorId = await authorManager.getAuthorId(token, req?.session?.user);
@@ -91,7 +91,7 @@ const computeOutdated = async (
9191
if (!isMinorOrMoreBehind(current, state.latest.version)) return EMPTY;
9292
if (!padId || !authorId) return EMPTY;
9393
// padManager is loaded via dynamic import to avoid circular-init w/ updater.
94-
const padManagerMod: any = await import('../../db/PadManager');
94+
const padManagerMod: any = await import('../../db/PadManager.js');
9595
const padManager = padManagerMod.default ?? padManagerMod;
9696
if (typeof padManager.isValidPadId !== 'function' || !padManager.isValidPadId(padId)) return EMPTY;
9797
if (!(await padManager.doesPadExist(padId))) return EMPTY;

src/node/utils/sanitizeProxyPath.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import settings from './Settings';
1+
import settings from './Settings.js';
22

33
/**
44
* Sanitize the URL-path prefix Etherpad is being served under.

src/tests/backend-new/specs/hooks/express/firstAuthorOf.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {describe, expect, it} from 'vitest';
2-
import {firstAuthorOf} from '../../../../../node/hooks/express/updateStatus';
2+
import {firstAuthorOf} from '../../../../../node/hooks/express/updateStatus.js';
33

44
const makePad = (entries: Record<number, [string, string]>): any => ({
55
pool: {numToAttrib: entries},

src/tests/backend-new/specs/hooks/express/updateStatus.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ import {describe, it, expect, vi, beforeAll, beforeEach, afterEach} from 'vitest
1717
import express from 'express';
1818
import supertest from 'supertest';
1919
import type {Express} from 'express';
20-
import type {UpdateState} from '../../../../../node/updater/types';
21-
import {EMPTY_STATE} from '../../../../../node/updater/types';
22-
import {getEpVersion} from '../../../../../node/utils/Settings';
23-
import {parseSemver} from '../../../../../node/updater/versionCompare';
20+
import type {UpdateState} from '../../../../../node/updater/types.js';
21+
import {EMPTY_STATE} from '../../../../../node/updater/types.js';
22+
import {getEpVersion} from '../../../../../node/utils/Settings.js';
23+
import {parseSemver} from '../../../../../node/updater/versionCompare.js';
2424

2525
// ---------------------------------------------------------------------------
2626
// Module mocks — must appear before any import that transitively imports them.
@@ -67,13 +67,13 @@ vi.mock('../../../../../node/db/PadManager', () => {
6767
// Import the SUT *after* vi.mock declarations so the mocks take effect.
6868
// ---------------------------------------------------------------------------
6969

70-
import * as stateModule from '../../../../../node/updater/state';
71-
import * as authorManagerModule from '../../../../../node/db/AuthorManager';
70+
import * as stateModule from '../../../../../node/updater/state.js';
71+
import * as authorManagerModule from '../../../../../node/db/AuthorManager.js';
7272
import {
7373
expressCreateServer,
7474
_resetBadgeCacheForTests,
7575
_setBadgeCacheCapForTests,
76-
} from '../../../../../node/hooks/express/updateStatus';
76+
} from '../../../../../node/hooks/express/updateStatus.js';
7777

7878
// ---------------------------------------------------------------------------
7979
// Helpers
@@ -181,7 +181,7 @@ afterEach(() => {
181181

182182
const getPadMap = async (): Promise<Map<string, any>> => {
183183
// Dynamic import returns the mock factory's return value.
184-
const mod: any = await import('../../../../../node/db/PadManager');
184+
const mod: any = await import('../../../../../node/db/PadManager.js');
185185
return mod.__pads__ as Map<string, any>;
186186
};
187187

src/tests/backend-new/specs/sanitizeProxyPath.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* - rejects path-traversal segments.
1010
*/
1111
import {describe, it, expect} from 'vitest';
12-
import {sanitizeProxyPath} from '../../../node/utils/sanitizeProxyPath';
12+
import {sanitizeProxyPath} from '../../../node/utils/sanitizeProxyPath.js';
1313

1414
const mockReq = (val: string|undefined) => ({
1515
header: (name: string) => name.toLowerCase() === 'x-proxy-path' ? val : undefined,
@@ -27,7 +27,7 @@ describe('sanitizeProxyPath', () => {
2727

2828
it('returns "" when the req object has no header()', () => {
2929
expect(sanitizeProxyPath(undefined)).toBe('');
30-
// @ts-expect-error — exercising the defensive branch
30+
// @ts-expect-error — exercising the defensive branch with an incompatible type
3131
expect(sanitizeProxyPath({})).toBe('');
3232
});
3333
});
@@ -189,7 +189,7 @@ describe('sanitizeProxyPath', () => {
189189
});
190190

191191
it('defaults trustProxy from settings when opts not provided', async () => {
192-
const settings = (await import('../../../node/utils/Settings')).default;
192+
const settings = (await import('../../../node/utils/Settings.js')).default;
193193
const original = settings.trustProxy;
194194
try {
195195
settings.trustProxy = true;

src/tests/backend-new/specs/updater/MaintenanceWindow.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
parseWindow,
44
inWindow,
55
nextWindowStart,
6-
} from '../../../../node/updater/MaintenanceWindow';
6+
} from '../../../../node/updater/MaintenanceWindow.js';
77

88
describe('parseWindow', () => {
99
it('accepts a valid same-day window with tz=local', () => {

src/tests/backend-new/specs/updater/smtpTransportKey.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {describe, it, expect} from 'vitest';
2-
import {smtpTransportKey} from '../../../../node/updater/index';
2+
import {smtpTransportKey} from '../../../../node/updater/index.js';
33

44
describe('smtpTransportKey', () => {
55
// Regression for Qodo PR #7753 review: the nodemailer transport cache was

0 commit comments

Comments
 (0)