Skip to content
Merged
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
25 changes: 25 additions & 0 deletions .changeset/7837-vscode-export-react-side-effect-import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
'object-ui': patch
---

Fix the phantom import the VS Code extension's **Export to React** command wrote into
every file it generated (objectui#7837).

`generateReactComponent()` emitted a preamble that imported `registerDefaultRenderers`
from `@object-ui/components` and then called it. That symbol is on **no export** of
that package: its built `dist/index.d.ts` carries exactly one `register*` name,
`registerPlaceholders`, and `registerDefaultRenderers` appears **0 times** in either
`dist/index.d.ts` or `dist/index.js`. So every file the command produced failed to
compile with `TS2305` naming a symbol the user never typed.

`@object-ui/components` registers its renderers as an **import side effect** —
`sideEffects: true` in its manifest, `import './renderers'` in its barrel under the
comment `Register all ObjectUI renderers (side-effects)`, and **114 `register(` call
sites** at module scope in the built `dist/index.js`. There is no registration function
to call, so the generated preamble now imports the package for the side effect and says
why. Same spelling the root README landed for objectui#7417.

`packages/vscode-extension/DESIGN.md`, which documented the identical two lines, is
corrected in the same commit so the design record does not freeze the defect.

No public surface moved: no export added, no signature changed.
6 changes: 3 additions & 3 deletions packages/vscode-extension/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,9 @@ if (currentType === 'input') {
```typescript
import React from 'react';
import { SchemaRenderer } from '@object-ui/react';
import { registerDefaultRenderers } from '@object-ui/components';

registerDefaultRenderers();
// Importing the package registers every default renderer as a side effect —
// there is no separate registration call.
import '@object-ui/components';

const schema = { /* 用户的schema */ };

Expand Down
2 changes: 1 addition & 1 deletion packages/vscode-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@
"scripts": {
"vscode:prepublish": "pnpm run build",
"build": "tsup",
"type-check": "tsc --noEmit",
"type-check": "tsc --noEmit && tsc -p tsconfig.test.json",
"lint": "eslint .",
"dev": "tsup --watch",
"test": "vitest run --passWithNoTests --root ../.. packages/vscode-extension/",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* objectui#7837 — pins on the preamble the `Export to React` command writes
* into every file a user gets.
*
* WHY A PIN AT ALL. The preamble lives inside a TEMPLATE LITERAL, so nothing
* in this package's toolchain reads it: `tsc --noEmit` sees a string, `tsup`
* copies it through, and the doc gates' surfaces are `content/docs`, the
* per-app docs trees, `packages/NAME/README.md` and the root `README.md` —
* none of which is this file. For as long as the preamble imported
* `registerDefaultRenderers` from `@object-ui/components` and called it, this
* package's own type-check was green and every generated file failed to
* compile with `TS2305` on a symbol the user never typed. There was no binding
* between the verifier and the thing verified; these assertions are it.
*
* WHY SOURCE TEXT AND NOT A COMPILE. Compiling the emitted code under test
* would mean exporting `generateReactComponent()`, and objectui#7837 is
* explicitly not allowed to move this package's export surface. So these read
* the template out of the source instead. That is weaker than compiling the
* output — it cannot catch a NEW phantom, only the return of this one and the
* loss of its replacement — and the stronger instrument is filed separately.
*
* WHY THE SIDE-EFFECT IMPORT IS THE CORRECT SPELLING (measured, not recalled).
* `@object-ui/components` declares `sideEffects: true`, its barrel runs
* `import './renderers'` under the comment `Register all ObjectUI renderers
* (side-effects)`, and its built `dist/index.js` carries 114 module-scope
* `register(` call sites. Its built `dist/index.d.ts` exports exactly one
* `register*` name — `registerPlaceholders`. There is no registration function
* to call. Same spelling the root README landed for objectui#7417.
*/

import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';

const EXTENSION_SRC = readFileSync(
resolve(dirname(fileURLToPath(import.meta.url)), '../extension.ts'),
'utf8'
);

/**
* The body of the template literal `generateReactComponent()` returns — i.e.
* the text of the file the user receives, with the schema still uninterpolated.
*
* Every failure mode throws with "this pin needs rewriting, not deleting": if
* the generator is refactored, the pin has to be pointed at the new shape, and
* a silently-skipped assertion is exactly the failure this file exists to stop.
*/
function generatedFileTemplate(): string {
const fn = EXTENSION_SRC.indexOf('function generateReactComponent');
if (fn < 0) {
throw new Error(
'generateReactComponent() is gone from extension.ts — this pin needs rewriting, not deleting.'
);
}
const open = EXTENSION_SRC.indexOf('return `', fn);
if (open < 0) {
throw new Error(
'generateReactComponent() no longer returns a template literal — this pin needs rewriting, not deleting.'
);
}
const start = open + 'return `'.length;
const close = EXTENSION_SRC.indexOf('`;', start);
if (close < 0) {
throw new Error(
'unterminated template literal in generateReactComponent() — this pin needs rewriting, not deleting.'
);
}
return EXTENSION_SRC.slice(start, close);
}

describe('Export to React — the generated file preamble (objectui#7837)', () => {
it('names no `registerDefaultRenderers`, which @object-ui/components does not export', () => {
expect(generatedFileTemplate()).not.toContain('registerDefaultRenderers');
// Belt and braces: the identifier is absent from the whole module, so a
// second copy cannot reappear in a helper the template interpolates.
expect(EXTENSION_SRC).not.toContain('registerDefaultRenderers');
});

it('imports @object-ui/components for its side effect, with no named binding', () => {
const template = generatedFileTemplate();
expect(template).toContain("import '@object-ui/components';");
// A named import from that package is how the defect was spelled. Assert
// the SHAPE is gone, not just the one identifier, so the next phantom off
// that specifier fails here too.
expect(template).not.toMatch(/import\s*\{[^}]*\}\s*from\s*'@object-ui\/components'/);
});

it('still imports SchemaRenderer from @object-ui/react — the renderer it calls', () => {
const template = generatedFileTemplate();
expect(template).toContain("import { SchemaRenderer } from '@object-ui/react';");
expect(template).toContain('<SchemaRenderer schema={schema} />');
});
});
7 changes: 3 additions & 4 deletions packages/vscode-extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,10 +227,9 @@ function generateReactComponent(schema: any): string {

return `import React from 'react';
import { SchemaRenderer } from '@object-ui/react';
import { registerDefaultRenderers } from '@object-ui/components';

// Register default components once
registerDefaultRenderers();
// Importing the package registers every default renderer as a side effect —
// there is no separate registration call.
import '@object-ui/components';

const schema = ${schemaJson};

Expand Down
35 changes: 35 additions & 0 deletions packages/vscode-extension/tsconfig.test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
// Type-checks this package's TESTS, which `tsconfig.json` excludes.
//
// Why the exclusion exists and why it was a hole: `tsconfig.json` is the
// package BUILD (`rootDir: ./src`, `declaration`, `outDir: ./dist`), so a
// test file left in that program would emit into `dist` and ship inside the
// `.vsix`. Excluding it keeps the build honest — but until this project
// existed, no `tsc` invocation read the test at all, and an unchecked test
// can assert a contract the compiler never checked and then read as evidence
// that the contract holds. Same shape and same reason as the 35 sibling
// `tsconfig.test.json` files; `scripts/check-type-check-coverage.mjs`
// enforces the chaining off `type-check`, since a config nothing runs is the
// objectui#3009 failure itself.
"extends": "./tsconfig.json",
"compilerOptions": {
// A checking project, never an emitting one.
"noEmit": true,
"declaration": false,
"declarationMap": false,
"sourceMap": false,
// The BUILD is CommonJS (`module: node16`, no `"type": "module"`), which is
// what tsup emits for VS Code. Vitest is not that program: it loads these
// files as ESM, so `import.meta.url` — the only stable way for a test to
// locate its sibling source on disk — is legal there and TS1470 under the
// build's CommonJS target. This project therefore describes the runtime the
// tests actually run in, and the build config keeps describing the .vsix.
"module": "ESNext",
"moduleResolution": "Bundler"
},
// Both fields are overridden, not merged: `tsconfig.json`'s `exclude` lists
// `**/*.test.ts`, so inheriting it would leave this project with no inputs —
// green, and reading nothing.
"include": ["src/**/*.test.ts"],
"exclude": ["node_modules", "dist"]
}
Loading