@dimina-kit/devtools 0.4.0-dev.20260828092243 → 0.4.0-dev.20260831134112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main/app/app.js +4 -0
- package/dist/main/index.bundle.js +99 -11
- package/dist/main/ipc/projects.d.ts +1 -1
- package/dist/main/ipc/projects.js +25 -1
- package/dist/main/services/projects/create-project-service.js +9 -2
- package/dist/main/services/projects/local-provider.js +6 -0
- package/dist/main/services/projects/project-repository.d.ts +25 -0
- package/dist/main/services/projects/project-repository.js +88 -12
- package/dist/main/services/projects/types.d.ts +13 -2
- package/dist/main/services/workbench-context.d.ts +18 -4
- package/dist/main/services/workbench-context.js +1 -0
- package/dist/main/services/workspace/workspace-service.d.ts +2 -0
- package/dist/main/services/workspace/workspace-service.js +14 -0
- package/dist/preload/windows/simulator.cjs.map +1 -1
- package/dist/render-host/pageFrame.html +1 -1
- package/dist/render-host/preload.cjs +16 -4
- package/dist/renderer/assets/{button-DsNqZm7o.js → button-D5Q2wkXq.js} +2 -2
- package/dist/renderer/assets/{createLucideIcon-CYJeT1ji.js → createLucideIcon-Bqi8hpdi.js} +2 -2
- package/dist/renderer/assets/{dialog-pT4qWmka.js → dialog-e1TecTOY.js} +3 -3
- package/dist/renderer/assets/{dist-o70xmpc1.js → dist-DKWdE1TK.js} +2 -2
- package/dist/renderer/assets/{hostSidebarDefault-NvGZlE9O.js → hostSidebarDefault-B3yLfy8j.js} +2 -2
- package/dist/renderer/assets/index-CsPD68N_.js +5 -0
- package/dist/renderer/assets/{input-4QxTXMZN.js → input-Ct3alvkq.js} +2 -2
- package/dist/renderer/assets/ipc-channels-overlays-LsDzGAtV.js +2 -0
- package/dist/renderer/assets/jsx-runtime-2vXHouwr.css +1 -0
- package/dist/renderer/assets/{jsx-runtime-CqVdW3eg.js → jsx-runtime-DGLHmmOq.js} +1 -1
- package/dist/renderer/assets/{popover-CJiSWwuE.js → popover-Dix5cm_b.js} +2 -2
- package/dist/renderer/assets/project-api-k7Oq9iaq.js +2 -0
- package/dist/renderer/assets/{projectCreateDialog-DMWNhw_1.js → projectCreateDialog-Bbjz75bP.js} +2 -2
- package/dist/renderer/assets/{select-C86pNcsu.js → select-acGb0AmJ.js} +2 -2
- package/dist/renderer/assets/{settings-BoI8mLT1.js → settings-CQrTMr43.js} +2 -2
- package/dist/renderer/assets/{settings-api-Bs_g4NHs.js → settings-api-CrUQ9rYV.js} +2 -2
- package/dist/renderer/assets/{settings-tab-bar-DHLLygqv.js → settings-tab-bar-Cw1kVIf2.js} +2 -2
- package/dist/renderer/assets/{tooltip-BhAyY0T4.js → tooltip-B_-_YVbw.js} +2 -2
- package/dist/renderer/assets/{types-YYjkvane.js → types-CsEKtFyr.js} +2 -2
- package/dist/renderer/assets/{updateDialog-DPum3wx1.js → updateDialog-NsF8223A.js} +2 -2
- package/dist/renderer/assets/{view-api-DFhb2R8-.js → view-api-CLn3rcQE.js} +2 -2
- package/dist/renderer/assets/{workbenchSettings-DrajRQUk.js → workbenchSettings-CV4LVA9b.js} +2 -2
- package/dist/renderer/entries/host-sidebar-default/index.html +5 -5
- package/dist/renderer/entries/main/index.html +14 -12
- package/dist/renderer/entries/popover/index.html +8 -8
- package/dist/renderer/entries/project-create-dialog/index.html +11 -11
- package/dist/renderer/entries/settings/index.html +9 -9
- package/dist/renderer/entries/tooltip/index.html +5 -5
- package/dist/renderer/entries/update-dialog/index.html +9 -9
- package/dist/renderer/entries/workbench-settings/index.html +9 -9
- package/dist/shared/ipc-channels.d.ts +4 -0
- package/dist/shared/ipc-channels.js +4 -0
- package/dist/shared/ipc-schemas.d.ts +11 -0
- package/dist/shared/ipc-schemas.js +14 -0
- package/dist/shared/types.d.ts +61 -0
- package/package.json +8 -8
- package/dist/renderer/assets/index-zErhG3ta.js +0 -5
- package/dist/renderer/assets/ipc-channels-overlays-Blr5TNoB.js +0 -2
- package/dist/renderer/assets/jsx-runtime-B-3rq1y5.css +0 -1
- package/dist/renderer/assets/project-api-DZ1Zi6EC.js +0 -2
package/dist/main/app/app.js
CHANGED
|
@@ -169,6 +169,10 @@ function createContext(config, mainWindow, rendererDir) {
|
|
|
169
169
|
projectTemplates: config.projectTemplates,
|
|
170
170
|
builtinTemplates: config.builtinTemplates,
|
|
171
171
|
customCreateProjectDialog: config.customCreateProjectDialog,
|
|
172
|
+
// No cast needed here: the hook receives the project record, so the
|
|
173
|
+
// main-process `Project` the context hands it satisfies the structural
|
|
174
|
+
// `EditableProject` the config declares.
|
|
175
|
+
customEditProjectDialog: config.customEditProjectDialog,
|
|
172
176
|
onBeforeOpenProject: config.onBeforeOpenProject,
|
|
173
177
|
});
|
|
174
178
|
}
|
|
@@ -485,10 +485,14 @@ var ProjectsChannel = {
|
|
|
485
485
|
List: "projects:list",
|
|
486
486
|
Add: "projects:add",
|
|
487
487
|
Remove: "projects:remove",
|
|
488
|
+
/** Edit an existing record's name / icon; its path stays the identity. */
|
|
489
|
+
Update: "projects:update",
|
|
488
490
|
/** Merged + sanitized template catalog for the create-project dialog. */
|
|
489
491
|
ListTemplates: "projects:listTemplates",
|
|
490
492
|
/** Host-supplied create-project dialog hook (returns input or null). */
|
|
491
493
|
OpenCreateDialog: "projects:openCreateDialog",
|
|
494
|
+
/** Host-supplied edit-project dialog hook; `null` means no hook is configured. */
|
|
495
|
+
OpenEditDialog: "projects:openEditDialog",
|
|
492
496
|
/** Server-side scaffold + register (delegates to create-project-service). */
|
|
493
497
|
Create: "projects:create",
|
|
494
498
|
/** Default values used to pre-fill the create-project dialog (baseDir). */
|
|
@@ -6391,6 +6395,7 @@ __export(project_repository_exports, {
|
|
|
6391
6395
|
removeProject: () => removeProject,
|
|
6392
6396
|
saveCompileConfig: () => saveCompileConfig,
|
|
6393
6397
|
updateLastOpened: () => updateLastOpened,
|
|
6398
|
+
updateProject: () => updateProject,
|
|
6394
6399
|
updateProjectSettings: () => updateProjectSettings,
|
|
6395
6400
|
validateProjectDir: () => validateProjectDir
|
|
6396
6401
|
});
|
|
@@ -6411,6 +6416,30 @@ function readProjectConfig(dirPath) {
|
|
|
6411
6416
|
}
|
|
6412
6417
|
return merged;
|
|
6413
6418
|
}
|
|
6419
|
+
function readProjectName(dirPath) {
|
|
6420
|
+
const raw = readProjectConfig(dirPath).projectname;
|
|
6421
|
+
if (typeof raw !== "string" || !raw.trim()) return null;
|
|
6422
|
+
try {
|
|
6423
|
+
return decodeURIComponent(raw);
|
|
6424
|
+
} catch {
|
|
6425
|
+
return raw;
|
|
6426
|
+
}
|
|
6427
|
+
}
|
|
6428
|
+
function writeProjectName(dirPath, name) {
|
|
6429
|
+
const configPath = path9.join(dirPath, "project.private.config.json");
|
|
6430
|
+
let config = {};
|
|
6431
|
+
if (fs5.existsSync(configPath)) {
|
|
6432
|
+
try {
|
|
6433
|
+
config = JSON.parse(fs5.readFileSync(configPath, "utf-8"));
|
|
6434
|
+
} catch {
|
|
6435
|
+
throw new Error(`\u65E0\u6CD5\u91CD\u547D\u540D\uFF1A${configPath} \u4E0D\u662F\u5408\u6CD5 JSON`);
|
|
6436
|
+
}
|
|
6437
|
+
}
|
|
6438
|
+
fs5.writeFileSync(
|
|
6439
|
+
configPath,
|
|
6440
|
+
JSON.stringify({ ...config, projectname: encodeURIComponent(name) }, null, 2)
|
|
6441
|
+
);
|
|
6442
|
+
}
|
|
6414
6443
|
function detectRuntimeType(dirPath) {
|
|
6415
6444
|
const compileType = readProjectConfig(dirPath).compileType;
|
|
6416
6445
|
if (compileType === "game") return "minigame";
|
|
@@ -6472,16 +6501,7 @@ function hasProject(dirPath) {
|
|
|
6472
6501
|
}
|
|
6473
6502
|
function addProject(dirPath) {
|
|
6474
6503
|
const projects = load();
|
|
6475
|
-
|
|
6476
|
-
try {
|
|
6477
|
-
const configPath = path9.join(dirPath, "project.config.json");
|
|
6478
|
-
if (fs5.existsSync(configPath)) {
|
|
6479
|
-
const cfg = JSON.parse(fs5.readFileSync(configPath, "utf-8"));
|
|
6480
|
-
if (cfg.projectname) name = cfg.projectname;
|
|
6481
|
-
}
|
|
6482
|
-
} catch (err2) {
|
|
6483
|
-
log2.warn("Failed to read project name from config", err2);
|
|
6484
|
-
}
|
|
6504
|
+
const name = readProjectName(dirPath) ?? path9.basename(dirPath);
|
|
6485
6505
|
const type = detectRuntimeType(dirPath);
|
|
6486
6506
|
const project = { name, path: dirPath, lastOpened: null, type };
|
|
6487
6507
|
const idx = projects.findIndex((p) => p.path === dirPath);
|
|
@@ -6496,6 +6516,26 @@ function addProject(dirPath) {
|
|
|
6496
6516
|
function removeProject(dirPath) {
|
|
6497
6517
|
save(load().filter((p) => p.path !== dirPath));
|
|
6498
6518
|
}
|
|
6519
|
+
function updateProject(dirPath, patch) {
|
|
6520
|
+
const projects = load();
|
|
6521
|
+
const idx = projects.findIndex((p) => p.path === dirPath);
|
|
6522
|
+
if (idx < 0) return null;
|
|
6523
|
+
const next = { ...projects[idx] };
|
|
6524
|
+
if (patch.name !== void 0) {
|
|
6525
|
+
const name = patch.name.trim();
|
|
6526
|
+
if (!name) throw new Error("Project name cannot be empty");
|
|
6527
|
+
next.name = name;
|
|
6528
|
+
}
|
|
6529
|
+
if (patch.iconUrl !== void 0) {
|
|
6530
|
+
const iconUrl = patch.iconUrl.trim();
|
|
6531
|
+
if (iconUrl) next.iconUrl = iconUrl;
|
|
6532
|
+
else delete next.iconUrl;
|
|
6533
|
+
}
|
|
6534
|
+
if (next.name !== projects[idx].name) writeProjectName(dirPath, next.name);
|
|
6535
|
+
projects[idx] = next;
|
|
6536
|
+
save(projects);
|
|
6537
|
+
return next;
|
|
6538
|
+
}
|
|
6499
6539
|
function updateLastOpened(dirPath) {
|
|
6500
6540
|
const projects = load();
|
|
6501
6541
|
const idx = projects.findIndex((p) => p.path === dirPath);
|
|
@@ -6725,6 +6765,15 @@ function createWorkspaceService(ctx) {
|
|
|
6725
6765
|
removeProject: async (dirPath) => {
|
|
6726
6766
|
await provider.removeProject(dirPath);
|
|
6727
6767
|
},
|
|
6768
|
+
updateProject: async (dirPath, patch) => {
|
|
6769
|
+
const projects = await provider.listProjects();
|
|
6770
|
+
const existing = projects.find((p) => p.path === dirPath);
|
|
6771
|
+
if (!existing) throw new Error(`No such project: ${dirPath}`);
|
|
6772
|
+
if (!provider.updateProject) {
|
|
6773
|
+
throw new Error("\u5F53\u524D\u5BBF\u4E3B\u4E0D\u652F\u6301\u7F16\u8F91\u9879\u76EE");
|
|
6774
|
+
}
|
|
6775
|
+
return provider.updateProject(dirPath, patch);
|
|
6776
|
+
},
|
|
6728
6777
|
hasProject: async (dirPath) => {
|
|
6729
6778
|
const projects = await provider.listProjects();
|
|
6730
6779
|
return projects.some((p) => p.path === dirPath);
|
|
@@ -6895,6 +6944,11 @@ function createLocalProjectsProvider() {
|
|
|
6895
6944
|
listProjects: () => listProjects(),
|
|
6896
6945
|
addProject: (dirPath) => addProject(dirPath),
|
|
6897
6946
|
removeProject: (dirPath) => removeProject(dirPath),
|
|
6947
|
+
updateProject: (dirPath, patch) => {
|
|
6948
|
+
const updated = updateProject(dirPath, patch);
|
|
6949
|
+
if (!updated) throw new Error(`No such project: ${dirPath}`);
|
|
6950
|
+
return updated;
|
|
6951
|
+
},
|
|
6898
6952
|
validateProjectDir: (dirPath) => validateProjectDir(dirPath),
|
|
6899
6953
|
updateLastOpened: (dirPath) => updateLastOpened(dirPath),
|
|
6900
6954
|
getCompileConfig: (dirPath) => getCompileConfig(dirPath),
|
|
@@ -7167,6 +7221,7 @@ function createWorkbenchContext(opts) {
|
|
|
7167
7221
|
opts.builtinTemplates ?? "all"
|
|
7168
7222
|
);
|
|
7169
7223
|
ctx.customCreateProjectDialog = opts.customCreateProjectDialog;
|
|
7224
|
+
ctx.customEditProjectDialog = opts.customEditProjectDialog;
|
|
7170
7225
|
ctx.onBeforeOpenProject = opts.onBeforeOpenProject;
|
|
7171
7226
|
ctx.workspace = createWorkspaceService(ctx);
|
|
7172
7227
|
ctx.senderPolicy = createWorkbenchSenderPolicy(ctx);
|
|
@@ -7443,7 +7498,15 @@ var SimulatorCustomApiInvokeSchema = z3.tuple([
|
|
|
7443
7498
|
]);
|
|
7444
7499
|
var ProjectGetPagesSchema = z3.tuple([AbsolutePath]);
|
|
7445
7500
|
var ProjectGetCompileConfigSchema = z3.tuple([AbsolutePath]);
|
|
7501
|
+
var ProjectsUpdateSchema = z3.tuple([
|
|
7502
|
+
AbsolutePath,
|
|
7503
|
+
z3.object({
|
|
7504
|
+
name: z3.string().min(1).max(200).optional(),
|
|
7505
|
+
iconUrl: z3.string().max(2048).optional()
|
|
7506
|
+
})
|
|
7507
|
+
]);
|
|
7446
7508
|
var ProjectsRemoveSchema = z3.tuple([AbsolutePath]);
|
|
7509
|
+
var ProjectsOpenEditDialogSchema = z3.tuple([AbsolutePath]);
|
|
7447
7510
|
var WorkbenchSettingsSaveSchema = z3.tuple([
|
|
7448
7511
|
z3.looseObject({
|
|
7449
7512
|
cdp: z3.looseObject({
|
|
@@ -7625,7 +7688,7 @@ function writeProjectConfig(target, name) {
|
|
|
7625
7688
|
cfg = {};
|
|
7626
7689
|
}
|
|
7627
7690
|
}
|
|
7628
|
-
cfg.projectname = name;
|
|
7691
|
+
cfg.projectname = encodeURIComponent(name);
|
|
7629
7692
|
fs7.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
|
|
7630
7693
|
}
|
|
7631
7694
|
async function createProject(input, ctx) {
|
|
@@ -7680,6 +7743,13 @@ function registerProjectsIpc(ctx) {
|
|
|
7680
7743
|
}).handle(ProjectsChannel.Remove, (_event, ...args) => {
|
|
7681
7744
|
const [dirPath] = validate(ProjectsChannel.Remove, ProjectsRemoveSchema, args);
|
|
7682
7745
|
return ctx.workspace.removeProject(dirPath);
|
|
7746
|
+
}).handle(ProjectsChannel.Update, (_event, ...args) => {
|
|
7747
|
+
const [dirPath, patch] = validate(
|
|
7748
|
+
ProjectsChannel.Update,
|
|
7749
|
+
ProjectsUpdateSchema,
|
|
7750
|
+
args
|
|
7751
|
+
);
|
|
7752
|
+
return ctx.workspace.updateProject(dirPath, patch);
|
|
7683
7753
|
}).handle(ProjectsChannel.ListTemplates, () => {
|
|
7684
7754
|
return sanitizeTemplates(ctx.projectTemplates ?? []);
|
|
7685
7755
|
}).handle(ProjectsChannel.OpenCreateDialog, async () => {
|
|
@@ -7689,6 +7759,20 @@ function registerProjectsIpc(ctx) {
|
|
|
7689
7759
|
parentWindow: ctx.windows.mainWindow,
|
|
7690
7760
|
templates: sanitized
|
|
7691
7761
|
});
|
|
7762
|
+
}).handle(ProjectsChannel.OpenEditDialog, async (_event, ...args) => {
|
|
7763
|
+
if (!ctx.customEditProjectDialog) return null;
|
|
7764
|
+
const [dirPath] = validate(
|
|
7765
|
+
ProjectsChannel.OpenEditDialog,
|
|
7766
|
+
ProjectsOpenEditDialogSchema,
|
|
7767
|
+
args
|
|
7768
|
+
);
|
|
7769
|
+
const project = (await ctx.workspace.listProjects()).find((p) => p.path === dirPath);
|
|
7770
|
+
if (!project) throw new Error(`No such project: ${dirPath}`);
|
|
7771
|
+
const result = await ctx.customEditProjectDialog({
|
|
7772
|
+
parentWindow: ctx.windows.mainWindow,
|
|
7773
|
+
project
|
|
7774
|
+
});
|
|
7775
|
+
return { result };
|
|
7692
7776
|
}).handle(ProjectsChannel.Create, async (_event, ...args) => {
|
|
7693
7777
|
const [raw] = args;
|
|
7694
7778
|
if (!raw || typeof raw !== "object") {
|
|
@@ -11108,6 +11192,10 @@ function createContext(config, mainWindow, rendererDir2) {
|
|
|
11108
11192
|
projectTemplates: config.projectTemplates,
|
|
11109
11193
|
builtinTemplates: config.builtinTemplates,
|
|
11110
11194
|
customCreateProjectDialog: config.customCreateProjectDialog,
|
|
11195
|
+
// No cast needed here: the hook receives the project record, so the
|
|
11196
|
+
// main-process `Project` the context hands it satisfies the structural
|
|
11197
|
+
// `EditableProject` the config declares.
|
|
11198
|
+
customEditProjectDialog: config.customEditProjectDialog,
|
|
11111
11199
|
onBeforeOpenProject: config.onBeforeOpenProject
|
|
11112
11200
|
});
|
|
11113
11201
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { WorkbenchContext } from '../services/workbench-context.js';
|
|
2
2
|
import type { WorkbenchModule } from '../services/module.js';
|
|
3
3
|
import type { Disposable } from '@dimina-kit/electron-deck/main';
|
|
4
|
-
type ProjectsIpcCtx = Pick<WorkbenchContext, 'workspace' | 'windows' | 'senderPolicy' | 'projectsProvider' | 'projectTemplates' | 'customCreateProjectDialog' | 'notify' | 'views'>;
|
|
4
|
+
type ProjectsIpcCtx = Pick<WorkbenchContext, 'workspace' | 'windows' | 'senderPolicy' | 'projectsProvider' | 'projectTemplates' | 'customCreateProjectDialog' | 'customEditProjectDialog' | 'notify' | 'views'>;
|
|
5
5
|
export declare function registerProjectsIpc(ctx: ProjectsIpcCtx): Disposable;
|
|
6
6
|
export declare const projectsModule: WorkbenchModule;
|
|
7
7
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { app, dialog } from 'electron';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { ProjectsChannel, DialogChannel } from '../../shared/ipc-channels.js';
|
|
4
|
-
import { ProjectsAddSchema, ProjectsRemoveSchema, } from '../../shared/ipc-schemas.js';
|
|
4
|
+
import { ProjectsAddSchema, ProjectsOpenEditDialogSchema, ProjectsRemoveSchema, ProjectsUpdateSchema, } from '../../shared/ipc-schemas.js';
|
|
5
5
|
import { validate } from '../utils/ipc-schema.js';
|
|
6
6
|
import { IpcRegistry } from '../utils/ipc-registry.js';
|
|
7
7
|
import { sanitizeTemplates } from '../services/projects/templates.js';
|
|
@@ -59,6 +59,10 @@ export function registerProjectsIpc(ctx) {
|
|
|
59
59
|
.handle(ProjectsChannel.Remove, (_event, ...args) => {
|
|
60
60
|
const [dirPath] = validate(ProjectsChannel.Remove, ProjectsRemoveSchema, args);
|
|
61
61
|
return ctx.workspace.removeProject(dirPath);
|
|
62
|
+
})
|
|
63
|
+
.handle(ProjectsChannel.Update, (_event, ...args) => {
|
|
64
|
+
const [dirPath, patch] = validate(ProjectsChannel.Update, ProjectsUpdateSchema, args);
|
|
65
|
+
return ctx.workspace.updateProject(dirPath, patch);
|
|
62
66
|
})
|
|
63
67
|
// ── template catalog + create flow ──
|
|
64
68
|
.handle(ProjectsChannel.ListTemplates, () => {
|
|
@@ -75,6 +79,26 @@ export function registerProjectsIpc(ctx) {
|
|
|
75
79
|
parentWindow: ctx.windows.mainWindow,
|
|
76
80
|
templates: sanitized,
|
|
77
81
|
});
|
|
82
|
+
})
|
|
83
|
+
.handle(ProjectsChannel.OpenEditDialog, async (_event, ...args) => {
|
|
84
|
+
if (!ctx.customEditProjectDialog)
|
|
85
|
+
return null;
|
|
86
|
+
const [dirPath] = validate(ProjectsChannel.OpenEditDialog, ProjectsOpenEditDialogSchema, args);
|
|
87
|
+
// Read from the workspace, not the renderer's args — the host sees
|
|
88
|
+
// devtools' own authoritative record, never a client-supplied one.
|
|
89
|
+
const project = (await ctx.workspace.listProjects()).find((p) => p.path === dirPath);
|
|
90
|
+
if (!project)
|
|
91
|
+
throw new Error(`No such project: ${dirPath}`);
|
|
92
|
+
const result = await ctx.customEditProjectDialog({
|
|
93
|
+
parentWindow: ctx.windows.mainWindow,
|
|
94
|
+
project,
|
|
95
|
+
});
|
|
96
|
+
// Wrapped in `{ result }` so `null` on the wire means ONLY "no hook
|
|
97
|
+
// configured" (renderer falls back to the built-in dialog). A hook that
|
|
98
|
+
// ran and was cancelled resolves `result: null`, which the renderer
|
|
99
|
+
// must treat as "do nothing" — otherwise a cancelled host dialog would
|
|
100
|
+
// fall through into the built-in one right behind it.
|
|
101
|
+
return { result };
|
|
78
102
|
})
|
|
79
103
|
.handle(ProjectsChannel.Create, async (_event, ...args) => {
|
|
80
104
|
// We deliberately don't run this through zod yet — the input shape is
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* 3. copies the template `source` directory into `input.path`, or invokes
|
|
9
9
|
* `template.generate(input.path, { name })` for programmatic templates.
|
|
10
10
|
* 4. rewrites `<input.path>/project.config.json`'s `projectname` to
|
|
11
|
-
* `input.name
|
|
11
|
+
* `encodeURIComponent(input.name)` — same encoding project-repository's
|
|
12
|
+
* `readProjectName`/`writeProjectName` use for the edit path.
|
|
12
13
|
* 5. registers the project with `ctx.projectsProvider.addProject(input.path)`
|
|
13
14
|
* and returns the resulting Project.
|
|
14
15
|
*/
|
|
@@ -81,6 +82,12 @@ async function materializeTemplate(target, template, name) {
|
|
|
81
82
|
* Rewrite `<target>/project.config.json`'s `projectname` (best-effort: create
|
|
82
83
|
* the file if the template didn't ship one — gives the user a working
|
|
83
84
|
* starting point even for tiny generators).
|
|
85
|
+
*
|
|
86
|
+
* Encodes with `encodeURIComponent`, the same convention project-repository's
|
|
87
|
+
* `readProjectName`/`writeProjectName` use (aligned with WeChat DevTools:
|
|
88
|
+
* non-ASCII names go through URL encoding). Writing the raw name here would
|
|
89
|
+
* make this the one path that disagrees with the rename path, so a name with
|
|
90
|
+
* `%` or non-ASCII characters would come back different after an edit.
|
|
84
91
|
*/
|
|
85
92
|
function writeProjectConfig(target, name) {
|
|
86
93
|
const cfgPath = path.join(target, 'project.config.json');
|
|
@@ -93,7 +100,7 @@ function writeProjectConfig(target, name) {
|
|
|
93
100
|
cfg = {};
|
|
94
101
|
}
|
|
95
102
|
}
|
|
96
|
-
cfg.projectname = name;
|
|
103
|
+
cfg.projectname = encodeURIComponent(name);
|
|
97
104
|
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
|
|
98
105
|
}
|
|
99
106
|
export async function createProject(input, ctx) {
|
|
@@ -14,6 +14,12 @@ export function createLocalProjectsProvider() {
|
|
|
14
14
|
listProjects: () => repo.listProjects(),
|
|
15
15
|
addProject: (dirPath) => repo.addProject(dirPath),
|
|
16
16
|
removeProject: (dirPath) => repo.removeProject(dirPath),
|
|
17
|
+
updateProject: (dirPath, patch) => {
|
|
18
|
+
const updated = repo.updateProject(dirPath, patch);
|
|
19
|
+
if (!updated)
|
|
20
|
+
throw new Error(`No such project: ${dirPath}`);
|
|
21
|
+
return updated;
|
|
22
|
+
},
|
|
17
23
|
validateProjectDir: (dirPath) => repo.validateProjectDir(dirPath),
|
|
18
24
|
updateLastOpened: (dirPath) => repo.updateLastOpened(dirPath),
|
|
19
25
|
getCompileConfig: (dirPath) => repo.getCompileConfig(dirPath),
|
|
@@ -7,6 +7,22 @@ export interface Project {
|
|
|
7
7
|
compileConfig?: CompileConfig;
|
|
8
8
|
/** Absent on projects added before mini-game support — treat as 'miniprogram'. */
|
|
9
9
|
type?: ProjectType;
|
|
10
|
+
/**
|
|
11
|
+
* User-supplied icon for the project card. Absent means the card falls back
|
|
12
|
+
* to the first character of `name`.
|
|
13
|
+
*/
|
|
14
|
+
iconUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The subset of a project record the user may edit after import. `path` is the
|
|
18
|
+
* record's identity (it keys every other per-project store — compile config,
|
|
19
|
+
* thumbnail, watcher) and is deliberately not patchable: pointing an existing
|
|
20
|
+
* record at another directory is an import, not an edit.
|
|
21
|
+
*/
|
|
22
|
+
export interface ProjectPatch {
|
|
23
|
+
name?: string;
|
|
24
|
+
/** Empty string clears the icon and restores the name-initial fallback. */
|
|
25
|
+
iconUrl?: string;
|
|
10
26
|
}
|
|
11
27
|
export interface ProjectPages {
|
|
12
28
|
pages: string[];
|
|
@@ -20,6 +36,15 @@ export declare function validateProjectDir(dirPath: string): string | null;
|
|
|
20
36
|
export declare function hasProject(dirPath: string): boolean;
|
|
21
37
|
export declare function addProject(dirPath: string): Project;
|
|
22
38
|
export declare function removeProject(dirPath: string): void;
|
|
39
|
+
/**
|
|
40
|
+
* Apply a user edit to the record at `dirPath`. Returns the updated record, or
|
|
41
|
+
* `null` when no record matches — callers decide whether a missing project is
|
|
42
|
+
* an error (the IPC path) or a no-op.
|
|
43
|
+
*
|
|
44
|
+
* A name change is written through to the project's own config (see
|
|
45
|
+
* `writeProjectName`); the icon lives only in our list.
|
|
46
|
+
*/
|
|
47
|
+
export declare function updateProject(dirPath: string, patch: ProjectPatch): Project | null;
|
|
23
48
|
export declare function updateLastOpened(dirPath: string): void;
|
|
24
49
|
export declare function getCompileConfig(dirPath: string): CompileConfig;
|
|
25
50
|
export declare function getProjectPages(dirPath: string): ProjectPages;
|
|
@@ -23,6 +23,54 @@ function readProjectConfig(dirPath) {
|
|
|
23
23
|
}
|
|
24
24
|
return merged;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* The project's display name as the project itself carries it.
|
|
28
|
+
*
|
|
29
|
+
* `projectname` lives in the merged config with the private file winning, and
|
|
30
|
+
* is URL-encoded when it leaves ASCII — WeChat DevTools 36.6.0 writes
|
|
31
|
+
* `"projectname": "%E6%BD%AE%E7%8E%A9%E6%97%8F"` and its own project list only
|
|
32
|
+
* mirrors that value. ASCII names encode to themselves, so plain values that
|
|
33
|
+
* were written by hand round-trip unchanged.
|
|
34
|
+
*
|
|
35
|
+
* Returns null when the config carries no usable name, leaving the caller to
|
|
36
|
+
* fall back to the directory name.
|
|
37
|
+
*/
|
|
38
|
+
function readProjectName(dirPath) {
|
|
39
|
+
const raw = readProjectConfig(dirPath).projectname;
|
|
40
|
+
if (typeof raw !== 'string' || !raw.trim())
|
|
41
|
+
return null;
|
|
42
|
+
try {
|
|
43
|
+
return decodeURIComponent(raw);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// A literal '%' that isn't a valid escape — keep the author's text.
|
|
47
|
+
return raw;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Persist the display name into `project.private.config.json`, the per-
|
|
52
|
+
* developer half of the project config (WeChat DevTools writes the name there
|
|
53
|
+
* too, leaving the shared `project.config.json` alone).
|
|
54
|
+
*
|
|
55
|
+
* This is what makes the config — not our list file — the single owner of the
|
|
56
|
+
* name: `addProject` re-reads it, so re-importing a directory restores the
|
|
57
|
+
* user's rename instead of resetting it.
|
|
58
|
+
*/
|
|
59
|
+
function writeProjectName(dirPath, name) {
|
|
60
|
+
const configPath = path.join(dirPath, 'project.private.config.json');
|
|
61
|
+
let config = {};
|
|
62
|
+
if (fs.existsSync(configPath)) {
|
|
63
|
+
try {
|
|
64
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Refuse rather than overwrite: this file also holds the user's IDE
|
|
68
|
+
// settings and compile conditions, which a blind rewrite would drop.
|
|
69
|
+
throw new Error(`无法重命名:${configPath} 不是合法 JSON`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
fs.writeFileSync(configPath, JSON.stringify({ ...config, projectname: encodeURIComponent(name) }, null, 2));
|
|
73
|
+
}
|
|
26
74
|
/**
|
|
27
75
|
* Mirrors `dimina/fe/packages/compiler/src/env.js`'s `detectRuntimeType` —
|
|
28
76
|
* an explicit `compileType` wins, else a mini-game is inferred from
|
|
@@ -95,18 +143,9 @@ export function hasProject(dirPath) {
|
|
|
95
143
|
}
|
|
96
144
|
export function addProject(dirPath) {
|
|
97
145
|
const projects = load();
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (fs.existsSync(configPath)) {
|
|
102
|
-
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
103
|
-
if (cfg.projectname)
|
|
104
|
-
name = cfg.projectname;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
catch (err) {
|
|
108
|
-
log.warn('Failed to read project name from config', err);
|
|
109
|
-
}
|
|
146
|
+
// The config is the name's owner, so re-adding a directory picks up a rename
|
|
147
|
+
// the user made here rather than reverting it.
|
|
148
|
+
const name = readProjectName(dirPath) ?? path.basename(dirPath);
|
|
110
149
|
const type = detectRuntimeType(dirPath);
|
|
111
150
|
const project = { name, path: dirPath, lastOpened: null, type };
|
|
112
151
|
const idx = projects.findIndex((p) => p.path === dirPath);
|
|
@@ -122,6 +161,43 @@ export function addProject(dirPath) {
|
|
|
122
161
|
export function removeProject(dirPath) {
|
|
123
162
|
save(load().filter((p) => p.path !== dirPath));
|
|
124
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Apply a user edit to the record at `dirPath`. Returns the updated record, or
|
|
166
|
+
* `null` when no record matches — callers decide whether a missing project is
|
|
167
|
+
* an error (the IPC path) or a no-op.
|
|
168
|
+
*
|
|
169
|
+
* A name change is written through to the project's own config (see
|
|
170
|
+
* `writeProjectName`); the icon lives only in our list.
|
|
171
|
+
*/
|
|
172
|
+
export function updateProject(dirPath, patch) {
|
|
173
|
+
const projects = load();
|
|
174
|
+
const idx = projects.findIndex((p) => p.path === dirPath);
|
|
175
|
+
if (idx < 0)
|
|
176
|
+
return null;
|
|
177
|
+
const next = { ...projects[idx] };
|
|
178
|
+
if (patch.name !== undefined) {
|
|
179
|
+
const name = patch.name.trim();
|
|
180
|
+
if (!name)
|
|
181
|
+
throw new Error('Project name cannot be empty');
|
|
182
|
+
next.name = name;
|
|
183
|
+
}
|
|
184
|
+
if (patch.iconUrl !== undefined) {
|
|
185
|
+
const iconUrl = patch.iconUrl.trim();
|
|
186
|
+
if (iconUrl)
|
|
187
|
+
next.iconUrl = iconUrl;
|
|
188
|
+
else
|
|
189
|
+
delete next.iconUrl;
|
|
190
|
+
}
|
|
191
|
+
// The config write comes first and is allowed to throw: a rename that only
|
|
192
|
+
// reached our list would look applied until the next re-import silently
|
|
193
|
+
// reverted it. The icon has no counterpart in the project config — it is
|
|
194
|
+
// ours alone — so it never touches the project directory.
|
|
195
|
+
if (next.name !== projects[idx].name)
|
|
196
|
+
writeProjectName(dirPath, next.name);
|
|
197
|
+
projects[idx] = next;
|
|
198
|
+
save(projects);
|
|
199
|
+
return next;
|
|
200
|
+
}
|
|
125
201
|
export function updateLastOpened(dirPath) {
|
|
126
202
|
const projects = load();
|
|
127
203
|
const idx = projects.findIndex((p) => p.path === dirPath);
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* template catalog, and the "新建项目" dialog.
|
|
6
6
|
*/
|
|
7
7
|
import type { CompileConfig } from '../../../shared/types.js';
|
|
8
|
-
import type { Project } from './project-repository.js';
|
|
9
|
-
export type { Project };
|
|
8
|
+
import type { Project, ProjectPatch } from './project-repository.js';
|
|
9
|
+
export type { Project, ProjectPatch };
|
|
10
10
|
export type { ProjectTemplate, CreateProjectInput } from '../../../shared/types.js';
|
|
11
11
|
/**
|
|
12
12
|
* Default compile config returned by WorkspaceService when the injected
|
|
@@ -40,6 +40,17 @@ export interface ProjectsProvider {
|
|
|
40
40
|
validateProjectDir?(dirPath: string): string | null | Promise<string | null>;
|
|
41
41
|
addProject(dirPath: string): Project | Promise<Project>;
|
|
42
42
|
removeProject(dirPath: string): void | Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Apply a user edit (name / icon) to an existing record. `dirPath` is the
|
|
45
|
+
* record's identity, so it is never part of the patch — see `ProjectPatch`.
|
|
46
|
+
*
|
|
47
|
+
* Default when omitted: throws, and the renderer shows that error in the
|
|
48
|
+
* edit dialog. A silent no-op would report success (dialog closes, list
|
|
49
|
+
* reloads) while discarding the user's input, since the caller has no way
|
|
50
|
+
* to tell "saved" apart from "provider doesn't support this". Implement
|
|
51
|
+
* this if your UI exposes the project-edit dialog.
|
|
52
|
+
*/
|
|
53
|
+
updateProject?(dirPath: string, patch: ProjectPatch): Project | Promise<Project>;
|
|
43
54
|
/**
|
|
44
55
|
* Record that the user just opened a project (drives "recent" ordering).
|
|
45
56
|
*
|
|
@@ -21,8 +21,8 @@ import { type WorkspaceService } from './workspace/workspace-service.js';
|
|
|
21
21
|
import { type SimulatorApiRegistry } from './simulator/custom-apis.js';
|
|
22
22
|
import { type SimulatorUiExtensionRegistry } from './simulator/ui-extensions.js';
|
|
23
23
|
import { sanitizeTemplates } from './projects/templates.js';
|
|
24
|
-
import type { ProjectsProvider, ProjectTemplate } from './projects/types.js';
|
|
25
|
-
import type { CustomCreateProjectDialogResult } from '../../shared/types.js';
|
|
24
|
+
import type { Project, ProjectsProvider, ProjectTemplate } from './projects/types.js';
|
|
25
|
+
import type { CustomCreateProjectDialogResult, CustomEditProjectDialogResult } from '../../shared/types.js';
|
|
26
26
|
/**
|
|
27
27
|
* Shared mutable state for the workbench application.
|
|
28
28
|
* Passed to each IPC module so they can read/write shared state without closures.
|
|
@@ -96,6 +96,17 @@ export interface WorkbenchContext extends RuntimeContext {
|
|
|
96
96
|
parentWindow: BrowserWindow;
|
|
97
97
|
templates: ProjectTemplate[];
|
|
98
98
|
}) => Promise<CustomCreateProjectDialogResult>;
|
|
99
|
+
/**
|
|
100
|
+
* Optional host hook for the "编辑项目" dialog. When present, the
|
|
101
|
+
* `projects:openEditDialog` IPC runs it and wraps whatever it resolves to
|
|
102
|
+
* as `{ result }`; when absent, the IPC returns `null` and the renderer
|
|
103
|
+
* shows the built-in dialog. `project` comes from `workspace.listProjects`,
|
|
104
|
+
* not the renderer's IPC args.
|
|
105
|
+
*/
|
|
106
|
+
customEditProjectDialog?: (ctx: {
|
|
107
|
+
parentWindow: BrowserWindow;
|
|
108
|
+
project: Project;
|
|
109
|
+
}) => Promise<CustomEditProjectDialogResult>;
|
|
99
110
|
/**
|
|
100
111
|
* Host permission gate consulted by `workspace.openProject` BEFORE any side
|
|
101
112
|
* effect. Throwing vetoes the open (see `WorkbenchAppConfig.onBeforeOpenProject`).
|
|
@@ -239,8 +250,9 @@ export interface WorkbenchContext extends RuntimeContext {
|
|
|
239
250
|
* because their shapes intentionally differ from the config:
|
|
240
251
|
* - `preloadPath` / `rendererDir` are REQUIRED here (the caller resolves the
|
|
241
252
|
* defaults before constructing the context) but optional in the config;
|
|
242
|
-
* - `projectsProvider` / `projectTemplates` / `customCreateProjectDialog`
|
|
243
|
-
* the main-process types, not the structural
|
|
253
|
+
* - `projectsProvider` / `projectTemplates` / `customCreateProjectDialog` /
|
|
254
|
+
* `customEditProjectDialog` use the main-process types, not the structural
|
|
255
|
+
* mirrors in `shared/types`.
|
|
244
256
|
*/
|
|
245
257
|
export interface CreateContextOptions extends Pick<WorkbenchConfig, 'adapter' | 'apiNamespaces' | 'appName' | 'fileTypes'> {
|
|
246
258
|
mainWindow: BrowserWindow;
|
|
@@ -255,6 +267,8 @@ export interface CreateContextOptions extends Pick<WorkbenchConfig, 'adapter' |
|
|
|
255
267
|
builtinTemplates?: 'all' | 'none' | readonly string[];
|
|
256
268
|
/** Host-supplied "新建项目" dialog hook. */
|
|
257
269
|
customCreateProjectDialog?: WorkbenchContext['customCreateProjectDialog'];
|
|
270
|
+
/** Host-supplied "编辑项目" dialog hook. */
|
|
271
|
+
customEditProjectDialog?: WorkbenchContext['customEditProjectDialog'];
|
|
258
272
|
/** Host permission gate run before a project opens (throw to veto). */
|
|
259
273
|
onBeforeOpenProject?: WorkbenchContext['onBeforeOpenProject'];
|
|
260
274
|
}
|
|
@@ -74,6 +74,7 @@ export function createWorkbenchContext(opts) {
|
|
|
74
74
|
ctx.projectsProvider = opts.projectsProvider ?? createLocalProjectsProvider();
|
|
75
75
|
ctx.projectTemplates = resolveTemplates(BUILTIN_TEMPLATES, opts.projectTemplates ?? [], opts.builtinTemplates ?? 'all');
|
|
76
76
|
ctx.customCreateProjectDialog = opts.customCreateProjectDialog;
|
|
77
|
+
ctx.customEditProjectDialog = opts.customEditProjectDialog;
|
|
77
78
|
ctx.onBeforeOpenProject = opts.onBeforeOpenProject;
|
|
78
79
|
ctx.workspace = createWorkspaceService(ctx);
|
|
79
80
|
ctx.senderPolicy = createWorkbenchSenderPolicy(ctx);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AppInfo, CompileConfig, ProjectSession } from '../../../shared/types.js';
|
|
2
2
|
import type { WorkbenchContext } from '../workbench-context.js';
|
|
3
3
|
import type { Project, ProjectPages, ProjectSettings } from '../projects/project-repository.js';
|
|
4
|
+
import type { ProjectPatch } from '../projects/types.js';
|
|
4
5
|
/**
|
|
5
6
|
* Result returned to the renderer after `project:open` finishes.
|
|
6
7
|
* Mirrors the previous inline shape in ipc/session.ts so no
|
|
@@ -28,6 +29,7 @@ export interface WorkspaceService {
|
|
|
28
29
|
listProjects(): Promise<Project[]>;
|
|
29
30
|
addProject(dirPath: string): Promise<Project>;
|
|
30
31
|
removeProject(dirPath: string): Promise<void>;
|
|
32
|
+
updateProject(dirPath: string, patch: ProjectPatch): Promise<Project>;
|
|
31
33
|
hasProject(dirPath: string): Promise<boolean>;
|
|
32
34
|
validateProjectDir(dirPath: string): Promise<string | null>;
|
|
33
35
|
openProject(projectPath: string): Promise<OpenProjectResult>;
|
|
@@ -158,6 +158,20 @@ export function createWorkspaceService(ctx) {
|
|
|
158
158
|
removeProject: async (dirPath) => {
|
|
159
159
|
await provider.removeProject(dirPath);
|
|
160
160
|
},
|
|
161
|
+
updateProject: async (dirPath, patch) => {
|
|
162
|
+
const projects = await provider.listProjects();
|
|
163
|
+
const existing = projects.find((p) => p.path === dirPath);
|
|
164
|
+
if (!existing)
|
|
165
|
+
throw new Error(`No such project: ${dirPath}`);
|
|
166
|
+
// A provider without `updateProject` cannot persist the edit. Returning
|
|
167
|
+
// the record unchanged would look like a successful save to the
|
|
168
|
+
// renderer (dialog closes, list reloads) while silently discarding the
|
|
169
|
+
// user's input, so this must fail loudly instead.
|
|
170
|
+
if (!provider.updateProject) {
|
|
171
|
+
throw new Error('当前宿主不支持编辑项目');
|
|
172
|
+
}
|
|
173
|
+
return provider.updateProject(dirPath, patch);
|
|
174
|
+
},
|
|
161
175
|
hasProject: async (dirPath) => {
|
|
162
176
|
const projects = await provider.listProjects();
|
|
163
177
|
return projects.some((p) => p.path === dirPath);
|