@vobs/cli 1.7.4 → 1.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +551 -5
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +551 -5
- package/dist/cli.js.map +1 -1
- package/dist/commands/dsh.cjs +677 -0
- package/dist/commands/dsh.cjs.map +1 -0
- package/dist/commands/dsh.d.cts +43 -0
- package/dist/commands/dsh.d.ts +43 -0
- package/dist/commands/dsh.js +638 -0
- package/dist/commands/dsh.js.map +1 -0
- package/dist/commands/generate.cjs.map +1 -1
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/index.cjs +543 -5
- package/dist/commands/index.cjs.map +1 -1
- package/dist/commands/index.d.cts +2 -0
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.js +540 -5
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/init.cjs +5 -2
- package/dist/commands/init.cjs.map +1 -1
- package/dist/commands/init.js +5 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/index.cjs +553 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +551 -5
- package/dist/index.js.map +1 -1
- package/dist/repl.cjs +5 -2
- package/dist/repl.cjs.map +1 -1
- package/dist/repl.js +5 -2
- package/dist/repl.js.map +1 -1
- package/dist/start.cjs +553 -7
- package/dist/start.cjs.map +1 -1
- package/dist/start.js +551 -5
- package/dist/start.js.map +1 -1
- package/dist/templates/dsh-plugin/README.md.hbs +54 -0
- package/dist/templates/dsh-plugin/cordis.patch.yml.hbs +7 -0
- package/dist/templates/dsh-plugin/package.json.hbs +40 -0
- package/dist/templates/dsh-plugin/src/client/index.tsx.hbs +24 -0
- package/dist/templates/dsh-plugin/src/client/panel.tsx.hbs +45 -0
- package/dist/templates/dsh-plugin/src/client/styles.ts.hbs +65 -0
- package/dist/templates/dsh-plugin/src/host/index.ts.hbs +20 -0
- package/dist/templates/dsh-plugin/tsconfig.json.hbs +19 -0
- package/dist/templates/dsh-plugin/vite.config.ts.hbs +13 -0
- package/dist/utils/file.cjs +5 -2
- package/dist/utils/file.cjs.map +1 -1
- package/dist/utils/file.d.cts +2 -2
- package/dist/utils/file.d.ts +2 -2
- package/dist/utils/file.js +5 -2
- package/dist/utils/file.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +28 -0
- package/src/commands/dsh.test.ts +279 -0
- package/src/commands/dsh.ts +673 -0
- package/src/commands/index.ts +2 -1
- package/src/templates/dsh-plugin/README.md.hbs +54 -0
- package/src/templates/dsh-plugin/cordis.patch.yml.hbs +7 -0
- package/src/templates/dsh-plugin/package.json.hbs +40 -0
- package/src/templates/dsh-plugin/src/client/index.tsx.hbs +24 -0
- package/src/templates/dsh-plugin/src/client/panel.tsx.hbs +45 -0
- package/src/templates/dsh-plugin/src/client/styles.ts.hbs +65 -0
- package/src/templates/dsh-plugin/src/host/index.ts.hbs +20 -0
- package/src/templates/dsh-plugin/tsconfig.json.hbs +19 -0
- package/src/templates/dsh-plugin/vite.config.ts.hbs +13 -0
- package/src/utils/file.ts +7 -3
package/dist/utils/file.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ declare function writeFile(dir: string, filename: string, content: string): void
|
|
|
8
8
|
declare function ensureDir(dir: string): void;
|
|
9
9
|
/** Check if a path exists */
|
|
10
10
|
declare function pathExists(path: string): boolean;
|
|
11
|
-
/** Scaffold a project from
|
|
12
|
-
declare function scaffoldProject(targetDir: string, context: Record<string, unknown
|
|
11
|
+
/** Scaffold a project from a named template under `src/templates`. */
|
|
12
|
+
declare function scaffoldProject(targetDir: string, context: Record<string, unknown>, template?: string): Promise<void>;
|
|
13
13
|
|
|
14
14
|
export { ensureDir, getTemplatesDir, pathExists, renderTemplate, scaffoldProject, writeFile };
|
package/dist/utils/file.js
CHANGED
|
@@ -56,8 +56,11 @@ async function copyDirRecursive(srcDir, destDir, context) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
__name(copyDirRecursive, "copyDirRecursive");
|
|
59
|
-
async function scaffoldProject(targetDir, context) {
|
|
60
|
-
const templateDir = resolve(TEMPLATES_ROOT,
|
|
59
|
+
async function scaffoldProject(targetDir, context, template = "basic") {
|
|
60
|
+
const templateDir = resolve(TEMPLATES_ROOT, template);
|
|
61
|
+
if (!existsSync(templateDir)) {
|
|
62
|
+
throw new Error(`\u6A21\u677F\u4E0D\u5B58\u5728: ${template}\uFF08\u53EF\u7528\u6A21\u677F\u89C1 ${TEMPLATES_ROOT}\uFF09`);
|
|
63
|
+
}
|
|
61
64
|
ensureDir(targetDir);
|
|
62
65
|
await copyDirRecursive(templateDir, targetDir, context);
|
|
63
66
|
}
|
package/dist/utils/file.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/file.ts"],"sourcesContent":["import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs'\nimport { resolve, dirname } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport Handlebars from 'handlebars'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\nconst TEMPLATES_ROOT = resolve(__dirname, '..', 'templates')\n\n/** Get the absolute path to the templates directory */\nexport function getTemplatesDir(): string {\n return TEMPLATES_ROOT\n}\n\n/** Read a template file and compile it with Handlebars */\nexport function renderTemplate(\n templatePath: string,\n context: Record<string, unknown>\n): string {\n const source = readFileSync(templatePath, 'utf-8')\n const template = Handlebars.compile(source)\n return template(context)\n}\n\n/** Write a file, creating parent directories if needed */\nexport function writeFile(dir: string, filename: string, content: string): void {\n const filePath = resolve(dir, filename)\n mkdirSync(dirname(filePath), { recursive: true })\n writeFileSync(filePath, content, 'utf-8')\n}\n\n/** Ensure a directory exists */\nexport function ensureDir(dir: string): void {\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true })\n }\n}\n\n/** Check if a path exists */\nexport function pathExists(path: string): boolean {\n return existsSync(path)\n}\n\n/** Copy a template directory recursively, rendering each .hbs file */\nasync function copyDirRecursive(srcDir: string, destDir: string, context: Record<string, unknown>): Promise<void> {\n const { readdirSync } = await import('node:fs')\n const { join } = await import('node:path')\n\n const entries = readdirSync(srcDir, { withFileTypes: true })\n\n for (const entry of entries) {\n const srcPath = join(srcDir, entry.name)\n const isHbs = entry.name.endsWith('.hbs')\n const destName = isHbs ? entry.name.slice(0, -4) : entry.name\n const destPath = join(destDir, destName)\n\n if (entry.isDirectory()) {\n ensureDir(destPath)\n await copyDirRecursive(srcPath, destPath, context)\n } else if (isHbs) {\n const content = renderTemplate(srcPath, context)\n writeFile(destDir, destName, content)\n } else {\n const content = readFileSync(srcPath, 'utf-8')\n writeFile(destDir, destName, content)\n }\n }\n}\n\n/** Scaffold a project from
|
|
1
|
+
{"version":3,"sources":["../../src/utils/file.ts"],"sourcesContent":["import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs'\nimport { resolve, dirname } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport Handlebars from 'handlebars'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\nconst TEMPLATES_ROOT = resolve(__dirname, '..', 'templates')\n\n/** Get the absolute path to the templates directory */\nexport function getTemplatesDir(): string {\n return TEMPLATES_ROOT\n}\n\n/** Read a template file and compile it with Handlebars */\nexport function renderTemplate(\n templatePath: string,\n context: Record<string, unknown>\n): string {\n const source = readFileSync(templatePath, 'utf-8')\n const template = Handlebars.compile(source)\n return template(context)\n}\n\n/** Write a file, creating parent directories if needed */\nexport function writeFile(dir: string, filename: string, content: string): void {\n const filePath = resolve(dir, filename)\n mkdirSync(dirname(filePath), { recursive: true })\n writeFileSync(filePath, content, 'utf-8')\n}\n\n/** Ensure a directory exists */\nexport function ensureDir(dir: string): void {\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true })\n }\n}\n\n/** Check if a path exists */\nexport function pathExists(path: string): boolean {\n return existsSync(path)\n}\n\n/** Copy a template directory recursively, rendering each .hbs file */\nasync function copyDirRecursive(srcDir: string, destDir: string, context: Record<string, unknown>): Promise<void> {\n const { readdirSync } = await import('node:fs')\n const { join } = await import('node:path')\n\n const entries = readdirSync(srcDir, { withFileTypes: true })\n\n for (const entry of entries) {\n const srcPath = join(srcDir, entry.name)\n const isHbs = entry.name.endsWith('.hbs')\n const destName = isHbs ? entry.name.slice(0, -4) : entry.name\n const destPath = join(destDir, destName)\n\n if (entry.isDirectory()) {\n ensureDir(destPath)\n await copyDirRecursive(srcPath, destPath, context)\n } else if (isHbs) {\n const content = renderTemplate(srcPath, context)\n writeFile(destDir, destName, content)\n } else {\n const content = readFileSync(srcPath, 'utf-8')\n writeFile(destDir, destName, content)\n }\n }\n}\n\n/** Scaffold a project from a named template under `src/templates`. */\nexport async function scaffoldProject(\n targetDir: string,\n context: Record<string, unknown>,\n template = 'basic'\n): Promise<void> {\n const templateDir = resolve(TEMPLATES_ROOT, template)\n if (!existsSync(templateDir)) {\n throw new Error(`模板不存在: ${template}(可用模板见 ${TEMPLATES_ROOT})`)\n }\n ensureDir(targetDir)\n await copyDirRecursive(templateDir, targetDir, context)\n}"],"mappings":";;;;AAAA,SAAS,YAAY,WAAW,eAAe,oBAAoB;AACnE,SAAS,SAAS,eAAe;AACjC,SAAS,qBAAqB;AAC9B,OAAO,gBAAgB;AAEvB,IAAM,YAAY,QAAQ,cAAc,YAAY,GAAG,CAAC;AACxD,IAAM,iBAAiB,QAAQ,WAAW,MAAM,WAAW;AAGpD,SAAS,kBAA0B;AACxC,SAAO;AACT;AAFgB;AAKT,SAAS,eACd,cACA,SACQ;AACR,QAAM,SAAS,aAAa,cAAc,OAAO;AACjD,QAAM,WAAW,WAAW,QAAQ,MAAM;AAC1C,SAAO,SAAS,OAAO;AACzB;AAPgB;AAUT,SAAS,UAAU,KAAa,UAAkB,SAAuB;AAC9E,QAAM,WAAW,QAAQ,KAAK,QAAQ;AACtC,YAAU,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AAChD,gBAAc,UAAU,SAAS,OAAO;AAC1C;AAJgB;AAOT,SAAS,UAAU,KAAmB;AAC3C,MAAI,CAAC,WAAW,GAAG,GAAG;AACpB,cAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,EACpC;AACF;AAJgB;AAOT,SAAS,WAAW,MAAuB;AAChD,SAAO,WAAW,IAAI;AACxB;AAFgB;AAKhB,eAAe,iBAAiB,QAAgB,SAAiB,SAAiD;AAChH,QAAM,EAAE,YAAY,IAAI,MAAM,OAAO,IAAS;AAC9C,QAAM,EAAE,KAAK,IAAI,MAAM,OAAO,MAAW;AAEzC,QAAM,UAAU,YAAY,QAAQ,EAAE,eAAe,KAAK,CAAC;AAE3D,aAAW,SAAS,SAAS;AAC3B,UAAM,UAAU,KAAK,QAAQ,MAAM,IAAI;AACvC,UAAM,QAAQ,MAAM,KAAK,SAAS,MAAM;AACxC,UAAM,WAAW,QAAQ,MAAM,KAAK,MAAM,GAAG,EAAE,IAAI,MAAM;AACzD,UAAM,WAAW,KAAK,SAAS,QAAQ;AAEvC,QAAI,MAAM,YAAY,GAAG;AACvB,gBAAU,QAAQ;AAClB,YAAM,iBAAiB,SAAS,UAAU,OAAO;AAAA,IACnD,WAAW,OAAO;AAChB,YAAM,UAAU,eAAe,SAAS,OAAO;AAC/C,gBAAU,SAAS,UAAU,OAAO;AAAA,IACtC,OAAO;AACL,YAAM,UAAU,aAAa,SAAS,OAAO;AAC7C,gBAAU,SAAS,UAAU,OAAO;AAAA,IACtC;AAAA,EACF;AACF;AAvBe;AA0Bf,eAAsB,gBACpB,WACA,SACA,WAAW,SACI;AACf,QAAM,cAAc,QAAQ,gBAAgB,QAAQ;AACpD,MAAI,CAAC,WAAW,WAAW,GAAG;AAC5B,UAAM,IAAI,MAAM,mCAAU,QAAQ,wCAAU,cAAc,QAAG;AAAA,EAC/D;AACA,YAAU,SAAS;AACnB,QAAM,iBAAiB,aAAa,WAAW,OAAO;AACxD;AAXsB;","names":[]}
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { devCommand } from './commands/dev.js'
|
|
|
4
4
|
import { buildCommand } from './commands/build.js'
|
|
5
5
|
import { generateCommand } from './commands/generate.js'
|
|
6
6
|
import { addCommand } from './commands/add.js'
|
|
7
|
+
import { dshCommand } from './commands/dsh.js'
|
|
7
8
|
import { showBanner, showLogo, showDevBanner } from './banner.js'
|
|
8
9
|
|
|
9
10
|
export function createCLI(): ReturnType<typeof cac> {
|
|
@@ -91,6 +92,33 @@ export function createCLI(): ReturnType<typeof cac> {
|
|
|
91
92
|
})
|
|
92
93
|
})
|
|
93
94
|
|
|
95
|
+
cli
|
|
96
|
+
.command('dsh <action> [target]', 'DSH 插件工具链(init / dev / build / check / install)')
|
|
97
|
+
.option('--dir <dir>', 'init:目标目录')
|
|
98
|
+
.option('--pm <pm>', 'init:包管理器(pnpm/npm)')
|
|
99
|
+
.option('--port <port>', 'dev:预览端口', { default: 5199 })
|
|
100
|
+
.option('--profile <name>', 'install:DSH profile 名', { default: 'desktop' })
|
|
101
|
+
.option('--spec <spec>', 'install:完整安装 spec')
|
|
102
|
+
.option('--repo <owner/name>', 'install:GitHub owner/name')
|
|
103
|
+
.option('--tag <ref>', 'install:Git tag / 分支 / commit')
|
|
104
|
+
.option('--subpath <path>', 'install:仓库内子目录')
|
|
105
|
+
.option('--from <path>', 'install:从本地目录安装')
|
|
106
|
+
.option('--dry', 'install:只打印命令,不执行')
|
|
107
|
+
.action(async (action: string, target: string | undefined, options: Record<string, unknown>) => {
|
|
108
|
+
await dshCommand(action, target, {
|
|
109
|
+
dir: options.dir as string | undefined,
|
|
110
|
+
pm: options.pm as 'pnpm' | 'npm' | undefined,
|
|
111
|
+
profile: options.profile as string | undefined,
|
|
112
|
+
spec: options.spec as string | undefined,
|
|
113
|
+
repo: options.repo as string | undefined,
|
|
114
|
+
tag: options.tag as string | undefined,
|
|
115
|
+
subpath: options.subpath as string | undefined,
|
|
116
|
+
from: options.from as string | undefined,
|
|
117
|
+
dry: options.dry === true,
|
|
118
|
+
port: options.port === undefined ? undefined : Number(options.port)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
94
122
|
cli
|
|
95
123
|
.command('logo', 'Show the vobs CLI logo and version info')
|
|
96
124
|
.action(() => {
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
+
import { buildInstallSpec, buildPreviewHtml, buildShellCommand, collectIssues, resolveExport } from './dsh.js'
|
|
6
|
+
|
|
7
|
+
/* ---------------------------------------------------------------- 夹具 */
|
|
8
|
+
|
|
9
|
+
const tempDirs: string[] = []
|
|
10
|
+
|
|
11
|
+
function makeProject(files: Record<string, string>): string {
|
|
12
|
+
const dir = mkdtempSync(join(tmpdir(), 'vobs-dsh-check-'))
|
|
13
|
+
tempDirs.push(dir)
|
|
14
|
+
for (const [relative, content] of Object.entries(files)) {
|
|
15
|
+
const target = join(dir, relative)
|
|
16
|
+
mkdirSync(join(target, '..'), { recursive: true })
|
|
17
|
+
writeFileSync(target, content, 'utf8')
|
|
18
|
+
}
|
|
19
|
+
return dir
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
while (tempDirs.length > 0) rmSync(tempDirs.pop() as string, { recursive: true, force: true })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
/** 一个各处都合法的插件项目。 */
|
|
27
|
+
function healthyProject(overrides: Record<string, string> = {}): Record<string, string> {
|
|
28
|
+
return {
|
|
29
|
+
'package.json': JSON.stringify(
|
|
30
|
+
{
|
|
31
|
+
name: 'dsh-plugin-demo',
|
|
32
|
+
version: '0.1.0',
|
|
33
|
+
type: 'module',
|
|
34
|
+
main: './lib/index.js',
|
|
35
|
+
exports: {
|
|
36
|
+
'.': './lib/index.js',
|
|
37
|
+
'./client': './lib/client.js',
|
|
38
|
+
'./cordis.patch.yml': './cordis.patch.yml',
|
|
39
|
+
'./package.json': './package.json'
|
|
40
|
+
},
|
|
41
|
+
dsh: {
|
|
42
|
+
manifestVersion: 1,
|
|
43
|
+
bundle: { patch: './cordis.patch.yml' },
|
|
44
|
+
client: { platform: 'web' }
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
null,
|
|
48
|
+
2
|
|
49
|
+
),
|
|
50
|
+
'cordis.patch.yml': "- insert:\n - id: dsh-plugin-demo\n name: dsh-plugin-demo\n",
|
|
51
|
+
'lib/index.js': 'export const name = "dsh-plugin-demo"\nexport function apply() {}\n',
|
|
52
|
+
'lib/client.js':
|
|
53
|
+
'// generated\nwindow.__ModuleLoader__.load({id:"dsh-plugin-demo",factory:function(require){\n' +
|
|
54
|
+
'"use strict";var module={exports:{}};var exports=module.exports;\n' +
|
|
55
|
+
'globalThis["__VOBS_DSH_REACT__"]=require("react");\nmodule.exports={};\n}});\n',
|
|
56
|
+
...overrides
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* ------------------------------------------------------------ collectIssues */
|
|
61
|
+
|
|
62
|
+
describe('collectIssues', () => {
|
|
63
|
+
it('健康项目零错误', () => {
|
|
64
|
+
const result = collectIssues(makeProject(healthyProject()))
|
|
65
|
+
expect(result.errors).toEqual([])
|
|
66
|
+
expect(result.passed.length).toBeGreaterThan(4)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('缺 dsh.bundle.patch 直接判错', () => {
|
|
70
|
+
const dir = makeProject(
|
|
71
|
+
healthyProject({
|
|
72
|
+
'package.json': JSON.stringify({ name: 'p', version: '1.0.0', exports: {} })
|
|
73
|
+
})
|
|
74
|
+
)
|
|
75
|
+
const result = collectIssues(dir)
|
|
76
|
+
expect(result.errors.join('\n')).toContain('dsh.bundle.patch')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('patch 文件不存在判错', () => {
|
|
80
|
+
const manifest = JSON.parse(healthyProject()['package.json'] as string) as Record<string, unknown>
|
|
81
|
+
;(manifest.dsh as Record<string, unknown>).bundle = { patch: './missing.yml' }
|
|
82
|
+
const result = collectIssues(makeProject(healthyProject({ 'package.json': JSON.stringify(manifest) })))
|
|
83
|
+
expect(result.errors.join('\n')).toContain('missing.yml')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('patch 里没有 insert 判错', () => {
|
|
87
|
+
const result = collectIssues(makeProject(healthyProject({ 'cordis.patch.yml': '- id: other\n' })))
|
|
88
|
+
expect(result.errors.join('\n')).toContain('insert')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('platform 不是 web 判错', () => {
|
|
92
|
+
const manifest = JSON.parse(healthyProject()['package.json'] as string) as Record<string, unknown>
|
|
93
|
+
;(manifest.dsh as Record<string, unknown>).client = { platform: 'node' }
|
|
94
|
+
const result = collectIssues(makeProject(healthyProject({ 'package.json': JSON.stringify(manifest) })))
|
|
95
|
+
expect(result.errors.join('\n')).toContain('web')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('声明了 prepare 判错(DSH 安装时不构建)', () => {
|
|
99
|
+
const manifest = JSON.parse(healthyProject()['package.json'] as string) as Record<string, unknown>
|
|
100
|
+
manifest.scripts = { prepare: 'vite build' }
|
|
101
|
+
const result = collectIssues(makeProject(healthyProject({ 'package.json': JSON.stringify(manifest) })))
|
|
102
|
+
expect(result.errors.join('\n')).toContain('prepare')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('依赖里出现 workspace: 判错(Git 子目录直装解析不了)', () => {
|
|
106
|
+
const manifest = JSON.parse(healthyProject()['package.json'] as string) as Record<string, unknown>
|
|
107
|
+
manifest.devDependencies = { '@vobs/vobs': 'workspace:*' }
|
|
108
|
+
const result = collectIssues(makeProject(healthyProject({ 'package.json': JSON.stringify(manifest) })))
|
|
109
|
+
expect(result.errors.join('\n')).toContain('workspace:')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('客户端产物缺失判错', () => {
|
|
113
|
+
const files = healthyProject()
|
|
114
|
+
delete files['lib/client.js']
|
|
115
|
+
const result = collectIssues(makeProject(files))
|
|
116
|
+
expect(result.errors.join('\n')).toContain('客户端产物')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('产物里出现动态 import 判错', () => {
|
|
120
|
+
const code = `${healthyProject()['lib/client.js'] as string}\nconst load = () => import("./chunk.js")\n`
|
|
121
|
+
const result = collectIssues(makeProject(healthyProject({ 'lib/client.js': code })))
|
|
122
|
+
expect(result.errors.join('\n')).toContain('动态 import')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('产物 require 了非平台模块判错', () => {
|
|
126
|
+
const code = (healthyProject()['lib/client.js'] as string).replace('require("react")', 'require("lodash")')
|
|
127
|
+
const result = collectIssues(makeProject(healthyProject({ 'lib/client.js': code })))
|
|
128
|
+
expect(result.errors.join('\n')).toContain('lodash')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('__ModuleLoader__.load 出现多次判错', () => {
|
|
132
|
+
const code = `${healthyProject()['lib/client.js'] as string}\nwindow.__ModuleLoader__.load({id:"x",factory(){}});\n`
|
|
133
|
+
const result = collectIssues(makeProject(healthyProject({ 'lib/client.js': code })))
|
|
134
|
+
expect(result.errors.join('\n')).toContain('2 次')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('模块 id 与包名不一致只给警告', () => {
|
|
138
|
+
const code = (healthyProject()['lib/client.js'] as string).replace(/"dsh-plugin-demo"/u, '"wrong-id"')
|
|
139
|
+
const result = collectIssues(makeProject(healthyProject({ 'lib/client.js': code })))
|
|
140
|
+
expect(result.errors).toEqual([])
|
|
141
|
+
expect(result.warnings.join('\n')).toContain('模块 id')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('package.json 语法错误时给出明确错误', () => {
|
|
145
|
+
const result = collectIssues(makeProject({ 'package.json': '{ not json' }))
|
|
146
|
+
expect(result.errors[0]).toContain('package.json')
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
/* --------------------------------------------------------- buildInstallSpec */
|
|
151
|
+
|
|
152
|
+
describe('buildInstallSpec', () => {
|
|
153
|
+
it('显式 spec 原样透传', () => {
|
|
154
|
+
expect(buildInstallSpec({ spec: 'github:a/b#v1&path:/x' }).spec).toBe('github:a/b#v1&path:/x')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('repo 无 tag 无子目录', () => {
|
|
158
|
+
expect(buildInstallSpec({ repo: 'vobsjs/vobs' }).spec).toBe('github:vobsjs/vobs')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('repo + tag', () => {
|
|
162
|
+
expect(buildInstallSpec({ repo: 'vobsjs/vobs', tag: 'v1.7.6' }).spec).toBe('github:vobsjs/vobs#v1.7.6')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('repo + tag + 子目录用 & 连接', () => {
|
|
166
|
+
expect(buildInstallSpec({ repo: 'vobsjs/vobs', tag: 'v1.7.6', subpath: '/packages/dsh-console' }).spec).toBe(
|
|
167
|
+
'github:vobsjs/vobs#v1.7.6&path:/packages/dsh-console'
|
|
168
|
+
)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('repo + 只有子目录时用 # 起头', () => {
|
|
172
|
+
expect(buildInstallSpec({ repo: 'vobsjs/vobs', subpath: '/packages/dsh-plugin' }).spec).toBe(
|
|
173
|
+
'github:vobsjs/vobs#path:/packages/dsh-plugin'
|
|
174
|
+
)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('--from 解析成绝对路径', () => {
|
|
178
|
+
const result = buildInstallSpec({ from: '.' })
|
|
179
|
+
expect(result.error).toBeUndefined()
|
|
180
|
+
expect(result.spec?.includes(':')).toBe(true)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('--from 指向不存在的路径时报错', () => {
|
|
184
|
+
const result = buildInstallSpec({ from: './definitely-not-here-xyz' })
|
|
185
|
+
expect(result.error).toContain('不存在')
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('什么都不给时报错并说明可选参数', () => {
|
|
189
|
+
const result = buildInstallSpec({})
|
|
190
|
+
expect(result.error).toContain('--spec')
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
/* ---------------------------------------------------------- buildShellCommand */
|
|
195
|
+
|
|
196
|
+
describe('buildShellCommand', () => {
|
|
197
|
+
it('给含 & 的 spec 加引号,否则 cmd 会把它当命令分隔符', () => {
|
|
198
|
+
const command = buildShellCommand('dsh', [
|
|
199
|
+
'plugin',
|
|
200
|
+
'--profile',
|
|
201
|
+
'desktop',
|
|
202
|
+
'add',
|
|
203
|
+
'github:vobsjs/vobs#v1.7.6&path:/packages/dsh-console'
|
|
204
|
+
])
|
|
205
|
+
expect(command).toBe(
|
|
206
|
+
'dsh plugin --profile desktop add "github:vobsjs/vobs#v1.7.6&path:/packages/dsh-console"'
|
|
207
|
+
)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('普通参数不加多余引号', () => {
|
|
211
|
+
expect(buildShellCommand('dsh', ['plugin', '--profile', 'desktop'])).toBe('dsh plugin --profile desktop')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('含空格的参数加引号', () => {
|
|
215
|
+
expect(buildShellCommand('dsh', ['add', 'C:/my plugins/demo'])).toBe('dsh add "C:/my plugins/demo"')
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('参数内的双引号按 cmd 规则翻倍', () => {
|
|
219
|
+
expect(buildShellCommand('dsh', ['add', 'a"b'])).toBe('dsh add "a""b"')
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
/* ------------------------------------------------------------ dev 预览页 */
|
|
224
|
+
|
|
225
|
+
describe('buildPreviewHtml', () => {
|
|
226
|
+
const html = buildPreviewHtml({ packageName: 'dsh-plugin-demo' })
|
|
227
|
+
|
|
228
|
+
it('在插件 bundle 之前装 __ModuleLoader__ 垫片', () => {
|
|
229
|
+
const shimAt = html.indexOf('window.__ModuleLoader__')
|
|
230
|
+
const bundleAt = html.indexOf('<script src="/client.js">')
|
|
231
|
+
expect(shimAt).toBeGreaterThan(-1)
|
|
232
|
+
expect(bundleAt).toBeGreaterThan(shimAt)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('垫片把 factory 收进注册表而不是执行它', () => {
|
|
236
|
+
expect(html).toContain('factories.set(entry.id, entry.factory)')
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('用 startPreview 启动并传入包名', () => {
|
|
240
|
+
expect(html).toContain("from '/preview.js'")
|
|
241
|
+
expect(html).toContain('packageName: "dsh-plugin-demo"')
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('订阅 SSE 以便重建后自动刷新', () => {
|
|
245
|
+
expect(html).toContain("new EventSource('/events')")
|
|
246
|
+
expect(html).toContain('location.reload()')
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('包名被转义,含 </script> 也不会突破 script 标签', () => {
|
|
250
|
+
const injected = buildPreviewHtml({ packageName: '</script><script>alert(1)</script>' })
|
|
251
|
+
expect(injected).toContain('packageName: "\\u003c/script>')
|
|
252
|
+
expect(injected).not.toContain('</script><script>alert(1)')
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('标题里的 HTML 元字符被剔除', () => {
|
|
256
|
+
const html = buildPreviewHtml({ packageName: 'p', title: '<img src=x onerror=alert(1)>' })
|
|
257
|
+
expect(html).toContain('<title>vobs preview · img src=x onerror=alert(1)</title>')
|
|
258
|
+
expect(html).not.toContain('<img')
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
describe('resolveExport', () => {
|
|
263
|
+
it('字符串写法直接返回', () => {
|
|
264
|
+
expect(resolveExport({ exports: { './client': './lib/client.js' } }, './client')).toBe('./lib/client.js')
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('条件对象按 default / import / require 顺序取', () => {
|
|
268
|
+
expect(resolveExport({ exports: { './client': { import: './lib/client.mjs' } } }, './client')).toBe('./lib/client.mjs')
|
|
269
|
+
expect(resolveExport({ exports: { './client': { require: './lib/client.cjs' } } }, './client')).toBe('./lib/client.cjs')
|
|
270
|
+
expect(
|
|
271
|
+
resolveExport({ exports: { './client': { types: './x.d.ts', default: './lib/client.js' } } }, './client')
|
|
272
|
+
).toBe('./lib/client.js')
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('缺失时返回 undefined', () => {
|
|
276
|
+
expect(resolveExport({ exports: {} }, './client')).toBeUndefined()
|
|
277
|
+
expect(resolveExport({}, './client')).toBeUndefined()
|
|
278
|
+
})
|
|
279
|
+
})
|