| 1 | import * as markodown from "./mod"; |
| 2 | import * as marko from "@marko/compiler"; |
| 3 | import type * as esbuild from "esbuild"; |
| 4 | import * as fs from "node:fs/promises"; |
| 5 | import * as path from "node:path"; |
| 6 | |
| 7 | export type EsbuildPluginOptions = |
| 8 | & Omit<markodown.TransformOptions, "source" | "format"> |
| 9 | & { |
| 10 | marko?: typeof marko; |
| 11 | markoOptions?: marko.Config; |
| 12 | }; |
| 13 | |
| 14 | /** |
| 15 | * esbuild or Bun bundling plugin. Because Bun markets itself as compatible with |
| 16 | * plugins made for esbuild, this plugin is designed to work around differences |
| 17 | * in Bun's implementation to allow it to work. Bugs reported for subtle behavior |
| 18 | * mistakes that are not present when using `esbuild` will not be fixed. |
| 19 | */ |
| 20 | export default function esbuildPlugin( |
| 21 | options: EsbuildPluginOptions = {}, |
| 22 | ): esbuild.Plugin { |
| 23 | const { compile } = options.marko ?? marko; |
| 24 | return { |
| 25 | name: "@clo/markodown/esbuild.ts", |
| 26 | async setup(b) { |
| 27 | b.onLoad( |
| 28 | { filter: /\.(?:mdo|mkd)$/ }, |
| 29 | // NOTE: Bun doesn't pass `with`, so a default is needed |
| 30 | async ({ path: file, suffix, with: { type } = {} }) => { |
| 31 | if (type || suffix) { |
| 32 | // NOTE: Bun does not appear to support fallbacks with `null` |
| 33 | /* v8 ignore next -- @preserve */ |
| 34 | // @ts-expect-error (third-party types not installed) |
| 35 | if (typeof Bun !== "undefined" && !b.esbuild?.build) { |
| 36 | return { |
| 37 | contents: (await fs.readFile(file)), |
| 38 | loader: path.extname(file).slice(1) as "tsx" | "ts", |
| 39 | }; |
| 40 | } |
| 41 | |
| 42 | return null; |
| 43 | } |
| 44 | const result = markodown.transform({ |
| 45 | ...options, |
| 46 | source: await fs.readFile(file, "utf-8"), |
| 47 | selfImport: "./" + path.basename(file), |
| 48 | }); |
| 49 | if (!result.success) { |
| 50 | return { |
| 51 | errors: result.errors.map((err) => ({ |
| 52 | text: err.message, |
| 53 | location: { |
| 54 | line: err.line, |
| 55 | column: err.column - 1, |
| 56 | }, |
| 57 | notes: err.labels.map((note) => ({ |
| 58 | text: note.message, |
| 59 | location: { |
| 60 | line: note.line, |
| 61 | column: note.column - 1, |
| 62 | length: note.width, |
| 63 | }, |
| 64 | })), |
| 65 | })), |
| 66 | watchFiles: [file], |
| 67 | }; |
| 68 | } |
| 69 | |
| 70 | // TODO: source maps! |
| 71 | // TODO: parse marko errors |
| 72 | const { code, meta } = await compile( |
| 73 | result.text, |
| 74 | file + ".marko", |
| 75 | options?.markoOptions ?? {}, |
| 76 | ); |
| 77 | |
| 78 | return { |
| 79 | loader: "js", |
| 80 | contents: code, |
| 81 | watchFiles: meta.watchFiles, |
| 82 | resolveDir: path.dirname(file), |
| 83 | }; |
| 84 | }, |
| 85 | ); |
| 86 | }, |
| 87 | }; |
| 88 | } |