Skip to content

Commit

Permalink
Allow reload after changes in imported files (#124)
Browse files Browse the repository at this point in the history
This changes do several things:

1. Fix the cache clearing after modifications in imported files. The
fs-provider uses the file path. The cache uses the URL. I have added
some logic to generate the URL from the path. This URL can then be used
to clear the cache.
2. Implement logic for clearing the cache in indirectly imported files.
The new Map requireCache.knownDependencies contains a list of all files
that are imported for a specific file. This information is used to
recursively clear the caches of other modules when one of its
dependencies has changed.
3. Add more JSDoc Type annotations
4. Some minor syntax cleanups (let => const, || => ??, self=>globalThis,
...)

This updates for changes in the main file are still broken due to a bug
in a prior commit.

---------

Co-authored-by: Davor Hrg <[email protected]>
  • Loading branch information
Kaladum and hrgdavor authored Oct 22, 2024
1 parent 72393cb commit 2d0c630
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 29 deletions.
2 changes: 1 addition & 1 deletion apps/engine-test/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
}
Expand Down
9 changes: 5 additions & 4 deletions apps/jscad-web/main.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
addToCache,
analyzeProject,
clearCache,
clearFs,
extractEntries,
fileDropped,
Expand Down Expand Up @@ -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 })
}
Expand All @@ -121,8 +122,6 @@ document.body.ondrop = async ev => {
await resetFileRefs()
if (!sw) await initFs()
showDrop(false)
workerApi.jscadClearTempCache()

await fileDropped(sw, files)

reloadProject()
Expand All @@ -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)
Expand Down Expand Up @@ -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 })
Expand Down
1 change: 0 additions & 1 deletion packages/fs-provider/fs-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,6 @@ export const checkFiles = async sw => {
}

// TODO clear sw cache
// TODO sendCmd jscadClearFileCache {files}
}
requestAnimationFrame(() => checkFiles(sw))
}
Expand Down
97 changes: 76 additions & 21 deletions packages/require/src/require.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -44,36 +60,40 @@ 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')) {
// jsdelivr will read package.json and tell us what the main file is
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
}
}
Expand All @@ -93,23 +113,23 @@ 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/[email protected]/index.js uses require to read package.json
if (extension === 'json') {
exports = JSON.parse(source)
} else {
// 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
// import jscad from "@jscad/modeling";
// 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
}
}

Expand All @@ -132,14 +152,38 @@ const requireModule = (id, url, source, _require) => {
}
}

/**
* @typedef ClearFileCacheOptions
* @prop {Array<String>} 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())
}
}
}

/**
Expand All @@ -150,9 +194,20 @@ export const jscadClearTempCache = () => {
requireCache.alias = {}
}


/**
* @type {{
* local:Object.<string,Object>
* alias:Object.<string,string>
* module:Object.<string,Object>
* bundleAlias:Object.<string,string>
* knownDependencies:Map.<string,Set<string>>
* }}
*/
export const requireCache = {
local: {},
alias: {},
module: {},
bundleAlias: {},
knownDependencies: new Map(),
}
3 changes: 1 addition & 2 deletions packages/worker/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ import { extractPathInfo, readAsArrayBuffer, readAsText } from '../fs-provider/f
@typedef ExportDataOptions
@prop {string} format
@typedef ClearFileCacheOptions
@prop {Array<String>} files
@typedef {import('@jscadui/require').ClearFileCacheOptions} ClearFileCacheOptions
@typedef RunMainOptions
@prop {Object} params
Expand Down

0 comments on commit 2d0c630

Please sign in to comment.