aboutsummaryrefslogtreecommitdiff
path: root/packages/bun-polyfills/tools/bun_test_loader.mjs
blob: 2466157ce7212ab658ea647c249f1565ffc19100 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// @ts-check
/// <reference types="typings-esm-loader" />
/// <reference types="bun-types" />
import { fileURLToPath, pathToFileURL } from 'node:url';
import path from 'node:path';
import util from 'node:util';
import fs from 'node:fs';
import $ from 'chalk';
import bunwasm from 'bun-wasm';
import { TransformResponseStatus } from 'bun-wasm/schema';

await bunwasm.init();
const NO_STACK = () => void 0;
const decoder = new TextDecoder('utf-8');
const libRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'dist', 'src');
const knownBunModules = ['sqlite', 'ffi', 'jsc', 'test'];
/** @type {string} */
let mainURL;

/** @type {resolve} */
export async function resolve(specifier, context, nextResolve) {
    if (context.parentURL === undefined) mainURL = specifier;
    if (specifier === 'bun') return { url: pathToFileURL(path.resolve(libRoot, 'modules', 'bun.js')).href, format: 'module', shortCircuit: true };
    if (specifier.startsWith('bun:')) {
        const module = specifier.slice(4);
        if (!knownBunModules.includes(module)) {
            const err = new Error(`[bun-polyfills] Unknown or unimplemented bun module "${specifier}"`);
            Error.captureStackTrace(err, NO_STACK);
            throw err;
        }
        if (module === 'sqlite') throw new Error('bun:sqlite polyfill is not implemented yet');
        return { url: pathToFileURL(path.resolve(libRoot, 'modules', module + '.js')).href, format: 'module', shortCircuit: true };
    }
    //console.debug('trying to resolve', specifier, 'from', context.parentURL);
    /** @type {Resolve.Return | Error} */
    let next;
    /** @type {string} */
    let format;
    try {
        next = await nextResolve(specifier, context);
        if (next.shortCircuit || next.format === 'builtin' || next.format === 'wasm') return next;
        specifier = next.url;
        format = next.format ?? 'module';
    } catch (err) {
        next = err;
        format = 'module';
    }
    //console.debug('resolved', specifier, 'from', context.parentURL, 'to', Reflect.get(next, 'url') ?? next);
    if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('file://')) {
        if (!specifier.startsWith('file://')) {
            const parent = context.parentURL ? fileURLToPath(context.parentURL) : process.cwd();
            specifier = pathToFileURL(path.resolve(path.dirname(parent), specifier)).href;
        }
        const specifierPath = fileURLToPath(specifier);
        const exists = fs.existsSync(specifierPath);
        if (specifier.endsWith('.ts') && exists) return { url: specifier, format: 'ts' + format, shortCircuit: true };
        if (specifier.endsWith('.js') && exists) return { url: specifier, format, shortCircuit: true };
        if (specifier.endsWith('.ts') && fs.existsSync(specifierPath.slice(0, -3) + '.js')) return { url: specifier.slice(0, -3) + '.js', format, shortCircuit: true };
        if (specifier.endsWith('.js') && fs.existsSync(specifierPath.slice(0, -3) + '.ts')) return { url: specifier.slice(0, -3) + '.ts', format: 'ts' + format, shortCircuit: true };
        if (fs.existsSync(specifierPath + '.ts')) return { url: specifier + '.ts', format: 'ts' + format, shortCircuit: true };
        if (fs.existsSync(specifierPath + '.js')) return { url: specifier + '.js', format, shortCircuit: true };
        if (fs.existsSync(specifierPath + '.json')) return { url: specifier + '.json', format: 'json', shortCircuit: true };
        if (fs.existsSync(specifierPath + '/index.ts')) return { url: specifier + '/index.ts', format: 'ts' + format, shortCircuit: true };
        if (fs.existsSync(specifierPath + '/index.js')) return { url: specifier + '/index.js', format, shortCircuit: true };
        if (fs.existsSync(specifierPath + '/index.json')) return { url: specifier + '/index.json', format: 'json', shortCircuit: true };
    }
    if (next instanceof Error) throw next;
    else return next;
}

/** @type {load} */
export async function load(url, context, nextLoad) {
    //console.debug('Loading', url, 'with context', context);
    if (context.format === 'tsmodule' || context.format === 'tscommonjs') {
        const filepath = fileURLToPath(url);
        const src = fs.readFileSync(filepath, 'utf-8');
        const transform = bunwasm.transformSync(src, path.basename(filepath), 'ts');
        if (transform.status === TransformResponseStatus.fail) {
            if (transform.errors.length) {
                throw formatBuildErrors(transform.errors);
            } else {
                const err = new Error('Unknown transform error');
                Error.captureStackTrace(err, NO_STACK);
                throw err;
            }
        }
        return {
            shortCircuit: true,
            format: /** @type {ModuleFormat} */(context.format.slice(2)),
            source: decoder.decode(transform.files[0].data),
        };
    }
    if (context.format === 'json') context.importAssertions.type = 'json';
    const loaded = await nextLoad(url, context);
    if (url.startsWith('file://') && loaded.format === 'module') {
        const src = typeof loaded.source === 'string' ? loaded.source : decoder.decode(loaded.source);
        return { shortCircuit: true, format: 'module', source: src };
    }
    else return loaded;
}

/** @param {import('bun-wasm/schema').Message[]} buildErrors */
function formatBuildErrors(buildErrors) {
    const formatted = buildErrors.map(err => {
        const loc = err.data.location;
        const str = `${$.redBright('error')}${$.gray(':')} ${$.bold(err.data.text)}\n` +
        (loc
            ? `${highlightErrorChar(loc.line_text, loc.column)}\n` +
                $.redBright.bold('^'.padStart(loc.column)) + '\n' +
                `${$.bold(loc.file)}${$.gray(':')}${$.yellowBright(loc.line)}${$.gray(':')}${$.yellowBright(loc.column)} ${$.gray(loc.offset)}`
            : ''
        );
        const newerr = new Error(str);
        newerr.name = 'BuildError';
        newerr.stack = str;
        return newerr;
    });
    const aggregate = new AggregateError(formatted, `Input code has ${formatted.length} error${formatted.length === 1 ? '' : 's'}`);
    Error.captureStackTrace(aggregate, NO_STACK);
    aggregate.name = 'BuildFailed';
    return aggregate;
}

/**
 * @param {string} str
 * @param {number} at */
function highlightErrorChar(str, at) {
    return str.slice(0, at) + $.red(str[at]) + str.slice(at + 1);
}