Transitive Plugin Dependency Resolution
Transitive Plugin Dependency Resolution
Written for AI agents. See Log Methodology Note below for details.
Background
Design Log #65 introduced makeJayInit for plugin initialization. The dev server discovers plugins by scanning the project's package.json dependencies and executes their init functions in dependency order.
Current discovery in plugin-init-discovery.ts:
// Scans ONLY direct dependencies from project's package.json
const allDeps = {
...projectPackageJson.dependencies,
...projectPackageJson.devDependencies,
};
for (const depName of Object.keys(allDeps)) {
// Try to find plugin.yaml...
}
Problem
Two issues arise with transitive plugin dependencies:
Issue 1: Client Import Resolution in Dev Mode
When wix-stores depends on wix-server-client:
Source file (wix-stores/lib/contexts/wix-stores-context.ts):
import { WIX_CLIENT_CONTEXT } from '@jay-framework/wix-server-client';
Built dist file (wix-stores/dist/index.client.js):
import { WIX_CLIENT_CONTEXT } from '@jay-framework/wix-server-client'; // ❌ Wrong - should be /client
The problem: Both in production (built dist files) and dev mode, the import resolves to the main entry (index.js) instead of the client entry (index.client.js). The Vite build's external configuration preserves the import as-is without transformation.
Issue 2: Transitive Plugin Discovery
When a project only has wix-stores as a direct dependency:
// Project package.json
{
"dependencies": {
"@jay-framework/wix-stores": "workspace:^"
// wix-server-client is NOT listed (it's a transitive dep)
}
}
The discoverPluginsWithInit function only scans direct dependencies, so:
wix-storesis discovered ✅wix-server-clientis NOT discovered ❌
Result: wix-server-client's server init doesn't run → WIX_CLIENT_SERVICE is not registered → wix-stores init fails when calling getService(WIX_CLIENT_SERVICE).
Questions and Answers
Q1: Should plugin dependencies be explicitly declared in plugin.yaml? A: No, use existing package.json dependencies. This is already available and avoids redundancy.
Q2: How deep should transitive discovery go?
A: Recursively discover all plugin dependencies until no new plugins are found. Plugins are identified by having a plugin.yaml.
Q3: For Issue 1, where should the import transformation happen?
A: In the Jay Stack compiler's Vite plugin, we should transform imports from plugin packages to use the /client subpath when in client context.
Design
Fix for Issue 2: Recursive Plugin Discovery
Modify discoverPluginsWithInit to recursively discover transitive plugin dependencies:
export async function discoverPluginsWithInit(
options: PluginInitDiscoveryOptions,
): Promise<PluginWithInit[]> {
const plugins: PluginWithInit[] = [];
const visitedPackages = new Set<string>();
// Queue of package names to check
const packagesToCheck: string[] = [];
// 1. Seed with project's runtime dependencies (not devDependencies)
const projectPackageJson = JSON.parse(
fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf-8'),
);
packagesToCheck.push(...Object.keys(projectPackageJson.dependencies || {}));
// 2. Process queue, discovering transitive plugin dependencies
while (packagesToCheck.length > 0) {
const depName = packagesToCheck.shift()!;
if (visitedPackages.has(depName)) continue;
visitedPackages.add(depName);
// Check if this package is a Jay plugin
const pluginInfo = await tryDiscoverPlugin(depName, projectRoot);
if (!pluginInfo) continue;
plugins.push(pluginInfo);
// Add this plugin's dependencies to the queue
packagesToCheck.push(...pluginInfo.dependencies);
}
return plugins;
}
Fix for Issue 1: Client Import Transformation
Add a Vite plugin transformation that rewrites plugin package imports in client context.
Important: Use transform hook, not resolveId, because rollup's external option is evaluated before resolveId hooks. By transforming the source code, we change the import path before any resolution or external checking happens.
In jayStackCompiler (via plugin-client-import-resolver.ts):
transform(code, id, options) {
// Only transform in client context
if (options?.ssr) return null;
// Use regex to find and replace import declarations
// Rewrite: import { X } from '@jay-framework/wix-server-client'
// To: import { X } from '@jay-framework/wix-server-client/client'
const IMPORT_REGEX = /import\s+(.+?)\s+from\s+(['"])([^'"]+)\2/g;
return code.replace(IMPORT_REGEX, (match, clause, quote, source) => {
const packageName = extractPackageName(source);
if (!isJayPluginWithClientExport(packageName)) return match;
if (isSubpathImport(source, packageName)) return match;
return `import ${clause} from ${quote}${packageName}/client${quote}`;
});
}
Implementation Plan
Phase 1: Recursive Plugin Discovery (Issue 2)
Modify
discoverPluginsWithInitinplugin-init-discovery.ts:- Add
visitedPackages: Set<string>to avoid cycles - Add queue-based traversal of plugin dependencies
- Test with wix-stores → wix-server-client dependency chain
- Add
Update tests to verify transitive discovery
Phase 2: Client Import Transformation (Issue 1)
- Add import transformation logic to the Jay Stack Vite plugin
- Detect when code is being processed for client bundle
- Transform imports from plugin packages to use
/clientsubpath - Only apply when the package exports a
/clientsubpath
Trade-offs
| Approach | Pros | Cons |
|---|---|---|
| Recursive discovery | Simple, uses existing package.json | Could be slow with many packages |
| Explicit deps in plugin.yaml | Clear declaration | Redundant with package.json |
| Import transformation | Works transparently | Additional compile step |
| Vite aliases | Simple config | Must be set for each plugin |
Examples
Before (Issue 2)
Project has: wix-stores
wix-stores depends on: wix-server-client
Discovered: [wix-stores] ❌ Missing wix-server-client
After
Project has: wix-stores
wix-stores depends on: wix-server-client
Discovered: [wix-server-client, wix-stores] ✅ Both discovered
Init order: wix-server-client → wix-stores ✅ Correct order
Implementation Results
Phase 1: Recursive Plugin Discovery
Modified plugin-init-discovery.ts:
- Added
visitedPackages: Set<string>to track visited packages - Changed NPM plugin scanning to use queue-based traversal
- For each discovered plugin, its dependencies are added to the queue
- Transitive plugin dependencies are now discovered automatically
Key changes:
// Queue-based traversal for transitive dependencies
const packagesToCheck = [...initialDeps];
while (packagesToCheck.length > 0) {
const depName = packagesToCheck.shift()!;
if (visitedPackages.has(depName)) continue;
visitedPackages.add(depName);
// ... discover plugin ...
// Add this plugin's dependencies to the queue
for (const transitiveDep of dependencies) {
if (!visitedPackages.has(transitiveDep)) {
packagesToCheck.push(transitiveDep);
}
}
}
Phase 2: Client Import Transformation
Created new file plugin-client-import-resolver.ts:
createPluginClientImportResolver()Vite plugin- Uses
transformhook (notresolveId) to rewrite import declarations - The
transformapproach ensures rewrites happen BEFORE rollup'sexternaloption is evaluated - In client builds (options.ssr = false), rewrites main entry imports to /client
- Detects Jay plugins by checking for plugin.yaml
- Verifies /client export exists before rewriting
- Caches plugin detection results for performance
- Handles both
import ... fromandexport ... fromdeclarations
Added to jayStackCompiler() in index.ts:
// Plugin client import resolver - rewrites plugin package imports to /client
plugins.push(createPluginClientImportResolver({ verbose: shouldTrackImports }));
Note: Using transform instead of resolveId is critical because rollup's external configuration is evaluated before resolveId hooks can intercept the import. By transforming the source code, we change the import path before any resolution happens.
Phase 3: Client Init Filtering for Transitive Dependencies
The dev server filters which plugin inits to include in each page's client script based on usedPackages. However, this only included directly-used packages, not their transitive plugin dependencies.
Fixed in dev-server/lib/dev-server.ts:
- Added
filterPluginsForPage()function - Expands
usedPackagesto include transitive plugin dependencies - Uses the plugin's
dependenciesarray to find plugin-to-plugin dependencies
function filterPluginsForPage(
allPluginClientInits: PluginClientInitInfo[],
allPluginsWithInit: PluginWithInit[],
usedPackages: Set<string>,
): PluginClientInitInfo[] {
// Expand usedPackages to include transitive plugin dependencies
const expandedPackages = new Set<string>(usedPackages);
const toProcess = [...usedPackages];
while (toProcess.length > 0) {
const packageName = toProcess.pop()!;
const plugin = pluginsByPackage.get(packageName);
if (!plugin) continue;
for (const dep of plugin.dependencies) {
if (pluginsByPackage.has(dep) && !expandedPackages.has(dep)) {
expandedPackages.add(dep);
toProcess.push(dep);
}
}
}
// ...filter using expandedPackages
}
Files Changed
stack-server-runtime/lib/plugin-init-discovery.ts- Added transitive dependency discovery
compiler-jay-stack/lib/plugin-client-import-resolver.ts(new)- Vite plugin for client import resolution
- Extracted
PluginDetectorinterface for testability
compiler-jay-stack/lib/index.ts- Added import and usage of plugin client import resolver
- Exported new utilities
dev-server/lib/dev-server.ts- Added
filterPluginsForPage()to include transitive plugin dependencies in client init
- Added
Log Methodology Note
Note: These design logs are written primarily for AI agents as part of the Design Log methodology and made accessible here for human readers. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation.