almostnode 0.2.4 → 0.2.6
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/LICENSE +1 -1
- package/dist/__sw__.js +32 -1
- package/dist/assets/{runtime-worker-D9x_Ddwz.js → runtime-worker-B8_LZkBX.js} +85 -32
- package/dist/assets/runtime-worker-B8_LZkBX.js.map +1 -0
- package/dist/frameworks/next-dev-server.d.ts +63 -0
- package/dist/frameworks/next-dev-server.d.ts.map +1 -1
- package/dist/frameworks/tailwind-config-loader.d.ts +32 -0
- package/dist/frameworks/tailwind-config-loader.d.ts.map +1 -0
- package/dist/frameworks/vite-dev-server.d.ts +1 -0
- package/dist/frameworks/vite-dev-server.d.ts.map +1 -1
- package/dist/index.cjs +995 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +975 -60
- package/dist/index.mjs.map +1 -1
- package/dist/macaly-demo.d.ts +42 -0
- package/dist/macaly-demo.d.ts.map +1 -0
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/types/package-json.d.ts +16 -0
- package/dist/types/package-json.d.ts.map +1 -0
- package/dist/utils/hash.d.ts +6 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/virtual-fs.d.ts +1 -0
- package/dist/virtual-fs.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/frameworks/next-dev-server.ts +940 -34
- package/src/frameworks/tailwind-config-loader.ts +206 -0
- package/src/frameworks/vite-dev-server.ts +25 -0
- package/src/macaly-demo.ts +172 -0
- package/src/runtime.ts +84 -25
- package/src/types/package-json.ts +15 -0
- package/src/utils/hash.ts +12 -0
- package/src/virtual-fs.ts +14 -10
- package/dist/assets/runtime-worker-D9x_Ddwz.js.map +0 -1
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import { DevServer, DevServerOptions, ResponseData, HMRUpdate } from '../dev-server';
|
|
7
7
|
import { VirtualFS } from '../virtual-fs';
|
|
8
8
|
import { Buffer } from '../shims/stream';
|
|
9
|
+
import { simpleHash } from '../utils/hash';
|
|
10
|
+
import { loadTailwindConfig } from './tailwind-config-loader';
|
|
9
11
|
|
|
10
12
|
// Check if we're in a real browser environment (not jsdom or Node.js)
|
|
11
13
|
const isBrowser = typeof window !== 'undefined' &&
|
|
@@ -76,6 +78,8 @@ export interface NextDevServerOptions extends DevServerOptions {
|
|
|
76
78
|
preferAppRouter?: boolean;
|
|
77
79
|
/** Environment variables (NEXT_PUBLIC_* are available in browser code via process.env) */
|
|
78
80
|
env?: Record<string, string>;
|
|
81
|
+
/** Asset prefix for static files (e.g., '/marketing'). Auto-detected from next.config if not specified. */
|
|
82
|
+
assetPrefix?: string;
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
/**
|
|
@@ -282,25 +286,31 @@ const applyVirtualBase = (url) => {
|
|
|
282
286
|
|
|
283
287
|
export default function Link({ href, children, ...props }) {
|
|
284
288
|
const handleClick = (e) => {
|
|
289
|
+
console.log('[Link] Click handler called, href:', href);
|
|
290
|
+
|
|
285
291
|
if (props.onClick) {
|
|
286
292
|
props.onClick(e);
|
|
287
293
|
}
|
|
288
294
|
|
|
289
295
|
// Allow cmd/ctrl click to open in new tab
|
|
290
296
|
if (e.metaKey || e.ctrlKey) {
|
|
297
|
+
console.log('[Link] Meta/Ctrl key pressed, allowing default behavior');
|
|
291
298
|
return;
|
|
292
299
|
}
|
|
293
300
|
|
|
294
301
|
if (typeof href !== 'string' || !href || href.startsWith('#') || href.startsWith('?')) {
|
|
302
|
+
console.log('[Link] Skipping navigation for href:', href);
|
|
295
303
|
return;
|
|
296
304
|
}
|
|
297
305
|
|
|
298
306
|
if (/^(https?:)?\\/\\//.test(href)) {
|
|
307
|
+
console.log('[Link] External URL, allowing default behavior:', href);
|
|
299
308
|
return;
|
|
300
309
|
}
|
|
301
310
|
|
|
302
311
|
e.preventDefault();
|
|
303
312
|
const resolvedHref = applyVirtualBase(href);
|
|
313
|
+
console.log('[Link] Navigating to:', resolvedHref);
|
|
304
314
|
window.history.pushState({}, '', resolvedHref);
|
|
305
315
|
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
306
316
|
};
|
|
@@ -669,6 +679,323 @@ export default function Head({ children }) {
|
|
|
669
679
|
}
|
|
670
680
|
`;
|
|
671
681
|
|
|
682
|
+
/**
|
|
683
|
+
* Next.js Image shim code
|
|
684
|
+
* Provides a simple img-based implementation of next/image
|
|
685
|
+
*/
|
|
686
|
+
const NEXT_IMAGE_SHIM = `
|
|
687
|
+
import React from 'react';
|
|
688
|
+
|
|
689
|
+
function Image({
|
|
690
|
+
src,
|
|
691
|
+
alt = '',
|
|
692
|
+
width,
|
|
693
|
+
height,
|
|
694
|
+
fill,
|
|
695
|
+
loader,
|
|
696
|
+
quality = 75,
|
|
697
|
+
priority,
|
|
698
|
+
loading,
|
|
699
|
+
placeholder,
|
|
700
|
+
blurDataURL,
|
|
701
|
+
unoptimized,
|
|
702
|
+
onLoad,
|
|
703
|
+
onError,
|
|
704
|
+
style,
|
|
705
|
+
className,
|
|
706
|
+
sizes,
|
|
707
|
+
...rest
|
|
708
|
+
}) {
|
|
709
|
+
// Handle src - could be string or StaticImageData object
|
|
710
|
+
const imageSrc = typeof src === 'object' ? src.src : src;
|
|
711
|
+
|
|
712
|
+
// Build style object
|
|
713
|
+
const imgStyle = { ...style };
|
|
714
|
+
if (fill) {
|
|
715
|
+
imgStyle.position = 'absolute';
|
|
716
|
+
imgStyle.width = '100%';
|
|
717
|
+
imgStyle.height = '100%';
|
|
718
|
+
imgStyle.objectFit = imgStyle.objectFit || 'cover';
|
|
719
|
+
imgStyle.inset = '0';
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return React.createElement('img', {
|
|
723
|
+
src: imageSrc,
|
|
724
|
+
alt,
|
|
725
|
+
width: fill ? undefined : width,
|
|
726
|
+
height: fill ? undefined : height,
|
|
727
|
+
loading: priority ? 'eager' : (loading || 'lazy'),
|
|
728
|
+
decoding: 'async',
|
|
729
|
+
style: imgStyle,
|
|
730
|
+
className,
|
|
731
|
+
onLoad,
|
|
732
|
+
onError,
|
|
733
|
+
...rest
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
export default Image;
|
|
738
|
+
export { Image };
|
|
739
|
+
`;
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* next/dynamic shim - Dynamic imports with loading states
|
|
743
|
+
*/
|
|
744
|
+
const NEXT_DYNAMIC_SHIM = `
|
|
745
|
+
import React from 'react';
|
|
746
|
+
|
|
747
|
+
function dynamic(importFn, options = {}) {
|
|
748
|
+
const {
|
|
749
|
+
loading: LoadingComponent,
|
|
750
|
+
ssr = true,
|
|
751
|
+
} = options;
|
|
752
|
+
|
|
753
|
+
// Create a lazy component
|
|
754
|
+
const LazyComponent = React.lazy(importFn);
|
|
755
|
+
|
|
756
|
+
// Wrapper component that handles loading state
|
|
757
|
+
function DynamicComponent(props) {
|
|
758
|
+
const fallback = LoadingComponent
|
|
759
|
+
? React.createElement(LoadingComponent, { isLoading: true })
|
|
760
|
+
: null;
|
|
761
|
+
|
|
762
|
+
return React.createElement(
|
|
763
|
+
React.Suspense,
|
|
764
|
+
{ fallback },
|
|
765
|
+
React.createElement(LazyComponent, props)
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
return DynamicComponent;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export default dynamic;
|
|
773
|
+
export { dynamic };
|
|
774
|
+
`;
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* next/script shim - Loads external scripts
|
|
778
|
+
*/
|
|
779
|
+
const NEXT_SCRIPT_SHIM = `
|
|
780
|
+
import React from 'react';
|
|
781
|
+
|
|
782
|
+
function Script({
|
|
783
|
+
src,
|
|
784
|
+
strategy = 'afterInteractive',
|
|
785
|
+
onLoad,
|
|
786
|
+
onReady,
|
|
787
|
+
onError,
|
|
788
|
+
children,
|
|
789
|
+
dangerouslySetInnerHTML,
|
|
790
|
+
...rest
|
|
791
|
+
}) {
|
|
792
|
+
React.useEffect(function() {
|
|
793
|
+
if (!src && !children && !dangerouslySetInnerHTML) return;
|
|
794
|
+
|
|
795
|
+
var script = document.createElement('script');
|
|
796
|
+
|
|
797
|
+
if (src) {
|
|
798
|
+
script.src = src;
|
|
799
|
+
script.async = strategy !== 'beforeInteractive';
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
Object.keys(rest).forEach(function(key) {
|
|
803
|
+
script.setAttribute(key, rest[key]);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
if (children) {
|
|
807
|
+
script.textContent = children;
|
|
808
|
+
} else if (dangerouslySetInnerHTML && dangerouslySetInnerHTML.__html) {
|
|
809
|
+
script.textContent = dangerouslySetInnerHTML.__html;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
script.onload = function() {
|
|
813
|
+
if (onLoad) onLoad();
|
|
814
|
+
if (onReady) onReady();
|
|
815
|
+
};
|
|
816
|
+
script.onerror = onError;
|
|
817
|
+
|
|
818
|
+
document.head.appendChild(script);
|
|
819
|
+
|
|
820
|
+
return function() {
|
|
821
|
+
if (script.parentNode) {
|
|
822
|
+
script.parentNode.removeChild(script);
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
}, [src]);
|
|
826
|
+
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
export default Script;
|
|
831
|
+
export { Script };
|
|
832
|
+
`;
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* next/font/google shim - Loads Google Fonts via CDN
|
|
836
|
+
* Uses a Proxy to dynamically handle ANY Google Font without hardcoding
|
|
837
|
+
*/
|
|
838
|
+
const NEXT_FONT_GOOGLE_SHIM = `
|
|
839
|
+
// Track loaded fonts to avoid duplicate style injections
|
|
840
|
+
const loadedFonts = new Set();
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Convert font function name to Google Fonts family name
|
|
844
|
+
* Examples:
|
|
845
|
+
* DM_Sans -> DM Sans
|
|
846
|
+
* Open_Sans -> Open Sans
|
|
847
|
+
* Fraunces -> Fraunces
|
|
848
|
+
*/
|
|
849
|
+
function toFontFamily(fontName) {
|
|
850
|
+
return fontName.replace(/_/g, ' ');
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Inject font CSS into document
|
|
855
|
+
* - Adds preconnect links for faster font loading
|
|
856
|
+
* - Loads the font from Google Fonts CDN
|
|
857
|
+
* - Creates a CSS class that sets the CSS variable
|
|
858
|
+
*/
|
|
859
|
+
function injectFontCSS(fontFamily, variableName, weight, style) {
|
|
860
|
+
const fontKey = fontFamily + '-' + (variableName || 'default');
|
|
861
|
+
if (loadedFonts.has(fontKey)) {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
loadedFonts.add(fontKey);
|
|
865
|
+
|
|
866
|
+
if (typeof document === 'undefined') {
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Add preconnect links for faster loading (only once)
|
|
871
|
+
if (!document.querySelector('link[href="https://fonts.googleapis.com"]')) {
|
|
872
|
+
const preconnect1 = document.createElement('link');
|
|
873
|
+
preconnect1.rel = 'preconnect';
|
|
874
|
+
preconnect1.href = 'https://fonts.googleapis.com';
|
|
875
|
+
document.head.appendChild(preconnect1);
|
|
876
|
+
|
|
877
|
+
const preconnect2 = document.createElement('link');
|
|
878
|
+
preconnect2.rel = 'preconnect';
|
|
879
|
+
preconnect2.href = 'https://fonts.gstatic.com';
|
|
880
|
+
preconnect2.crossOrigin = 'anonymous';
|
|
881
|
+
document.head.appendChild(preconnect2);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Build Google Fonts URL
|
|
885
|
+
const escapedFamily = fontFamily.replace(/ /g, '+');
|
|
886
|
+
|
|
887
|
+
// Build axis list based on options
|
|
888
|
+
let axisList = '';
|
|
889
|
+
const axes = [];
|
|
890
|
+
|
|
891
|
+
// Handle italic style
|
|
892
|
+
if (style === 'italic') {
|
|
893
|
+
axes.push('ital');
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Handle weight - use specific weight or variable range
|
|
897
|
+
if (weight && weight !== '400' && !Array.isArray(weight)) {
|
|
898
|
+
// Specific weight requested
|
|
899
|
+
axes.push('wght');
|
|
900
|
+
if (style === 'italic') {
|
|
901
|
+
axisList = ':ital,wght@1,' + weight;
|
|
902
|
+
} else {
|
|
903
|
+
axisList = ':wght@' + weight;
|
|
904
|
+
}
|
|
905
|
+
} else if (Array.isArray(weight)) {
|
|
906
|
+
// Multiple weights
|
|
907
|
+
axes.push('wght');
|
|
908
|
+
axisList = ':wght@' + weight.join(';');
|
|
909
|
+
} else {
|
|
910
|
+
// Default: request common weights for flexibility
|
|
911
|
+
axisList = ':wght@400;500;600;700';
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const fontUrl = 'https://fonts.googleapis.com/css2?family=' +
|
|
915
|
+
escapedFamily + axisList + '&display=swap';
|
|
916
|
+
|
|
917
|
+
// Add link element for Google Fonts (if not already present)
|
|
918
|
+
if (!document.querySelector('link[href*="family=' + escapedFamily + '"]')) {
|
|
919
|
+
const link = document.createElement('link');
|
|
920
|
+
link.rel = 'stylesheet';
|
|
921
|
+
link.href = fontUrl;
|
|
922
|
+
document.head.appendChild(link);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Create style element for CSS variable at :root level (globally available)
|
|
926
|
+
// This makes the variable work without needing to apply the class to body
|
|
927
|
+
if (variableName) {
|
|
928
|
+
const styleEl = document.createElement('style');
|
|
929
|
+
styleEl.setAttribute('data-font-var', variableName);
|
|
930
|
+
styleEl.textContent = ':root { ' + variableName + ': "' + fontFamily + '", ' + (fontFamily.includes('Serif') ? 'serif' : 'sans-serif') + '; }';
|
|
931
|
+
document.head.appendChild(styleEl);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Create a font loader function for a specific font
|
|
937
|
+
*/
|
|
938
|
+
function createFontLoader(fontName) {
|
|
939
|
+
const fontFamily = toFontFamily(fontName);
|
|
940
|
+
|
|
941
|
+
return function(options = {}) {
|
|
942
|
+
const {
|
|
943
|
+
weight,
|
|
944
|
+
style = 'normal',
|
|
945
|
+
subsets = ['latin'],
|
|
946
|
+
variable,
|
|
947
|
+
display = 'swap',
|
|
948
|
+
preload = true,
|
|
949
|
+
fallback = ['sans-serif'],
|
|
950
|
+
adjustFontFallback = true
|
|
951
|
+
} = options;
|
|
952
|
+
|
|
953
|
+
// Inject the font CSS
|
|
954
|
+
injectFontCSS(fontFamily, variable, weight, style);
|
|
955
|
+
|
|
956
|
+
// Generate class name from variable (--font-inter -> __font-inter)
|
|
957
|
+
const className = variable
|
|
958
|
+
? variable.replace('--', '__')
|
|
959
|
+
: '__font-' + fontName.toLowerCase().replace(/_/g, '-');
|
|
960
|
+
|
|
961
|
+
return {
|
|
962
|
+
className,
|
|
963
|
+
variable: className,
|
|
964
|
+
style: {
|
|
965
|
+
fontFamily: '"' + fontFamily + '", ' + fallback.join(', ')
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Use a Proxy to dynamically create font loaders for ANY font name
|
|
973
|
+
* This allows: import { AnyGoogleFont } from "next/font/google"
|
|
974
|
+
*/
|
|
975
|
+
const fontProxy = new Proxy({}, {
|
|
976
|
+
get(target, prop) {
|
|
977
|
+
// Handle special properties
|
|
978
|
+
if (prop === '__esModule') return true;
|
|
979
|
+
if (prop === 'default') return fontProxy;
|
|
980
|
+
if (typeof prop !== 'string') return undefined;
|
|
981
|
+
|
|
982
|
+
// Create a font loader for this font name
|
|
983
|
+
return createFontLoader(prop);
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
// Export the proxy as both default and named exports
|
|
988
|
+
export default fontProxy;
|
|
989
|
+
|
|
990
|
+
// Re-export through proxy for named imports
|
|
991
|
+
export const {
|
|
992
|
+
Fraunces, Inter, DM_Sans, DM_Serif_Text, Roboto, Open_Sans, Lato,
|
|
993
|
+
Montserrat, Poppins, Playfair_Display, Merriweather, Raleway, Nunito,
|
|
994
|
+
Ubuntu, Oswald, Quicksand, Work_Sans, Fira_Sans, Barlow, Mulish, Rubik,
|
|
995
|
+
Noto_Sans, Manrope, Space_Grotesk, Geist, Geist_Mono
|
|
996
|
+
} = fontProxy;
|
|
997
|
+
`;
|
|
998
|
+
|
|
672
999
|
/**
|
|
673
1000
|
* NextDevServer - A lightweight Next.js-compatible development server
|
|
674
1001
|
*
|
|
@@ -714,6 +1041,21 @@ export class NextDevServer extends DevServer {
|
|
|
714
1041
|
/** Store options for later access (e.g., env vars) */
|
|
715
1042
|
private options: NextDevServerOptions;
|
|
716
1043
|
|
|
1044
|
+
/** Transform result cache for performance */
|
|
1045
|
+
private transformCache: Map<string, { code: string; hash: string }> = new Map();
|
|
1046
|
+
|
|
1047
|
+
/** Path aliases from tsconfig.json (e.g., @/* -> ./*) */
|
|
1048
|
+
private pathAliases: Map<string, string> = new Map();
|
|
1049
|
+
|
|
1050
|
+
/** Cached Tailwind config script (injected before CDN) */
|
|
1051
|
+
private tailwindConfigScript: string = '';
|
|
1052
|
+
|
|
1053
|
+
/** Whether Tailwind config has been loaded */
|
|
1054
|
+
private tailwindConfigLoaded: boolean = false;
|
|
1055
|
+
|
|
1056
|
+
/** Asset prefix for static files (e.g., '/marketing') */
|
|
1057
|
+
private assetPrefix: string = '';
|
|
1058
|
+
|
|
717
1059
|
constructor(vfs: VirtualFS, options: NextDevServerOptions) {
|
|
718
1060
|
super(vfs, options);
|
|
719
1061
|
this.options = options;
|
|
@@ -729,6 +1071,131 @@ export class NextDevServer extends DevServer {
|
|
|
729
1071
|
// Prefer App Router if /app directory exists with a page.jsx file
|
|
730
1072
|
this.useAppRouter = this.hasAppRouter();
|
|
731
1073
|
}
|
|
1074
|
+
|
|
1075
|
+
// Load path aliases from tsconfig.json
|
|
1076
|
+
this.loadPathAliases();
|
|
1077
|
+
|
|
1078
|
+
// Load assetPrefix from options or auto-detect from next.config
|
|
1079
|
+
this.loadAssetPrefix(options.assetPrefix);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* Load path aliases from tsconfig.json
|
|
1084
|
+
* Supports common patterns like @/* -> ./*
|
|
1085
|
+
*/
|
|
1086
|
+
private loadPathAliases(): void {
|
|
1087
|
+
try {
|
|
1088
|
+
const tsconfigPath = '/tsconfig.json';
|
|
1089
|
+
if (!this.vfs.existsSync(tsconfigPath)) {
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const content = this.vfs.readFileSync(tsconfigPath, 'utf-8');
|
|
1094
|
+
const tsconfig = JSON.parse(content);
|
|
1095
|
+
const paths = tsconfig?.compilerOptions?.paths;
|
|
1096
|
+
|
|
1097
|
+
if (!paths) {
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Convert tsconfig paths to a simple alias map
|
|
1102
|
+
// e.g., "@/*": ["./*"] becomes "@/" -> "/"
|
|
1103
|
+
for (const [alias, targets] of Object.entries(paths)) {
|
|
1104
|
+
if (Array.isArray(targets) && targets.length > 0) {
|
|
1105
|
+
// Remove trailing * from alias and target
|
|
1106
|
+
const aliasPrefix = alias.replace(/\*$/, '');
|
|
1107
|
+
const targetPrefix = (targets[0] as string).replace(/\*$/, '').replace(/^\./, '');
|
|
1108
|
+
this.pathAliases.set(aliasPrefix, targetPrefix);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
} catch (e) {
|
|
1112
|
+
// Silently ignore tsconfig parse errors
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Load assetPrefix from options or auto-detect from next.config.ts/js
|
|
1118
|
+
* The assetPrefix is used to prefix static asset URLs (e.g., '/marketing')
|
|
1119
|
+
*/
|
|
1120
|
+
private loadAssetPrefix(optionValue?: string): void {
|
|
1121
|
+
// If explicitly provided in options, use it
|
|
1122
|
+
if (optionValue !== undefined) {
|
|
1123
|
+
// Normalize: ensure it starts with / and doesn't end with /
|
|
1124
|
+
this.assetPrefix = optionValue.startsWith('/') ? optionValue : `/${optionValue}`;
|
|
1125
|
+
if (this.assetPrefix.endsWith('/')) {
|
|
1126
|
+
this.assetPrefix = this.assetPrefix.slice(0, -1);
|
|
1127
|
+
}
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// Try to auto-detect from next.config.ts or next.config.js
|
|
1132
|
+
try {
|
|
1133
|
+
const configFiles = ['/next.config.ts', '/next.config.js', '/next.config.mjs'];
|
|
1134
|
+
|
|
1135
|
+
for (const configPath of configFiles) {
|
|
1136
|
+
if (!this.vfs.existsSync(configPath)) {
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const content = this.vfs.readFileSync(configPath, 'utf-8');
|
|
1141
|
+
|
|
1142
|
+
// Extract assetPrefix from config using regex
|
|
1143
|
+
// Matches: assetPrefix: "/marketing" or assetPrefix: '/marketing'
|
|
1144
|
+
const match = content.match(/assetPrefix\s*:\s*["']([^"']+)["']/);
|
|
1145
|
+
if (match) {
|
|
1146
|
+
let prefix = match[1];
|
|
1147
|
+
// Normalize: ensure it starts with / and doesn't end with /
|
|
1148
|
+
if (!prefix.startsWith('/')) {
|
|
1149
|
+
prefix = `/${prefix}`;
|
|
1150
|
+
}
|
|
1151
|
+
if (prefix.endsWith('/')) {
|
|
1152
|
+
prefix = prefix.slice(0, -1);
|
|
1153
|
+
}
|
|
1154
|
+
this.assetPrefix = prefix;
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
} catch (e) {
|
|
1159
|
+
// Silently ignore config parse errors
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Resolve path aliases in transformed code
|
|
1165
|
+
* Converts imports like "@/components/foo" to "/__virtual__/PORT/components/foo"
|
|
1166
|
+
* This ensures imports go through the virtual server instead of the main server
|
|
1167
|
+
*/
|
|
1168
|
+
private resolvePathAliases(code: string, currentFile: string): string {
|
|
1169
|
+
if (this.pathAliases.size === 0) {
|
|
1170
|
+
return code;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// Get the virtual server base path
|
|
1174
|
+
const virtualBase = `/__virtual__/${this.port}`;
|
|
1175
|
+
|
|
1176
|
+
let result = code;
|
|
1177
|
+
|
|
1178
|
+
for (const [alias, target] of this.pathAliases) {
|
|
1179
|
+
// Match import/export statements with the alias
|
|
1180
|
+
// Handles: import ... from "@/...", export ... from "@/...", import("@/...")
|
|
1181
|
+
const aliasEscaped = alias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1182
|
+
|
|
1183
|
+
// Pattern to match the alias in import/export statements
|
|
1184
|
+
// This matches: from "@/...", from '@/...', import("@/..."), import('@/...')
|
|
1185
|
+
const pattern = new RegExp(
|
|
1186
|
+
`(from\\s*['"]|import\\s*\\(\\s*['"])${aliasEscaped}([^'"]+)(['"])`,
|
|
1187
|
+
'g'
|
|
1188
|
+
);
|
|
1189
|
+
|
|
1190
|
+
result = result.replace(pattern, (match, prefix, path, quote) => {
|
|
1191
|
+
// Convert alias to virtual server path
|
|
1192
|
+
// e.g., @/components/faq -> /__virtual__/3001/components/faq
|
|
1193
|
+
const resolvedPath = `${virtualBase}${target}${path}`;
|
|
1194
|
+
return `${prefix}${resolvedPath}${quote}`;
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
return result;
|
|
732
1199
|
}
|
|
733
1200
|
|
|
734
1201
|
/**
|
|
@@ -758,28 +1225,58 @@ export class NextDevServer extends DevServer {
|
|
|
758
1225
|
/**
|
|
759
1226
|
* Generate a script tag that defines process.env with NEXT_PUBLIC_* variables
|
|
760
1227
|
* This makes environment variables available to browser code via process.env.NEXT_PUBLIC_*
|
|
1228
|
+
* Also includes all env variables for Server Component compatibility
|
|
761
1229
|
*/
|
|
762
1230
|
private generateEnvScript(): string {
|
|
763
1231
|
const env = this.options.env || {};
|
|
764
1232
|
|
|
765
|
-
//
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
return '';
|
|
1233
|
+
// Only include NEXT_PUBLIC_* vars in the HTML (client-side accessible)
|
|
1234
|
+
// Non-public vars should never be exposed in HTML for security
|
|
1235
|
+
const publicEnvVars: Record<string, string> = {};
|
|
1236
|
+
for (const [key, value] of Object.entries(env)) {
|
|
1237
|
+
if (key.startsWith('NEXT_PUBLIC_')) {
|
|
1238
|
+
publicEnvVars[key] = value;
|
|
1239
|
+
}
|
|
773
1240
|
}
|
|
774
1241
|
|
|
1242
|
+
// Always create process.env even if empty (some code checks for process.env existence)
|
|
1243
|
+
// This prevents "process is not defined" errors
|
|
775
1244
|
return `<script>
|
|
776
|
-
//
|
|
1245
|
+
// Environment variables (injected by NextDevServer)
|
|
777
1246
|
window.process = window.process || {};
|
|
778
1247
|
window.process.env = window.process.env || {};
|
|
779
1248
|
Object.assign(window.process.env, ${JSON.stringify(publicEnvVars)});
|
|
780
1249
|
</script>`;
|
|
781
1250
|
}
|
|
782
1251
|
|
|
1252
|
+
/**
|
|
1253
|
+
* Load Tailwind config from tailwind.config.ts and generate a script
|
|
1254
|
+
* that configures the Tailwind CDN at runtime
|
|
1255
|
+
*/
|
|
1256
|
+
private async loadTailwindConfigIfNeeded(): Promise<string> {
|
|
1257
|
+
// Return cached script if already loaded
|
|
1258
|
+
if (this.tailwindConfigLoaded) {
|
|
1259
|
+
return this.tailwindConfigScript;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
try {
|
|
1263
|
+
const result = await loadTailwindConfig(this.vfs, this.root);
|
|
1264
|
+
|
|
1265
|
+
if (result.success) {
|
|
1266
|
+
this.tailwindConfigScript = result.configScript;
|
|
1267
|
+
} else if (result.error) {
|
|
1268
|
+
console.warn('[NextDevServer] Tailwind config warning:', result.error);
|
|
1269
|
+
this.tailwindConfigScript = '';
|
|
1270
|
+
}
|
|
1271
|
+
} catch (error) {
|
|
1272
|
+
console.warn('[NextDevServer] Failed to load tailwind.config:', error);
|
|
1273
|
+
this.tailwindConfigScript = '';
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
this.tailwindConfigLoaded = true;
|
|
1277
|
+
return this.tailwindConfigScript;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
783
1280
|
/**
|
|
784
1281
|
* Check if App Router is available
|
|
785
1282
|
*/
|
|
@@ -809,13 +1306,39 @@ export class NextDevServer extends DevServer {
|
|
|
809
1306
|
body?: Buffer
|
|
810
1307
|
): Promise<ResponseData> {
|
|
811
1308
|
const urlObj = new URL(url, 'http://localhost');
|
|
812
|
-
|
|
1309
|
+
let pathname = urlObj.pathname;
|
|
1310
|
+
|
|
1311
|
+
// Strip virtual prefix if present (e.g., /__virtual__/3001/foo -> /foo)
|
|
1312
|
+
const virtualPrefixMatch = pathname.match(/^\/__virtual__\/\d+/);
|
|
1313
|
+
if (virtualPrefixMatch) {
|
|
1314
|
+
pathname = pathname.slice(virtualPrefixMatch[0].length) || '/';
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// Strip assetPrefix if present (e.g., /marketing/images/foo.png -> /images/foo.png)
|
|
1318
|
+
// This allows static assets to be served from /public when using assetPrefix in next.config
|
|
1319
|
+
// Also handles double-slash case: /marketing//images/foo.png (when assetPrefix ends with /)
|
|
1320
|
+
if (this.assetPrefix && pathname.startsWith(this.assetPrefix)) {
|
|
1321
|
+
const rest = pathname.slice(this.assetPrefix.length);
|
|
1322
|
+
// Handle both /marketing/images and /marketing//images cases
|
|
1323
|
+
if (rest === '' || rest.startsWith('/')) {
|
|
1324
|
+
pathname = rest || '/';
|
|
1325
|
+
// Normalize double slashes that may occur from assetPrefix concatenation
|
|
1326
|
+
if (pathname.startsWith('//')) {
|
|
1327
|
+
pathname = pathname.slice(1);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
813
1331
|
|
|
814
1332
|
// Serve Next.js shims
|
|
815
1333
|
if (pathname.startsWith('/_next/shims/')) {
|
|
816
1334
|
return this.serveNextShim(pathname);
|
|
817
1335
|
}
|
|
818
1336
|
|
|
1337
|
+
// Route info endpoint for client-side navigation params extraction
|
|
1338
|
+
if (pathname === '/_next/route-info') {
|
|
1339
|
+
return this.serveRouteInfo(urlObj.searchParams.get('pathname') || '/');
|
|
1340
|
+
}
|
|
1341
|
+
|
|
819
1342
|
// Serve page components for client-side navigation (Pages Router)
|
|
820
1343
|
if (pathname.startsWith('/_next/pages/')) {
|
|
821
1344
|
return this.servePageComponent(pathname);
|
|
@@ -847,6 +1370,16 @@ export class NextDevServer extends DevServer {
|
|
|
847
1370
|
return this.transformAndServe(pathname, pathname);
|
|
848
1371
|
}
|
|
849
1372
|
|
|
1373
|
+
// Try to resolve file with different extensions (for imports without extensions)
|
|
1374
|
+
// e.g., /components/faq -> /components/faq.tsx
|
|
1375
|
+
const resolvedFile = this.resolveFileWithExtension(pathname);
|
|
1376
|
+
if (resolvedFile) {
|
|
1377
|
+
if (this.needsTransform(resolvedFile)) {
|
|
1378
|
+
return this.transformAndServe(resolvedFile, pathname);
|
|
1379
|
+
}
|
|
1380
|
+
return this.serveFile(resolvedFile);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
850
1383
|
// Serve regular files directly if they exist
|
|
851
1384
|
if (this.exists(pathname) && !this.isDirectory(pathname)) {
|
|
852
1385
|
return this.serveFile(pathname);
|
|
@@ -876,6 +1409,18 @@ export class NextDevServer extends DevServer {
|
|
|
876
1409
|
case 'navigation':
|
|
877
1410
|
code = NEXT_NAVIGATION_SHIM;
|
|
878
1411
|
break;
|
|
1412
|
+
case 'image':
|
|
1413
|
+
code = NEXT_IMAGE_SHIM;
|
|
1414
|
+
break;
|
|
1415
|
+
case 'dynamic':
|
|
1416
|
+
code = NEXT_DYNAMIC_SHIM;
|
|
1417
|
+
break;
|
|
1418
|
+
case 'script':
|
|
1419
|
+
code = NEXT_SCRIPT_SHIM;
|
|
1420
|
+
break;
|
|
1421
|
+
case 'font/google':
|
|
1422
|
+
code = NEXT_FONT_GOOGLE_SHIM;
|
|
1423
|
+
break;
|
|
879
1424
|
default:
|
|
880
1425
|
return this.notFound(pathname);
|
|
881
1426
|
}
|
|
@@ -893,6 +1438,32 @@ export class NextDevServer extends DevServer {
|
|
|
893
1438
|
};
|
|
894
1439
|
}
|
|
895
1440
|
|
|
1441
|
+
/**
|
|
1442
|
+
* Serve route info for client-side navigation
|
|
1443
|
+
* Returns params extracted from dynamic route segments
|
|
1444
|
+
*/
|
|
1445
|
+
private serveRouteInfo(pathname: string): ResponseData {
|
|
1446
|
+
const route = this.resolveAppRoute(pathname);
|
|
1447
|
+
|
|
1448
|
+
const info = route
|
|
1449
|
+
? { params: route.params, found: true }
|
|
1450
|
+
: { params: {}, found: false };
|
|
1451
|
+
|
|
1452
|
+
const json = JSON.stringify(info);
|
|
1453
|
+
const buffer = Buffer.from(json);
|
|
1454
|
+
|
|
1455
|
+
return {
|
|
1456
|
+
statusCode: 200,
|
|
1457
|
+
statusMessage: 'OK',
|
|
1458
|
+
headers: {
|
|
1459
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
1460
|
+
'Content-Length': String(buffer.length),
|
|
1461
|
+
'Cache-Control': 'no-cache',
|
|
1462
|
+
},
|
|
1463
|
+
body: buffer,
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
|
|
896
1467
|
/**
|
|
897
1468
|
* Serve static assets from /_next/static/
|
|
898
1469
|
*/
|
|
@@ -1506,7 +2077,7 @@ export class NextDevServer extends DevServer {
|
|
|
1506
2077
|
/**
|
|
1507
2078
|
* Resolve App Router route to page and layout files
|
|
1508
2079
|
*/
|
|
1509
|
-
private resolveAppRoute(pathname: string): { page: string; layouts: string[] } | null {
|
|
2080
|
+
private resolveAppRoute(pathname: string): { page: string; layouts: string[]; params: Record<string, string | string[]> } | null {
|
|
1510
2081
|
const extensions = ['.jsx', '.tsx', '.js', '.ts'];
|
|
1511
2082
|
const segments = pathname === '/' ? [] : pathname.split('/').filter(Boolean);
|
|
1512
2083
|
|
|
@@ -1541,7 +2112,8 @@ export class NextDevServer extends DevServer {
|
|
|
1541
2112
|
for (const ext of extensions) {
|
|
1542
2113
|
const pagePath = `${dirPath}/page${ext}`;
|
|
1543
2114
|
if (this.exists(pagePath)) {
|
|
1544
|
-
|
|
2115
|
+
// Static route - no params
|
|
2116
|
+
return { page: pagePath, layouts, params: {} };
|
|
1545
2117
|
}
|
|
1546
2118
|
}
|
|
1547
2119
|
|
|
@@ -1551,18 +2123,20 @@ export class NextDevServer extends DevServer {
|
|
|
1551
2123
|
|
|
1552
2124
|
/**
|
|
1553
2125
|
* Resolve dynamic App Router routes like /app/[id]/page.jsx
|
|
2126
|
+
* Also extracts route params from dynamic segments
|
|
1554
2127
|
*/
|
|
1555
2128
|
private resolveAppDynamicRoute(
|
|
1556
2129
|
pathname: string,
|
|
1557
2130
|
segments: string[]
|
|
1558
|
-
): { page: string; layouts: string[] } | null {
|
|
2131
|
+
): { page: string; layouts: string[]; params: Record<string, string | string[]> } | null {
|
|
1559
2132
|
const extensions = ['.jsx', '.tsx', '.js', '.ts'];
|
|
1560
2133
|
|
|
1561
2134
|
const tryPath = (
|
|
1562
2135
|
dirPath: string,
|
|
1563
2136
|
remainingSegments: string[],
|
|
1564
|
-
layouts: string[]
|
|
1565
|
-
|
|
2137
|
+
layouts: string[],
|
|
2138
|
+
params: Record<string, string | string[]>
|
|
2139
|
+
): { page: string; layouts: string[]; params: Record<string, string | string[]> } | null => {
|
|
1566
2140
|
// Check for layout at current level
|
|
1567
2141
|
for (const ext of extensions) {
|
|
1568
2142
|
const layoutPath = `${dirPath}/layout${ext}`;
|
|
@@ -1576,7 +2150,7 @@ export class NextDevServer extends DevServer {
|
|
|
1576
2150
|
for (const ext of extensions) {
|
|
1577
2151
|
const pagePath = `${dirPath}/page${ext}`;
|
|
1578
2152
|
if (this.exists(pagePath)) {
|
|
1579
|
-
return { page: pagePath, layouts };
|
|
2153
|
+
return { page: pagePath, layouts, params };
|
|
1580
2154
|
}
|
|
1581
2155
|
}
|
|
1582
2156
|
return null;
|
|
@@ -1587,18 +2161,34 @@ export class NextDevServer extends DevServer {
|
|
|
1587
2161
|
// Try exact match first
|
|
1588
2162
|
const exactPath = `${dirPath}/${current}`;
|
|
1589
2163
|
if (this.isDirectory(exactPath)) {
|
|
1590
|
-
const result = tryPath(exactPath, rest, layouts);
|
|
2164
|
+
const result = tryPath(exactPath, rest, layouts, params);
|
|
1591
2165
|
if (result) return result;
|
|
1592
2166
|
}
|
|
1593
2167
|
|
|
1594
|
-
// Try dynamic
|
|
2168
|
+
// Try dynamic segments
|
|
1595
2169
|
try {
|
|
1596
2170
|
const entries = this.vfs.readdirSync(dirPath);
|
|
1597
2171
|
for (const entry of entries) {
|
|
1598
|
-
|
|
2172
|
+
// Handle catch-all routes [...slug]
|
|
2173
|
+
if (entry.startsWith('[...') && entry.endsWith(']')) {
|
|
2174
|
+
const dynamicPath = `${dirPath}/${entry}`;
|
|
2175
|
+
if (this.isDirectory(dynamicPath)) {
|
|
2176
|
+
// Extract param name from [...slug]
|
|
2177
|
+
const paramName = entry.slice(4, -1);
|
|
2178
|
+
// Catch-all captures all remaining segments
|
|
2179
|
+
const newParams = { ...params, [paramName]: [current, ...rest] };
|
|
2180
|
+
const result = tryPath(dynamicPath, [], layouts, newParams);
|
|
2181
|
+
if (result) return result;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
// Handle single dynamic segment [param]
|
|
2185
|
+
else if (entry.startsWith('[') && entry.endsWith(']') && !entry.includes('.')) {
|
|
1599
2186
|
const dynamicPath = `${dirPath}/${entry}`;
|
|
1600
2187
|
if (this.isDirectory(dynamicPath)) {
|
|
1601
|
-
|
|
2188
|
+
// Extract param name from [id]
|
|
2189
|
+
const paramName = entry.slice(1, -1);
|
|
2190
|
+
const newParams = { ...params, [paramName]: current };
|
|
2191
|
+
const result = tryPath(dynamicPath, rest, layouts, newParams);
|
|
1602
2192
|
if (result) return result;
|
|
1603
2193
|
}
|
|
1604
2194
|
}
|
|
@@ -1620,14 +2210,14 @@ export class NextDevServer extends DevServer {
|
|
|
1620
2210
|
}
|
|
1621
2211
|
}
|
|
1622
2212
|
|
|
1623
|
-
return tryPath(this.appDir, segments, layouts);
|
|
2213
|
+
return tryPath(this.appDir, segments, layouts, {});
|
|
1624
2214
|
}
|
|
1625
2215
|
|
|
1626
2216
|
/**
|
|
1627
2217
|
* Generate HTML for App Router with nested layouts
|
|
1628
2218
|
*/
|
|
1629
2219
|
private async generateAppRouterHtml(
|
|
1630
|
-
route: { page: string; layouts: string[] },
|
|
2220
|
+
route: { page: string; layouts: string[]; params: Record<string, string | string[]> },
|
|
1631
2221
|
pathname: string
|
|
1632
2222
|
): Promise<string> {
|
|
1633
2223
|
// Use virtual server prefix for all file imports so the service worker can intercept them
|
|
@@ -1658,6 +2248,9 @@ export class NextDevServer extends DevServer {
|
|
|
1658
2248
|
// Generate env script for NEXT_PUBLIC_* variables
|
|
1659
2249
|
const envScript = this.generateEnvScript();
|
|
1660
2250
|
|
|
2251
|
+
// Load Tailwind config if available (must be injected BEFORE CDN script)
|
|
2252
|
+
const tailwindConfigScript = await this.loadTailwindConfigIfNeeded();
|
|
2253
|
+
|
|
1661
2254
|
return `<!DOCTYPE html>
|
|
1662
2255
|
<html lang="en">
|
|
1663
2256
|
<head>
|
|
@@ -1667,6 +2260,7 @@ export class NextDevServer extends DevServer {
|
|
|
1667
2260
|
<title>Next.js App</title>
|
|
1668
2261
|
${envScript}
|
|
1669
2262
|
${TAILWIND_CDN_SCRIPT}
|
|
2263
|
+
${tailwindConfigScript}
|
|
1670
2264
|
${CORS_PROXY_SCRIPT}
|
|
1671
2265
|
${globalCssLinks.join('\n ')}
|
|
1672
2266
|
${REACT_REFRESH_PREAMBLE}
|
|
@@ -1688,7 +2282,11 @@ export class NextDevServer extends DevServer {
|
|
|
1688
2282
|
"next/link": "${virtualPrefix}/_next/shims/link.js",
|
|
1689
2283
|
"next/router": "${virtualPrefix}/_next/shims/router.js",
|
|
1690
2284
|
"next/head": "${virtualPrefix}/_next/shims/head.js",
|
|
1691
|
-
"next/navigation": "${virtualPrefix}/_next/shims/navigation.js"
|
|
2285
|
+
"next/navigation": "${virtualPrefix}/_next/shims/navigation.js",
|
|
2286
|
+
"next/image": "${virtualPrefix}/_next/shims/image.js",
|
|
2287
|
+
"next/dynamic": "${virtualPrefix}/_next/shims/dynamic.js",
|
|
2288
|
+
"next/script": "${virtualPrefix}/_next/shims/script.js",
|
|
2289
|
+
"next/font/google": "${virtualPrefix}/_next/shims/font/google.js"
|
|
1692
2290
|
}
|
|
1693
2291
|
}
|
|
1694
2292
|
</script>
|
|
@@ -1702,6 +2300,39 @@ export class NextDevServer extends DevServer {
|
|
|
1702
2300
|
|
|
1703
2301
|
const virtualBase = '${virtualPrefix}';
|
|
1704
2302
|
|
|
2303
|
+
// Initial route params (embedded by server for initial page load)
|
|
2304
|
+
const initialRouteParams = ${JSON.stringify(route.params)};
|
|
2305
|
+
const initialPathname = '${pathname}';
|
|
2306
|
+
|
|
2307
|
+
// Route params cache for client-side navigation
|
|
2308
|
+
const routeParamsCache = new Map();
|
|
2309
|
+
routeParamsCache.set(initialPathname, initialRouteParams);
|
|
2310
|
+
|
|
2311
|
+
// Extract route params from server for client-side navigation
|
|
2312
|
+
async function extractRouteParams(pathname) {
|
|
2313
|
+
// Strip virtual base if present
|
|
2314
|
+
let route = pathname;
|
|
2315
|
+
if (route.startsWith(virtualBase)) {
|
|
2316
|
+
route = route.slice(virtualBase.length);
|
|
2317
|
+
}
|
|
2318
|
+
route = route.replace(/^\\/+/, '/') || '/';
|
|
2319
|
+
|
|
2320
|
+
// Check cache first
|
|
2321
|
+
if (routeParamsCache.has(route)) {
|
|
2322
|
+
return routeParamsCache.get(route);
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
try {
|
|
2326
|
+
const response = await fetch(virtualBase + '/_next/route-info?pathname=' + encodeURIComponent(route));
|
|
2327
|
+
const info = await response.json();
|
|
2328
|
+
routeParamsCache.set(route, info.params || {});
|
|
2329
|
+
return info.params || {};
|
|
2330
|
+
} catch (e) {
|
|
2331
|
+
console.error('[Router] Failed to extract route params:', e);
|
|
2332
|
+
return {};
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
|
|
1705
2336
|
// Convert URL path to app router page module path
|
|
1706
2337
|
function getAppPageModulePath(pathname) {
|
|
1707
2338
|
let route = pathname;
|
|
@@ -1768,11 +2399,60 @@ export class NextDevServer extends DevServer {
|
|
|
1768
2399
|
return layouts;
|
|
1769
2400
|
}
|
|
1770
2401
|
|
|
2402
|
+
// Wrapper for async Server Components
|
|
2403
|
+
function AsyncComponent({ component: Component, pathname, search }) {
|
|
2404
|
+
const [content, setContent] = React.useState(null);
|
|
2405
|
+
const [error, setError] = React.useState(null);
|
|
2406
|
+
|
|
2407
|
+
React.useEffect(() => {
|
|
2408
|
+
let cancelled = false;
|
|
2409
|
+
async function render() {
|
|
2410
|
+
try {
|
|
2411
|
+
// Create searchParams as a Promise (Next.js 15 pattern)
|
|
2412
|
+
const url = new URL(window.location.href);
|
|
2413
|
+
const searchParamsObj = Object.fromEntries(url.searchParams);
|
|
2414
|
+
const searchParams = Promise.resolve(searchParamsObj);
|
|
2415
|
+
|
|
2416
|
+
// Extract route params from pathname (fetches from server for dynamic routes)
|
|
2417
|
+
const routeParams = await extractRouteParams(pathname);
|
|
2418
|
+
const params = Promise.resolve(routeParams);
|
|
2419
|
+
|
|
2420
|
+
// Call component with props like Next.js does for page components
|
|
2421
|
+
const result = Component({ searchParams, params });
|
|
2422
|
+
if (result && typeof result.then === 'function') {
|
|
2423
|
+
// It's a Promise (async component)
|
|
2424
|
+
const resolved = await result;
|
|
2425
|
+
if (!cancelled) setContent(resolved);
|
|
2426
|
+
} else {
|
|
2427
|
+
// Synchronous component - result is already JSX
|
|
2428
|
+
if (!cancelled) setContent(result);
|
|
2429
|
+
}
|
|
2430
|
+
} catch (e) {
|
|
2431
|
+
console.error('[AsyncComponent] Error rendering:', e);
|
|
2432
|
+
if (!cancelled) setError(e);
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
render();
|
|
2436
|
+
return () => { cancelled = true; };
|
|
2437
|
+
}, [Component, pathname, search]);
|
|
2438
|
+
|
|
2439
|
+
if (error) {
|
|
2440
|
+
return React.createElement('div', { style: { color: 'red', padding: '20px' } },
|
|
2441
|
+
'Error: ' + error.message
|
|
2442
|
+
);
|
|
2443
|
+
}
|
|
2444
|
+
if (!content) {
|
|
2445
|
+
return React.createElement('div', { style: { padding: '20px' } }, 'Loading...');
|
|
2446
|
+
}
|
|
2447
|
+
return content;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
1771
2450
|
// Router component
|
|
1772
2451
|
function Router() {
|
|
1773
2452
|
const [Page, setPage] = React.useState(null);
|
|
1774
2453
|
const [layouts, setLayouts] = React.useState([]);
|
|
1775
2454
|
const [path, setPath] = React.useState(window.location.pathname);
|
|
2455
|
+
const [search, setSearch] = React.useState(window.location.search);
|
|
1776
2456
|
|
|
1777
2457
|
React.useEffect(() => {
|
|
1778
2458
|
Promise.all([loadPage(path), loadLayouts(path)]).then(([P, L]) => {
|
|
@@ -1784,21 +2464,35 @@ export class NextDevServer extends DevServer {
|
|
|
1784
2464
|
React.useEffect(() => {
|
|
1785
2465
|
const handleNavigation = async () => {
|
|
1786
2466
|
const newPath = window.location.pathname;
|
|
2467
|
+
const newSearch = window.location.search;
|
|
2468
|
+
console.log('[Router] handleNavigation called, newPath:', newPath, 'current path:', path);
|
|
2469
|
+
|
|
2470
|
+
// Always update search params
|
|
2471
|
+
if (newSearch !== search) {
|
|
2472
|
+
setSearch(newSearch);
|
|
2473
|
+
}
|
|
2474
|
+
|
|
1787
2475
|
if (newPath !== path) {
|
|
2476
|
+
console.log('[Router] Path changed, loading new page...');
|
|
1788
2477
|
setPath(newPath);
|
|
1789
2478
|
const [P, L] = await Promise.all([loadPage(newPath), loadLayouts(newPath)]);
|
|
2479
|
+
console.log('[Router] Page loaded:', !!P, 'Layouts:', L.length);
|
|
1790
2480
|
if (P) setPage(() => P);
|
|
1791
2481
|
setLayouts(L);
|
|
2482
|
+
} else {
|
|
2483
|
+
console.log('[Router] Path unchanged, skipping navigation');
|
|
1792
2484
|
}
|
|
1793
2485
|
};
|
|
1794
2486
|
window.addEventListener('popstate', handleNavigation);
|
|
2487
|
+
console.log('[Router] Added popstate listener for path:', path);
|
|
1795
2488
|
return () => window.removeEventListener('popstate', handleNavigation);
|
|
1796
|
-
}, [path]);
|
|
2489
|
+
}, [path, search]);
|
|
1797
2490
|
|
|
1798
2491
|
if (!Page) return null;
|
|
1799
2492
|
|
|
1800
|
-
//
|
|
1801
|
-
|
|
2493
|
+
// Use AsyncComponent wrapper to handle async Server Components
|
|
2494
|
+
// Pass search to force re-render when query params change
|
|
2495
|
+
let content = React.createElement(AsyncComponent, { component: Page, pathname: path, search: search });
|
|
1802
2496
|
for (let i = layouts.length - 1; i >= 0; i--) {
|
|
1803
2497
|
content = React.createElement(layouts[i], null, content);
|
|
1804
2498
|
}
|
|
@@ -1958,6 +2652,9 @@ export class NextDevServer extends DevServer {
|
|
|
1958
2652
|
// Generate env script for NEXT_PUBLIC_* variables
|
|
1959
2653
|
const envScript = this.generateEnvScript();
|
|
1960
2654
|
|
|
2655
|
+
// Load Tailwind config if available (must be injected BEFORE CDN script)
|
|
2656
|
+
const tailwindConfigScript = await this.loadTailwindConfigIfNeeded();
|
|
2657
|
+
|
|
1961
2658
|
return `<!DOCTYPE html>
|
|
1962
2659
|
<html lang="en">
|
|
1963
2660
|
<head>
|
|
@@ -1967,6 +2664,7 @@ export class NextDevServer extends DevServer {
|
|
|
1967
2664
|
<title>Next.js App</title>
|
|
1968
2665
|
${envScript}
|
|
1969
2666
|
${TAILWIND_CDN_SCRIPT}
|
|
2667
|
+
${tailwindConfigScript}
|
|
1970
2668
|
${CORS_PROXY_SCRIPT}
|
|
1971
2669
|
${globalCssLinks.join('\n ')}
|
|
1972
2670
|
${REACT_REFRESH_PREAMBLE}
|
|
@@ -1980,7 +2678,12 @@ export class NextDevServer extends DevServer {
|
|
|
1980
2678
|
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client?dev",
|
|
1981
2679
|
"next/link": "${virtualPrefix}/_next/shims/link.js",
|
|
1982
2680
|
"next/router": "${virtualPrefix}/_next/shims/router.js",
|
|
1983
|
-
"next/head": "${virtualPrefix}/_next/shims/head.js"
|
|
2681
|
+
"next/head": "${virtualPrefix}/_next/shims/head.js",
|
|
2682
|
+
"next/navigation": "${virtualPrefix}/_next/shims/navigation.js",
|
|
2683
|
+
"next/image": "${virtualPrefix}/_next/shims/image.js",
|
|
2684
|
+
"next/dynamic": "${virtualPrefix}/_next/shims/dynamic.js",
|
|
2685
|
+
"next/script": "${virtualPrefix}/_next/shims/script.js",
|
|
2686
|
+
"next/font/google": "${virtualPrefix}/_next/shims/font/google.js"
|
|
1984
2687
|
}
|
|
1985
2688
|
}
|
|
1986
2689
|
</script>
|
|
@@ -2102,6 +2805,39 @@ export class NextDevServer extends DevServer {
|
|
|
2102
2805
|
};
|
|
2103
2806
|
}
|
|
2104
2807
|
|
|
2808
|
+
/**
|
|
2809
|
+
* Try to resolve a file path by adding common extensions
|
|
2810
|
+
* e.g., /components/faq -> /components/faq.tsx
|
|
2811
|
+
* Also handles index files in directories
|
|
2812
|
+
*/
|
|
2813
|
+
private resolveFileWithExtension(pathname: string): string | null {
|
|
2814
|
+
// If the file already has an extension and exists, return it
|
|
2815
|
+
if (/\.\w+$/.test(pathname) && this.exists(pathname)) {
|
|
2816
|
+
return pathname;
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
// Common extensions to try, in order of preference
|
|
2820
|
+
const extensions = ['.tsx', '.ts', '.jsx', '.js'];
|
|
2821
|
+
|
|
2822
|
+
// Try adding extensions directly
|
|
2823
|
+
for (const ext of extensions) {
|
|
2824
|
+
const withExt = pathname + ext;
|
|
2825
|
+
if (this.exists(withExt)) {
|
|
2826
|
+
return withExt;
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
// Try as a directory with index file
|
|
2831
|
+
for (const ext of extensions) {
|
|
2832
|
+
const indexPath = pathname + '/index' + ext;
|
|
2833
|
+
if (this.exists(indexPath)) {
|
|
2834
|
+
return indexPath;
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
return null;
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2105
2841
|
/**
|
|
2106
2842
|
* Check if a file needs transformation
|
|
2107
2843
|
*/
|
|
@@ -2115,7 +2851,31 @@ export class NextDevServer extends DevServer {
|
|
|
2115
2851
|
private async transformAndServe(filePath: string, urlPath: string): Promise<ResponseData> {
|
|
2116
2852
|
try {
|
|
2117
2853
|
const content = this.vfs.readFileSync(filePath, 'utf8');
|
|
2118
|
-
const
|
|
2854
|
+
const hash = simpleHash(content);
|
|
2855
|
+
|
|
2856
|
+
// Check transform cache
|
|
2857
|
+
const cached = this.transformCache.get(filePath);
|
|
2858
|
+
if (cached && cached.hash === hash) {
|
|
2859
|
+
const buffer = Buffer.from(cached.code);
|
|
2860
|
+
return {
|
|
2861
|
+
statusCode: 200,
|
|
2862
|
+
statusMessage: 'OK',
|
|
2863
|
+
headers: {
|
|
2864
|
+
'Content-Type': 'application/javascript; charset=utf-8',
|
|
2865
|
+
'Content-Length': String(buffer.length),
|
|
2866
|
+
'Cache-Control': 'no-cache',
|
|
2867
|
+
'X-Transformed': 'true',
|
|
2868
|
+
'X-Cache': 'hit',
|
|
2869
|
+
},
|
|
2870
|
+
body: buffer,
|
|
2871
|
+
};
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
// Use filePath (with extension) for transform so loader is correctly determined
|
|
2875
|
+
const transformed = await this.transformCode(content, filePath);
|
|
2876
|
+
|
|
2877
|
+
// Cache the transform result
|
|
2878
|
+
this.transformCache.set(filePath, { code: transformed, hash });
|
|
2119
2879
|
|
|
2120
2880
|
const buffer = Buffer.from(transformed);
|
|
2121
2881
|
return {
|
|
@@ -2164,12 +2924,15 @@ export class NextDevServer extends DevServer {
|
|
|
2164
2924
|
// CSS imports in ESM would fail with MIME type errors
|
|
2165
2925
|
const codeWithoutCssImports = this.stripCssImports(code);
|
|
2166
2926
|
|
|
2927
|
+
// Resolve path aliases (e.g., @/ -> /) before transformation
|
|
2928
|
+
const codeWithResolvedAliases = this.resolvePathAliases(codeWithoutCssImports, filename);
|
|
2929
|
+
|
|
2167
2930
|
let loader: 'js' | 'jsx' | 'ts' | 'tsx' = 'js';
|
|
2168
2931
|
if (filename.endsWith('.jsx')) loader = 'jsx';
|
|
2169
2932
|
else if (filename.endsWith('.tsx')) loader = 'tsx';
|
|
2170
2933
|
else if (filename.endsWith('.ts')) loader = 'ts';
|
|
2171
2934
|
|
|
2172
|
-
const result = await esbuild.transform(
|
|
2935
|
+
const result = await esbuild.transform(codeWithResolvedAliases, {
|
|
2173
2936
|
loader,
|
|
2174
2937
|
format: 'esm',
|
|
2175
2938
|
target: 'esnext',
|
|
@@ -2179,12 +2942,80 @@ export class NextDevServer extends DevServer {
|
|
|
2179
2942
|
sourcefile: filename,
|
|
2180
2943
|
});
|
|
2181
2944
|
|
|
2945
|
+
// Redirect bare npm imports to esm.sh CDN
|
|
2946
|
+
const codeWithCdnImports = this.redirectNpmImports(result.code);
|
|
2947
|
+
|
|
2182
2948
|
// Add React Refresh registration for JSX/TSX files
|
|
2183
2949
|
if (/\.(jsx|tsx)$/.test(filename)) {
|
|
2184
|
-
return this.addReactRefresh(
|
|
2950
|
+
return this.addReactRefresh(codeWithCdnImports, filename);
|
|
2185
2951
|
}
|
|
2186
2952
|
|
|
2187
|
-
return
|
|
2953
|
+
return codeWithCdnImports;
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
/**
|
|
2957
|
+
* Redirect bare npm package imports to esm.sh CDN
|
|
2958
|
+
* e.g., import { Crisp } from "crisp-sdk-web" -> import { Crisp } from "https://esm.sh/crisp-sdk-web?external=react"
|
|
2959
|
+
*
|
|
2960
|
+
* IMPORTANT: We redirect ALL npm packages to esm.sh URLs (including React)
|
|
2961
|
+
* because import maps don't work reliably for dynamically imported modules.
|
|
2962
|
+
*/
|
|
2963
|
+
private redirectNpmImports(code: string): string {
|
|
2964
|
+
// Explicit mappings for common packages (ensures correct esm.sh URLs)
|
|
2965
|
+
const explicitMappings: Record<string, string> = {
|
|
2966
|
+
'react': 'https://esm.sh/react@18.2.0?dev',
|
|
2967
|
+
'react/jsx-runtime': 'https://esm.sh/react@18.2.0&dev/jsx-runtime',
|
|
2968
|
+
'react/jsx-dev-runtime': 'https://esm.sh/react@18.2.0&dev/jsx-dev-runtime',
|
|
2969
|
+
'react-dom': 'https://esm.sh/react-dom@18.2.0?dev',
|
|
2970
|
+
'react-dom/client': 'https://esm.sh/react-dom@18.2.0/client?dev',
|
|
2971
|
+
};
|
|
2972
|
+
|
|
2973
|
+
// Packages that are local or have custom shims (NOT npm packages)
|
|
2974
|
+
const localPackages = new Set([
|
|
2975
|
+
'next/link', 'next/router', 'next/head', 'next/navigation',
|
|
2976
|
+
'next/dynamic', 'next/image', 'next/script', 'next/font/google',
|
|
2977
|
+
'convex/_generated/api'
|
|
2978
|
+
]);
|
|
2979
|
+
|
|
2980
|
+
// Pattern to match import statements with bare package specifiers
|
|
2981
|
+
// Matches: from "package" or from 'package' where package doesn't start with . / or http
|
|
2982
|
+
const importPattern = /(from\s*['"])([^'"./][^'"]*?)(['"])/g;
|
|
2983
|
+
|
|
2984
|
+
return code.replace(importPattern, (match, prefix, packageName, suffix) => {
|
|
2985
|
+
// Skip if already a URL or local virtual path
|
|
2986
|
+
if (packageName.startsWith('http://') ||
|
|
2987
|
+
packageName.startsWith('https://') ||
|
|
2988
|
+
packageName.startsWith('/__virtual__')) {
|
|
2989
|
+
return match;
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
// Check explicit mappings first
|
|
2993
|
+
if (explicitMappings[packageName]) {
|
|
2994
|
+
return `${prefix}${explicitMappings[packageName]}${suffix}`;
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
// Skip local/shimmed packages (they're handled via import map or virtual paths)
|
|
2998
|
+
if (localPackages.has(packageName)) {
|
|
2999
|
+
return match;
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
// Check if it's a subpath import of a local package
|
|
3003
|
+
const basePkg = packageName.includes('/') ? packageName.split('/')[0] : packageName;
|
|
3004
|
+
|
|
3005
|
+
// Handle scoped packages (@org/pkg)
|
|
3006
|
+
const isScoped = basePkg.startsWith('@');
|
|
3007
|
+
const scopedBasePkg = isScoped && packageName.includes('/')
|
|
3008
|
+
? packageName.split('/').slice(0, 2).join('/')
|
|
3009
|
+
: basePkg;
|
|
3010
|
+
|
|
3011
|
+
if (localPackages.has(scopedBasePkg)) {
|
|
3012
|
+
return match;
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
// Redirect to esm.sh with external=react to avoid bundling React twice
|
|
3016
|
+
const esmUrl = `https://esm.sh/${packageName}?external=react`;
|
|
3017
|
+
return `${prefix}${esmUrl}${suffix}`;
|
|
3018
|
+
});
|
|
2188
3019
|
}
|
|
2189
3020
|
|
|
2190
3021
|
/**
|
|
@@ -2194,13 +3025,18 @@ export class NextDevServer extends DevServer {
|
|
|
2194
3025
|
private stripCssImports(code: string): string {
|
|
2195
3026
|
// Match import statements for CSS files (with or without semicolon)
|
|
2196
3027
|
// Handles: import './styles.css'; import "./globals.css" import '../path/file.css'
|
|
2197
|
-
|
|
3028
|
+
// NOTE: Don't match trailing whitespace (\s*) as it would consume newlines
|
|
3029
|
+
// and break subsequent imports that start on the next line
|
|
3030
|
+
return code.replace(/import\s+['"][^'"]+\.css['"]\s*;?/g, '');
|
|
2198
3031
|
}
|
|
2199
3032
|
|
|
2200
3033
|
/**
|
|
2201
3034
|
* Transform API handler code to CommonJS for eval execution
|
|
2202
3035
|
*/
|
|
2203
3036
|
private async transformApiHandler(code: string, filename: string): Promise<string> {
|
|
3037
|
+
// Resolve path aliases first
|
|
3038
|
+
const codeWithResolvedAliases = this.resolvePathAliases(code, filename);
|
|
3039
|
+
|
|
2204
3040
|
if (isBrowser) {
|
|
2205
3041
|
// Use esbuild in browser
|
|
2206
3042
|
await initEsbuild();
|
|
@@ -2215,7 +3051,7 @@ export class NextDevServer extends DevServer {
|
|
|
2215
3051
|
else if (filename.endsWith('.tsx')) loader = 'tsx';
|
|
2216
3052
|
else if (filename.endsWith('.ts')) loader = 'ts';
|
|
2217
3053
|
|
|
2218
|
-
const result = await esbuild.transform(
|
|
3054
|
+
const result = await esbuild.transform(codeWithResolvedAliases, {
|
|
2219
3055
|
loader,
|
|
2220
3056
|
format: 'cjs', // CommonJS for eval execution
|
|
2221
3057
|
target: 'esnext',
|
|
@@ -2227,7 +3063,7 @@ export class NextDevServer extends DevServer {
|
|
|
2227
3063
|
}
|
|
2228
3064
|
|
|
2229
3065
|
// Simple ESM to CJS transform for Node.js/test environment
|
|
2230
|
-
let transformed =
|
|
3066
|
+
let transformed = codeWithResolvedAliases;
|
|
2231
3067
|
|
|
2232
3068
|
// Convert: import X from 'Y' -> const X = require('Y')
|
|
2233
3069
|
transformed = transformed.replace(
|
|
@@ -2393,6 +3229,76 @@ ${registrations}
|
|
|
2393
3229
|
}
|
|
2394
3230
|
}
|
|
2395
3231
|
|
|
3232
|
+
/**
|
|
3233
|
+
* Override serveFile to wrap JSON files as ES modules
|
|
3234
|
+
* This is needed because browsers can't dynamically import raw JSON files
|
|
3235
|
+
*/
|
|
3236
|
+
protected serveFile(filePath: string): ResponseData {
|
|
3237
|
+
// For JSON files, wrap as ES module so they can be dynamically imported
|
|
3238
|
+
if (filePath.endsWith('.json')) {
|
|
3239
|
+
try {
|
|
3240
|
+
const normalizedPath = this.resolvePath(filePath);
|
|
3241
|
+
const content = this.vfs.readFileSync(normalizedPath);
|
|
3242
|
+
|
|
3243
|
+
// Properly convert content to string
|
|
3244
|
+
// VirtualFS may return string, Buffer, or Uint8Array
|
|
3245
|
+
let jsonContent: string;
|
|
3246
|
+
if (typeof content === 'string') {
|
|
3247
|
+
jsonContent = content;
|
|
3248
|
+
} else if (content instanceof Uint8Array) {
|
|
3249
|
+
// Use TextDecoder for Uint8Array (includes Buffer in browser)
|
|
3250
|
+
jsonContent = new TextDecoder('utf-8').decode(content);
|
|
3251
|
+
} else {
|
|
3252
|
+
// Fallback for other buffer-like objects
|
|
3253
|
+
jsonContent = Buffer.from(content).toString('utf-8');
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
// Wrap JSON as ES module
|
|
3257
|
+
const esModuleContent = `export default ${jsonContent};`;
|
|
3258
|
+
const buffer = Buffer.from(esModuleContent);
|
|
3259
|
+
|
|
3260
|
+
return {
|
|
3261
|
+
statusCode: 200,
|
|
3262
|
+
statusMessage: 'OK',
|
|
3263
|
+
headers: {
|
|
3264
|
+
'Content-Type': 'application/javascript; charset=utf-8',
|
|
3265
|
+
'Content-Length': String(buffer.length),
|
|
3266
|
+
'Cache-Control': 'no-cache',
|
|
3267
|
+
},
|
|
3268
|
+
body: buffer,
|
|
3269
|
+
};
|
|
3270
|
+
} catch (error) {
|
|
3271
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
3272
|
+
return this.notFound(filePath);
|
|
3273
|
+
}
|
|
3274
|
+
return this.serverError(error);
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
// For all other files, use the parent implementation
|
|
3279
|
+
return super.serveFile(filePath);
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
/**
|
|
3283
|
+
* Resolve a path (helper to access protected method from parent)
|
|
3284
|
+
*/
|
|
3285
|
+
protected resolvePath(urlPath: string): string {
|
|
3286
|
+
// Remove query string and hash
|
|
3287
|
+
let path = urlPath.split('?')[0].split('#')[0];
|
|
3288
|
+
|
|
3289
|
+
// Normalize path
|
|
3290
|
+
if (!path.startsWith('/')) {
|
|
3291
|
+
path = '/' + path;
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
// Join with root
|
|
3295
|
+
if (this.root !== '/') {
|
|
3296
|
+
path = this.root + path;
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
return path;
|
|
3300
|
+
}
|
|
3301
|
+
|
|
2396
3302
|
/**
|
|
2397
3303
|
* Stop the server
|
|
2398
3304
|
*/
|