create-cordisx-plugin 0.1.0-beta.0 → 0.1.0-beta.3
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 +80 -5
- package/dist/cli.js +668 -36
- package/package.json +8 -5
- package/template/README.md +60 -11
- package/template/README.zh-Hans.md +39 -0
- package/template/package.json +20 -7
- package/template/src/overview-page.tsx +30 -0
- package/template/src/vite-env.d.ts +1 -0
- package/template/src/{{packageName}}.tsx +96 -0
- package/template/test/manifest.mjs +18 -2
- package/template/tsconfig.json +4 -2
- package/template/vite.config.ts +13 -0
- package/template/src/{{packageName}}.ts +0 -48
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)}`);
|