@b9g/libuild 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,406 @@
1
+ /// <reference types="./test-runner.d.ts" />
2
+ import {
3
+ __require
4
+ } from "./_chunks/chunk-IBOCQ33F.js";
5
+
6
+ // src/test-runner.ts
7
+ import * as FS from "fs/promises";
8
+ import * as Path from "path";
9
+ import { createServer } from "http";
10
+ import * as ESBuild from "esbuild";
11
+ import { fileURLToPath } from "url";
12
+ import { createRequire } from "module";
13
+ var __dirname = Path.dirname(fileURLToPath(import.meta.url));
14
+ var DEFAULT_PATTERNS = [
15
+ "**/*.test.ts",
16
+ "**/*.test.tsx",
17
+ "**/*.test.js",
18
+ "**/*.test.jsx",
19
+ "**/*.spec.ts",
20
+ "**/*.spec.tsx",
21
+ "**/*.spec.js",
22
+ "**/*.spec.jsx",
23
+ "**/test/**/*.ts",
24
+ "**/test/**/*.tsx",
25
+ "**/test/**/*.js",
26
+ "**/test/**/*.jsx"
27
+ ];
28
+ var IGNORE_PATTERNS = [
29
+ "**/node_modules/**",
30
+ "**/dist/**",
31
+ "**/.git/**",
32
+ "**/coverage/**"
33
+ ];
34
+ async function findTestFiles(cwd, patterns) {
35
+ const { glob } = await import("glob");
36
+ const files = [];
37
+ for (const pattern of patterns) {
38
+ const matches = await glob(pattern, {
39
+ cwd,
40
+ ignore: IGNORE_PATTERNS,
41
+ absolute: true
42
+ });
43
+ files.push(...matches);
44
+ }
45
+ return [...new Set(files)];
46
+ }
47
+ function generateTestEntry(testFiles, platform) {
48
+ const imports = testFiles.map((file, i) => `import "${file.replace(/\\/g, "/")}";`).join("\n");
49
+ return `// Auto-generated test entry for ${platform}
50
+ ${imports}
51
+ `;
52
+ }
53
+ function isBrowserPlatform(platform) {
54
+ return platform === "chromium" || platform === "firefox" || platform === "webkit";
55
+ }
56
+ async function bundleTests(testFiles, platform, outDir, cwd) {
57
+ const entryContent = generateTestEntry(testFiles, platform);
58
+ const entryPath = Path.join(outDir, `entry-${platform}.ts`);
59
+ const outPath = Path.join(outDir, `bundle-${platform}.js`);
60
+ await FS.writeFile(entryPath, entryContent);
61
+ const isBrowser = isBrowserPlatform(platform);
62
+ const shimName = isBrowser ? "test-browser" : `test-${platform}`;
63
+ let shimPath;
64
+ try {
65
+ shimPath = __require.resolve(`@b9g/libuild/${shimName}`);
66
+ } catch {
67
+ shimPath = Path.join(__dirname, `${shimName}.js`);
68
+ }
69
+ const requireShim = `
70
+ import { createRequire } from "module";
71
+ const require = createRequire(import.meta.url);
72
+ `;
73
+ const buildOptions = {
74
+ entryPoints: [entryPath],
75
+ bundle: true,
76
+ format: "esm",
77
+ outfile: outPath,
78
+ platform: isBrowser ? "browser" : "node",
79
+ target: isBrowser ? "es2020" : "node18",
80
+ // Replace @b9g/libuild/test with platform-specific shim
81
+ alias: {
82
+ "@b9g/libuild/test": shimPath
83
+ },
84
+ // External runtime-specific modules
85
+ external: platform === "bun" ? ["bun:test"] : [],
86
+ // Inject require shim for node/bun to handle CJS deps like expect/chalk
87
+ ...isBrowser ? {} : { banner: { js: requireShim } },
88
+ // Define for dead code elimination
89
+ define: {
90
+ "process.env.NODE_ENV": '"test"'
91
+ },
92
+ logLevel: "warning"
93
+ };
94
+ await ESBuild.build(buildOptions);
95
+ return outPath;
96
+ }
97
+ function parseTapOutput(output) {
98
+ let passed = 0;
99
+ let failed = 0;
100
+ const errors = [];
101
+ const lines = output.split("\n");
102
+ let lastTestName = "";
103
+ let lastTestPassed = true;
104
+ for (let i = 0; i < lines.length; i++) {
105
+ const line = lines[i];
106
+ const okMatch = line.match(/^\s*ok \d+ - (.+)/);
107
+ const notOkMatch = line.match(/^\s*not ok \d+ - (.+)/);
108
+ if (okMatch) {
109
+ lastTestName = okMatch[1];
110
+ lastTestPassed = true;
111
+ } else if (notOkMatch) {
112
+ lastTestName = notOkMatch[1];
113
+ lastTestPassed = false;
114
+ }
115
+ if (line.includes("type: 'test'") || line.includes('type: "test"')) {
116
+ if (lastTestPassed) {
117
+ passed++;
118
+ } else {
119
+ failed++;
120
+ errors.push({ name: lastTestName, error: "Test failed" });
121
+ }
122
+ }
123
+ }
124
+ return { passed, failed, errors };
125
+ }
126
+ async function runNodeTests(bundlePath, timeout) {
127
+ const { spawn } = await import("child_process");
128
+ return new Promise((resolve) => {
129
+ const child = spawn("node", ["--test", "--test-reporter=tap", bundlePath], {
130
+ stdio: ["pipe", "pipe", "pipe"],
131
+ timeout
132
+ });
133
+ let stdout = "";
134
+ let stderr = "";
135
+ let pendingTest = null;
136
+ const printedTests = /* @__PURE__ */ new Set();
137
+ child.stdout?.on("data", (data) => {
138
+ const text = data.toString();
139
+ stdout += text;
140
+ for (const line of text.split("\n")) {
141
+ const okMatch = line.match(/^\s*ok \d+ - (.+)/);
142
+ const notOkMatch = line.match(/^\s*not ok \d+ - (.+)/);
143
+ if (okMatch) {
144
+ pendingTest = { name: okMatch[1], passed: true };
145
+ } else if (notOkMatch) {
146
+ pendingTest = { name: notOkMatch[1], passed: false };
147
+ }
148
+ if ((line.includes("type: 'test'") || line.includes('type: "test"')) && pendingTest) {
149
+ const key = `${pendingTest.name}-${pendingTest.passed}`;
150
+ if (!printedTests.has(key)) {
151
+ printedTests.add(key);
152
+ if (pendingTest.passed) {
153
+ console.log(`\u2713 ${pendingTest.name}`);
154
+ } else {
155
+ console.log(`\u2717 ${pendingTest.name}`);
156
+ }
157
+ }
158
+ pendingTest = null;
159
+ }
160
+ }
161
+ });
162
+ child.stderr?.on("data", (data) => {
163
+ stderr += data.toString();
164
+ process.stderr.write(data);
165
+ });
166
+ child.on("close", () => {
167
+ const { passed, failed, errors } = parseTapOutput(stdout);
168
+ resolve({
169
+ platform: "node",
170
+ passed,
171
+ failed,
172
+ errors
173
+ });
174
+ });
175
+ child.on("error", (err) => {
176
+ resolve({
177
+ platform: "node",
178
+ passed: 0,
179
+ failed: 1,
180
+ errors: [{ name: "spawn error", error: err.message }]
181
+ });
182
+ });
183
+ });
184
+ }
185
+ function parseBunOutput(output) {
186
+ let passed = 0;
187
+ let failed = 0;
188
+ const errors = [];
189
+ const passMatch = output.match(/^\s*(\d+)\s+pass/m);
190
+ const failMatch = output.match(/^\s*(\d+)\s+fail/m);
191
+ if (passMatch)
192
+ passed = parseInt(passMatch[1], 10);
193
+ if (failMatch)
194
+ failed = parseInt(failMatch[1], 10);
195
+ return { passed, failed, errors };
196
+ }
197
+ async function runBunTests(bundlePath, timeout) {
198
+ const { spawn } = await import("child_process");
199
+ return new Promise((resolve) => {
200
+ const child = spawn("bun", ["test", bundlePath], {
201
+ stdio: ["pipe", "pipe", "pipe"],
202
+ timeout
203
+ });
204
+ let stdout = "";
205
+ let stderr = "";
206
+ child.stdout?.on("data", (data) => {
207
+ const text = data.toString();
208
+ stdout += text;
209
+ process.stdout.write(data);
210
+ });
211
+ child.stderr?.on("data", (data) => {
212
+ const text = data.toString();
213
+ stderr += text;
214
+ process.stderr.write(data);
215
+ });
216
+ child.on("close", () => {
217
+ const { passed, failed, errors } = parseBunOutput(stdout + stderr);
218
+ resolve({
219
+ platform: "bun",
220
+ passed,
221
+ failed,
222
+ errors
223
+ });
224
+ });
225
+ child.on("error", (err) => {
226
+ resolve({
227
+ platform: "bun",
228
+ passed: 0,
229
+ failed: 1,
230
+ errors: [{ name: "spawn error", error: err.message }]
231
+ });
232
+ });
233
+ });
234
+ }
235
+ async function runBrowserTests(bundlePath, browser, timeout, debug, cwd) {
236
+ let playwright;
237
+ try {
238
+ const require2 = createRequire(Path.join(cwd, "package.json"));
239
+ playwright = require2("playwright");
240
+ } catch {
241
+ console.error("Playwright is required for browser tests.");
242
+ console.error("Install it with: npm install -D playwright");
243
+ return {
244
+ platform: `browser (${browser})`,
245
+ passed: 0,
246
+ failed: 1,
247
+ errors: [{ name: "setup", error: "Playwright not installed" }]
248
+ };
249
+ }
250
+ const bundleContent = await FS.readFile(bundlePath, "utf-8");
251
+ const html = `<!DOCTYPE html>
252
+ <html>
253
+ <head>
254
+ <meta charset="utf-8">
255
+ <title>libuild tests</title>
256
+ </head>
257
+ <body>
258
+ <script type="module">
259
+ ${bundleContent}
260
+ </script>
261
+ </body>
262
+ </html>`;
263
+ let server;
264
+ let port;
265
+ await new Promise((resolve) => {
266
+ server = createServer((req, res) => {
267
+ res.setHeader("Content-Type", "text/html");
268
+ res.end(html);
269
+ });
270
+ server.listen(0, () => {
271
+ const addr = server.address();
272
+ port = typeof addr === "object" && addr ? addr.port : 3e3;
273
+ resolve();
274
+ });
275
+ });
276
+ try {
277
+ const browserInstance = await playwright[browser].launch({
278
+ headless: !debug
279
+ });
280
+ const context = await browserInstance.newContext();
281
+ const page = await context.newPage();
282
+ page.on("console", (msg) => {
283
+ const type = msg.type();
284
+ const text = msg.text();
285
+ if (type === "error") {
286
+ console.error(text);
287
+ } else {
288
+ console.log(text);
289
+ }
290
+ });
291
+ page.on("pageerror", (err) => {
292
+ console.error("Page error:", err.message);
293
+ });
294
+ await page.goto(`http://localhost:${port}/`);
295
+ await page.waitForFunction(
296
+ () => globalThis.__LIBUILD_TEST__?.ended === true,
297
+ { timeout }
298
+ );
299
+ const results = await page.evaluate(() => globalThis.__LIBUILD_TEST__);
300
+ if (!debug) {
301
+ await browserInstance.close();
302
+ } else {
303
+ console.log("\nDebug mode: browser left open. Press Ctrl+C to exit.");
304
+ await new Promise(() => {
305
+ });
306
+ }
307
+ return {
308
+ platform: `browser (${browser})`,
309
+ passed: results.passed,
310
+ failed: results.failed,
311
+ errors: results.errors
312
+ };
313
+ } finally {
314
+ server.close();
315
+ }
316
+ }
317
+ function printResults(results) {
318
+ console.log("\n" + "=".repeat(60));
319
+ console.log("Test Results Summary");
320
+ console.log("=".repeat(60));
321
+ let allPassed = true;
322
+ for (const result of results) {
323
+ const status = result.failed === 0 ? "\u2713" : "\u2717";
324
+ const color = result.failed === 0 ? "\x1B[32m" : "\x1B[31m";
325
+ const reset = "\x1B[0m";
326
+ console.log(
327
+ `${color}${status}${reset} ${result.platform}: ${result.passed} passed, ${result.failed} failed`
328
+ );
329
+ if (result.failed > 0) {
330
+ allPassed = false;
331
+ for (const error of result.errors) {
332
+ console.log(` \u2717 ${error.name}`);
333
+ console.log(` ${error.error}`);
334
+ }
335
+ }
336
+ }
337
+ console.log("=".repeat(60));
338
+ return allPassed;
339
+ }
340
+ async function runTests(options = {}) {
341
+ const opts = {
342
+ cwd: options.cwd || process.cwd(),
343
+ patterns: options.patterns || DEFAULT_PATTERNS,
344
+ platforms: options.platforms || ["bun"],
345
+ debug: options.debug || false,
346
+ timeout: options.timeout || 6e4,
347
+ watch: options.watch || false
348
+ };
349
+ console.log("Finding test files...");
350
+ const testFiles = await findTestFiles(opts.cwd, opts.patterns);
351
+ if (testFiles.length === 0) {
352
+ console.log("No test files found.");
353
+ return true;
354
+ }
355
+ console.log(`Found ${testFiles.length} test file(s)`);
356
+ const tempDir = Path.join(opts.cwd, ".libuild-test");
357
+ await FS.mkdir(tempDir, { recursive: true });
358
+ const results = [];
359
+ try {
360
+ for (const platform of opts.platforms) {
361
+ console.log(`
362
+ Building tests for ${platform}...`);
363
+ const bundlePath = await bundleTests(testFiles, platform, tempDir, opts.cwd);
364
+ console.log(`Running tests on ${platform}...`);
365
+ let result;
366
+ if (platform === "bun") {
367
+ result = await runBunTests(bundlePath, opts.timeout);
368
+ } else if (platform === "node") {
369
+ result = await runNodeTests(bundlePath, opts.timeout);
370
+ } else {
371
+ result = await runBrowserTests(bundlePath, platform, opts.timeout, opts.debug, opts.cwd);
372
+ }
373
+ results.push(result);
374
+ }
375
+ return printResults(results);
376
+ } finally {
377
+ if (!opts.debug) {
378
+ await FS.rm(tempDir, { recursive: true, force: true }).catch(() => {
379
+ });
380
+ }
381
+ }
382
+ }
383
+ async function detectPlatforms() {
384
+ const platforms = [];
385
+ const { spawn } = await import("child_process");
386
+ try {
387
+ await new Promise((resolve, reject) => {
388
+ const child = spawn("bun", ["--version"], { stdio: "ignore" });
389
+ child.on("close", (code) => code === 0 ? resolve() : reject());
390
+ child.on("error", reject);
391
+ });
392
+ platforms.push("bun");
393
+ } catch {
394
+ }
395
+ platforms.push("node");
396
+ try {
397
+ await import("playwright");
398
+ platforms.push("browser");
399
+ } catch {
400
+ }
401
+ return platforms;
402
+ }
403
+ export {
404
+ detectPlatforms,
405
+ runTests
406
+ };
package/src/test.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @b9g/libuild/test - Cross-platform test utilities
3
+ *
4
+ * Provides a unified testing API across Bun, Node, and browsers.
5
+ * Uses top-level await to detect runtime and load appropriate shim.
6
+ */
7
+ declare const describe: typeof import("node:test").describe | import("bun:test").Describe<[]>, test: typeof import("node:test") | import("bun:test").Test<[]>, it: typeof import("node:test") | import("bun:test").Test<[]>, expect: import("expect").Expect | import("bun:test").Expect, beforeAll: typeof import("node:test").before | typeof import("bun:test").beforeAll, afterAll: typeof import("node:test").after | typeof import("bun:test").afterAll, beforeEach: typeof import("node:test").beforeEach | typeof import("bun:test").beforeEach, afterEach: typeof import("node:test").afterEach | typeof import("bun:test").afterEach;
8
+ export { describe, test, it, expect, beforeAll, afterAll, beforeEach, afterEach, };
package/src/test.js ADDED
@@ -0,0 +1,25 @@
1
+ /// <reference types="./test.d.ts" />
2
+ import "./_chunks/chunk-IBOCQ33F.js";
3
+
4
+ // src/test.ts
5
+ var isBun = typeof Bun !== "undefined";
6
+ var {
7
+ describe,
8
+ test,
9
+ it,
10
+ expect,
11
+ beforeAll,
12
+ afterAll,
13
+ beforeEach,
14
+ afterEach
15
+ } = isBun ? await import("./test-bun.js") : await import("./test-node.js");
16
+ export {
17
+ afterAll,
18
+ afterEach,
19
+ beforeAll,
20
+ beforeEach,
21
+ describe,
22
+ expect,
23
+ it,
24
+ test
25
+ };
package/src/cli.cjs DELETED
@@ -1,183 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- var __create = Object.create;
4
- var __defProp = Object.defineProperty;
5
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
- var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __getProtoOf = Object.getPrototypeOf;
8
- var __hasOwnProp = Object.prototype.hasOwnProperty;
9
- var __copyProps = (to, from, except, desc) => {
10
- if (from && typeof from === "object" || typeof from === "function") {
11
- for (let key of __getOwnPropNames(from))
12
- if (!__hasOwnProp.call(to, key) && key !== except)
13
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
- }
15
- return to;
16
- };
17
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
- // If the importer is in node compatibility mode or this is not an ESM
19
- // file that has been converted to a CommonJS file using a Babel-
20
- // compatible transform (i.e. "__esModule" has not been set), then set
21
- // "default" to the CommonJS "module.exports" for node compatibility.
22
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
- mod
24
- ));
25
-
26
- // src/cli.ts
27
- var import_util = require("util");
28
- var Path = __toESM(require("path"), 1);
29
- var import_libuild = require("./libuild.cjs");
30
- var { values, positionals } = (0, import_util.parseArgs)({
31
- args: process.argv.slice(2),
32
- options: {
33
- help: { type: "boolean", short: "h" },
34
- version: { type: "boolean", short: "v" },
35
- save: { type: "boolean" },
36
- "no-save": { type: "boolean" }
37
- },
38
- allowPositionals: true,
39
- strict: false
40
- // Allow unknown options to be passed through
41
- });
42
- var HELP_TEXT = `
43
- libuild - Zero-config library builds
44
-
45
- Usage:
46
- libuild [command] [directory] [options]
47
-
48
- Commands:
49
- build Build the library (default command)
50
- publish Build and publish the library
51
-
52
- Arguments:
53
- directory Optional directory to build (defaults to current directory)
54
-
55
- Options:
56
- --save Update root package.json to point to dist files
57
- --no-save Skip package.json updates (for publish command)
58
-
59
- IMPORTANT:
60
- \u2022 libuild is zero-config - there is NO libuild.config.js file
61
- \u2022 Configuration comes from your package.json (main, module, exports, etc.)
62
- \u2022 Use --save to regenerate package.json fields based on built files
63
- \u2022 Invalid bin/exports paths are automatically cleaned up with --save
64
-
65
- For publish command, all additional flags are forwarded to npm publish.
66
-
67
- Examples:
68
- libuild # Build the library in current directory
69
- libuild build # Same as above
70
- libuild ../other-proj # Build library in ../other-proj
71
- libuild build --save # Build and update package.json for npm link
72
- libuild publish ../lib # Build and publish library in ../lib
73
- libuild publish --dry-run --tag beta # Build and publish with npm flags
74
- `;
75
- async function main() {
76
- if (values.help) {
77
- console.log(HELP_TEXT);
78
- process.exit(0);
79
- }
80
- if (values.version) {
81
- const pkg = await import("../package.json");
82
- console.log(pkg.version);
83
- process.exit(0);
84
- }
85
- const command = positionals[0] || "build";
86
- let targetDir;
87
- for (let i = 1; i < positionals.length; i++) {
88
- const arg = positionals[i];
89
- const flagValueFlags = ["--tag", "--access", "--registry", "--otp", "--workspace"];
90
- const isValueForFlag = flagValueFlags.some((flag) => {
91
- const flagIndex = process.argv.indexOf(flag);
92
- const argIndex = process.argv.indexOf(arg);
93
- return flagIndex !== -1 && argIndex === flagIndex + 1;
94
- });
95
- if (isValueForFlag) {
96
- continue;
97
- }
98
- if (!arg.startsWith("--")) {
99
- try {
100
- const isSystemPath = Path.isAbsolute(arg) && (arg.startsWith("/bin/") || arg.startsWith("/usr/") || arg.startsWith("/etc/") || arg.startsWith("/var/") || arg.startsWith("/opt/") || arg.startsWith("/tmp/") || arg.startsWith("/System/") || arg.startsWith("/Applications/"));
101
- if (isSystemPath) {
102
- continue;
103
- }
104
- const resolvedPath = Path.resolve(arg);
105
- if (arg.includes("/") || arg.includes("\\") || arg === "." || arg === ".." || arg.startsWith("./") || arg.startsWith("../")) {
106
- targetDir = arg;
107
- break;
108
- }
109
- const fs = await import("fs/promises");
110
- try {
111
- const stat = await fs.stat(resolvedPath);
112
- if (stat.isDirectory() && !Path.isAbsolute(arg)) {
113
- targetDir = arg;
114
- break;
115
- }
116
- } catch {
117
- }
118
- } catch {
119
- }
120
- }
121
- }
122
- const cwd = targetDir ? Path.resolve(targetDir) : process.cwd();
123
- const shouldSave = values.save || command === "publish" && !values["no-save"];
124
- const allowedNpmFlags = [
125
- "--dry-run",
126
- "--tag",
127
- "--access",
128
- "--registry",
129
- "--otp",
130
- "--provenance",
131
- "--workspace",
132
- "--workspaces",
133
- "--include-workspace-root"
134
- ];
135
- const rawExtraArgs = process.argv.slice(2).filter(
136
- (arg) => !["build", "publish"].includes(arg) && !["--save", "--no-save", "--help", "-h", "--version", "-v"].includes(arg) && arg !== targetDir
137
- // Don't filter out the target directory
138
- );
139
- const extraArgs = [];
140
- for (let i = 0; i < rawExtraArgs.length; i++) {
141
- const arg = rawExtraArgs[i];
142
- if (arg.startsWith("--")) {
143
- const flagName = arg.split("=")[0];
144
- if (allowedNpmFlags.includes(flagName)) {
145
- extraArgs.push(arg);
146
- if (!arg.includes("=") && ["--tag", "--access", "--registry", "--otp", "--workspace"].includes(flagName)) {
147
- if (i + 1 < rawExtraArgs.length && !rawExtraArgs[i + 1].startsWith("--")) {
148
- extraArgs.push(rawExtraArgs[i + 1]);
149
- i++;
150
- }
151
- }
152
- } else {
153
- console.warn(`Warning: Ignoring unknown/unsafe npm flag: ${arg}`);
154
- }
155
- } else {
156
- const prevArg = i > 0 ? rawExtraArgs[i - 1] : "";
157
- const isPrevArgValueFlag = ["--tag", "--access", "--registry", "--otp", "--workspace"].includes(prevArg) && !prevArg.includes("=");
158
- if (isPrevArgValueFlag && extraArgs.includes(prevArg)) {
159
- extraArgs.push(arg);
160
- } else {
161
- console.warn(`Warning: Ignoring unexpected argument: ${arg}`);
162
- }
163
- }
164
- }
165
- try {
166
- switch (command) {
167
- case "build":
168
- await (0, import_libuild.build)(cwd, shouldSave);
169
- break;
170
- case "publish":
171
- await (0, import_libuild.publish)(cwd, shouldSave, extraArgs);
172
- break;
173
- default:
174
- console.error(`Unknown command: ${command}`);
175
- console.log(HELP_TEXT);
176
- process.exit(1);
177
- }
178
- } catch (error) {
179
- console.error("Error:", error.message);
180
- process.exit(1);
181
- }
182
- }
183
- main();