diff --git a/apps/engine-test/main.js b/apps/engine-test/main.js index 994fda6..238dff5 100644 --- a/apps/engine-test/main.js +++ b/apps/engine-test/main.js @@ -246,7 +246,7 @@ async function initFs() { sw = await registerServiceWorker('bundle.fs-serviceworker.js?prefix=/swfs/') sw.defProjectName = 'jscad' sw.onfileschange = files => { - workerApi.jscadClearFileCache({ files }) + workerApi.jscadClearFileCache({ files, root: sw.base }) if (sw.fileToRun) jscadScript({ url: sw.fileToRun, base: sw.base }) } } diff --git a/apps/jscad-web/main.js b/apps/jscad-web/main.js index 3d0c680..f8eae4b 100644 --- a/apps/jscad-web/main.js +++ b/apps/jscad-web/main.js @@ -1,6 +1,7 @@ import { addToCache, analyzeProject, + clearCache, clearFs, extractEntries, fileDropped, @@ -101,7 +102,7 @@ async function initFs() { if (files.includes('/package.json')) { reloadProject() } else { - workerApi.jscadClearFileCache({ files }) + workerApi.jscadClearFileCache({ files, root: sw.base }) editor.filesChanged(files) if (sw.fileToRun) jscadScript({ url: sw.fileToRun, base: sw.base }) } @@ -121,8 +122,6 @@ document.body.ondrop = async ev => { await resetFileRefs() if (!sw) await initFs() showDrop(false) - workerApi.jscadClearTempCache() - await fileDropped(sw, files) reloadProject() @@ -134,6 +133,8 @@ document.body.ondrop = async ev => { } async function reloadProject() { + workerApi.jscadClearTempCache() + clearCache(sw.cache) saveMap = {} sw.filesToCheck = [] let { alias, script } = await analyzeProject(sw) @@ -383,7 +384,7 @@ editor.init( // it is expected if multiple files require same file/module that first time it is loaded // but for others resolved module is returned // if not cleared by calling jscadClearFileCache, require will not try to reload the file - await workerApi.jscadClearFileCache({ files: [path] }) + await workerApi.jscadClearFileCache({ files: [path] , root: sw.base}) if (sw.fileToRun) jscadScript({ url: sw.fileToRun, base: sw.base }) } else { jscadScript({ script }) diff --git a/packages/fs-provider/fs-provider.js b/packages/fs-provider/fs-provider.js index 9a606ec..b3e9e7e 100644 --- a/packages/fs-provider/fs-provider.js +++ b/packages/fs-provider/fs-provider.js @@ -346,7 +346,6 @@ export const checkFiles = async sw => { } // TODO clear sw cache - // TODO sendCmd jscadClearFileCache {files} } requestAnimationFrame(() => checkFiles(sw)) } diff --git a/packages/require/src/require.js b/packages/require/src/require.js index 7a38b64..1ab5efd 100644 --- a/packages/require/src/require.js +++ b/packages/require/src/require.js @@ -20,21 +20,37 @@ export { resolveUrl } from './resolveUrl' // we need eval to do the same without prefix // https://esbuild.github.io/content-types/#direct-eval // to be nice to bundlers we need indirect eval -// also self is not available in nodejs -export const runModule = (typeof self === 'undefined' ? eval : self.eval)( - '(require, exports, module, source)=>eval(source)', -) +export const runModule = globalThis.eval('(require, exports, module, source)=>eval(source)') +/** + * @typedef SourceWithUrl + * @prop {string} url + * @prop {string} script + */ + +/** + * + * @param {SourceWithUrl | string} urlOrSource + * @param {*} transform + * @param {(path:string,options?:{base:string,output:string})=>string} readFile + * @param {string} base + * @param {string} root + * @param {*} importData + * @param {*} moduleBase + * @returns + */ export const require = (urlOrSource, transform, readFile, base, root, importData = null, moduleBase = MODULE_BASE) => { + /** @type {string | undefined} */ let source + /** @type {string} */ let url let isRelativeFile let cache let cacheUrl let bundleAlias - if(typeof urlOrSource === 'string'){ + if (typeof urlOrSource === 'string') {//Only the URL is given url = urlOrSource - }else{ + } else { //URL and source are given (this is the main file) source = urlOrSource.script url = urlOrSource.url isRelativeFile = true @@ -44,27 +60,31 @@ export const require = (urlOrSource, transform, readFile, base, root, importData if (source === undefined) { bundleAlias = requireCache.bundleAlias[url] - const aliasedUrl = bundleAlias || requireCache.alias[url] || url + const aliasedUrl = bundleAlias ?? requireCache.alias[url] ?? url const resolved = resolveUrl(aliasedUrl, base, root, moduleBase) const resolvedStr = resolved.url.toString() - const arr = resolvedStr.split('/') + const urlComponents = resolvedStr.split('/') // no file ext is usually module from CDN - const isJs = !arr[arr.length-1].includes('.') || resolvedStr.endsWith('.ts') || resolvedStr.endsWith('.js') - if(!isJs && importData){ + const isJs = !urlComponents[urlComponents.length - 1].includes('.') || resolvedStr.endsWith('.ts') || resolvedStr.endsWith('.js') + if (!isJs && importData) { const info = extractPathInfo(resolvedStr) - let content = readFile(resolvedStr,{output: importData.isBinaryExt(info.ext)}) + const content = readFile(resolvedStr, { output: importData.isBinaryExt(info.ext) }) return importData.deserialize(info, content) } isRelativeFile = resolved.isRelativeFile resolvedUrl = resolved.url cacheUrl = resolved.url + requireCache.knownDependencies.get(base)?.add(cacheUrl)//Mark this module as a dependency of the base module - cache = requireCache[isRelativeFile ? 'local':'module'] + cache = requireCache[isRelativeFile ? 'local' : 'module'] exports = cache[cacheUrl] // get from cache if (!exports) { // not cached + + //Clear the known dependencies of the old version this module + requireCache.knownDependencies.set(cacheUrl, new Set()) try { source = readFile(resolvedUrl) if (resolvedUrl.includes('jsdelivr.net')) { @@ -72,8 +92,8 @@ export const require = (urlOrSource, transform, readFile, base, root, importData const srch = ' * Original file: ' let idx = source.indexOf(srch) if (idx != -1) { - const idx2 = source.indexOf('\n', idx+srch.length+1) - const realFile = new URL(source.substring(idx+srch.length, idx2), resolvedUrl).toString() + const idx2 = source.indexOf('\n', idx + srch.length + 1) + const realFile = new URL(source.substring(idx + srch.length, idx2), resolvedUrl).toString() resolvedUrl = base = realFile } } @@ -93,7 +113,7 @@ export const require = (urlOrSource, transform, readFile, base, root, importData } } if (source !== undefined) { - let extension = getExtension(resolvedUrl) + const extension = getExtension(resolvedUrl) // https://cdn.jsdelivr.net/npm/@jscad/svg-serializer@2.3.13/index.js uses require to read package.json if (extension === 'json') { exports = JSON.parse(source) @@ -101,7 +121,7 @@ export const require = (urlOrSource, transform, readFile, base, root, importData // do not transform bundles that are already cjs ( requireCache.bundleAlias.*) if (transform && !bundleAlias) source = transform(source, resolvedUrl).code // construct require function relative to resolvedUrl - let requireFunc = newUrl => require(newUrl, transform, readFile, resolvedUrl, root, importData, moduleBase) + const requireFunc = newUrl => require(newUrl, transform, readFile, resolvedUrl, root, importData, moduleBase) const module = requireModule(url, resolvedUrl, source, requireFunc) module.local = isRelativeFile exports = module.exports @@ -109,7 +129,7 @@ export const require = (urlOrSource, transform, readFile, base, root, importData // will be effectively transformed to // const jscad = require('@jscad/modeling').default // we need to plug-in default if missing - if(!('default' in exports)) exports.default = exports + if (!('default' in exports)) exports.default = exports } } @@ -132,14 +152,38 @@ const requireModule = (id, url, source, _require) => { } } +/** + * @typedef ClearFileCacheOptions + * @prop {Array} files + * @prop {string} root + */ + /** * Clear file cache for specific files. Used when a file has changed. + * @param {ClearFileCacheOptions} obj */ -export const clearFileCache = async ({files}) => { +export const clearFileCache = ({ files, root }) => { const cache = requireCache.local - files.forEach(f=>{ - delete cache[f] - }) + + /** + * @param {string} url + */ + const clearDependencies = (url) => { + delete cache[url] + const dependents = [...requireCache.knownDependencies.entries()].filter(([_, value]) => value.has(url)) + for (const [dependency, _] of dependents) { + clearDependencies(dependency) + } + } + + for (const file of files) { + delete cache[file] + if (root !== undefined) { + const path = file.startsWith("/") ? `.${file}` : file + const url = new URL(path, root) + clearDependencies(url.toString()) + } + } } /** @@ -150,9 +194,20 @@ export const jscadClearTempCache = () => { requireCache.alias = {} } + +/** + * @type {{ + * local:Object. + * alias:Object. + * module:Object. + * bundleAlias:Object. + * knownDependencies:Map.> + * }} + */ export const requireCache = { local: {}, alias: {}, module: {}, bundleAlias: {}, + knownDependencies: new Map(), } diff --git a/packages/worker/worker.js b/packages/worker/worker.js index e2dbaa1..d65785f 100644 --- a/packages/worker/worker.js +++ b/packages/worker/worker.js @@ -21,8 +21,7 @@ import { extractPathInfo, readAsArrayBuffer, readAsText } from '../fs-provider/f @typedef ExportDataOptions @prop {string} format - @typedef ClearFileCacheOptions - @prop {Array} files +@typedef {import('@jscadui/require').ClearFileCacheOptions} ClearFileCacheOptions @typedef RunMainOptions @prop {Object} params