|
| 1 | +/** |
| 2 | + * ObjectUI |
| 3 | + * Copyright (c) 2024-present ObjectStack Inc. |
| 4 | + * |
| 5 | + * This source code is licensed under the MIT license found in the |
| 6 | + * LICENSE file in the root directory of this source tree. |
| 7 | + */ |
| 8 | + |
| 9 | +/** |
| 10 | + * objectui#7837 — pins on the preamble the `Export to React` command writes |
| 11 | + * into every file a user gets. |
| 12 | + * |
| 13 | + * WHY A PIN AT ALL. The preamble lives inside a TEMPLATE LITERAL, so nothing |
| 14 | + * in this package's toolchain reads it: `tsc --noEmit` sees a string, `tsup` |
| 15 | + * copies it through, and the doc gates' surfaces are `content/docs`, the |
| 16 | + * per-app docs trees, `packages/NAME/README.md` and the root `README.md` — |
| 17 | + * none of which is this file. For as long as the preamble imported |
| 18 | + * `registerDefaultRenderers` from `@object-ui/components` and called it, this |
| 19 | + * package's own type-check was green and every generated file failed to |
| 20 | + * compile with `TS2305` on a symbol the user never typed. There was no binding |
| 21 | + * between the verifier and the thing verified; these assertions are it. |
| 22 | + * |
| 23 | + * WHY SOURCE TEXT AND NOT A COMPILE. Compiling the emitted code under test |
| 24 | + * would mean exporting `generateReactComponent()`, and objectui#7837 is |
| 25 | + * explicitly not allowed to move this package's export surface. So these read |
| 26 | + * the template out of the source instead. That is weaker than compiling the |
| 27 | + * output — it cannot catch a NEW phantom, only the return of this one and the |
| 28 | + * loss of its replacement — and the stronger instrument is filed separately. |
| 29 | + * |
| 30 | + * WHY THE SIDE-EFFECT IMPORT IS THE CORRECT SPELLING (measured, not recalled). |
| 31 | + * `@object-ui/components` declares `sideEffects: true`, its barrel runs |
| 32 | + * `import './renderers'` under the comment `Register all ObjectUI renderers |
| 33 | + * (side-effects)`, and its built `dist/index.js` carries 114 module-scope |
| 34 | + * `register(` call sites. Its built `dist/index.d.ts` exports exactly one |
| 35 | + * `register*` name — `registerPlaceholders`. There is no registration function |
| 36 | + * to call. Same spelling the root README landed for objectui#7417. |
| 37 | + */ |
| 38 | + |
| 39 | +import { readFileSync } from 'node:fs'; |
| 40 | +import { dirname, resolve } from 'node:path'; |
| 41 | +import { fileURLToPath } from 'node:url'; |
| 42 | +import { describe, expect, it } from 'vitest'; |
| 43 | + |
| 44 | +const EXTENSION_SRC = readFileSync( |
| 45 | + resolve(dirname(fileURLToPath(import.meta.url)), '../extension.ts'), |
| 46 | + 'utf8' |
| 47 | +); |
| 48 | + |
| 49 | +/** |
| 50 | + * The body of the template literal `generateReactComponent()` returns — i.e. |
| 51 | + * the text of the file the user receives, with the schema still uninterpolated. |
| 52 | + * |
| 53 | + * Every failure mode throws with "this pin needs rewriting, not deleting": if |
| 54 | + * the generator is refactored, the pin has to be pointed at the new shape, and |
| 55 | + * a silently-skipped assertion is exactly the failure this file exists to stop. |
| 56 | + */ |
| 57 | +function generatedFileTemplate(): string { |
| 58 | + const fn = EXTENSION_SRC.indexOf('function generateReactComponent'); |
| 59 | + if (fn < 0) { |
| 60 | + throw new Error( |
| 61 | + 'generateReactComponent() is gone from extension.ts — this pin needs rewriting, not deleting.' |
| 62 | + ); |
| 63 | + } |
| 64 | + const open = EXTENSION_SRC.indexOf('return `', fn); |
| 65 | + if (open < 0) { |
| 66 | + throw new Error( |
| 67 | + 'generateReactComponent() no longer returns a template literal — this pin needs rewriting, not deleting.' |
| 68 | + ); |
| 69 | + } |
| 70 | + const start = open + 'return `'.length; |
| 71 | + const close = EXTENSION_SRC.indexOf('`;', start); |
| 72 | + if (close < 0) { |
| 73 | + throw new Error( |
| 74 | + 'unterminated template literal in generateReactComponent() — this pin needs rewriting, not deleting.' |
| 75 | + ); |
| 76 | + } |
| 77 | + return EXTENSION_SRC.slice(start, close); |
| 78 | +} |
| 79 | + |
| 80 | +describe('Export to React — the generated file preamble (objectui#7837)', () => { |
| 81 | + it('names no `registerDefaultRenderers`, which @object-ui/components does not export', () => { |
| 82 | + expect(generatedFileTemplate()).not.toContain('registerDefaultRenderers'); |
| 83 | + // Belt and braces: the identifier is absent from the whole module, so a |
| 84 | + // second copy cannot reappear in a helper the template interpolates. |
| 85 | + expect(EXTENSION_SRC).not.toContain('registerDefaultRenderers'); |
| 86 | + }); |
| 87 | + |
| 88 | + it('imports @object-ui/components for its side effect, with no named binding', () => { |
| 89 | + const template = generatedFileTemplate(); |
| 90 | + expect(template).toContain("import '@object-ui/components';"); |
| 91 | + // A named import from that package is how the defect was spelled. Assert |
| 92 | + // the SHAPE is gone, not just the one identifier, so the next phantom off |
| 93 | + // that specifier fails here too. |
| 94 | + expect(template).not.toMatch(/import\s*\{[^}]*\}\s*from\s*'@object-ui\/components'/); |
| 95 | + }); |
| 96 | + |
| 97 | + it('still imports SchemaRenderer from @object-ui/react — the renderer it calls', () => { |
| 98 | + const template = generatedFileTemplate(); |
| 99 | + expect(template).toContain("import { SchemaRenderer } from '@object-ui/react';"); |
| 100 | + expect(template).toContain('<SchemaRenderer schema={schema} />'); |
| 101 | + }); |
| 102 | +}); |
0 commit comments