@yidun/cdn-upload-webpack-plugin 1.1.5 → 1.1.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/.eslintignore +2 -2
- package/.eslintrc.js +9 -9
- package/README.md +68 -45
- package/package.json +24 -24
- package/src/adaptor/nos.js +39 -39
- package/src/index.js +45 -45
- package/src/task.js +103 -103
- package/src/utils.js +73 -41
- package/src/vite.js +137 -46
package/.eslintignore
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
/dist/
|
|
2
|
-
/dashboard/
|
|
1
|
+
/dist/
|
|
2
|
+
/dashboard/
|
|
3
3
|
node_modules
|
package/.eslintrc.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
root: true,
|
|
3
|
-
env: {
|
|
4
|
-
node: true
|
|
5
|
-
},
|
|
6
|
-
extends: [
|
|
7
|
-
'standard'
|
|
8
|
-
]
|
|
9
|
-
}
|
|
1
|
+
module.exports = {
|
|
2
|
+
root: true,
|
|
3
|
+
env: {
|
|
4
|
+
node: true
|
|
5
|
+
},
|
|
6
|
+
extends: [
|
|
7
|
+
'standard'
|
|
8
|
+
]
|
|
9
|
+
}
|
package/README.md
CHANGED
|
@@ -1,45 +1,68 @@
|
|
|
1
|
-
## cdn-upload-webpack-plugin, 用于将静态资源上传到 cdn
|
|
2
|
-
|
|
3
|
-
### usage
|
|
4
|
-
|
|
5
|
-
### 构造函数
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
/**
|
|
9
|
-
* @param {Object} options
|
|
10
|
-
* @param {Array|String} dirs 待上传的文件目录
|
|
11
|
-
* @param {Boolean} includeRootDir 文件的相对路径计算时包不包含根目录,默认 true
|
|
12
|
-
* @param {RegExp|Array<RegExp>} ignores 排除特定文件
|
|
13
|
-
* @param {Object} client 上传 client 配置,目前只支持 NosClient,不同的 client 配置参数可能不同,以 nos 举例
|
|
14
|
-
* @param {String} accessId
|
|
15
|
-
* @param {String} secretKey
|
|
16
|
-
* @param {String} bucket 桶名
|
|
17
|
-
* @param {String} namespace 不同项目在使用同一个桶时做隔离
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
1
|
+
## cdn-upload-webpack-plugin, 用于将静态资源上传到 cdn
|
|
2
|
+
|
|
3
|
+
### usage
|
|
4
|
+
|
|
5
|
+
### 构造函数
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
/**
|
|
9
|
+
* @param {Object} options
|
|
10
|
+
* @param {Array|String} dirs 待上传的文件目录
|
|
11
|
+
* @param {Boolean} includeRootDir 文件的相对路径计算时包不包含根目录,默认 true
|
|
12
|
+
* @param {RegExp|Array<RegExp>} ignores 排除特定文件
|
|
13
|
+
* @param {Object} client 上传 client 配置,目前只支持 NosClient,不同的 client 配置参数可能不同,以 nos 举例
|
|
14
|
+
* @param {String} accessId
|
|
15
|
+
* @param {String} secretKey
|
|
16
|
+
* @param {String} bucket 桶名
|
|
17
|
+
* @param {String} namespace 不同项目在使用同一个桶时做隔离
|
|
18
|
+
* @param {Boolean} incrementalUpload 仅 vite 插件支持。是否开启增量上传(仅对 content-hash 文件生效),默认 false
|
|
19
|
+
* @param {Number} precheckParallelCount 仅 vite 插件支持。增量上传预检查并发数,默认与 parallelCount 一致
|
|
20
|
+
* @param {Boolean} cleanupAfterUpload 仅 vite 插件支持。上传成功后是否清理本地资源,默认 false
|
|
21
|
+
* @param {RegExp|Array<RegExp>} cleanupKeepRules 仅 vite 插件支持。清理后保留规则,默认 [/\.html$/]
|
|
22
|
+
*/
|
|
23
|
+
new CdnUploadWebpackPlugin(options)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 以 vue-cli 项目举例
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
configureWebpack: config => {
|
|
30
|
+
if (process.env.NODE_ENV === 'production') {
|
|
31
|
+
config.plugins.push(...[
|
|
32
|
+
new CdnUploadWebpackPlugin({
|
|
33
|
+
dirs: path.resolve(__dirname, './dist'),
|
|
34
|
+
ignore: /.html$/,
|
|
35
|
+
client: {
|
|
36
|
+
bucket: '',
|
|
37
|
+
accessId: '',
|
|
38
|
+
secretKey: '',
|
|
39
|
+
namespace: 'yidunfe/cdntest'
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
])
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### adaptor
|
|
48
|
+
|
|
49
|
+
目前仅支持 nos 上传,后续如有其他的上传需求,可自行扩展 adaptor,可通过 client 参数区分
|
|
50
|
+
|
|
51
|
+
### vite 额外能力:上传成功后清理本地静态资源
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
CdnUploadVitePlugin({
|
|
55
|
+
dirs: [path.join(__dirname, './dist')],
|
|
56
|
+
ignores: [/\.html$/, /\.map$/],
|
|
57
|
+
incrementalUpload: true,
|
|
58
|
+
precheckParallelCount: 24,
|
|
59
|
+
cleanupAfterUpload: true,
|
|
60
|
+
cleanupKeepRules: [/index\.html$/],
|
|
61
|
+
client: {
|
|
62
|
+
bucket: '',
|
|
63
|
+
accessId: '',
|
|
64
|
+
secretKey: '',
|
|
65
|
+
namespace: 'yidunfe/cdntest'
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
```
|
package/package.json
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@yidun/cdn-upload-webpack-plugin",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "A webpack plugin for upload dist to cdn.",
|
|
5
|
-
"main": "src/index.js",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"test": "node test/index.js"
|
|
8
|
-
},
|
|
9
|
-
"author": "zhanglulu01",
|
|
10
|
-
"license": "ISC",
|
|
11
|
-
"devDependencies": {
|
|
12
|
-
"eslint": "^7.14.0",
|
|
13
|
-
"eslint-config-standard": "^16.0.2",
|
|
14
|
-
"eslint-plugin-import": "^2.22.1",
|
|
15
|
-
"eslint-plugin-node": "^11.1.0",
|
|
16
|
-
"eslint-plugin-promise": "^4.2.1",
|
|
17
|
-
"eslint-plugin-standard": "^5.0.0"
|
|
18
|
-
},
|
|
19
|
-
"dependencies": {
|
|
20
|
-
"@nos-sdk/nos-node-sdk": "^0.2.6",
|
|
21
|
-
"bluebird": "^3.7.2",
|
|
22
|
-
"node-fetch": "^2.7.0"
|
|
23
|
-
}
|
|
24
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@yidun/cdn-upload-webpack-plugin",
|
|
3
|
+
"version": "1.1.7",
|
|
4
|
+
"description": "A webpack plugin for upload dist to cdn.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node test/index.js"
|
|
8
|
+
},
|
|
9
|
+
"author": "zhanglulu01",
|
|
10
|
+
"license": "ISC",
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"eslint": "^7.14.0",
|
|
13
|
+
"eslint-config-standard": "^16.0.2",
|
|
14
|
+
"eslint-plugin-import": "^2.22.1",
|
|
15
|
+
"eslint-plugin-node": "^11.1.0",
|
|
16
|
+
"eslint-plugin-promise": "^4.2.1",
|
|
17
|
+
"eslint-plugin-standard": "^5.0.0"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@nos-sdk/nos-node-sdk": "^0.2.6",
|
|
21
|
+
"bluebird": "^3.7.2",
|
|
22
|
+
"node-fetch": "^2.7.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/adaptor/nos.js
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
const { Task } = require('../task')
|
|
2
|
-
const { NosClient } = require('@nos-sdk/nos-node-sdk')
|
|
3
|
-
const { normalizeObjectKey } = require('../utils')
|
|
4
|
-
const fs = require('fs')
|
|
5
|
-
|
|
6
|
-
class NosUploadTask extends Task {
|
|
7
|
-
constructor (nosOptions, file) {
|
|
8
|
-
super(() => this.upload())
|
|
9
|
-
const { accessId, secretKey, bucket, endpoint = 'nos.netease.com', namespace = '' } = nosOptions
|
|
10
|
-
const nosClient = new NosClient({
|
|
11
|
-
accessKey: accessId,
|
|
12
|
-
accessSecret: secretKey,
|
|
13
|
-
endpoint: `https://${endpoint}`,
|
|
14
|
-
defaultBucket: bucket
|
|
15
|
-
})
|
|
16
|
-
this._nosClient = nosClient
|
|
17
|
-
this._bucket = bucket
|
|
18
|
-
this._file = file
|
|
19
|
-
this._namespace = namespace
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
upload () {
|
|
23
|
-
return new Promise((resolve, reject) => {
|
|
24
|
-
try {
|
|
25
|
-
const stream = fs.createReadStream(this._file.path)
|
|
26
|
-
const data = this._nosClient.putObject({
|
|
27
|
-
body: stream,
|
|
28
|
-
objectKey: normalizeObjectKey(`${this._namespace}/${this._file.relative}`)
|
|
29
|
-
})
|
|
30
|
-
resolve(data)
|
|
31
|
-
} catch (error) {
|
|
32
|
-
console.error(`upload to nos fail, because ${error}`)
|
|
33
|
-
reject(error)
|
|
34
|
-
}
|
|
35
|
-
})
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
module.exports = NosUploadTask
|
|
1
|
+
const { Task } = require('../task')
|
|
2
|
+
const { NosClient } = require('@nos-sdk/nos-node-sdk')
|
|
3
|
+
const { normalizeObjectKey } = require('../utils')
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
|
|
6
|
+
class NosUploadTask extends Task {
|
|
7
|
+
constructor (nosOptions, file) {
|
|
8
|
+
super(() => this.upload())
|
|
9
|
+
const { accessId, secretKey, bucket, endpoint = 'nos.netease.com', namespace = '' } = nosOptions
|
|
10
|
+
const nosClient = new NosClient({
|
|
11
|
+
accessKey: accessId,
|
|
12
|
+
accessSecret: secretKey,
|
|
13
|
+
endpoint: `https://${endpoint}`,
|
|
14
|
+
defaultBucket: bucket
|
|
15
|
+
})
|
|
16
|
+
this._nosClient = nosClient
|
|
17
|
+
this._bucket = bucket
|
|
18
|
+
this._file = file
|
|
19
|
+
this._namespace = namespace
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
upload () {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
try {
|
|
25
|
+
const stream = fs.createReadStream(this._file.path)
|
|
26
|
+
const data = this._nosClient.putObject({
|
|
27
|
+
body: stream,
|
|
28
|
+
objectKey: normalizeObjectKey(`${this._namespace}/${this._file.relative}`)
|
|
29
|
+
})
|
|
30
|
+
resolve(data)
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error(`upload to nos fail, because ${error}`)
|
|
33
|
+
reject(error)
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = NosUploadTask
|
package/src/index.js
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
const { getFiles } = require('./utils')
|
|
2
|
-
const { TaskDispatcher } = require('./task')
|
|
3
|
-
const NosUploadTask = require('./adaptor/nos')
|
|
4
|
-
const CdnUploadVitePlugin = require('./vite')
|
|
5
|
-
|
|
6
|
-
exports.CdnUploadWebpackPlugin = class CdnUploadWebpackPlugin {
|
|
7
|
-
/**
|
|
8
|
-
* @param {Object} options
|
|
9
|
-
* @param {Array|String} dirs 待上传的文件目录
|
|
10
|
-
* @param {Boolean} includeRootDir 文件的相对路径计算时包不包含根目录,默认 true
|
|
11
|
-
* @param {RegExp|Array<RegExp>} ignores 排除特定文件
|
|
12
|
-
* @param {Object} client 上传 client 配置,目前只支持 NosClient,不同的 client 配置参数可能不同,以 nos 举例
|
|
13
|
-
* @param {String} accessId
|
|
14
|
-
* @param {String} secretKey
|
|
15
|
-
* @param {String} bucket 桶名
|
|
16
|
-
* @param {String} namespace 不同项目在使用同一个桶时做隔离
|
|
17
|
-
* @param {Number} parallelCount 并行上传数, 默认 3
|
|
18
|
-
*/
|
|
19
|
-
constructor (options) {
|
|
20
|
-
const { dirs, includeRootDir, ignores, client, parallelCount } = options
|
|
21
|
-
this._dirs = dirs
|
|
22
|
-
this._includeRootDir = includeRootDir
|
|
23
|
-
this._ignores = ignores
|
|
24
|
-
this._client = client
|
|
25
|
-
this._parallelCount = parallelCount
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
apply (compiler) {
|
|
29
|
-
compiler.hooks.afterEmit.tapAsync('CdnUploadWebpackPlugin', (compiler, callback) => {
|
|
30
|
-
const files = getFiles(this._dirs, this._includeRootDir, this._ignores)
|
|
31
|
-
const tasks = files.map(file => new NosUploadTask(this._client, file))
|
|
32
|
-
const taskDispatcher = new TaskDispatcher(tasks, {
|
|
33
|
-
onSuccess: () => {
|
|
34
|
-
console.log('CdnUploadWebpackPlugin: assets upload success')
|
|
35
|
-
callback()
|
|
36
|
-
},
|
|
37
|
-
onError: error => callback(error)
|
|
38
|
-
})
|
|
39
|
-
console.log('CdnUploadWebpackPlugin: assets uploading ...')
|
|
40
|
-
taskDispatcher.dispatch(this._parallelCount)
|
|
41
|
-
})
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
exports.CdnUploadVitePlugin = CdnUploadVitePlugin
|
|
1
|
+
const { getFiles } = require('./utils')
|
|
2
|
+
const { TaskDispatcher } = require('./task')
|
|
3
|
+
const NosUploadTask = require('./adaptor/nos')
|
|
4
|
+
const CdnUploadVitePlugin = require('./vite')
|
|
5
|
+
|
|
6
|
+
exports.CdnUploadWebpackPlugin = class CdnUploadWebpackPlugin {
|
|
7
|
+
/**
|
|
8
|
+
* @param {Object} options
|
|
9
|
+
* @param {Array|String} dirs 待上传的文件目录
|
|
10
|
+
* @param {Boolean} includeRootDir 文件的相对路径计算时包不包含根目录,默认 true
|
|
11
|
+
* @param {RegExp|Array<RegExp>} ignores 排除特定文件
|
|
12
|
+
* @param {Object} client 上传 client 配置,目前只支持 NosClient,不同的 client 配置参数可能不同,以 nos 举例
|
|
13
|
+
* @param {String} accessId
|
|
14
|
+
* @param {String} secretKey
|
|
15
|
+
* @param {String} bucket 桶名
|
|
16
|
+
* @param {String} namespace 不同项目在使用同一个桶时做隔离
|
|
17
|
+
* @param {Number} parallelCount 并行上传数, 默认 3
|
|
18
|
+
*/
|
|
19
|
+
constructor (options) {
|
|
20
|
+
const { dirs, includeRootDir, ignores, client, parallelCount } = options
|
|
21
|
+
this._dirs = dirs
|
|
22
|
+
this._includeRootDir = includeRootDir
|
|
23
|
+
this._ignores = ignores
|
|
24
|
+
this._client = client
|
|
25
|
+
this._parallelCount = parallelCount
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
apply (compiler) {
|
|
29
|
+
compiler.hooks.afterEmit.tapAsync('CdnUploadWebpackPlugin', (compiler, callback) => {
|
|
30
|
+
const files = getFiles(this._dirs, this._includeRootDir, this._ignores)
|
|
31
|
+
const tasks = files.map(file => new NosUploadTask(this._client, file))
|
|
32
|
+
const taskDispatcher = new TaskDispatcher(tasks, {
|
|
33
|
+
onSuccess: () => {
|
|
34
|
+
console.log('CdnUploadWebpackPlugin: assets upload success')
|
|
35
|
+
callback()
|
|
36
|
+
},
|
|
37
|
+
onError: error => callback(error)
|
|
38
|
+
})
|
|
39
|
+
console.log('CdnUploadWebpackPlugin: assets uploading ...')
|
|
40
|
+
taskDispatcher.dispatch(this._parallelCount)
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
exports.CdnUploadVitePlugin = CdnUploadVitePlugin
|
package/src/task.js
CHANGED
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
const { noop } = require('./utils')
|
|
2
|
-
|
|
3
|
-
const TASK_STATUS = {
|
|
4
|
-
PENDING: 'pending',
|
|
5
|
-
DOING: 'doing',
|
|
6
|
-
REJECTED: 'rejected',
|
|
7
|
-
FULFILLED: 'fulfilled'
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
let uid = 0 // 任务 id
|
|
11
|
-
/**
|
|
12
|
-
* task 基类,提供重试等功能,供 adaptor 继承,如 NosUploadTask
|
|
13
|
-
*/
|
|
14
|
-
class Task {
|
|
15
|
-
constructor (invoke) {
|
|
16
|
-
this.status = TASK_STATUS.PENDING
|
|
17
|
-
this._id = uid++
|
|
18
|
-
this._invoke = invoke
|
|
19
|
-
this._retryCount = 3
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async start () {
|
|
23
|
-
let retryCount = this._retryCount
|
|
24
|
-
let res
|
|
25
|
-
try {
|
|
26
|
-
this.status = TASK_STATUS.DOING
|
|
27
|
-
res = await this._invoke()
|
|
28
|
-
} catch (error) {
|
|
29
|
-
retryCount--
|
|
30
|
-
if (retryCount < 0) {
|
|
31
|
-
this.status = TASK_STATUS.REJECTED
|
|
32
|
-
return Promise.reject(error)
|
|
33
|
-
} else {
|
|
34
|
-
res = await this._invoke()
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
this.status = TASK_STATUS.FULFILLED
|
|
38
|
-
return res
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const PARALLEL_COUNT = 3 // 上传并行数
|
|
43
|
-
/**
|
|
44
|
-
* 任务调度器,完成整个上传任务的调度
|
|
45
|
-
*/
|
|
46
|
-
class TaskDispatcher {
|
|
47
|
-
constructor (tasks, callbacks = {}) {
|
|
48
|
-
this.status = TASK_STATUS.PENDING
|
|
49
|
-
this._tasks = tasks
|
|
50
|
-
this._activeTasks = []
|
|
51
|
-
this._onError = callbacks.onError || noop
|
|
52
|
-
this._onSuccess = callbacks.onSuccess || noop
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
getPendingTask (count) {
|
|
56
|
-
return this._tasks.filter(t => t.status === TASK_STATUS.PENDING).slice(0, count)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
getPendingTaskCount () {
|
|
60
|
-
return this._tasks.filter(t => t.status === TASK_STATUS.PENDING).length
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
removeActiveTask (task) {
|
|
64
|
-
const index = this._activeTasks.findIndex(t => task._id === t._id)
|
|
65
|
-
if (index > -1) {
|
|
66
|
-
this._activeTasks.splice(index, 1)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
isAllTaskFulfilled () {
|
|
71
|
-
return this._tasks.every(t => t.status === TASK_STATUS.FULFILLED)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* 任务调度入口
|
|
76
|
-
* @param {Number} count
|
|
77
|
-
*/
|
|
78
|
-
dispatch (count = PARALLEL_COUNT) {
|
|
79
|
-
if (TASK_STATUS.REJECTED === this.status) return
|
|
80
|
-
|
|
81
|
-
this._activeTasks = this._activeTasks.concat(this.getPendingTask(count))
|
|
82
|
-
this._activeTasks.forEach(task => {
|
|
83
|
-
if (task.status === TASK_STATUS.PENDING) {
|
|
84
|
-
task.start().then(res => {
|
|
85
|
-
this.removeActiveTask(task)
|
|
86
|
-
if (this.getPendingTaskCount() > 0) {
|
|
87
|
-
this.dispatch(1)
|
|
88
|
-
} else if (this.isAllTaskFulfilled()) {
|
|
89
|
-
this._onSuccess(this._tasks)
|
|
90
|
-
}
|
|
91
|
-
}).catch(error => {
|
|
92
|
-
this.status = TASK_STATUS.REJECTED
|
|
93
|
-
this._onError(error)
|
|
94
|
-
})
|
|
95
|
-
}
|
|
96
|
-
})
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
module.exports = {
|
|
101
|
-
Task,
|
|
102
|
-
TaskDispatcher
|
|
103
|
-
}
|
|
1
|
+
const { noop } = require('./utils')
|
|
2
|
+
|
|
3
|
+
const TASK_STATUS = {
|
|
4
|
+
PENDING: 'pending',
|
|
5
|
+
DOING: 'doing',
|
|
6
|
+
REJECTED: 'rejected',
|
|
7
|
+
FULFILLED: 'fulfilled'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let uid = 0 // 任务 id
|
|
11
|
+
/**
|
|
12
|
+
* task 基类,提供重试等功能,供 adaptor 继承,如 NosUploadTask
|
|
13
|
+
*/
|
|
14
|
+
class Task {
|
|
15
|
+
constructor (invoke) {
|
|
16
|
+
this.status = TASK_STATUS.PENDING
|
|
17
|
+
this._id = uid++
|
|
18
|
+
this._invoke = invoke
|
|
19
|
+
this._retryCount = 3
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async start () {
|
|
23
|
+
let retryCount = this._retryCount
|
|
24
|
+
let res
|
|
25
|
+
try {
|
|
26
|
+
this.status = TASK_STATUS.DOING
|
|
27
|
+
res = await this._invoke()
|
|
28
|
+
} catch (error) {
|
|
29
|
+
retryCount--
|
|
30
|
+
if (retryCount < 0) {
|
|
31
|
+
this.status = TASK_STATUS.REJECTED
|
|
32
|
+
return Promise.reject(error)
|
|
33
|
+
} else {
|
|
34
|
+
res = await this._invoke()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
this.status = TASK_STATUS.FULFILLED
|
|
38
|
+
return res
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const PARALLEL_COUNT = 3 // 上传并行数
|
|
43
|
+
/**
|
|
44
|
+
* 任务调度器,完成整个上传任务的调度
|
|
45
|
+
*/
|
|
46
|
+
class TaskDispatcher {
|
|
47
|
+
constructor (tasks, callbacks = {}) {
|
|
48
|
+
this.status = TASK_STATUS.PENDING
|
|
49
|
+
this._tasks = tasks
|
|
50
|
+
this._activeTasks = []
|
|
51
|
+
this._onError = callbacks.onError || noop
|
|
52
|
+
this._onSuccess = callbacks.onSuccess || noop
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getPendingTask (count) {
|
|
56
|
+
return this._tasks.filter(t => t.status === TASK_STATUS.PENDING).slice(0, count)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getPendingTaskCount () {
|
|
60
|
+
return this._tasks.filter(t => t.status === TASK_STATUS.PENDING).length
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
removeActiveTask (task) {
|
|
64
|
+
const index = this._activeTasks.findIndex(t => task._id === t._id)
|
|
65
|
+
if (index > -1) {
|
|
66
|
+
this._activeTasks.splice(index, 1)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
isAllTaskFulfilled () {
|
|
71
|
+
return this._tasks.every(t => t.status === TASK_STATUS.FULFILLED)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 任务调度入口
|
|
76
|
+
* @param {Number} count
|
|
77
|
+
*/
|
|
78
|
+
dispatch (count = PARALLEL_COUNT) {
|
|
79
|
+
if (TASK_STATUS.REJECTED === this.status) return
|
|
80
|
+
|
|
81
|
+
this._activeTasks = this._activeTasks.concat(this.getPendingTask(count))
|
|
82
|
+
this._activeTasks.forEach(task => {
|
|
83
|
+
if (task.status === TASK_STATUS.PENDING) {
|
|
84
|
+
task.start().then(res => {
|
|
85
|
+
this.removeActiveTask(task)
|
|
86
|
+
if (this.getPendingTaskCount() > 0) {
|
|
87
|
+
this.dispatch(1)
|
|
88
|
+
} else if (this.isAllTaskFulfilled()) {
|
|
89
|
+
this._onSuccess(this._tasks)
|
|
90
|
+
}
|
|
91
|
+
}).catch(error => {
|
|
92
|
+
this.status = TASK_STATUS.REJECTED
|
|
93
|
+
this._onError(error)
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
Task,
|
|
102
|
+
TaskDispatcher
|
|
103
|
+
}
|
package/src/utils.js
CHANGED
|
@@ -1,41 +1,73 @@
|
|
|
1
|
-
const Promise = require('bluebird')
|
|
2
|
-
const fs = Promise.promisifyAll(require('fs'))
|
|
3
|
-
const path = require('path')
|
|
4
|
-
|
|
5
|
-
function getFiles (dirs, includeRootDir = true, ignores) {
|
|
6
|
-
if (typeof dirs === 'string') dirs = [dirs]
|
|
7
|
-
if (ignores instanceof RegExp) ignores = [ignores]
|
|
8
|
-
|
|
9
|
-
const res = []
|
|
10
|
-
dirs.forEach(dir => deep(dir, dir))
|
|
11
|
-
|
|
12
|
-
function deep (dir, rootDir) {
|
|
13
|
-
const files = fs.readdirSync(dir)
|
|
14
|
-
files.forEach(fileName => {
|
|
15
|
-
const pathName = `${dir}${path.sep}${fileName}`
|
|
16
|
-
const info = fs.lstatSync(pathName)
|
|
17
|
-
if (info.isDirectory()) {
|
|
18
|
-
deep(pathName, rootDir)
|
|
19
|
-
} else {
|
|
20
|
-
if (!ignores || ignores.every(ignore => !ignore.test(pathName))) {
|
|
21
|
-
res.push({
|
|
22
|
-
path: pathName,
|
|
23
|
-
relative: path.relative(includeRootDir ? path.join(rootDir, '../') : rootDir, pathName)
|
|
24
|
-
})
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
})
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return res
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function normalizeObjectKey (objectKey) {
|
|
34
|
-
return path.normalize(objectKey).replace(/\\/g, '/')
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
1
|
+
const Promise = require('bluebird')
|
|
2
|
+
const fs = Promise.promisifyAll(require('fs'))
|
|
3
|
+
const path = require('path')
|
|
4
|
+
|
|
5
|
+
function getFiles (dirs, includeRootDir = true, ignores) {
|
|
6
|
+
if (typeof dirs === 'string') dirs = [dirs]
|
|
7
|
+
if (ignores instanceof RegExp) ignores = [ignores]
|
|
8
|
+
|
|
9
|
+
const res = []
|
|
10
|
+
dirs.forEach(dir => deep(dir, dir))
|
|
11
|
+
|
|
12
|
+
function deep (dir, rootDir) {
|
|
13
|
+
const files = fs.readdirSync(dir)
|
|
14
|
+
files.forEach(fileName => {
|
|
15
|
+
const pathName = `${dir}${path.sep}${fileName}`
|
|
16
|
+
const info = fs.lstatSync(pathName)
|
|
17
|
+
if (info.isDirectory()) {
|
|
18
|
+
deep(pathName, rootDir)
|
|
19
|
+
} else {
|
|
20
|
+
if (!ignores || ignores.every(ignore => !ignore.test(pathName))) {
|
|
21
|
+
res.push({
|
|
22
|
+
path: pathName,
|
|
23
|
+
relative: path.relative(includeRootDir ? path.join(rootDir, '../') : rootDir, pathName)
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return res
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeObjectKey (objectKey) {
|
|
34
|
+
return path.normalize(objectKey).replace(/\\/g, '/')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function cleanupFiles (dirs, keepRules = [/\.html$/]) {
|
|
38
|
+
if (typeof dirs === 'string') dirs = [dirs]
|
|
39
|
+
if (!(keepRules instanceof Array)) keepRules = [keepRules]
|
|
40
|
+
|
|
41
|
+
const shouldKeep = (filePath) => keepRules.some((rule) => rule.test(filePath))
|
|
42
|
+
|
|
43
|
+
function cleanDir (dir) {
|
|
44
|
+
if (!fs.existsSync(dir)) return
|
|
45
|
+
const entries = fs.readdirSync(dir)
|
|
46
|
+
entries.forEach((entryName) => {
|
|
47
|
+
const entryPath = path.join(dir, entryName)
|
|
48
|
+
const stat = fs.lstatSync(entryPath)
|
|
49
|
+
if (stat.isDirectory()) {
|
|
50
|
+
cleanDir(entryPath)
|
|
51
|
+
const remain = fs.readdirSync(entryPath)
|
|
52
|
+
if (remain.length === 0) fs.rmdirSync(entryPath)
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
if (!shouldKeep(entryPath)) fs.unlinkSync(entryPath)
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
dirs.forEach((dir) => {
|
|
60
|
+
try {
|
|
61
|
+
cleanDir(dir)
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.warn(`cleanupFiles warn: failed to clean "${dir}", error: ${error.message || error}`)
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = {
|
|
69
|
+
getFiles,
|
|
70
|
+
normalizeObjectKey,
|
|
71
|
+
cleanupFiles,
|
|
72
|
+
noop () {}
|
|
73
|
+
}
|
package/src/vite.js
CHANGED
|
@@ -1,46 +1,137 @@
|
|
|
1
|
-
const fetch = require('node-fetch')
|
|
2
|
-
const { getFiles } = require('./utils')
|
|
3
|
-
const { TaskDispatcher } = require('./task')
|
|
4
|
-
const NosUploadTask = require('./adaptor/nos')
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
1
|
+
const fetch = require('node-fetch')
|
|
2
|
+
const { getFiles, cleanupFiles } = require('./utils')
|
|
3
|
+
const { TaskDispatcher } = require('./task')
|
|
4
|
+
const NosUploadTask = require('./adaptor/nos')
|
|
5
|
+
|
|
6
|
+
function isContentHashedFile (relativePath) {
|
|
7
|
+
// e.g. assets/index-abc12345.js / css / png / svg...
|
|
8
|
+
return /-[a-zA-Z0-9_]{8,}\.[^/]+$/.test(relativePath)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function shouldUploadFile (domain, namespace, file) {
|
|
12
|
+
if (!isContentHashedFile(file.relative)) return true
|
|
13
|
+
const url = `${domain}/${namespace}/${file.relative}`
|
|
14
|
+
try {
|
|
15
|
+
const headResp = await fetch(url, { method: 'HEAD' })
|
|
16
|
+
// hashed file already exists remotely, can be safely skipped
|
|
17
|
+
return !headResp.ok
|
|
18
|
+
} catch (error) {
|
|
19
|
+
// network uncertainty -> upload to be safe
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function mapWithConcurrency (items, concurrency, mapper) {
|
|
25
|
+
const size = Math.max(1, Number(concurrency) || 1)
|
|
26
|
+
const results = new Array(items.length)
|
|
27
|
+
let cursor = 0
|
|
28
|
+
async function worker () {
|
|
29
|
+
while (cursor < items.length) {
|
|
30
|
+
const index = cursor++
|
|
31
|
+
results[index] = await mapper(items[index], index)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const workers = Array.from({ length: Math.min(size, items.length || 1) }, () => worker())
|
|
35
|
+
await Promise.all(workers)
|
|
36
|
+
return results
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {Object} options
|
|
41
|
+
* @param {Array|String} dirs 待上传的文件目录
|
|
42
|
+
* @param {Boolean} includeRootDir 文件的相对路径计算时包不包含根目录,默认 true
|
|
43
|
+
* @param {RegExp|Array<RegExp>} ignores 排除特定文件
|
|
44
|
+
* @param {Object} client 上传 client 配置,目前只支持 NosClient,不同的 client 配置参数可能不同,以 nos 举例
|
|
45
|
+
* @param {String} accessId
|
|
46
|
+
* @param {String} secretKey
|
|
47
|
+
* @param {String} bucket 桶名
|
|
48
|
+
* @param {String} namespace 不同项目在使用同一个桶时做隔离
|
|
49
|
+
* @param {Number} parallelCount 并行上传数, 默认 3
|
|
50
|
+
* @param {Boolean} incrementalUpload 是否开启增量上传(仅对 content-hash 文件生效),默认 false
|
|
51
|
+
* @param {Boolean} cleanupAfterUpload 上传成功后是否清理本地静态资源,默认 false
|
|
52
|
+
* @param {RegExp|Array<RegExp>} cleanupKeepRules 清理后保留规则,默认仅保留 html
|
|
53
|
+
* @returns
|
|
54
|
+
*/
|
|
55
|
+
function CdnUploadVitePlugin (options) {
|
|
56
|
+
const {
|
|
57
|
+
dirs,
|
|
58
|
+
includeRootDir,
|
|
59
|
+
ignores,
|
|
60
|
+
client,
|
|
61
|
+
parallelCount,
|
|
62
|
+
precheckParallelCount = parallelCount ?? 3,
|
|
63
|
+
domain = 'https://yidunfe.nosdn.127.net',
|
|
64
|
+
incrementalUpload = false,
|
|
65
|
+
cleanupAfterUpload = false,
|
|
66
|
+
cleanupKeepRules = [/\.html$/]
|
|
67
|
+
} = options
|
|
68
|
+
let buildStartAt = 0
|
|
69
|
+
return {
|
|
70
|
+
name: 'vite-plugin-cdn-upload',
|
|
71
|
+
buildStart () {
|
|
72
|
+
buildStartAt = Date.now()
|
|
73
|
+
},
|
|
74
|
+
async closeBundle () {
|
|
75
|
+
const uploadStageStart = Date.now()
|
|
76
|
+
const files = getFiles(dirs, includeRootDir, ignores)
|
|
77
|
+
let filesToUpload = files
|
|
78
|
+
if (incrementalUpload) {
|
|
79
|
+
const precheckStart = Date.now()
|
|
80
|
+
const uploadFlags = await mapWithConcurrency(
|
|
81
|
+
files,
|
|
82
|
+
precheckParallelCount,
|
|
83
|
+
(file) => shouldUploadFile(domain, client.namespace || '', file)
|
|
84
|
+
)
|
|
85
|
+
filesToUpload = files.filter((_, index) => uploadFlags[index])
|
|
86
|
+
const skippedCount = files.length - filesToUpload.length
|
|
87
|
+
const precheckCost = Date.now() - precheckStart
|
|
88
|
+
console.log(`CdnUploadVitePlugin: precheck done, total=${files.length}, upload=${filesToUpload.length}, skipped=${skippedCount}, cost=${precheckCost}ms`)
|
|
89
|
+
if (skippedCount > 0) {
|
|
90
|
+
console.log(`CdnUploadVitePlugin: skip upload ${skippedCount} hashed files already on CDN`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (filesToUpload.length === 0) {
|
|
94
|
+
if (cleanupAfterUpload) {
|
|
95
|
+
cleanupFiles(dirs, cleanupKeepRules)
|
|
96
|
+
console.log('CdnUploadVitePlugin: local static assets cleaned')
|
|
97
|
+
}
|
|
98
|
+
const totalCost = Date.now() - uploadStageStart
|
|
99
|
+
console.log(`CdnUploadVitePlugin: no files need upload, total upload stage cost=${totalCost}ms`)
|
|
100
|
+
if (buildStartAt > 0) {
|
|
101
|
+
console.log(`CdnUploadVitePlugin: build+upload total cost=${Date.now() - buildStartAt}ms`)
|
|
102
|
+
}
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
const tasks = filesToUpload.map((file) => new NosUploadTask(client, file))
|
|
106
|
+
const uploadStart = Date.now()
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
const taskDispatcher = new TaskDispatcher(tasks, {
|
|
109
|
+
onSuccess: async (task) => {
|
|
110
|
+
// cdn 上传成功后健康检查
|
|
111
|
+
const { _file, _namespace } = task?.[0] || {}
|
|
112
|
+
const data = await fetch(`${domain}/${_namespace}/${_file.relative}`)
|
|
113
|
+
if (!data.ok || data.status !== 200) {
|
|
114
|
+
return reject(new Error(`CdnUploadVitePlugin Error: url: ${data.url}, status: ${data.status}`))
|
|
115
|
+
}
|
|
116
|
+
if (cleanupAfterUpload) {
|
|
117
|
+
cleanupFiles(dirs, cleanupKeepRules)
|
|
118
|
+
console.log('CdnUploadVitePlugin: local static assets cleaned')
|
|
119
|
+
}
|
|
120
|
+
const uploadCost = Date.now() - uploadStart
|
|
121
|
+
const totalCost = Date.now() - uploadStageStart
|
|
122
|
+
console.log(`CdnUploadVitePlugin: upload cost=${uploadCost}ms, total upload stage cost=${totalCost}ms`)
|
|
123
|
+
if (buildStartAt > 0) {
|
|
124
|
+
console.log(`CdnUploadVitePlugin: build+upload total cost=${Date.now() - buildStartAt}ms`)
|
|
125
|
+
}
|
|
126
|
+
console.log('CdnUploadVitePlugin: assets upload success')
|
|
127
|
+
resolve()
|
|
128
|
+
},
|
|
129
|
+
onError: (error) => reject(error)
|
|
130
|
+
})
|
|
131
|
+
console.log('CdnUploadVitePlugin: assets uploading ...')
|
|
132
|
+
taskDispatcher.dispatch(parallelCount)
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
module.exports = CdnUploadVitePlugin
|