@jk2908/solas 0.4.4 → 0.5.1

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.
@@ -4,6 +4,7 @@ import { BasePath } from '../utils/base-path.js';
4
4
  import { Logger } from '../utils/logger.js';
5
5
  import { Solas } from '../solas.js';
6
6
  import { toPathPattern } from './http-router/utils.js';
7
+ import { Runtime as AdapterRuntime } from './runtimes/runtime.js';
7
8
  const logger = new Logger();
8
9
  export { Prerender };
9
10
  var Prerender;
@@ -60,18 +61,18 @@ var Prerender;
60
61
  * Load the postponed state for a given route from the file system, if it exists
61
62
  */
62
63
  async function loadPostponedState(outDir, pathname) {
63
- let file;
64
+ let filePath;
64
65
  try {
65
- file = Bun.file(path.join(getPath(outDir, pathname), 'postponed.json'));
66
+ filePath = path.join(getPath(outDir, pathname), 'postponed.json');
66
67
  }
67
68
  catch (err) {
68
69
  logger.warn(`[prerender:artifacts] rejected postponed state path for ${pathname}`, Logger.print(err));
69
70
  return null;
70
71
  }
71
- if (!(await file.exists()))
72
+ if (!(await AdapterRuntime.exists(filePath)))
72
73
  return null;
73
74
  try {
74
- return JSON.parse(await file.text());
75
+ return JSON.parse(await AdapterRuntime.readText(filePath));
75
76
  }
76
77
  catch {
77
78
  return null;
@@ -82,18 +83,18 @@ var Prerender;
82
83
  * Load the prelude HTML for a given route from the file system, if it exists
83
84
  */
84
85
  async function loadPrelude(outDir, pathname) {
85
- let file;
86
+ let filePath;
86
87
  try {
87
- file = Bun.file(path.join(getPath(outDir, pathname), 'prelude.html'));
88
+ filePath = path.join(getPath(outDir, pathname), 'prelude.html');
88
89
  }
89
90
  catch (err) {
90
91
  logger.warn(`[prerender:artifacts] rejected prelude path for ${pathname}`, Logger.print(err));
91
92
  return null;
92
93
  }
93
- if (!(await file.exists()))
94
+ if (!(await AdapterRuntime.exists(filePath)))
94
95
  return null;
95
96
  try {
96
- return await file.text();
97
+ return await AdapterRuntime.readText(filePath);
97
98
  }
98
99
  catch {
99
100
  return null;
@@ -104,18 +105,18 @@ var Prerender;
104
105
  * Load the prerender artifact metadata for a given route from the file system, if it exists and is valid
105
106
  */
106
107
  async function loadMetadata(outDir, pathname) {
107
- let file;
108
+ let filePath;
108
109
  try {
109
- file = Bun.file(path.join(getPath(outDir, pathname), 'metadata.json'));
110
+ filePath = path.join(getPath(outDir, pathname), 'metadata.json');
110
111
  }
111
112
  catch (err) {
112
113
  logger.warn(`[prerender:artifacts] rejected metadata path for ${pathname}`, Logger.print(err));
113
114
  return null;
114
115
  }
115
- if (!(await file.exists()))
116
+ if (!(await AdapterRuntime.exists(filePath)))
116
117
  return null;
117
118
  try {
118
- const value = JSON.parse(await file.text());
119
+ const value = JSON.parse(await AdapterRuntime.readText(filePath));
119
120
  if (!value || typeof value !== 'object')
120
121
  return null;
121
122
  const schema = value.schema;
@@ -270,7 +271,7 @@ var Prerender;
270
271
  const params = await buildContext.exportReader.value(filePath, 'params', (v) => typeof v === 'function');
271
272
  if (!params)
272
273
  return [];
273
- const resolved = await Promise.try(() => params());
274
+ const resolved = await Promise.resolve().then(() => params());
274
275
  if (!Array.isArray(resolved))
275
276
  return [];
276
277
  return resolved;
@@ -0,0 +1,9 @@
1
+ import { RuntimeBase } from './runtime.js';
2
+ export declare class RuntimeBun extends RuntimeBase {
3
+ exists(filePath: string): Promise<boolean>;
4
+ readText(filePath: string): Promise<string>;
5
+ readBuffer(filePath: string): Promise<ArrayBuffer>;
6
+ mimeType(filePath: string): string;
7
+ write(filePath: string, content: string | NodeJS.ArrayBufferView): Promise<void>;
8
+ hash(value: string): string;
9
+ }
@@ -0,0 +1,33 @@
1
+ import { RuntimeBase } from './runtime.js';
2
+ export class RuntimeBun extends RuntimeBase {
3
+ async exists(filePath) {
4
+ return Bun.file(filePath).exists();
5
+ }
6
+ readText(filePath) {
7
+ return Bun.file(filePath).text();
8
+ }
9
+ readBuffer(filePath) {
10
+ return Bun.file(filePath).arrayBuffer();
11
+ }
12
+ mimeType(filePath) {
13
+ return Bun.file(filePath).type || 'application/octet-stream';
14
+ }
15
+ async write(filePath, content) {
16
+ // normalise wider arraybuffer views into a shape Bun.write accepts directly
17
+ await Bun.write(filePath, typeof content === 'string'
18
+ ? content
19
+ : content instanceof Uint8Array
20
+ ? content
21
+ : new Uint8Array(content.buffer, content.byteOffset, content.byteLength));
22
+ }
23
+ hash(value) {
24
+ const hash = Bun.hash(value);
25
+ // Bun.hash returns an integer-like value, so keep it in BigInt space and avoid
26
+ // precision loss. Clamp it to an unsigned 64-bit value before hex
27
+ // formatting. Pad to 16 chars to match the Node runtime
28
+ // output shape
29
+ return BigInt.asUintN(64, typeof hash === 'bigint' ? hash : BigInt(hash))
30
+ .toString(16)
31
+ .padStart(16, '0');
32
+ }
33
+ }
@@ -0,0 +1 @@
1
+ export declare function getMimeTypeFromPath(filePath: string): string;
@@ -0,0 +1,35 @@
1
+ import path from 'node:path';
2
+ const MIME_BY_EXT = {
3
+ '.txt': 'text/plain; charset=utf-8',
4
+ '.css': 'text/css; charset=utf-8',
5
+ '.html': 'text/html; charset=utf-8',
6
+ '.js': 'text/javascript; charset=utf-8',
7
+ '.mjs': 'text/javascript; charset=utf-8',
8
+ '.cjs': 'text/javascript; charset=utf-8',
9
+ '.json': 'application/json; charset=utf-8',
10
+ '.map': 'application/json; charset=utf-8',
11
+ '.xml': 'application/xml; charset=utf-8',
12
+ '.svg': 'image/svg+xml',
13
+ '.png': 'image/png',
14
+ '.jpg': 'image/jpeg',
15
+ '.jpeg': 'image/jpeg',
16
+ '.gif': 'image/gif',
17
+ '.webp': 'image/webp',
18
+ '.avif': 'image/avif',
19
+ '.ico': 'image/x-icon',
20
+ '.woff': 'font/woff',
21
+ '.woff2': 'font/woff2',
22
+ '.ttf': 'font/ttf',
23
+ '.otf': 'font/otf',
24
+ '.eot': 'application/vnd.ms-fontobject',
25
+ '.pdf': 'application/pdf',
26
+ '.wasm': 'application/wasm',
27
+ '.mp4': 'video/mp4',
28
+ '.webm': 'video/webm',
29
+ '.mp3': 'audio/mpeg',
30
+ '.ogg': 'audio/ogg',
31
+ };
32
+ export function getMimeTypeFromPath(filePath) {
33
+ const ext = path.extname(filePath).toLowerCase();
34
+ return MIME_BY_EXT[ext] ?? 'application/octet-stream';
35
+ }
@@ -0,0 +1,9 @@
1
+ import { RuntimeBase } from './runtime.js';
2
+ export declare class RuntimeNode extends RuntimeBase {
3
+ exists(filePath: string): Promise<boolean>;
4
+ readText(filePath: string): Promise<string>;
5
+ readBuffer(filePath: string): Promise<ArrayBuffer>;
6
+ mimeType(filePath: string): string;
7
+ write(filePath: string, content: string | NodeJS.ArrayBufferView): Promise<void>;
8
+ hash(value: string): string;
9
+ }
@@ -0,0 +1,31 @@
1
+ import { createHash } from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import { getMimeTypeFromPath } from './mime.js';
4
+ import { RuntimeBase } from './runtime.js';
5
+ export class RuntimeNode extends RuntimeBase {
6
+ async exists(filePath) {
7
+ try {
8
+ await fs.access(filePath);
9
+ return true;
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
15
+ readText(filePath) {
16
+ return fs.readFile(filePath, 'utf-8');
17
+ }
18
+ async readBuffer(filePath) {
19
+ const buffer = await fs.readFile(filePath);
20
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
21
+ }
22
+ mimeType(filePath) {
23
+ return getMimeTypeFromPath(filePath);
24
+ }
25
+ async write(filePath, content) {
26
+ await fs.writeFile(filePath, content);
27
+ }
28
+ hash(value) {
29
+ return createHash('sha256').update(value).digest('hex').slice(0, 16);
30
+ }
31
+ }
@@ -0,0 +1,29 @@
1
+ export declare namespace Runtime {
2
+ type Impl = {
3
+ exists: (filePath: string) => Promise<boolean>;
4
+ readText: (filePath: string) => Promise<string>;
5
+ readBuffer: (filePath: string) => Promise<ArrayBuffer>;
6
+ mimeType: (filePath: string) => string;
7
+ write: (filePath: string, content: string | NodeJS.ArrayBufferView) => Promise<void>;
8
+ hash: (value: string) => string;
9
+ };
10
+ }
11
+ export declare abstract class RuntimeBase implements Runtime.Impl {
12
+ abstract exists(filePath: string): Promise<boolean>;
13
+ abstract readText(filePath: string): Promise<string>;
14
+ abstract readBuffer(filePath: string): Promise<ArrayBuffer>;
15
+ abstract mimeType(filePath: string): string;
16
+ abstract write(filePath: string, content: string | NodeJS.ArrayBufferView): Promise<void>;
17
+ abstract hash(value: string): string;
18
+ }
19
+ export declare class Runtime {
20
+ #private;
21
+ static set runtime(runtime: Runtime.Impl);
22
+ static get runtime(): Runtime.Impl;
23
+ static exists(filePath: string): Promise<boolean>;
24
+ static readText(filePath: string): Promise<string>;
25
+ static readBuffer(filePath: string): Promise<ArrayBuffer>;
26
+ static mimeType(filePath: string): string;
27
+ static write(filePath: string, content: string | NodeJS.ArrayBufferView): Promise<void>;
28
+ static hash(value: string): string;
29
+ }
@@ -0,0 +1,32 @@
1
+ export class RuntimeBase {
2
+ }
3
+ export class Runtime {
4
+ static #runtime;
5
+ static set runtime(runtime) {
6
+ this.#runtime = runtime;
7
+ }
8
+ static get runtime() {
9
+ if (!this.#runtime) {
10
+ throw new Error('No runtime configured. Please set the runtime implementation before using runtime methods.');
11
+ }
12
+ return this.#runtime;
13
+ }
14
+ static exists(filePath) {
15
+ return this.runtime.exists(filePath);
16
+ }
17
+ static readText(filePath) {
18
+ return this.runtime.readText(filePath);
19
+ }
20
+ static readBuffer(filePath) {
21
+ return this.runtime.readBuffer(filePath);
22
+ }
23
+ static mimeType(filePath) {
24
+ return this.runtime.mimeType(filePath);
25
+ }
26
+ static write(filePath, content) {
27
+ return this.runtime.write(filePath, content);
28
+ }
29
+ static hash(value) {
30
+ return this.runtime.hash(value);
31
+ }
32
+ }
@@ -0,0 +1,13 @@
1
+ import { RuntimeBase } from '../internal/runtimes/runtime.js';
2
+ declare class BunAdapter extends RuntimeBase {
3
+ readonly name: "bun";
4
+ readonly module: string;
5
+ exists(filePath: string): Promise<boolean>;
6
+ readText(filePath: string): Promise<string>;
7
+ readBuffer(filePath: string): Promise<ArrayBuffer>;
8
+ mimeType(filePath: string): string;
9
+ write(filePath: string, content: string | NodeJS.ArrayBufferView): Promise<void>;
10
+ hash(value: string): string;
11
+ }
12
+ export default function bunAdapter(): BunAdapter;
13
+ export {};
@@ -0,0 +1,39 @@
1
+ import { RuntimeBase } from '../internal/runtimes/runtime.js';
2
+ import { Solas } from '../solas.js';
3
+ class BunAdapter extends RuntimeBase {
4
+ name = 'bun';
5
+ module = `${Solas.Config.PKG_NAME}/runtimes/bun`;
6
+ async exists(filePath) {
7
+ return Bun.file(filePath).exists();
8
+ }
9
+ readText(filePath) {
10
+ return Bun.file(filePath).text();
11
+ }
12
+ readBuffer(filePath) {
13
+ return Bun.file(filePath).arrayBuffer();
14
+ }
15
+ mimeType(filePath) {
16
+ return Bun.file(filePath).type || 'application/octet-stream';
17
+ }
18
+ async write(filePath, content) {
19
+ // normalise wider arraybuffer views into a shape Bun.write accepts directly
20
+ await Bun.write(filePath, typeof content === 'string'
21
+ ? content
22
+ : content instanceof Uint8Array
23
+ ? content
24
+ : new Uint8Array(content.buffer, content.byteOffset, content.byteLength));
25
+ }
26
+ hash(value) {
27
+ const hash = Bun.hash(value);
28
+ // Bun.hash returns an integer-like value, so keep it in BigInt space and avoid
29
+ // precision loss. Clamp it to an unsigned 64-bit value before hex
30
+ // formatting. Pad to 16 chars to match the Node adapter
31
+ // output shape
32
+ return BigInt.asUintN(64, typeof hash === 'bigint' ? hash : BigInt(hash))
33
+ .toString(16)
34
+ .padStart(16, '0');
35
+ }
36
+ }
37
+ export default function bunAdapter() {
38
+ return new BunAdapter();
39
+ }
package/dist/solas.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Prerender } from './internal/prerender.js';
2
- import type { PluginConfig } from './types.js';
2
+ import type { PluginConfig, Runtime } from './types.js';
3
+ import { Runtime as InternalRuntime } from './internal/runtimes/runtime.js';
3
4
  export declare namespace Solas {
4
5
  interface Routes {
5
6
  }
@@ -18,6 +19,7 @@ export declare namespace Solas {
18
19
  const $: unique symbol;
19
20
  const REQUEST_META_KEY: string;
20
21
  const LOG_LEVELS: readonly ["debug", "info", "warn", "error", "fatal"];
22
+ const RUNTIMES: readonly ["auto", "node", "bun"];
21
23
  const PRERENDER_MODES: readonly ["full", "ppr", false];
22
24
  const TRAILING_SLASH_MODES: readonly ["always", "never", "ignore"];
23
25
  const RUNTIME_MANIFEST = "runtime-manifest.json";
@@ -34,6 +36,7 @@ export declare namespace Solas {
34
36
  artifacts: Prerender.Artifact.Manifest;
35
37
  publicFiles: ReadonlySet<string>;
36
38
  };
39
+ function create(runtime: Runtime): InternalRuntime.Impl;
37
40
  function getManifestPath(outDir: string): string;
38
41
  function loadManifest(outDir: string): Promise<Manifest | null>;
39
42
  }
package/dist/solas.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { RuntimeBun } from './internal/runtimes/bun.js';
2
+ import { RuntimeNode } from './internal/runtimes/node.js';
3
+ import { Runtime as InternalRuntime } from './internal/runtimes/runtime.js';
1
4
  export { Solas };
2
5
  var Solas;
3
6
  (function (Solas) {
@@ -17,10 +20,12 @@ var Solas;
17
20
  Config.$ = Symbol(Config.SLUG);
18
21
  Config.REQUEST_META_KEY = `__${Config.SLUG.toUpperCase()}__`;
19
22
  Config.LOG_LEVELS = ['debug', 'info', 'warn', 'error', 'fatal'];
23
+ Config.RUNTIMES = ['auto', 'node', 'bun'];
20
24
  Config.PRERENDER_MODES = ['full', 'ppr', false];
21
25
  Config.TRAILING_SLASH_MODES = ['always', 'never', 'ignore'];
22
26
  Config.RUNTIME_MANIFEST = 'runtime-manifest.json';
23
27
  const CONFIG_KEYS = new Set([
28
+ 'runtime',
24
29
  'port',
25
30
  'logger',
26
31
  'metadata',
@@ -49,6 +54,11 @@ var Solas;
49
54
  errors.push(`Unknown config key: ${key}`);
50
55
  }
51
56
  }
57
+ if ('runtime' in input &&
58
+ input.runtime !== undefined &&
59
+ !isRuntime(input.runtime)) {
60
+ errors.push("config.runtime must be 'auto', 'node', or 'bun'");
61
+ }
52
62
  if ('url' in input && input.url !== undefined) {
53
63
  if (typeof input.url !== 'string') {
54
64
  errors.push('config.url must be a string');
@@ -161,6 +171,14 @@ var Solas;
161
171
  Solas.getVersion = getVersion;
162
172
  let Runtime;
163
173
  (function (Runtime) {
174
+ function create(runtime) {
175
+ if (runtime === 'bun' ||
176
+ (runtime === 'auto' && typeof globalThis.Bun !== 'undefined')) {
177
+ return new RuntimeBun();
178
+ }
179
+ return new RuntimeNode();
180
+ }
181
+ Runtime.create = create;
164
182
  const manifestCache = new Map();
165
183
  function getManifestPath(outDir) {
166
184
  return [outDir, Config.GENERATED_DIR, Config.RUNTIME_MANIFEST]
@@ -177,13 +195,13 @@ var Solas;
177
195
  if (manifestCache.has(outDir)) {
178
196
  return manifestCache.get(outDir) ?? null;
179
197
  }
180
- const file = Bun.file(getManifestPath(outDir));
181
- if (!(await file.exists())) {
198
+ const manifestPath = getManifestPath(outDir);
199
+ if (!(await InternalRuntime.exists(manifestPath))) {
182
200
  manifestCache.set(outDir, null);
183
201
  return null;
184
202
  }
185
203
  try {
186
- const value = JSON.parse(await file.text());
204
+ const value = JSON.parse(await InternalRuntime.readText(manifestPath));
187
205
  if (!isRecord(value)) {
188
206
  manifestCache.set(outDir, null);
189
207
  return null;
@@ -230,12 +248,12 @@ var Solas;
230
248
  return null;
231
249
  }
232
250
  }
233
- const manifest = {
251
+ const runtimeManifest = {
234
252
  artifacts: artifacts,
235
253
  publicFiles: new Set(publicFiles ?? []),
236
254
  };
237
- manifestCache.set(outDir, manifest);
238
- return manifest;
255
+ manifestCache.set(outDir, runtimeManifest);
256
+ return runtimeManifest;
239
257
  }
240
258
  catch {
241
259
  manifestCache.set(outDir, null);
@@ -255,3 +273,6 @@ var Solas;
255
273
  function isRecord(value) {
256
274
  return typeof value === 'object' && value !== null && !Array.isArray(value);
257
275
  }
276
+ function isRuntime(value) {
277
+ return typeof value === 'string' && new Set(Solas.Config.RUNTIMES).has(value);
278
+ }
package/dist/types.d.ts CHANGED
@@ -9,8 +9,10 @@ import type { Metadata } from './internal/metadata.js';
9
9
  import type { HttpException } from './internal/navigation/http-exception.js';
10
10
  import { Solas } from './solas.js';
11
11
  export type LogLevel = (typeof Solas.Config.LOG_LEVELS)[number];
12
+ export type Runtime = (typeof Solas.Config.RUNTIMES)[number];
12
13
  type Origin = `http://${string}` | `https://${string}`;
13
14
  type PluginConfigBase = {
15
+ runtime?: Runtime;
14
16
  port?: number;
15
17
  precompress?: boolean;
16
18
  prerender?: Route.Prerender;
@@ -30,7 +32,10 @@ export type PluginConfig = PluginConfigBase & ({
30
32
  url?: Origin;
31
33
  sitemap?: false;
32
34
  });
33
- export type RuntimeConfig = PluginConfig & {
35
+ export type ConfiguredPluginConfig = PluginConfig & {
36
+ runtime: Runtime;
37
+ };
38
+ export type RuntimeConfig = Omit<PluginConfig, 'runtime'> & {
34
39
  precompress: NonNullable<PluginConfig['precompress']>;
35
40
  trailingSlash: NonNullable<PluginConfig['trailingSlash']>;
36
41
  trustedOrigins: NonNullable<PluginConfig['trustedOrigins']>;
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { brotliCompress } from 'node:zlib';
5
+ import { Runtime } from '../internal/runtimes/runtime.js';
5
6
  export { Compress };
6
7
  var Compress;
7
8
  (function (Compress) {
@@ -25,8 +26,7 @@ var Compress;
25
26
  return output;
26
27
  }
27
28
  async function compress(input) {
28
- const file = Bun.file(input);
29
- const buffer = Buffer.from(await file.arrayBuffer());
29
+ const buffer = Buffer.from(await Runtime.readBuffer(input));
30
30
  const compressed = await new Promise((fulfill, reject) => {
31
31
  brotliCompress(buffer, (err, res) => {
32
32
  if (err) {
@@ -1,9 +1,10 @@
1
+ import fs from 'node:fs/promises';
1
2
  import path from 'node:path';
3
+ import { parseSync, } from 'oxc-parser';
2
4
  export class ExportReader {
3
- #transpilers = new Map();
4
5
  #loadModule = null;
5
6
  /**
6
- * Pick the Bun loader type that matches the source file extension
7
+ * Pick the parser language that matches the source file extension
7
8
  */
8
9
  static #getLoaderType(filePath) {
9
10
  const ext = path.extname(filePath).toLowerCase();
@@ -17,28 +18,6 @@ export class ExportReader {
17
18
  return 'tsx';
18
19
  throw new Error(`Unsupported module extension: ${ext || '(none)'} in ${filePath}`);
19
20
  }
20
- /**
21
- * Parse a literal value from a string
22
- */
23
- static #parse(value) {
24
- const trimmed = value.trim();
25
- // keep quoted literals as strings without evaluating the
26
- // source text
27
- if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
28
- (trimmed.startsWith("'") && trimmed.endsWith("'")) ||
29
- (trimmed.startsWith('`') && trimmed.endsWith('`'))) {
30
- return trimmed.slice(1, -1);
31
- }
32
- if (trimmed === 'true')
33
- return true;
34
- if (trimmed === 'false')
35
- return false;
36
- if (trimmed === 'null')
37
- return null;
38
- const n = Number(trimmed);
39
- if (Number.isFinite(n))
40
- return n;
41
- }
42
21
  /**
43
22
  * Set the Vite server's SSR module loader so we can execute modules
44
23
  */
@@ -46,30 +25,35 @@ export class ExportReader {
46
25
  this.#loadModule = l;
47
26
  }
48
27
  /**
49
- * Reuse one transpiler per supported loader so scans match the module syntax
28
+ * Parse a source file as an ESM route module
50
29
  */
51
- #getTranspiler(filePath) {
52
- const type = ExportReader.#getLoaderType(filePath);
53
- const cached = this.#transpilers.get(type);
54
- if (cached)
55
- return cached;
56
- const transpiler = new Bun.Transpiler({ loader: type });
57
- this.#transpilers.set(type, transpiler);
58
- return transpiler;
30
+ async #parse(filePath) {
31
+ const source = await this.raw(filePath);
32
+ const result = parseSync(filePath, source, {
33
+ lang: ExportReader.#getLoaderType(filePath),
34
+ sourceType: 'module',
35
+ preserveParens: false,
36
+ });
37
+ if (result.errors.length > 0) {
38
+ throw new Error(result.errors[0]?.message ?? `Failed to parse ${filePath}`);
39
+ }
40
+ return result;
59
41
  }
60
42
  /**
61
43
  * Read the raw text content of a file
62
44
  */
63
45
  async raw(filePath) {
64
- return Bun.file(filePath).text();
46
+ return fs.readFile(filePath, 'utf-8');
65
47
  }
66
48
  /**
67
49
  * Get the names of all exports from a file
68
50
  */
69
51
  async exports(filePath) {
70
- // use Bun's transpiler scan so we can inspect export names
71
- // without loading the module
72
- return this.#getTranspiler(filePath).scan(await this.raw(filePath)).exports;
52
+ const { module } = await this.#parse(filePath);
53
+ return Array.from(new Set(module.staticExports.flatMap((entry) => entry.entries
54
+ .filter((specifier) => !specifier.isType)
55
+ .map((specifier) => specifier.exportName.name)
56
+ .filter((name) => typeof name === 'string' && name.length > 0))));
73
57
  }
74
58
  /**
75
59
  * Check if a file exports a specific name
@@ -86,27 +70,8 @@ export class ExportReader {
86
70
  async literal(filePath, name, validate) {
87
71
  if (!(await this.has(filePath, name)))
88
72
  return;
89
- // transpile first so comments and type-only syntax do not confuse the
90
- // literal matcher with exports that do not actually exist at runtime
91
- const code = this.#getTranspiler(filePath).transformSync(await this.raw(filePath));
92
- // build the matcher from escaped plain-text pieces so arbitrary export names
93
- // cannot change the regex shape
94
- const source =
95
- // match: `export const|let|var ` at statement boundaries
96
- '(?:^|[;\\n])\\s*export\\s+(?:const|let|var)\\s+' +
97
- // treat export name as plain text in regex
98
- name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
99
- // capture one supported literal value (string, number, boolean, null)
100
- '\\s*=\\s*(?<value>(?:"(?:[^"\\\\]|\\\\.)*"|\'(?:[^\'\\\\]|\\\\.)*\'|\\x60(?:[^\\x60\\\\]|\\\\.)*\\x60|true|false|null|-?\\d+(?:\\.\\d+)?))(?=\\s|;|$)';
101
- // multiline mode lets ^ match the start of each transpiled line, so the
102
- // export regex stays anchored to a real statement boundary instead of
103
- // the file start
104
- const text = code.match(new RegExp(source, 'm'))?.groups?.value;
105
- if (!text)
106
- return;
107
- // only support cheap literal parsing here. Anything richer should go through
108
- // value() so module semantics stay correct
109
- const value = ExportReader.#parse(text);
73
+ const { program } = await this.#parse(filePath);
74
+ const value = ExportReader.#readLiteralExport(program, name);
110
75
  if (value === undefined)
111
76
  return;
112
77
  if (!validate || validate(value))
@@ -130,4 +95,50 @@ export class ExportReader {
130
95
  if (!validate || validate(value))
131
96
  return value;
132
97
  }
98
+ static #readLiteralExport(program, name) {
99
+ for (const statement of program.body) {
100
+ if (statement.type !== 'ExportNamedDeclaration')
101
+ continue;
102
+ const declaration = statement.declaration;
103
+ if (!declaration || declaration.type !== 'VariableDeclaration')
104
+ continue;
105
+ for (const declarator of declaration.declarations) {
106
+ if (declarator.id.type !== 'Identifier' || declarator.id.name !== name)
107
+ continue;
108
+ return ExportReader.#readLiteralValue(declarator.init);
109
+ }
110
+ }
111
+ }
112
+ static #readLiteralValue(value) {
113
+ if (!value || typeof value !== 'object' || !('type' in value))
114
+ return;
115
+ const node = value;
116
+ if (node.type === 'Literal') {
117
+ const literal = value;
118
+ if (typeof literal.value === 'string' ||
119
+ typeof literal.value === 'number' ||
120
+ typeof literal.value === 'boolean' ||
121
+ literal.value === null) {
122
+ return literal.value;
123
+ }
124
+ }
125
+ if (node.type === 'TemplateLiteral' &&
126
+ Array.isArray(value.expressions) &&
127
+ value.expressions.length === 0 &&
128
+ Array.isArray(value.quasis) &&
129
+ value.quasis.length === 1) {
130
+ const quasi = value.quasis[0];
131
+ if (quasi?.value && typeof quasi.value.cooked === 'string') {
132
+ return quasi.value.cooked;
133
+ }
134
+ }
135
+ if (node.type === 'UnaryExpression' &&
136
+ value.operator === '-' &&
137
+ value.argument.type === 'Literal' &&
138
+ value.argument.value !== null &&
139
+ typeof value.argument.value === 'number') {
140
+ const argument = value.argument.value;
141
+ return -argument;
142
+ }
143
+ }
133
144
  }