232 lines
8.8 KiB
JavaScript
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
|
|
}
|
|
)
|
|
},
|
|
}
|
|
}
|