@jk2908/solas 0.4.4 → 0.5.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.
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jk2908/solas",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "description": "A Vite + React meta-framework exploring streaming, Server Components, and partial prerendering. Designed for simplicity and lightness",
5
5
  "keywords": [
6
6
  "framework",
@@ -33,6 +33,10 @@
33
33
  "types": "./dist/index.d.ts",
34
34
  "import": "./dist/index.js"
35
35
  },
36
+ "./$": {
37
+ "types": "./dist/solas.d.ts",
38
+ "import": "./dist/solas.js"
39
+ },
36
40
  "./env/rsc": {
37
41
  "types": "./dist/internal/env/rsc.d.ts",
38
42
  "import": "./dist/internal/env/rsc.js"
@@ -82,11 +86,13 @@
82
86
  },
83
87
  "dependencies": {
84
88
  "@vitejs/plugin-rsc": "^0.5.20",
89
+ "mime-types": "^3.0.2",
90
+ "oxc-parser": "^0.134.0",
85
91
  "path-to-regexp": "^8.3.0"
86
92
  },
87
93
  "devDependencies": {
88
94
  "@prettier/plugin-oxc": "^0.1.3",
89
- "@types/bun": "^1.2.22",
95
+ "@types/mime-types": "^3.0.1",
90
96
  "@typescript/native-preview": "^7.0.0-dev.20260224.1",
91
97
  "network-information-types": "^0.1.1",
92
98
  "oxfmt": "^0.35.0",
@@ -105,7 +111,5 @@
105
111
  "optional": true
106
112
  }
107
113
  },
108
- "engines": {
109
- "bun": ">=1.2.0"
110
- }
114
+ "engines": {}
111
115
  }