@yidun/cdn-upload-webpack-plugin 1.1.6 → 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 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
- new CdnUploadWebpackPlugin(options)
20
- ```
21
-
22
- ### 以 vue-cli 项目举例
23
-
24
- ```
25
- configureWebpack: config => {
26
- if (process.env.NODE_ENV === 'production') {
27
- config.plugins.push(...[
28
- new CdnUploadWebpackPlugin({
29
- dirs: path.resolve(__dirname, './dist'),
30
- ignore: /.html$/,
31
- client: {
32
- bucket: '',
33
- accessId: '',
34
- secretKey: '',
35
- namespace: 'yidunfe/cdntest'
36
- }
37
- })
38
- ])
39
- }
40
- },
41
- ```
42
-
43
- ### adaptor
44
-
45
- 目前仅支持 nos 上传,后续如有其他的上传需求,可自行扩展 adaptor,可通过 client 参数区分
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.6",
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
+ }
@@ -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,57 +1,45 @@
1
- const fetch = require('node-fetch')
2
- const { getFiles } = require('./utils')
3
- const { TaskDispatcher } = require('./task')
4
- const NosUploadTask = require('./adaptor/nos')
5
- const CdnUploadVitePlugin = require('./vite')
6
-
7
- exports.CdnUploadWebpackPlugin = class CdnUploadWebpackPlugin {
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 {Number} parallelCount 并行上传数, 默认 3
19
- */
20
- constructor (options) {
21
- const { dirs, includeRootDir, ignores, client, parallelCount, domain = 'https://yidunfe.nosdn.127.net' } = options
22
- this._dirs = dirs
23
- this._includeRootDir = includeRootDir
24
- this._ignores = ignores
25
- this._client = client
26
- this._parallelCount = parallelCount
27
- this._domain = domain
28
- }
29
-
30
- apply (compiler) {
31
- compiler.hooks.afterEmit.tapAsync('CdnUploadWebpackPlugin', (compiler, callback) => {
32
- const files = getFiles(this._dirs, this._includeRootDir, this._ignores)
33
- const tasks = files.map(file => new NosUploadTask(this._client, file))
34
- const taskDispatcher = new TaskDispatcher(tasks, {
35
- onSuccess: async (task) => {
36
- if (!task) {
37
- callback(new Error('CdnUploadVitePlugin Error: task is empty'))
38
- return
39
- }
40
- // cdn 上传成功后健康检查
41
- const { _file, _namespace } = task[0] || {}
42
- const data = await fetch(`${this._domain}/${_namespace}/${_file.relative}`)
43
- if (!data.ok || data.status !== 200) {
44
- callback(new Error(`CdnUploadVitePlugin Error: url: ${data.url}, status: ${data.status}`))
45
- }
46
- console.log('CdnUploadVitePlugin: assets upload success')
47
- callback()
48
- },
49
- onError: error => callback(error)
50
- })
51
- console.log('CdnUploadWebpackPlugin: assets uploading ...')
52
- taskDispatcher.dispatch(this._parallelCount)
53
- })
54
- }
55
- }
56
-
57
- 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
- module.exports = {
38
- getFiles,
39
- normalizeObjectKey,
40
- noop () {}
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,50 +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
- * @param {Object} options
8
- * @param {Array|String} dirs 待上传的文件目录
9
- * @param {Boolean} includeRootDir 文件的相对路径计算时包不包含根目录,默认 true
10
- * @param {RegExp|Array<RegExp>} ignores 排除特定文件
11
- * @param {Object} client 上传 client 配置,目前只支持 NosClient,不同的 client 配置参数可能不同,以 nos 举例
12
- * @param {String} accessId
13
- * @param {String} secretKey
14
- * @param {String} bucket 桶名
15
- * @param {String} namespace 不同项目在使用同一个桶时做隔离
16
- * @param {Number} parallelCount 并行上传数, 默认 3
17
- * @returns
18
- */
19
- function CdnUploadVitePlugin (options) {
20
- const { dirs, includeRootDir, ignores, client, parallelCount, domain = 'https://yidunfe.nosdn.127.net' } = options
21
- return {
22
- name: 'vite-plugin-cdn-upload',
23
- closeBundle () {
24
- const files = getFiles(dirs, includeRootDir, ignores)
25
- const tasks = files.map((file) => new NosUploadTask(client, file))
26
- return new Promise((resolve, reject) => {
27
- const taskDispatcher = new TaskDispatcher(tasks, {
28
- onSuccess: async (task) => {
29
- if (!task) {
30
- reject(new Error('CdnUploadVitePlugin Error: task is empty'))
31
- return
32
- }
33
- // cdn 上传成功后健康检查
34
- const { _file, _namespace } = task[0] || {}
35
- const data = await fetch(`${domain}/${_namespace}/${_file.relative}`)
36
- if (!data.ok || data.status !== 200) {
37
- reject(new Error(`CdnUploadVitePlugin Error: url: ${data.url}, status: ${data.status}`))
38
- }
39
- console.log('CdnUploadVitePlugin: assets upload success')
40
- resolve()
41
- },
42
- onError: (error) => reject(error)
43
- })
44
- console.log('CdnUploadVitePlugin: assets uploading ...')
45
- taskDispatcher.dispatch(parallelCount)
46
- })
47
- }
48
- }
49
- }
50
- module.exports = CdnUploadVitePlugin
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