From 4f122bbb9ca3622e6b1f15d08be1a32a6bc7edf1 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Fri, 7 Oct 2022 14:38:07 +0200 Subject: [PATCH] add esbuild loader --- packages/esbuild_deno_loader | 1 - packages/esbuild_deno_loader/LICENSE | 21 ++ packages/esbuild_deno_loader/README.md | 21 ++ packages/esbuild_deno_loader/deps.ts | 13 ++ .../esbuild_deno_loader/examples/bundle.ts | 11 + packages/esbuild_deno_loader/makefile | 11 + packages/esbuild_deno_loader/mod.ts | 110 ++++++++++ packages/esbuild_deno_loader/src/deno.ts | 89 ++++++++ .../esbuild_deno_loader/src/native_loader.ts | 65 ++++++ .../src/portable_loader.ts | 194 ++++++++++++++++++ packages/esbuild_deno_loader/src/shared.ts | 40 ++++ packages/esbuild_deno_loader/test_deps.ts | 6 + .../esbuild_deno_loader/testdata/data.json | 6 + .../esbuild_deno_loader/testdata/importmap.js | 1 + .../testdata/importmap.json | 5 + packages/esbuild_deno_loader/testdata/mod.js | 2 + packages/esbuild_deno_loader/testdata/mod.jsx | 11 + packages/esbuild_deno_loader/testdata/mod.mjs | 2 + packages/esbuild_deno_loader/testdata/mod.mts | 4 + packages/esbuild_deno_loader/testdata/mod.ts | 4 + packages/esbuild_deno_loader/testdata/mod.tsx | 11 + 21 files changed, 627 insertions(+), 1 deletion(-) delete mode 160000 packages/esbuild_deno_loader create mode 100644 packages/esbuild_deno_loader/LICENSE create mode 100644 packages/esbuild_deno_loader/README.md create mode 100644 packages/esbuild_deno_loader/deps.ts create mode 100644 packages/esbuild_deno_loader/examples/bundle.ts create mode 100644 packages/esbuild_deno_loader/makefile create mode 100644 packages/esbuild_deno_loader/mod.ts create mode 100644 packages/esbuild_deno_loader/src/deno.ts create mode 100644 packages/esbuild_deno_loader/src/native_loader.ts create mode 100644 packages/esbuild_deno_loader/src/portable_loader.ts create mode 100644 packages/esbuild_deno_loader/src/shared.ts create mode 100644 packages/esbuild_deno_loader/test_deps.ts create mode 100644 packages/esbuild_deno_loader/testdata/data.json create mode 100644 packages/esbuild_deno_loader/testdata/importmap.js create mode 100644 packages/esbuild_deno_loader/testdata/importmap.json create mode 100644 packages/esbuild_deno_loader/testdata/mod.js create mode 100644 packages/esbuild_deno_loader/testdata/mod.jsx create mode 100644 packages/esbuild_deno_loader/testdata/mod.mjs create mode 100644 packages/esbuild_deno_loader/testdata/mod.mts create mode 100644 packages/esbuild_deno_loader/testdata/mod.ts create mode 100644 packages/esbuild_deno_loader/testdata/mod.tsx diff --git a/packages/esbuild_deno_loader b/packages/esbuild_deno_loader deleted file mode 160000 index 70c34bb6..00000000 --- a/packages/esbuild_deno_loader +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 70c34bb6d77b2dd05255f0e4a813bb62239d62d1 diff --git a/packages/esbuild_deno_loader/LICENSE b/packages/esbuild_deno_loader/LICENSE new file mode 100644 index 00000000..5e0fffb3 --- /dev/null +++ b/packages/esbuild_deno_loader/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Luca Casonato + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/esbuild_deno_loader/README.md b/packages/esbuild_deno_loader/README.md new file mode 100644 index 00000000..23a58037 --- /dev/null +++ b/packages/esbuild_deno_loader/README.md @@ -0,0 +1,21 @@ +# esbuild_deno_loader + +Deno module resolution for `esbuild`. + +## Example + +This example bundles an entrypoint into a single ESM output. + +```js +import * as esbuild from "https://deno.land/x/esbuild@v0.14.51/mod.js"; +import { denoPlugin } from "https://deno.land/x/esbuild_deno_loader@0.5.2/mod.ts"; + +await esbuild.build({ + plugins: [denoPlugin()], + entryPoints: ["https://deno.land/std@0.150.0/hash/sha1.ts"], + outfile: "./dist/sha1.esm.js", + bundle: true, + format: "esm", +}); +esbuild.stop(); +``` diff --git a/packages/esbuild_deno_loader/deps.ts b/packages/esbuild_deno_loader/deps.ts new file mode 100644 index 00000000..bce59f83 --- /dev/null +++ b/packages/esbuild_deno_loader/deps.ts @@ -0,0 +1,13 @@ +import type * as esbuild from "https://deno.land/x/esbuild@v0.14.54/mod.d.ts"; +export type { esbuild }; +export { + fromFileUrl, + resolve, + toFileUrl, +} from "https://deno.land/std@0.150.0/path/mod.ts"; +export { basename, extname } from "https://deno.land/std@0.150.0/path/mod.ts"; +export { + resolveImportMap, + resolveModuleSpecifier, +} from "https://deno.land/x/importmap@0.2.1/mod.ts"; +export type { ImportMap } from "https://deno.land/x/importmap@0.2.1/mod.ts"; diff --git a/packages/esbuild_deno_loader/examples/bundle.ts b/packages/esbuild_deno_loader/examples/bundle.ts new file mode 100644 index 00000000..fcc3791e --- /dev/null +++ b/packages/esbuild_deno_loader/examples/bundle.ts @@ -0,0 +1,11 @@ +import * as esbuild from "https://deno.land/x/esbuild@v0.14.51/mod.js"; +import { denoPlugin } from "https://deno.land/x/esbuild_deno_loader@0.5.2/mod.ts"; + +await esbuild.build({ + plugins: [denoPlugin()], + entryPoints: ["https://deno.land/std@0.150.0/hash/sha1.ts"], + outfile: "./dist/sha1.esm.js", + bundle: true, + format: "esm", +}); +esbuild.stop(); diff --git a/packages/esbuild_deno_loader/makefile b/packages/esbuild_deno_loader/makefile new file mode 100644 index 00000000..4e26f2ab --- /dev/null +++ b/packages/esbuild_deno_loader/makefile @@ -0,0 +1,11 @@ +test: + deno test -A + +lint: + deno lint + +fmt: + deno fmt + +fmt/check: + deno fmt --check \ No newline at end of file diff --git a/packages/esbuild_deno_loader/mod.ts b/packages/esbuild_deno_loader/mod.ts new file mode 100644 index 00000000..a1b6b088 --- /dev/null +++ b/packages/esbuild_deno_loader/mod.ts @@ -0,0 +1,110 @@ +import { + esbuild, + ImportMap, + resolveImportMap, + resolveModuleSpecifier, + toFileUrl, +} from "./deps.ts"; +import { load as nativeLoad } from "./src/native_loader.ts"; +import { load as portableLoad } from "./src/portable_loader.ts"; +import { ModuleEntry } from "./src/deno.ts"; + +export interface DenoPluginOptions { + /** + * Specify the URL to an import map to use when resolving import specifiers. + * The URL must be fetchable with `fetch`. + */ + importMapURL?: URL; + /** + * Specify which loader to use. By default this will use the `native` loader, + * unless `Deno.run` is not available. + * + * - `native`: Shells out to the Deno execuatble under the hood to load + * files. Requires --allow-read and --allow-run. + * - `portable`: Do module downloading and caching with only Web APIs. + * Requires --allow-net. + */ + loader?: "native" | "portable"; +} + +/** The default loader to use. */ +export const DEFAULT_LOADER: "native" | "portable" = + typeof Deno.run === "function" ? "native" : "portable"; + +export function denoPlugin(options: DenoPluginOptions = {}): esbuild.Plugin { + const loader = options.loader ?? DEFAULT_LOADER; + return { + name: "deno", + setup(build) { + const infoCache = new Map(); + let importMap: ImportMap | null = null; + + build.onStart(async function onStart() { + if (options.importMapURL !== undefined) { + const resp = await fetch(options.importMapURL.href); + const txt = await resp.text(); + importMap = resolveImportMap(JSON.parse(txt), options.importMapURL); + } else { + importMap = null; + } + }); + + build.onResolve( + { filter: /.*/ }, + function onResolve( + args: esbuild.OnResolveArgs, + ): esbuild.OnResolveResult | null | undefined { + // console.log("To resolve", args.path); + const resolveDir = args.resolveDir + ? `${toFileUrl(args.resolveDir).href}/` + : ""; + const referrer = args.importer || resolveDir; + let resolved: URL; + if (importMap !== null) { + const res = resolveModuleSpecifier( + args.path, + importMap, + new URL(referrer) || undefined, + ); + resolved = new URL(res); + } else { + resolved = new URL(args.path, referrer); + } + // console.log("Resolved", resolved.href); + if (build.initialOptions.external) { + for (const external of build.initialOptions.external) { + if (resolved.href.startsWith(external)) { + // console.log("Got external", args.path, resolved.href); + return { path: resolved.href, external: true }; + } + } + } + if (resolved.href.endsWith(".css")) { + return { + path: resolved.href.substring("file://".length), + }; + } + return { path: resolved.href, namespace: "deno" }; + }, + ); + + build.onLoad( + { filter: /.*/ }, + function onLoad( + args: esbuild.OnLoadArgs, + ): Promise { + if (args.path.endsWith(".css")) { + return Promise.resolve(null); + } + const url = new URL(args.path); + switch (loader) { + case "native": + return nativeLoad(infoCache, url, options); + case "portable": + return portableLoad(url, options); + } + }, + ); + }, + }; +} diff --git a/packages/esbuild_deno_loader/src/deno.ts b/packages/esbuild_deno_loader/src/deno.ts new file mode 100644 index 00000000..1b8ffb81 --- /dev/null +++ b/packages/esbuild_deno_loader/src/deno.ts @@ -0,0 +1,89 @@ +// Lifted from https://raw.githubusercontent.com/denoland/deno_graph/89affe43c9d3d5c9165c8089687c107d53ed8fe1/lib/media_type.ts +export type MediaType = + | "JavaScript" + | "Mjs" + | "Cjs" + | "JSX" + | "TypeScript" + | "Mts" + | "Cts" + | "Dts" + | "Dmts" + | "Dcts" + | "TSX" + | "Json" + | "Wasm" + | "TsBuildInfo" + | "SourceMap" + | "Unknown"; + +export interface InfoOutput { + roots: string[]; + modules: ModuleEntry[]; + redirects: Record; +} + +export interface ModuleEntry { + specifier: string; + size: number; + mediaType?: MediaType; + local?: string; + checksum?: string; + emit?: string; + map?: string; + error?: string; +} + +interface DenoInfoOptions { + importMap?: string; +} + +let tempDir: null | string; + +export async function info( + specifier: URL, + options: DenoInfoOptions, +): Promise { + const cmd = [ + Deno.execPath(), + "info", + "--json", + ]; + if (options.importMap !== undefined) { + cmd.push("--import-map", options.importMap); + } + cmd.push(specifier.href); + + if (!tempDir) { + tempDir = Deno.makeTempDirSync(); + } + + let proc; + + try { + proc = Deno.run({ + cmd, + stdout: "piped", + cwd: tempDir, + }); + const raw = await proc.output(); + const status = await proc.status(); + if (!status.success) { + throw new Error(`Failed to call 'deno info' on '${specifier.href}'`); + } + const txt = new TextDecoder().decode(raw); + return JSON.parse(txt); + } finally { + try { + proc?.stdout.close(); + } catch (err) { + if (err instanceof Deno.errors.BadResource) { + // ignore the error + } else { + // deno-lint-ignore no-unsafe-finally + throw err; + } + } + proc?.close(); + } +} diff --git a/packages/esbuild_deno_loader/src/native_loader.ts b/packages/esbuild_deno_loader/src/native_loader.ts new file mode 100644 index 00000000..670149f2 --- /dev/null +++ b/packages/esbuild_deno_loader/src/native_loader.ts @@ -0,0 +1,65 @@ +import { esbuild, fromFileUrl } from "../deps.ts"; +import * as deno from "./deno.ts"; +import { mediaTypeToLoader, transformRawIntoContent } from "./shared.ts"; + +export interface LoadOptions { + importMapURL?: URL; +} + +export async function load( + infoCache: Map, + url: URL, + options: LoadOptions +): Promise { + switch (url.protocol) { + case "http:": + case "https:": + case "data:": + return await loadFromCLI(infoCache, url, options); + case "file:": { + const res = await loadFromCLI(infoCache, url, options); + res.watchFiles = [fromFileUrl(url.href)]; + return res; + } + } + return null; +} + +async function loadFromCLI( + infoCache: Map, + specifier: URL, + options: LoadOptions +): Promise { + const specifierRaw = specifier.href; + if (!infoCache.has(specifierRaw)) { + const { modules, redirects } = await deno.info(specifier, { + importMap: options.importMapURL?.href, + }); + for (const module of modules) { + infoCache.set(module.specifier, module); + } + for (const [specifier, redirect] of Object.entries(redirects)) { + const redirected = infoCache.get(redirect); + if (!redirected) { + throw new TypeError("Unreachable."); + } + infoCache.set(specifier, redirected); + } + } + + const module = infoCache.get(specifierRaw); + if (!module) { + throw new TypeError("Unreachable."); + } + + if (module.error) throw new Error(module.error); + if (!module.local) throw new Error("Module not downloaded yet."); + const mediaType = module.mediaType ?? "Unknown"; + + const loader = mediaTypeToLoader(mediaType); + + const raw = await Deno.readFile(module.local); + const contents = transformRawIntoContent(raw, mediaType); + + return { contents, loader }; +} diff --git a/packages/esbuild_deno_loader/src/portable_loader.ts b/packages/esbuild_deno_loader/src/portable_loader.ts new file mode 100644 index 00000000..24005cdd --- /dev/null +++ b/packages/esbuild_deno_loader/src/portable_loader.ts @@ -0,0 +1,194 @@ +import { esbuild, extname, fromFileUrl } from "../deps.ts"; +import * as deno from "./deno.ts"; +import { mediaTypeToLoader, transformRawIntoContent } from "./shared.ts"; + +export interface LoadOptions { + importMapURL?: URL; +} + +export async function load( + url: URL, + _options: LoadOptions, +): Promise { + switch (url.protocol) { + case "http:": + case "https:": + case "data:": + return await loadWithFetch(url); + case "file:": { + const res = await loadWithReadFile(url); + res.watchFiles = [fromFileUrl(url.href)]; + return res; + } + } + return null; +} + +async function loadWithFetch( + specifier: URL, +): Promise { + const specifierRaw = specifier.href; + + // TODO(lucacasonato): redirects! + const resp = await fetch(specifierRaw); + if (!resp.ok) { + throw new Error( + `Encountered status code ${resp.status} while fetching ${specifierRaw}.`, + ); + } + + const contentType = resp.headers.get("content-type"); + const mediaType = mapContentType( + new URL(resp.url || specifierRaw), + contentType, + ); + + const loader = mediaTypeToLoader(mediaType); + + const raw = new Uint8Array(await resp.arrayBuffer()); + const contents = transformRawIntoContent(raw, mediaType); + + return { contents, loader }; +} + +async function loadWithReadFile(specifier: URL): Promise { + const path = fromFileUrl(specifier); + + const mediaType = mapContentType(specifier, null); + const loader = mediaTypeToLoader(mediaType); + + const raw = await Deno.readFile(path); + const contents = transformRawIntoContent(raw, mediaType); + + return { contents, loader }; +} + +function mapContentType( + specifier: URL, + contentType: string | null, +): deno.MediaType { + if (contentType !== null) { + const contentTypes = contentType.split(";"); + const mediaType = contentTypes[0].toLowerCase(); + switch (mediaType) { + case "application/typescript": + case "text/typescript": + case "video/vnd.dlna.mpeg-tts": + case "video/mp2t": + case "application/x-typescript": + return mapJsLikeExtension(specifier, "TypeScript"); + case "application/javascript": + case "text/javascript": + case "application/ecmascript": + case "text/ecmascript": + case "application/x-javascript": + case "application/node": + return mapJsLikeExtension(specifier, "JavaScript"); + case "text/jsx": + return "JSX"; + case "text/tsx": + return "TSX"; + case "application/json": + case "text/json": + return "Json"; + case "application/wasm": + return "Wasm"; + case "text/plain": + case "application/octet-stream": + return mediaTypeFromSpecifier(specifier); + default: + return "Unknown"; + } + } else { + return mediaTypeFromSpecifier(specifier); + } +} + +function mapJsLikeExtension( + specifier: URL, + defaultType: deno.MediaType, +): deno.MediaType { + const path = specifier.pathname; + switch (extname(path)) { + case ".jsx": + return "JSX"; + case ".mjs": + return "Mjs"; + case ".cjs": + return "Cjs"; + case ".tsx": + return "TSX"; + case ".ts": + if (path.endsWith(".d.ts")) { + return "Dts"; + } else { + return defaultType; + } + case ".mts": { + if (path.endsWith(".d.mts")) { + return "Dmts"; + } else { + return defaultType == "JavaScript" ? "Mjs" : "Mts"; + } + } + case ".cts": { + if (path.endsWith(".d.cts")) { + return "Dcts"; + } else { + return defaultType == "JavaScript" ? "Cjs" : "Cts"; + } + } + default: + return defaultType; + } +} + +function mediaTypeFromSpecifier(specifier: URL): deno.MediaType { + const path = specifier.pathname; + switch (extname(path)) { + case "": + if (path.endsWith("/.tsbuildinfo")) { + return "TsBuildInfo"; + } else { + return "Unknown"; + } + case ".ts": + if (path.endsWith(".d.ts")) { + return "Dts"; + } else { + return "TypeScript"; + } + case ".mts": + if (path.endsWith(".d.mts")) { + return "Dmts"; + } else { + return "Mts"; + } + case ".cts": + if (path.endsWith(".d.cts")) { + return "Dcts"; + } else { + return "Cts"; + } + case ".tsx": + return "TSX"; + case ".js": + return "JavaScript"; + case ".jsx": + return "JSX"; + case ".mjs": + return "Mjs"; + case ".cjs": + return "Cjs"; + case ".json": + return "Json"; + case ".wasm": + return "Wasm"; + case ".tsbuildinfo": + return "TsBuildInfo"; + case ".map": + return "SourceMap"; + default: + return "Unknown"; + } +} diff --git a/packages/esbuild_deno_loader/src/shared.ts b/packages/esbuild_deno_loader/src/shared.ts new file mode 100644 index 00000000..04aa9c9d --- /dev/null +++ b/packages/esbuild_deno_loader/src/shared.ts @@ -0,0 +1,40 @@ +import { esbuild } from "../deps.ts"; +import { MediaType } from "./deno.ts"; + +export function mediaTypeToLoader(mediaType: MediaType): esbuild.Loader { + switch (mediaType) { + case "JavaScript": + case "Mjs": + return "js"; + case "JSX": + return "jsx"; + case "TypeScript": + case "Mts": + return "ts"; + case "TSX": + return "tsx"; + case "Json": + return "js"; + default: + throw new Error(`Unhandled media type ${mediaType}.`); + } +} + +export function transformRawIntoContent( + raw: Uint8Array, + mediaType: MediaType +): string | Uint8Array { + switch (mediaType) { + case "Json": + return jsonToESM(raw); + default: + return raw; + } +} + +function jsonToESM(source: Uint8Array): string { + const sourceString = new TextDecoder().decode(source); + let json = JSON.stringify(JSON.parse(sourceString), null, 2); + json = json.replaceAll(`"__proto__":`, `["__proto__"]:`); + return `export default ${json};`; +} diff --git a/packages/esbuild_deno_loader/test_deps.ts b/packages/esbuild_deno_loader/test_deps.ts new file mode 100644 index 00000000..9f06eb9e --- /dev/null +++ b/packages/esbuild_deno_loader/test_deps.ts @@ -0,0 +1,6 @@ +import * as esbuild from "https://deno.land/x/esbuild@v0.14.51/mod.js"; +export { esbuild }; +export { + assert, + assertEquals, +} from "https://deno.land/std@0.150.0/testing/asserts.ts"; diff --git a/packages/esbuild_deno_loader/testdata/data.json b/packages/esbuild_deno_loader/testdata/data.json new file mode 100644 index 00000000..1b2a6bd7 --- /dev/null +++ b/packages/esbuild_deno_loader/testdata/data.json @@ -0,0 +1,6 @@ +{ + "hello": "world", + "__proto__": { + "sky": "universe" + } +} diff --git a/packages/esbuild_deno_loader/testdata/importmap.js b/packages/esbuild_deno_loader/testdata/importmap.js new file mode 100644 index 00000000..d81d1cbd --- /dev/null +++ b/packages/esbuild_deno_loader/testdata/importmap.js @@ -0,0 +1 @@ +export * from "mod"; diff --git a/packages/esbuild_deno_loader/testdata/importmap.json b/packages/esbuild_deno_loader/testdata/importmap.json new file mode 100644 index 00000000..b0ecb285 --- /dev/null +++ b/packages/esbuild_deno_loader/testdata/importmap.json @@ -0,0 +1,5 @@ +{ + "imports": { + "mod": "./mod.ts" + } +} diff --git a/packages/esbuild_deno_loader/testdata/mod.js b/packages/esbuild_deno_loader/testdata/mod.js new file mode 100644 index 00000000..68412366 --- /dev/null +++ b/packages/esbuild_deno_loader/testdata/mod.js @@ -0,0 +1,2 @@ +const bool = "asd"; +export { bool }; diff --git a/packages/esbuild_deno_loader/testdata/mod.jsx b/packages/esbuild_deno_loader/testdata/mod.jsx new file mode 100644 index 00000000..374adb52 --- /dev/null +++ b/packages/esbuild_deno_loader/testdata/mod.jsx @@ -0,0 +1,11 @@ +function createElement(fn) { + return fn(); +} + +const React = { createElement }; + +function Asd() { + return "foo"; +} + +export default ; diff --git a/packages/esbuild_deno_loader/testdata/mod.mjs b/packages/esbuild_deno_loader/testdata/mod.mjs new file mode 100644 index 00000000..68412366 --- /dev/null +++ b/packages/esbuild_deno_loader/testdata/mod.mjs @@ -0,0 +1,2 @@ +const bool = "asd"; +export { bool }; diff --git a/packages/esbuild_deno_loader/testdata/mod.mts b/packages/esbuild_deno_loader/testdata/mod.mts new file mode 100644 index 00000000..35282209 --- /dev/null +++ b/packages/esbuild_deno_loader/testdata/mod.mts @@ -0,0 +1,4 @@ +let bool: string; +bool = "asd"; +bool = "asd2"; +export { bool }; diff --git a/packages/esbuild_deno_loader/testdata/mod.ts b/packages/esbuild_deno_loader/testdata/mod.ts new file mode 100644 index 00000000..35282209 --- /dev/null +++ b/packages/esbuild_deno_loader/testdata/mod.ts @@ -0,0 +1,4 @@ +let bool: string; +bool = "asd"; +bool = "asd2"; +export { bool }; diff --git a/packages/esbuild_deno_loader/testdata/mod.tsx b/packages/esbuild_deno_loader/testdata/mod.tsx new file mode 100644 index 00000000..4bbf202e --- /dev/null +++ b/packages/esbuild_deno_loader/testdata/mod.tsx @@ -0,0 +1,11 @@ +function createElement(fn: () => string) { + return fn(); +} + +const React = { createElement }; + +function Asd() { + return "foo"; +} + +export default ;