Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

Commit 0fd1144

Browse files
fix(app-shell): explicit extensions on every relative specifier, so plain Node can load the published entry (#5383)
* fix(app-shell): explicit extensions on every relative specifier, so plain Node can load the entry (#5357) Node's ESM resolver does not extension-search relative specifiers and `tsc` never rewrites them, so an extensionless `./Foo` in the source stayed extensionless in `dist` and `import('@object-ui/app-shell')` under plain Node died with ERR_MODULE_NOT_FOUND on the package's own first re-export. Every consumer this repository exercises goes through a bundler, which is why it was green. 1271 relative specifiers now carry the extension they emit as (1227 `.js`, 44 `/index.js`). Every one was identified by the TypeScript PARSER and resolved against the FILESYSTEM: the suffix follows whichever candidate file exists, never the shape of the string. Three strings that parse as imports live in prose comments and were left alone. The parser was used rather than the gate's own regex because the two disagree: `withoutCommentedCode` strips block comments with a pattern that is not line-comment aware, so a back-ticked `@objectstack/*` inside a `//` line opens a block comment it runs to the next close, swallowing two live `import` lines in DraftChangesPanel.tsx. Parser 1271, gate regex 1270, and the one the gate cannot see is real code. Filed separately; not fixed here. `@object-ui/app-shell` was the last name in SPECIFIER_DEBT, so deleting it empties the map: the specifier leg stops being a ratchet with grandfathered names and becomes a hard requirement. The entry still does not evaluate under plain Node, for a different reason that the specifier error was masking: it statically imports @object-ui/plugin-dashboard, whose module-scope react-grid-layout stylesheet Node cannot load at all. That is LOAD_DEBT's class, not this one, and it is recorded there beside the package it comes from. Two consumers outside the package needed the same convention applied: `check-i18n-call-site-keys.mjs` resolved `./i18n.js` to a path with no file behind it and reported the whole metadata-admin table as an unregistered translator, and one source-text pin in @object-ui/i18n spelled the old specifier. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE * fix(console-starter): the alias-closure walker resolves an emitted `.js` specifier to its source (#5357) `resolveModule()` probed candidate extensions but had no notion that `./Foo.js` is how a package whose build preserves specifiers has to spell `Foo.tsx`. So every converted package became partially unwalkable: the walk followed the package entry, then failed on each `./X.js` re-export inside it and stopped. The relative branch of the walk drops an unresolvable specifier with NO record — `if (resolved) walk(resolved); continue;` — so `closure.unresolvable` could never see this class and stayed empty. The only assertion that could catch it was the `filesWalked` floor, and only once enough of the graph had gone. Measured with the walker's own logic: origin/main, as shipped 890 files walked, 236 relative specifiers dropped this branch, as shipped 401 files walked, 275 dropped (below the 500 floor) either tree, repaired 1242 files walked, 0 dropped So this was landing green since objectui#4538 and continued through the six packages of objectui#5214; app-shell converting is what finally crossed the floor. The floor did its job — two PRs late, because the direct symptom was being swallowed. The lookup is a pure FALLBACK after every existing candidate has failed, so a package that genuinely ships a `.js` beside its TypeScript still resolves to that file, and any specifier that already resolved returns the same answer. Repaired, both trees walk an identical 1242 files and reach the same 29 packages with 0 missing alias entries — the alias table was already complete, so the truncated walk was not hiding a second defect. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a20b81d commit 0fd1144

282 files changed

Lines changed: 1380 additions & 1290 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
Ship explicit extensions on every relative import specifier, so plain Node can load the published entry
6+
7+
Node's ESM resolver does not extension-search relative specifiers, and `tsc` never rewrites them — so an extensionless `./Foo` in the source stayed extensionless in `dist`, and `import('@object-ui/app-shell')` under plain Node died with `ERR_MODULE_NOT_FOUND` on the package's own first re-export. Bundled consumers were unaffected, which is why nothing was red. 1271 specifiers now carry the extension they emit as (1227 `.js`, 44 `/index.js`), and `dist/` no longer emits a single extensionless one.
8+
9+
- Every specifier was identified by the TypeScript parser and resolved against the filesystem — the suffix follows whichever candidate file exists, never the shape of the string. Three strings that parse as imports live in prose comments and were left alone.
10+
- `@object-ui/app-shell` is the last entry in the `SPECIFIER_DEBT` ledger of `scripts/check-node-esm-load.mjs`, so removing it empties the map: the specifier leg is no longer a ratchet with grandfathered names but a hard requirement for every published package that preserves specifiers.
11+
- The entry still does not evaluate under plain Node, for a different and already-ledgered reason: it statically imports `@object-ui/plugin-dashboard`, whose module-scope `react-grid-layout` stylesheet Node cannot load at all. That failure was previously masked behind the specifier error and is now recorded in `LOAD_DEBT` beside the package it comes from.

‎examples/console-starter/test/vite-alias-closure.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,23 @@ function resolveModule(target: string): string | null {
6161
const indexFile = path.join(target, `index${ext}`);
6262
if (fs.existsSync(indexFile)) return indexFile;
6363
}
64+
// An ESM-correct specifier addresses the EMITTED file: `./Foo.js` is how a
65+
// package whose build preserves specifiers has to spell `Foo.tsx`, because
66+
// Node's resolver does not extension-search relative specifiers (objectui#4538,
67+
// enforced per pull request by `pnpm check:esm-specifiers`). Tried only after
68+
// every candidate above has failed, so a package that genuinely ships a `.js`
69+
// next to its TypeScript still resolves to that file and nothing that already
70+
// resolved changes answer.
71+
//
72+
// Load-bearing, and it degraded SILENTLY: the relative branch of the walk
73+
// drops an unresolvable specifier with no record, so as packages converted to
74+
// explicit extensions the walk kept shrinking while every assertion stayed
75+
// green. Measured on `main` before objectui#5357: 236 relative specifiers
76+
// dropped and 890 files walked, against 1242 once they resolve. Only the
77+
// `filesWalked` floor could see it, and only once app-shell — the largest
78+
// package — converted too and took the count to 401.
79+
const asSource = target.replace(/\.(js|jsx|mjs|cjs)$/, '');
80+
if (asSource !== target) return resolveModule(asSource);
6481
return null;
6582
}
6683

‎packages/app-shell/src/chrome/CommandPalette.tsx‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ import {
2929
Search,
3030
} from 'lucide-react';
3131
import { useRecordSearch } from '@object-ui/react';
32-
import { useTheme } from './ThemeProvider';
33-
import { useExpressionContext, evaluateVisibility } from '../providers/ExpressionProvider';
32+
import { useTheme } from './ThemeProvider.js';
33+
import { useExpressionContext, evaluateVisibility } from '../providers/ExpressionProvider.js';
3434
import { useObjectTranslation } from '@object-ui/i18n';
35-
import { resolveKeyedI18nLabel, getRecordDisplayName, appRouteSegment } from '../utils';
36-
import { getIcon } from '../utils/getIcon';
37-
import { useRecentItems } from '../context/RecentItemsProvider';
38-
import { useCommandPalette } from '../context/CommandPaletteProvider';
35+
import { resolveKeyedI18nLabel, getRecordDisplayName, appRouteSegment } from '../utils/index.js';
36+
import { getIcon } from '../utils/getIcon.js';
37+
import { useRecentItems } from '../context/RecentItemsProvider.js';
38+
import { useCommandPalette } from '../context/CommandPaletteProvider.js';
3939
import { resolveHref } from '@object-ui/layout';
4040
import { useAuth } from '@object-ui/auth';
4141

‎packages/app-shell/src/chrome/ConditionalAuthWrapper.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { useState, useEffect, useCallback, ReactNode } from 'react';
1010
import { getSharedDiscovery } from '@object-ui/data-objectstack';
1111
import { AuthProvider } from '@object-ui/auth';
1212
import type { PreviewModeOptions } from '@object-ui/auth';
13-
import { LoadingScreen } from './LoadingScreen';
13+
import { LoadingScreen } from './LoadingScreen.js';
1414
import { isServiceUsable, type DiscoveryInfo } from '@object-ui/react';
1515

1616
interface ConditionalAuthWrapperProps {

‎packages/app-shell/src/chrome/ConsoleToaster.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import { Toaster as Sonner } from 'sonner';
1010
import { CircleCheck, Info, LoaderCircle, OctagonX, TriangleAlert } from 'lucide-react';
11-
import { useTheme } from './ThemeProvider';
11+
import { useTheme } from './ThemeProvider.js';
1212
import { useObjectTranslation } from '@object-ui/i18n';
1313

1414
type ToasterProps = React.ComponentProps<typeof Sonner>;

‎packages/app-shell/src/chrome/ErrorBoundary.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { Component, type ErrorInfo, type ReactNode } from 'react';
1919
import { Button, Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
2020
import { AlertTriangle, RotateCcw, Home } from 'lucide-react';
2121
import { useObjectTranslation } from '@object-ui/i18n';
22-
import { captureError } from '../observability';
22+
import { captureError } from '../observability/index.js';
2323

2424
/** Inner fallback component that uses the i18n hook */
2525
function DefaultErrorFallback({ error, onReset }: { error: Error; onReset: () => void }) {

‎packages/app-shell/src/chrome/KeyboardShortcutsDialog.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import {
1414
DialogDescription,
1515
} from '@object-ui/components';
1616
import { useObjectTranslation } from '@object-ui/i18n';
17-
import { useUrlOverlay } from '../hooks/useUrlOverlay';
18-
import { KEYBOARD_SHORTCUTS_PARAM } from '../urlParams';
17+
import { useUrlOverlay } from '../hooks/useUrlOverlay.js';
18+
import { KEYBOARD_SHORTCUTS_PARAM } from '../urlParams.js';
1919

2020
interface ShortcutEntry {
2121
keys: string[];

‎packages/app-shell/src/chrome/LoadingScreen.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { Spinner, Button } from '@object-ui/components';
33
import { Database, CheckCircle2, Loader2, AlertCircle, RefreshCw } from 'lucide-react';
44
import { useState, useEffect, useMemo } from 'react';
5-
import { getProductName, getLogoUrl } from '../runtime-config';
5+
import { getProductName, getLogoUrl } from '../runtime-config.js';
66
import { en as enLocale, builtInLocales } from '@object-ui/i18n';
77

88
interface LoadingScreenProps {
Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
export { CommandPalette } from './CommandPalette';
2-
export { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog';
3-
export { OnboardingWalkthrough } from './OnboardingWalkthrough';
4-
export { ConditionalAuthWrapper } from './ConditionalAuthWrapper';
5-
export { ConsoleToaster } from './ConsoleToaster';
6-
export { RouteFader } from './RouteFader';
7-
export { ErrorBoundary } from './ErrorBoundary';
8-
export { LoadingScreen } from './LoadingScreen';
9-
export { ThemeProvider, useTheme } from './ThemeProvider';
10-
export { toastWithUndo, type ToastWithUndoOptions } from './toast-helpers';
11-
export { presentNotificationToast } from './notificationToast';
1+
export { CommandPalette } from './CommandPalette.js';
2+
export { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog.js';
3+
export { OnboardingWalkthrough } from './OnboardingWalkthrough.js';
4+
export { ConditionalAuthWrapper } from './ConditionalAuthWrapper.js';
5+
export { ConsoleToaster } from './ConsoleToaster.js';
6+
export { RouteFader } from './RouteFader.js';
7+
export { ErrorBoundary } from './ErrorBoundary.js';
8+
export { LoadingScreen } from './LoadingScreen.js';
9+
export { ThemeProvider, useTheme } from './ThemeProvider.js';
10+
export { toastWithUndo, type ToastWithUndoOptions } from './toast-helpers.js';
11+
export { presentNotificationToast } from './notificationToast.js';

‎packages/app-shell/src/components/AppShell.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ReactNode } from 'react';
2-
import type { AppShellProps } from '../types';
2+
import type { AppShellProps } from '../types.js';
33

44
/**
55
* AppShell - Minimal layout container

0 commit comments

Comments
 (0)