@dimina-kit/devtools 0.3.1-dev.20260518054451 → 0.3.1-dev.20260518114324
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/README.md +120 -0
- package/dist/main/app/app.js +8 -0
- package/dist/main/index.bundle.js +322 -85
- package/dist/main/ipc/projects.d.ts +3 -1
- package/dist/main/ipc/projects.js +88 -4
- package/dist/main/ipc/settings.js +8 -6
- package/dist/main/services/projects/builtin-templates.d.ts +3 -0
- package/dist/main/services/projects/builtin-templates.js +24 -0
- package/dist/main/services/projects/create-project-service.d.ts +7 -0
- package/dist/main/services/projects/create-project-service.js +84 -0
- package/dist/main/services/projects/index.d.ts +7 -0
- package/dist/main/services/projects/index.js +9 -0
- package/dist/main/services/projects/local-provider.d.ts +3 -0
- package/dist/main/services/projects/local-provider.js +25 -0
- package/dist/main/services/projects/templates.d.ts +21 -0
- package/dist/main/services/projects/templates.js +39 -0
- package/dist/main/services/projects/thumbnail.d.ts +6 -2
- package/dist/main/services/projects/thumbnail.js +13 -8
- package/dist/main/services/projects/types.d.ts +118 -0
- package/dist/main/services/projects/types.js +19 -0
- package/dist/main/services/settings/index.d.ts +7 -0
- package/dist/main/services/settings/index.js +4 -0
- package/dist/main/services/workbench-context.d.ts +35 -0
- package/dist/main/services/workbench-context.js +9 -0
- package/dist/main/services/workspace/workspace-service.d.ts +8 -8
- package/dist/main/services/workspace/workspace-service.js +65 -13
- package/dist/main/windows/main-window/create.js +1 -1
- package/dist/renderer/assets/index-Czz_bp6b.js +46 -0
- package/dist/renderer/assets/{input-AH9UDYmj.js → input-DqVQNCx_.js} +2 -2
- package/dist/renderer/assets/{ipc-transport-DNgBM6Qz.js → ipc-transport-CCHrCtDr.js} +2 -2
- package/dist/renderer/assets/ipc-transport-DqbRdZNO.css +1 -0
- package/dist/renderer/assets/{popover-D1hoVYiY.js → popover-BRg4XI_w.js} +2 -2
- package/dist/renderer/assets/{select-BgDHRj6r.js → select-CvILHHZU.js} +2 -2
- package/dist/renderer/assets/{settings-m-tcmZy_.js → settings-BFNfnYvU.js} +2 -2
- package/dist/renderer/assets/{settings-api-DBKlNKV3.js → settings-api-DOAgmBst.js} +2 -2
- package/dist/renderer/assets/{workbenchSettings-CGacu-pI.js → workbenchSettings-DjVMgMMH.js} +2 -2
- package/dist/renderer/entries/main/index.html +5 -5
- package/dist/renderer/entries/popover/index.html +5 -5
- package/dist/renderer/entries/settings/index.html +5 -5
- package/dist/renderer/entries/workbench-settings/index.html +4 -4
- package/dist/shared/ipc-channels.d.ts +14 -1
- package/dist/shared/ipc-channels.js +8 -0
- package/dist/shared/ipc-schemas.d.ts +1 -0
- package/dist/shared/ipc-schemas.js +1 -0
- package/dist/shared/types.d.ts +80 -0
- package/package.json +7 -2
- package/templates/blank/app.js +6 -0
- package/templates/blank/app.json +11 -0
- package/templates/blank/app.wxss +5 -0
- package/templates/blank/pages/index/index.js +6 -0
- package/templates/blank/pages/index/index.json +3 -0
- package/templates/blank/pages/index/index.wxml +3 -0
- package/templates/blank/pages/index/index.wxss +11 -0
- package/templates/blank/project.config.json +13 -0
- package/templates/blank/sitemap.json +9 -0
- package/templates/taro-todo/app.js +1 -0
- package/templates/taro-todo/app.json +1 -0
- package/templates/taro-todo/app.wxss +1 -0
- package/templates/taro-todo/base.wxml +851 -0
- package/templates/taro-todo/common.js +1 -0
- package/templates/taro-todo/comp.js +1 -0
- package/templates/taro-todo/comp.json +1 -0
- package/templates/taro-todo/comp.wxml +2 -0
- package/templates/taro-todo/custom-wrapper.js +1 -0
- package/templates/taro-todo/custom-wrapper.json +1 -0
- package/templates/taro-todo/custom-wrapper.wxml +4 -0
- package/templates/taro-todo/pages/index/index.js +2 -0
- package/templates/taro-todo/pages/index/index.json +1 -0
- package/templates/taro-todo/pages/index/index.wxml +2 -0
- package/templates/taro-todo/pages/index/index.wxss +1 -0
- package/templates/taro-todo/project.config.json +44 -0
- package/templates/taro-todo/runtime.js +1 -0
- package/templates/taro-todo/taro.js +2 -0
- package/templates/taro-todo/utils.wxs +39 -0
- package/templates/taro-todo/vendors.js +2 -0
- package/dist/renderer/assets/index-BFdItKBK.js +0 -46
- package/dist/renderer/assets/ipc-transport-Dvmd5JZj.css +0 -1
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { dialog } from 'electron';
|
|
1
|
+
import { app, dialog } from 'electron';
|
|
2
|
+
import path from 'path';
|
|
2
3
|
import { ProjectsChannel, DialogChannel } from '../../shared/ipc-channels.js';
|
|
3
4
|
import { ProjectsAddSchema, ProjectsRemoveSchema, } from '../../shared/ipc-schemas.js';
|
|
4
5
|
import { validate } from '../utils/ipc-schema.js';
|
|
5
6
|
import { IpcRegistry } from '../utils/ipc-registry.js';
|
|
7
|
+
import { sanitizeTemplates } from '../services/projects/templates.js';
|
|
8
|
+
import { createProject } from '../services/projects/create-project-service.js';
|
|
9
|
+
import { loadWorkbenchSettings, saveWorkbenchSettings, } from '../services/settings/index.js';
|
|
6
10
|
export function registerProjectsIpc(ctx) {
|
|
7
11
|
return new IpcRegistry(ctx.senderPolicy)
|
|
8
12
|
.handle(ProjectsChannel.List, () => {
|
|
@@ -17,7 +21,7 @@ export function registerProjectsIpc(ctx) {
|
|
|
17
21
|
})
|
|
18
22
|
.handle(ProjectsChannel.Add, async (_event, ...args) => {
|
|
19
23
|
const [dirPath] = validate(ProjectsChannel.Add, ProjectsAddSchema, args);
|
|
20
|
-
const dirError = ctx.workspace.validateProjectDir(dirPath);
|
|
24
|
+
const dirError = await ctx.workspace.validateProjectDir(dirPath);
|
|
21
25
|
if (dirError) {
|
|
22
26
|
await dialog.showMessageBox(ctx.mainWindow, {
|
|
23
27
|
type: 'error',
|
|
@@ -28,8 +32,8 @@ export function registerProjectsIpc(ctx) {
|
|
|
28
32
|
});
|
|
29
33
|
throw new Error(dirError);
|
|
30
34
|
}
|
|
31
|
-
const duplicate = ctx.workspace.hasProject(dirPath);
|
|
32
|
-
const project = ctx.workspace.addProject(dirPath);
|
|
35
|
+
const duplicate = await ctx.workspace.hasProject(dirPath);
|
|
36
|
+
const project = await ctx.workspace.addProject(dirPath);
|
|
33
37
|
if (duplicate) {
|
|
34
38
|
await dialog.showMessageBox(ctx.mainWindow, {
|
|
35
39
|
type: 'info',
|
|
@@ -44,8 +48,88 @@ export function registerProjectsIpc(ctx) {
|
|
|
44
48
|
.handle(ProjectsChannel.Remove, (_event, ...args) => {
|
|
45
49
|
const [dirPath] = validate(ProjectsChannel.Remove, ProjectsRemoveSchema, args);
|
|
46
50
|
return ctx.workspace.removeProject(dirPath);
|
|
51
|
+
})
|
|
52
|
+
// ── Phase 3: template catalog + create flow ──
|
|
53
|
+
.handle(ProjectsChannel.ListTemplates, () => {
|
|
54
|
+
// Sanitize at the IPC boundary: `generate` is a function and the
|
|
55
|
+
// structured-clone algorithm Electron uses for invoke would otherwise
|
|
56
|
+
// throw "could not be cloned" before the renderer ever sees it.
|
|
57
|
+
return sanitizeTemplates(ctx.projectTemplates ?? []);
|
|
58
|
+
})
|
|
59
|
+
.handle(ProjectsChannel.OpenCreateDialog, async () => {
|
|
60
|
+
if (!ctx.customCreateProjectDialog)
|
|
61
|
+
return null;
|
|
62
|
+
const sanitized = sanitizeTemplates(ctx.projectTemplates ?? []);
|
|
63
|
+
return await ctx.customCreateProjectDialog({
|
|
64
|
+
parentWindow: ctx.mainWindow,
|
|
65
|
+
templates: sanitized,
|
|
66
|
+
});
|
|
67
|
+
})
|
|
68
|
+
.handle(ProjectsChannel.Create, async (_event, ...args) => {
|
|
69
|
+
// We deliberately don't run this through zod yet — the input shape is
|
|
70
|
+
// wide (any template can stash arbitrary `extra` fields) and the
|
|
71
|
+
// service does its own per-field validation. Bound size to keep this
|
|
72
|
+
// a cheap DoS guard.
|
|
73
|
+
const [raw] = args;
|
|
74
|
+
if (!raw || typeof raw !== 'object') {
|
|
75
|
+
throw new Error('projects:create expects a CreateProjectInput object');
|
|
76
|
+
}
|
|
77
|
+
const input = raw;
|
|
78
|
+
let project;
|
|
79
|
+
try {
|
|
80
|
+
project = await createProject(input, {
|
|
81
|
+
templates: ctx.projectTemplates ?? [],
|
|
82
|
+
projectsProvider: ctx.projectsProvider,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
// Mirror the Add flow: surface scaffold failures (invalid name,
|
|
87
|
+
// non-empty target dir, missing template, remote backend reject,
|
|
88
|
+
// …) as a native dialog so users see *why* the create silently
|
|
89
|
+
// fizzled. The renderer's catch then just bails out.
|
|
90
|
+
await dialog.showMessageBox(ctx.mainWindow, {
|
|
91
|
+
type: 'error',
|
|
92
|
+
title: '创建项目失败',
|
|
93
|
+
message: '无法创建项目',
|
|
94
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
95
|
+
buttons: ['确定'],
|
|
96
|
+
});
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
// Remember the parent of the just-created project so the next open of
|
|
100
|
+
// the create dialog pre-fills its directory under the same workspace.
|
|
101
|
+
try {
|
|
102
|
+
const settings = loadWorkbenchSettings();
|
|
103
|
+
const newBase = path.dirname(input.path);
|
|
104
|
+
if (newBase && settings.lastCreateBaseDir !== newBase) {
|
|
105
|
+
saveWorkbenchSettings({ ...settings, lastCreateBaseDir: newBase });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
console.warn('[projects:create] failed to persist lastCreateBaseDir', err);
|
|
110
|
+
}
|
|
111
|
+
return project;
|
|
112
|
+
})
|
|
113
|
+
.handle(ProjectsChannel.GetCreateDefaults, () => {
|
|
114
|
+
// Fallback chain: persisted last parent → user's Documents → home.
|
|
115
|
+
// Documents covers the common "我把项目都放在 Documents/ 下" case for
|
|
116
|
+
// first-time users on macOS; home is just a safe final fallback.
|
|
117
|
+
const settings = loadWorkbenchSettings();
|
|
118
|
+
const baseDir = settings.lastCreateBaseDir ??
|
|
119
|
+
safeAppPath('documents') ??
|
|
120
|
+
safeAppPath('home') ??
|
|
121
|
+
'';
|
|
122
|
+
return { baseDir };
|
|
47
123
|
});
|
|
48
124
|
}
|
|
125
|
+
function safeAppPath(name) {
|
|
126
|
+
try {
|
|
127
|
+
return app.getPath(name);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
49
133
|
export const projectsModule = {
|
|
50
134
|
setup: (ctx) => registerProjectsIpc(ctx),
|
|
51
135
|
};
|
|
@@ -45,10 +45,11 @@ export function registerSettingsIpc(ctx) {
|
|
|
45
45
|
const [visible] = validate(SettingsChannel.SetVisible, SettingsSetVisibleSchema, args);
|
|
46
46
|
if (visible) {
|
|
47
47
|
await ctx.views.showSettings();
|
|
48
|
+
const projectPath = ctx.workspace.getProjectPath();
|
|
48
49
|
ctx.notify.settingsInit({
|
|
49
|
-
projectPath
|
|
50
|
-
config: ctx.workspace.getCompileConfig(
|
|
51
|
-
projectSettings: ctx.workspace.getProjectSettings(
|
|
50
|
+
projectPath,
|
|
51
|
+
config: await ctx.workspace.getCompileConfig(projectPath),
|
|
52
|
+
projectSettings: ctx.workspace.getProjectSettings(projectPath),
|
|
52
53
|
});
|
|
53
54
|
}
|
|
54
55
|
else {
|
|
@@ -56,10 +57,11 @@ export function registerSettingsIpc(ctx) {
|
|
|
56
57
|
ctx.notify.settingsClosed();
|
|
57
58
|
}
|
|
58
59
|
})
|
|
59
|
-
.on(SettingsChannel.ConfigChanged, (_, ...args) => {
|
|
60
|
+
.on(SettingsChannel.ConfigChanged, async (_, ...args) => {
|
|
60
61
|
const [config] = validate(SettingsChannel.ConfigChanged, SettingsConfigChangedSchema, args);
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
const projectPath = ctx.workspace.getProjectPath();
|
|
63
|
+
if (projectPath) {
|
|
64
|
+
await ctx.workspace.saveCompileConfig(projectPath, config);
|
|
63
65
|
}
|
|
64
66
|
ctx.notify.settingsChanged(config);
|
|
65
67
|
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in template catalog. Each entry's `source.path` points at a
|
|
3
|
+
* directory inside `<devtools-package-root>/templates/`, which is
|
|
4
|
+
* committed to the repo and (via the `files` field of package.json's
|
|
5
|
+
* publish manifest) included in the published tarball.
|
|
6
|
+
*/
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { devtoolsPackageRoot } from '../../utils/paths.js';
|
|
9
|
+
const TEMPLATES_DIR = path.join(devtoolsPackageRoot, 'templates');
|
|
10
|
+
export const BUILTIN_TEMPLATES = [
|
|
11
|
+
{
|
|
12
|
+
id: 'blank',
|
|
13
|
+
name: 'Blank',
|
|
14
|
+
description: '最小骨架:app.* + 一个 index 页面',
|
|
15
|
+
source: { type: 'directory', path: path.join(TEMPLATES_DIR, 'blank') },
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'taro-todo',
|
|
19
|
+
name: 'Taro Todo',
|
|
20
|
+
description: 'Taro 编译产物的 Todo 示例',
|
|
21
|
+
source: { type: 'directory', path: path.join(TEMPLATES_DIR, 'taro-todo') },
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
//# sourceMappingURL=builtin-templates.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CreateProjectInput, Project, ProjectTemplate, ProjectsProvider } from './types.js';
|
|
2
|
+
export interface CreateProjectCtx {
|
|
3
|
+
templates: ProjectTemplate[];
|
|
4
|
+
projectsProvider: ProjectsProvider;
|
|
5
|
+
}
|
|
6
|
+
export declare function createProject(input: CreateProjectInput, ctx: CreateProjectCtx): Promise<Project>;
|
|
7
|
+
//# sourceMappingURL=create-project-service.d.ts.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical server-side path for the "新建项目" flow.
|
|
3
|
+
*
|
|
4
|
+
* `createProject(input, ctx)`:
|
|
5
|
+
* 1. validates `input.name` (non-empty after trim) and `input.path` (does
|
|
6
|
+
* not exist OR exists but is an empty directory).
|
|
7
|
+
* 2. resolves `input.templateId` (default 'blank') against `ctx.templates`.
|
|
8
|
+
* 3. copies the template `source` directory into `input.path`, or invokes
|
|
9
|
+
* `template.generate(input.path, { name })` for programmatic templates.
|
|
10
|
+
* 4. rewrites `<input.path>/project.config.json`'s `projectname` to
|
|
11
|
+
* `input.name`.
|
|
12
|
+
* 5. registers the project with `ctx.projectsProvider.addProject(input.path)`
|
|
13
|
+
* and returns the resulting Project.
|
|
14
|
+
*/
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
const DEFAULT_TEMPLATE_ID = 'blank';
|
|
18
|
+
export async function createProject(input, ctx) {
|
|
19
|
+
// 1. name validation
|
|
20
|
+
const name = (input.name ?? '').trim();
|
|
21
|
+
if (name.length === 0) {
|
|
22
|
+
throw new Error('Project name cannot be empty');
|
|
23
|
+
}
|
|
24
|
+
// 2. path validation: does-not-exist OR exists+empty.
|
|
25
|
+
const target = input.path;
|
|
26
|
+
if (!target || typeof target !== 'string') {
|
|
27
|
+
throw new Error('Project path is required');
|
|
28
|
+
}
|
|
29
|
+
if (fs.existsSync(target)) {
|
|
30
|
+
const stat = fs.statSync(target);
|
|
31
|
+
if (!stat.isDirectory()) {
|
|
32
|
+
throw new Error(`Project path exists and is not a directory: ${target}`);
|
|
33
|
+
}
|
|
34
|
+
const entries = fs.readdirSync(target);
|
|
35
|
+
if (entries.length > 0) {
|
|
36
|
+
throw new Error(`Project path is not empty (refusing to overwrite): ${target}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// 3. template lookup (default 'blank')
|
|
40
|
+
const templateId = input.templateId ?? DEFAULT_TEMPLATE_ID;
|
|
41
|
+
const template = ctx.templates.find((t) => t.id === templateId);
|
|
42
|
+
if (!template) {
|
|
43
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
44
|
+
}
|
|
45
|
+
if (!template.source && !template.generate) {
|
|
46
|
+
throw new Error(`Template '${templateId}' has neither a source directory nor a generate function`);
|
|
47
|
+
}
|
|
48
|
+
// 4. materialise: ensure target exists, then copy or generate.
|
|
49
|
+
fs.mkdirSync(target, { recursive: true });
|
|
50
|
+
if (template.generate) {
|
|
51
|
+
await template.generate(target, { name });
|
|
52
|
+
}
|
|
53
|
+
else if (template.source) {
|
|
54
|
+
if (!fs.existsSync(template.source.path)) {
|
|
55
|
+
throw new Error(`Template source missing on disk: ${template.source.path}`);
|
|
56
|
+
}
|
|
57
|
+
// `recursive: true` makes cpSync mirror the entire tree (files,
|
|
58
|
+
// subdirs, symlinks). `force: true` mirrors over an empty target.
|
|
59
|
+
fs.cpSync(template.source.path, target, {
|
|
60
|
+
recursive: true,
|
|
61
|
+
force: true,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// 5. rewrite project.config.json projectname (best-effort: create the file
|
|
65
|
+
// if the template didn't ship one — gives the user a working starting
|
|
66
|
+
// point even for tiny generators).
|
|
67
|
+
const cfgPath = path.join(target, 'project.config.json');
|
|
68
|
+
let cfg = {};
|
|
69
|
+
if (fs.existsSync(cfgPath)) {
|
|
70
|
+
try {
|
|
71
|
+
cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
cfg = {};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
cfg.projectname = name;
|
|
78
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
|
|
79
|
+
// 6. register with provider — this is what makes the new project appear
|
|
80
|
+
// in the list immediately after the dialog closes.
|
|
81
|
+
const created = await ctx.projectsProvider.addProject(target);
|
|
82
|
+
return created;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=create-project-service.js.map
|
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
export * from './project-repository.js';
|
|
2
|
+
export { createLocalProjectsProvider } from './local-provider.js';
|
|
3
|
+
export type { ProjectsProvider, ProjectTemplate, CreateProjectInput, BuiltinTemplatesMode, } from './types.js';
|
|
4
|
+
export { DEFAULT_COMPILE_CONFIG } from './types.js';
|
|
5
|
+
export { resolveTemplates, sanitizeTemplates } from './templates.js';
|
|
6
|
+
export { BUILTIN_TEMPLATES } from './builtin-templates.js';
|
|
7
|
+
export { createProject } from './create-project-service.js';
|
|
8
|
+
export type { CreateProjectCtx } from './create-project-service.js';
|
|
2
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -2,5 +2,14 @@
|
|
|
2
2
|
// keeps working. The filesystem-level helpers live in project-repository.ts;
|
|
3
3
|
// higher-level coordination (session + renderer notifications) lives in
|
|
4
4
|
// ../workspace/workspace-service.ts.
|
|
5
|
+
//
|
|
6
|
+
// Phase 1 adds the extensibility surface: `types.ts` (ProjectsProvider,
|
|
7
|
+
// ProjectTemplate, CreateProjectInput) and `local-provider.ts` (the default
|
|
8
|
+
// implementation injected when the host omits `projectsProvider`).
|
|
5
9
|
export * from './project-repository.js';
|
|
10
|
+
export { createLocalProjectsProvider } from './local-provider.js';
|
|
11
|
+
export { DEFAULT_COMPILE_CONFIG } from './types.js';
|
|
12
|
+
export { resolveTemplates, sanitizeTemplates } from './templates.js';
|
|
13
|
+
export { BUILTIN_TEMPLATES } from './builtin-templates.js';
|
|
14
|
+
export { createProject } from './create-project-service.js';
|
|
6
15
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default ProjectsProvider — persists the project list to
|
|
3
|
+
* `<userData>/dimina-projects.json` and exposes the validation / compile-
|
|
4
|
+
* config helpers historically provided by `project-repository.ts`.
|
|
5
|
+
*
|
|
6
|
+
* The repository module is kept for thumbnail/paths/page helpers and for
|
|
7
|
+
* back-compat re-exports; the canonical list & validation surface for the
|
|
8
|
+
* workspace service is this provider.
|
|
9
|
+
*/
|
|
10
|
+
import * as repo from './project-repository.js';
|
|
11
|
+
import { loadThumbnail, saveThumbnailFromDataUrl } from './thumbnail.js';
|
|
12
|
+
export function createLocalProjectsProvider() {
|
|
13
|
+
return {
|
|
14
|
+
listProjects: () => repo.listProjects(),
|
|
15
|
+
addProject: (dirPath) => repo.addProject(dirPath),
|
|
16
|
+
removeProject: (dirPath) => repo.removeProject(dirPath),
|
|
17
|
+
validateProjectDir: (dirPath) => repo.validateProjectDir(dirPath),
|
|
18
|
+
updateLastOpened: (dirPath) => repo.updateLastOpened(dirPath),
|
|
19
|
+
getCompileConfig: (dirPath) => repo.getCompileConfig(dirPath),
|
|
20
|
+
saveCompileConfig: (dirPath, cfg) => repo.saveCompileConfig(dirPath, cfg),
|
|
21
|
+
saveThumbnail: (dirPath, dataUrl) => saveThumbnailFromDataUrl(dirPath, dataUrl),
|
|
22
|
+
getThumbnail: (dirPath) => loadThumbnail(dirPath),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=local-provider.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in template policy merge logic.
|
|
3
|
+
*
|
|
4
|
+
* `resolveTemplates(builtin, injected, mode)`:
|
|
5
|
+
* - mode='all' → keep every built-in
|
|
6
|
+
* - mode='none' → drop every built-in
|
|
7
|
+
* - mode=string[] → keep only built-ins whose id is in the whitelist
|
|
8
|
+
*
|
|
9
|
+
* Injected templates always win over a same-id built-in. Order is:
|
|
10
|
+
* injected (input order) ++ remaining built-ins (in their input order
|
|
11
|
+
* for 'all'; in the whitelist's order for an array mode).
|
|
12
|
+
*/
|
|
13
|
+
import type { BuiltinTemplatesMode, ProjectTemplate } from './types.js';
|
|
14
|
+
export declare function resolveTemplates(builtin: ProjectTemplate[], injected: ProjectTemplate[], mode: BuiltinTemplatesMode): ProjectTemplate[];
|
|
15
|
+
/**
|
|
16
|
+
* Strip non-serialisable fields (e.g. `generate` functions) so the merged
|
|
17
|
+
* catalog can be sent across the renderer IPC boundary. Anything left is
|
|
18
|
+
* structured-clonable.
|
|
19
|
+
*/
|
|
20
|
+
export declare function sanitizeTemplates(templates: ProjectTemplate[]): ProjectTemplate[];
|
|
21
|
+
//# sourceMappingURL=templates.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function resolveTemplates(builtin, injected, mode) {
|
|
2
|
+
let kept;
|
|
3
|
+
if (mode === 'none') {
|
|
4
|
+
kept = [];
|
|
5
|
+
}
|
|
6
|
+
else if (mode === 'all') {
|
|
7
|
+
kept = [...builtin];
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
// Whitelist: keep only listed ids, in the whitelist's order. Unknown
|
|
11
|
+
// ids are dropped silently — the user opted in to a curated subset and
|
|
12
|
+
// we don't want a typo to crash the new-project dialog.
|
|
13
|
+
const byId = new Map(builtin.map((t) => [t.id, t]));
|
|
14
|
+
kept = [];
|
|
15
|
+
for (const id of mode) {
|
|
16
|
+
const t = byId.get(id);
|
|
17
|
+
if (t)
|
|
18
|
+
kept.push(t);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// Strip any built-ins whose id is overridden by an injected template, then
|
|
22
|
+
// prepend the injected list so host-supplied versions win on rendering.
|
|
23
|
+
const injectedIds = new Set(injected.map((t) => t.id));
|
|
24
|
+
const remaining = kept.filter((t) => !injectedIds.has(t.id));
|
|
25
|
+
return [...injected, ...remaining];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Strip non-serialisable fields (e.g. `generate` functions) so the merged
|
|
29
|
+
* catalog can be sent across the renderer IPC boundary. Anything left is
|
|
30
|
+
* structured-clonable.
|
|
31
|
+
*/
|
|
32
|
+
export function sanitizeTemplates(templates) {
|
|
33
|
+
return templates.map((t) => {
|
|
34
|
+
// `generate` is a function and would throw on structuredClone / IPC.
|
|
35
|
+
const { generate: _generate, ...rest } = t;
|
|
36
|
+
return rest;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=templates.js.map
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Persist a data-URL screenshot for the local default provider. Silently
|
|
3
|
+
* drops the call when the URL isn't the expected PNG base64 form — the
|
|
4
|
+
* provider contract documents this as best-effort.
|
|
5
|
+
*/
|
|
6
|
+
export declare function saveThumbnailFromDataUrl(projectPath: string, imageDataUrl: string): void;
|
|
3
7
|
export declare function loadThumbnail(projectPath: string): string | null;
|
|
4
8
|
//# sourceMappingURL=thumbnail.d.ts.map
|
|
@@ -11,19 +11,24 @@ function hashProjectPath(projectPath) {
|
|
|
11
11
|
function getThumbnailPath(projectPath) {
|
|
12
12
|
return path.join(getThumbnailDir(), `${hashProjectPath(projectPath)}.png`);
|
|
13
13
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
const DATA_URL_PNG_PREFIX = 'data:image/png;base64,';
|
|
15
|
+
/**
|
|
16
|
+
* Persist a data-URL screenshot for the local default provider. Silently
|
|
17
|
+
* drops the call when the URL isn't the expected PNG base64 form — the
|
|
18
|
+
* provider contract documents this as best-effort.
|
|
19
|
+
*/
|
|
20
|
+
export function saveThumbnailFromDataUrl(projectPath, imageDataUrl) {
|
|
21
|
+
if (!imageDataUrl.startsWith(DATA_URL_PNG_PREFIX))
|
|
22
|
+
return;
|
|
23
|
+
const png = Buffer.from(imageDataUrl.slice(DATA_URL_PNG_PREFIX.length), 'base64');
|
|
24
|
+
fs.mkdirSync(getThumbnailDir(), { recursive: true });
|
|
25
|
+
fs.writeFileSync(getThumbnailPath(projectPath), png);
|
|
21
26
|
}
|
|
22
27
|
export function loadThumbnail(projectPath) {
|
|
23
28
|
const filePath = getThumbnailPath(projectPath);
|
|
24
29
|
try {
|
|
25
30
|
const buf = fs.readFileSync(filePath);
|
|
26
|
-
return
|
|
31
|
+
return `${DATA_URL_PNG_PREFIX}${buf.toString('base64')}`;
|
|
27
32
|
}
|
|
28
33
|
catch {
|
|
29
34
|
return null;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public extensibility surface for the project list panel. Hosts that embed
|
|
3
|
+
* dimina-devtools (e.g. qdmp) can inject implementations of these types via
|
|
4
|
+
* `WorkbenchAppConfig` to fully take over the project source-of-truth, the
|
|
5
|
+
* template catalog, and the "新建项目" dialog.
|
|
6
|
+
*/
|
|
7
|
+
import type { CompileConfig } from '../../../shared/types.js';
|
|
8
|
+
import type { Project } from './project-repository.js';
|
|
9
|
+
export type { Project };
|
|
10
|
+
/**
|
|
11
|
+
* Default compile config returned by WorkspaceService when the injected
|
|
12
|
+
* provider omits `getCompileConfig`, or when no record exists for a path.
|
|
13
|
+
* Exported so host providers can mirror the canonical shape instead of
|
|
14
|
+
* re-deriving the magic values.
|
|
15
|
+
*/
|
|
16
|
+
export declare const DEFAULT_COMPILE_CONFIG: CompileConfig;
|
|
17
|
+
/**
|
|
18
|
+
* Pluggable project-list backend. The default implementation
|
|
19
|
+
* (LocalProjectsProvider) persists to `<userData>/dimina-projects.json`.
|
|
20
|
+
*
|
|
21
|
+
* All methods may return synchronously OR as a Promise.
|
|
22
|
+
*
|
|
23
|
+
* Optional methods documented per field below; when the host omits an
|
|
24
|
+
* optional method, WorkspaceService applies a documented default
|
|
25
|
+
* (typically a safe no-op) — it does NOT silently fall back to the local
|
|
26
|
+
* file-system helpers, since for a remote provider the project path may
|
|
27
|
+
* not exist on this machine at all.
|
|
28
|
+
*/
|
|
29
|
+
export interface ProjectsProvider {
|
|
30
|
+
listProjects(): Project[] | Promise<Project[]>;
|
|
31
|
+
/**
|
|
32
|
+
* Validate that `dirPath` points at a mini-app source tree.
|
|
33
|
+
* Return `null` if valid, or a user-facing error message string.
|
|
34
|
+
*
|
|
35
|
+
* Default when omitted: returns `null` (no validation). Remote
|
|
36
|
+
* providers SHOULD implement this if the host UI exposes "import an
|
|
37
|
+
* existing directory"; otherwise the user can add unreachable paths.
|
|
38
|
+
*/
|
|
39
|
+
validateProjectDir?(dirPath: string): string | null | Promise<string | null>;
|
|
40
|
+
addProject(dirPath: string): Project | Promise<Project>;
|
|
41
|
+
removeProject(dirPath: string): void | Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Record that the user just opened a project (drives "recent" ordering).
|
|
44
|
+
*
|
|
45
|
+
* Default when omitted: silently no-ops. The renderer's "recent" sort
|
|
46
|
+
* order will then reflect whatever the provider's `listProjects` returns.
|
|
47
|
+
*/
|
|
48
|
+
updateLastOpened?(dirPath: string): void | Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Read the per-project compile config.
|
|
51
|
+
*
|
|
52
|
+
* Default when omitted: returns `DEFAULT_COMPILE_CONFIG`
|
|
53
|
+
* (`{ startPage: '', scene: 1001, queryParams: [] }`).
|
|
54
|
+
*/
|
|
55
|
+
getCompileConfig?(dirPath: string): CompileConfig | Promise<CompileConfig>;
|
|
56
|
+
/**
|
|
57
|
+
* Persist the per-project compile config.
|
|
58
|
+
*
|
|
59
|
+
* Default when omitted: silently no-ops — the renderer's edits will
|
|
60
|
+
* not survive a reload. Implement this if your UI exposes the compile
|
|
61
|
+
* config panel.
|
|
62
|
+
*/
|
|
63
|
+
saveCompileConfig?(dirPath: string, cfg: CompileConfig): void | Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Persist a captured screenshot for the given project. `imageDataUrl`
|
|
66
|
+
* is a `data:image/png;base64,...` string so a remote provider can ship
|
|
67
|
+
* it straight to its backend without re-encoding.
|
|
68
|
+
*
|
|
69
|
+
* Default when omitted: silently no-ops (the screenshot is dropped).
|
|
70
|
+
* Implement this for hosts whose projects don't live on the local
|
|
71
|
+
* filesystem and that want thumbnails to round-trip through their own
|
|
72
|
+
* storage.
|
|
73
|
+
*/
|
|
74
|
+
saveThumbnail?(dirPath: string, imageDataUrl: string): void | Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Load the most recently saved thumbnail for the given project.
|
|
77
|
+
* Returns a `data:image/png;base64,...` string or `null` when there
|
|
78
|
+
* is none.
|
|
79
|
+
*
|
|
80
|
+
* Default when omitted: returns `null`.
|
|
81
|
+
*/
|
|
82
|
+
getThumbnail?(dirPath: string): string | null | Promise<string | null>;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A template the user can pick from in the "新建项目" dialog. Exactly one of
|
|
86
|
+
* `source` or `generate` should be supplied; if both are omitted, the
|
|
87
|
+
* service refuses to materialise the template.
|
|
88
|
+
*/
|
|
89
|
+
export interface ProjectTemplate {
|
|
90
|
+
/** Stable identifier. Used by host whitelists and the `templateId` field of CreateProjectInput. */
|
|
91
|
+
id: string;
|
|
92
|
+
/** Human-readable label shown in the dialog. */
|
|
93
|
+
name: string;
|
|
94
|
+
description?: string;
|
|
95
|
+
icon?: string;
|
|
96
|
+
/** Copy-tree source. `path` must be absolute. */
|
|
97
|
+
source?: {
|
|
98
|
+
type: 'directory';
|
|
99
|
+
path: string;
|
|
100
|
+
};
|
|
101
|
+
/** Programmatic generator. `target` is the absolute destination directory. */
|
|
102
|
+
generate?: (target: string, opts: {
|
|
103
|
+
name: string;
|
|
104
|
+
}) => Promise<void>;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Payload returned from the create-project dialog. The service uses this to
|
|
108
|
+
* scaffold disk content and to register the project with the provider.
|
|
109
|
+
*/
|
|
110
|
+
export interface CreateProjectInput {
|
|
111
|
+
name: string;
|
|
112
|
+
path: string;
|
|
113
|
+
templateId?: string;
|
|
114
|
+
extra?: Record<string, unknown>;
|
|
115
|
+
}
|
|
116
|
+
/** Built-in template policy. */
|
|
117
|
+
export type BuiltinTemplatesMode = 'all' | 'none' | readonly string[];
|
|
118
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public extensibility surface for the project list panel. Hosts that embed
|
|
3
|
+
* dimina-devtools (e.g. qdmp) can inject implementations of these types via
|
|
4
|
+
* `WorkbenchAppConfig` to fully take over the project source-of-truth, the
|
|
5
|
+
* template catalog, and the "新建项目" dialog.
|
|
6
|
+
*/
|
|
7
|
+
import { DEFAULT_SCENE } from '../../../shared/constants.js';
|
|
8
|
+
/**
|
|
9
|
+
* Default compile config returned by WorkspaceService when the injected
|
|
10
|
+
* provider omits `getCompileConfig`, or when no record exists for a path.
|
|
11
|
+
* Exported so host providers can mirror the canonical shape instead of
|
|
12
|
+
* re-deriving the magic values.
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_COMPILE_CONFIG = {
|
|
15
|
+
startPage: '',
|
|
16
|
+
scene: DEFAULT_SCENE,
|
|
17
|
+
queryParams: [],
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -13,6 +13,13 @@ export interface WorkbenchSettings {
|
|
|
13
13
|
watch: boolean;
|
|
14
14
|
};
|
|
15
15
|
theme: ThemeSource;
|
|
16
|
+
/**
|
|
17
|
+
* Parent directory used to pre-fill the "新建项目" dialog's target path.
|
|
18
|
+
* Updated whenever a project is successfully created. `null` until the
|
|
19
|
+
* first create; the dialog then falls back to a platform default
|
|
20
|
+
* (Documents).
|
|
21
|
+
*/
|
|
22
|
+
lastCreateBaseDir: string | null;
|
|
16
23
|
}
|
|
17
24
|
export declare function loadWorkbenchSettings(): WorkbenchSettings;
|
|
18
25
|
export declare function saveWorkbenchSettings(settings: WorkbenchSettings): void;
|
|
@@ -14,6 +14,7 @@ const DEFAULTS = {
|
|
|
14
14
|
watch: true,
|
|
15
15
|
},
|
|
16
16
|
theme: 'system',
|
|
17
|
+
lastCreateBaseDir: null,
|
|
17
18
|
};
|
|
18
19
|
function getSettingsFile() {
|
|
19
20
|
return path.join(app.getPath('userData'), 'dimina-workbench-settings.json');
|
|
@@ -37,6 +38,9 @@ export function loadWorkbenchSettings() {
|
|
|
37
38
|
watch: data.compile?.watch ?? DEFAULTS.compile.watch,
|
|
38
39
|
},
|
|
39
40
|
theme,
|
|
41
|
+
lastCreateBaseDir: typeof data.lastCreateBaseDir === 'string'
|
|
42
|
+
? data.lastCreateBaseDir
|
|
43
|
+
: DEFAULTS.lastCreateBaseDir,
|
|
40
44
|
};
|
|
41
45
|
}
|
|
42
46
|
catch {
|