Skip to content

Commit 1e10d75

Browse files
chunjiuxiangechen
authored andcommitted
fix(plugin): improve demo plugin loading and cross-platform packaging
Harden plugin file drag-and-drop handling and normalize file extension matching to improve loading reliability. Update demo plugin manifests and packaging workflow to produce compatible archives and use a shared Node-based packager across platforms. Made-with: Cursor
1 parent f3e3040 commit 1e10d75

File tree

10 files changed

+122
-39
lines changed

10 files changed

+122
-39
lines changed

packages/chili/src/application.ts

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -83,31 +83,13 @@ export class Application implements IApplication {
8383

8484
private initWindowEvents() {
8585
window.onbeforeunload = this.handleWindowUnload;
86-
this.mainWindow?.addEventListener(
87-
"dragstart",
88-
(ev) => {
89-
ev.preventDefault();
90-
},
91-
false,
92-
);
93-
this.mainWindow?.addEventListener(
94-
"dragover",
95-
(ev) => {
96-
ev.stopPropagation();
97-
ev.preventDefault();
98-
ev.dataTransfer!.dropEffect = "copy";
99-
},
100-
false,
101-
);
102-
this.mainWindow?.addEventListener(
103-
"drop",
104-
(ev) => {
105-
ev.stopPropagation();
106-
ev.preventDefault();
107-
this.importFiles(ev.dataTransfer?.files);
108-
},
109-
false,
110-
);
86+
this.mainWindow?.addEventListener("dragstart", this.handleDragStart, false);
87+
// Use capture to intercept file-drop before nested widgets call stopPropagation().
88+
this.mainWindow?.addEventListener("dragover", this.handleDragOver, true);
89+
this.mainWindow?.addEventListener("drop", this.handleDrop, true);
90+
// Fallback: catch drag/drop anywhere in page, also in capture phase.
91+
window.addEventListener("dragover", this.handleDragOver, true);
92+
window.addEventListener("drop", this.handleDrop, true);
11193
}
11294

11395
private readonly handleWindowUnload = (event: BeforeUnloadEvent) => {
@@ -119,6 +101,25 @@ export class Application implements IApplication {
119101
}
120102
};
121103

104+
private readonly handleDragStart = (ev: DragEvent) => {
105+
ev.preventDefault();
106+
};
107+
108+
private readonly handleDragOver = (ev: DragEvent) => {
109+
ev.stopPropagation();
110+
ev.preventDefault();
111+
if (ev.dataTransfer) {
112+
ev.dataTransfer.dropEffect = "copy";
113+
}
114+
};
115+
116+
private readonly handleDrop = (ev: DragEvent) => {
117+
ev.stopPropagation();
118+
ev.preventDefault();
119+
const files = this.extractDroppedFiles(ev.dataTransfer);
120+
this.importFiles(files);
121+
};
122+
122123
async importFiles(files: File[] | FileList | undefined) {
123124
if (!files || files.length === 0) {
124125
return;
@@ -162,9 +163,10 @@ export class Application implements IApplication {
162163
const imports: File[] = [];
163164
const plugins: File[] = [];
164165
for (const element of files) {
165-
if (element.name.endsWith(DOCUMENT_FILE_EXTENSION)) {
166+
const fileName = element.name.toLowerCase();
167+
if (fileName.endsWith(DOCUMENT_FILE_EXTENSION)) {
166168
opens.push(element);
167-
} else if (element.name.endsWith(PLUGIN_FILE_EXTENSION)) {
169+
} else if (fileName.endsWith(PLUGIN_FILE_EXTENSION)) {
168170
plugins.push(element);
169171
} else {
170172
imports.push(element);
@@ -173,6 +175,17 @@ export class Application implements IApplication {
173175
return { opens, imports, plugins };
174176
}
175177

178+
private extractDroppedFiles(dataTransfer: DataTransfer | null): File[] {
179+
if (!dataTransfer) return [];
180+
const fromFileList = Array.from(dataTransfer.files ?? []);
181+
if (fromFileList.length > 0) return fromFileList;
182+
const fromItems = Array.from(dataTransfer.items ?? [])
183+
.filter((item) => item.kind === "file")
184+
.map((item) => item.getAsFile())
185+
.filter((file): file is File => file !== null);
186+
return fromItems;
187+
}
188+
176189
async openDocument(id: string): Promise<IDocument | undefined> {
177190
const document = await Document.open(this, id);
178191
await this.createActiveView(document);

plugins/helloworld-js/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ Package the plugin (creates .chiliplugin file):
2929
npm run package
3030
```
3131

32+
The packaging script is cross-platform and works on both Windows (PowerShell) and macOS/Linux.
33+
3234
## Installation
3335

3436
Drag and drop the .chiliplugin file into Chili3D.

plugins/helloworld-js/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "Hello World JS",
33
"version": "1.0.0",
44
"description": "A demo plugin for Chili3D showing plugin system capabilities",
5-
"main": "src/extension.js",
5+
"main": "extension.js",
66
"author": {
77
"name": "Chili3D Team"
88
},

plugins/helloworld-js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"main": "extension.js",
66
"type": "module",
77
"scripts": {
8-
"package": "rm -rf dist && mkdir dist && zip -r ./dist/helloworld-js.chiliplugin manifest.json ./src/extension.js icons"
8+
"package": "node ../scripts/package-plugin.mjs --entry src/extension.js --output dist/helloworld-js.chiliplugin"
99
},
1010
"keywords": [
1111
"chili3d",

plugins/helloworld-js/src/extension.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ class HelloWorldCommand {
1010

1111
CommandStore.registerCommand(HelloWorldCommand, {
1212
key: "demo.hello",
13-
icon: {
14-
type: "plugin",
15-
path: "icons/hello.svg",
16-
},
13+
icon: "icon-info",
1714
});
1815

1916
const DemoPlugin = {

plugins/helloworld-ts/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ npm run build
4141
npm run package
4242
```
4343

44+
The packaging script is cross-platform and works on both Windows (PowerShell) and macOS/Linux.
45+
4446
## Installation
4547

4648
Drag and drop the `.chiliplugin` file into Chili3D.

plugins/helloworld-ts/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "Demo Plugin",
33
"version": "1.0.0",
44
"description": "A demo plugin for Chili3D showing plugin system capabilities",
5-
"main": "dist/extension.js",
5+
"main": "extension.js",
66
"author": {
77
"name": "Chili3D Team"
88
},

plugins/helloworld-ts/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"type": "module",
77
"scripts": {
88
"build": "rspack build",
9-
"package": "npm run build && zip -r ./dist/helloworld-ts.chiliplugin manifest.json ./dist/extension.js icons"
9+
"package": "npm run build && node ../scripts/package-plugin.mjs --entry dist/extension.js --output dist/helloworld-ts.chiliplugin"
1010
},
1111
"devDependencies": {
1212
"chili-api": "*",

plugins/helloworld-ts/src/commands/hello.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ import { command, type I18nKeys, type IApplication, type ICommand, PubSub } from
55

66
@command({
77
key: "demo.hello" as any,
8-
icon: {
9-
type: "plugin",
10-
path: "icons/hello.svg",
11-
},
8+
icon: "icon-info",
129
helpText: "demo.hello.description" as any,
1310
})
1411
export class HelloWorldCommand implements ICommand {

plugins/scripts/package-plugin.mjs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Part of the Chili3d Project, under the AGPL-3.0 License.
2+
// See LICENSE file in the project root for full license information.
3+
4+
import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
5+
import path from "node:path";
6+
import JSZip from "jszip";
7+
8+
function parseArgs(argv) {
9+
const args = new Map();
10+
for (let i = 0; i < argv.length; i += 2) {
11+
const key = argv[i];
12+
const value = argv[i + 1];
13+
if (!key?.startsWith("--") || value === undefined) {
14+
continue;
15+
}
16+
args.set(key.slice(2), value);
17+
}
18+
return {
19+
entry: args.get("entry"),
20+
output: args.get("output"),
21+
};
22+
}
23+
24+
async function addDirectory(zip, rootDir, zipDir) {
25+
const entries = await readdir(rootDir, { withFileTypes: true });
26+
for (const entry of entries) {
27+
const abs = path.join(rootDir, entry.name);
28+
const zipPath = `${zipDir}/${entry.name}`;
29+
if (entry.isDirectory()) {
30+
await addDirectory(zip, abs, zipPath);
31+
continue;
32+
}
33+
if (entry.isFile()) {
34+
const content = await readFile(abs);
35+
zip.file(zipPath, content);
36+
}
37+
}
38+
}
39+
40+
async function exists(filePath) {
41+
try {
42+
await stat(filePath);
43+
return true;
44+
} catch {
45+
return false;
46+
}
47+
}
48+
49+
async function main() {
50+
const { entry, output } = parseArgs(process.argv.slice(2));
51+
if (!entry || !output) {
52+
throw new Error("Missing required args: --entry <path> --output <path>");
53+
}
54+
55+
const zip = new JSZip();
56+
zip.file("manifest.json", await readFile("manifest.json"));
57+
zip.file("extension.js", await readFile(entry));
58+
59+
if (await exists("icons")) {
60+
await addDirectory(zip, "icons", "icons");
61+
}
62+
63+
await rm("dist", { recursive: true, force: true });
64+
await mkdir(path.dirname(output), { recursive: true });
65+
const data = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
66+
await writeFile(output, data);
67+
}
68+
69+
main().catch((err) => {
70+
console.error(err);
71+
process.exit(1);
72+
});

0 commit comments

Comments
 (0)