Skip to content
Draft
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
9 changes: 9 additions & 0 deletions packages/shared/src/cli/commands/plugin/sync/sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ describe("plugin sync", () => {
expect(imports[0].source).toBe("./plugins/my-plugin");
});

it("extracts relative imports with file extension (.js)", () => {
const imports = parseCode(
`import { myPlugin } from "./my-plugin/my-plugin.js";`,
);
expect(imports).toHaveLength(1);
expect(imports[0].name).toBe("myPlugin");
expect(imports[0].source).toBe("./my-plugin/my-plugin.js");
});

it("handles double-quoted specifiers", () => {
const imports = parseCode(`import { foo } from "@databricks/appkit";`);
expect(imports[0].source).toBe("@databricks/appkit");
Expand Down
35 changes: 28 additions & 7 deletions packages/shared/src/cli/commands/plugin/sync/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,14 @@ function resolveLocalManifest(
return null;
}

// Case 0: Import path already includes an extension and resolves to a file
// e.g. ./my-plugin/my-plugin.js → ./my-plugin/manifest.json
if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) {
const dir = path.dirname(resolved);
const manifestPath = path.join(dir, "manifest.json");
if (fs.existsSync(manifestPath)) return manifestPath;
}

// Case 1: Import path is a directory with manifest.json
// e.g. ./plugins/my-plugin → ./plugins/my-plugin/manifest.json
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
Expand All @@ -219,9 +227,13 @@ function resolveLocalManifest(

// Case 2: Import path + extension resolves to a file
// e.g. ./plugins/my-plugin → ./plugins/my-plugin.ts
// Look for manifest.json in the same directory
// If resolved already has an extension (e.g. .js), try base path + each ext
// so that ./my-plugin/my-plugin.js finds ./my-plugin/my-plugin.ts on disk
const basePath = path.extname(resolved).match(/^\.(js|ts|tsx|jsx)$/)
? resolved.slice(0, -path.extname(resolved).length)
: resolved;
for (const ext of RESOLVE_EXTENSIONS) {
const filePath = `${resolved}${ext}`;
const filePath = `${basePath}${ext}`;
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
const dir = path.dirname(filePath);
const manifestPath = path.join(dir, "manifest.json");
Expand Down Expand Up @@ -561,14 +573,23 @@ function runPluginsSync(options: {
let plugin: TemplatePlugin | undefined;

if (isLocal) {
// Resolve the import source to an absolute path from the server file directory
const resolvedImportDir = path.resolve(serverFileDir, imp.source);
// Resolve the import source to the plugin directory for comparison.
// When the path is a file (or has an extension but file is .ts on disk),
// use its directory so ./my-plugin/my-plugin.js matches plugin at ./my-plugin
const resolvedImportPath = path.resolve(serverFileDir, imp.source);
const exists = fs.existsSync(resolvedImportPath);
const resolvedImportDir =
exists && fs.statSync(resolvedImportPath).isFile()
? path.dirname(resolvedImportPath)
: exists && fs.statSync(resolvedImportPath).isDirectory()
? resolvedImportPath
: path.dirname(resolvedImportPath);
// Match by directory only: one manifest per dir, and manifest.name may be
// kebab-case (e.g. "my-plugin") while the import is camelCase (myPlugin)
plugin = Object.values(plugins).find((p) => {
if (!p.package.startsWith(".")) return false;
const resolvedPluginDir = path.resolve(cwd, p.package);
return (
resolvedPluginDir === resolvedImportDir && p.name === imp.originalName
);
return resolvedPluginDir === resolvedImportDir;
});
} else {
// npm import: direct string comparison
Expand Down