almostnode 0.2.7 → 0.2.9
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.
- package/README.md +4 -2
- package/dist/CNAME +1 -0
- package/dist/__sw__.js +80 -84
- package/dist/assets/{runtime-worker-B8_LZkBX.js → runtime-worker-ujGAG2t7.js} +1278 -828
- package/dist/assets/runtime-worker-ujGAG2t7.js.map +1 -0
- package/dist/frameworks/code-transforms.d.ts.map +1 -1
- package/dist/frameworks/next-config-parser.d.ts +16 -0
- package/dist/frameworks/next-config-parser.d.ts.map +1 -0
- package/dist/frameworks/next-dev-server.d.ts +6 -6
- package/dist/frameworks/next-dev-server.d.ts.map +1 -1
- package/dist/frameworks/next-html-generator.d.ts +35 -0
- package/dist/frameworks/next-html-generator.d.ts.map +1 -0
- package/dist/frameworks/next-shims.d.ts +79 -0
- package/dist/frameworks/next-shims.d.ts.map +1 -0
- package/dist/index.cjs +3024 -2465
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +3336 -2787
- package/dist/index.mjs.map +1 -1
- package/dist/og-image.png +0 -0
- package/dist/runtime.d.ts +26 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/server-bridge.d.ts +2 -0
- package/dist/server-bridge.d.ts.map +1 -1
- package/dist/shims/crypto.d.ts +2 -0
- package/dist/shims/crypto.d.ts.map +1 -1
- package/dist/shims/esbuild.d.ts.map +1 -1
- package/dist/shims/fs.d.ts.map +1 -1
- package/dist/shims/http.d.ts +29 -0
- package/dist/shims/http.d.ts.map +1 -1
- package/dist/shims/path.d.ts.map +1 -1
- package/dist/shims/stream.d.ts.map +1 -1
- package/dist/shims/vfs-adapter.d.ts.map +1 -1
- package/dist/shims/ws.d.ts +2 -0
- package/dist/shims/ws.d.ts.map +1 -1
- package/dist/types/package-json.d.ts +1 -0
- package/dist/types/package-json.d.ts.map +1 -1
- package/dist/utils/binary-encoding.d.ts +13 -0
- package/dist/utils/binary-encoding.d.ts.map +1 -0
- package/dist/virtual-fs.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/convex-app-demo-entry.ts +229 -35
- package/src/frameworks/code-transforms.ts +5 -1
- package/src/frameworks/next-config-parser.ts +140 -0
- package/src/frameworks/next-dev-server.ts +76 -1675
- package/src/frameworks/next-html-generator.ts +597 -0
- package/src/frameworks/next-shims.ts +1050 -0
- package/src/frameworks/tailwind-config-loader.ts +1 -1
- package/src/index.ts +2 -0
- package/src/runtime.ts +271 -25
- package/src/server-bridge.ts +61 -28
- package/src/shims/crypto.ts +13 -0
- package/src/shims/esbuild.ts +4 -1
- package/src/shims/fs.ts +9 -11
- package/src/shims/http.ts +312 -3
- package/src/shims/path.ts +6 -13
- package/src/shims/stream.ts +12 -26
- package/src/shims/vfs-adapter.ts +5 -2
- package/src/shims/ws.ts +95 -2
- package/src/types/package-json.ts +1 -0
- package/src/utils/binary-encoding.ts +43 -0
- package/src/virtual-fs.ts +7 -15
- package/dist/assets/runtime-worker-B8_LZkBX.js.map +0 -1
|
@@ -0,0 +1,1050 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js shim constants
|
|
3
|
+
* Static HTML/JS strings used by NextDevServer for browser-side Next.js emulation.
|
|
4
|
+
* These are injected into generated HTML pages as inline scripts or served as virtual modules.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tailwind CSS CDN script for runtime JIT compilation
|
|
9
|
+
*/
|
|
10
|
+
export const TAILWIND_CDN_SCRIPT = `<script src="https://cdn.tailwindcss.com"></script>`;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* CORS Proxy script - provides proxyFetch function in the iframe
|
|
14
|
+
* Reads proxy URL from localStorage (set by parent window)
|
|
15
|
+
*/
|
|
16
|
+
export const CORS_PROXY_SCRIPT = `
|
|
17
|
+
<script>
|
|
18
|
+
// CORS Proxy support for external API calls
|
|
19
|
+
window.__getCorsProxy = function() {
|
|
20
|
+
return localStorage.getItem('__corsProxyUrl') || null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
window.__setCorsProxy = function(url) {
|
|
24
|
+
if (url) {
|
|
25
|
+
localStorage.setItem('__corsProxyUrl', url);
|
|
26
|
+
} else {
|
|
27
|
+
localStorage.removeItem('__corsProxyUrl');
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
window.__proxyFetch = async function(url, options) {
|
|
32
|
+
const proxyUrl = window.__getCorsProxy();
|
|
33
|
+
if (proxyUrl) {
|
|
34
|
+
const proxiedUrl = proxyUrl + encodeURIComponent(url);
|
|
35
|
+
return fetch(proxiedUrl, options);
|
|
36
|
+
}
|
|
37
|
+
return fetch(url, options);
|
|
38
|
+
};
|
|
39
|
+
</script>
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* React Refresh preamble - MUST run before React is loaded
|
|
44
|
+
*/
|
|
45
|
+
export const REACT_REFRESH_PREAMBLE = `
|
|
46
|
+
<script type="module">
|
|
47
|
+
// Block until React Refresh is loaded and initialized
|
|
48
|
+
const RefreshRuntime = await import('https://esm.sh/react-refresh@0.14.0/runtime').then(m => m.default || m);
|
|
49
|
+
|
|
50
|
+
RefreshRuntime.injectIntoGlobalHook(window);
|
|
51
|
+
window.$RefreshRuntime$ = RefreshRuntime;
|
|
52
|
+
window.$RefreshRegCount$ = 0;
|
|
53
|
+
|
|
54
|
+
window.$RefreshReg$ = (type, id) => {
|
|
55
|
+
window.$RefreshRegCount$++;
|
|
56
|
+
RefreshRuntime.register(type, id);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
window.$RefreshSig$ = () => (type) => type;
|
|
60
|
+
|
|
61
|
+
console.log('[HMR] React Refresh initialized');
|
|
62
|
+
</script>
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* HMR client script for Next.js
|
|
67
|
+
*/
|
|
68
|
+
export const HMR_CLIENT_SCRIPT = `
|
|
69
|
+
<script type="module">
|
|
70
|
+
(function() {
|
|
71
|
+
const hotModules = new Map();
|
|
72
|
+
const pendingUpdates = new Map();
|
|
73
|
+
|
|
74
|
+
window.__vite_hot_context__ = function createHotContext(ownerPath) {
|
|
75
|
+
if (hotModules.has(ownerPath)) {
|
|
76
|
+
return hotModules.get(ownerPath);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const hot = {
|
|
80
|
+
data: {},
|
|
81
|
+
accept(callback) {
|
|
82
|
+
hot._acceptCallback = callback;
|
|
83
|
+
},
|
|
84
|
+
dispose(callback) {
|
|
85
|
+
hot._disposeCallback = callback;
|
|
86
|
+
},
|
|
87
|
+
invalidate() {
|
|
88
|
+
location.reload();
|
|
89
|
+
},
|
|
90
|
+
prune(callback) {
|
|
91
|
+
hot._pruneCallback = callback;
|
|
92
|
+
},
|
|
93
|
+
on(event, cb) {},
|
|
94
|
+
off(event, cb) {},
|
|
95
|
+
send(event, data) {},
|
|
96
|
+
_acceptCallback: null,
|
|
97
|
+
_disposeCallback: null,
|
|
98
|
+
_pruneCallback: null,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
hotModules.set(ownerPath, hot);
|
|
102
|
+
return hot;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Listen for HMR updates via postMessage (works with sandboxed iframes)
|
|
106
|
+
window.addEventListener('message', async (event) => {
|
|
107
|
+
// Filter for HMR messages only
|
|
108
|
+
if (!event.data || event.data.channel !== 'next-hmr') return;
|
|
109
|
+
const { type, path, timestamp } = event.data;
|
|
110
|
+
|
|
111
|
+
if (type === 'update') {
|
|
112
|
+
console.log('[HMR] Update:', path);
|
|
113
|
+
|
|
114
|
+
if (path.endsWith('.css')) {
|
|
115
|
+
const links = document.querySelectorAll('link[rel="stylesheet"]');
|
|
116
|
+
links.forEach(link => {
|
|
117
|
+
const href = link.getAttribute('href');
|
|
118
|
+
if (href && href.includes(path.replace(/^\\//, ''))) {
|
|
119
|
+
link.href = href.split('?')[0] + '?t=' + timestamp;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const styles = document.querySelectorAll('style[data-next-dev-id]');
|
|
124
|
+
styles.forEach(style => {
|
|
125
|
+
const id = style.getAttribute('data-next-dev-id');
|
|
126
|
+
if (id && id.includes(path.replace(/^\\//, ''))) {
|
|
127
|
+
import(path + '?t=' + timestamp).catch(() => {});
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
} else if (path.match(/\\.(jsx?|tsx?)$/)) {
|
|
131
|
+
await handleJSUpdate(path, timestamp);
|
|
132
|
+
}
|
|
133
|
+
} else if (type === 'full-reload') {
|
|
134
|
+
console.log('[HMR] Full reload');
|
|
135
|
+
location.reload();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
async function handleJSUpdate(path, timestamp) {
|
|
140
|
+
const normalizedPath = path.startsWith('/') ? path : '/' + path;
|
|
141
|
+
const hot = hotModules.get(normalizedPath);
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
if (hot && hot._disposeCallback) {
|
|
145
|
+
hot._disposeCallback(hot.data);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (window.$RefreshRuntime$) {
|
|
149
|
+
pendingUpdates.set(normalizedPath, timestamp);
|
|
150
|
+
|
|
151
|
+
if (pendingUpdates.size === 1) {
|
|
152
|
+
setTimeout(async () => {
|
|
153
|
+
try {
|
|
154
|
+
for (const [modulePath, ts] of pendingUpdates) {
|
|
155
|
+
const moduleUrl = '.' + modulePath + '?t=' + ts;
|
|
156
|
+
await import(moduleUrl);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
window.$RefreshRuntime$.performReactRefresh();
|
|
160
|
+
console.log('[HMR] Updated', pendingUpdates.size, 'module(s)');
|
|
161
|
+
|
|
162
|
+
pendingUpdates.clear();
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('[HMR] Failed to apply update:', error);
|
|
165
|
+
pendingUpdates.clear();
|
|
166
|
+
location.reload();
|
|
167
|
+
}
|
|
168
|
+
}, 30);
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
console.log('[HMR] React Refresh not available, reloading page');
|
|
172
|
+
location.reload();
|
|
173
|
+
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.error('[HMR] Update failed:', error);
|
|
176
|
+
location.reload();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log('[HMR] Next.js client ready');
|
|
181
|
+
})();
|
|
182
|
+
</script>
|
|
183
|
+
`;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Next.js Link shim code
|
|
187
|
+
*/
|
|
188
|
+
export const NEXT_LINK_SHIM = `
|
|
189
|
+
import React from 'react';
|
|
190
|
+
|
|
191
|
+
const getVirtualBasePath = () => {
|
|
192
|
+
const match = window.location.pathname.match(/^\\/__virtual__\\/\\d+(?:\\/|$)/);
|
|
193
|
+
if (!match) return '';
|
|
194
|
+
return match[0].endsWith('/') ? match[0] : match[0] + '/';
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const getBasePath = () => window.__NEXT_BASE_PATH__ || '';
|
|
198
|
+
|
|
199
|
+
const applyVirtualBase = (url) => {
|
|
200
|
+
if (typeof url !== 'string') return url;
|
|
201
|
+
if (!url || url.startsWith('#') || url.startsWith('?')) return url;
|
|
202
|
+
if (/^(https?:)?\\/\\//.test(url)) return url;
|
|
203
|
+
|
|
204
|
+
// Apply basePath first
|
|
205
|
+
const bp = getBasePath();
|
|
206
|
+
if (bp && url.startsWith('/') && !url.startsWith(bp + '/') && url !== bp) {
|
|
207
|
+
url = bp + url;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const base = getVirtualBasePath();
|
|
211
|
+
if (!base) return url;
|
|
212
|
+
if (url.startsWith(base)) return url;
|
|
213
|
+
if (url.startsWith('/')) return base + url.slice(1);
|
|
214
|
+
return base + url;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export default function Link({ href, children, ...props }) {
|
|
218
|
+
const handleClick = (e) => {
|
|
219
|
+
console.log('[Link] Click handler called, href:', href);
|
|
220
|
+
|
|
221
|
+
if (props.onClick) {
|
|
222
|
+
props.onClick(e);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Allow cmd/ctrl click to open in new tab
|
|
226
|
+
if (e.metaKey || e.ctrlKey) {
|
|
227
|
+
console.log('[Link] Meta/Ctrl key pressed, allowing default behavior');
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (typeof href !== 'string' || !href || href.startsWith('#') || href.startsWith('?')) {
|
|
232
|
+
console.log('[Link] Skipping navigation for href:', href);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (/^(https?:)?\\/\\//.test(href)) {
|
|
237
|
+
console.log('[Link] External URL, allowing default behavior:', href);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
const resolvedHref = applyVirtualBase(href);
|
|
243
|
+
console.log('[Link] Navigating to:', resolvedHref);
|
|
244
|
+
window.history.pushState({}, '', resolvedHref);
|
|
245
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
return React.createElement('a', { href, onClick: handleClick, ...props }, children);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export { Link };
|
|
252
|
+
`;
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Next.js Router shim code
|
|
256
|
+
*/
|
|
257
|
+
export const NEXT_ROUTER_SHIM = `
|
|
258
|
+
import React, { useState, useEffect, createContext, useContext } from 'react';
|
|
259
|
+
|
|
260
|
+
const RouterContext = createContext(null);
|
|
261
|
+
|
|
262
|
+
const getVirtualBasePath = () => {
|
|
263
|
+
const match = window.location.pathname.match(/^\\/__virtual__\\/\\d+(?:\\/|$)/);
|
|
264
|
+
if (!match) return '';
|
|
265
|
+
return match[0].endsWith('/') ? match[0] : match[0] + '/';
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const applyVirtualBase = (url) => {
|
|
269
|
+
if (typeof url !== 'string') return url;
|
|
270
|
+
if (!url || url.startsWith('#') || url.startsWith('?')) return url;
|
|
271
|
+
if (/^(https?:)?\\/\\//.test(url)) return url;
|
|
272
|
+
|
|
273
|
+
const base = getVirtualBasePath();
|
|
274
|
+
if (!base) return url;
|
|
275
|
+
if (url.startsWith(base)) return url;
|
|
276
|
+
if (url.startsWith('/')) return base + url.slice(1);
|
|
277
|
+
return base + url;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const stripVirtualBase = (pathname) => {
|
|
281
|
+
const match = pathname.match(/^\\/__virtual__\\/\\d+(?:\\/|$)/);
|
|
282
|
+
if (!match) return pathname;
|
|
283
|
+
return '/' + pathname.slice(match[0].length);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export function useRouter() {
|
|
287
|
+
const [pathname, setPathname] = useState(
|
|
288
|
+
typeof window !== 'undefined' ? stripVirtualBase(window.location.pathname) : '/'
|
|
289
|
+
);
|
|
290
|
+
const [query, setQuery] = useState({});
|
|
291
|
+
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
const updateRoute = () => {
|
|
294
|
+
setPathname(stripVirtualBase(window.location.pathname));
|
|
295
|
+
setQuery(Object.fromEntries(new URLSearchParams(window.location.search)));
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
window.addEventListener('popstate', updateRoute);
|
|
299
|
+
updateRoute();
|
|
300
|
+
|
|
301
|
+
return () => window.removeEventListener('popstate', updateRoute);
|
|
302
|
+
}, []);
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
pathname,
|
|
306
|
+
query,
|
|
307
|
+
asPath: pathname + window.location.search,
|
|
308
|
+
push: (url, as, options) => {
|
|
309
|
+
if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
|
|
310
|
+
window.location.href = url;
|
|
311
|
+
return Promise.resolve(true);
|
|
312
|
+
}
|
|
313
|
+
const resolvedUrl = applyVirtualBase(url);
|
|
314
|
+
window.history.pushState({}, '', resolvedUrl);
|
|
315
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
316
|
+
return Promise.resolve(true);
|
|
317
|
+
},
|
|
318
|
+
replace: (url, as, options) => {
|
|
319
|
+
if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
|
|
320
|
+
window.location.href = url;
|
|
321
|
+
return Promise.resolve(true);
|
|
322
|
+
}
|
|
323
|
+
const resolvedUrl = applyVirtualBase(url);
|
|
324
|
+
window.history.replaceState({}, '', resolvedUrl);
|
|
325
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
326
|
+
return Promise.resolve(true);
|
|
327
|
+
},
|
|
328
|
+
prefetch: () => Promise.resolve(),
|
|
329
|
+
back: () => window.history.back(),
|
|
330
|
+
forward: () => window.history.forward(),
|
|
331
|
+
reload: () => window.location.reload(),
|
|
332
|
+
events: {
|
|
333
|
+
on: () => {},
|
|
334
|
+
off: () => {},
|
|
335
|
+
emit: () => {},
|
|
336
|
+
},
|
|
337
|
+
isFallback: false,
|
|
338
|
+
isReady: true,
|
|
339
|
+
isPreview: false,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export const Router = {
|
|
344
|
+
events: {
|
|
345
|
+
on: () => {},
|
|
346
|
+
off: () => {},
|
|
347
|
+
emit: () => {},
|
|
348
|
+
},
|
|
349
|
+
push: (url) => {
|
|
350
|
+
if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
|
|
351
|
+
window.location.href = url;
|
|
352
|
+
return Promise.resolve(true);
|
|
353
|
+
}
|
|
354
|
+
const resolvedUrl = applyVirtualBase(url);
|
|
355
|
+
window.history.pushState({}, '', resolvedUrl);
|
|
356
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
357
|
+
return Promise.resolve(true);
|
|
358
|
+
},
|
|
359
|
+
replace: (url) => {
|
|
360
|
+
if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
|
|
361
|
+
window.location.href = url;
|
|
362
|
+
return Promise.resolve(true);
|
|
363
|
+
}
|
|
364
|
+
const resolvedUrl = applyVirtualBase(url);
|
|
365
|
+
window.history.replaceState({}, '', resolvedUrl);
|
|
366
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
367
|
+
return Promise.resolve(true);
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
export default { useRouter, Router };
|
|
372
|
+
`;
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Next.js Navigation shim code (App Router)
|
|
376
|
+
*
|
|
377
|
+
* This shim provides App Router-specific navigation hooks from 'next/navigation'.
|
|
378
|
+
* These are DIFFERENT from the Pages Router hooks in 'next/router':
|
|
379
|
+
*
|
|
380
|
+
* Pages Router (next/router):
|
|
381
|
+
* - useRouter() returns { pathname, query, push, replace, events, ... }
|
|
382
|
+
* - Has router.events for route change subscriptions
|
|
383
|
+
* - query object contains URL params
|
|
384
|
+
*
|
|
385
|
+
* App Router (next/navigation):
|
|
386
|
+
* - useRouter() returns { push, replace, back, forward, refresh, prefetch }
|
|
387
|
+
* - usePathname() for current path
|
|
388
|
+
* - useSearchParams() for URL search params
|
|
389
|
+
* - useParams() for dynamic route segments
|
|
390
|
+
* - No events - use useEffect with pathname/searchParams instead
|
|
391
|
+
*
|
|
392
|
+
* @see https://nextjs.org/docs/app/api-reference/functions/use-router
|
|
393
|
+
*/
|
|
394
|
+
export const NEXT_NAVIGATION_SHIM = `
|
|
395
|
+
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
396
|
+
|
|
397
|
+
const getVirtualBasePath = () => {
|
|
398
|
+
const match = window.location.pathname.match(/^\\/__virtual__\\/\\d+(?:\\/|$)/);
|
|
399
|
+
if (!match) return '';
|
|
400
|
+
return match[0].endsWith('/') ? match[0] : match[0] + '/';
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const applyVirtualBase = (url) => {
|
|
404
|
+
if (typeof url !== 'string') return url;
|
|
405
|
+
if (!url || url.startsWith('#') || url.startsWith('?')) return url;
|
|
406
|
+
if (/^(https?:)?\\/\\//.test(url)) return url;
|
|
407
|
+
|
|
408
|
+
const base = getVirtualBasePath();
|
|
409
|
+
if (!base) return url;
|
|
410
|
+
if (url.startsWith(base)) return url;
|
|
411
|
+
if (url.startsWith('/')) return base + url.slice(1);
|
|
412
|
+
return base + url;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const stripVirtualBase = (pathname) => {
|
|
416
|
+
const match = pathname.match(/^\\/__virtual__\\/\\d+(?:\\/|$)/);
|
|
417
|
+
if (!match) return pathname;
|
|
418
|
+
return '/' + pathname.slice(match[0].length);
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* App Router's useRouter hook
|
|
423
|
+
* Returns navigation methods only (no pathname, no query)
|
|
424
|
+
* Use usePathname() and useSearchParams() for URL info
|
|
425
|
+
*/
|
|
426
|
+
export function useRouter() {
|
|
427
|
+
const push = useCallback((url, options) => {
|
|
428
|
+
if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
|
|
429
|
+
window.location.href = url;
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const resolvedUrl = applyVirtualBase(url);
|
|
433
|
+
window.history.pushState({}, '', resolvedUrl);
|
|
434
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
435
|
+
}, []);
|
|
436
|
+
|
|
437
|
+
const replace = useCallback((url, options) => {
|
|
438
|
+
if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
|
|
439
|
+
window.location.href = url;
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const resolvedUrl = applyVirtualBase(url);
|
|
443
|
+
window.history.replaceState({}, '', resolvedUrl);
|
|
444
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
445
|
+
}, []);
|
|
446
|
+
|
|
447
|
+
const back = useCallback(() => window.history.back(), []);
|
|
448
|
+
const forward = useCallback(() => window.history.forward(), []);
|
|
449
|
+
const refresh = useCallback(() => window.location.reload(), []);
|
|
450
|
+
const prefetch = useCallback(() => Promise.resolve(), []);
|
|
451
|
+
|
|
452
|
+
return useMemo(() => ({
|
|
453
|
+
push,
|
|
454
|
+
replace,
|
|
455
|
+
back,
|
|
456
|
+
forward,
|
|
457
|
+
refresh,
|
|
458
|
+
prefetch,
|
|
459
|
+
}), [push, replace, back, forward, refresh, prefetch]);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* usePathname - Returns the current URL pathname
|
|
464
|
+
* Reactively updates when navigation occurs
|
|
465
|
+
* @example const pathname = usePathname(); // '/dashboard/settings'
|
|
466
|
+
*/
|
|
467
|
+
export function usePathname() {
|
|
468
|
+
const [pathname, setPathname] = useState(
|
|
469
|
+
typeof window !== 'undefined' ? stripVirtualBase(window.location.pathname) : '/'
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
useEffect(() => {
|
|
473
|
+
const handler = () => setPathname(stripVirtualBase(window.location.pathname));
|
|
474
|
+
window.addEventListener('popstate', handler);
|
|
475
|
+
return () => window.removeEventListener('popstate', handler);
|
|
476
|
+
}, []);
|
|
477
|
+
|
|
478
|
+
return pathname;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* useSearchParams - Returns the current URL search parameters
|
|
483
|
+
* @example const searchParams = useSearchParams();
|
|
484
|
+
* const query = searchParams.get('q'); // '?q=hello' -> 'hello'
|
|
485
|
+
*/
|
|
486
|
+
export function useSearchParams() {
|
|
487
|
+
const [searchParams, setSearchParams] = useState(() => {
|
|
488
|
+
if (typeof window === 'undefined') return new URLSearchParams();
|
|
489
|
+
return new URLSearchParams(window.location.search);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
useEffect(() => {
|
|
493
|
+
const handler = () => {
|
|
494
|
+
setSearchParams(new URLSearchParams(window.location.search));
|
|
495
|
+
};
|
|
496
|
+
window.addEventListener('popstate', handler);
|
|
497
|
+
return () => window.removeEventListener('popstate', handler);
|
|
498
|
+
}, []);
|
|
499
|
+
|
|
500
|
+
return searchParams;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* useParams - Returns dynamic route parameters
|
|
505
|
+
* For route /users/[id]/page.jsx with URL /users/123:
|
|
506
|
+
* @example const { id } = useParams(); // { id: '123' }
|
|
507
|
+
*
|
|
508
|
+
* Fetches params from the server's route-info endpoint for dynamic routes.
|
|
509
|
+
*/
|
|
510
|
+
export function useParams() {
|
|
511
|
+
const [params, setParams] = useState(() => {
|
|
512
|
+
// Check if initial params were embedded by the server
|
|
513
|
+
if (typeof window !== 'undefined' && window.__NEXT_ROUTE_PARAMS__) {
|
|
514
|
+
return window.__NEXT_ROUTE_PARAMS__;
|
|
515
|
+
}
|
|
516
|
+
return {};
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
useEffect(() => {
|
|
520
|
+
let cancelled = false;
|
|
521
|
+
|
|
522
|
+
const fetchParams = async () => {
|
|
523
|
+
const pathname = stripVirtualBase(window.location.pathname);
|
|
524
|
+
const base = getVirtualBasePath();
|
|
525
|
+
const baseUrl = base ? base.replace(/\\/$/, '') : '';
|
|
526
|
+
|
|
527
|
+
try {
|
|
528
|
+
const response = await fetch(baseUrl + '/_next/route-info?pathname=' + encodeURIComponent(pathname));
|
|
529
|
+
const info = await response.json();
|
|
530
|
+
if (!cancelled && info.params) {
|
|
531
|
+
setParams(info.params);
|
|
532
|
+
}
|
|
533
|
+
} catch (e) {
|
|
534
|
+
// Silently fail - static routes won't have params
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
fetchParams();
|
|
539
|
+
|
|
540
|
+
const handler = () => fetchParams();
|
|
541
|
+
window.addEventListener('popstate', handler);
|
|
542
|
+
return () => {
|
|
543
|
+
cancelled = true;
|
|
544
|
+
window.removeEventListener('popstate', handler);
|
|
545
|
+
};
|
|
546
|
+
}, []);
|
|
547
|
+
|
|
548
|
+
return params;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* useSelectedLayoutSegment - Returns the active child segment one level below
|
|
553
|
+
* Useful for styling active nav items in layouts
|
|
554
|
+
* @example For /dashboard/settings, returns 'settings' in dashboard layout
|
|
555
|
+
*/
|
|
556
|
+
export function useSelectedLayoutSegment() {
|
|
557
|
+
const pathname = usePathname();
|
|
558
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
559
|
+
return segments[0] || null;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* useSelectedLayoutSegments - Returns all active child segments
|
|
564
|
+
* @example For /dashboard/settings/profile, returns ['dashboard', 'settings', 'profile']
|
|
565
|
+
*/
|
|
566
|
+
export function useSelectedLayoutSegments() {
|
|
567
|
+
const pathname = usePathname();
|
|
568
|
+
return pathname.split('/').filter(Boolean);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* redirect - Programmatic redirect (typically used in Server Components)
|
|
573
|
+
* In this browser implementation, performs immediate navigation
|
|
574
|
+
*/
|
|
575
|
+
export function redirect(url) {
|
|
576
|
+
if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
|
|
577
|
+
window.location.href = url;
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
window.location.href = applyVirtualBase(url);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* notFound - Trigger the not-found UI
|
|
585
|
+
* In this browser implementation, throws an error
|
|
586
|
+
*/
|
|
587
|
+
export function notFound() {
|
|
588
|
+
throw new Error('NEXT_NOT_FOUND');
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Re-export Link for convenience (can import from next/navigation or next/link)
|
|
592
|
+
export { default as Link } from 'next/link';
|
|
593
|
+
`;
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Next.js Head shim code
|
|
597
|
+
*/
|
|
598
|
+
export const NEXT_HEAD_SHIM = `
|
|
599
|
+
import React, { useEffect } from 'react';
|
|
600
|
+
|
|
601
|
+
export default function Head({ children }) {
|
|
602
|
+
useEffect(() => {
|
|
603
|
+
// Process children and update document.head
|
|
604
|
+
React.Children.forEach(children, (child) => {
|
|
605
|
+
if (!React.isValidElement(child)) return;
|
|
606
|
+
|
|
607
|
+
const { type, props } = child;
|
|
608
|
+
|
|
609
|
+
if (type === 'title' && props.children) {
|
|
610
|
+
document.title = Array.isArray(props.children)
|
|
611
|
+
? props.children.join('')
|
|
612
|
+
: props.children;
|
|
613
|
+
} else if (type === 'meta') {
|
|
614
|
+
const existingMeta = props.name
|
|
615
|
+
? document.querySelector(\`meta[name="\${props.name}"]\`)
|
|
616
|
+
: props.property
|
|
617
|
+
? document.querySelector(\`meta[property="\${props.property}"]\`)
|
|
618
|
+
: null;
|
|
619
|
+
|
|
620
|
+
if (existingMeta) {
|
|
621
|
+
Object.keys(props).forEach(key => {
|
|
622
|
+
existingMeta.setAttribute(key, props[key]);
|
|
623
|
+
});
|
|
624
|
+
} else {
|
|
625
|
+
const meta = document.createElement('meta');
|
|
626
|
+
Object.keys(props).forEach(key => {
|
|
627
|
+
meta.setAttribute(key, props[key]);
|
|
628
|
+
});
|
|
629
|
+
document.head.appendChild(meta);
|
|
630
|
+
}
|
|
631
|
+
} else if (type === 'link') {
|
|
632
|
+
const link = document.createElement('link');
|
|
633
|
+
Object.keys(props).forEach(key => {
|
|
634
|
+
link.setAttribute(key, props[key]);
|
|
635
|
+
});
|
|
636
|
+
document.head.appendChild(link);
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
}, [children]);
|
|
640
|
+
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
`;
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Next.js Image shim code
|
|
647
|
+
* Provides a simple img-based implementation of next/image
|
|
648
|
+
*/
|
|
649
|
+
export const NEXT_IMAGE_SHIM = `
|
|
650
|
+
import React from 'react';
|
|
651
|
+
|
|
652
|
+
function Image({
|
|
653
|
+
src,
|
|
654
|
+
alt = '',
|
|
655
|
+
width,
|
|
656
|
+
height,
|
|
657
|
+
fill,
|
|
658
|
+
loader,
|
|
659
|
+
quality = 75,
|
|
660
|
+
priority,
|
|
661
|
+
loading,
|
|
662
|
+
placeholder,
|
|
663
|
+
blurDataURL,
|
|
664
|
+
unoptimized,
|
|
665
|
+
onLoad,
|
|
666
|
+
onError,
|
|
667
|
+
style,
|
|
668
|
+
className,
|
|
669
|
+
sizes,
|
|
670
|
+
...rest
|
|
671
|
+
}) {
|
|
672
|
+
// Handle src - could be string or StaticImageData object
|
|
673
|
+
const imageSrc = typeof src === 'object' ? src.src : src;
|
|
674
|
+
|
|
675
|
+
// Build style object
|
|
676
|
+
const imgStyle = { ...style };
|
|
677
|
+
if (fill) {
|
|
678
|
+
imgStyle.position = 'absolute';
|
|
679
|
+
imgStyle.width = '100%';
|
|
680
|
+
imgStyle.height = '100%';
|
|
681
|
+
imgStyle.objectFit = imgStyle.objectFit || 'cover';
|
|
682
|
+
imgStyle.inset = '0';
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return React.createElement('img', {
|
|
686
|
+
src: imageSrc,
|
|
687
|
+
alt,
|
|
688
|
+
width: fill ? undefined : width,
|
|
689
|
+
height: fill ? undefined : height,
|
|
690
|
+
loading: priority ? 'eager' : (loading || 'lazy'),
|
|
691
|
+
decoding: 'async',
|
|
692
|
+
style: imgStyle,
|
|
693
|
+
className,
|
|
694
|
+
onLoad,
|
|
695
|
+
onError,
|
|
696
|
+
...rest
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export default Image;
|
|
701
|
+
export { Image };
|
|
702
|
+
`;
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* next/dynamic shim - Dynamic imports with loading states
|
|
706
|
+
*/
|
|
707
|
+
export const NEXT_DYNAMIC_SHIM = `
|
|
708
|
+
import React from 'react';
|
|
709
|
+
|
|
710
|
+
function dynamic(importFn, options = {}) {
|
|
711
|
+
const {
|
|
712
|
+
loading: LoadingComponent,
|
|
713
|
+
ssr = true,
|
|
714
|
+
} = options;
|
|
715
|
+
|
|
716
|
+
// Create a lazy component
|
|
717
|
+
const LazyComponent = React.lazy(importFn);
|
|
718
|
+
|
|
719
|
+
// Wrapper component that handles loading state
|
|
720
|
+
function DynamicComponent(props) {
|
|
721
|
+
const fallback = LoadingComponent
|
|
722
|
+
? React.createElement(LoadingComponent, { isLoading: true })
|
|
723
|
+
: null;
|
|
724
|
+
|
|
725
|
+
return React.createElement(
|
|
726
|
+
React.Suspense,
|
|
727
|
+
{ fallback },
|
|
728
|
+
React.createElement(LazyComponent, props)
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return DynamicComponent;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export default dynamic;
|
|
736
|
+
export { dynamic };
|
|
737
|
+
`;
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* next/script shim - Loads external scripts
|
|
741
|
+
*/
|
|
742
|
+
export const NEXT_SCRIPT_SHIM = `
|
|
743
|
+
import React from 'react';
|
|
744
|
+
|
|
745
|
+
function Script({
|
|
746
|
+
src,
|
|
747
|
+
strategy = 'afterInteractive',
|
|
748
|
+
onLoad,
|
|
749
|
+
onReady,
|
|
750
|
+
onError,
|
|
751
|
+
children,
|
|
752
|
+
dangerouslySetInnerHTML,
|
|
753
|
+
...rest
|
|
754
|
+
}) {
|
|
755
|
+
React.useEffect(function() {
|
|
756
|
+
if (!src && !children && !dangerouslySetInnerHTML) return;
|
|
757
|
+
|
|
758
|
+
var script = document.createElement('script');
|
|
759
|
+
|
|
760
|
+
if (src) {
|
|
761
|
+
script.src = src;
|
|
762
|
+
script.async = strategy !== 'beforeInteractive';
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
Object.keys(rest).forEach(function(key) {
|
|
766
|
+
script.setAttribute(key, rest[key]);
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
if (children) {
|
|
770
|
+
script.textContent = children;
|
|
771
|
+
} else if (dangerouslySetInnerHTML && dangerouslySetInnerHTML.__html) {
|
|
772
|
+
script.textContent = dangerouslySetInnerHTML.__html;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
script.onload = function() {
|
|
776
|
+
if (onLoad) onLoad();
|
|
777
|
+
if (onReady) onReady();
|
|
778
|
+
};
|
|
779
|
+
script.onerror = onError;
|
|
780
|
+
|
|
781
|
+
document.head.appendChild(script);
|
|
782
|
+
|
|
783
|
+
return function() {
|
|
784
|
+
if (script.parentNode) {
|
|
785
|
+
script.parentNode.removeChild(script);
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
}, [src]);
|
|
789
|
+
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
export default Script;
|
|
794
|
+
export { Script };
|
|
795
|
+
`;
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* next/font/google shim - Loads Google Fonts via CDN
|
|
799
|
+
* Uses a Proxy to dynamically handle ANY Google Font without hardcoding
|
|
800
|
+
*/
|
|
801
|
+
export const NEXT_FONT_GOOGLE_SHIM = `
|
|
802
|
+
// Track loaded fonts to avoid duplicate style injections
|
|
803
|
+
const loadedFonts = new Set();
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Convert font function name to Google Fonts family name
|
|
807
|
+
* Examples:
|
|
808
|
+
* DM_Sans -> DM Sans
|
|
809
|
+
* Open_Sans -> Open Sans
|
|
810
|
+
* Fraunces -> Fraunces
|
|
811
|
+
*/
|
|
812
|
+
function toFontFamily(fontName) {
|
|
813
|
+
return fontName.replace(/_/g, ' ');
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Inject font CSS into document
|
|
818
|
+
* - Adds preconnect links for faster font loading
|
|
819
|
+
* - Loads the font from Google Fonts CDN
|
|
820
|
+
* - Creates a CSS class that sets the CSS variable
|
|
821
|
+
*/
|
|
822
|
+
function injectFontCSS(fontFamily, variableName, weight, style) {
|
|
823
|
+
const fontKey = fontFamily + '-' + (variableName || 'default');
|
|
824
|
+
if (loadedFonts.has(fontKey)) {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
loadedFonts.add(fontKey);
|
|
828
|
+
|
|
829
|
+
if (typeof document === 'undefined') {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// Add preconnect links for faster loading (only once)
|
|
834
|
+
if (!document.querySelector('link[href="https://fonts.googleapis.com"]')) {
|
|
835
|
+
const preconnect1 = document.createElement('link');
|
|
836
|
+
preconnect1.rel = 'preconnect';
|
|
837
|
+
preconnect1.href = 'https://fonts.googleapis.com';
|
|
838
|
+
document.head.appendChild(preconnect1);
|
|
839
|
+
|
|
840
|
+
const preconnect2 = document.createElement('link');
|
|
841
|
+
preconnect2.rel = 'preconnect';
|
|
842
|
+
preconnect2.href = 'https://fonts.gstatic.com';
|
|
843
|
+
preconnect2.crossOrigin = 'anonymous';
|
|
844
|
+
document.head.appendChild(preconnect2);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Build Google Fonts URL
|
|
848
|
+
const escapedFamily = fontFamily.replace(/ /g, '+');
|
|
849
|
+
|
|
850
|
+
// Build axis list based on options
|
|
851
|
+
let axisList = '';
|
|
852
|
+
const axes = [];
|
|
853
|
+
|
|
854
|
+
// Handle italic style
|
|
855
|
+
if (style === 'italic') {
|
|
856
|
+
axes.push('ital');
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Handle weight - use specific weight or variable range
|
|
860
|
+
if (weight && weight !== '400' && !Array.isArray(weight)) {
|
|
861
|
+
// Specific weight requested
|
|
862
|
+
axes.push('wght');
|
|
863
|
+
if (style === 'italic') {
|
|
864
|
+
axisList = ':ital,wght@1,' + weight;
|
|
865
|
+
} else {
|
|
866
|
+
axisList = ':wght@' + weight;
|
|
867
|
+
}
|
|
868
|
+
} else if (Array.isArray(weight)) {
|
|
869
|
+
// Multiple weights
|
|
870
|
+
axes.push('wght');
|
|
871
|
+
axisList = ':wght@' + weight.join(';');
|
|
872
|
+
} else {
|
|
873
|
+
// Default: request common weights for flexibility
|
|
874
|
+
axisList = ':wght@400;500;600;700';
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const fontUrl = 'https://fonts.googleapis.com/css2?family=' +
|
|
878
|
+
escapedFamily + axisList + '&display=swap';
|
|
879
|
+
|
|
880
|
+
// Add link element for Google Fonts (if not already present)
|
|
881
|
+
if (!document.querySelector('link[href*="family=' + escapedFamily + '"]')) {
|
|
882
|
+
const link = document.createElement('link');
|
|
883
|
+
link.rel = 'stylesheet';
|
|
884
|
+
link.href = fontUrl;
|
|
885
|
+
document.head.appendChild(link);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Create style element for CSS variable at :root level (globally available)
|
|
889
|
+
// This makes the variable work without needing to apply the class to body
|
|
890
|
+
if (variableName) {
|
|
891
|
+
const styleEl = document.createElement('style');
|
|
892
|
+
styleEl.setAttribute('data-font-var', variableName);
|
|
893
|
+
styleEl.textContent = ':root { ' + variableName + ': "' + fontFamily + '", ' + (fontFamily.includes('Serif') ? 'serif' : 'sans-serif') + '; }';
|
|
894
|
+
document.head.appendChild(styleEl);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Create a font loader function for a specific font
|
|
900
|
+
*/
|
|
901
|
+
function createFontLoader(fontName) {
|
|
902
|
+
const fontFamily = toFontFamily(fontName);
|
|
903
|
+
|
|
904
|
+
return function(options = {}) {
|
|
905
|
+
const {
|
|
906
|
+
weight,
|
|
907
|
+
style = 'normal',
|
|
908
|
+
subsets = ['latin'],
|
|
909
|
+
variable,
|
|
910
|
+
display = 'swap',
|
|
911
|
+
preload = true,
|
|
912
|
+
fallback = ['sans-serif'],
|
|
913
|
+
adjustFontFallback = true
|
|
914
|
+
} = options;
|
|
915
|
+
|
|
916
|
+
// Inject the font CSS
|
|
917
|
+
injectFontCSS(fontFamily, variable, weight, style);
|
|
918
|
+
|
|
919
|
+
// Generate class name from variable (--font-inter -> __font-inter)
|
|
920
|
+
const className = variable
|
|
921
|
+
? variable.replace('--', '__')
|
|
922
|
+
: '__font-' + fontName.toLowerCase().replace(/_/g, '-');
|
|
923
|
+
|
|
924
|
+
return {
|
|
925
|
+
className,
|
|
926
|
+
variable: className,
|
|
927
|
+
style: {
|
|
928
|
+
fontFamily: '"' + fontFamily + '", ' + fallback.join(', ')
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* Use a Proxy to dynamically create font loaders for ANY font name
|
|
936
|
+
* This allows: import { AnyGoogleFont } from "next/font/google"
|
|
937
|
+
*/
|
|
938
|
+
const fontProxy = new Proxy({}, {
|
|
939
|
+
get(target, prop) {
|
|
940
|
+
// Handle special properties
|
|
941
|
+
if (prop === '__esModule') return true;
|
|
942
|
+
if (prop === 'default') return fontProxy;
|
|
943
|
+
if (typeof prop !== 'string') return undefined;
|
|
944
|
+
|
|
945
|
+
// Create a font loader for this font name
|
|
946
|
+
return createFontLoader(prop);
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
// Export the proxy as both default and named exports
|
|
951
|
+
export default fontProxy;
|
|
952
|
+
|
|
953
|
+
// Re-export through proxy for named imports
|
|
954
|
+
export const {
|
|
955
|
+
Fraunces, Inter, DM_Sans, DM_Serif_Text, Roboto, Open_Sans, Lato,
|
|
956
|
+
Montserrat, Poppins, Playfair_Display, Merriweather, Raleway, Nunito,
|
|
957
|
+
Ubuntu, Oswald, Quicksand, Work_Sans, Fira_Sans, Barlow, Mulish, Rubik,
|
|
958
|
+
Noto_Sans, Manrope, Space_Grotesk, Geist, Geist_Mono
|
|
959
|
+
} = fontProxy;
|
|
960
|
+
`;
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* next/font/local shim - Loads local font files
|
|
964
|
+
* Accepts font source path and creates @font-face declaration + CSS variable
|
|
965
|
+
*/
|
|
966
|
+
export const NEXT_FONT_LOCAL_SHIM = `
|
|
967
|
+
const loadedLocalFonts = new Set();
|
|
968
|
+
|
|
969
|
+
function localFont(options = {}) {
|
|
970
|
+
const {
|
|
971
|
+
src,
|
|
972
|
+
weight,
|
|
973
|
+
style = 'normal',
|
|
974
|
+
variable,
|
|
975
|
+
display = 'swap',
|
|
976
|
+
fallback = ['sans-serif'],
|
|
977
|
+
declarations = [],
|
|
978
|
+
adjustFontFallback = true
|
|
979
|
+
} = options;
|
|
980
|
+
|
|
981
|
+
// Determine font family name from variable or src
|
|
982
|
+
const familyName = variable
|
|
983
|
+
? variable.replace('--', '').replace(/-/g, ' ')
|
|
984
|
+
: 'local-font-' + Math.random().toString(36).slice(2, 8);
|
|
985
|
+
|
|
986
|
+
const fontKey = familyName + '-' + (variable || 'default');
|
|
987
|
+
if (typeof document !== 'undefined' && !loadedLocalFonts.has(fontKey)) {
|
|
988
|
+
loadedLocalFonts.add(fontKey);
|
|
989
|
+
|
|
990
|
+
// Build @font-face declarations
|
|
991
|
+
let fontFaces = '';
|
|
992
|
+
|
|
993
|
+
if (typeof src === 'string') {
|
|
994
|
+
// Single source
|
|
995
|
+
fontFaces = '@font-face {\\n' +
|
|
996
|
+
' font-family: "' + familyName + '";\\n' +
|
|
997
|
+
' src: url("' + src + '");\\n' +
|
|
998
|
+
' font-weight: ' + (weight || '400') + ';\\n' +
|
|
999
|
+
' font-style: ' + style + ';\\n' +
|
|
1000
|
+
' font-display: ' + display + ';\\n' +
|
|
1001
|
+
'}';
|
|
1002
|
+
} else if (Array.isArray(src)) {
|
|
1003
|
+
// Multiple sources (different weights/styles)
|
|
1004
|
+
fontFaces = src.map(function(s) {
|
|
1005
|
+
const path = typeof s === 'string' ? s : s.path;
|
|
1006
|
+
const w = (typeof s === 'object' && s.weight) || weight || '400';
|
|
1007
|
+
const st = (typeof s === 'object' && s.style) || style;
|
|
1008
|
+
return '@font-face {\\n' +
|
|
1009
|
+
' font-family: "' + familyName + '";\\n' +
|
|
1010
|
+
' src: url("' + path + '");\\n' +
|
|
1011
|
+
' font-weight: ' + w + ';\\n' +
|
|
1012
|
+
' font-style: ' + st + ';\\n' +
|
|
1013
|
+
' font-display: ' + display + ';\\n' +
|
|
1014
|
+
'}';
|
|
1015
|
+
}).join('\\n');
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Inject font-face CSS
|
|
1019
|
+
if (fontFaces) {
|
|
1020
|
+
var styleEl = document.createElement('style');
|
|
1021
|
+
styleEl.setAttribute('data-local-font', fontKey);
|
|
1022
|
+
styleEl.textContent = fontFaces;
|
|
1023
|
+
document.head.appendChild(styleEl);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// Inject CSS variable at :root level
|
|
1027
|
+
if (variable) {
|
|
1028
|
+
var varStyle = document.createElement('style');
|
|
1029
|
+
varStyle.setAttribute('data-font-var', variable);
|
|
1030
|
+
varStyle.textContent = ':root { ' + variable + ': "' + familyName + '", ' + fallback.join(', ') + '; }';
|
|
1031
|
+
document.head.appendChild(varStyle);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const className = variable
|
|
1036
|
+
? variable.replace('--', '__')
|
|
1037
|
+
: '__font-' + familyName.toLowerCase().replace(/\\s+/g, '-');
|
|
1038
|
+
|
|
1039
|
+
return {
|
|
1040
|
+
className,
|
|
1041
|
+
variable: className,
|
|
1042
|
+
style: {
|
|
1043
|
+
fontFamily: '"' + familyName + '", ' + fallback.join(', ')
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
export default localFont;
|
|
1049
|
+
export { localFont };
|
|
1050
|
+
`;
|