Skip to content

Commit ae5fa8e

Browse files
committed
Refactor extension handling by removing explicit imports and implementing automatic side-effect imports via Vite plugin. Update documentation to reflect changes in extension management.
1 parent 582f6f2 commit ae5fa8e

11 files changed

Lines changed: 317 additions & 70 deletions

File tree

packages/app-e2e/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Minimal Docks shell for Playwright only (not the public demo app in `packages/ap
2121
Entry is [src/main.ts](./src/main.ts): `registerApp` plus auxiliary/toolbar contributions as specs require.
2222

2323
- **Extensions:** list them in `AppDefinition.extensions` (e.g. `@eclipse-docks/extension-ai-system`, `@eclipse-docks/extension-monaco-editor` for workspace file editing in E2E).
24-
- **Static side-effect import:** [src/extensions.ts](./src/extensions.ts) loads the AI extension’s registration module **before** `main.ts` registers E2E auxiliary tabs so order is **`[aiview, …e2e tabs]`**. That avoids `wa-tab-group` “first tab” fallbacks being mistaken for successful `coupledEditors` coupling.
24+
- **Side-effect imports:** [vite.config.ts](./vite.config.ts) uses `resolveDepVersionsPlugin()` (automatic `@eclipse-docks/extension-*` side-effect imports are on by default). [src/main.ts](./src/main.ts) additionally imports the in-repo [`extension-ai-system/src/ai-system-extension.ts`](../../packages/extension-ai-system/src/ai-system-extension.ts) so that registration runs after the package entry and **before** the rest of `main.ts` adds E2E auxiliary tabs order **`[aiview, …e2e tabs]`** — avoiding `wa-tab-group` “first tab” fallbacks being mistaken for successful `coupledEditors` coupling.
2525

2626
### `coupledEditors` example
2727

packages/app-e2e/src/extensions.ts

Lines changed: 0 additions & 6 deletions
This file was deleted.

packages/app-e2e/src/main.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
/**
2+
* In-repo AI extension registration (after package entry from resolveDepVersionsPlugin’s default extension side-effects).
3+
* Keeps auxiliary tab order before E2E contributions (see README).
4+
*/
5+
import '../../extension-ai-system/src/ai-system-extension';
6+
17
import {
28
TOOLBAR_MAIN,
39
SIDEBAR_AUXILIARY,
@@ -7,7 +13,6 @@ import {
713
} from '@eclipse-docks/core';
814
import { html } from '@eclipse-docks/core/externals/lit';
915

10-
import './extensions';
1116
import './e2e-coupled-panel';
1217

1318
contributionRegistry.registerContribution(SIDEBAR_AUXILIARY, {

packages/app/src/extensions.ts

Lines changed: 0 additions & 34 deletions
This file was deleted.

packages/app/src/main.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { TOOLBAR_MAIN, appLoaderService, contributionRegistry, type HTMLContribu
22
import { html } from '@eclipse-docks/core/externals/lit';
33
import { fetchReleases } from "@eclipse-docks/extension-github-service";
44

5-
import './extensions';
65
import './dashboard-layout';
76

87
contributionRegistry.registerContribution(TOOLBAR_MAIN, {

packages/core/src/vite-plugin-resolve-deps.ts

Lines changed: 144 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,20 +93,161 @@ export function resolveDepVersions(
9393

9494
const RESOLVED_PACKAGE_INFO_KEY = '__RESOLVED_PACKAGE_INFO__';
9595

96-
export function resolveDepVersionsPlugin(options?: {
96+
/** Virtual module id; injected into `index.html` before `main.ts` when extension side-effects are enabled. */
97+
export const VIRTUAL_EXTENSION_IMPORTS = 'virtual:eclipse-docks-extension-imports';
98+
99+
const RESOLVED_VIRTUAL_EXTENSION_IMPORTS = `\0${VIRTUAL_EXTENSION_IMPORTS}`;
100+
101+
const DEFAULT_EXTENSION_PATTERN = /^@eclipse-docks\/extension-/;
102+
103+
const DEFAULT_PRIORITY_FIRST = ['@eclipse-docks/extension-pwa'];
104+
105+
/** Matches Vite’s default app entry in index.html. */
106+
const MAIN_TS_SCRIPT_RE =
107+
/<script\b[^>]*\btype\s*=\s*["']module["'][^>]*\bsrc\s*=\s*["'][^"']*\/src\/main\.ts["'][^>]*>\s*<\/script>/i;
108+
109+
export interface ExtensionSideEffectsOptions {
110+
/**
111+
* When false, disables automatic extension side-effect imports. Omitted or true keeps them on.
112+
*/
113+
enabled?: boolean;
114+
/** Dependency names to skip (even if they match `packageNamePattern`). */
115+
exclude?: string[];
116+
/**
117+
* Packages to load first, in order (only those present in dependencies are imported).
118+
* Default includes PWA so `beforeinstallprompt` can register early.
119+
*/
120+
priorityFirst?: string[];
121+
/**
122+
* Which direct `dependencies` keys qualify as Eclipse Docks extensions.
123+
* @default /^@eclipse-docks\/extension-/
124+
*/
125+
packageNamePattern?: RegExp;
126+
}
127+
128+
export type ResolveDepVersionsPluginOptions = {
97129
includeDevDependencies?: boolean;
98-
}): Plugin {
130+
/**
131+
* By default, registers a virtual module that side-effect-imports every matching direct
132+
* `dependencies` entry (see `ExtensionSideEffectsOptions`), and injects
133+
* `import 'virtual:eclipse-docks-extension-imports'` into `index.html` **before** `/src/main.ts`.
134+
* Pass `false` or `{ enabled: false }` to disable.
135+
*/
136+
extensionSideEffects?: boolean | ExtensionSideEffectsOptions;
137+
};
138+
139+
/** Normalized match options for {@link listExtensionSideEffectPackages}. */
140+
export type ExtensionSideEffectsListOptions = {
141+
exclude: Set<string>;
142+
priorityFirst: string[];
143+
pattern: RegExp;
144+
};
145+
146+
function normalizeExtensionSideEffects(
147+
opt: boolean | ExtensionSideEffectsOptions | undefined,
148+
): ExtensionSideEffectsListOptions | null {
149+
if (opt === false) return null;
150+
if (opt === undefined || opt === true) {
151+
return {
152+
exclude: new Set(),
153+
priorityFirst: [...DEFAULT_PRIORITY_FIRST],
154+
pattern: DEFAULT_EXTENSION_PATTERN,
155+
};
156+
}
157+
if (opt.enabled === false) return null;
158+
return {
159+
exclude: new Set(opt.exclude ?? []),
160+
priorityFirst: opt.priorityFirst ?? [...DEFAULT_PRIORITY_FIRST],
161+
pattern: opt.packageNamePattern ?? DEFAULT_EXTENSION_PATTERN,
162+
};
163+
}
164+
165+
export function listExtensionSideEffectPackages(
166+
dependencies: Record<string, string>,
167+
sideEffects: ExtensionSideEffectsListOptions,
168+
): string[] {
169+
const names = Object.keys(dependencies).filter(
170+
(name) => sideEffects.pattern.test(name) && !sideEffects.exclude.has(name),
171+
);
172+
const prioritySet = new Set(sideEffects.priorityFirst);
173+
const first = sideEffects.priorityFirst.filter((p) => names.includes(p));
174+
const rest = names.filter((n) => !prioritySet.has(n)).sort((a, b) => a.localeCompare(b));
175+
return [...first, ...rest];
176+
}
177+
178+
export function resolveDepVersionsPlugin(
179+
options?: ResolveDepVersionsPluginOptions,
180+
): Plugin {
181+
let appRoot = process.cwd();
182+
let extensionSideEffectsActive = false;
183+
let extensionImportPackages: string[] = [];
184+
99185
return {
100186
name: 'resolve-dep-versions',
101187
config(config) {
102188
const root = config.root ? path.resolve(config.root) : process.cwd();
103189
const info = resolvePackageInfo(root, options);
104-
const value = info ?? { name: '', version: '0.0.0', description: undefined, dependencies: {}, marketplaceCatalogUrls: undefined };
190+
const value =
191+
info ?? {
192+
name: '',
193+
version: '0.0.0',
194+
description: undefined,
195+
dependencies: {},
196+
marketplaceCatalogUrls: undefined,
197+
};
105198
return {
106199
define: {
107200
[RESOLVED_PACKAGE_INFO_KEY]: JSON.stringify(value),
108201
},
109202
};
110203
},
204+
configResolved(config) {
205+
appRoot = path.resolve(config.root ?? process.cwd());
206+
const normalized = normalizeExtensionSideEffects(options?.extensionSideEffects);
207+
extensionSideEffectsActive = normalized !== null;
208+
if (!normalized) {
209+
extensionImportPackages = [];
210+
return;
211+
}
212+
const info = resolvePackageInfo(appRoot, options);
213+
extensionImportPackages = listExtensionSideEffectPackages(
214+
info?.dependencies ?? {},
215+
normalized,
216+
);
217+
},
218+
resolveId(id) {
219+
if (id === VIRTUAL_EXTENSION_IMPORTS) {
220+
return RESOLVED_VIRTUAL_EXTENSION_IMPORTS;
221+
}
222+
return undefined;
223+
},
224+
load(id) {
225+
if (id !== RESOLVED_VIRTUAL_EXTENSION_IMPORTS) {
226+
return null;
227+
}
228+
if (extensionImportPackages.length === 0) {
229+
return 'export {};\n';
230+
}
231+
return extensionImportPackages.map((pkg) => `import ${JSON.stringify(pkg)};\n`).join('');
232+
},
233+
transformIndexHtml: {
234+
order: 'pre',
235+
handler(html) {
236+
if (!extensionSideEffectsActive) {
237+
return html;
238+
}
239+
if (extensionImportPackages.length === 0) {
240+
return html;
241+
}
242+
if (html.includes(VIRTUAL_EXTENSION_IMPORTS)) {
243+
return html;
244+
}
245+
if (!MAIN_TS_SCRIPT_RE.test(html)) {
246+
return html;
247+
}
248+
const inject = `<script type="module">import ${JSON.stringify(VIRTUAL_EXTENSION_IMPORTS)};</script>\n`;
249+
return html.replace(MAIN_TS_SCRIPT_RE, (match) => `${inject}${match}`);
250+
},
251+
},
111252
};
112253
}

0 commit comments

Comments
 (0)