Skip to content

Commit 9f70317

Browse files
committed
refactor: streamline package version resolution and enhance extension loading process
- Renamed and refactored functions for better clarity in package version resolution. - Introduced a new method to collect transitive extension dependencies from package.json files. - Improved the extension loading sequence to ensure dependencies are loaded before settings extensions. - Added tests for the new functionality to ensure correct behavior in resolving extension dependencies.
1 parent d7ca574 commit 9f70317

4 files changed

Lines changed: 212 additions & 31 deletions

File tree

packages/core/src/core/apploader.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -416,14 +416,8 @@ class AppLoaderService {
416416
this.systemRequiredExtensions.forEach(extId => extensionsSet.add(extId));
417417
app.extensions = Array.from(extensionsSet);
418418

419-
// At this point, all core/built-in extension modules and app contributions
420-
// have been registered. It is now safe for the extension registry to
421-
// load any extensions that are marked as enabled in settings (including
422-
// persisted external extensions).
423-
this.dispatchLoadProgress('Loading extensions…');
424-
await extensionRegistry.loadEnabledExtensions();
425-
426-
// Enable and load app extensions so commands/contributions are registered before UI is shown
419+
// Enable and load app extensions first so declared dependencies
420+
// are loaded before persisted settings extensions run.
427421
if (app.extensions.length > 0) {
428422
this.dispatchLoadProgress('Enabling extensions…');
429423
await Promise.all(
@@ -435,6 +429,11 @@ class AppLoaderService {
435429
);
436430
}
437431

432+
// Load any additional extensions marked as enabled in settings (including
433+
// persisted external extensions).
434+
this.dispatchLoadProgress('Loading extensions…');
435+
await extensionRegistry.loadEnabledExtensions();
436+
438437
// Initialize new app
439438
if (app.initialize) {
440439
this.dispatchLoadProgress('Initializing…');

packages/core/src/core/extensionregistry.ts

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,24 @@ class ExtensionRegistry {
301301
* await extensionRegistry.load('@eclipse-docks/extension-notebook')
302302
* ```
303303
*/
304+
/**
305+
* Extension ids match npm package names. When a dependency was not side-effect-imported
306+
* (e.g. only a transitive package.json dependency), importing the package registers it.
307+
*/
308+
private async ensureExtensionRegistered(extensionId: string): Promise<void> {
309+
if (this.extensions[extensionId]) {
310+
return;
311+
}
312+
try {
313+
await import(/* @vite-ignore */ extensionId);
314+
} catch (error) {
315+
logger.debug(`Could not side-effect import extension package ${extensionId}: ${error}`);
316+
}
317+
if (!this.extensions[extensionId]) {
318+
throw new Error("Extension not found: " + extensionId);
319+
}
320+
}
321+
304322
public async load(extensionId: string, loadingChain: string[] = []): Promise<void> {
305323
// Already loaded, return immediately
306324
if (this.loadedExtensions.has(extensionId)) {
@@ -321,15 +339,20 @@ class ExtensionRegistry {
321339

322340
const extension = this.extensions[extensionId]
323341
if (!extension) {
342+
await this.ensureExtensionRegistered(extensionId);
343+
}
344+
345+
const resolvedExtension = this.extensions[extensionId]
346+
if (!resolvedExtension) {
324347
throw new Error("Extension not found: " + extensionId)
325348
}
326349

327350
const loadingPromise = (async () => {
328351
try {
329352
logger.debug(`Loading extension: ${extensionId}`);
330-
if (extension.dependencies && extension.dependencies.length > 0) {
353+
if (resolvedExtension.dependencies && resolvedExtension.dependencies.length > 0) {
331354
const newChain = [...loadingChain, extensionId]
332-
for (const depId of extension.dependencies) {
355+
for (const depId of resolvedExtension.dependencies) {
333356
await this.load(depId, newChain)
334357
// Enable the dependency if it's not already enabled
335358
if (!this.isEnabled(depId)) {
@@ -339,14 +362,14 @@ class ExtensionRegistry {
339362
}
340363
}
341364

342-
const module = await taskService.runAsync("Loading extension: " + extension.name, async () => {
343-
if (extension.loader) {
344-
return extension.loader()
345-
} else if (extension.url) {
346-
let finalUrl = extension.url;
347-
if (esmShService.isSourceIdentifier(extension.url)) {
348-
finalUrl = esmShService.normalizeToEsmSh(extension.url);
349-
logger.debug(`Normalized extension URL: ${extension.url} -> ${finalUrl}`);
365+
const module = await taskService.runAsync("Loading extension: " + resolvedExtension.name, async () => {
366+
if (resolvedExtension.loader) {
367+
return resolvedExtension.loader()
368+
} else if (resolvedExtension.url) {
369+
let finalUrl = resolvedExtension.url;
370+
if (esmShService.isSourceIdentifier(resolvedExtension.url)) {
371+
finalUrl = esmShService.normalizeToEsmSh(resolvedExtension.url);
372+
logger.debug(`Normalized extension URL: ${resolvedExtension.url} -> ${finalUrl}`);
350373
}
351374
return import(/* @vite-ignore */ finalUrl)
352375
}

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

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,31 +18,41 @@ interface PackageJson {
1818
devDependencies?: Record<string, string>;
1919
}
2020

21-
function findPackageVersion(appRoot: string, depName: string): string | null {
22-
const segments = depName.startsWith('@')
23-
? depName.split('/')
24-
: [depName];
21+
function packageJsonPath(appRoot: string, packageName: string): string | null {
22+
const segments = packageName.startsWith('@')
23+
? packageName.split('/')
24+
: [packageName];
2525
const relativePath = path.join('node_modules', ...segments, 'package.json');
2626
let dir = path.resolve(appRoot);
2727
const root = path.parse(dir).root;
2828

2929
while (true) {
3030
const pkgPath = path.join(dir, relativePath);
3131
if (existsSync(pkgPath)) {
32-
try {
33-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as PackageJson;
34-
if (typeof pkg.version === 'string') return pkg.version;
35-
} catch {
36-
// ignore parse errors
37-
}
38-
return null;
32+
return pkgPath;
3933
}
4034
if (dir === root) break;
4135
dir = path.dirname(dir);
4236
}
4337
return null;
4438
}
4539

40+
function readPackageJson(appRoot: string, packageName: string): PackageJson | null {
41+
const pkgPath = packageJsonPath(appRoot, packageName);
42+
if (!pkgPath) return null;
43+
try {
44+
return JSON.parse(readFileSync(pkgPath, 'utf8')) as PackageJson;
45+
} catch {
46+
return null;
47+
}
48+
}
49+
50+
function findPackageVersion(appRoot: string, depName: string): string | null {
51+
const pkg = readPackageJson(appRoot, depName);
52+
if (typeof pkg?.version === 'string') return pkg.version;
53+
return null;
54+
}
55+
4656
function resolveDepVersionsFromPkg(
4757
appRoot: string,
4858
pkg: PackageJson,
@@ -176,6 +186,51 @@ export function listExtensionSideEffectPackages(
176186
return [...first, ...rest];
177187
}
178188

189+
/**
190+
* Walks extension package.json `dependencies` to collect transitive extension-* packages
191+
* (e.g. extension-utils → extension-python-runtime → extension-terminal).
192+
*/
193+
export function collectTransitiveExtensionPackages(
194+
appRoot: string,
195+
seeds: string[],
196+
sideEffects: ExtensionSideEffectsListOptions,
197+
): string[] {
198+
const found = new Set<string>();
199+
const queue = [...seeds];
200+
const visited = new Set<string>();
201+
202+
while (queue.length > 0) {
203+
const name = queue.shift()!;
204+
if (visited.has(name)) continue;
205+
visited.add(name);
206+
207+
const pkg = readPackageJson(appRoot, name);
208+
if (!pkg?.dependencies) continue;
209+
210+
for (const dep of Object.keys(pkg.dependencies)) {
211+
if (!sideEffects.pattern.test(dep) || sideEffects.exclude.has(dep)) continue;
212+
found.add(dep);
213+
queue.push(dep);
214+
}
215+
}
216+
217+
return [...found];
218+
}
219+
220+
export function resolveExtensionSideEffectPackages(
221+
appRoot: string,
222+
dependencies: Record<string, string>,
223+
sideEffects: ExtensionSideEffectsListOptions,
224+
): string[] {
225+
const direct = listExtensionSideEffectPackages(dependencies, sideEffects);
226+
const transitive = collectTransitiveExtensionPackages(appRoot, direct, sideEffects);
227+
const allNames = [...new Set([...direct, ...transitive])];
228+
return listExtensionSideEffectPackages(
229+
Object.fromEntries(allNames.map((name) => [name, '*'])),
230+
sideEffects,
231+
);
232+
}
233+
179234
export function resolveDepVersionsPlugin(
180235
options?: ResolveDepVersionsPluginOptions,
181236
): Plugin {
@@ -211,7 +266,8 @@ export function resolveDepVersionsPlugin(
211266
return;
212267
}
213268
const info = resolvePackageInfo(appRoot, options);
214-
extensionImportPackages = listExtensionSideEffectPackages(
269+
extensionImportPackages = resolveExtensionSideEffectPackages(
270+
appRoot,
215271
info?.dependencies ?? {},
216272
normalized,
217273
);

packages/core/test/units/vite-plugin-resolve-deps.test.ts

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
1+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
22
import { tmpdir } from 'node:os';
33
import path from 'node:path';
44
import { describe, expect, it } from 'vitest';
55
import type { IndexHtmlTransformContext, Plugin, ResolvedConfig } from 'vite';
66
import {
7+
collectTransitiveExtensionPackages,
78
listExtensionSideEffectPackages,
89
resolveDepVersionsPlugin,
10+
resolveExtensionSideEffectPackages,
911
VIRTUAL_EXTENSION_IMPORTS,
1012
} from '../../src/vite-plugin-resolve-deps';
1113

@@ -75,6 +77,107 @@ describe('listExtensionSideEffectPackages', () => {
7577
});
7678
});
7779

80+
describe('collectTransitiveExtensionPackages', () => {
81+
it('collects extension dependencies from package.json files', () => {
82+
withTempPackageJson(
83+
{
84+
'@eclipse-docks/extension-utils': '*',
85+
},
86+
(root) => {
87+
const utilsDir = path.join(root, 'node_modules', '@eclipse-docks', 'extension-utils');
88+
const runtimeDir = path.join(
89+
root,
90+
'node_modules',
91+
'@eclipse-docks',
92+
'extension-python-runtime',
93+
);
94+
mkdirSync(utilsDir, { recursive: true });
95+
mkdirSync(runtimeDir, { recursive: true });
96+
writeFileSync(
97+
path.join(utilsDir, 'package.json'),
98+
JSON.stringify({
99+
name: '@eclipse-docks/extension-utils',
100+
dependencies: {
101+
'@eclipse-docks/extension-python-runtime': '*',
102+
},
103+
}),
104+
);
105+
writeFileSync(
106+
path.join(runtimeDir, 'package.json'),
107+
JSON.stringify({
108+
name: '@eclipse-docks/extension-python-runtime',
109+
dependencies: {
110+
'@eclipse-docks/extension-terminal': '*',
111+
},
112+
}),
113+
);
114+
115+
expect(
116+
collectTransitiveExtensionPackages(
117+
root,
118+
['@eclipse-docks/extension-utils'],
119+
defaultSideEffects,
120+
),
121+
).toEqual([
122+
'@eclipse-docks/extension-python-runtime',
123+
'@eclipse-docks/extension-terminal',
124+
]);
125+
},
126+
);
127+
});
128+
});
129+
130+
describe('resolveExtensionSideEffectPackages', () => {
131+
it('merges direct and transitive extension packages', () => {
132+
withTempPackageJson(
133+
{
134+
'@eclipse-docks/extension-utils': '*',
135+
},
136+
(root) => {
137+
const utilsDir = path.join(root, 'node_modules', '@eclipse-docks', 'extension-utils');
138+
const runtimeDir = path.join(
139+
root,
140+
'node_modules',
141+
'@eclipse-docks',
142+
'extension-python-runtime',
143+
);
144+
mkdirSync(utilsDir, { recursive: true });
145+
mkdirSync(runtimeDir, { recursive: true });
146+
writeFileSync(
147+
path.join(utilsDir, 'package.json'),
148+
JSON.stringify({
149+
name: '@eclipse-docks/extension-utils',
150+
dependencies: {
151+
'@eclipse-docks/extension-python-runtime': '*',
152+
},
153+
}),
154+
);
155+
writeFileSync(
156+
path.join(runtimeDir, 'package.json'),
157+
JSON.stringify({
158+
name: '@eclipse-docks/extension-python-runtime',
159+
dependencies: {
160+
'@eclipse-docks/extension-terminal': '*',
161+
},
162+
}),
163+
);
164+
165+
expect(
166+
resolveExtensionSideEffectPackages(
167+
root,
168+
{ '@eclipse-docks/extension-utils': '*' },
169+
defaultSideEffects,
170+
),
171+
).toEqual([
172+
'@eclipse-docks/extension-python-runtime',
173+
'@eclipse-docks/extension-terminal',
174+
'@eclipse-docks/extension-utils',
175+
]);
176+
},
177+
);
178+
});
179+
});
180+
78181
function runConfigResolved(plugin: Plugin, root: string) {
79182
const cr = plugin.configResolved;
80183
if (typeof cr === 'function') {

0 commit comments

Comments
 (0)