in src/codegen/bundle-functions.ts [68:282]
async function processFileSplit(filename: string): Promise<{ functions: BundledBuiltin[]; internal: boolean }> {
const basename = path.basename(filename, ".ts");
let contents = await Bun.file(filename).text();
contents = applyGlobalReplacements(contents);
// first approach doesnt work perfectly because we actually need to split each function declaration
// and then compile those separately
const consumeWhitespace = /^\s*/;
const consumeTopLevelContent =
/^(\/\*|\/\/|type|import|interface|\$|const enum|export (?:async )?function|(?:async )?function)/;
const consumeEndOfType = /;|.(?=export|type|interface|\$|\/\/|\/\*|function|const enum)/;
const functions: ParsedBuiltin[] = [];
let directives: Record<string, any> = {};
const bundledFunctions: BundledBuiltin[] = [];
let internal = false;
const topLevelEnums: { name: string; code: string }[] = [];
while (contents.length) {
contents = contents.replace(consumeWhitespace, "");
if (!contents.length) break;
const match = contents.match(consumeTopLevelContent);
if (!match) {
throw new SyntaxError("Could not process input:\n" + contents.slice(0, contents.indexOf("\n")));
}
contents = contents.slice(match.index!);
if (match[1] === "import") {
// TODO: we may want to do stuff with these
const i = contents.indexOf(";");
contents = contents.slice(i + 1);
} else if (match[1] === "/*") {
const i = contents.indexOf("*/") + 2;
internal ||= contents.slice(0, i).includes("@internal");
contents = contents.slice(i);
} else if (match[1] === "//") {
const i = contents.indexOf("\n") + 1;
internal ||= contents.slice(0, i).includes("@internal");
contents = contents.slice(i);
} else if (match[1] === "type" || match[1] === "export type") {
const i = contents.search(consumeEndOfType);
contents = contents.slice(i + 1);
} else if (match[1] === "interface") {
contents = sliceSourceCode(contents, false).rest;
} else if (match[1] === "const enum") {
const { result, rest } = sliceSourceCode(contents, false);
const i = result.indexOf("{\n");
// Support const enums in module scope.
topLevelEnums.push({
name: result.slice("const enum ".length, i).trim(),
code: "\n" + result,
});
contents = rest;
} else if (match[1] === "$") {
const directive = contents.match(/^\$([a-zA-Z0-9]+)(?:\s*=\s*([^\r\n]+?))?\s*;?\r?\n/);
if (!directive) {
throw new SyntaxError("Could not parse directive:\n" + contents.slice(0, contents.indexOf("\n")));
}
const name = directive[1];
let value;
try {
value = directive[2] ? JSON.parse(directive[2]) : true;
} catch (error) {
throw new SyntaxError("Could not parse directive value " + directive[2] + " (must be JSON parsable)");
}
if (name === "constructor") {
directives.ConstructAbility = "CanConstruct";
} else if (name === "nakedConstructor") {
directives.ConstructAbility = "CanConstruct";
directives.ConstructKind = "Naked";
} else {
directives[name] = value;
}
contents = contents.slice(directive[0].length);
} else if (match[1] === "export function" || match[1] === "export async function") {
const declaration = contents.match(
/^export\s+(async\s+)?function\s+([a-zA-Z0-9]+)\s*\(([^)]*)\)(?:\s*:\s*([^{\n]+))?\s*{?/,
);
if (!declaration)
throw new SyntaxError("Could not parse function declaration:\n" + contents.slice(0, contents.indexOf("\n")));
const async = !!declaration[1];
const name = declaration[2];
const paramString = declaration[3];
const params =
paramString.trim().length === 0 ? [] : paramString.split(",").map(x => x.replace(/:.+$/, "").trim());
if (params[0] === "this") {
params.shift();
}
const { result, rest } = sliceSourceCode(contents.slice(declaration[0].length - 1), true, x =>
globalThis.requireTransformer(x, SRC_DIR + "/" + basename),
);
const source = result.trim().slice(2, -1);
const constEnumsUsedInFunction: string[] = [];
if (topLevelEnums.length) {
// If the function references a top-level const enum let's add the code
// to the top-level scope of the function so that the transpiler will
// inline all the values and strip out the enum object.
for (const { name, code } of topLevelEnums) {
// Only include const enums which are referenced in the function source.
if (source.includes(name)) {
constEnumsUsedInFunction.push(code);
}
}
}
functions.push({
name,
params,
directives,
source,
async,
enums: constEnumsUsedInFunction,
});
contents = rest;
directives = {};
} else if (match[1] === "function" || match[1] === "async function") {
const fnname = contents.match(/^function ([a-zA-Z0-9]+)\(([^)]*)\)(?:\s*:\s*([^{\n]+))?\s*{?/)![1];
throw new SyntaxError("All top level functions must be exported: " + fnname);
} else {
throw new Error("TODO: parse " + match[1]);
}
}
for (const fn of functions) {
const tmpFile = path.join(TMP_DIR, `${basename}.${fn.name}.ts`);
// not sure if this optimization works properly in jsc builtins
// const useThis = fn.usesThis;
const useThis = true;
// TODO: we should use format=IIFE so we could bundle imports and extra functions.
await Bun.write(
tmpFile,
`// @ts-nocheck
// GENERATED TEMP FILE - DO NOT EDIT
// Sourced from ${path.relative(TMP_DIR, filename)}
${fn.enums.join("\n")}
// do not allow the bundler to rename a symbol to $
($);
$$capture_start$$(${fn.async ? "async " : ""}${
useThis
? `function(${fn.params.join(",")})`
: `${fn.params.length === 1 ? fn.params[0] : `(${fn.params.join(",")})`}=>`
} {${fn.source}}).$$capture_end$$;
`,
);
await Bun.sleep(1);
const build = await Bun.build({
entrypoints: [tmpFile],
define,
minify: { syntax: true, whitespace: false },
});
if (!build.success) {
throw new AggregateError(build.logs, "Failed bundling builtin function " + fn.name + " from " + basename + ".ts");
}
if (build.outputs.length !== 1) {
throw new Error("expected one output");
}
const output = await build.outputs[0].text();
let usesDebug = output.includes("$debug_log");
let usesAssert = output.includes("$assert");
const captured = output.match(/\$\$capture_start\$\$([\s\S]+)\.\$\$capture_end\$\$/)![1];
const finalReplacement =
(fn.directives.sloppy
? captured
: captured.replace(
/function\s*\(.*?\)\s*{/,
'$&"use strict";' +
(usesDebug ? createLogClientJS("BUILTINS", fn.name) : "") +
(usesAssert ? createAssertClientJS(fn.name) : ""),
)
)
.replace(/^\((async )?function\(/, "($1function (")
.replace(/__intrinsic__/g, "@")
.replace(/__no_intrinsic__/g, "") + "\n";
const errors = [...finalReplacement.matchAll(/@bundleError\((.*)\)/g)];
if (errors.length) {
throw new Error(`Errors in ${basename}.ts:\n${errors.map(x => x[1]).join("\n")}`);
}
bundledFunctions.push({
name: fn.name,
directives: fn.directives,
source: finalReplacement,
params: fn.params,
visibility: fn.directives.visibility ?? (fn.directives.linkTimeConstant ? "Private" : "Public"),
isGetter: !!fn.directives.getter,
constructAbility: fn.directives.ConstructAbility ?? "CannotConstruct",
constructKind: fn.directives.ConstructKind ?? "None",
isLinkTimeConstant: !!fn.directives.linkTimeConstant,
intrinsic: fn.directives.intrinsic ?? "NoIntrinsic",
// Not known yet.
sourceOffset: 0,
overriddenName: fn.directives.getter
? `"get ${fn.name}"_s`
: fn.directives.overriddenName
? `"${fn.directives.overriddenName}"_s`
: "ASCIILiteral()",
});
}
return {
functions: bundledFunctions.sort((a, b) => a.name.localeCompare(b.name)),
internal,
};
}