@hashrock/ono 0.1.2 → 0.2.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,188 +1,83 @@
1
1
  /**
2
- * Browser compiler utilities shared with the REPL worker.
2
+ * Browser compiler - shared with the REPL worker.
3
+ *
4
+ * Uses the same mini bundler as the Node build (bundler.js/parser.js).
5
+ * Package imports are rejected — the browser has no module resolution —
6
+ * and the bundle is evaluated with new Function, with the JSX runtime
7
+ * passed in as parameters.
3
8
  */
4
9
 
5
10
  import { transformJSX } from '../transformer.js';
6
11
  import { renderToString } from '../renderer.js';
7
- import { h } from '../jsx-runtime.js';
12
+ import { h, Fragment } from '../jsx-runtime.js';
13
+ import { bundle } from '../bundler.js';
8
14
  import { getUnoGenerator } from './unocss.js';
9
15
 
10
- function resolveImport(from, to) {
11
- if (to.startsWith('./') || to.startsWith('../')) {
12
- const fromParts = from.split('/').slice(0, -1);
13
- const targetParts = to.split('/');
14
- const resolved = [...fromParts];
15
-
16
- for (const part of targetParts) {
17
- if (!part || part === '.') continue;
18
- if (part === '..') {
19
- resolved.pop();
20
- } else {
21
- resolved.push(part);
22
- }
16
+ /**
17
+ * Join a relative specifier against the importing file's virtual path
18
+ * @param {string} fromId
19
+ * @param {string} specifier
20
+ */
21
+ function resolveImport(fromId, specifier) {
22
+ const fromParts = fromId.split('/').slice(0, -1);
23
+ const targetParts = specifier.split('/');
24
+ const resolved = [...fromParts];
25
+
26
+ for (const part of targetParts) {
27
+ if (!part || part === '.') continue;
28
+ if (part === '..') {
29
+ resolved.pop();
30
+ } else {
31
+ resolved.push(part);
23
32
  }
24
-
25
- return resolved.join('/');
26
33
  }
27
34
 
28
- return to;
35
+ return resolved.join('/');
29
36
  }
30
37
 
31
- function topoSortModules(files, entryPoint) {
32
- const filenames = Object.keys(files);
33
- const dependencyMap = new Map();
34
- const importPattern = /import\s+(?:[\s\S]+?)?from\s+['"]([^'"]+)['"]/g;
35
-
36
- for (const filename of filenames) {
37
- const source = files[filename] || '';
38
- const deps = [];
39
- source.replace(importPattern, (match, specifier) => {
40
- const resolved = resolveImport(filename, specifier);
41
- if (files[resolved]) {
42
- deps.push(resolved);
43
- }
44
- return match;
45
- });
46
- dependencyMap.set(filename, deps);
47
- }
48
-
49
- const visited = new Set();
50
- const order = [];
51
-
52
- const visit = (file) => {
53
- if (!files[file] || visited.has(file)) return;
54
- visited.add(file);
55
- const deps = dependencyMap.get(file) || [];
56
- for (const dep of deps) {
57
- visit(dep);
58
- }
59
- order.push(file);
60
- };
61
-
62
- if (entryPoint) {
63
- visit(entryPoint);
64
- }
65
-
66
- for (const filename of filenames) {
67
- if (!visited.has(filename)) {
68
- visit(filename);
69
- }
38
+ /**
39
+ * @param {Record<string, string>} files
40
+ * @param {string} entryPoint
41
+ */
42
+ async function bundleProject(files, entryPoint) {
43
+ if (!(entryPoint in files)) {
44
+ throw new Error(`Entry point '${entryPoint}' not found`);
70
45
  }
71
46
 
72
- return order;
73
- }
74
-
75
- function bundleModules(files, entryPoint) {
76
- const order = topoSortModules(files, entryPoint);
77
- let bundledCode = 'const __modules = {};\n';
78
-
79
- for (const filename of order) {
80
- const transformedCode = transformJSX(files[filename], filename);
81
- let code = transformedCode;
82
- const exportMappings = new Map();
83
-
84
- const addExport = (exportName, localName = exportName) => {
85
- if (!exportName || exportMappings.has(exportName)) return;
86
- exportMappings.set(exportName, localName);
87
- };
88
-
89
- code = code.replace(/import\s+{([^}]+)}\s+from\s+['"]([^'"]+)['"]/g, (match, imports, path) => {
90
- const resolvedPath = resolveImport(filename, path);
91
- const importNames = imports.split(',').map(part => part.trim()).filter(Boolean);
92
-
93
- return importNames
94
- .map(name => {
95
- const [local, alias] = name.split(/\s+as\s+/).map(token => token && token.trim());
96
- const localName = alias || local;
97
- const exportName = local;
98
- return `const ${localName} = __modules['${resolvedPath}']['${exportName}'];`;
99
- })
100
- .join('\n');
101
- });
102
-
103
- code = code.replace(/import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g, (match, localName, path) => {
104
- const resolvedPath = resolveImport(filename, path);
105
- return `const ${localName} = __modules['${resolvedPath}']['default'];`;
106
- });
107
-
108
- code = code.replace(/import\s+\*\s+as\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g, (match, localName, path) => {
109
- const resolvedPath = resolveImport(filename, path);
110
- return `const ${localName} = __modules['${resolvedPath}'];`;
111
- });
112
-
113
- code = code.replace(/export\s+default\s+function\s+([A-Za-z_$][\w$]*)/g, (match, name) => {
114
- addExport('default', name);
115
- return `function ${name}`;
116
- });
117
-
118
- code = code.replace(/export\s+default\s+([A-Za-z_$][\w$]*)/g, (match, name) => {
119
- addExport('default', name);
120
- return `${name}`;
121
- });
122
-
123
- code = code.replace(/export\s+(const|let|var)\s+([A-Za-z_$][\w$]*)/g, (match, kind, name) => {
124
- addExport(name);
125
- return `${kind} ${name}`;
126
- });
127
-
128
- code = code.replace(/export\s+function\s+([A-Za-z_$][\w$]*)/g, (match, name) => {
129
- addExport(name);
130
- return `function ${name}`;
131
- });
132
-
133
- code = code.replace(/export\s+class\s+([A-Za-z_$][\w$]*)/g, (match, name) => {
134
- addExport(name);
135
- return `class ${name}`;
136
- });
137
-
138
- code = code.replace(/export\s*{\s*([^}]+)\s*};?/g, (match, names) => {
139
- names
140
- .split(',')
141
- .map(part => part.trim())
142
- .filter(Boolean)
143
- .forEach(part => {
144
- const [local, alias] = part.split(/\s+as\s+/).map(token => token && token.trim());
145
- addExport(alias || local, local);
146
- });
147
- return '';
148
- });
149
-
150
- code = code.replace(/export\s*{\s*};?/g, '');
151
-
152
- if (filename === entryPoint) {
153
- const functionMatches = code.matchAll(/function\s+([A-Za-z_$][\w$]*)/g);
154
- for (const match of functionMatches) {
155
- const name = match[1];
156
- addExport(name);
157
- }
158
- }
159
-
160
- const exportEntries = Array.from(exportMappings.entries()).map(([exportName, localName]) => {
161
- if (exportName === 'default') {
162
- return `'default': ${localName}`;
163
- }
164
- if (exportName === localName) {
165
- return exportName;
47
+ const { code } = await bundle({
48
+ entry: entryPoint,
49
+ resolve: (specifier, fromId) => {
50
+ const resolved = resolveImport(fromId, specifier);
51
+ if (!(resolved in files)) {
52
+ throw new Error(`Cannot find module '${specifier}' imported from '${fromId}'`);
166
53
  }
167
- return `'${exportName}': ${localName}`;
168
- });
169
-
170
- const exportsObject = exportEntries.length > 0 ? `{ ${exportEntries.join(', ')} }` : '{}';
171
-
172
- bundledCode += `
173
- __modules['${filename}'] = (() => {
174
- ${code}
175
- return ${exportsObject};
176
- })();
177
- `;
178
- }
54
+ return resolved;
55
+ },
56
+ load: (id) => transformJSX(files[id], id),
57
+ onExternal: 'error',
58
+ exposeEntryFunctions: true,
59
+ });
60
+
61
+ return code;
62
+ }
179
63
 
180
- bundledCode += `const __entry = __modules['${entryPoint}'];\n`;
64
+ /** @param {string} bundledCode */
65
+ function evaluateEntryModule(bundledCode) {
66
+ const getEntryModule = new Function('h', 'Fragment', `
67
+ ${bundledCode}
68
+ return __ono_entry;
69
+ `);
181
70
 
182
- return bundledCode;
71
+ return getEntryModule(h, Fragment);
183
72
  }
184
73
 
185
- function extractRenderCandidate(entryModule, entryCode) {
74
+ /**
75
+ * Pick what to render from the entry module: the default export if there
76
+ * is one, otherwise the last exported function (REPL snippets usually
77
+ * define components and finish with an App function), otherwise any value.
78
+ * @param {any} entryModule
79
+ */
80
+ function extractRenderCandidate(entryModule) {
186
81
  if (!entryModule || typeof entryModule !== 'object') {
187
82
  return null;
188
83
  }
@@ -195,21 +90,9 @@ function extractRenderCandidate(entryModule, entryCode) {
195
90
  return () => defaultExport;
196
91
  }
197
92
 
198
- const lastCallMatch = entryCode.match(/([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*$/m);
199
- if (lastCallMatch) {
200
- const lastCallName = lastCallMatch[1];
201
- const candidate = entryModule[lastCallName];
202
- if (typeof candidate === 'function') {
203
- return candidate;
204
- }
205
- if (candidate !== undefined) {
206
- return () => candidate;
207
- }
208
- }
209
-
210
- const fallbackFunction = Object.values(entryModule).find(value => typeof value === 'function');
211
- if (fallbackFunction) {
212
- return fallbackFunction;
93
+ const functions = Object.values(entryModule).filter((value) => typeof value === 'function');
94
+ if (functions.length > 0) {
95
+ return functions[functions.length - 1];
213
96
  }
214
97
 
215
98
  const firstValue = Object.values(entryModule)[0];
@@ -220,20 +103,15 @@ function extractRenderCandidate(entryModule, entryCode) {
220
103
  return null;
221
104
  }
222
105
 
223
- function evaluateEntryModule(bundledCode) {
224
- const getEntryModule = new Function('h', `
225
- ${bundledCode}
226
- return __entry;
227
- `);
228
-
229
- return getEntryModule(h);
230
- }
231
-
106
+ /**
107
+ * @param {Record<string, string>} files
108
+ * @param {string} entryPoint
109
+ * @param {{ enableUno?: boolean, unoConfig?: any }} [options]
110
+ */
232
111
  export async function compileProject(files, entryPoint = 'index.jsx', options = {}) {
233
- const bundled = bundleModules(files, entryPoint);
112
+ const bundled = await bundleProject(files, entryPoint);
234
113
  const entryModule = evaluateEntryModule(bundled);
235
- const entryCode = files[entryPoint] || '';
236
- const renderCandidate = extractRenderCandidate(entryModule, entryCode);
114
+ const renderCandidate = extractRenderCandidate(entryModule);
237
115
 
238
116
  if (!renderCandidate) {
239
117
  throw new Error('Unable to determine a render function in the entry module.');
@@ -5,9 +5,12 @@
5
5
  import { createGenerator } from '@unocss/core';
6
6
  import { presetUno } from '@unocss/preset-uno';
7
7
 
8
+ /** @type {string | null} */
8
9
  let cachedGeneratorKey = null;
10
+ /** @type {Promise<any> | null} */
9
11
  let cachedGeneratorPromise = null;
10
12
 
13
+ /** @param {object} [config] */
11
14
  function serializeConfig(config = {}) {
12
15
  try {
13
16
  return JSON.stringify(config);
@@ -16,12 +19,13 @@ function serializeConfig(config = {}) {
16
19
  }
17
20
  }
18
21
 
22
+ /** @param {object} [config] */
19
23
  export async function getUnoGenerator(config = {}) {
20
24
  const key = serializeConfig(config);
21
25
  if (!cachedGeneratorPromise || cachedGeneratorKey !== key) {
22
26
  cachedGeneratorKey = key;
23
27
  cachedGeneratorPromise = createGenerator({
24
- presets: [presetUno()],
28
+ presets: [/** @type {any} */ (presetUno())],
25
29
  ...config,
26
30
  });
27
31
  }