Repository: liyuec/webSite_Update_Detector Branch: main Commit: 99215ef26971 Files: 13 Total size: 36.3 KB Directory structure: gitextract_gtr080x6/ ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── build/ │ ├── rollup.config.js │ └── rollup.link.config.js ├── core/ │ ├── config.js │ ├── detector.js │ ├── worker.js │ ├── workerOfFunc copy.js │ └── workerOfFunc.js ├── index.js └── package.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .DS_Store node_modules /dist # local env files .env.local .env.*.local # Log files npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: .npmrc ================================================ registry=https://registry.npmjs.org ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 老实的切图Man Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # webSite_Update_Detector if web site (spa) updated,we can discover ================================================ FILE: build/rollup.config.js ================================================ import {terser} from "rollup-plugin-minification"; import resolve from '@rollup/plugin-node-resolve'; import common from 'rollup-plugin-commonjs'; import babel from '@rollup/plugin-babel'; import clearDirectory from 'rollup-plugin-clear-directory'; console.log('****************rollup.config.js******************') export default{ input:'index.js', output:[ { file:'./dist/index.esm.js', format:'esm', //sourcemap:true exports:'named' }, { file:'./dist/index.umd.js', format:'umd', name:'detectorumd' } ], plugins:[ clearDirectory({ targets: ['dist'] } ), common(), resolve(), terser() ] } ================================================ FILE: build/rollup.link.config.js ================================================ import resolve from '@rollup/plugin-node-resolve'; import common from 'rollup-plugin-commonjs'; import clearDirectory from 'rollup-plugin-clear-directory'; console.log('****************rollup.link.config.js******************') export default{ input:'index.js', output:[ { file:'./dist/index.esm.js', format:'esm', //sourcemap:true exports:'named' }, { file:'./dist/index.umd.js', format:'umd', name:'detectorumd' } ], plugins:[ clearDirectory({ targets: ['dist'] } ), common(), resolve() ] } ================================================ FILE: core/config.js ================================================ const baseConfig = { //1分钟 minInterval:(1000 * 60), //间隔轮询 interval:10000, //是否检查当前轮询时间,自动间隔 isCheckWaitTime:true, //获取的站点 http || https + host checkSiteHost:'', //每次检查完毕,用户无论干嘛,都增加这个时长 intervalAddTime:30000, //最大间隔检查上线 maxInterval:1000 * 60, //检查什么 checkWho:['script','css'] } class baseClass{ constructor(ops){ this.interval = ops.interval === void 0 ? baseConfig.interval : ops.interval; this.isCheckWaitTime = ops.isCheckWaitTime === void 0 ? baseConfig.isCheckWaitTime : ops.isCheckWaitTimes; this.checkSiteHost = ops.checkSiteHost; this.intervalAddTime = ops.intervalAddTime === void 0 ? baseConfig.intervalAddTime : ops.intervalAddTime; this.maxInterval = ops.maxInterval === void 0 ? baseConfig.maxInterval : ops.maxInterval; this.checkWho = ops.checkWho === void 0 ? baseConfig.checkWho : ops.checkWho; } } const channelName = 'webUpdateDetector' const channelMsg = { //开始 begin:channelName + ':begin', upgradation:channelName + ":upgradation" } const localEnum = { detectorName:'localDetector' } const interval = 2000; const expireInterval = 4500; export { baseConfig, baseClass, channelName, channelMsg, localEnum, interval, expireInterval } ================================================ FILE: core/detector.js ================================================ import {baseConfig,baseClass,channelName,channelMsg,localEnum,interval,expireInterval} from './config'; import {getWorker} from './worker'; function extractLinksAndScripts(){ const result = { _css: [], _script: [] }; const links = document.getElementsByTagName('link'); for (let i = 0; i < links.length; i++) { const link = links[i]; const href = link.getAttribute('href'); if (href && (href.endsWith('.css') || href.endsWith('.CSS'))) { result._css.push(href); } if (href && (href.endsWith('.js') || href.endsWith('.JS'))) { result._script.push(href); } } const scripts = document.getElementsByTagName('script'); for (let i = 0; i < scripts.length; i++) { const script = scripts[i]; const src = script.getAttribute('src'); if (src && (src.endsWith('.js') || src.endsWith('.JS'))) { result._script.push(src); } } return result; } /** * 暂不考虑不支持webWorker的情况 */ class detector extends baseClass{ #isStart = false; #worker = void 0; #scripts = null; constructor(opts){ super(opts) this.#isStart = false; this.callBack = opts.callBack; } start(){ if(document.readyState !== 'complete'){ setTimeout(()=>{ this.start() },interval); return; } if(!this.#isStart){ this.#isStart = true; this.#worker = getWorker({ callBack:this.callBack }); if(this.#scripts === null){ this.#scripts = extractLinksAndScripts(); } this.#worker.postMessage({ checkSiteHost:this.checkSiteHost, domSccripts:this.#scripts._script, domCsss:this.#scripts._css, checkWho:this.checkWho, msg:'start', interval:this.interval, intervalAddTime:this.intervalAddTime, maxInterval:this.maxInterval }) window.addEventListener("beforeunload", (e) => { this.#worker.postMessage({ msg:'beforeunload' }) }) } } stop(){ this.#isStart = false; this.#worker.postMessage({msg:'stop'}) } } export default detector ================================================ FILE: core/worker.js ================================================ import {channelMsg,localEnum} from './config'; function getWorkerBlobStr(){ let js = ` self.onmessage = function(e){ let {checkSiteHost,domSccripts,domCsss,checkWho,msg,interval,intervalAddTime,maxInterval} = e.data, htmlText = '', errText = '', timeId = null, suminterval = interval, sendChannelIntervalId = null; let expireInterval = 3000, myStartTimeInterval = 4000, /** * channelIntervals * expireTime */ otherChannelIntervals = []; function getWebSite(){ return new Promise((r,rj)=>{ let _checkSiteHost = checkSiteHost + '?timekey=' + new Date().getTime() fetch(_checkSiteHost).then(res=>{ return res.text(); }).then(_htmlText =>{ htmlText = _htmlText; r('ok') }).catch(err=>{ errText = err; console.warn('get web site error:',errText) rj('wrong') }) }) } function extractLinksAndScripts(htmlString) { const css = []; const js = []; // 提取 CSS 链接 let startIndex = 0; while (startIndex < htmlString.length) { const linkStartIndex = htmlString.indexOf('= expireInterval){ _myStartTimeInterval = min + 500; }else{ _myStartTimeInterval = max + 500; } */ _myStartTimeInterval = max + 500; } isMyChange = true; //console.error('替换后的 _myStartTimeInterval:',_myStartTimeInterval,result.toString()) myStartTimeInterval = _myStartTimeInterval; sendChannelInterval(); } function setChannelIntervals(channelIntervals,expireTime,type){ if(type === channelMsg.begin){ otherChannelIntervals = []; } //console.log('otherChannelIntervals length:',otherChannelIntervals.length) if(Object.prototype.toString.call(channelIntervals).slice(8,-1) === 'Array'){ channelIntervals.forEach(i=>{ otherChannelIntervals.push({ channelIntervals:i.channelIntervals, expireTime:i.expireTime }); }) }else{ otherChannelIntervals.push({ channelIntervals:channelIntervals, expireTime:expireTime }); } } /** * 超过3秒没更新,算过期,因为每秒都要广播; */ function getChannelIntervals(){ let _otherChannelIntervals = otherChannelIntervals, res = [], min = void 0, max = void 0, result = [], now = Date.now(); _otherChannelIntervals.forEach(i=>{ //if(now - i.expireTime <= 3000){ res.push(i) result.push(i.channelIntervals) //} }) otherChannelIntervals = JSON.parse(JSON.stringify(res)); min = Math.min(...result); max = Math.max(...result); return { result, min, max } } const channelName = 'webUpdateDetector' const channelMsg = { //开始 begin:channelName + ':begin', interval:channelName + ':interval', script_diff:channelName + ':script_diff', css_diff:channelName + ':css_diff', no_file_update:channelName + ':no_file_update', error:channelName + ':error' } var _channel = getChannel(); let SlaveTimerId = null; let setIntervalId = null; function getChannel(opts){ const _channnel = new BroadcastChannel(channelName); _channnel.onmessage = function(_messageEvent){ let {msg,channelIntervals,expireTime} = _messageEvent.data; switch(msg){ //其他tab已经有请求了 case channelMsg.begin: clearTimeout(SlaveTimerId); clearInterval(setIntervalId); setChannelIntervals(channelIntervals,expireTime,channelMsg.begin); myStartTimeIntervalChange(); SlaveTimerId = setTimeout(() => { start(); }, myStartTimeInterval); //console.log('其他tab已经有请求了:',myStartTimeInterval,msg,channelIntervals,expireTime,otherChannelIntervals) break; //记录其他channel的存活情况; case channelMsg.interval: setChannelIntervals(channelIntervals,expireTime,channelMsg.interval); myStartTimeIntervalChange(); //console.log('记录其他channel的存活情况:',myStartTimeInterval,msg,channelIntervals,expireTime,otherChannelIntervals) break; case channelMsg.script_diff: self.postMessage({ update:1, msg:'script_diff' }) break; case channelMsg.css_diff: self.postMessage({ update:1, msg:'css_diff' }) break; case channelMsg.no_file_update: self.postMessage({ update:0, msg:'no_file_update' }) break; case channelMsg.error: self.postMessage({ update:-1, msg:'error:' + err }) break; } } return _channnel; } function sendChannelInterval(isInterval = false){ function send(){ _channel.postMessage({ msg:channelMsg.interval, //channelIntervals:otherChannelIntervals.length > 0 ? otherChannelIntervals : myStartTimeInterval, channelIntervals:myStartTimeInterval, expireTime:Date.now() }) } if(isInterval){ sendChannelIntervalId = setInterval(()=>{ send(); },2000); }else{ send(); } } let isSendChannel = false; function sendChannel(){ function send(){ _channel.postMessage({ msg:channelMsg.begin, channelIntervals:otherChannelIntervals.length > 0 ? otherChannelIntervals : myStartTimeInterval, expireTime:Date.now() }) setTimeout(send,2000) } if(!isSendChannel){ send(); } /* setIntervalId = setInterval(()=>{ send(); },2000); */ } function start(){ if(!!timeId){ suminterval = suminterval <= maxInterval ? suminterval += intervalAddTime : suminterval; } sendChannel(); getWebSite().then(res=>{ let extractObj = extractLinksAndScripts(htmlText), diffResult = null, siteObj = { _script:[], _css:[] }; siteObj._script = extractObj.js; siteObj._css = extractObj.css; if(checkWho.includes('script')){ diffResult = differentValue(domSccripts,siteObj._script); if(diffResult !== null){ _channel.postMessage({ msg:channelMsg.script_diff }) self.postMessage({ update:1, msg:'script_diff' }) return; } } if(checkWho.includes('css')){ diffResult = differentValue(domCsss,siteObj._css); if(diffResult !== null){ _channel.postMessage({ msg:channelMsg.css_diff }) self.postMessage({ update:1, msg:'css_diff' }) return; } } timeId = setTimeout(()=>{ _channel.postMessage({ msg:channelMsg.no_file_update }) self.postMessage({ update:0, msg:'no_file_update' }) start(); },suminterval) }).catch(err=>{ _channel.postMessage({ msg:channelMsg.error }) self.postMessage({ update:-1, msg:'error:' + err }) }) } switch(msg){ case 'start': //这里直接频道判断,因为浏览器不会保存之前的消息; /* setIntervalId = setInterval(()=>{ _channel.postMessage({ msg:channelMsg.begin }) },1000); */ SlaveTimerId = setTimeout(()=>{ start(); },expireInterval) //start(); break; case 'stop': clearTimeout(timeId); break; case 'beforeunload': clearInterval(setIntervalId); _channel.close(); break; } } `, _blob = new Blob([js],{ type: 'application/javascript' }); return window.URL.createObjectURL(_blob); } function createWorker(opts){ let _worker = Object.create(null), callBack = opts.callBack, channel = opts.channel; if(window.Worker){ _worker = new Worker(getWorkerBlobStr(),{ type:'classic', credentials:'omit', name:'webDetectorWorker' }) /* let workerUri = new URL('./workerOfFunc.js',import.meta.url) _worker = new Worker(workerUri.toString(),{ type:'classic', credentials:'omit', name:'webDetectorWorker' }) */ }else{ console.error('你的浏览器不支持web worker;web worker支持IE10+') } _worker.addEventListener("message", function (e) { let {update,msg} = e.data; switch(update){ case -1: channel.postMessage(channelMsg.upgradation) callBack({ update:update, msg:msg }) break; case 0: callBack({ update:update, msg:msg }) break; case 1: //有不一致的情况; callBack({ update:update, msg:msg }) break; } }) _worker.addEventListener("error", function (e,event) { console.error(`__worker error:`,e,event) }) _worker.addEventListener("messageerror", function (e,event) { console.error(`__worker error:`,e,event) }) return _worker; } const getWorker = (function(){ let worker = null; /** * type :预留 */ return function(opts,type = 'default'){ if(worker === null){ worker = new createWorker(opts) } return worker; } })() export { getWorker } ================================================ FILE: core/workerOfFunc copy.js ================================================ /* const _regexLinkStr = ']+href=[\'"]([^\'"]+\\.(css))[\'"][^>]*>', _regexLinkJsStr = ']+href=[\'"]([^\'"]+\\.(js))[\'"][^>]*>', _regexScriptStr = ']+src=[\'"]([^"\']+)["\']'; */ self.onmessage = function(e){ let {checkSiteHost,domSccripts,domCsss,checkWho,msg,interval,intervalAddTime,maxInterval} = e.data, htmlText = '', errText = '', timeId = null, suminterval = interval; function getWebSite(){ return new Promise((r,rj)=>{ fetch(checkSiteHost).then(res=>{ return res.text(); }).then(_htmlText =>{ htmlText = _htmlText; r('ok') }).catch(err=>{ errText = err; console.warn('get web site error:',errText) rj('wrong') }) }) } function extractLinksAndScripts(str) { str = str.replaceAll('\x3C','<'); str = str.replaceAll('async',' '); str = str.replaceAll('defer',' '); const _regexLinkStr = ']+href=[\\'"]([^\\'"]+\\\\.(css))[\\'"][^>]*>', _regexLinkJsStr = ']+href=[\\'"]([^\\'"]+\\\\.(js))[\\'"][^>]*>', ']+src=[\\'"]([^\']+)["\']' _regexScriptStr = ']+src=[\\'"]([^\\']+)[\\']', regexLinkStr = new RegExp(_regexLinkStr, 'g'), regexLinkJsStr = new RegExp(_regexLinkJsStr, 'g'), regexScriptStr = new RegExp(_regexScriptStr, 'g'), matchStr = regexLinkStr.exec(str), matchLinkJsStr = regexLinkJsStr.exec(str), matchScriptStr = regexScriptStr.exec(str), result = { _css: [], _script: [] }; if(matchStr !== null){ matchStr.forEach(i => { result._css.push(i); }); } if(matchLinkJsStr !== null){ matchLinkJsStr.forEach(i => { result._script.push(i); }); } if(matchScriptStr !== null){ matchScriptStr.forEach(i => { if(i !== 'js'){ result._script.push(i); } }); } return result; } function differentValue(arr1, arr2) { var targetArray = arr1.length <= arr2.length ? arr1 : arr2; var otherArray = targetArray === arr1 ? arr2 : arr1; for (var i = 0; i < targetArray.length; i++) { if (targetArray[i] !== otherArray[i]) { return targetArray[i]; } } return null; } function start(){ if(!!timeId){ suminterval <= maxInterval ? suminterval += intervalAddTime : suminterval; } getWebSite().then(res=>{ let siteObj = extractLinksAndScripts(htmlText), diffResult = null; if(checkWho.includes('script')){ diffResult = differentValue(domSccripts,siteObj._script); if(diffResult !== null){ self.postMessage({ update:1, msg:'script_diff' }) return; } } if(checkWho.includes('css')){ diffResult = differentValue(domCsss,siteObj._css); if(diffResult !== null){ self.postMessage({ update:1, msg:'css_diff' }) return; } } timeId = setTimeout(()=>{ self.postMessage({ update:0, msg:'no_file_update' }) },suminterval) }).catch(err=>{ self.postMessage({ update:-1, msg:'error:' + err }) }) } switch(msg){ case 'start': start(); break; case 'stop': clearTimeout(timeId); break; } } ================================================ FILE: core/workerOfFunc.js ================================================ self.onmessage = function(e){ let {checkSiteHost,domSccripts,domCsss,checkWho,msg,interval,intervalAddTime,maxInterval} = e.data, htmlText = '', errText = '', timeId = null, suminterval = interval, sendChannelIntervalId = null; let expireInterval = 3000, myStartTimeInterval = 4000, /** * channelIntervals * expireTime */ otherChannelIntervals = []; function getWebSite(){ return new Promise((r,rj)=>{ let _checkSiteHost = checkSiteHost + '?timekey=' + new Date().getTime() fetch(_checkSiteHost).then(res=>{ return res.text(); }).then(_htmlText =>{ htmlText = _htmlText; r('ok') }).catch(err=>{ errText = err; console.warn('get web site error:',errText) rj('wrong') }) }) } function extractLinksAndScripts(htmlString) { const css = []; const js = []; // 提取 CSS 链接 let startIndex = 0; while (startIndex < htmlString.length) { const linkStartIndex = htmlString.indexOf('= expireInterval){ _myStartTimeInterval = min + 500; }else{ _myStartTimeInterval = max + 500; } */ _myStartTimeInterval = max + 500; } isMyChange = true; //console.error('替换后的 _myStartTimeInterval:',_myStartTimeInterval,result.toString()) myStartTimeInterval = _myStartTimeInterval; sendChannelInterval(); } function setChannelIntervals(channelIntervals,expireTime,type){ if(type === channelMsg.begin){ otherChannelIntervals = []; } //console.log('otherChannelIntervals length:',otherChannelIntervals.length) if(Object.prototype.toString.call(channelIntervals).slice(8,-1) === 'Array'){ channelIntervals.forEach(i=>{ otherChannelIntervals.push({ channelIntervals:i.channelIntervals, expireTime:i.expireTime }); }) }else{ otherChannelIntervals.push({ channelIntervals:channelIntervals, expireTime:expireTime }); } } /** * 超过3秒没更新,算过期,因为每秒都要广播; */ function getChannelIntervals(){ let _otherChannelIntervals = otherChannelIntervals, res = [], min = void 0, max = void 0, result = [], now = Date.now(); _otherChannelIntervals.forEach(i=>{ //if(now - i.expireTime <= 3000){ res.push(i) result.push(i.channelIntervals) //} }) otherChannelIntervals = JSON.parse(JSON.stringify(res)); min = Math.min(...result); max = Math.max(...result); return { result, min, max } } const channelName = 'webUpdateDetector' const channelMsg = { //开始 begin:channelName + ':begin', interval:channelName + ':interval', script_diff:channelName + ':script_diff', css_diff:channelName + ':css_diff', no_file_update:channelName + ':no_file_update', error:channelName + ':error' } var _channel = getChannel(); let SlaveTimerId = null; let setIntervalId = null; function getChannel(opts){ const _channnel = new BroadcastChannel(channelName); _channnel.onmessage = function(_messageEvent){ let {msg,channelIntervals,expireTime} = _messageEvent.data; switch(msg){ //其他tab已经有请求了 case channelMsg.begin: clearTimeout(SlaveTimerId); clearInterval(setIntervalId); setChannelIntervals(channelIntervals,expireTime,channelMsg.begin); myStartTimeIntervalChange(); SlaveTimerId = setTimeout(() => { start(); }, myStartTimeInterval); //console.log('其他tab已经有请求了:',myStartTimeInterval,msg,channelIntervals,expireTime,otherChannelIntervals) break; //记录其他channel的存活情况; case channelMsg.interval: setChannelIntervals(channelIntervals,expireTime,channelMsg.interval); myStartTimeIntervalChange(); //console.log('记录其他channel的存活情况:',myStartTimeInterval,msg,channelIntervals,expireTime,otherChannelIntervals) break; case channelMsg.script_diff: self.postMessage({ update:1, msg:'script_diff' }) break; case channelMsg.css_diff: self.postMessage({ update:1, msg:'css_diff' }) break; case channelMsg.no_file_update: self.postMessage({ update:0, msg:'no_file_update' }) break; case channelMsg.error: self.postMessage({ update:-1, msg:'error:' + err }) break; } } return _channnel; } function sendChannelInterval(isInterval = false){ function send(){ _channel.postMessage({ msg:channelMsg.interval, //channelIntervals:otherChannelIntervals.length > 0 ? otherChannelIntervals : myStartTimeInterval, channelIntervals:myStartTimeInterval, expireTime:Date.now() }) } if(isInterval){ sendChannelIntervalId = setInterval(()=>{ send(); },2000); }else{ send(); } } let isSendChannel = false; function sendChannel(){ function send(){ _channel.postMessage({ msg:channelMsg.begin, channelIntervals:otherChannelIntervals.length > 0 ? otherChannelIntervals : myStartTimeInterval, expireTime:Date.now() }) setTimeout(send,2000) } if(!isSendChannel){ send(); } /* setIntervalId = setInterval(()=>{ send(); },2000); */ } function start(){ if(!!timeId){ suminterval = suminterval <= maxInterval ? suminterval += intervalAddTime : suminterval; } sendChannel(); getWebSite().then(res=>{ let extractObj = extractLinksAndScripts(htmlText), diffResult = null, siteObj = { _script:[], _css:[] }; siteObj._script = extractObj.js; siteObj._css = extractObj.css; if(checkWho.includes('script')){ diffResult = differentValue(domSccripts,siteObj._script); if(diffResult !== null){ _channel.postMessage({ msg:channelMsg.script_diff }) self.postMessage({ update:1, msg:'script_diff' }) return; } } if(checkWho.includes('css')){ diffResult = differentValue(domCsss,siteObj._css); if(diffResult !== null){ _channel.postMessage({ msg:channelMsg.css_diff }) self.postMessage({ update:1, msg:'css_diff' }) return; } } timeId = setTimeout(()=>{ _channel.postMessage({ msg:channelMsg.no_file_update }) self.postMessage({ update:0, msg:'no_file_update' }) start(); },suminterval) }).catch(err=>{ _channel.postMessage({ msg:channelMsg.error }) self.postMessage({ update:-1, msg:'error:' + err }) }) } switch(msg){ case 'start': //这里直接频道判断,因为浏览器不会保存之前的消息; /* setIntervalId = setInterval(()=>{ _channel.postMessage({ msg:channelMsg.begin }) },1000); */ SlaveTimerId = setTimeout(()=>{ start(); },expireInterval) //start(); break; case 'stop': clearTimeout(timeId); break; case 'beforeunload': clearInterval(setIntervalId); _channel.close(); break; } } ================================================ FILE: index.js ================================================ import detector from './core/detector' const createDetector = (function(){ let _detector = null; return function(opts){ if(_detector === null){ _detector = new detector(opts); } return _detector; } })() export default createDetector; ================================================ FILE: package.json ================================================ { "name": "website_update_detector", "version": "1.2.0", "description": "web前端主动发现站点发布更新 客户端更新 主动发现更新 发现更新", "private": false, "type": "module", "main": "./dist/index.umd.js", "module": "./dist/index.esm.js", "keywords": [ "web前端主动发现站点发布更新", " 客户端更新", "主动发现更新", "发现更新" ], "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "rolluplink": "npm run build:link", "rollupbuild": "npm run build:esm", "build:esm": "rollup -c ./build/rollup.config.js", "build:link": "rollup -c ./build/rollup.link.config.js" }, "repository": { "type": "git", "url": "git+https://github.com/liyuec/webSite_Update_Detector.git" }, "author": "liyuec", "license": "MIT", "browserslist": [ "> 1%", "last 2 versions", "not dead" ], "devDependencies": { "@rollup/plugin-babel": "^6.0.3", "@rollup/plugin-node-resolve": "^15.1.0", "rollup": "^3.26.2", "rollup-plugin-clear-directory": "^1.0.1", "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-copy": "^3.4.0", "rollup-plugin-minification": "^0.2.0", "rollup-plugin-postcss": "^4.0.2" }, "bugs": { "url": "https://github.com/liyuec/webSite_Update_Detector/issues" }, "homepage": "https://github.com/liyuec/webSite_Update_Detector#readme", "files": [ "dist" ], "publishConfig": { "access": "public" } }