tibi-svelte-starter/esbuild-svelte.plugin.js
2021-09-13 18:12:40 +02:00

232 lines
8.8 KiB
JavaScript

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
}
)
},
}
}