The API stores form instruments that every client then refuses to render. InstrumentInterpreter.interpret rejects a non-interactive instrument whose bundle imports /runtime/v1/react@* or /runtime/v1/react-dom@* (findReactImport, added in f46a257). That check runs only in the browser. InstrumentsService.create evaluates the same bundle server-side and validates it against $AnyInstrument, but never applies the React rule. So a form whose block imports useState, whether from an instrument repository import or a bundle upload, is accepted, stored and listed. Every attempt to open it in apps/web or the gateway then fails with "Cannot import '/runtime/v1/react@19.x/index.js' in an instrument of kind 'FORM'". The commit message says the browser is "the earliest point the two kinds can be told apart", but the API evaluates the bundle and knows its kind first.
Where
apps/api/src/instruments/instruments.service.ts:94-109:
const result = await this.virtualizationService.eval(bundle);
// ...
const parseResult = await $AnyInstrument.safeParseAsync(result.value);
if (!parseResult.success) {
throw new UnprocessableEntityException({ /* ... */ });
}
const instance = parseResult.data;
compared with the client-side rule, packages/instrument-interpreter/src/index.ts:30-37:
if (instrument.kind !== 'INTERACTIVE') {
const reactImport = findReactImport(bundle);
if (reactImport) {
throw new Error(
`Cannot import '${reactImport}' in an instrument of kind '${instrument.kind}': React is available only to interactive instruments, ...`
Reproduce
- In an instrument repository, add a form under
lib/forms/ whose block imports { useState } from /runtime/v1/react@19.x.
- Add the repository in
apps/web (Admin > Instrument Repositories) and assign the form to a group.
- Start a session and open the form.
Actual: the import succeeds and the form is listed, but opening it shows the instrument error page with "React is available only to interactive instruments".
Expected: the API rejects the bundle with 422 and the same message, so the repository import reports that instrument as failed and it is never offered to users.
Tests
apps/api/src/instruments/__tests__/instruments.service.test.ts: it('should reject a non-interactive bundle that imports react, so no stored form fails in every client').
e2e, in testing/src/specs/admin-instrument-repos.spec.ts: a repository fixture form that imports react is reported as failed, not imported.
Suggested fix
Move findReactImport (or the whole kind check) into a shared package the API can import, such as @opendatacapture/instrument-utils or runtime-internal. Call it in InstrumentsService.create after the $AnyInstrument parse and throw UnprocessableEntityException with the interpreter's message. Keep the client-side check for bundles that never pass through the API, such as the playground.
The API stores form instruments that every client then refuses to render.
InstrumentInterpreter.interpretrejects a non-interactive instrument whose bundle imports/runtime/v1/react@*or/runtime/v1/react-dom@*(findReactImport, added in f46a257). That check runs only in the browser.InstrumentsService.createevaluates the same bundle server-side and validates it against$AnyInstrument, but never applies the React rule. So a form whose block importsuseState, whether from an instrument repository import or a bundle upload, is accepted, stored and listed. Every attempt to open it inapps/webor the gateway then fails with "Cannot import '/runtime/v1/react@19.x/index.js' in an instrument of kind 'FORM'". The commit message says the browser is "the earliest point the two kinds can be told apart", but the API evaluates the bundle and knows itskindfirst.Where
apps/api/src/instruments/instruments.service.ts:94-109:compared with the client-side rule,
packages/instrument-interpreter/src/index.ts:30-37:Reproduce
lib/forms/whose block imports{ useState }from/runtime/v1/react@19.x.apps/web(Admin > Instrument Repositories) and assign the form to a group.Actual: the import succeeds and the form is listed, but opening it shows the instrument error page with "React is available only to interactive instruments".
Expected: the API rejects the bundle with 422 and the same message, so the repository import reports that instrument as failed and it is never offered to users.
Tests
apps/api/src/instruments/__tests__/instruments.service.test.ts:it('should reject a non-interactive bundle that imports react, so no stored form fails in every client').e2e, in
testing/src/specs/admin-instrument-repos.spec.ts: a repository fixture form that imports react is reported as failed, not imported.Suggested fix
Move
findReactImport(or the whole kind check) into a shared package the API can import, such as@opendatacapture/instrument-utilsorruntime-internal. Call it inInstrumentsService.createafter the$AnyInstrumentparse and throwUnprocessableEntityExceptionwith the interpreter's message. Keep the client-side check for bundles that never pass through the API, such as the playground.