@jk2908/solas 0.1.0

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.
Files changed (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +333 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +219 -0
  5. package/dist/error-boundary.d.ts +1 -0
  6. package/dist/error-boundary.js +1 -0
  7. package/dist/index.d.ts +7 -0
  8. package/dist/index.js +235 -0
  9. package/dist/internal/build.d.ts +104 -0
  10. package/dist/internal/build.js +633 -0
  11. package/dist/internal/codegen/config.d.ts +5 -0
  12. package/dist/internal/codegen/config.js +19 -0
  13. package/dist/internal/codegen/environments.d.ts +12 -0
  14. package/dist/internal/codegen/environments.js +42 -0
  15. package/dist/internal/codegen/manifest.d.ts +5 -0
  16. package/dist/internal/codegen/manifest.js +15 -0
  17. package/dist/internal/codegen/maps.d.ts +5 -0
  18. package/dist/internal/codegen/maps.js +75 -0
  19. package/dist/internal/codegen/utils.d.ts +1 -0
  20. package/dist/internal/codegen/utils.js +2 -0
  21. package/dist/internal/env/browser.d.ts +4 -0
  22. package/dist/internal/env/browser.js +58 -0
  23. package/dist/internal/env/request-context.d.ts +19 -0
  24. package/dist/internal/env/request-context.js +2 -0
  25. package/dist/internal/env/rsc.d.ts +39 -0
  26. package/dist/internal/env/rsc.js +368 -0
  27. package/dist/internal/env/ssr.d.ts +42 -0
  28. package/dist/internal/env/ssr.js +149 -0
  29. package/dist/internal/env/utils.d.ts +2 -0
  30. package/dist/internal/env/utils.js +28 -0
  31. package/dist/internal/metadata.d.ts +81 -0
  32. package/dist/internal/metadata.js +185 -0
  33. package/dist/internal/navigation/http-exception-boundary.d.ts +12 -0
  34. package/dist/internal/navigation/http-exception-boundary.js +48 -0
  35. package/dist/internal/navigation/http-exception.d.ts +33 -0
  36. package/dist/internal/navigation/http-exception.js +45 -0
  37. package/dist/internal/navigation/link.d.ts +13 -0
  38. package/dist/internal/navigation/link.js +63 -0
  39. package/dist/internal/navigation/redirect-boundary.d.ts +12 -0
  40. package/dist/internal/navigation/redirect-boundary.js +39 -0
  41. package/dist/internal/navigation/redirect.d.ts +21 -0
  42. package/dist/internal/navigation/redirect.js +63 -0
  43. package/dist/internal/navigation/use-search-params.d.ts +1 -0
  44. package/dist/internal/navigation/use-search-params.js +13 -0
  45. package/dist/internal/prerender.d.ts +151 -0
  46. package/dist/internal/prerender.js +422 -0
  47. package/dist/internal/render/head.d.ts +4 -0
  48. package/dist/internal/render/head.js +38 -0
  49. package/dist/internal/render/tree.d.ts +47 -0
  50. package/dist/internal/render/tree.js +108 -0
  51. package/dist/internal/router/create-router.d.ts +6 -0
  52. package/dist/internal/router/create-router.js +95 -0
  53. package/dist/internal/router/pattern.d.ts +8 -0
  54. package/dist/internal/router/pattern.js +31 -0
  55. package/dist/internal/router/prefetcher.d.ts +47 -0
  56. package/dist/internal/router/prefetcher.js +90 -0
  57. package/dist/internal/router/resolver.d.ts +174 -0
  58. package/dist/internal/router/resolver.js +356 -0
  59. package/dist/internal/router/router-context.d.ts +11 -0
  60. package/dist/internal/router/router-context.js +7 -0
  61. package/dist/internal/router/router-provider.d.ts +6 -0
  62. package/dist/internal/router/router-provider.js +131 -0
  63. package/dist/internal/router/router.d.ts +79 -0
  64. package/dist/internal/router/router.js +417 -0
  65. package/dist/internal/router/use-router.d.ts +5 -0
  66. package/dist/internal/router/use-router.js +5 -0
  67. package/dist/internal/server/cookies.d.ts +6 -0
  68. package/dist/internal/server/cookies.js +17 -0
  69. package/dist/internal/server/dynamic.d.ts +9 -0
  70. package/dist/internal/server/dynamic.js +22 -0
  71. package/dist/internal/server/headers.d.ts +5 -0
  72. package/dist/internal/server/headers.js +19 -0
  73. package/dist/internal/server/url.d.ts +5 -0
  74. package/dist/internal/server/url.js +16 -0
  75. package/dist/internal/ui/defaults/error.d.ts +4 -0
  76. package/dist/internal/ui/defaults/error.js +6 -0
  77. package/dist/internal/ui/error-boundary.d.ts +26 -0
  78. package/dist/internal/ui/error-boundary.js +41 -0
  79. package/dist/navigation.d.ts +6 -0
  80. package/dist/navigation.js +6 -0
  81. package/dist/prerender.d.ts +1 -0
  82. package/dist/prerender.js +1 -0
  83. package/dist/router.d.ts +4 -0
  84. package/dist/router.js +4 -0
  85. package/dist/server.d.ts +4 -0
  86. package/dist/server.js +4 -0
  87. package/dist/solas.d.ts +32 -0
  88. package/dist/solas.js +125 -0
  89. package/dist/types.d.ts +93 -0
  90. package/dist/types.js +1 -0
  91. package/dist/utils/compress.d.ts +11 -0
  92. package/dist/utils/compress.js +76 -0
  93. package/dist/utils/context.d.ts +6 -0
  94. package/dist/utils/context.js +25 -0
  95. package/dist/utils/cookies.d.ts +3 -0
  96. package/dist/utils/cookies.js +35 -0
  97. package/dist/utils/export-reader.d.ts +29 -0
  98. package/dist/utils/export-reader.js +117 -0
  99. package/dist/utils/format.d.ts +6 -0
  100. package/dist/utils/format.js +72 -0
  101. package/dist/utils/logger.d.ts +52 -0
  102. package/dist/utils/logger.js +105 -0
  103. package/dist/utils/time.d.ts +4 -0
  104. package/dist/utils/time.js +29 -0
  105. package/package.json +111 -0
@@ -0,0 +1,32 @@
1
+ import type { PluginConfig } from './types';
2
+ export declare namespace Solas {
3
+ namespace Config {
4
+ const NAME = "Solas";
5
+ const SLUG: string;
6
+ const PKG_NAME: string;
7
+ const OUT_DIR = "dist";
8
+ const APP_DIR = "app";
9
+ const GENERATED_DIR: string;
10
+ const ENTRY_RSC = "entry.rsc.tsx";
11
+ const ENTRY_SSR = "entry.ssr.tsx";
12
+ const ENTRY_BROWSER = "entry.browser.tsx";
13
+ const ASSETS_DIR = "assets";
14
+ const $: unique symbol;
15
+ const REQUEST_META: string;
16
+ const LOG_LEVELS: readonly ["debug", "info", "warn", "error", "fatal"];
17
+ const PRERENDER_MODES: readonly ["full", "ppr", false];
18
+ /**
19
+ * Validate the plugin configuration object, throwing an error if invalid
20
+ * @param input - the unvalidated configuration object
21
+ * @return the typed and validated configuration object
22
+ */
23
+ function validate(input: unknown): PluginConfig;
24
+ }
25
+ function getVersion(): string;
26
+ namespace Events {
27
+ const names: {
28
+ readonly NAVIGATION: `${string}navigation`;
29
+ readonly NAVIGATION_ERROR: `${string}navigationerror`;
30
+ };
31
+ }
32
+ }
package/dist/solas.js ADDED
@@ -0,0 +1,125 @@
1
+ export { Solas };
2
+ var Solas;
3
+ (function (Solas) {
4
+ let Config;
5
+ (function (Config) {
6
+ Config.NAME = 'Solas';
7
+ Config.SLUG = Config.NAME.toLowerCase();
8
+ Config.PKG_NAME = `@jk2908/${Config.SLUG}`;
9
+ Config.OUT_DIR = 'dist';
10
+ Config.APP_DIR = 'app';
11
+ Config.GENERATED_DIR = `.${Config.SLUG}`;
12
+ Config.ENTRY_RSC = 'entry.rsc.tsx';
13
+ Config.ENTRY_SSR = 'entry.ssr.tsx';
14
+ Config.ENTRY_BROWSER = 'entry.browser.tsx';
15
+ Config.ASSETS_DIR = 'assets';
16
+ Config.$ = Symbol(Config.SLUG);
17
+ Config.REQUEST_META = `__${Config.SLUG.toUpperCase()}__`;
18
+ Config.LOG_LEVELS = ['debug', 'info', 'warn', 'error', 'fatal'];
19
+ Config.PRERENDER_MODES = ['full', 'ppr', false];
20
+ const CONFIG_KEYS = new Set([
21
+ 'logger',
22
+ 'metadata',
23
+ 'precompress',
24
+ 'prerender',
25
+ 'trailingSlash',
26
+ 'url',
27
+ ]);
28
+ const LOGGER_KEYS = new Set(['level']);
29
+ /**
30
+ * Validate the plugin configuration object, throwing an error if invalid
31
+ * @param input - the unvalidated configuration object
32
+ * @return the typed and validated configuration object
33
+ */
34
+ function validate(input) {
35
+ if (input === undefined)
36
+ return {};
37
+ const errors = [];
38
+ if (!isRecord(input)) {
39
+ throw new Error(`[${Config.NAME}] Invalid config:\n- Expected plugin config to be an object`);
40
+ }
41
+ for (const key of Object.keys(input)) {
42
+ if (!CONFIG_KEYS.has(key)) {
43
+ errors.push(`Unknown config key: ${key}`);
44
+ }
45
+ }
46
+ if ('url' in input && input.url !== undefined) {
47
+ if (typeof input.url !== 'string') {
48
+ errors.push('config.url must be a string');
49
+ }
50
+ else {
51
+ try {
52
+ const url = new URL(input.url);
53
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
54
+ errors.push('config.url must use http:// or https://');
55
+ }
56
+ }
57
+ catch {
58
+ errors.push('config.url must be a valid URL');
59
+ }
60
+ }
61
+ }
62
+ if ('precompress' in input && input.precompress !== undefined) {
63
+ if (typeof input.precompress !== 'boolean') {
64
+ errors.push('config.precompress must be a boolean');
65
+ }
66
+ }
67
+ if ('prerender' in input && input.prerender !== undefined) {
68
+ if (!new Set(Config.PRERENDER_MODES).has(input.prerender)) {
69
+ errors.push("config.prerender must be 'full', 'ppr', or false");
70
+ }
71
+ }
72
+ if ('trailingSlash' in input && input.trailingSlash !== undefined) {
73
+ if (typeof input.trailingSlash !== 'boolean') {
74
+ errors.push('config.trailingSlash must be a boolean');
75
+ }
76
+ }
77
+ if ('metadata' in input &&
78
+ input.metadata !== undefined &&
79
+ !isRecord(input.metadata)) {
80
+ errors.push('config.metadata must be an object when provided');
81
+ }
82
+ if ('logger' in input && input.logger !== undefined) {
83
+ if (!isRecord(input.logger)) {
84
+ errors.push('config.logger must be an object when provided');
85
+ }
86
+ else {
87
+ for (const key of Object.keys(input.logger)) {
88
+ if (!LOGGER_KEYS.has(key)) {
89
+ errors.push(`Unknown config.logger key: ${key}`);
90
+ }
91
+ }
92
+ if ('level' in input.logger && input.logger.level !== undefined) {
93
+ if (typeof input.logger.level !== 'string' ||
94
+ !new Set(Config.LOG_LEVELS).has(input.logger.level)) {
95
+ errors.push('config.logger.level must be one of: debug, info, warn, error, fatal');
96
+ }
97
+ }
98
+ }
99
+ }
100
+ if (errors.length > 0) {
101
+ throw new Error(`[${Config.NAME}] Invalid config:\n- ${errors.join('\n- ')}`);
102
+ }
103
+ return input;
104
+ }
105
+ Config.validate = validate;
106
+ })(Config = Solas.Config || (Solas.Config = {}));
107
+ function getVersion() {
108
+ const value = import.meta.env.SOLAS_VERSION;
109
+ if (typeof value !== 'string' || value.length === 0) {
110
+ throw new Error(`[${Config.NAME}] Missing ${Config.NAME} package version`);
111
+ }
112
+ return value;
113
+ }
114
+ Solas.getVersion = getVersion;
115
+ let Events;
116
+ (function (Events) {
117
+ Events.names = {
118
+ NAVIGATION: `${Config.SLUG}navigation`,
119
+ NAVIGATION_ERROR: `${Config.SLUG}navigationerror`,
120
+ };
121
+ })(Events = Solas.Events || (Solas.Events = {}));
122
+ })(Solas || (Solas = {}));
123
+ function isRecord(value) {
124
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
125
+ }
@@ -0,0 +1,93 @@
1
+ type BunRequest = Request & {
2
+ params?: Record<string, string | string[]>;
3
+ };
4
+ import { Solas } from './solas';
5
+ import { ExportReader } from './utils/export-reader';
6
+ import type { Build } from './internal/build';
7
+ import type { Metadata } from './internal/metadata';
8
+ import type { HttpException } from './internal/navigation/http-exception';
9
+ import type { Router } from './internal/router/router';
10
+ export type LogLevel = (typeof Solas.Config.LOG_LEVELS)[number];
11
+ export type PluginConfig = {
12
+ url?: `http://${string}` | `https://${string}`;
13
+ port?: number;
14
+ precompress?: boolean;
15
+ prerender?: Route.Prerender;
16
+ metadata?: Metadata.Item;
17
+ trailingSlash?: boolean;
18
+ readonly logger?: {
19
+ level?: LogLevel;
20
+ };
21
+ };
22
+ export type RuntimeConfig = PluginConfig & {
23
+ precompress: NonNullable<PluginConfig['precompress']>;
24
+ trailingSlash: NonNullable<PluginConfig['trailingSlash']>;
25
+ };
26
+ export type BuildContext = {
27
+ prerenderedRoutes: Set<string>;
28
+ exportReader: ExportReader;
29
+ };
30
+ export type SolasRequest = Request & {};
31
+ export type Segment = {
32
+ __id: string;
33
+ __path: string;
34
+ __params: string[];
35
+ __kind: typeof Build.EntryKind.PAGE;
36
+ __depth: number;
37
+ method: 'get';
38
+ paths: {
39
+ layouts: (string | null)[];
40
+ '401s': (string | null)[];
41
+ '403s': (string | null)[];
42
+ '404s': (string | null)[];
43
+ '500s': (string | null)[];
44
+ loaders: (string | null)[];
45
+ middlewares: (string | null)[];
46
+ page?: string | null;
47
+ };
48
+ error?: HttpException | Error;
49
+ prerender: Route.Prerender;
50
+ dynamic: boolean;
51
+ wildcard: boolean;
52
+ };
53
+ export type Endpoint = {
54
+ __id: string;
55
+ __path: string;
56
+ __params: string[];
57
+ __kind: typeof Build.EntryKind.ENDPOINT;
58
+ method: Lowercase<HttpMethod>;
59
+ middlewares: (string | null)[];
60
+ };
61
+ export type ManifestEntry = Segment | Endpoint;
62
+ export type Manifest = Awaited<ReturnType<typeof Build.Finder.prototype.process>>['manifest'];
63
+ export type View<TProps> = React.ComponentType<TProps> | React.LazyExoticComponent<React.ComponentType<TProps>>;
64
+ export type StaticImport = Record<string, unknown>;
65
+ export type DynamicImport<T = Record<string, unknown>> = () => Promise<T>;
66
+ export type MapEntry = {
67
+ shell?: StaticImport;
68
+ page?: DynamicImport;
69
+ layouts?: readonly (DynamicImport | null)[];
70
+ '401s'?: readonly (DynamicImport | null)[];
71
+ '403s'?: readonly (DynamicImport | null)[];
72
+ '404s'?: readonly (DynamicImport | null)[];
73
+ '500s'?: readonly (DynamicImport | null)[];
74
+ loaders?: readonly (DynamicImport | null)[];
75
+ middlewares?: readonly (Router.Middleware | null)[];
76
+ endpoint?: (req?: BunRequest) => unknown;
77
+ };
78
+ export type ImportMap = Record<string, MapEntry>;
79
+ export type HttpMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS';
80
+ export type Primitive = string | number | boolean | bigint | symbol | null | undefined;
81
+ export type LooseNumber<T extends number> = T | (number & {});
82
+ export type BuildManifest = {
83
+ prerenderedRoutes: string[];
84
+ precompress: boolean;
85
+ };
86
+ export declare namespace Route {
87
+ type Metadata = Metadata.Item | ((input: Metadata.Input<Router.Params>) => Promise<Metadata.Item> | Metadata.Item);
88
+ type Prerender = (typeof Solas.Config.PRERENDER_MODES)[number];
89
+ }
90
+ export type BoundaryError = Error & {
91
+ digest?: string;
92
+ };
93
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ import { Solas } from './solas';
@@ -0,0 +1,11 @@
1
+ export declare namespace Compress {
2
+ /**
3
+ * Compress a file or directory
4
+ */
5
+ function run(input: string, config?: {
6
+ filter?: (f: string) => boolean;
7
+ }): AsyncGenerator<{
8
+ input: string;
9
+ compressed: Uint8Array;
10
+ }>;
11
+ }
@@ -0,0 +1,76 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { brotliCompress } from 'node:zlib';
5
+ export { Compress };
6
+ var Compress;
7
+ (function (Compress) {
8
+ const DEFAULT_CONCURRENCY = Math.max(1, Math.min(os.cpus().length, 8));
9
+ async function collect(input, filter, output = []) {
10
+ const stat = await fs.stat(input);
11
+ if (!stat.isDirectory()) {
12
+ if (filter(input))
13
+ output.push(input);
14
+ return output;
15
+ }
16
+ for (const entry of await fs.readdir(input, { withFileTypes: true })) {
17
+ const next = path.join(input, entry.name);
18
+ if (entry.isDirectory()) {
19
+ await collect(next, filter, output);
20
+ continue;
21
+ }
22
+ if (entry.isFile() && filter(next))
23
+ output.push(next);
24
+ }
25
+ return output;
26
+ }
27
+ async function compress(input) {
28
+ const file = Bun.file(input);
29
+ const buffer = Buffer.from(await file.arrayBuffer());
30
+ const compressed = await new Promise((fulfill, reject) => {
31
+ brotliCompress(buffer, (err, res) => {
32
+ if (err) {
33
+ reject(err);
34
+ }
35
+ else {
36
+ fulfill(res);
37
+ }
38
+ });
39
+ });
40
+ // return the input path plus a zero-copy Uint8Array view over the
41
+ // compressed Buffer using its exact offset and length
42
+ return {
43
+ input,
44
+ compressed: new Uint8Array(compressed.buffer, compressed.byteOffset, compressed.byteLength),
45
+ };
46
+ }
47
+ /**
48
+ * Compress a file or directory
49
+ */
50
+ async function* run(input, config = {}) {
51
+ const { filter = f => /\.(js|css|html|svg|json|txt)$/.test(f) } = config;
52
+ const targets = await collect(input, filter);
53
+ if (!targets.length)
54
+ return;
55
+ let index = 0;
56
+ const pending = new Map();
57
+ function enqueue() {
58
+ while (index < targets.length && pending.size < DEFAULT_CONCURRENCY) {
59
+ const i = index++;
60
+ const value = targets[i];
61
+ pending.set(i, compress(value).then(compressed => ({
62
+ index: i,
63
+ value: compressed,
64
+ })));
65
+ }
66
+ }
67
+ enqueue();
68
+ while (pending.size > 0) {
69
+ const settled = await Promise.race(pending.values());
70
+ pending.delete(settled.index);
71
+ yield settled.value;
72
+ enqueue();
73
+ }
74
+ }
75
+ Compress.run = run;
76
+ })(Compress || (Compress = {}));
@@ -0,0 +1,6 @@
1
+ export declare namespace Context {
2
+ function create<T>(name: string): {
3
+ use(): NonNullable<T>;
4
+ write<R>(value: T, fn: () => R | Promise<R>): R | Promise<R>;
5
+ };
6
+ }
@@ -0,0 +1,25 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { Logger } from './logger';
3
+ const logger = new Logger();
4
+ export { Context };
5
+ var Context;
6
+ (function (Context) {
7
+ function create(name) {
8
+ const storage = new AsyncLocalStorage();
9
+ return {
10
+ use() {
11
+ const r = storage.getStore();
12
+ if (!r) {
13
+ const error = new Error(`No ${name} context available`);
14
+ logger.error(`[Context:create] ${error.message}`, error);
15
+ throw error;
16
+ }
17
+ return r;
18
+ },
19
+ write(value, fn) {
20
+ return storage.run(value, fn);
21
+ },
22
+ };
23
+ }
24
+ Context.create = create;
25
+ })(Context || (Context = {}));
@@ -0,0 +1,3 @@
1
+ export declare namespace Cookies {
2
+ function parse(header: string | null | undefined): Map<string, string>;
3
+ }
@@ -0,0 +1,35 @@
1
+ export { Cookies };
2
+ var Cookies;
3
+ (function (Cookies) {
4
+ function decode(value) {
5
+ try {
6
+ // some clients encode spaces as '+'
7
+ return decodeURIComponent(value.replace(/\+/g, ' '));
8
+ }
9
+ catch {
10
+ // keep raw value if decoding fails
11
+ return value;
12
+ }
13
+ }
14
+ function parse(header) {
15
+ const out = new Map();
16
+ if (!header)
17
+ return out;
18
+ for (const part of header.split(';')) {
19
+ // cookie values may contain =, so only split on the
20
+ // first separator
21
+ const separator = part.indexOf('=');
22
+ const raw = separator === -1 ? part : part.slice(0, separator);
23
+ const key = raw.trim();
24
+ if (!key)
25
+ continue;
26
+ // later duplicates are ignored so the first cookie value wins
27
+ if (out.has(key))
28
+ continue;
29
+ const value = separator === -1 ? '' : part.slice(separator + 1).trim();
30
+ out.set(key, decode(value));
31
+ }
32
+ return out;
33
+ }
34
+ Cookies.parse = parse;
35
+ })(Cookies || (Cookies = {}));
@@ -0,0 +1,29 @@
1
+ export declare class ExportReader {
2
+ #private;
3
+ /**
4
+ * Read the raw text content of a file
5
+ */
6
+ raw(filePath: string): Promise<string>;
7
+ /**
8
+ * Get the names of all exports from a file
9
+ */
10
+ exports(filePath: string): Promise<string[]>;
11
+ /**
12
+ * Check if a file exports a specific name
13
+ */
14
+ has(filePath: string, name: string): Promise<boolean>;
15
+ /**
16
+ * Read a simple literal export from a file without executing it
17
+ * @description supports string, number, boolean, and null literals.
18
+ * The export must be in the form of `export const|let|var name = <literal>`
19
+ */
20
+ literal<T>(filePath: string, name: string, validate?: ExportReader.Validator<T>): Promise<T | undefined>;
21
+ /**
22
+ * Read an export from a file by executing the module
23
+ */
24
+ value<T>(filePath: string, name: string, validate?: ExportReader.Validator<T>): Promise<T | undefined>;
25
+ }
26
+ export declare namespace ExportReader {
27
+ type Loader = 'js' | 'jsx' | 'ts' | 'tsx';
28
+ type Validator<T> = (value: unknown) => value is T;
29
+ }
@@ -0,0 +1,117 @@
1
+ import path from 'node:path';
2
+ export class ExportReader {
3
+ #transpilers = new Map();
4
+ /**
5
+ * Pick the Bun loader that matches the source file extension
6
+ */
7
+ static #getLoader(filePath) {
8
+ const ext = path.extname(filePath).toLowerCase();
9
+ if (ext === '.js' || ext === '.mjs' || ext === '.cjs')
10
+ return 'js';
11
+ if (ext === '.jsx')
12
+ return 'jsx';
13
+ if (ext === '.ts' || ext === '.mts' || ext === '.cts')
14
+ return 'ts';
15
+ if (ext === '.tsx')
16
+ return 'tsx';
17
+ throw new Error(`Unsupported module extension: ${ext || '(none)'} in ${filePath}`);
18
+ }
19
+ /**
20
+ * Reuse one transpiler per supported loader so scans match the module syntax
21
+ */
22
+ #getTranspiler(filePath) {
23
+ const loader = ExportReader.#getLoader(filePath);
24
+ const cached = this.#transpilers.get(loader);
25
+ if (cached)
26
+ return cached;
27
+ const transpiler = new Bun.Transpiler({ loader });
28
+ this.#transpilers.set(loader, transpiler);
29
+ return transpiler;
30
+ }
31
+ /**
32
+ * Parse a literal value from a string
33
+ */
34
+ static #parse(value) {
35
+ const trimmed = value.trim();
36
+ // keep quoted literals as strings without evaluating the
37
+ // source text
38
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
39
+ (trimmed.startsWith("'") && trimmed.endsWith("'")) ||
40
+ (trimmed.startsWith('`') && trimmed.endsWith('`'))) {
41
+ return trimmed.slice(1, -1);
42
+ }
43
+ if (trimmed === 'true')
44
+ return true;
45
+ if (trimmed === 'false')
46
+ return false;
47
+ if (trimmed === 'null')
48
+ return null;
49
+ const n = Number(trimmed);
50
+ if (Number.isFinite(n))
51
+ return n;
52
+ }
53
+ /**
54
+ * Read the raw text content of a file
55
+ */
56
+ async raw(filePath) {
57
+ return Bun.file(filePath).text();
58
+ }
59
+ /**
60
+ * Get the names of all exports from a file
61
+ */
62
+ async exports(filePath) {
63
+ // use Bun's transpiler scan so we can inspect export names
64
+ // without loading the module
65
+ return this.#getTranspiler(filePath).scan(await this.raw(filePath)).exports;
66
+ }
67
+ /**
68
+ * Check if a file exports a specific name
69
+ */
70
+ async has(filePath, name) {
71
+ const names = await this.exports(filePath);
72
+ return names.includes(name);
73
+ }
74
+ /**
75
+ * Read a simple literal export from a file without executing it
76
+ * @description supports string, number, boolean, and null literals.
77
+ * The export must be in the form of `export const|let|var name = <literal>`
78
+ */
79
+ async literal(filePath, name, validate) {
80
+ const code = await this.raw(filePath);
81
+ // build the matcher from escaped plain-text pieces so arbitrary export names
82
+ // cannot change the regex shape
83
+ const source =
84
+ // match: `export const|let|var `
85
+ '\\bexport\\s+(?:const|let|var)\\s+' +
86
+ // treat export name as plain text in regex
87
+ name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
88
+ // capture one supported literal value (string, number, boolean, null)
89
+ '\\s*=\\s*(?<value>(?:"(?:[^"\\\\]|\\\\.)*"|\'(?:[^\'\\\\]|\\\\.)*\'|\\x60(?:[^\\x60\\\\]|\\\\.)*\\x60|true|false|null|-?\\d+(?:\\.\\d+)?))(?=\\s|;|$)';
90
+ const text = code.match(new RegExp(source))?.groups?.value;
91
+ if (!text)
92
+ return;
93
+ // only support cheap literal parsing here. Anything richer should go through
94
+ // value() so module semantics stay correct
95
+ const value = ExportReader.#parse(text);
96
+ if (value === undefined)
97
+ return;
98
+ if (!validate || validate(value))
99
+ return value;
100
+ }
101
+ /**
102
+ * Read an export from a file by executing the module
103
+ */
104
+ async value(filePath, name, validate) {
105
+ if (!(await this.has(filePath, name)))
106
+ return;
107
+ // resolve from the project root so generated/build-time callers can pass the
108
+ // same workspace-relative paths used elsewhere in the route graph
109
+ const abs = path.resolve(process.cwd(), filePath);
110
+ const mod = (await import(/* @vite-ignore */ abs));
111
+ const value = mod[name];
112
+ if (value === undefined)
113
+ return;
114
+ if (!validate || validate(value))
115
+ return value;
116
+ }
117
+ }
@@ -0,0 +1,6 @@
1
+ export declare namespace Format {
2
+ /**
3
+ * Format a file in-place using oxfmt with our preferred code style
4
+ */
5
+ function run(filePath: string): Promise<void>;
6
+ }
@@ -0,0 +1,72 @@
1
+ import path from 'node:path';
2
+ import { format } from 'oxfmt';
3
+ import { Logger } from './logger';
4
+ const logger = new Logger();
5
+ const BASE_OPTIONS = {
6
+ useTabs: true,
7
+ tabWidth: 2,
8
+ printWidth: 90,
9
+ singleQuote: true,
10
+ jsxSingleQuote: false,
11
+ quoteProps: 'as-needed',
12
+ trailingComma: 'all',
13
+ semi: false,
14
+ arrowParens: 'avoid',
15
+ bracketSameLine: true,
16
+ bracketSpacing: true,
17
+ endOfLine: 'lf',
18
+ };
19
+ const SUPPORTED_EXTENSIONS = new Set([
20
+ '.js',
21
+ '.jsx',
22
+ '.ts',
23
+ '.tsx',
24
+ '.mjs',
25
+ '.cjs',
26
+ '.mts',
27
+ '.cts',
28
+ '.json',
29
+ '.jsonc',
30
+ '.json5',
31
+ '.css',
32
+ '.scss',
33
+ '.less',
34
+ '.md',
35
+ '.mdx',
36
+ '.html',
37
+ '.yml',
38
+ '.yaml',
39
+ '.toml',
40
+ ]);
41
+ export { Format };
42
+ var Format;
43
+ (function (Format) {
44
+ /**
45
+ * Format a file in-place using oxfmt with our preferred code style
46
+ */
47
+ async function run(filePath) {
48
+ try {
49
+ const ext = path.extname(filePath).toLowerCase();
50
+ if (!SUPPORTED_EXTENSIONS.has(ext)) {
51
+ logger.warn(`[format] Skipping unsupported file type: ${filePath}`);
52
+ return;
53
+ }
54
+ const file = Bun.file(filePath);
55
+ const source = await file.text();
56
+ const options = ext === '.json' ? { ...BASE_OPTIONS, trailingComma: 'none' } : BASE_OPTIONS;
57
+ const result = await format(filePath, source, options);
58
+ if (result.errors.length > 0) {
59
+ logger.warn(`[format] oxfmt failed for ${filePath}: ${result.errors[0]?.message}`);
60
+ return;
61
+ }
62
+ if (result.code === source)
63
+ return;
64
+ await Bun.write(filePath, result.code);
65
+ logger.info(`[format] Formatted file: ${filePath}`);
66
+ }
67
+ catch (err) {
68
+ logger.error(`[format] Failed to format file: ${filePath}`, err);
69
+ }
70
+ }
71
+ Format.run = run;
72
+ })(Format || (Format = {}));