const { preprocess, compile } = require("svelte/compiler") const { dirname, relative } = require("path") const { promisify } = require("util") const { readFile, statSync, readFileSync } = require("fs") const convertMessage = ({ message, start, end, filename, frame }) => ({ text: message, location: start && end && { file: filename, line: start.line, column: start.column, length: start.line === end.line ? end.column - start.column : 0, lineText: frame, }, }) const SVELTE_FILTER = /\.svelte$/ const FAKE_CSS_FILTER = /\.esbuild-svelte-fake-css$/ module.exports = (options) => { return { name: "esbuild-svelte", setup(build) { if (!options) { options = {} } // see if we are incrementally building or watching for changes and enable the cache // also checks if it has already been defined and ignores this if it has if ( options.cache == undefined && (build.initialOptions.incremental || build.initialOptions.watch) ) { options.cache = true } // disable entry file generation by default if (options.fromEntryFile == undefined) { options.fromEntryFile = false } //Store generated css code for use in fake import const cssCode = new Map() const fileCache = new Map() //check and see if trying to load svelte files directly build.onResolve({ filter: SVELTE_FILTER }, ({ path, kind }) => { if (kind === "entry-point" && options?.fromEntryFile) { return { path, namespace: "esbuild-svelte-direct-import" } } }) //main loader build.onLoad( { filter: SVELTE_FILTER, namespace: "esbuild-svelte-direct-import", }, async (args) => { return { errors: [ { text: "esbuild-svelte doesn't support creating entry files yet", }, ], } } ) //main loader build.onLoad({ filter: SVELTE_FILTER }, async (args) => { // if told to use the cache, check if it contains the file, // and if the modified time is not greater than the time when it was cached // if so, return the cached data if (options?.cache === true && fileCache.has(args.path)) { const cachedFile = fileCache.get(args.path) || { dependencies: new Map(), data: null, } // should never hit the null b/c of has check let cacheValid = true //for each dependency check if the mtime is still valid //if an exception is generated (file was deleted or something) then cache isn't valid try { cachedFile.dependencies.forEach((time, path) => { if (statSync(path).mtime > time) { cacheValid = false } }) } catch { cacheValid = false } if (cacheValid) { return cachedFile.data } else { fileCache.delete(args.path) //can remove from cache if no longer valid } } //reading files let source = await promisify(readFile)(args.path, "utf8") let filename = relative(process.cwd(), args.path) //file modification time storage const dependencyModifcationTimes = new Map() dependencyModifcationTimes.set( args.path, statSync(args.path).mtime ) // add the target file //actually compile file let preprocessMap try { //do preprocessor stuff if it exists if (options?.preprocess) { let preprocessResult = await preprocess( source, options.preprocess, { filename, } ) source = preprocessResult.code preprocessMap = preprocessResult.map // if caching then we need to store the modifcation times for all dependencies if (options?.cache === true) { preprocessResult.dependencies?.forEach((entry) => { dependencyModifcationTimes.set( entry, statSync(entry).mtime ) }) } } let compileOptions = { css: false, ...options?.compileOptions, } if (preprocessMap) { for (let i = 0; i < preprocessMap.sources.length; i++) { preprocessMap.sources[i] = preprocessMap.sources[ i ]?.replace(/(.+\/)+/, "") } compileOptions.sourcemap = preprocessMap } let { js, css, warnings } = compile(source, { ...compileOptions, filename, }) if (!js.map.sourcesContent) { try { js.map.sourcesContent = [ readFileSync(filename, "utf8"), ] } catch (e) {} } // console.log(js.map) let contents = js.code + `\n//# sourceMappingURL=` + js.map.toUrl() //if svelte emits css seperately, then store it in a map and import it from the js if (!compileOptions.css && css.code) { let cssPath = args.path .replace(".svelte", ".esbuild-svelte-fake-css") .replace(/\\/g, "/") cssCode.set( cssPath, css.code + `/*# sourceMappingURL=${css.map.toUrl()} */` ) contents = contents + `\nimport "${cssPath}";` } const result = { contents, warnings: warnings.map(convertMessage), } // if we are told to cache, then cache if (options?.cache === true) { fileCache.set(args.path, { data: result, dependencies: dependencyModifcationTimes, }) } // make sure to tell esbuild to watch any additional files used if supported if (build.initialOptions.watch) { // this array does include the orignal file, but esbuild should be smart enough to ignore it result.watchFiles = Array.from( dependencyModifcationTimes.keys() ) } return result } catch (e) { return { errors: [convertMessage(e)] } } }) //if the css exists in our map, then output it with the css loader build.onResolve({ filter: FAKE_CSS_FILTER }, ({ path }) => { return { path, namespace: "fakecss" } }) build.onLoad( { filter: FAKE_CSS_FILTER, namespace: "fakecss" }, ({ path }) => { const css = cssCode.get(path) return css ? { contents: css, loader: "css", resolveDir: dirname(path), } : null } ) }, } }