@nocobase/cli-v1 2.1.0-beta.22 → 2.1.0-beta.24
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 +5 -5
- package/src/commands/build.js +1 -1
- package/src/commands/docs.js +306 -0
- package/src/commands/index.js +1 -0
- package/src/util.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/cli-v1",
|
|
3
|
-
"version": "2.1.0-beta.
|
|
3
|
+
"version": "2.1.0-beta.24",
|
|
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.
|
|
11
|
+
"@nocobase/cli": "2.1.0-beta.24",
|
|
12
12
|
"@nocobase/license-kit": "^0.3.8",
|
|
13
|
-
"@nocobase/utils": "2.1.0-beta.
|
|
13
|
+
"@nocobase/utils": "2.1.0-beta.24",
|
|
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.
|
|
31
|
+
"@nocobase/devtools": "2.1.0-beta.24"
|
|
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": "
|
|
38
|
+
"gitHead": "f77b85530a2d127d9bfe4dca3a26fbb02c1139ba"
|
|
39
39
|
}
|
package/src/commands/build.js
CHANGED
|
@@ -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
|
+
};
|
package/src/commands/index.js
CHANGED
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'], {
|