dsh-enterprise-market 0.1.0 → 0.2.0

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/lib/client.js CHANGED
@@ -31,8 +31,8 @@ window.__ModuleLoader__.load({
31
31
  updated: '更新',
32
32
  local: '本机发布',
33
33
  uploadHeading: '发布自建插件',
34
- uploadHint: '选择打包好的 .tgz,发布到企业 npm 仓库。上传人姓名取自当前企业身份并写入包元数据,同事在市场里就能看到。',
35
- chooseFile: '选择 .tgz 文件',
34
+ uploadHint: '选择 .zip 或 .tgz,发布到企业 npm 仓库。zip 由宿主自动解包打包,上传人姓名取自当前企业身份并写入包元数据,同事在市场里就能看到。',
35
+ chooseFile: '选择 .zip 或 .tgz 文件',
36
36
  noFile: '尚未选择文件',
37
37
  namePlaceholder: '包名(可留空,用包内 package.json)',
38
38
  descPlaceholder: '一句话用途(可留空)',
@@ -41,8 +41,8 @@ window.__ModuleLoader__.load({
41
41
  published: '已发布',
42
42
  publishFailed: '发布失败',
43
43
  needIdentity: '未认证企业身份,不能发布',
44
- needFile: '请先选择 .tgz 文件',
45
- packHint: '在插件目录执行 npm pack,再上传生成的 .tgz。',
44
+ needFile: '请先选择 .zip 或 .tgz 文件',
45
+ packHint: '直接上传插件目录压缩成的 .zip,或已打包的 .tgz;插件名必须以 traness- 开头。',
46
46
  npmSignedIn: 'npm 已登录',
47
47
  npmSignedOut: 'npm 未登录',
48
48
  npmLoginHint: '本机 npm 未登录,发布会被仓库拒绝。先在终端执行 npm login,再回来上传。',
@@ -56,7 +56,9 @@ window.__ModuleLoader__.load({
56
56
  'origin-refused': '请求来源不受信任,已拒绝。',
57
57
  'registry-unreachable': '企业仓库暂时不可达。',
58
58
  'publish-failed': 'npm 拒绝发布,请检查包名是否已被占用、是否已登录。',
59
- 'invalid-tarball': '这个 .tgz 读不出 package/package.json。',
59
+ 'invalid-tarball': '这个归档读不出 package.json,请确认压缩包里是插件目录本身。',
60
+ 'invalid-archive': '压缩包里找不到 package.json,请把插件目录(含 package.json)打成一个 zip。',
61
+ 'name-prefix-required': '插件名必须以 traness- 开头,请修改 package.json 的 name 后重新上传。',
60
62
  'invalid-manifest': '包内 package.json 缺少 name 或 version。',
61
63
  'identity-unverified': '未认证企业身份,不能发布。',
62
64
  'credential-missing': '没有可用的企业令牌。',
@@ -94,8 +96,8 @@ window.__ModuleLoader__.load({
94
96
  updated: 'Updated',
95
97
  local: 'Published locally',
96
98
  uploadHeading: 'Publish a self-built plugin',
97
- uploadHint: 'Pick the packed .tgz and publish it to the enterprise npm registry. Your verified name is written into the package metadata so colleagues see the uploader.',
98
- chooseFile: 'Choose a .tgz file',
99
+ uploadHint: 'Pick a .zip or .tgz and publish it to the enterprise npm registry. A zip is unpacked and packed by the host; your verified name is written into the package metadata so colleagues see the uploader.',
100
+ chooseFile: 'Choose a .zip or .tgz file',
99
101
  noFile: 'No file selected',
100
102
  namePlaceholder: 'Package name (optional; defaults to the package.json inside)',
101
103
  descPlaceholder: 'One-line purpose (optional)',
@@ -552,7 +554,7 @@ window.__ModuleLoader__.load({
552
554
  h('input', {
553
555
  ref: inputRef,
554
556
  type: 'file',
555
- accept: '.tgz,.tar.gz,application/gzip',
557
+ accept: '.zip,.tgz,.tar.gz,application/zip,application/gzip',
556
558
  className: 'emk-hidden',
557
559
  onChange: (event) => {
558
560
  setFile(event.target.files?.[0] ?? null)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-enterprise-market",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "TranESS 企业插件市场:员工把自建插件发布到企业 npm 仓库,市场按企业身份显示上传人姓名,同事可直接下载安装。",
package/src/host/npm.js CHANGED
@@ -1,7 +1,16 @@
1
- import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
1
+ import { access, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'
2
2
  import { tmpdir } from 'node:os'
3
3
  import { join } from 'node:path'
4
4
  import { createHash } from 'node:crypto'
5
+
6
+ const exists = async (path) => {
7
+ try {
8
+ await access(path)
9
+ return true
10
+ } catch {
11
+ return false
12
+ }
13
+ }
5
14
  import { findCli, run } from './exec.js'
6
15
 
7
16
  /** The metadata field the market reads the uploader from. */
@@ -51,6 +60,18 @@ export function isTarballName(fileName) {
51
60
  return /\.(tgz|tar\.gz)$/i.test(fileName)
52
61
  }
53
62
 
63
+ /** Whether the spec names an archive this market can publish: tgz or zip. */
64
+ export function isArchiveName(fileName) {
65
+ return isTarballName(fileName) || /[.]zip$/i.test(fileName)
66
+ }
67
+
68
+ /** 企业插件命名硬约束:必须以 traness- 开头,不做自动改名。 */
69
+ export const PLUGIN_NAME_PREFIX = 'traness-'
70
+
71
+ /** @param {string} name */
72
+ export function nameMatchesPolicy(name) {
73
+ return typeof name === 'string' && name.startsWith(PLUGIN_NAME_PREFIX) && name.length > PLUGIN_NAME_PREFIX.length
74
+ }
54
75
  /**
55
76
  * Extract a tarball, inject the uploader metadata and the discovery keyword,
56
77
  * and publish the resulting directory with the user's npm. The directory is a
@@ -69,6 +90,25 @@ export function isTarballName(fileName) {
69
90
  * run?: typeof run,
70
91
  * }} input
71
92
  */
93
+
94
+ /**
95
+ * 找到插件根目录:npm tarball 固定在 'package/';zip 允许根目录直接是插件,
96
+ * 或只包一层文件夹(两种结构都支持)。
97
+ *
98
+ * @param {string} scratch
99
+ * @param {boolean} isZip
100
+ */
101
+ async function locatePluginRoot(scratch, isZip) {
102
+ const direct = join(scratch, isZip ? '' : 'package', 'package.json')
103
+ if (await exists(direct)) return join(scratch, isZip ? '' : 'package')
104
+ if (!isZip) return null
105
+ const entries = await readdir(scratch, { withFileTypes: true })
106
+ const dirs = entries.filter((entry) => entry.isDirectory())
107
+ if (dirs.length !== 1) return null
108
+ const nested = join(scratch, dirs[0].name)
109
+ if (!(await exists(join(nested, 'package.json')))) return null
110
+ return nested
111
+ }
72
112
  export async function publishTarball(input) {
73
113
  const registry = registryBase(input.registry)
74
114
  const npm = findCli(input.npmBin ?? 'npm')
@@ -79,14 +119,19 @@ export async function publishTarball(input) {
79
119
  let authFile
80
120
  try {
81
121
  await writeFile(tarballPath, input.tarball, { mode: 0o600 })
82
- const extracted = await (input.run ?? run)('tar', ['-xzf', tarballPath, '-C', scratch], {
122
+ const isZip = /[.]zip$/i.test(input.fileName)
123
+ const extracted = await (input.run ?? run)('tar', [isZip ? '-xf' : '-xzf', tarballPath, '-C', scratch], {
83
124
  timeoutMs: 30_000,
84
125
  })
85
126
  if (extracted.code !== 0) {
86
127
  return { ok: false, reason: 'invalid-tarball', detail: 'tar could not read the uploaded file' }
87
128
  }
88
- // A packed npm tarball puts everything under one `package/` directory.
89
- const pkgDir = join(scratch, 'package')
129
+ // A packed npm tarball puts everything under one 'package/' directory; a
130
+ // user zip may put package.json at the root or inside a single top folder.
131
+ const pkgDir = await locatePluginRoot(scratch, isZip)
132
+ if (pkgDir === null) {
133
+ return { ok: false, reason: 'invalid-archive', detail: 'the archive has no package.json at its root' }
134
+ }
90
135
  let manifest
91
136
  try {
92
137
  manifest = JSON.parse(await readFile(join(pkgDir, 'package.json'), 'utf8'))
@@ -99,6 +144,15 @@ export async function publishTarball(input) {
99
144
  if (typeof manifest.version !== 'string' || manifest.version.trim() === '') {
100
145
  return { ok: false, reason: 'invalid-manifest', detail: 'package.json has no version' }
101
146
  }
147
+ const requestedName = typeof input.name === 'string' ? input.name.trim() : ''
148
+ const effectiveName = requestedName !== '' ? requestedName : manifest.name
149
+ if (!nameMatchesPolicy(effectiveName)) {
150
+ return {
151
+ ok: false,
152
+ reason: 'name-prefix-required',
153
+ detail: '插件名必须以 ' + PLUGIN_NAME_PREFIX + ' 开头,当前为 ' + effectiveName,
154
+ }
155
+ }
102
156
  const name = typeof input.name === 'string' && input.name.trim() !== '' ? input.name.trim() : manifest.name
103
157
  const description =
104
158
  typeof input.description === 'string' && input.description.trim() !== ''