create-cordisx-plugin 0.1.0-beta.1 → 0.1.0-beta.4
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 +75 -10
- package/dist/cli.js +668 -36
- package/package.json +8 -5
- package/template/README.md +48 -7
- package/template/README.zh-Hans.md +39 -0
- package/template/package.json +18 -5
- package/template/src/overview-page.tsx +30 -0
- package/template/src/vite-env.d.ts +1 -0
- package/template/src/{{packageName}}.tsx +15 -26
- package/template/test/manifest.mjs +17 -1
- package/template/tsconfig.json +1 -1
- package/template/vite.config.ts +13 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# create-cordisx-plugin
|
|
2
2
|
|
|
3
|
-
Create
|
|
3
|
+
Create trusted-local CordisX plugins in the project shape that fits their use.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm create cordisx-plugin@beta my-plugin
|
|
@@ -11,20 +11,76 @@ npx create-cordisx-plugin@beta my-plugin
|
|
|
11
11
|
The `@beta` qualifier is required while npm `latest` remains the non-functional
|
|
12
12
|
`0.0.0` package-name reservation.
|
|
13
13
|
|
|
14
|
-
The
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
The positional command remains the short path for one standalone plugin. Two
|
|
15
|
+
explicit modes cover larger project shapes:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# A dedicated repository containing several independent plugin packages.
|
|
19
|
+
npx create-cordisx-plugin@beta --mode workspace my-suite \
|
|
20
|
+
--plugin chatroom --plugin calendar
|
|
21
|
+
|
|
22
|
+
# Add a CordisX development package to an existing business project.
|
|
23
|
+
npx create-cordisx-plugin@beta --mode embedded ./my-business-project \
|
|
24
|
+
--plugin incident-room
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`workspace` creates one `cordisx.config.json`, one development server command,
|
|
28
|
+
and independently addressable packages under `plugins/<id>`. Use
|
|
29
|
+
`--package-manager pnpm` to also create `pnpm-workspace.yaml`; npm, Yarn, and Bun
|
|
30
|
+
can use the generated `package.json#workspaces` declaration.
|
|
31
|
+
|
|
32
|
+
`embedded` creates `.cordisx/config.json`, `.cordisx/package.json`, an
|
|
33
|
+
independent `.cordisx/tsconfig.json`, and `.cordisx/plugins/<id>`. Existing
|
|
34
|
+
business files are not replaced. Repeating the command with a new plugin id
|
|
35
|
+
appends that plugin while preserving the existing CordisX package and config
|
|
36
|
+
fields. The config entries are relative to `.cordisx/config.json`.
|
|
37
|
+
|
|
38
|
+
Embedded mode defaults to `--integration auto`: it joins a detected pnpm,
|
|
39
|
+
npm, Yarn, or Bun workspace, while retaining `.cordisx` as its own package and
|
|
40
|
+
TypeScript boundary. pnpm workspace integration updates the existing
|
|
41
|
+
`pnpm-workspace.yaml`; the other supported workspace shapes update the root
|
|
42
|
+
`package.json#workspaces`. Use `--integration isolated` to keep installation
|
|
43
|
+
inside `.cordisx`, or `--integration workspace` to require an existing
|
|
44
|
+
supported workspace. `--package-manager` makes non-interactive automation
|
|
45
|
+
deterministic. Existing pnpm comments, quoted keys, and block or flow sequence
|
|
46
|
+
styles are preserved. If the workspace YAML cannot be parsed or its `packages`
|
|
47
|
+
value cannot be updated without changing its meaning, creation stops and rolls
|
|
48
|
+
back without changing the business project.
|
|
49
|
+
|
|
50
|
+
Every generated plugin includes a version-1 manifest, a structured toolbar
|
|
51
|
+
route, and a component-only React page module. Its standalone, workspace, or
|
|
52
|
+
embedded environment supplies typecheck, production Vite build, manifest test,
|
|
53
|
+
and `cordisx dev --dry-run` commands. Production output uses a stable
|
|
54
|
+
`dist/runtime/module.js` entry plus content-addressed `chunks/`, `assets/`, and formal
|
|
55
|
+
`artifact.json`. Declarations remain outside that closed graph in `dist/types/`.
|
|
56
|
+
CSS and static assets remain external files, and dynamic
|
|
57
|
+
imports load their graph only when reached. Workspaces and embedded projects
|
|
58
|
+
build each plugin separately so independently replaceable generations never
|
|
59
|
+
share output chunks.
|
|
60
|
+
|
|
61
|
+
Every generated production config calls the public
|
|
62
|
+
`cordisx/vite` `cordisXPluginViteConfig()` helper. That reusable author/Host
|
|
63
|
+
pipeline owns the deterministic output rules, virtualizes only the closed Host
|
|
64
|
+
singleton imports, and emits the formal `dist/runtime/artifact.json`. A portable
|
|
65
|
+
CordisX package manifest points its browser entry at the adjacent prebuilt
|
|
66
|
+
`dist/runtime/module.js`; the Host validates and retains that complete indexed graph.
|
|
67
|
+
|
|
68
|
+
Development remains one CordisX-owned Vite server and HMR graph using the
|
|
69
|
+
original source entries. The environment installs no private React runtime:
|
|
70
|
+
React, React DOM, their types, JSX runtimes, and the initial CordisX component
|
|
71
|
+
set are provided by the Host through `cordisx/react` and `cordisx/ui`. Plugin
|
|
72
|
+
artifacts that bundle a private React copy are rejected.
|
|
20
73
|
|
|
21
74
|
The generated route and page use closed route-v2/page-v3 documents with real
|
|
22
75
|
localized title and description dictionaries. Canonical ids, path, outlet,
|
|
23
76
|
params, and chrome remain untranslated machine fields. CordisX owns route and
|
|
24
77
|
page chrome, the React root, theme, error boundary, and lifecycle cleanup; the
|
|
25
|
-
plugin owns only the controlled body component.
|
|
26
|
-
|
|
27
|
-
|
|
78
|
+
plugin owns only the controlled body component. Local component modules are
|
|
79
|
+
laid out as Vite React Fast Refresh boundaries; plugin entry, manifest, and
|
|
80
|
+
`apply()` changes use Cordis lifecycle replacement. Installed production
|
|
81
|
+
packages update through normal package lifecycle rather than development HMR.
|
|
82
|
+
The generator does not install to a marketplace, sign the plugin, or provide a
|
|
83
|
+
permission sandbox.
|
|
28
84
|
|
|
29
85
|
The `create-cordisx-plugin` tool itself is licensed under
|
|
30
86
|
`AGPL-3.0-or-later`. Files under its marked `template` directory and projects
|
|
@@ -33,3 +89,12 @@ An independent plugin using only public, versioned CordisX plugin interfaces
|
|
|
33
89
|
may be commercial, sold, distributed through a marketplace, and licensed under
|
|
34
90
|
terms chosen by its author. The custom Exception is not a standard SPDX
|
|
35
91
|
exception and should receive legal review before stable.
|
|
92
|
+
|
|
93
|
+
## CSS authoring
|
|
94
|
+
|
|
95
|
+
All three modes generate `src/vite-env.d.ts` in each plugin source tree.
|
|
96
|
+
Use side-effect CSS imports in the owning component and keep the complete
|
|
97
|
+
Vite output graph. Explicit `?inline` imports produce text; CSS Modules produce
|
|
98
|
+
class maps only with a compatible Host development transform. Consult the
|
|
99
|
+
[maintained CSS guidance](https://github.com/cordisx/cordisx/blob/main/skills/cordisx-plugin-development/references/css-and-lifecycle.md)
|
|
100
|
+
for ownership, current limitations, and separate HMR/unload verification.
|
package/dist/cli.js
CHANGED
|
@@ -1,24 +1,125 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { mkdir, readFile,
|
|
2
|
+
import { mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { isMap, isScalar, isSeq, parseDocument } from 'yaml';
|
|
5
6
|
const HELP = `Usage:
|
|
6
7
|
npm create cordisx-plugin <directory>
|
|
7
8
|
npx create-cordisx-plugin <directory>
|
|
9
|
+
create-cordisx-plugin --mode workspace <directory> --plugin <id> [--plugin <id> ...]
|
|
10
|
+
create-cordisx-plugin --mode embedded <project> --plugin <id> [options]
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
Modes:
|
|
13
|
+
single Create one standalone plugin project (default)
|
|
14
|
+
workspace Create a dedicated multi-plugin workspace
|
|
15
|
+
embedded Add one or more plugins under <project>/.cordisx
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--plugin <id> Plugin id; repeat for multiple plugins
|
|
19
|
+
--integration <mode> embedded mode: auto, workspace, or isolated
|
|
20
|
+
--package-manager <manager> auto, npm, pnpm, yarn, or bun
|
|
21
|
+
-h, --help Show this help
|
|
22
|
+
|
|
23
|
+
Embedded mode always creates an independent .cordisx package.json and
|
|
24
|
+
tsconfig.json. With --integration auto it joins an existing workspace when one
|
|
25
|
+
is detected; otherwise it remains an isolated Node project.`;
|
|
10
26
|
function packageName(directory) {
|
|
11
27
|
const normalized = path.basename(path.resolve(directory))
|
|
12
28
|
.normalize('NFKD')
|
|
13
29
|
.toLowerCase()
|
|
14
30
|
.replaceAll(/[^a-z0-9._-]+/g, '-')
|
|
15
31
|
.replace(/^[._-]+|[._-]+$/g, '')
|
|
16
|
-
.slice(0,
|
|
32
|
+
.slice(0, 180);
|
|
17
33
|
return normalized === '' ? 'cordisx-plugin' : normalized;
|
|
18
34
|
}
|
|
19
|
-
function
|
|
35
|
+
function derivedPluginId(name) {
|
|
20
36
|
return name === 'host' || name.startsWith('cordisx.') ? `local-${name}` : name;
|
|
21
37
|
}
|
|
38
|
+
function explicitPluginId(value) {
|
|
39
|
+
const id = value.trim();
|
|
40
|
+
if (!/^[a-z0-9][a-z0-9._-]{0,95}$/.test(id))
|
|
41
|
+
throw new Error(`invalid plugin id: ${value}`);
|
|
42
|
+
if (id === 'host' || id.startsWith('cordisx.'))
|
|
43
|
+
throw new Error(`reserved plugin id: ${id}`);
|
|
44
|
+
return id;
|
|
45
|
+
}
|
|
46
|
+
function optionValue(argv, index, name) {
|
|
47
|
+
const current = argv[index];
|
|
48
|
+
const inline = current?.startsWith(`${name}=`) === true ? current.slice(name.length + 1) : undefined;
|
|
49
|
+
if (inline !== undefined) {
|
|
50
|
+
if (inline === '')
|
|
51
|
+
throw new Error(`${name} requires a value`);
|
|
52
|
+
return [inline, index];
|
|
53
|
+
}
|
|
54
|
+
const next = argv[index + 1];
|
|
55
|
+
if (next === undefined || next.startsWith('-'))
|
|
56
|
+
throw new Error(`${name} requires a value`);
|
|
57
|
+
return [next, index + 1];
|
|
58
|
+
}
|
|
59
|
+
function choice(value, values, name) {
|
|
60
|
+
if (!values.includes(value))
|
|
61
|
+
throw new Error(`${name} must be one of: ${values.join(', ')}`);
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
function parseArguments(argv) {
|
|
65
|
+
if (argv.includes('--help') || argv.includes('-h'))
|
|
66
|
+
return 'help';
|
|
67
|
+
let mode = 'single';
|
|
68
|
+
let integration = 'auto';
|
|
69
|
+
let packageManager;
|
|
70
|
+
const plugins = [];
|
|
71
|
+
const positionals = [];
|
|
72
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
73
|
+
const argument = argv[index];
|
|
74
|
+
if (argument === '--mode' || argument.startsWith('--mode=')) {
|
|
75
|
+
const [value, nextIndex] = optionValue(argv, index, '--mode');
|
|
76
|
+
mode = choice(value, ['single', 'workspace', 'embedded'], '--mode');
|
|
77
|
+
index = nextIndex;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (argument === '--plugin' || argument.startsWith('--plugin=')) {
|
|
81
|
+
const [value, nextIndex] = optionValue(argv, index, '--plugin');
|
|
82
|
+
plugins.push(explicitPluginId(value));
|
|
83
|
+
index = nextIndex;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (argument === '--integration' || argument.startsWith('--integration=')) {
|
|
87
|
+
const [value, nextIndex] = optionValue(argv, index, '--integration');
|
|
88
|
+
integration = choice(value, ['auto', 'workspace', 'isolated'], '--integration');
|
|
89
|
+
index = nextIndex;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (argument === '--package-manager' || argument.startsWith('--package-manager=')) {
|
|
93
|
+
const [value, nextIndex] = optionValue(argv, index, '--package-manager');
|
|
94
|
+
if (value !== 'auto') {
|
|
95
|
+
packageManager = choice(value, ['npm', 'pnpm', 'yarn', 'bun'], '--package-manager');
|
|
96
|
+
}
|
|
97
|
+
index = nextIndex;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (argument.startsWith('-'))
|
|
101
|
+
throw new Error(`unknown option: ${argument}\n\n${HELP}`);
|
|
102
|
+
positionals.push(argument);
|
|
103
|
+
}
|
|
104
|
+
if (positionals.length !== 1)
|
|
105
|
+
throw new Error(HELP);
|
|
106
|
+
if (mode === 'single' && plugins.length > 0) {
|
|
107
|
+
throw new Error('--plugin is only valid with --mode workspace or --mode embedded');
|
|
108
|
+
}
|
|
109
|
+
if (mode !== 'embedded' && integration !== 'auto')
|
|
110
|
+
throw new Error('--integration is only valid with --mode embedded');
|
|
111
|
+
if (mode !== 'single' && plugins.length === 0)
|
|
112
|
+
throw new Error(`${mode} mode requires at least one --plugin <id>`);
|
|
113
|
+
if (new Set(plugins).size !== plugins.length)
|
|
114
|
+
throw new Error('plugin ids must be unique');
|
|
115
|
+
return {
|
|
116
|
+
directory: positionals[0],
|
|
117
|
+
mode,
|
|
118
|
+
plugins,
|
|
119
|
+
integration,
|
|
120
|
+
...(packageManager === undefined ? {} : { packageManager }),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
22
123
|
function replaceTokens(value, replacements) {
|
|
23
124
|
let output = value;
|
|
24
125
|
for (const [token, replacement] of Object.entries(replacements)) {
|
|
@@ -26,15 +127,12 @@ function replaceTokens(value, replacements) {
|
|
|
26
127
|
}
|
|
27
128
|
return output;
|
|
28
129
|
}
|
|
29
|
-
async function
|
|
130
|
+
async function pathState(target) {
|
|
30
131
|
try {
|
|
31
132
|
const targetStat = await stat(target);
|
|
32
133
|
if (!targetStat.isDirectory())
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (entries.length > 0)
|
|
36
|
-
throw new Error(`target directory is not empty: ${target}`);
|
|
37
|
-
return 'empty';
|
|
134
|
+
return 'file';
|
|
135
|
+
return (await readdir(target)).length === 0 ? 'empty' : 'nonempty';
|
|
38
136
|
}
|
|
39
137
|
catch (error) {
|
|
40
138
|
if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
|
|
@@ -42,6 +140,16 @@ async function targetState(target) {
|
|
|
42
140
|
throw error;
|
|
43
141
|
}
|
|
44
142
|
}
|
|
143
|
+
async function readOptional(file) {
|
|
144
|
+
try {
|
|
145
|
+
return await readFile(file, 'utf8');
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
|
|
149
|
+
return undefined;
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
45
153
|
async function renderTemplate(source, destination, replacements) {
|
|
46
154
|
await mkdir(destination, { recursive: true });
|
|
47
155
|
for (const entry of await readdir(source, { withFileTypes: true })) {
|
|
@@ -54,28 +162,145 @@ async function renderTemplate(source, destination, replacements) {
|
|
|
54
162
|
}
|
|
55
163
|
if (!entry.isFile())
|
|
56
164
|
throw new Error(`unsupported template entry: ${input}`);
|
|
57
|
-
|
|
58
|
-
|
|
165
|
+
await writeFile(output, replaceTokens(await readFile(input, 'utf8'), replacements), {
|
|
166
|
+
encoding: 'utf8',
|
|
167
|
+
flag: 'wx',
|
|
168
|
+
});
|
|
59
169
|
}
|
|
60
170
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
171
|
+
function json(value) {
|
|
172
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
173
|
+
}
|
|
174
|
+
function commonCompilerOptions() {
|
|
175
|
+
return {
|
|
176
|
+
target: 'ES2022',
|
|
177
|
+
module: 'NodeNext',
|
|
178
|
+
moduleResolution: 'NodeNext',
|
|
179
|
+
jsx: 'react-jsx',
|
|
180
|
+
jsxImportSource: 'cordisx/react',
|
|
181
|
+
lib: ['ES2023', 'DOM'],
|
|
182
|
+
strict: true,
|
|
183
|
+
declaration: true,
|
|
184
|
+
forceConsistentCasingInFileNames: true,
|
|
185
|
+
skipLibCheck: true,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function pluginConfig(ids) {
|
|
189
|
+
return json({
|
|
190
|
+
version: 1,
|
|
191
|
+
plugins: ids.map(id => ({ id, entry: `./plugins/${id}/src/index.tsx`, enabled: true, config: {} })),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
function manifestTest(ids, modulePath) {
|
|
195
|
+
const imports = ids.map((id, index) => `const plugin${index} = await import('${modulePath(id)}')`).join('\n');
|
|
196
|
+
const cases = ids.map((id, index) => ` ['${id}', plugin${index}],`).join('\n');
|
|
197
|
+
return `import assert from 'node:assert/strict'
|
|
198
|
+
import test from 'node:test'
|
|
199
|
+
|
|
200
|
+
const component = () => null
|
|
201
|
+
const noop = () => undefined
|
|
202
|
+
const React = new Proxy({ Fragment: Symbol('Fragment'), Suspense: component, lazy: () => component }, {
|
|
203
|
+
get(target, property) { return Reflect.get(target, property) ?? noop },
|
|
204
|
+
})
|
|
205
|
+
globalThis.__cordisxSharedReactRuntime = {
|
|
206
|
+
React,
|
|
207
|
+
defineReactPage: page => page,
|
|
208
|
+
jsxRuntime: { Fragment: React.Fragment, jsx: component, jsxs: component },
|
|
209
|
+
jsxDevRuntime: { Fragment: React.Fragment, jsxDEV: component },
|
|
210
|
+
ui: new Proxy({}, { get: () => component }),
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
${imports}
|
|
214
|
+
|
|
215
|
+
const plugins = [
|
|
216
|
+
${cases}
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
test('all configured CordisX plugins export matching manifests', () => {
|
|
220
|
+
for (const [id, plugin] of plugins) {
|
|
221
|
+
assert.equal(plugin.manifest.schemaVersion, 1)
|
|
222
|
+
assert.equal(plugin.manifest.id, id)
|
|
223
|
+
assert.equal(typeof plugin.apply, 'function')
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
`;
|
|
227
|
+
}
|
|
228
|
+
function projectBuildScript(configName) {
|
|
229
|
+
return `import { readFile } from 'node:fs/promises'
|
|
230
|
+
import path from 'node:path'
|
|
231
|
+
import { fileURLToPath } from 'node:url'
|
|
232
|
+
import { build } from 'vite'
|
|
233
|
+
|
|
234
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
235
|
+
const config = JSON.parse(await readFile(path.join(root, '${configName}'), 'utf8'))
|
|
236
|
+
if (config.version !== 1 || !Array.isArray(config.plugins)) throw new Error('CordisX project config must have version 1 and a plugins array')
|
|
237
|
+
const ids = new Set()
|
|
238
|
+
for (const plugin of config.plugins) {
|
|
239
|
+
if (plugin === null || typeof plugin !== 'object' || typeof plugin.id !== 'string'
|
|
240
|
+
|| !/^[a-z0-9][a-z0-9._-]{0,95}$/.test(plugin.id) || ids.has(plugin.id)) {
|
|
241
|
+
throw new Error('CordisX project config contains an invalid or duplicated plugin id')
|
|
242
|
+
}
|
|
243
|
+
ids.add(plugin.id)
|
|
244
|
+
await build({ configFile: path.join(root, 'plugins', plugin.id, 'vite.config.ts') })
|
|
245
|
+
}
|
|
246
|
+
`;
|
|
247
|
+
}
|
|
248
|
+
async function renderPluginSource(templateRoot, destination, id, outDir = 'dist/runtime') {
|
|
249
|
+
const replacements = { packageName: id, pluginId: id, sourceEntry: 'src/index.tsx', outDir };
|
|
250
|
+
const source = replaceTokens(await readFile(path.join(templateRoot, 'src', '{{packageName}}.tsx'), 'utf8'), replacements);
|
|
251
|
+
const page = replaceTokens(await readFile(path.join(templateRoot, 'src', 'overview-page.tsx'), 'utf8'), replacements);
|
|
252
|
+
const viteConfig = replaceTokens(await readFile(path.join(templateRoot, 'vite.config.ts'), 'utf8'), replacements);
|
|
253
|
+
await mkdir(path.join(destination, 'src'), { recursive: true });
|
|
254
|
+
await writeFile(path.join(destination, 'src', 'index.tsx'), source, { encoding: 'utf8', flag: 'wx' });
|
|
255
|
+
await writeFile(path.join(destination, 'src', 'overview-page.tsx'), page, { encoding: 'utf8', flag: 'wx' });
|
|
256
|
+
await writeFile(path.join(destination, 'src', 'vite-env.d.ts'), await readFile(path.join(templateRoot, 'src', 'vite-env.d.ts'), 'utf8'), { encoding: 'utf8', flag: 'wx' });
|
|
257
|
+
await writeFile(path.join(destination, 'vite.config.ts'), viteConfig, { encoding: 'utf8', flag: 'wx' });
|
|
258
|
+
await writeFile(path.join(destination, 'README.md'), `# ${id}\n\nThe local-development lifecycle entry is \`src/index.tsx\`. React page components live in component-only modules such as \`src/overview-page.tsx\` so Vite can apply React Fast Refresh. Production config calls the public \`cordisx/vite\` helper to write one immutable Vite ESM graph with \`module.js\`, content-addressed chunks, CSS, assets, and formal \`artifact.json\` under \`${outDir}/\` relative to this directory. Declarations are emitted separately according to the owning tsconfig. A portable package entry points at the adjacent \`${outDir}/module.js\`; the Host validates and retains the indexed graph.\n\nKeep \`src/vite-env.d.ts\` for Vite CSS types. Ordinary CSS is a side-effect import in its owning component; explicit \`?inline\` yields text. CSS HMR does not prove plugin-unload cleanup. See [CSS guidance](https://github.com/cordisx/cordisx/blob/main/skills/cordisx-plugin-development/references/css-and-lifecycle.md) before changing import forms.\n\nThis plugin starts private and \`UNLICENSED\`. Its generated source is Marked Template Material under the CordisX Independent Plugin Exception; choose a license before distribution.\n`, { encoding: 'utf8', flag: 'wx' });
|
|
259
|
+
await writeFile(path.join(destination, 'README.zh-Hans.md'), `# ${id}\n\n本地开发的生命周期入口为 \`src/index.tsx\`。React 页面组件位于 \`src/overview-page.tsx\` 等纯组件模块中,因此 Vite 可以应用 React Fast Refresh。生产配置调用公开的 \`cordisx/vite\` helper,在相对于此目录的 \`${outDir}/\` 下生成独立、不可变的 Vite ESM graph,包括 \`module.js\`、带内容摘要的 chunk、CSS、静态资源和正式的 \`artifact.json\`;类型声明按所属 tsconfig 单独输出。可移植 package 的入口指向相邻的 \`${outDir}/module.js\`;Host 会验证并保留该索引 graph。\n\n保留 \`src/vite-env.d.ts\` 提供的 Vite CSS 类型。普通 CSS 使用副作用导入;显式 \`?inline\` 返回文本。CSS HMR 不等于插件卸载回收。更改导入前阅读 [CSS 指导](https://github.com/cordisx/cordisx/blob/main/skills/cordisx-plugin-development/references/css-and-lifecycle.md)。\n\n此插件默认私有且使用 \`UNLICENSED\`。生成源码属于 CordisX 独立插件例外中的已标记模板材料;分发前请选择许可证。\n`, { encoding: 'utf8', flag: 'wx' });
|
|
260
|
+
}
|
|
261
|
+
function pluginPackage(workspaceName, id, cordisxVersion) {
|
|
262
|
+
return json({
|
|
263
|
+
name: `${workspaceName}-${id}`.slice(0, 214),
|
|
264
|
+
version: '0.1.0',
|
|
265
|
+
private: true,
|
|
266
|
+
license: 'UNLICENSED',
|
|
267
|
+
type: 'module',
|
|
268
|
+
files: ['dist', 'README.md', 'README.zh-Hans.md'],
|
|
269
|
+
main: './dist/runtime/module.js',
|
|
270
|
+
types: './dist/types/index.d.ts',
|
|
271
|
+
exports: {
|
|
272
|
+
'.': {
|
|
273
|
+
types: './dist/types/index.d.ts',
|
|
274
|
+
import: './dist/runtime/module.js',
|
|
275
|
+
default: './dist/runtime/module.js',
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
scripts: {
|
|
279
|
+
build: 'vite build && tsc -p tsconfig.json --emitDeclarationOnly',
|
|
280
|
+
typecheck: 'tsc -p tsconfig.json --noEmit',
|
|
281
|
+
},
|
|
282
|
+
devDependencies: {
|
|
283
|
+
'@deepseek-ai/cordis': '4.0.1',
|
|
284
|
+
cordisx: cordisxVersion,
|
|
285
|
+
typescript: '^5.9.2',
|
|
286
|
+
vite: '8.2.2',
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
function pluginTsconfig() {
|
|
291
|
+
return json({
|
|
292
|
+
extends: '../../tsconfig.base.json',
|
|
293
|
+
compilerOptions: { rootDir: 'src', outDir: 'dist/types', composite: true },
|
|
294
|
+
include: ['src/**/*.ts', 'src/**/*.tsx'],
|
|
295
|
+
exclude: ['dist', 'node_modules'],
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
async function createSingle(target, directory, manager, templateRoot, cordisxVersion) {
|
|
299
|
+
const state = await pathState(target);
|
|
300
|
+
if (state === 'file')
|
|
301
|
+
throw new Error(`target exists and is not a directory: ${target}`);
|
|
302
|
+
if (state === 'nonempty')
|
|
303
|
+
throw new Error(`target directory is not empty: ${target}`);
|
|
79
304
|
let created = false;
|
|
80
305
|
try {
|
|
81
306
|
if (state === 'missing') {
|
|
@@ -83,19 +308,426 @@ async function main(argv) {
|
|
|
83
308
|
created = true;
|
|
84
309
|
}
|
|
85
310
|
const name = packageName(directory);
|
|
311
|
+
const id = derivedPluginId(name);
|
|
86
312
|
await renderTemplate(templateRoot, target, {
|
|
87
313
|
packageName: name,
|
|
88
|
-
pluginId:
|
|
89
|
-
cordisxVersion
|
|
314
|
+
pluginId: id,
|
|
315
|
+
cordisxVersion,
|
|
316
|
+
packageManager: manager,
|
|
317
|
+
sourceEntry: `src/${id}.tsx`,
|
|
318
|
+
outDir: 'dist/runtime',
|
|
90
319
|
});
|
|
320
|
+
if (name !== id)
|
|
321
|
+
await rename(path.join(target, 'src', `${name}.tsx`), path.join(target, 'src', `${id}.tsx`));
|
|
91
322
|
}
|
|
92
323
|
catch (error) {
|
|
93
|
-
|
|
94
|
-
|
|
324
|
+
await rm(target, { recursive: true, force: true });
|
|
325
|
+
if (!created)
|
|
326
|
+
await mkdir(target);
|
|
95
327
|
throw error;
|
|
96
328
|
}
|
|
97
|
-
|
|
98
|
-
|
|
329
|
+
}
|
|
330
|
+
async function createWorkspace(target, directory, ids, manager, templateRoot, cordisxVersion) {
|
|
331
|
+
const state = await pathState(target);
|
|
332
|
+
if (state === 'file')
|
|
333
|
+
throw new Error(`target exists and is not a directory: ${target}`);
|
|
334
|
+
if (state === 'nonempty')
|
|
335
|
+
throw new Error(`target directory is not empty: ${target}`);
|
|
336
|
+
let created = false;
|
|
337
|
+
try {
|
|
338
|
+
if (state === 'missing') {
|
|
339
|
+
await mkdir(target, { recursive: false });
|
|
340
|
+
created = true;
|
|
341
|
+
}
|
|
342
|
+
const name = packageName(directory);
|
|
343
|
+
await writeFile(path.join(target, 'package.json'), json({
|
|
344
|
+
name,
|
|
345
|
+
version: '0.1.0',
|
|
346
|
+
private: true,
|
|
347
|
+
license: 'UNLICENSED',
|
|
348
|
+
type: 'module',
|
|
349
|
+
workspaces: ['plugins/*'],
|
|
350
|
+
scripts: {
|
|
351
|
+
build: 'node scripts/build-plugins.mjs && tsc -b --emitDeclarationOnly --force',
|
|
352
|
+
check: 'tsc -b && node scripts/build-plugins.mjs && tsc -b --emitDeclarationOnly --force && node --test test/plugins.mjs',
|
|
353
|
+
dev: 'cordisx dev --config ./cordisx.config.json',
|
|
354
|
+
'dev:dry-run': 'cordisx dev --config ./cordisx.config.json --dry-run',
|
|
355
|
+
test: 'node --test test/plugins.mjs',
|
|
356
|
+
typecheck: 'tsc -b',
|
|
357
|
+
},
|
|
358
|
+
devDependencies: { cordisx: cordisxVersion, typescript: '^5.9.2', vite: '8.2.2' },
|
|
359
|
+
engines: { node: '>=22.19' },
|
|
360
|
+
}), { encoding: 'utf8', flag: 'wx' });
|
|
361
|
+
await writeFile(path.join(target, 'tsconfig.base.json'), json({ compilerOptions: commonCompilerOptions() }), {
|
|
362
|
+
encoding: 'utf8',
|
|
363
|
+
flag: 'wx',
|
|
364
|
+
});
|
|
365
|
+
await writeFile(path.join(target, 'tsconfig.json'), json({
|
|
366
|
+
files: [],
|
|
367
|
+
references: ids.map(id => ({ path: `./plugins/${id}` })),
|
|
368
|
+
}), { encoding: 'utf8', flag: 'wx' });
|
|
369
|
+
await writeFile(path.join(target, 'cordisx.config.json'), pluginConfig(ids), { encoding: 'utf8', flag: 'wx' });
|
|
370
|
+
await mkdir(path.join(target, 'scripts'), { recursive: true });
|
|
371
|
+
await writeFile(path.join(target, 'scripts', 'build-plugins.mjs'), projectBuildScript('cordisx.config.json'), {
|
|
372
|
+
encoding: 'utf8',
|
|
373
|
+
flag: 'wx',
|
|
374
|
+
});
|
|
375
|
+
await writeFile(path.join(target, '.gitignore'), 'node_modules/\ndist/\n*.tsbuildinfo\n', {
|
|
376
|
+
encoding: 'utf8',
|
|
377
|
+
flag: 'wx',
|
|
378
|
+
});
|
|
379
|
+
await writeFile(path.join(target, 'README.md'), `# ${name}\n\nA dedicated CordisX multi-plugin workspace. Each plugin builds into its own Vite ESM graph so chunks and assets never couple independently replaceable plugin generations.\n\n\`\`\`bash\n${manager} install\n${manager} run check\n${manager} run dev:dry-run\n${manager} run dev\n\`\`\`\n`, { encoding: 'utf8', flag: 'wx' });
|
|
380
|
+
if (manager === 'pnpm') {
|
|
381
|
+
await writeFile(path.join(target, 'pnpm-workspace.yaml'), "packages:\n - 'plugins/*'\n", {
|
|
382
|
+
encoding: 'utf8',
|
|
383
|
+
flag: 'wx',
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
for (const id of ids) {
|
|
387
|
+
const pluginRoot = path.join(target, 'plugins', id);
|
|
388
|
+
await renderPluginSource(templateRoot, pluginRoot, id);
|
|
389
|
+
await writeFile(path.join(pluginRoot, 'package.json'), pluginPackage(name, id, cordisxVersion), {
|
|
390
|
+
encoding: 'utf8',
|
|
391
|
+
flag: 'wx',
|
|
392
|
+
});
|
|
393
|
+
await writeFile(path.join(pluginRoot, 'tsconfig.json'), pluginTsconfig(), { encoding: 'utf8', flag: 'wx' });
|
|
394
|
+
}
|
|
395
|
+
await mkdir(path.join(target, 'test'), { recursive: true });
|
|
396
|
+
await writeFile(path.join(target, 'test', 'plugins.mjs'), manifestTest(ids, id => `../plugins/${id}/dist/runtime/module.js`), { encoding: 'utf8', flag: 'wx' });
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
await rm(target, { recursive: true, force: true });
|
|
400
|
+
if (!created)
|
|
401
|
+
await mkdir(target);
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
function packageManagerFromManifest(contents) {
|
|
406
|
+
if (contents === undefined)
|
|
407
|
+
return undefined;
|
|
408
|
+
const manifest = JSON.parse(contents);
|
|
409
|
+
if (typeof manifest.packageManager !== 'string')
|
|
410
|
+
return undefined;
|
|
411
|
+
const name = manifest.packageManager.split('@')[0];
|
|
412
|
+
return name === 'npm' || name === 'pnpm' || name === 'yarn' || name === 'bun' ? name : undefined;
|
|
413
|
+
}
|
|
414
|
+
async function detectedPackageManager(projectRoot, manifest) {
|
|
415
|
+
const declared = packageManagerFromManifest(manifest);
|
|
416
|
+
if (declared !== undefined)
|
|
417
|
+
return declared;
|
|
418
|
+
for (const [file, manager] of [
|
|
419
|
+
['pnpm-lock.yaml', 'pnpm'],
|
|
420
|
+
['yarn.lock', 'yarn'],
|
|
421
|
+
['bun.lock', 'bun'],
|
|
422
|
+
['bun.lockb', 'bun'],
|
|
423
|
+
['package-lock.json', 'npm'],
|
|
424
|
+
]) {
|
|
425
|
+
if (await readOptional(path.join(projectRoot, file)) !== undefined)
|
|
426
|
+
return manager;
|
|
427
|
+
}
|
|
428
|
+
return 'npm';
|
|
429
|
+
}
|
|
430
|
+
async function detectWorkspace(projectRoot) {
|
|
431
|
+
for (const file of ['pnpm-workspace.yaml', 'pnpm-workspace.yml']) {
|
|
432
|
+
const manifestPath = path.join(projectRoot, file);
|
|
433
|
+
if (await readOptional(manifestPath) !== undefined)
|
|
434
|
+
return { manager: 'pnpm', kind: 'pnpm-yaml', manifestPath };
|
|
435
|
+
}
|
|
436
|
+
const manifestPath = path.join(projectRoot, 'package.json');
|
|
437
|
+
const contents = await readOptional(manifestPath);
|
|
438
|
+
if (contents === undefined)
|
|
439
|
+
return undefined;
|
|
440
|
+
const manifest = JSON.parse(contents);
|
|
441
|
+
const workspaces = Array.isArray(manifest.workspaces)
|
|
442
|
+
? manifest.workspaces
|
|
443
|
+
: manifest.workspaces !== null && typeof manifest.workspaces === 'object'
|
|
444
|
+
? manifest.workspaces.packages
|
|
445
|
+
: undefined;
|
|
446
|
+
if (!Array.isArray(workspaces))
|
|
447
|
+
return undefined;
|
|
448
|
+
const manager = await detectedPackageManager(projectRoot, contents);
|
|
449
|
+
// pnpm intentionally ignores package.json#workspaces; only its YAML file is
|
|
450
|
+
// evidence that the project is a pnpm workspace.
|
|
451
|
+
if (manager === 'pnpm')
|
|
452
|
+
return undefined;
|
|
453
|
+
return { manager, kind: 'package-json', manifestPath };
|
|
454
|
+
}
|
|
455
|
+
function addPnpmWorkspace(contents, entry) {
|
|
456
|
+
const normalized = entry.replaceAll('\\', '/');
|
|
457
|
+
const verifyUpdate = (candidate) => {
|
|
458
|
+
const updated = parseDocument(candidate, { prettyErrors: true });
|
|
459
|
+
if (updated.errors.length > 0) {
|
|
460
|
+
throw new Error('cannot safely update pnpm-workspace.yaml: the updated document did not validate');
|
|
461
|
+
}
|
|
462
|
+
const updatedPackages = updated.get('packages', true);
|
|
463
|
+
if (!isSeq(updatedPackages) || !updatedPackages.items.some(item => isScalar(item) && item.value === normalized)) {
|
|
464
|
+
throw new Error('cannot safely update pnpm-workspace.yaml: the updated document did not validate');
|
|
465
|
+
}
|
|
466
|
+
return candidate;
|
|
467
|
+
};
|
|
468
|
+
const document = parseDocument(contents, { keepSourceTokens: true, prettyErrors: true });
|
|
469
|
+
if (document.errors.length > 0) {
|
|
470
|
+
throw new Error(`cannot safely update pnpm-workspace.yaml: ${document.errors[0].message}`);
|
|
471
|
+
}
|
|
472
|
+
if (!isMap(document.contents)) {
|
|
473
|
+
throw new Error('cannot safely update pnpm-workspace.yaml: the document root must be a mapping');
|
|
474
|
+
}
|
|
475
|
+
const packages = document.get('packages', true);
|
|
476
|
+
if (packages === undefined && !document.contents.has('packages')) {
|
|
477
|
+
const range = document.contents.range;
|
|
478
|
+
if (range === null || range === undefined) {
|
|
479
|
+
throw new Error('cannot safely update pnpm-workspace.yaml: the root mapping has no editable source range');
|
|
480
|
+
}
|
|
481
|
+
const newline = contents.includes('\r\n') ? '\r\n' : '\n';
|
|
482
|
+
const insertion = `${contents.slice(0, range[1]).endsWith('\n') ? '' : newline}packages:${newline} - '${normalized.replaceAll("'", "''")}'${newline}`;
|
|
483
|
+
return verifyUpdate(`${contents.slice(0, range[1])}${insertion}${contents.slice(range[1])}`);
|
|
484
|
+
}
|
|
485
|
+
if (!isSeq(packages)) {
|
|
486
|
+
throw new Error('cannot safely update pnpm-workspace.yaml: packages must be a sequence');
|
|
487
|
+
}
|
|
488
|
+
if (packages.anchor !== undefined || packages.tag !== undefined) {
|
|
489
|
+
throw new Error('cannot safely update pnpm-workspace.yaml: anchored or tagged packages sequences are not supported');
|
|
490
|
+
}
|
|
491
|
+
const values = packages.items.map(item => {
|
|
492
|
+
if (!isScalar(item) || typeof item.value !== 'string') {
|
|
493
|
+
throw new Error('cannot safely update pnpm-workspace.yaml: every packages entry must be a string');
|
|
494
|
+
}
|
|
495
|
+
return item.value;
|
|
496
|
+
});
|
|
497
|
+
if (values.includes(normalized))
|
|
498
|
+
return contents;
|
|
499
|
+
const range = packages.range;
|
|
500
|
+
if (range === null || range === undefined) {
|
|
501
|
+
throw new Error('cannot safely update pnpm-workspace.yaml: packages has no editable source range');
|
|
502
|
+
}
|
|
503
|
+
const quoted = `'${normalized.replaceAll("'", "''")}'`;
|
|
504
|
+
const [start, end] = range;
|
|
505
|
+
if (packages.flow === true) {
|
|
506
|
+
const closing = end - 1;
|
|
507
|
+
if (contents[closing] !== ']') {
|
|
508
|
+
throw new Error('cannot safely update pnpm-workspace.yaml: packages flow sequence has no closing bracket');
|
|
509
|
+
}
|
|
510
|
+
const inner = contents.slice(start + 1, closing);
|
|
511
|
+
const trailingWhitespace = inner.match(/\s*$/)?.[0] ?? '';
|
|
512
|
+
const body = inner.slice(0, inner.length - trailingWhitespace.length);
|
|
513
|
+
const separator = body.trim() === '' || body.trimEnd().endsWith(',') ? '' : ',';
|
|
514
|
+
const spacing = body === '' || /\s$/.test(body) ? '' : ' ';
|
|
515
|
+
return verifyUpdate(`${contents.slice(0, start + 1)}${body}${separator}${spacing}${quoted}${trailingWhitespace}${contents.slice(closing)}`);
|
|
516
|
+
}
|
|
517
|
+
const source = contents.slice(start, end);
|
|
518
|
+
const newline = contents.includes('\r\n') ? '\r\n' : '\n';
|
|
519
|
+
const lineStart = contents.lastIndexOf('\n', start - 1) + 1;
|
|
520
|
+
const indent = contents.slice(lineStart, start);
|
|
521
|
+
if (!/^\s*$/.test(indent)) {
|
|
522
|
+
throw new Error('cannot safely update pnpm-workspace.yaml: packages sequence indentation is ambiguous');
|
|
523
|
+
}
|
|
524
|
+
const insertion = source.endsWith('\n') ? `${indent}- ${quoted}${newline}` : `${newline}${indent}- ${quoted}`;
|
|
525
|
+
return verifyUpdate(`${contents.slice(0, end)}${insertion}${contents.slice(end)}`);
|
|
526
|
+
}
|
|
527
|
+
function addPackageWorkspace(contents, entry) {
|
|
528
|
+
const manifest = JSON.parse(contents);
|
|
529
|
+
const current = manifest.workspaces;
|
|
530
|
+
if (Array.isArray(current)) {
|
|
531
|
+
if (!current.includes(entry))
|
|
532
|
+
manifest.workspaces = [...current, entry];
|
|
533
|
+
}
|
|
534
|
+
else if (current !== null && typeof current === 'object' && Array.isArray(current.packages)) {
|
|
535
|
+
const packages = current.packages;
|
|
536
|
+
if (!packages.includes(entry))
|
|
537
|
+
manifest.workspaces = { ...current, packages: [...packages, entry] };
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
throw new Error('project package.json has an unsupported workspaces declaration');
|
|
541
|
+
}
|
|
542
|
+
return json(manifest);
|
|
543
|
+
}
|
|
544
|
+
function embeddedPackage(projectName, cordisxVersion, packageManagerSpec) {
|
|
545
|
+
return json({
|
|
546
|
+
name: `${projectName}-cordisx-development`.slice(0, 214),
|
|
547
|
+
private: true,
|
|
548
|
+
license: 'UNLICENSED',
|
|
549
|
+
type: 'module',
|
|
550
|
+
...(packageManagerSpec === undefined ? {} : { packageManager: packageManagerSpec }),
|
|
551
|
+
scripts: {
|
|
552
|
+
build: 'node scripts/build-plugins.mjs && tsc -p tsconfig.json --emitDeclarationOnly',
|
|
553
|
+
check: 'tsc -p tsconfig.json --noEmit && node scripts/build-plugins.mjs && tsc -p tsconfig.json --emitDeclarationOnly && node --test test/*.mjs',
|
|
554
|
+
dev: 'cordisx dev --config ./config.json',
|
|
555
|
+
'dev:dry-run': 'cordisx dev --config ./config.json --dry-run',
|
|
556
|
+
test: 'node --test test/*.mjs',
|
|
557
|
+
typecheck: 'tsc -p tsconfig.json --noEmit',
|
|
558
|
+
},
|
|
559
|
+
devDependencies: {
|
|
560
|
+
'@deepseek-ai/cordis': '4.0.1',
|
|
561
|
+
cordisx: cordisxVersion,
|
|
562
|
+
typescript: '^5.9.2',
|
|
563
|
+
vite: '8.2.2',
|
|
564
|
+
},
|
|
565
|
+
engines: { node: '>=22.19' },
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
function embeddedTsconfig() {
|
|
569
|
+
return json({
|
|
570
|
+
compilerOptions: { ...commonCompilerOptions(), rootDir: 'plugins', outDir: 'dist/types' },
|
|
571
|
+
include: ['plugins/**/*.ts', 'plugins/**/*.tsx'],
|
|
572
|
+
exclude: ['dist', 'node_modules'],
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
function embeddedReadme(manager, integrated) {
|
|
576
|
+
const install = integrated
|
|
577
|
+
? `Run \`${manager} install\` from the containing workspace root.`
|
|
578
|
+
: manager === 'pnpm'
|
|
579
|
+
? 'Run `pnpm install --ignore-workspace` in this directory.'
|
|
580
|
+
: `Run \`${manager} install\` in this directory.`;
|
|
581
|
+
return `# Project CordisX development\n\nThis directory is an independent Node and TypeScript boundary for CordisX plugins embedded in the containing business project. Each configured plugin builds into its own Vite ESM graph under \`dist/runtime/<plugin-id>/\`, while declarations stay in \`dist/types/\`; production chunks and assets do not cross plugin generation boundaries.\n\n${install}\n\nThen run \`${manager} run check\`, \`${manager} run dev:dry-run\`, or \`${manager} run dev\` here.\n`;
|
|
582
|
+
}
|
|
583
|
+
async function existingEmbeddedConfig(file) {
|
|
584
|
+
const contents = await readOptional(file);
|
|
585
|
+
if (contents === undefined)
|
|
586
|
+
return { value: { version: 1, plugins: [] }, ids: [] };
|
|
587
|
+
const value = JSON.parse(contents);
|
|
588
|
+
if (value.version !== 1 || !Array.isArray(value.plugins)) {
|
|
589
|
+
throw new Error(`${file} is not a version-1 CordisX project config`);
|
|
590
|
+
}
|
|
591
|
+
const ids = value.plugins.map((plugin, index) => {
|
|
592
|
+
if (plugin === null || typeof plugin !== 'object' || typeof plugin.id !== 'string') {
|
|
593
|
+
throw new Error(`${file} plugins[${index}] is invalid`);
|
|
594
|
+
}
|
|
595
|
+
return plugin.id;
|
|
596
|
+
});
|
|
597
|
+
return { value, ids };
|
|
598
|
+
}
|
|
599
|
+
async function createEmbedded(target, ids, requestedIntegration, requestedManager, templateRoot, cordisxVersion) {
|
|
600
|
+
const state = await pathState(target);
|
|
601
|
+
if (state === 'missing')
|
|
602
|
+
throw new Error(`embedded project does not exist: ${target}`);
|
|
603
|
+
if (state === 'file')
|
|
604
|
+
throw new Error(`embedded target is not a directory: ${target}`);
|
|
605
|
+
const projectManifest = await readOptional(path.join(target, 'package.json'));
|
|
606
|
+
const workspace = await detectWorkspace(target);
|
|
607
|
+
const integrated = requestedIntegration === 'workspace'
|
|
608
|
+
|| (requestedIntegration === 'auto' && workspace !== undefined);
|
|
609
|
+
if (integrated && workspace === undefined) {
|
|
610
|
+
throw new Error('workspace integration requested, but the project has no supported workspace declaration');
|
|
611
|
+
}
|
|
612
|
+
if (integrated && requestedManager !== undefined && requestedManager !== workspace.manager) {
|
|
613
|
+
throw new Error(`detected ${workspace.manager} workspace does not match --package-manager ${requestedManager}`);
|
|
614
|
+
}
|
|
615
|
+
const manager = requestedManager ?? workspace?.manager ?? await detectedPackageManager(target, projectManifest);
|
|
616
|
+
const declaredManagerSpec = projectManifest === undefined
|
|
617
|
+
? undefined
|
|
618
|
+
: JSON.parse(projectManifest).packageManager;
|
|
619
|
+
const isolatedManagerSpec = !integrated
|
|
620
|
+
&& typeof declaredManagerSpec === 'string'
|
|
621
|
+
&& declaredManagerSpec.startsWith(`${manager}@`)
|
|
622
|
+
? declaredManagerSpec
|
|
623
|
+
: undefined;
|
|
624
|
+
const cordisxRoot = path.join(target, '.cordisx');
|
|
625
|
+
const cordisxRootWasMissing = await pathState(cordisxRoot) === 'missing';
|
|
626
|
+
const configPath = path.join(cordisxRoot, 'config.json');
|
|
627
|
+
const existing = await existingEmbeddedConfig(configPath);
|
|
628
|
+
for (const id of ids) {
|
|
629
|
+
if (existing.ids.includes(id))
|
|
630
|
+
throw new Error(`plugin already exists in .cordisx/config.json: ${id}`);
|
|
631
|
+
if (await pathState(path.join(cordisxRoot, 'plugins', id)) !== 'missing') {
|
|
632
|
+
throw new Error(`plugin directory already exists: .cordisx/plugins/${id}`);
|
|
633
|
+
}
|
|
634
|
+
if (await readOptional(path.join(cordisxRoot, 'test', `${id}.mjs`)) !== undefined) {
|
|
635
|
+
throw new Error(`plugin test already exists: .cordisx/test/${id}.mjs`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
const previousFiles = new Map();
|
|
639
|
+
const pluginRoots = ids.map(id => path.join(cordisxRoot, 'plugins', id));
|
|
640
|
+
const remember = async (file) => {
|
|
641
|
+
if (!previousFiles.has(file))
|
|
642
|
+
previousFiles.set(file, await readOptional(file));
|
|
643
|
+
};
|
|
644
|
+
const put = async (file, contents, onlyWhenMissing = false) => {
|
|
645
|
+
await remember(file);
|
|
646
|
+
if (onlyWhenMissing && previousFiles.get(file) !== undefined)
|
|
647
|
+
return;
|
|
648
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
649
|
+
await writeFile(file, contents, 'utf8');
|
|
650
|
+
};
|
|
651
|
+
try {
|
|
652
|
+
const nextPlugins = [
|
|
653
|
+
...existing.value.plugins,
|
|
654
|
+
...ids.map(id => ({ id, entry: `./plugins/${id}/src/index.tsx`, enabled: true, config: {} })),
|
|
655
|
+
];
|
|
656
|
+
await put(configPath, json({ ...existing.value, plugins: nextPlugins }));
|
|
657
|
+
await put(path.join(cordisxRoot, 'package.json'), embeddedPackage(packageName(target), cordisxVersion, isolatedManagerSpec), true);
|
|
658
|
+
await put(path.join(cordisxRoot, 'tsconfig.json'), embeddedTsconfig(), true);
|
|
659
|
+
await put(path.join(cordisxRoot, 'scripts', 'build-plugins.mjs'), projectBuildScript('config.json'), true);
|
|
660
|
+
await put(path.join(cordisxRoot, '.gitignore'), 'node_modules/\ndist/\ncache/\n*.tsbuildinfo\n', true);
|
|
661
|
+
await put(path.join(cordisxRoot, 'README.md'), embeddedReadme(manager, integrated), true);
|
|
662
|
+
if (!integrated && manager === 'yarn')
|
|
663
|
+
await put(path.join(cordisxRoot, 'yarn.lock'), '', true);
|
|
664
|
+
for (const id of ids) {
|
|
665
|
+
await renderPluginSource(templateRoot, path.join(cordisxRoot, 'plugins', id), id, `../../dist/runtime/${id}`);
|
|
666
|
+
}
|
|
667
|
+
for (const id of ids) {
|
|
668
|
+
await put(path.join(cordisxRoot, 'test', `${id}.mjs`), manifestTest([id], pluginId => `../dist/runtime/${pluginId}/module.js`));
|
|
669
|
+
}
|
|
670
|
+
if (integrated) {
|
|
671
|
+
const workspaceEntry = path.relative(target, cordisxRoot).replaceAll('\\', '/') || '.cordisx';
|
|
672
|
+
await remember(workspace.manifestPath);
|
|
673
|
+
const contents = previousFiles.get(workspace.manifestPath);
|
|
674
|
+
await writeFile(workspace.manifestPath, workspace.kind === 'pnpm-yaml'
|
|
675
|
+
? addPnpmWorkspace(contents, workspaceEntry)
|
|
676
|
+
: addPackageWorkspace(contents, workspaceEntry), 'utf8');
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
catch (error) {
|
|
680
|
+
for (const pluginRoot of pluginRoots)
|
|
681
|
+
await rm(pluginRoot, { recursive: true, force: true });
|
|
682
|
+
for (const [file, previous] of [...previousFiles].reverse()) {
|
|
683
|
+
if (previous === undefined)
|
|
684
|
+
await rm(file, { force: true });
|
|
685
|
+
else
|
|
686
|
+
await writeFile(file, previous, 'utf8');
|
|
687
|
+
}
|
|
688
|
+
if (cordisxRootWasMissing)
|
|
689
|
+
await rm(cordisxRoot, { recursive: true, force: true });
|
|
690
|
+
throw error;
|
|
691
|
+
}
|
|
692
|
+
return { manager, integrated };
|
|
693
|
+
}
|
|
694
|
+
async function main(argv) {
|
|
695
|
+
const parsed = parseArguments(argv);
|
|
696
|
+
if (parsed === 'help') {
|
|
697
|
+
console.log(HELP);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const target = path.resolve(process.cwd(), parsed.directory);
|
|
701
|
+
const packageRoot = fileURLToPath(new URL('..', import.meta.url));
|
|
702
|
+
const templateRoot = path.join(packageRoot, 'template');
|
|
703
|
+
const creatorManifest = JSON.parse(await readFile(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
704
|
+
if (typeof creatorManifest.version !== 'string' || creatorManifest.version.length === 0) {
|
|
705
|
+
throw new Error('create-cordisx-plugin package version is invalid');
|
|
706
|
+
}
|
|
707
|
+
if (parsed.mode === 'single') {
|
|
708
|
+
const manager = parsed.packageManager ?? 'npm';
|
|
709
|
+
await createSingle(target, parsed.directory, manager, templateRoot, creatorManifest.version);
|
|
710
|
+
console.log(`Created CordisX plugin in ${target}`);
|
|
711
|
+
console.log(`\nNext steps:\n cd ${parsed.directory}\n ${manager} install\n ${manager} run check\n ${manager} run dev:dry-run`);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (parsed.mode === 'workspace') {
|
|
715
|
+
const manager = parsed.packageManager ?? 'npm';
|
|
716
|
+
await createWorkspace(target, parsed.directory, parsed.plugins, manager, templateRoot, creatorManifest.version);
|
|
717
|
+
console.log(`Created CordisX plugin workspace in ${target}`);
|
|
718
|
+
console.log(`\nNext steps:\n cd ${parsed.directory}\n ${manager} install\n ${manager} run check\n ${manager} run dev:dry-run`);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const result = await createEmbedded(target, parsed.plugins, parsed.integration, parsed.packageManager, templateRoot, creatorManifest.version);
|
|
722
|
+
console.log(`Added CordisX plugin${parsed.plugins.length === 1 ? '' : 's'} to ${path.join(target, '.cordisx')}`);
|
|
723
|
+
console.log(`Environment: ${result.integrated ? `${result.manager} workspace` : `isolated ${result.manager}`}`);
|
|
724
|
+
if (result.integrated) {
|
|
725
|
+
console.log(`\nNext steps:\n cd ${parsed.directory}\n ${result.manager} install\n cd .cordisx\n ${result.manager} run check\n ${result.manager} run dev:dry-run`);
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
const install = result.manager === 'pnpm' ? 'pnpm install --ignore-workspace' : `${result.manager} install`;
|
|
729
|
+
console.log(`\nNext steps:\n cd ${path.join(parsed.directory, '.cordisx')}\n ${install}\n ${result.manager} run check\n ${result.manager} run dev:dry-run`);
|
|
730
|
+
}
|
|
99
731
|
}
|
|
100
732
|
main(process.argv.slice(2)).catch((error) => {
|
|
101
733
|
console.error(`[create-cordisx-plugin] ${error instanceof Error ? error.message : String(error)}`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-cordisx-plugin",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
4
|
-
"description": "Create
|
|
3
|
+
"version": "0.1.0-beta.4",
|
|
4
|
+
"description": "Create standalone, workspace, or embedded CordisX plugin projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist",
|
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
"create-cordisx-plugin": "dist/cli.js"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
|
-
"build": "tsc -p tsconfig.json",
|
|
19
|
-
"check": "npm run typecheck",
|
|
18
|
+
"build": "tsc -p tsconfig.json && node scripts/normalize-bin-mode.mjs",
|
|
19
|
+
"check": "npm run typecheck && npm run build && npm test",
|
|
20
20
|
"check:package": "node scripts/check-package-contents.mjs",
|
|
21
21
|
"prepack": "npm run build",
|
|
22
|
+
"test": "node --test test/*.node-test.mjs",
|
|
22
23
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
23
24
|
},
|
|
24
25
|
"engines": {
|
|
@@ -42,11 +43,13 @@
|
|
|
42
43
|
],
|
|
43
44
|
"publishConfig": {
|
|
44
45
|
"access": "public",
|
|
45
|
-
"tag": "beta",
|
|
46
46
|
"provenance": true
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/node": "^22.18.1",
|
|
50
50
|
"typescript": "^5.9.2"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"yaml": "2.8.1"
|
|
51
54
|
}
|
|
52
55
|
}
|
package/template/README.md
CHANGED
|
@@ -1,17 +1,36 @@
|
|
|
1
1
|
# {{packageName}}
|
|
2
2
|
|
|
3
3
|
A minimal trusted-local CordisX React plugin. Its manifest is exported from
|
|
4
|
-
`src/{{pluginId}}.tsx`, which is also
|
|
4
|
+
`src/{{pluginId}}.tsx`, which is also CordisX's local-development entry.
|
|
5
|
+
The page component lives in `src/overview-page.tsx` as a named component-only
|
|
6
|
+
module so local development can preserve its state with React Fast Refresh.
|
|
5
7
|
|
|
6
8
|
```bash
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
{{packageManager}} install
|
|
10
|
+
{{packageManager}} run check
|
|
11
|
+
{{packageManager}} run dev:dry-run
|
|
12
|
+
{{packageManager}} run dev
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
`
|
|
14
|
-
`
|
|
15
|
+
`{{packageManager}} run dev:dry-run` validates the plugin module graph without
|
|
16
|
+
launching Codex Desktop. `{{packageManager}} run dev` launches the separate
|
|
17
|
+
CordisX development host. Component-only modules use React Fast Refresh;
|
|
18
|
+
manifest, entry, and `apply()` changes replace the plugin generation.
|
|
19
|
+
|
|
20
|
+
`{{packageManager}} run build` creates a production Vite ESM graph in `dist/runtime/`.
|
|
21
|
+
Its stable `module.js` entry may load content-addressed JavaScript chunks, CSS,
|
|
22
|
+
and static assets only when an `import()` reaches them. The generated formal
|
|
23
|
+
`artifact.json` records that graph for packaging; declarations are kept outside
|
|
24
|
+
the closed graph in `dist/types/`, and `package.json#files` includes the complete
|
|
25
|
+
`dist/` tree. Production generations remain immutable package
|
|
26
|
+
artifacts and do not use the development HMR connection.
|
|
27
|
+
|
|
28
|
+
The generated config calls the public `cordisx/vite`
|
|
29
|
+
`cordisXPluginViteConfig()` helper. That shared author/Host pipeline owns the
|
|
30
|
+
output rules, virtualizes only the closed Host singleton imports, and emits the
|
|
31
|
+
formal `dist/runtime/artifact.json`. A portable CordisX package manifest points its
|
|
32
|
+
browser entry at the adjacent prebuilt `dist/runtime/module.js`; the Host validates and
|
|
33
|
+
retains the complete indexed graph.
|
|
15
34
|
|
|
16
35
|
The template intentionally requests no platform capabilities. It contributes a
|
|
17
36
|
structured toolbar route and a React page body. Import React from
|
|
@@ -32,3 +51,25 @@ CordisX plugin interfaces, it may be commercial and use a license you choose.
|
|
|
32
51
|
Replace `UNLICENSED` in `package.json` with that license before distribution.
|
|
33
52
|
The Exception does not cover copying or modifying CordisX host/runtime/CLI code
|
|
34
53
|
or using private interfaces.
|
|
54
|
+
|
|
55
|
+
Plugin-owned styles use `import './details.css'` in the owning component (or
|
|
56
|
+
lazy page module). Keep `src/vite-env.d.ts`: Vite types distinguish side-effect
|
|
57
|
+
CSS, CSS Module class maps, and explicit `?inline` text. Ordinary CSS imports
|
|
58
|
+
do not return strings. Component unmount is not CSS-module disposal; verify
|
|
59
|
+
plugin replacement separately from CSS HMR. See the maintained
|
|
60
|
+
[CSS guidance](https://github.com/cordisx/cordisx/blob/main/skills/cordisx-plugin-development/references/css-and-lifecycle.md)
|
|
61
|
+
for lifecycle boundaries and installed-Host CSS Modules limitations.
|
|
62
|
+
|
|
63
|
+
For operation feedback, use `ctx.notifications.show()` and declare
|
|
64
|
+
`notifications` in plugin injection. Host supplies plugin identity, cards and
|
|
65
|
+
user suppression rules. Do not create a Toast provider, manually positioned
|
|
66
|
+
alert or page-wide operation error; keep field validation and durable business
|
|
67
|
+
state next to the relevant control. See the
|
|
68
|
+
[notification guide](https://github.com/cordisx/cordisx/blob/efbff656d84b482d51598bc5ba303d24134e0c62/.agents/docs/notifications.md).
|
|
69
|
+
Verify SDK/Host availability first: an older version without this service is a
|
|
70
|
+
capability gap, not a reason to build a private DOM fallback.
|
|
71
|
+
|
|
72
|
+
For modals, use Host `Dialog`/`DialogProvider` from `cordisx/ui` or `ctx.dialogs`.
|
|
73
|
+
Keep full JSX in the body and use structured header/footer actions. The Host
|
|
74
|
+
owns the rightmost close button, focus, theme and lifecycle. See the
|
|
75
|
+
[dialog guide](https://github.com/cordisx/cordisx/blob/main/.agents/docs/dialogs.md).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# {{packageName}}
|
|
2
|
+
|
|
3
|
+
一个最小的 CordisX 本地可信插件。插件清单由
|
|
4
|
+
`src/{{pluginId}}.tsx` 导出;该文件同时也是 CordisX 本地开发使用的入口。
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
{{packageManager}} install
|
|
8
|
+
{{packageManager}} run check
|
|
9
|
+
{{packageManager}} run dev:dry-run
|
|
10
|
+
{{packageManager}} run dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`{{packageManager}} run dev:dry-run` 会在不启动 Codex Desktop 的情况下打包插件;
|
|
14
|
+
`{{packageManager}} run dev` 会启动独立的 CordisX 开发宿主。
|
|
15
|
+
|
|
16
|
+
`{{packageManager}} run build` 会在 `dist/runtime/` 中生成生产用 Vite ESM graph。固定入口
|
|
17
|
+
`module.js` 可以在 `import()` 实际触发后再加载带内容摘要的 JavaScript chunk、
|
|
18
|
+
CSS 和静态资源;正式的 `artifact.json` 会记录完整产物图,类型声明则隔离在
|
|
19
|
+
`dist/types/`,
|
|
20
|
+
`package.json#files` 会将整个 `dist/` 纳入打包。生产 generation 仍是不可变包
|
|
21
|
+
产物,不使用开发 HMR 连接。
|
|
22
|
+
|
|
23
|
+
生成的配置会调用公开的 `cordisx/vite` `cordisXPluginViteConfig()` helper。
|
|
24
|
+
这个作者侧与 Host 共用的流水线负责产物规则,只虚拟化封闭的 Host 单例导入,并
|
|
25
|
+
生成正式的 `dist/runtime/artifact.json`。可移植 CordisX package manifest 应把浏览器入口
|
|
26
|
+
指向相邻的预构建 `dist/runtime/module.js`;Host 会验证并保留该完整索引 graph。
|
|
27
|
+
|
|
28
|
+
这个生成项目属于 [CordisX 独立插件例外](https://github.com/cordisx/cordisx/blob/main/CORDISX-INDEPENDENT-PLUGIN-EXCEPTION.md)
|
|
29
|
+
定义的“已标记模板材料”。只要它保持为独立插件,并且只使用有文档、带版本的
|
|
30
|
+
CordisX 公共插件接口,就可以用于商业用途并采用你选择的许可证。发布前请将
|
|
31
|
+
`package.json` 中的 `UNLICENSED` 替换为所选许可证。该例外不包括复制或修改
|
|
32
|
+
CordisX Host、Runtime、CLI 代码,也不包括使用私有接口。
|
|
33
|
+
|
|
34
|
+
插件自有样式在所属组件或 lazy 页面模块中使用 `import './details.css'`。
|
|
35
|
+
保留 `src/vite-env.d.ts`:Vite 类型区分普通 CSS 副作用导入、CSS Modules
|
|
36
|
+
类名映射和显式 `?inline` 文本,普通 CSS 不返回字符串。组件卸载不等于
|
|
37
|
+
CSS 模块回收;CSS HMR 与插件替换需要分别验证。具体生命周期和已安装
|
|
38
|
+
Host 的 CSS Modules 限制见
|
|
39
|
+
[CSS 指导](https://github.com/cordisx/cordisx/blob/main/skills/cordisx-plugin-development/references/css-and-lifecycle.md)。
|
package/template/package.json
CHANGED
|
@@ -5,11 +5,23 @@
|
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"description": "A CordisX plugin",
|
|
7
7
|
"type": "module",
|
|
8
|
-
"
|
|
9
|
-
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md",
|
|
11
|
+
"README.zh-Hans.md"
|
|
12
|
+
],
|
|
13
|
+
"main": "./dist/runtime/module.js",
|
|
14
|
+
"types": "./dist/types/{{pluginId}}.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/types/{{pluginId}}.d.ts",
|
|
18
|
+
"import": "./dist/runtime/module.js",
|
|
19
|
+
"default": "./dist/runtime/module.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
10
22
|
"scripts": {
|
|
11
|
-
"build": "tsc -p tsconfig.json",
|
|
12
|
-
"check": "
|
|
23
|
+
"build": "vite build && tsc -p tsconfig.json --emitDeclarationOnly",
|
|
24
|
+
"check": "tsc -p tsconfig.json --noEmit && vite build && tsc -p tsconfig.json --emitDeclarationOnly && node --test test/manifest.mjs",
|
|
13
25
|
"dev": "cordisx dev ./src/{{pluginId}}.tsx",
|
|
14
26
|
"dev:dry-run": "cordisx dev ./src/{{pluginId}}.tsx --dry-run",
|
|
15
27
|
"test": "node --test test/manifest.mjs",
|
|
@@ -18,7 +30,8 @@
|
|
|
18
30
|
"devDependencies": {
|
|
19
31
|
"@deepseek-ai/cordis": "4.0.1",
|
|
20
32
|
"cordisx": "{{cordisxVersion}}",
|
|
21
|
-
"typescript": "^5.9.2"
|
|
33
|
+
"typescript": "^5.9.2",
|
|
34
|
+
"vite": "8.2.2"
|
|
22
35
|
},
|
|
23
36
|
"engines": {
|
|
24
37
|
"node": ">=22.19"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useState } from 'cordisx/react'
|
|
2
|
+
import { Button, Card, Heading, Stack, Text } from 'cordisx/ui'
|
|
3
|
+
import type { CordisXReactPageProps } from 'cordisx/contracts'
|
|
4
|
+
|
|
5
|
+
export type Messages = {
|
|
6
|
+
'command.open': undefined
|
|
7
|
+
'page.title': undefined
|
|
8
|
+
'page.description': undefined
|
|
9
|
+
'route.title': undefined
|
|
10
|
+
'route.description': undefined
|
|
11
|
+
'counter.label': { count: number }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A named component-only module forms a stable React Fast Refresh boundary. */
|
|
15
|
+
export function OverviewPage({ t }: CordisXReactPageProps<Messages>) {
|
|
16
|
+
const [count, setCount] = useState(0)
|
|
17
|
+
return (
|
|
18
|
+
<Stack gap="large">
|
|
19
|
+
<Heading>{t('page.title')}</Heading>
|
|
20
|
+
<Card>
|
|
21
|
+
<Stack gap="medium" align="flex-start">
|
|
22
|
+
<Text tone="muted">{t('page.description')}</Text>
|
|
23
|
+
<Button variant="primary" onClick={() => setCount(value => value + 1)}>
|
|
24
|
+
{t('counter.label', { count })}
|
|
25
|
+
</Button>
|
|
26
|
+
</Stack>
|
|
27
|
+
</Card>
|
|
28
|
+
</Stack>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis'
|
|
2
|
-
import { defineReactPage,
|
|
3
|
-
import { Button, Card, Heading, Stack, Text } from 'cordisx/ui'
|
|
2
|
+
import { defineReactPage, lazy, Suspense } from 'cordisx/react'
|
|
4
3
|
import {
|
|
5
4
|
CORDISX_PAGE_SCHEMA_V3,
|
|
6
5
|
CORDISX_PLUGIN_MANIFEST_SCHEMA_V1,
|
|
7
6
|
CORDISX_ROUTE_SCHEMA_V2,
|
|
8
7
|
type CordisXPluginManifestV1,
|
|
8
|
+
type CordisXReactPageProps,
|
|
9
9
|
} from 'cordisx/contracts'
|
|
10
|
-
import type {} from '
|
|
11
|
-
|
|
12
|
-
type Messages = {
|
|
13
|
-
'command.open': undefined
|
|
14
|
-
'page.title': undefined
|
|
15
|
-
'page.description': undefined
|
|
16
|
-
'route.title': undefined
|
|
17
|
-
'route.description': undefined
|
|
18
|
-
'counter.label': { count: number }
|
|
19
|
-
}
|
|
10
|
+
import type { Messages } from './overview-page.js'
|
|
20
11
|
|
|
21
12
|
export const manifest = {
|
|
22
13
|
$schema: CORDISX_PLUGIN_MANIFEST_SCHEMA_V1,
|
|
@@ -48,22 +39,20 @@ const route = {
|
|
|
48
39
|
description: { key: 'route.description', fallback: 'Open the plugin React example.' },
|
|
49
40
|
} as const
|
|
50
41
|
|
|
51
|
-
const
|
|
52
|
-
const
|
|
42
|
+
const OverviewPage = lazy(async () => {
|
|
43
|
+
const module = await import('./overview-page.js')
|
|
44
|
+
return { default: module.OverviewPage }
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
function OverviewPageBoundary(props: CordisXReactPageProps<Messages>) {
|
|
53
48
|
return (
|
|
54
|
-
<
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
<Stack gap="medium" align="flex-start">
|
|
58
|
-
<Text tone="muted">{t('page.description')}</Text>
|
|
59
|
-
<Button variant="primary" onClick={() => setCount(value => value + 1)}>
|
|
60
|
-
{t('counter.label', { count })}
|
|
61
|
-
</Button>
|
|
62
|
-
</Stack>
|
|
63
|
-
</Card>
|
|
64
|
-
</Stack>
|
|
49
|
+
<Suspense fallback={null}>
|
|
50
|
+
<OverviewPage {...props} />
|
|
51
|
+
</Suspense>
|
|
65
52
|
)
|
|
66
|
-
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const mountOverview = defineReactPage<Messages>(OverviewPageBoundary)
|
|
67
56
|
|
|
68
57
|
export function apply(ctx: Context): void {
|
|
69
58
|
ctx.i18n.define<Messages>({
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import test from 'node:test'
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
const component = () => null
|
|
5
|
+
const noop = () => undefined
|
|
6
|
+
const React = new Proxy({ Fragment: Symbol('Fragment'), Suspense: component, lazy: () => component }, {
|
|
7
|
+
get(target, property) {
|
|
8
|
+
return Reflect.get(target, property) ?? noop
|
|
9
|
+
},
|
|
10
|
+
})
|
|
11
|
+
globalThis.__cordisxSharedReactRuntime = {
|
|
12
|
+
React,
|
|
13
|
+
defineReactPage: page => page,
|
|
14
|
+
jsxRuntime: { Fragment: React.Fragment, jsx: component, jsxs: component },
|
|
15
|
+
jsxDevRuntime: { Fragment: React.Fragment, jsxDEV: component },
|
|
16
|
+
ui: new Proxy({}, { get: () => component }),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { apply, inject, manifest } = await import('../dist/runtime/module.js')
|
|
4
20
|
|
|
5
21
|
test('exports a minimal CordisX plugin module', () => {
|
|
6
22
|
assert.equal(manifest.schemaVersion, 1)
|
package/template/tsconfig.json
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
2
|
+
import { cordisXPluginViteConfig } from 'cordisx/vite'
|
|
3
|
+
|
|
4
|
+
const projectRoot = fileURLToPath(new URL('.', import.meta.url))
|
|
5
|
+
|
|
6
|
+
const config: ReturnType<typeof cordisXPluginViteConfig> = cordisXPluginViteConfig({
|
|
7
|
+
root: projectRoot,
|
|
8
|
+
entry: fileURLToPath(new URL('./{{sourceEntry}}', import.meta.url)),
|
|
9
|
+
outDir: fileURLToPath(new URL('./{{outDir}}', import.meta.url)),
|
|
10
|
+
entryFileName: 'module.js',
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export default config
|