@nocobase/cli-v1 2.1.0-beta.22 → 2.1.0-beta.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/cli-v1",
3
- "version": "2.1.0-beta.22",
3
+ "version": "2.1.0-beta.23",
4
4
  "description": "",
5
5
  "license": "Apache-2.0",
6
6
  "main": "./src/index.js",
@@ -8,9 +8,9 @@
8
8
  "nocobase-v1": "./bin/index.js"
9
9
  },
10
10
  "dependencies": {
11
- "@nocobase/cli": "2.1.0-beta.22",
11
+ "@nocobase/cli": "2.1.0-beta.23",
12
12
  "@nocobase/license-kit": "^0.3.8",
13
- "@nocobase/utils": "2.1.0-beta.22",
13
+ "@nocobase/utils": "2.1.0-beta.23",
14
14
  "@types/fs-extra": "^11.0.1",
15
15
  "@umijs/utils": "3.5.20",
16
16
  "chalk": "^4.1.1",
@@ -28,12 +28,12 @@
28
28
  "tsx": "^4.19.0"
29
29
  },
30
30
  "devDependencies": {
31
- "@nocobase/devtools": "2.1.0-beta.22"
31
+ "@nocobase/devtools": "2.1.0-beta.23"
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",
35
35
  "url": "git+https://github.com/nocobase/nocobase.git",
36
36
  "directory": "packages/core/cli"
37
37
  },
38
- "gitHead": "53ad02861ed8e813103f59659804417118c85b4c"
38
+ "gitHead": "bb4c0d3551bf9eff505b63756dd24a0813231f16"
39
39
  }
@@ -80,7 +80,7 @@ module.exports = (cli) => {
80
80
  options.retry ? '--retry' : '',
81
81
  ]);
82
82
  buildIndexHtml(true);
83
- if (options.packages && !options.packages.includes('@nocobase/app')) {
83
+ if (!pkgs.includes('@nocobase/app')) {
84
84
  await buildClientV2();
85
85
  }
86
86
  });
@@ -0,0 +1,306 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ const chalk = require('chalk');
11
+ const { Command } = require('commander');
12
+ const { resolve } = require('path');
13
+
14
+ const REQUIRED_ENV_VARS = [
15
+ 'DOCS_ALI_OSS_ACCESS_KEY_ID',
16
+ 'DOCS_ALI_OSS_ACCESS_KEY_SECRET',
17
+ 'DOCS_ALI_OSS_BUCKET',
18
+ 'DOCS_ALI_OSS_REGION',
19
+ 'DOCS_ALI_CDN_DOMAIN',
20
+ ];
21
+
22
+ const TIMESTAMP_DIR_PATTERN = /^\d{14}\/$/;
23
+ const KEEP_VERSIONS = 1;
24
+ const OSS_LIST_MAX_KEYS = 1000;
25
+ const OSS_DELETE_BATCH_SIZE = 1000;
26
+
27
+ /**
28
+ * Normalize domain — strip protocol if present
29
+ * @param {string} domain
30
+ * @returns {string}
31
+ */
32
+ function normalizeDomain(domain) {
33
+ return domain.replace(/^https?:\/\//, '').replace(/\/+$/, '');
34
+ }
35
+
36
+ /**
37
+ * Create an Alibaba Cloud CDN client
38
+ * @returns {import('@alicloud/cdn20180510').default}
39
+ */
40
+ function createCdnClient() {
41
+ const Cdn20180510 = require('@alicloud/cdn20180510');
42
+ const OpenApi = require('@alicloud/openapi-client');
43
+
44
+ const config = new OpenApi.Config({
45
+ accessKeyId: process.env.DOCS_ALI_OSS_ACCESS_KEY_ID,
46
+ accessKeySecret: process.env.DOCS_ALI_OSS_ACCESS_KEY_SECRET,
47
+ });
48
+ config.endpoint = 'cdn.aliyuncs.com';
49
+
50
+ return new Cdn20180510.default(config);
51
+ }
52
+
53
+ /**
54
+ * Update CDN origin URL rewrite rule to point to the new timestamp directory
55
+ * @param {import('@alicloud/cdn20180510').default} cdnClient
56
+ * @param {string} domain
57
+ * @param {string} timestampDir
58
+ */
59
+ const REWRITE_RULES = [
60
+ // /en/ 下无后缀的页面路由,去掉 /en/ 前缀并补 .html(如 /en/vector-store → /{ts}/vector-store.html)
61
+ { sourceUrl: '^/en/([^.]*[^/.])$', targetTemplate: (ts) => `/${ts}/$1.html`, flag: 'break' },
62
+ // 其他语言(如 /cn/)无后缀的页面路由,补 .html(如 /cn/vector-store → /{ts}/cn/vector-store.html)
63
+ { sourceUrl: '^/([^.]*[^/.])$', targetTemplate: (ts) => `/${ts}/$1.html`, flag: 'break' },
64
+ // /en/ 下的静态资源和目录,去掉 /en/ 前缀(如 /en/style.css → /{ts}/style.css,/en/ → /{ts}/)
65
+ { sourceUrl: '^/en/(.*)', targetTemplate: (ts) => `/${ts}/$1`, flag: 'break' },
66
+ // 兜底:所有其他请求加时间戳前缀(如 /cn/style.css → /{ts}/cn/style.css,/ → /{ts}/)
67
+ { sourceUrl: '^/(.*)', targetTemplate: (ts) => `/${ts}/$1`, flag: 'break' },
68
+ ];
69
+
70
+ async function updateCdnOriginRewrite(cdnClient, domain, timestampDir) {
71
+ const Cdn20180510 = require('@alicloud/cdn20180510');
72
+
73
+ // Fetch existing configs to find configIds
74
+ const existingConfigMap = {};
75
+ try {
76
+ const describeRequest = new Cdn20180510.DescribeCdnDomainConfigsRequest({
77
+ domainName: domain,
78
+ functionNames: 'back_to_origin_url_rewrite',
79
+ });
80
+ const describeResponse = await cdnClient.describeCdnDomainConfigs(describeRequest);
81
+ const configs = describeResponse.body?.domainConfigs?.domainConfig;
82
+ if (configs && configs.length > 0) {
83
+ for (const config of configs) {
84
+ const args = config.functionArgs?.functionArg || [];
85
+ const sourceArg = args.find((a) => a.argName === 'source_url');
86
+ if (sourceArg) {
87
+ existingConfigMap[sourceArg.argValue] = config.configId;
88
+ }
89
+ }
90
+ }
91
+ } catch (error) {
92
+ console.log(chalk.yellow(`Could not fetch existing CDN config (may be first deployment): ${error.message}`));
93
+ }
94
+
95
+ // Build function configs for all rules
96
+ const functions = REWRITE_RULES.map((rule) => {
97
+ const functionConfig = {
98
+ functionName: 'back_to_origin_url_rewrite',
99
+ functionArgs: [
100
+ { argName: 'source_url', argValue: rule.sourceUrl },
101
+ { argName: 'target_url', argValue: rule.targetTemplate(timestampDir) },
102
+ { argName: 'flag', argValue: rule.flag },
103
+ ],
104
+ };
105
+ if (existingConfigMap[rule.sourceUrl]) {
106
+ functionConfig.configId = existingConfigMap[rule.sourceUrl];
107
+ }
108
+ return functionConfig;
109
+ });
110
+
111
+ const setRequest = new Cdn20180510.BatchSetCdnDomainConfigRequest({
112
+ domainNames: domain,
113
+ functions: JSON.stringify(functions),
114
+ });
115
+
116
+ await cdnClient.batchSetCdnDomainConfig(setRequest);
117
+ }
118
+
119
+ /**
120
+ * Poll CDN API until the rewrite rule status becomes "success"
121
+ * @param {import('@alicloud/cdn20180510').default} cdnClient
122
+ * @param {string} domain
123
+ * @param {string} timestampDir
124
+ */
125
+ async function waitForRewriteRule(cdnClient, domain, timestampDir) {
126
+ const Cdn20180510 = require('@alicloud/cdn20180510');
127
+ const maxAttempts = 180; // 180 * 10s = 30min max
128
+ const interval = 10000;
129
+
130
+ for (let i = 1; i <= maxAttempts; i++) {
131
+ try {
132
+ const request = new Cdn20180510.DescribeCdnDomainConfigsRequest({
133
+ domainName: domain,
134
+ functionNames: 'back_to_origin_url_rewrite',
135
+ });
136
+ const response = await cdnClient.describeCdnDomainConfigs(request);
137
+ const configs = response.body?.domainConfigs?.domainConfig;
138
+
139
+ if (configs && configs.length > 0) {
140
+ const expectedPrefix = `/${timestampDir}/`;
141
+ const allEffective = REWRITE_RULES.every((rule) => {
142
+ const config = configs.find((c) => {
143
+ const cArgs = c.functionArgs?.functionArg || [];
144
+ const src = cArgs.find((a) => a.argName === 'source_url');
145
+ return src && src.argValue === rule.sourceUrl;
146
+ });
147
+ if (!config) return false;
148
+ const args = config.functionArgs?.functionArg || [];
149
+ const targetArg = args.find((a) => a.argName === 'target_url');
150
+ return config.status === 'success' && (targetArg?.argValue || '').startsWith(expectedPrefix);
151
+ });
152
+
153
+ if (allEffective) {
154
+ console.log(chalk.green(`All rewrite rules are effective (attempt ${i}/${maxAttempts})`));
155
+ return;
156
+ }
157
+ console.log(chalk.blue(`Rewrite rules not all effective yet, waiting... (attempt ${i}/${maxAttempts})`));
158
+ }
159
+ } catch (error) {
160
+ console.log(chalk.blue(`Failed to query rule status, retrying... (attempt ${i}/${maxAttempts})`));
161
+ }
162
+ await new Promise((resolve) => setTimeout(resolve, interval));
163
+ }
164
+
165
+ console.warn(chalk.yellow('Rewrite rule verification timed out after 30min, proceeding with cache refresh anyway'));
166
+ }
167
+
168
+ /**
169
+ * Refresh CDN cache for the domain
170
+ * @param {import('@alicloud/cdn20180510').default} cdnClient
171
+ * @param {string} domain
172
+ */
173
+ async function refreshCdnCache(cdnClient, domain) {
174
+ const Cdn20180510 = require('@alicloud/cdn20180510');
175
+
176
+ const request = new Cdn20180510.RefreshObjectCachesRequest({
177
+ objectPath: `https://${domain}/`,
178
+ objectType: 'Directory',
179
+ });
180
+
181
+ await cdnClient.refreshObjectCaches(request);
182
+ }
183
+
184
+ /**
185
+ * List all objects under a given OSS prefix (handles pagination)
186
+ * @param {import('ali-oss')} ossClient
187
+ * @param {string} prefix
188
+ * @returns {Promise<string[]>} list of object names
189
+ */
190
+ async function listAllObjects(ossClient, prefix) {
191
+ const allObjects = [];
192
+ let marker = null;
193
+ let isTruncated = true;
194
+
195
+ while (isTruncated) {
196
+ const params = { prefix, 'max-keys': OSS_LIST_MAX_KEYS };
197
+ if (marker) {
198
+ params.marker = marker;
199
+ }
200
+
201
+ const result = await ossClient.list(params);
202
+ const objects = result.objects || [];
203
+ for (const obj of objects) {
204
+ allObjects.push(obj.name);
205
+ }
206
+
207
+ isTruncated = result.isTruncated;
208
+ marker = result.nextMarker;
209
+ }
210
+
211
+ return allObjects;
212
+ }
213
+
214
+ /**
215
+ * Clean up old timestamp directories, keeping the latest N versions
216
+ * @param {import('ali-oss')} ossClient
217
+ * @param {number} keepCount
218
+ */
219
+ async function cleanupOldVersions(ossClient, keepCount) {
220
+ const result = await ossClient.list({ prefix: '', delimiter: '/', 'max-keys': OSS_LIST_MAX_KEYS });
221
+ const prefixes = result.prefixes || [];
222
+
223
+ const timestampDirs = prefixes.filter((p) => TIMESTAMP_DIR_PATTERN.test(p)).sort();
224
+
225
+ if (timestampDirs.length <= keepCount) {
226
+ console.log(chalk.green(`Found ${timestampDirs.length} version(s), no cleanup needed (keeping ${keepCount})`));
227
+ return;
228
+ }
229
+
230
+ const dirsToDelete = timestampDirs.slice(0, timestampDirs.length - keepCount);
231
+ console.log(chalk.blue(`Found ${timestampDirs.length} versions, will delete ${dirsToDelete.length} old version(s)`));
232
+
233
+ for (const dir of dirsToDelete) {
234
+ console.log(chalk.blue(` Deleting ${dir}...`));
235
+ const objectNames = await listAllObjects(ossClient, dir);
236
+
237
+ for (let i = 0; i < objectNames.length; i += OSS_DELETE_BATCH_SIZE) {
238
+ const batch = objectNames.slice(i, i + OSS_DELETE_BATCH_SIZE);
239
+ await ossClient.deleteMulti(batch, { quiet: true });
240
+ }
241
+
242
+ console.log(chalk.green(` Deleted ${objectNames.length} objects from ${dir}`));
243
+ }
244
+ }
245
+
246
+ /**
247
+ * @param {Command} cli
248
+ */
249
+ module.exports = (cli) => {
250
+ cli
251
+ .command('docs:update')
252
+ .option('--timestamp <timestamp>', 'timestamp directory to point CDN to (required)')
253
+ .allowUnknownOption()
254
+ .action(async (options) => {
255
+ // 1. Validate environment variables
256
+ const missingVars = REQUIRED_ENV_VARS.filter((v) => !process.env[v]);
257
+ if (missingVars.length > 0) {
258
+ console.error(chalk.red(`Missing required environment variables: ${missingVars.join(', ')}`));
259
+ process.exit(1);
260
+ }
261
+
262
+ // 2. Validate --timestamp
263
+ if (!options.timestamp) {
264
+ console.error(chalk.red('Missing required option: --timestamp <ts>'));
265
+ process.exit(1);
266
+ }
267
+
268
+ const timestamp = options.timestamp;
269
+ const domain = normalizeDomain(process.env.DOCS_ALI_CDN_DOMAIN);
270
+
271
+ // 3. Create OSS client (for cleanup)
272
+ const Client = require('ali-oss');
273
+ const ossClient = new Client({
274
+ accessKeyId: process.env.DOCS_ALI_OSS_ACCESS_KEY_ID,
275
+ accessKeySecret: process.env.DOCS_ALI_OSS_ACCESS_KEY_SECRET,
276
+ bucket: process.env.DOCS_ALI_OSS_BUCKET,
277
+ region: process.env.DOCS_ALI_OSS_REGION,
278
+ });
279
+
280
+ // 4. Update CDN origin rewrite rule
281
+ console.log(chalk.blue(`Updating CDN origin rewrite rule for ${domain}...`));
282
+ try {
283
+ const cdnClient = createCdnClient();
284
+ await updateCdnOriginRewrite(cdnClient, domain, timestamp);
285
+ console.log(chalk.green(`CDN origin rewrite updated to /${timestamp}/`));
286
+
287
+ // 5. Poll until rewrite rule takes effect, then refresh CDN cache
288
+ console.log(chalk.blue('Waiting for rewrite rule to propagate...'));
289
+ await waitForRewriteRule(cdnClient, domain, timestamp);
290
+ console.log(chalk.blue('Refreshing CDN cache...'));
291
+ await refreshCdnCache(cdnClient, domain);
292
+ console.log(chalk.green('CDN cache refresh submitted'));
293
+ } catch (error) {
294
+ console.error(chalk.red('CDN update failed (non-fatal):'), error.message);
295
+ }
296
+
297
+ // 6. Cleanup old versions (non-fatal)
298
+ console.log(chalk.blue(`Cleaning up old versions (keeping latest ${KEEP_VERSIONS})...`));
299
+ try {
300
+ await cleanupOldVersions(ossClient, KEEP_VERSIONS);
301
+ console.log(chalk.green('Cleanup complete'));
302
+ } catch (error) {
303
+ console.warn(chalk.yellow('Cleanup failed (non-fatal):'), error.message);
304
+ }
305
+ });
306
+ };
@@ -39,6 +39,7 @@ module.exports = (cli) => {
39
39
  require('./instance-id')(cli);
40
40
  require('./view-license-key')(cli);
41
41
  require('./client')(cli);
42
+ require('./docs')(cli);
42
43
  if (isPackageValid('@umijs/utils')) {
43
44
  require('./create-plugin')(cli);
44
45
  }
package/src/util.js CHANGED
@@ -260,6 +260,7 @@ exports.genTsConfigPaths = function genTsConfigPaths() {
260
260
  const cwd = process.cwd();
261
261
  const cwdLength = cwd.length;
262
262
  const paths = {
263
+ '@docs/*': ['docs/docs/*'],
263
264
  '@@/*': ['.dumi/tmp/*'],
264
265
  };
265
266
  const packages = fg.sync(['packages/*/*/package.json', 'packages/*/*/*/package.json'], {