fix(account-data-editor): load Ace before esm-resolver

This commit is contained in:
Tommaso Casaburi
2026-04-17 13:47:52 +07:00
parent 4cae9c3caa
commit 77d22713da
2 changed files with 55 additions and 21 deletions
@@ -58,6 +58,16 @@ vi.mock('@bitsocialnet/bitsocial-react-hooks', () => ({
vi.mock('react-ace', async () => { vi.mock('react-ace', async () => {
const ReactModule = await vi.importActual<typeof import('react')>('react'); const ReactModule = await vi.importActual<typeof import('react')>('react');
await Promise.resolve();
(
globalThis as typeof globalThis & {
ace?: { config?: { setModuleUrl?: ReturnType<typeof vi.fn> } };
}
).ace = {
config: {
setModuleUrl: vi.fn(),
},
};
return { return {
default: ({ value, onChange }: { value: string; onChange: (nextValue: string) => void }) => default: ({ value, onChange }: { value: string; onChange: (nextValue: string) => void }) =>
@@ -71,7 +81,13 @@ vi.mock('react-ace', async () => {
vi.mock('ace-builds/src-noconflict/mode-json', () => ({})); vi.mock('ace-builds/src-noconflict/mode-json', () => ({}));
vi.mock('ace-builds/src-noconflict/theme-monokai', () => ({})); vi.mock('ace-builds/src-noconflict/theme-monokai', () => ({}));
vi.mock('ace-builds/esm-resolver', () => ({})); vi.mock('ace-builds/esm-resolver', () => {
if (!(globalThis as typeof globalThis & { ace?: unknown }).ace) {
throw new Error('ace is not defined');
}
return {};
});
vi.mock('ace-builds/src-noconflict/worker-json?url', () => ({ default: '/worker-json.js' })); vi.mock('ace-builds/src-noconflict/worker-json?url', () => ({ default: '/worker-json.js' }));
let root: Root; let root: Root;
@@ -135,6 +151,7 @@ describe('AccountDataEditor', () => {
beforeEach(async () => { beforeEach(async () => {
vi.resetModules(); vi.resetModules();
vi.clearAllMocks(); vi.clearAllMocks();
delete (globalThis as typeof globalThis & { ace?: unknown }).ace;
AccountDataEditor = (await import('../account-data-editor')).default; AccountDataEditor = (await import('../account-data-editor')).default;
testState.account = { id: 'test-id', name: 'Account 1', author: { address: '0x123', shortAddress: '0x1...3' } }; testState.account = { id: 'test-id', name: 'Account 1', author: { address: '0x123', shortAddress: '0x1...3' } };
testState.alertMock.mockReset(); testState.alertMock.mockReset();
@@ -183,7 +200,8 @@ describe('AccountDataEditor', () => {
await waitForEditor(); await waitForEditor();
expect(container.textContent).not.toContain('loading_editor'); expect(container.textContent).not.toContain('loading_editor');
expect(queryEditor()).toBeTruthy(); expect(container.textContent).not.toContain('editor_fallback_warning');
expect(container.querySelector('[data-testid="ace-editor"]')).toBeTruthy();
await clickButton('return_to_settings'); await clickButton('return_to_settings');
@@ -12,15 +12,25 @@ type AceModuleLoadResult = {
onBeforeLoad: (ace: { config?: { setModuleUrl?: (name: string, value: string) => void } }) => void; onBeforeLoad: (ace: { config?: { setModuleUrl?: (name: string, value: string) => void } }) => void;
}; };
type EditorPhase = 'warning' | 'loading' | 'editor' | 'fallback';
type EditorState = {
phase: EditorPhase;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
AceEditor: React.ComponentType<any> | null;
aceOnBeforeLoad: AceModuleLoadResult['onBeforeLoad'] | undefined;
text: string;
};
const loadAce = async () => { const loadAce = async () => {
const [aceModule, workerJsonModule] = await Promise.all([ const aceModule = await import('react-ace');
import('react-ace'), const [workerJsonModule] = await Promise.all([
import('ace-builds/src-noconflict/worker-json?url'), import('ace-builds/src-noconflict/worker-json?url'),
import('ace-builds/esm-resolver'), import('ace-builds/esm-resolver'),
import('ace-builds/src-noconflict/mode-json'), import('ace-builds/src-noconflict/mode-json'),
import('ace-builds/src-noconflict/theme-monokai'), import('ace-builds/src-noconflict/theme-monokai'),
]); ]);
// Vite CJS interop can double-wrap the default export // Load react-ace first so esm-resolver sees the global ace instance.
const mod = aceModule.default; const mod = aceModule.default;
const Editor = typeof mod === 'function' ? mod : (mod as unknown as { default: typeof mod }).default; const Editor = typeof mod === 'function' ? mod : (mod as unknown as { default: typeof mod }).default;
@@ -39,31 +49,37 @@ const AccountDataEditor = () => {
const account = useAccount(); const account = useAccount();
const returnTo = (location.state as { returnTo?: string } | null)?.returnTo ?? DEFAULT_RETURN_TO; const returnTo = (location.state as { returnTo?: string } | null)?.returnTo ?? DEFAULT_RETURN_TO;
const [phase, setPhase] = useState<'warning' | 'loading' | 'editor' | 'fallback'>('warning'); const [{ phase, AceEditor, aceOnBeforeLoad, text }, setEditorState] = useState<EditorState>({
// eslint-disable-next-line @typescript-eslint/no-explicit-any phase: 'warning',
const [AceEditor, setAceEditor] = useState<React.ComponentType<any> | null>(null); AceEditor: null,
const [aceOnBeforeLoad, setAceOnBeforeLoad] = useState<AceModuleLoadResult['onBeforeLoad'] | undefined>(undefined); aceOnBeforeLoad: undefined,
const [text, setText] = useState(''); text: '',
});
useEffect(() => { useEffect(() => {
if (phase !== 'loading') return; if (phase !== 'loading') return;
loadAce() loadAce()
.then(({ Editor, onBeforeLoad }) => { .then(({ Editor, onBeforeLoad }) => {
setAceEditor(() => Editor); setEditorState({
setAceOnBeforeLoad(() => onBeforeLoad); phase: 'editor',
setText(buildEditableAccountJson(account)); AceEditor: Editor,
setPhase('editor'); aceOnBeforeLoad: onBeforeLoad,
text: buildEditableAccountJson(account),
});
}) })
.catch(() => { .catch(() => {
setAceOnBeforeLoad(undefined); setEditorState({
setText(buildEditableAccountJson(account)); phase: 'fallback',
setPhase('fallback'); AceEditor: null,
aceOnBeforeLoad: undefined,
text: buildEditableAccountJson(account),
});
}); });
}, [phase, account]); }, [phase, account]);
const handleGoBack = () => navigate(returnTo); const handleGoBack = () => navigate(returnTo);
const handleContinue = () => setPhase('loading'); const handleContinue = () => setEditorState((current) => ({ ...current, phase: 'loading' }));
const handleReset = () => setText(buildEditableAccountJson(account)); const handleReset = () => setEditorState((current) => ({ ...current, text: buildEditableAccountJson(account) }));
const handleReturn = () => navigate(returnTo); const handleReturn = () => navigate(returnTo);
const handleSave = async () => { const handleSave = async () => {
@@ -122,13 +138,13 @@ const AccountDataEditor = () => {
fontSize={13} fontSize={13}
showPrintMargin={false} showPrintMargin={false}
value={text} value={text}
onChange={setText} onChange={(nextText: string) => setEditorState((current) => ({ ...current, text: nextText }))}
onBeforeLoad={aceOnBeforeLoad} onBeforeLoad={aceOnBeforeLoad}
/> />
) : ( ) : (
<textarea <textarea
value={text} value={text}
onChange={(e) => setText(e.target.value)} onChange={(e) => setEditorState((current) => ({ ...current, text: e.target.value }))}
style={{ width: '100%', height: '500px', fontFamily: 'monospace', fontSize: 13 }} style={{ width: '100%', height: '500px', fontFamily: 'monospace', fontSize: 13 }}
spellCheck={false} spellCheck={false}
/> />