almostnode 0.2.7 → 0.2.8
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 +1 -1
- package/dist/__sw__.js +80 -84
- package/dist/assets/{runtime-worker-B8_LZkBX.js → runtime-worker-D8VYeuKv.js} +1448 -1121
- package/dist/assets/runtime-worker-D8VYeuKv.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 +2895 -2454
- 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 +3208 -2782
- package/dist/index.mjs.map +1 -1
- package/dist/runtime.d.ts +20 -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/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 +94 -15
- 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 +309 -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 +92 -2
- 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
|
@@ -516,8 +516,11 @@ async function deployToConvex(adminKey: string): Promise<void> {
|
|
|
516
516
|
vfs.writeFileSync('/package.json', packageJson);
|
|
517
517
|
|
|
518
518
|
// Create convex.json in /project
|
|
519
|
+
// codegen.fileType: "ts" ensures codegen creates .ts files that esbuild can resolve
|
|
520
|
+
// (default is "js/dts" which creates .js + .d.ts, but our source imports expect .ts)
|
|
519
521
|
vfs.writeFileSync('/project/convex.json', JSON.stringify({
|
|
520
|
-
functions: "convex/"
|
|
522
|
+
functions: "convex/",
|
|
523
|
+
codegen: { fileType: "ts" }
|
|
521
524
|
}, null, 2));
|
|
522
525
|
|
|
523
526
|
// Clean up /project/convex/ completely to ensure fresh state
|
|
@@ -550,19 +553,12 @@ async function deployToConvex(adminKey: string): Promise<void> {
|
|
|
550
553
|
}
|
|
551
554
|
vfs.mkdirSync('/project/convex', { recursive: true });
|
|
552
555
|
|
|
553
|
-
//
|
|
554
|
-
if (vfs.existsSync('/
|
|
555
|
-
|
|
556
|
-
try {
|
|
557
|
-
const files = vfs.readdirSync('/convex/_generated');
|
|
558
|
-
for (const file of files) {
|
|
559
|
-
vfs.unlinkSync(`/convex/_generated/${file}`);
|
|
560
|
-
}
|
|
561
|
-
vfs.rmdirSync('/convex/_generated');
|
|
562
|
-
} catch (e) {
|
|
563
|
-
log(`Warning: Could not remove /convex/_generated: ${e}`, 'warn');
|
|
564
|
-
}
|
|
556
|
+
// Remove /project/.env.local before CLI runs so we can detect when the new deployment creates it.
|
|
557
|
+
if (vfs.existsSync('/project/.env.local')) {
|
|
558
|
+
vfs.unlinkSync('/project/.env.local');
|
|
565
559
|
}
|
|
560
|
+
// NOTE: Do NOT delete /project/convex/_generated/ — esbuild needs those files to resolve
|
|
561
|
+
// imports like `from "./_generated/server"` in user code. The CLI's codegen will update them.
|
|
566
562
|
|
|
567
563
|
// Create convex config files (BOTH .ts and .js required!)
|
|
568
564
|
const convexConfig = `import { defineApp } from "convex/server";
|
|
@@ -580,14 +576,17 @@ export default app;
|
|
|
580
576
|
for (const file of convexFiles) {
|
|
581
577
|
const srcPath = `/convex/${file}`;
|
|
582
578
|
const destPath = `/project/convex/${file}`;
|
|
583
|
-
// Skip _generated directory and only copy files (not directories)
|
|
584
|
-
if (file === '_generated') continue;
|
|
585
579
|
try {
|
|
586
580
|
const stat = vfs.statSync(srcPath);
|
|
587
581
|
if (stat.isFile()) {
|
|
588
582
|
const content = vfs.readFileSync(srcPath, 'utf8');
|
|
589
583
|
vfs.writeFileSync(destPath, content);
|
|
590
|
-
log(` Copied ${file}`);
|
|
584
|
+
log(` Copied ${file} (${content.length}b)`);
|
|
585
|
+
// For todos.ts, show the mutation handler to verify modifications
|
|
586
|
+
if (file === 'todos.ts') {
|
|
587
|
+
const handlerMatch = content.match(/title:\s*args\.title[^\n]*/);
|
|
588
|
+
log(` → todos.ts mutation: ${handlerMatch ? handlerMatch[0] : 'handler not found'}`);
|
|
589
|
+
}
|
|
591
590
|
}
|
|
592
591
|
} catch (e) {
|
|
593
592
|
log(` Warning: Could not copy ${srcPath}: ${e}`, 'warn');
|
|
@@ -635,11 +634,14 @@ export default app;
|
|
|
635
634
|
];
|
|
636
635
|
for (const file of requiredFiles) {
|
|
637
636
|
if (vfs.existsSync(file)) {
|
|
638
|
-
// For convex source files, show content preview to verify it's fresh
|
|
639
637
|
if (file.includes('/project/convex/') && (file.endsWith('.ts') || file.endsWith('.js'))) {
|
|
640
638
|
const content = vfs.readFileSync(file, 'utf8');
|
|
641
|
-
|
|
642
|
-
|
|
639
|
+
log(` ✓ ${file} (${content.length}b)`, 'success');
|
|
640
|
+
// For todos.ts, verify the mutation content the CLI will push
|
|
641
|
+
if (file.endsWith('todos.ts')) {
|
|
642
|
+
const titleLine = content.match(/title:\s*args\.title[^\n]*/);
|
|
643
|
+
log(` → CLI will push: ${titleLine ? titleLine[0].trim() : 'title line not found'}`, 'info');
|
|
644
|
+
}
|
|
643
645
|
} else {
|
|
644
646
|
log(` ✓ ${file}`, 'success');
|
|
645
647
|
}
|
|
@@ -648,7 +650,159 @@ export default app;
|
|
|
648
650
|
}
|
|
649
651
|
}
|
|
650
652
|
|
|
651
|
-
//
|
|
653
|
+
// Patch CLI bundle: stub fetchDeploymentCanonicalSiteUrl
|
|
654
|
+
// This function was added in convex v1.31.7 and calls envGetInDeployment() to fetch CONVEX_SITE_URL
|
|
655
|
+
// from the deployment. The envGetInDeployment call hangs in our browser runtime because it uses
|
|
656
|
+
// a deployment API endpoint we can't handle. We derive the site URL from the deployment URL instead.
|
|
657
|
+
{
|
|
658
|
+
const cliBundlePath = '/project/node_modules/convex/dist/cli.bundle.cjs';
|
|
659
|
+
let cliSrc = vfs.readFileSync(cliBundlePath, 'utf8');
|
|
660
|
+
|
|
661
|
+
const fetchCanonSearch = [
|
|
662
|
+
'async function fetchDeploymentCanonicalSiteUrl(ctx, options) {',
|
|
663
|
+
' const result = await envGetInDeployment(ctx, options, "CONVEX_SITE_URL");',
|
|
664
|
+
' if (typeof result !== "string") {',
|
|
665
|
+
' return await ctx.crash({',
|
|
666
|
+
' exitCode: 1,',
|
|
667
|
+
' errorType: "invalid filesystem or env vars",',
|
|
668
|
+
' printedMessage: "Invalid process.env.CONVEX_SITE_URL"',
|
|
669
|
+
' });',
|
|
670
|
+
' }',
|
|
671
|
+
' return result;',
|
|
672
|
+
'}',
|
|
673
|
+
].join('\n');
|
|
674
|
+
const fetchCanonReplace = [
|
|
675
|
+
'async function fetchDeploymentCanonicalSiteUrl(ctx, options) {',
|
|
676
|
+
' // Stubbed: derive site URL from deployment URL (.convex.cloud -> .convex.site)',
|
|
677
|
+
' var siteUrl = (options?.deploymentUrl || "").replace(".convex.cloud", ".convex.site");',
|
|
678
|
+
' return siteUrl || "https://placeholder.convex.site";',
|
|
679
|
+
'}',
|
|
680
|
+
].join('\n');
|
|
681
|
+
|
|
682
|
+
let patched = cliSrc.replace(fetchCanonSearch, fetchCanonReplace);
|
|
683
|
+
const patch1Applied = patched !== cliSrc;
|
|
684
|
+
log(` Patch 1 (fetchDeploymentCanonicalSiteUrl): ${patch1Applied ? 'APPLIED' : 'already patched or not found'}`);
|
|
685
|
+
|
|
686
|
+
// Patch 2: Stub Sentry5.close() in flushAndExit so process.exit() actually fires.
|
|
687
|
+
// Without this patch, Sentry5.close() hangs forever and process.exit() is never called,
|
|
688
|
+
// making it impossible to detect when the CLI has finished pushing functions.
|
|
689
|
+
const flushAndExitSearch = [
|
|
690
|
+
'async function flushAndExit(exitCode, err) {',
|
|
691
|
+
' if (err) {',
|
|
692
|
+
' Sentry5.captureException(err);',
|
|
693
|
+
' }',
|
|
694
|
+
' await Sentry5.close();',
|
|
695
|
+
' return process.exit(exitCode);',
|
|
696
|
+
'}',
|
|
697
|
+
].join('\n');
|
|
698
|
+
const flushAndExitReplace = [
|
|
699
|
+
'async function flushAndExit(exitCode, err) {',
|
|
700
|
+
' // Patched: skip Sentry5.close() which hangs in browser runtime',
|
|
701
|
+
' var callerStack = new Error("flushAndExit-trace").stack || "";',
|
|
702
|
+
' globalThis.__cliExitInfo = { code: exitCode, msg: err ? (err.message || String(err)) : null, stack: err ? (err.stack || "").substring(0, 2000) : null, callerStack: callerStack.substring(0, 3000) };',
|
|
703
|
+
' return process.exit(exitCode);',
|
|
704
|
+
'}',
|
|
705
|
+
].join('\n');
|
|
706
|
+
|
|
707
|
+
const beforePatch2 = patched;
|
|
708
|
+
patched = patched.replace(flushAndExitSearch, flushAndExitReplace);
|
|
709
|
+
let patch2Applied = patched !== beforePatch2;
|
|
710
|
+
|
|
711
|
+
// Also handle re-patching: if already patched with an older version
|
|
712
|
+
if (!patch2Applied) {
|
|
713
|
+
const oldPatchMarker = '// Patched: skip Sentry5.close()';
|
|
714
|
+
const markerIdx = patched.indexOf(oldPatchMarker);
|
|
715
|
+
if (markerIdx > -1) {
|
|
716
|
+
// Find the function boundaries around our marker
|
|
717
|
+
const funcStart = patched.lastIndexOf('async function flushAndExit(exitCode, err) {', markerIdx);
|
|
718
|
+
const funcEnd = patched.indexOf('\n}', markerIdx);
|
|
719
|
+
if (funcStart > -1 && funcEnd > -1) {
|
|
720
|
+
const oldFunc = patched.substring(funcStart, funcEnd + 2);
|
|
721
|
+
if (oldFunc !== flushAndExitReplace) {
|
|
722
|
+
patched = patched.replace(oldFunc, flushAndExitReplace);
|
|
723
|
+
patch2Applied = patched !== beforePatch2;
|
|
724
|
+
if (patch2Applied) log(' Patch 2: re-patched (upgraded old patch)');
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
log(` Patch 2 (flushAndExit/Sentry5): ${patch2Applied ? 'APPLIED' : 'already up-to-date'}`);
|
|
731
|
+
|
|
732
|
+
// Patch 3: Capture Crash error details in watchAndPush catch block before flushAndExit
|
|
733
|
+
// The Crash object is thrown by runPush() and caught in watchAndPush. It has errorType,
|
|
734
|
+
// printedMessage, message, and stack. We save these to __cliCrashInfo before exiting.
|
|
735
|
+
const watchPushSearch = ' if (cmdOptions.once) {\n await outerCtx.flushAndExit(1, e.errorType);\n }';
|
|
736
|
+
const watchPushReplace = ' if (cmdOptions.once) {\n globalThis.__cliCrashInfo = { errorType: e.errorType, printedMessage: e.printedMessage || null, message: e.message || null, stack: (e.stack || "").substring(0, 2000) };\n await outerCtx.flushAndExit(1, e.errorType);\n }';
|
|
737
|
+
const beforePatch3 = patched;
|
|
738
|
+
patched = patched.replace(watchPushSearch, watchPushReplace);
|
|
739
|
+
const patch3Applied = patched !== beforePatch3;
|
|
740
|
+
log(` Patch 3 (watchAndPush crash details): ${patch3Applied ? 'APPLIED' : 'already patched or not found'}`);
|
|
741
|
+
|
|
742
|
+
// Patch 4: Skip post-esbuild file size mismatch check in doEsbuild
|
|
743
|
+
// The CLI checks that each bundled file's size hasn't changed after esbuild runs.
|
|
744
|
+
// In our browser VFS, stat().size may differ from esbuild's metafile byte count
|
|
745
|
+
// (e.g., UTF-8 encoding differences), causing a false "transient" crash.
|
|
746
|
+
// We skip the size comparison since VFS files can't change from external sources.
|
|
747
|
+
const sizeCheckSearch = ' if (st.size !== input.bytes) {\n logWarning(\n `Bundled file ${absPath} changed right after esbuild invocation`\n );\n return await ctx.crash({\n exitCode: 1,\n errorType: "transient",\n printedMessage: null\n });\n }';
|
|
748
|
+
const sizeCheckReplace = ' // Patched: skip file size check (VFS stat size may differ from esbuild byte count)';
|
|
749
|
+
const beforePatch4 = patched;
|
|
750
|
+
patched = patched.replace(sizeCheckSearch, sizeCheckReplace);
|
|
751
|
+
const patch4Applied = patched !== beforePatch4;
|
|
752
|
+
log(` Patch 4 (skip file size check): ${patch4Applied ? 'APPLIED' : 'already patched or not found'}`);
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
if (patched !== cliSrc) {
|
|
756
|
+
vfs.writeFileSync(cliBundlePath, patched);
|
|
757
|
+
log('CLI bundle patched and saved');
|
|
758
|
+
} else {
|
|
759
|
+
log('CLI bundle already patched (no changes needed)');
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Set up process.exit listener BEFORE running CLI.
|
|
764
|
+
// The CLI calls flushAndExit → process.exit(0) when deployment completes.
|
|
765
|
+
// This is the most reliable completion signal since it fires AFTER the push POST finishes.
|
|
766
|
+
const cliExitPromise = new Promise<number>((resolve) => {
|
|
767
|
+
const proc = cliRuntime!.getProcess();
|
|
768
|
+
proc.on('exit', (code: unknown) => {
|
|
769
|
+
log(`CLI process exited with code ${code}`);
|
|
770
|
+
const crashInfo = (globalThis as any).__cliCrashInfo;
|
|
771
|
+
if (crashInfo) {
|
|
772
|
+
log(`CLI CRASH: [${crashInfo.errorType}] msg=${crashInfo.printedMessage || crashInfo.message || '(no message)'}`, 'error');
|
|
773
|
+
if (crashInfo.stack) log(`CLI CRASH STACK: ${crashInfo.stack.substring(0, 500)}`, 'error');
|
|
774
|
+
}
|
|
775
|
+
const exitInfo = (globalThis as any).__cliExitInfo;
|
|
776
|
+
if (exitInfo) {
|
|
777
|
+
log(`CLI exit: code=${exitInfo.code} err=${exitInfo.msg || 'none'}`, exitInfo.code === 0 ? 'success' : 'error');
|
|
778
|
+
}
|
|
779
|
+
resolve(code as number);
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
// Intercept fetch to log Convex API push calls
|
|
784
|
+
const origFetch = globalThis.fetch;
|
|
785
|
+
globalThis.fetch = async function(input: RequestInfo | URL, init?: RequestInit) {
|
|
786
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
|
|
787
|
+
if (url.includes('convex.cloud') || url.includes('convex.site')) {
|
|
788
|
+
const method = init?.method || 'GET';
|
|
789
|
+
log(`[fetch] ${method} ${url.substring(0, 120)}`);
|
|
790
|
+
try {
|
|
791
|
+
const resp = await origFetch.call(globalThis, input, init);
|
|
792
|
+
log(`[fetch] ${method} ${url.substring(0, 80)} → ${resp.status} ${resp.statusText}`);
|
|
793
|
+
if (!resp.ok) {
|
|
794
|
+
const text = await resp.clone().text();
|
|
795
|
+
log(`[fetch] ERROR body: ${text.substring(0, 500)}`, 'error');
|
|
796
|
+
}
|
|
797
|
+
return resp;
|
|
798
|
+
} catch (e) {
|
|
799
|
+
log(`[fetch] ${method} ${url.substring(0, 80)} → NETWORK ERROR: ${(e as Error).message}`, 'error');
|
|
800
|
+
throw e;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return origFetch.call(globalThis, input, init);
|
|
804
|
+
} as typeof fetch;
|
|
805
|
+
|
|
652
806
|
const cliCode = `
|
|
653
807
|
// Set environment for Convex CLI
|
|
654
808
|
process.env.CONVEX_DEPLOY_KEY = '${adminKey}';
|
|
@@ -656,35 +810,51 @@ export default app;
|
|
|
656
810
|
// Set CLI arguments
|
|
657
811
|
process.argv = ['node', 'convex', 'dev', '--once'];
|
|
658
812
|
|
|
659
|
-
//
|
|
813
|
+
// Load and execute the CLI bundle
|
|
660
814
|
require('./node_modules/convex/dist/cli.bundle.cjs');
|
|
661
815
|
`;
|
|
662
816
|
|
|
817
|
+
// Capture unhandled rejections from CLI async code (void main())
|
|
818
|
+
const rejectionHandler = (event: PromiseRejectionEvent) => {
|
|
819
|
+
const err = event.reason;
|
|
820
|
+
const msg = err?.message || String(err);
|
|
821
|
+
if (!msg.includes('Process exited with code')) {
|
|
822
|
+
log(`[CLI ASYNC ERROR] ${msg}`, 'error');
|
|
823
|
+
if (err?.stack) log(`[CLI ASYNC STACK] ${err.stack.substring(0, 500)}`, 'error');
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
globalThis.addEventListener('unhandledrejection', rejectionHandler);
|
|
827
|
+
|
|
663
828
|
try {
|
|
664
829
|
cliRuntime.execute(cliCode, '/project/cli-runner.js');
|
|
830
|
+
log('CLI synchronous execution completed (async work continues in background)');
|
|
665
831
|
} catch (cliError) {
|
|
666
832
|
// Some errors are expected (like process.exit or stack overflow in watcher)
|
|
667
833
|
// The important work (deployment) happens before these errors
|
|
668
834
|
log(`CLI completed with: ${(cliError as Error).message}`, 'warn');
|
|
669
835
|
}
|
|
670
836
|
|
|
671
|
-
// Wait for
|
|
672
|
-
// Poll for .env.local creation instead of fixed timeout
|
|
837
|
+
// Wait for CLI to finish: either process.exit fires (reliable) or fall back to polling
|
|
673
838
|
logStatus('WAITING', 'Waiting for deployment to complete...');
|
|
674
|
-
const deploymentSucceeded = await waitForDeployment(vfs, 30000, 500);
|
|
675
839
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
840
|
+
const timeoutPromise = new Promise<'timeout'>((resolve) =>
|
|
841
|
+
setTimeout(() => resolve('timeout'), 90000)
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
const result = await Promise.race([cliExitPromise, timeoutPromise]);
|
|
845
|
+
|
|
846
|
+
// Restore original fetch and remove rejection handler
|
|
847
|
+
globalThis.fetch = origFetch;
|
|
848
|
+
globalThis.removeEventListener('unhandledrejection', rejectionHandler);
|
|
849
|
+
|
|
850
|
+
if (result === 'timeout') {
|
|
851
|
+
log('CLI did not exit within 90s, falling back to file polling...', 'warn');
|
|
852
|
+
const envCreated = await waitForDeployment(vfs, 15000, 500);
|
|
853
|
+
if (envCreated) {
|
|
854
|
+
await waitForGenerated(vfs, 15000, 500);
|
|
687
855
|
}
|
|
856
|
+
} else {
|
|
857
|
+
log(`CLI exited with code ${result}, deployment complete`);
|
|
688
858
|
}
|
|
689
859
|
|
|
690
860
|
// Check if deployment succeeded by reading .env.local (CLI creates it in /project)
|
|
@@ -732,6 +902,29 @@ export default app;
|
|
|
732
902
|
log(' WARNING: _generated directory not created - functions may not be deployed!', 'error');
|
|
733
903
|
}
|
|
734
904
|
|
|
905
|
+
// Ensure /convex/_generated/api.ts always exists (fallback if CLI didn't generate it)
|
|
906
|
+
if (!vfs.existsSync('/convex/_generated/api.ts')) {
|
|
907
|
+
log(' Restoring fallback api.ts (CLI did not generate one)');
|
|
908
|
+
vfs.mkdirSync('/convex/_generated', { recursive: true });
|
|
909
|
+
vfs.writeFileSync('/convex/_generated/api.ts', `// Convex API - fallback for browser demo
|
|
910
|
+
export const api = {
|
|
911
|
+
todos: {
|
|
912
|
+
list: "todos:list",
|
|
913
|
+
create: "todos:create",
|
|
914
|
+
toggle: "todos:toggle",
|
|
915
|
+
remove: "todos:remove",
|
|
916
|
+
},
|
|
917
|
+
} as const;
|
|
918
|
+
`);
|
|
919
|
+
}
|
|
920
|
+
if (!vfs.existsSync('/convex/_generated/server.ts')) {
|
|
921
|
+
vfs.mkdirSync('/convex/_generated', { recursive: true });
|
|
922
|
+
vfs.writeFileSync('/convex/_generated/server.ts', `// Server stubs for browser demo
|
|
923
|
+
export function query(config) { return config; }
|
|
924
|
+
export function mutation(config) { return config; }
|
|
925
|
+
`);
|
|
926
|
+
}
|
|
927
|
+
|
|
735
928
|
// Parse the Convex URL from .env.local
|
|
736
929
|
const match = envContent.match(/CONVEX_URL=(.+)/);
|
|
737
930
|
if (match) {
|
|
@@ -756,6 +949,7 @@ export default app;
|
|
|
756
949
|
convexUrl = parsed.url;
|
|
757
950
|
log(`Using fallback URL: ${convexUrl}`, 'warn');
|
|
758
951
|
}
|
|
952
|
+
logStatus('COMPLETE', `Connected to ${convexUrl} (fallback)`);
|
|
759
953
|
}
|
|
760
954
|
|
|
761
955
|
// Set the env var on the dev server (idiomatic Next.js pattern)
|
|
@@ -183,11 +183,15 @@ const EXPLICIT_MAPPINGS: Record<string, string> = {
|
|
|
183
183
|
'react-dom/client': 'https://esm.sh/react-dom@18.2.0/client?dev',
|
|
184
184
|
};
|
|
185
185
|
|
|
186
|
-
// Packages that are local
|
|
186
|
+
// Packages that are local, have custom shims, or are handled by the HTML import map.
|
|
187
|
+
// These are NOT redirected to esm.sh by redirectNpmImports.
|
|
187
188
|
const LOCAL_PACKAGES = new Set([
|
|
188
189
|
'next/link', 'next/router', 'next/head', 'next/navigation',
|
|
189
190
|
'next/dynamic', 'next/image', 'next/script', 'next/font/google',
|
|
190
191
|
'next/font/local', 'convex/_generated/api',
|
|
192
|
+
// Convex subpath imports — resolved by the import map in generated HTML
|
|
193
|
+
// (keeps version-pinned URLs consistent with import map)
|
|
194
|
+
'convex/react', 'convex/server', 'convex/values',
|
|
191
195
|
]);
|
|
192
196
|
|
|
193
197
|
/** Check if a package specifier is a bare npm import that should be redirected. */
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js Config Parser
|
|
3
|
+
*
|
|
4
|
+
* Extracts configuration values from next.config.{ts,js,mjs} files
|
|
5
|
+
* using acorn AST parsing with regex fallback.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as acorn from 'acorn';
|
|
9
|
+
import { stripTypescriptSyntax } from './tailwind-config-loader';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse a string value from a Next.js config file.
|
|
13
|
+
*
|
|
14
|
+
* @param content - Raw file content of next.config.{ts,js,mjs}
|
|
15
|
+
* @param key - Config key to extract (e.g., 'assetPrefix', 'basePath')
|
|
16
|
+
* @param isTypeScript - Whether the content needs TypeScript syntax stripping
|
|
17
|
+
* @returns The string value, or null if not found
|
|
18
|
+
*/
|
|
19
|
+
export function parseNextConfigValue(
|
|
20
|
+
content: string,
|
|
21
|
+
key: string,
|
|
22
|
+
isTypeScript: boolean = false
|
|
23
|
+
): string | null {
|
|
24
|
+
const processed = isTypeScript ? stripTypescriptSyntax(content) : content;
|
|
25
|
+
try {
|
|
26
|
+
return parseNextConfigValueAst(processed, key);
|
|
27
|
+
} catch {
|
|
28
|
+
return parseNextConfigValueRegex(processed, key);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
|
+
type ASTNode = any;
|
|
34
|
+
|
|
35
|
+
function parseNextConfigValueAst(content: string, key: string): string | null {
|
|
36
|
+
const ast = acorn.parse(content, {
|
|
37
|
+
ecmaVersion: 'latest',
|
|
38
|
+
sourceType: 'module',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Collect top-level variable declarations: name -> init node
|
|
42
|
+
const variables = new Map<string, ASTNode>();
|
|
43
|
+
for (const node of (ast as ASTNode).body) {
|
|
44
|
+
if (node.type === 'VariableDeclaration') {
|
|
45
|
+
for (const decl of node.declarations) {
|
|
46
|
+
if (decl.id?.name && decl.init) {
|
|
47
|
+
variables.set(decl.id.name, decl.init);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Find the exported config object
|
|
54
|
+
let configObject: ASTNode = null;
|
|
55
|
+
|
|
56
|
+
for (const node of (ast as ASTNode).body) {
|
|
57
|
+
// export default { ... } or export default configVar
|
|
58
|
+
if (node.type === 'ExportDefaultDeclaration') {
|
|
59
|
+
configObject = resolveToObject(node.declaration, variables);
|
|
60
|
+
if (configObject) break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// module.exports = { ... } or module.exports = configVar
|
|
64
|
+
if (
|
|
65
|
+
node.type === 'ExpressionStatement' &&
|
|
66
|
+
node.expression.type === 'AssignmentExpression' &&
|
|
67
|
+
node.expression.left.type === 'MemberExpression' &&
|
|
68
|
+
node.expression.left.object?.name === 'module' &&
|
|
69
|
+
node.expression.left.property?.name === 'exports'
|
|
70
|
+
) {
|
|
71
|
+
configObject = resolveToObject(node.expression.right, variables);
|
|
72
|
+
if (configObject) break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!configObject || configObject.type !== 'ObjectExpression') {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Find the property matching key
|
|
81
|
+
for (const prop of configObject.properties) {
|
|
82
|
+
if (prop.type !== 'Property') continue;
|
|
83
|
+
|
|
84
|
+
const propName =
|
|
85
|
+
prop.key.type === 'Identifier'
|
|
86
|
+
? prop.key.name
|
|
87
|
+
: prop.key.type === 'Literal'
|
|
88
|
+
? String(prop.key.value)
|
|
89
|
+
: null;
|
|
90
|
+
|
|
91
|
+
if (propName !== key) continue;
|
|
92
|
+
|
|
93
|
+
return resolveToString(prop.value, variables);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Resolve a node to an ObjectExpression, following Identifiers and CallExpressions */
|
|
100
|
+
function resolveToObject(
|
|
101
|
+
node: ASTNode,
|
|
102
|
+
variables: Map<string, ASTNode>
|
|
103
|
+
): ASTNode | null {
|
|
104
|
+
if (!node) return null;
|
|
105
|
+
if (node.type === 'ObjectExpression') return node;
|
|
106
|
+
if (node.type === 'Identifier') {
|
|
107
|
+
const init = variables.get(node.name);
|
|
108
|
+
return init ? resolveToObject(init, variables) : null;
|
|
109
|
+
}
|
|
110
|
+
// Handle wrapper functions like defineConfig({ ... })
|
|
111
|
+
if (node.type === 'CallExpression' && node.arguments.length > 0) {
|
|
112
|
+
return resolveToObject(node.arguments[0], variables);
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Resolve a node to a string value, following Identifiers */
|
|
118
|
+
function resolveToString(
|
|
119
|
+
node: ASTNode,
|
|
120
|
+
variables: Map<string, ASTNode>
|
|
121
|
+
): string | null {
|
|
122
|
+
if (!node) return null;
|
|
123
|
+
if (node.type === 'Literal' && typeof node.value === 'string') {
|
|
124
|
+
return node.value;
|
|
125
|
+
}
|
|
126
|
+
if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
|
|
127
|
+
return node.quasis[0]?.value?.cooked ?? null;
|
|
128
|
+
}
|
|
129
|
+
if (node.type === 'Identifier') {
|
|
130
|
+
const init = variables.get(node.name);
|
|
131
|
+
return init ? resolveToString(init, variables) : null;
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseNextConfigValueRegex(content: string, key: string): string | null {
|
|
137
|
+
const regex = new RegExp(`${key}\\s*:\\s*["'\`]([^"'\`]+)["'\`]`);
|
|
138
|
+
const match = content.match(regex);
|
|
139
|
+
return match ? match[1] : null;
|
|
140
|
+
}
|