@tanstack/start-plugin-core 1.167.35 → 1.169.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/import-protection/adapterUtils.d.ts +27 -0
- package/dist/esm/import-protection/adapterUtils.js +31 -0
- package/dist/esm/import-protection/adapterUtils.js.map +1 -0
- package/dist/esm/import-protection/analysis.d.ts +36 -0
- package/dist/esm/import-protection/analysis.js +407 -0
- package/dist/esm/import-protection/analysis.js.map +1 -0
- package/dist/esm/{import-protection-plugin → import-protection}/ast.js +1 -1
- package/dist/esm/import-protection/ast.js.map +1 -0
- package/dist/esm/import-protection/constants.d.ts +11 -0
- package/dist/esm/{import-protection-plugin → import-protection}/constants.js +7 -2
- package/dist/esm/import-protection/constants.js.map +1 -0
- package/dist/esm/{import-protection-plugin → import-protection}/defaults.js +1 -1
- package/dist/esm/import-protection/defaults.js.map +1 -0
- package/dist/esm/{import-protection-plugin → import-protection}/extensionlessAbsoluteIdResolver.js +2 -2
- package/dist/esm/import-protection/extensionlessAbsoluteIdResolver.js.map +1 -0
- package/dist/esm/{import-protection-plugin → import-protection}/matchers.js +1 -1
- package/dist/esm/import-protection/matchers.js.map +1 -0
- package/dist/esm/{import-protection-plugin/rewriteDeniedImports.d.ts → import-protection/rewrite.d.ts} +0 -4
- package/dist/esm/import-protection/rewrite.js +121 -0
- package/dist/esm/import-protection/rewrite.js.map +1 -0
- package/dist/esm/{import-protection-plugin → import-protection}/sourceLocation.d.ts +32 -3
- package/dist/esm/{import-protection-plugin → import-protection}/sourceLocation.js +65 -10
- package/dist/esm/import-protection/sourceLocation.js.map +1 -0
- package/dist/esm/{import-protection-plugin → import-protection}/trace.d.ts +0 -1
- package/dist/esm/{import-protection-plugin → import-protection}/trace.js +1 -1
- package/dist/esm/import-protection/trace.js.map +1 -0
- package/dist/esm/{import-protection-plugin → import-protection}/utils.d.ts +18 -1
- package/dist/esm/{import-protection-plugin → import-protection}/utils.js +13 -20
- package/dist/esm/import-protection/utils.js.map +1 -0
- package/dist/esm/import-protection/virtualModules.d.ts +25 -0
- package/dist/esm/{import-protection-plugin → import-protection}/virtualModules.js +5 -117
- package/dist/esm/import-protection/virtualModules.js.map +1 -0
- package/dist/esm/index.d.ts +1 -5
- package/dist/esm/index.js +2 -4
- package/dist/esm/post-build.d.ts +9 -0
- package/dist/esm/post-build.js +37 -0
- package/dist/esm/post-build.js.map +1 -0
- package/dist/esm/prerender.d.ts +11 -0
- package/dist/esm/prerender.js +159 -0
- package/dist/esm/prerender.js.map +1 -0
- package/dist/esm/rsbuild/dev-server.d.ts +21 -0
- package/dist/esm/rsbuild/dev-server.js +76 -0
- package/dist/esm/rsbuild/dev-server.js.map +1 -0
- package/dist/esm/rsbuild/import-protection.d.ts +10 -0
- package/dist/esm/rsbuild/import-protection.js +775 -0
- package/dist/esm/rsbuild/import-protection.js.map +1 -0
- package/dist/esm/rsbuild/index.d.ts +4 -0
- package/dist/esm/rsbuild/index.js +3 -0
- package/dist/esm/rsbuild/normalized-client-build.d.ts +18 -0
- package/dist/esm/rsbuild/normalized-client-build.js +207 -0
- package/dist/esm/rsbuild/normalized-client-build.js.map +1 -0
- package/dist/esm/rsbuild/planning.d.ts +52 -0
- package/dist/esm/rsbuild/planning.js +108 -0
- package/dist/esm/rsbuild/planning.js.map +1 -0
- package/dist/esm/rsbuild/plugin.d.ts +4 -0
- package/dist/esm/rsbuild/plugin.js +344 -0
- package/dist/esm/rsbuild/plugin.js.map +1 -0
- package/dist/esm/rsbuild/post-build.d.ts +6 -0
- package/dist/esm/rsbuild/post-build.js +57 -0
- package/dist/esm/rsbuild/post-build.js.map +1 -0
- package/dist/esm/rsbuild/schema.d.ts +3372 -0
- package/dist/esm/rsbuild/schema.js +12 -0
- package/dist/esm/rsbuild/schema.js.map +1 -0
- package/dist/esm/rsbuild/start-compiler-host.d.ts +20 -0
- package/dist/esm/rsbuild/start-compiler-host.js +150 -0
- package/dist/esm/rsbuild/start-compiler-host.js.map +1 -0
- package/dist/esm/rsbuild/start-router-plugin.d.ts +18 -0
- package/dist/esm/rsbuild/start-router-plugin.js +63 -0
- package/dist/esm/rsbuild/start-router-plugin.js.map +1 -0
- package/dist/esm/rsbuild/swc-rsc.d.ts +14 -0
- package/dist/esm/rsbuild/swc-rsc.js +93 -0
- package/dist/esm/rsbuild/swc-rsc.js.map +1 -0
- package/dist/esm/rsbuild/types.d.ts +17 -0
- package/dist/esm/rsbuild/types.js +0 -0
- package/dist/esm/rsbuild/virtual-modules.d.ts +53 -0
- package/dist/esm/rsbuild/virtual-modules.js +287 -0
- package/dist/esm/rsbuild/virtual-modules.js.map +1 -0
- package/dist/esm/schema.d.ts +43 -43
- package/dist/esm/start-compiler/compiler.d.ts +1 -1
- package/dist/esm/start-compiler/compiler.js +80 -9
- package/dist/esm/start-compiler/compiler.js.map +1 -1
- package/dist/esm/start-compiler/handleCreateServerFn.js +9 -0
- package/dist/esm/start-compiler/handleCreateServerFn.js.map +1 -1
- package/dist/esm/start-compiler/host.js +5 -1
- package/dist/esm/start-compiler/host.js.map +1 -1
- package/dist/esm/start-compiler/types.d.ts +1 -0
- package/dist/esm/utils.d.ts +1 -0
- package/dist/esm/utils.js +10 -1
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/{import-protection-plugin → vite/import-protection-plugin}/plugin.js +41 -92
- package/dist/esm/vite/import-protection-plugin/plugin.js.map +1 -0
- package/dist/esm/{import-protection-plugin → vite/import-protection-plugin}/types.d.ts +5 -5
- package/dist/esm/vite/import-protection-plugin/virtualModules.d.ts +8 -0
- package/dist/esm/vite/import-protection-plugin/virtualModules.js +49 -0
- package/dist/esm/vite/import-protection-plugin/virtualModules.js.map +1 -0
- package/dist/esm/vite/index.d.ts +5 -0
- package/dist/esm/vite/index.js +4 -0
- package/dist/esm/vite/plugin.js +1 -1
- package/dist/esm/vite/plugin.js.map +1 -1
- package/dist/esm/vite/post-server-build.js +14 -32
- package/dist/esm/vite/post-server-build.js.map +1 -1
- package/dist/esm/vite/prerender.d.ts +2 -2
- package/dist/esm/vite/prerender.js +17 -147
- package/dist/esm/vite/prerender.js.map +1 -1
- package/dist/esm/vite/schema.d.ts +23 -23
- package/dist/esm/vite/start-compiler-plugin/hot-update.d.ts +2 -0
- package/dist/esm/vite/start-compiler-plugin/hot-update.js +16 -0
- package/dist/esm/vite/start-compiler-plugin/hot-update.js.map +1 -0
- package/dist/esm/vite/start-compiler-plugin/module-specifier.js +9 -4
- package/dist/esm/vite/start-compiler-plugin/module-specifier.js.map +1 -1
- package/dist/esm/vite/start-compiler-plugin/plugin.js +86 -13
- package/dist/esm/vite/start-compiler-plugin/plugin.js.map +1 -1
- package/package.json +32 -4
- package/src/import-protection/INTERNALS.md +266 -0
- package/src/import-protection/adapterUtils.ts +94 -0
- package/src/import-protection/analysis.ts +853 -0
- package/src/{import-protection-plugin → import-protection}/constants.ts +7 -0
- package/src/import-protection/rewrite.ts +229 -0
- package/src/{import-protection-plugin → import-protection}/sourceLocation.ts +125 -9
- package/src/{import-protection-plugin → import-protection}/trace.ts +0 -1
- package/src/{import-protection-plugin → import-protection}/utils.ts +36 -21
- package/src/{import-protection-plugin → import-protection}/virtualModules.ts +30 -177
- package/src/index.ts +1 -8
- package/src/post-build.ts +64 -0
- package/src/prerender.ts +292 -0
- package/src/rsbuild/INTERNALS-import-protection.md +169 -0
- package/src/rsbuild/dev-server.ts +129 -0
- package/src/rsbuild/import-protection.ts +1599 -0
- package/src/rsbuild/index.ts +4 -0
- package/src/rsbuild/normalized-client-build.ts +346 -0
- package/src/rsbuild/planning.ts +234 -0
- package/src/rsbuild/plugin.ts +754 -0
- package/src/rsbuild/post-build.ts +96 -0
- package/src/rsbuild/schema.ts +31 -0
- package/src/rsbuild/start-compiler-host.ts +250 -0
- package/src/rsbuild/start-router-plugin.ts +86 -0
- package/src/rsbuild/swc-rsc.ts +166 -0
- package/src/rsbuild/types.ts +20 -0
- package/src/rsbuild/virtual-modules.ts +565 -0
- package/src/start-compiler/compiler.ts +153 -19
- package/src/start-compiler/handleCreateServerFn.ts +18 -0
- package/src/start-compiler/types.ts +1 -0
- package/src/utils.ts +14 -0
- package/src/vite/import-protection-plugin/INTERNALS.md +187 -0
- package/src/{import-protection-plugin → vite/import-protection-plugin}/plugin.ts +73 -158
- package/src/{import-protection-plugin → vite/import-protection-plugin}/types.ts +5 -5
- package/src/vite/import-protection-plugin/virtualModules.ts +122 -0
- package/src/vite/index.ts +8 -0
- package/src/vite/plugin.ts +1 -1
- package/src/vite/post-server-build.ts +14 -57
- package/src/vite/prerender.ts +19 -260
- package/src/vite/start-compiler-plugin/hot-update.ts +24 -0
- package/src/vite/start-compiler-plugin/module-specifier.ts +15 -5
- package/src/vite/start-compiler-plugin/plugin.ts +193 -18
- package/dist/esm/import-protection-plugin/ast.js.map +0 -1
- package/dist/esm/import-protection-plugin/constants.d.ts +0 -6
- package/dist/esm/import-protection-plugin/constants.js.map +0 -1
- package/dist/esm/import-protection-plugin/defaults.js.map +0 -1
- package/dist/esm/import-protection-plugin/extensionlessAbsoluteIdResolver.js.map +0 -1
- package/dist/esm/import-protection-plugin/matchers.js.map +0 -1
- package/dist/esm/import-protection-plugin/plugin.js.map +0 -1
- package/dist/esm/import-protection-plugin/postCompileUsage.d.ts +0 -13
- package/dist/esm/import-protection-plugin/postCompileUsage.js +0 -63
- package/dist/esm/import-protection-plugin/postCompileUsage.js.map +0 -1
- package/dist/esm/import-protection-plugin/rewriteDeniedImports.js +0 -205
- package/dist/esm/import-protection-plugin/rewriteDeniedImports.js.map +0 -1
- package/dist/esm/import-protection-plugin/sourceLocation.js.map +0 -1
- package/dist/esm/import-protection-plugin/trace.js.map +0 -1
- package/dist/esm/import-protection-plugin/utils.js.map +0 -1
- package/dist/esm/import-protection-plugin/virtualModules.d.ts +0 -78
- package/dist/esm/import-protection-plugin/virtualModules.js.map +0 -1
- package/dist/esm/start-compiler/load-module.d.ts +0 -14
- package/dist/esm/start-compiler/load-module.js +0 -18
- package/dist/esm/start-compiler/load-module.js.map +0 -1
- package/src/import-protection-plugin/INTERNALS.md +0 -700
- package/src/import-protection-plugin/postCompileUsage.ts +0 -100
- package/src/import-protection-plugin/rewriteDeniedImports.ts +0 -379
- package/src/start-compiler/load-module.ts +0 -31
- /package/dist/esm/{import-protection-plugin → import-protection}/ast.d.ts +0 -0
- /package/dist/esm/{import-protection-plugin → import-protection}/defaults.d.ts +0 -0
- /package/dist/esm/{import-protection-plugin → import-protection}/extensionlessAbsoluteIdResolver.d.ts +0 -0
- /package/dist/esm/{import-protection-plugin → import-protection}/matchers.d.ts +0 -0
- /package/dist/esm/{import-protection-plugin → vite/import-protection-plugin}/plugin.d.ts +0 -0
- /package/src/{import-protection-plugin → import-protection}/ast.ts +0 -0
- /package/src/{import-protection-plugin → import-protection}/defaults.ts +0 -0
- /package/src/{import-protection-plugin → import-protection}/extensionlessAbsoluteIdResolver.ts +0 -0
- /package/src/{import-protection-plugin → import-protection}/matchers.ts +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","names":[],"sources":["../../../src/import-protection/utils.ts"],"sourcesContent":["import {\n extname,\n isAbsolute,\n relative,\n resolve as resolvePath,\n} from 'node:path'\nimport { normalizePath } from '../utils'\n\nimport {\n IMPORT_PROTECTION_DEBUG,\n IMPORT_PROTECTION_DEBUG_FILTER,\n KNOWN_SOURCE_EXTENSIONS,\n} from './constants'\n\nexport type Pattern = string | RegExp\n\nexport function dedupePatterns(patterns: Array<Pattern>): Array<Pattern> {\n const out: Array<Pattern> = []\n const seen = new Set<string>()\n for (const p of patterns) {\n const key = typeof p === 'string' ? `s:${p}` : `r:${p.toString()}`\n if (seen.has(key)) continue\n seen.add(key)\n out.push(p)\n }\n return out\n}\n\nexport interface FileMatchers {\n files: Array<{ pattern: Pattern; test: (value: string) => boolean }>\n excludeFiles: Array<{ pattern: Pattern; test: (value: string) => boolean }>\n}\n\nexport function isFileExcluded(\n relativePath: string,\n matchers: Pick<FileMatchers, 'excludeFiles'>,\n): boolean {\n return (\n matchers.excludeFiles.length > 0 &&\n matchers.excludeFiles.some((matcher) => matcher.test(relativePath))\n )\n}\n\nexport function checkFileDenial(\n relativePath: string,\n matchers: FileMatchers,\n): FileMatchers['files'][number] | undefined {\n if (isFileExcluded(relativePath, matchers)) {\n return undefined\n }\n\n return matchers.files.find((matcher) => matcher.test(relativePath))\n}\n\nexport function dedupeViolationKey(info: {\n type: string\n importer: string\n specifier: string\n resolved?: string\n}): string {\n return `${info.type}:${info.importer}:${info.specifier}:${info.resolved ?? ''}`\n}\n\n/** Strip both `?query` and `#hash` from a module ID. */\nexport function stripQueryAndHash(id: string): string {\n const q = id.indexOf('?')\n const h = id.indexOf('#')\n if (q === -1 && h === -1) return id\n if (q === -1) return id.slice(0, h)\n if (h === -1) return id.slice(0, q)\n return id.slice(0, Math.min(q, h))\n}\n\n/**\n * Strip Vite query/hash parameters and normalize the path in one step.\n *\n * Results are memoized because the same module IDs are processed many\n * times across resolveId, transform, and trace-building hooks.\n */\nconst normalizeFilePathCache = new Map<string, string>()\nexport function normalizeFilePath(id: string): string {\n let result = normalizeFilePathCache.get(id)\n if (result === undefined) {\n result = normalizePath(stripQueryAndHash(id))\n normalizeFilePathCache.set(id, result)\n }\n return result\n}\n\n/** Clear the memoization cache (call from buildStart to bound growth). */\nexport function clearNormalizeFilePathCache(): void {\n normalizeFilePathCache.clear()\n}\n\nexport function escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/** Get a value from a Map, creating it with `factory` if absent. */\nexport function getOrCreate<TKey, TValue>(\n map: Map<TKey, TValue>,\n key: TKey,\n factory: () => TValue,\n): TValue {\n let value = map.get(key)\n if (value === undefined) {\n value = factory()\n map.set(key, value)\n }\n return value\n}\n\n/** Make a path relative to `root`, keeping non-rooted paths as-is. */\nexport function relativizePath(p: string, root: string): string {\n if (!p.startsWith(root)) return p\n const ch = p.charCodeAt(root.length)\n // Must be followed by a separator or end-of-string to be a true child\n if (ch !== 47 && !Number.isNaN(ch)) return p\n return ch === 47 ? p.slice(root.length + 1) : p.slice(root.length)\n}\n\n/** Log import-protection debug output when debug mode is enabled. */\nexport function debugLog(...args: Array<unknown>): void {\n if (!IMPORT_PROTECTION_DEBUG) return\n console.warn('[import-protection:debug]', ...args)\n}\n\n/** Check if any value matches the configured debug filter (if present). */\nexport function matchesDebugFilter(...values: Array<string>): boolean {\n const debugFilter = IMPORT_PROTECTION_DEBUG_FILTER\n if (!debugFilter) return true\n return values.some((v) => v.includes(debugFilter))\n}\n\n/** Strip `?query` (but not `#hash`) from a module ID. */\nexport function stripQuery(id: string): string {\n const queryIndex = id.indexOf('?')\n return queryIndex === -1 ? id : id.slice(0, queryIndex)\n}\n\nexport function withoutKnownExtension(id: string): string {\n const ext = extname(id)\n return KNOWN_SOURCE_EXTENSIONS.has(ext) ? id.slice(0, -ext.length) : id\n}\n\n/**\n * Check whether `filePath` is contained inside `directory` using a\n * boundary-safe comparison. A naïve `filePath.startsWith(directory)`\n * would incorrectly treat `/app/src2/foo.ts` as inside `/app/src`.\n */\nexport function isInsideDirectory(\n filePath: string,\n directory: string,\n): boolean {\n const rel = relative(resolvePath(directory), resolvePath(filePath))\n return rel.length > 0 && !rel.startsWith('..') && !isAbsolute(rel)\n}\n\n/**\n * Decide whether a violation should be deferred for later verification\n * rather than reported immediately.\n *\n * Build mode: always defer — generateBundle checks tree-shaking.\n * Dev mock mode: always defer — edge-survival verifies whether the Start\n * compiler strips the import (factory-safe pattern). All violation\n * types and specifier formats are handled uniformly by the\n * edge-survival mechanism in processPendingViolations.\n * Dev error mode: never defer — throw immediately (no mock fallback).\n */\nexport function shouldDeferViolation(opts: {\n isBuild: boolean\n isDevMock: boolean\n}): boolean {\n return opts.isBuild || opts.isDevMock\n}\n\nexport function buildSourceCandidates(\n source: string,\n resolved: string | undefined,\n root: string,\n): Set<string> {\n const candidates = new Set<string>()\n const push = (value: string | undefined) => {\n if (!value) return\n candidates.add(value)\n candidates.add(stripQuery(value))\n candidates.add(withoutKnownExtension(stripQuery(value)))\n }\n\n push(source)\n if (resolved) {\n push(resolved)\n const relativeResolved = relativizePath(resolved, root)\n push(relativeResolved)\n push(`./${relativeResolved}`)\n push(`/${relativeResolved}`)\n }\n\n return candidates\n}\n\nexport function buildResolutionCandidates(id: string): Array<string> {\n const normalized = normalizeFilePath(id)\n const stripped = stripQuery(normalized)\n\n return [...new Set([id, normalized, stripped])]\n}\n\nexport function canonicalizeResolvedId(\n id: string,\n root: string,\n resolveExtensionlessAbsoluteId: (value: string) => string,\n): string {\n const stripped = stripQuery(id)\n let normalized = normalizeFilePath(stripped)\n\n if (\n !isAbsolute(normalized) &&\n !normalized.startsWith('.') &&\n !normalized.startsWith('\\0') &&\n !/^[a-zA-Z]+:/.test(normalized)\n ) {\n normalized = normalizeFilePath(resolvePath(root, normalized))\n }\n\n return resolveExtensionlessAbsoluteId(normalized)\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { MARKER_PREFIX, MOCK_BUILD_PREFIX, MOCK_EDGE_PREFIX, MOCK_MODULE_ID, MOCK_RUNTIME_PREFIX } from './constants.js';
|
|
2
|
+
import { ViolationInfo } from './trace.js';
|
|
3
|
+
type MockAccessMode = 'error' | 'warn' | 'off';
|
|
4
|
+
export declare const RUNTIME_SUGGESTION_TEXT: string;
|
|
5
|
+
export declare function mockRuntimeModuleIdFromViolation(info: ViolationInfo, mode: MockAccessMode, root: string): string;
|
|
6
|
+
export declare function makeMockEdgeModuleId(exports: Array<string>, runtimeId: string): string;
|
|
7
|
+
export declare function loadSilentMockModule(): {
|
|
8
|
+
code: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function generateSelfContainedMockModule(exportNames: Array<string>): {
|
|
11
|
+
code: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function generateDevSelfDenialModule(exportNames: Array<string>, runtimeId: string): {
|
|
14
|
+
code: string;
|
|
15
|
+
};
|
|
16
|
+
export declare function loadMockEdgeModule(encodedPayload: string): {
|
|
17
|
+
code: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function loadMockRuntimeModule(encodedPayload: string): {
|
|
20
|
+
code: string;
|
|
21
|
+
};
|
|
22
|
+
export declare function loadMarkerModule(): {
|
|
23
|
+
code: string;
|
|
24
|
+
};
|
|
25
|
+
export { MARKER_PREFIX, MOCK_BUILD_PREFIX, MOCK_EDGE_PREFIX, MOCK_MODULE_ID, MOCK_RUNTIME_PREFIX, };
|
|
@@ -1,80 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolveViteId } from "../utils.js";
|
|
3
|
-
import { VITE_BROWSER_VIRTUAL_PREFIX } from "./constants.js";
|
|
1
|
+
import { MOCK_EDGE_PREFIX, MOCK_MODULE_ID, MOCK_RUNTIME_PREFIX } from "./constants.js";
|
|
4
2
|
import { relativizePath } from "./utils.js";
|
|
5
3
|
import { CLIENT_ENV_SUGGESTIONS } from "./trace.js";
|
|
6
|
-
import { isValidExportName } from "./
|
|
7
|
-
//#region src/import-protection
|
|
8
|
-
var MOCK_MODULE_ID = "tanstack-start-import-protection:mock";
|
|
9
|
-
var RESOLVED_MOCK_MODULE_ID = resolveViteId(MOCK_MODULE_ID);
|
|
10
|
-
/**
|
|
11
|
-
* Per-violation mock prefix used in build+error mode.
|
|
12
|
-
* Each deferred violation gets a unique ID so we can check which ones
|
|
13
|
-
* survived tree-shaking in `generateBundle`.
|
|
14
|
-
*/
|
|
15
|
-
var MOCK_BUILD_PREFIX = "tanstack-start-import-protection:mock:build:";
|
|
16
|
-
var RESOLVED_MOCK_BUILD_PREFIX = resolveViteId(MOCK_BUILD_PREFIX);
|
|
17
|
-
var MOCK_EDGE_PREFIX = "tanstack-start-import-protection:mock-edge:";
|
|
18
|
-
var RESOLVED_MOCK_EDGE_PREFIX = resolveViteId(MOCK_EDGE_PREFIX);
|
|
19
|
-
var MOCK_RUNTIME_PREFIX = "tanstack-start-import-protection:mock-runtime:";
|
|
20
|
-
var RESOLVED_MOCK_RUNTIME_PREFIX = resolveViteId(MOCK_RUNTIME_PREFIX);
|
|
21
|
-
var MARKER_PREFIX = "tanstack-start-import-protection:marker:";
|
|
22
|
-
var RESOLVED_MARKER_PREFIX = resolveViteId(MARKER_PREFIX);
|
|
23
|
-
var RESOLVED_MARKER_SERVER_ONLY = resolveViteId(`${MARKER_PREFIX}server-only`);
|
|
24
|
-
var RESOLVED_MARKER_CLIENT_ONLY = resolveViteId(`${MARKER_PREFIX}client-only`);
|
|
25
|
-
function resolvedMarkerVirtualModuleId(kind) {
|
|
26
|
-
return kind === "server" ? RESOLVED_MARKER_SERVER_ONLY : RESOLVED_MARKER_CLIENT_ONLY;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Convenience list for plugin `load` filters/handlers.
|
|
30
|
-
*
|
|
31
|
-
* Vite/Rollup call `load(id)` with the *resolved* virtual id (prefixed by `\0`).
|
|
32
|
-
* `resolveId(source)` sees the *unresolved* id/prefix (without `\0`).
|
|
33
|
-
*/
|
|
34
|
-
function getResolvedVirtualModuleMatchers() {
|
|
35
|
-
return RESOLVED_VIRTUAL_MODULE_MATCHERS;
|
|
36
|
-
}
|
|
37
|
-
var RESOLVED_VIRTUAL_MODULE_MATCHERS = [
|
|
38
|
-
RESOLVED_MOCK_MODULE_ID,
|
|
39
|
-
RESOLVED_MOCK_BUILD_PREFIX,
|
|
40
|
-
RESOLVED_MOCK_EDGE_PREFIX,
|
|
41
|
-
RESOLVED_MOCK_RUNTIME_PREFIX,
|
|
42
|
-
RESOLVED_MARKER_PREFIX
|
|
43
|
-
];
|
|
44
|
-
var RESOLVE_PREFIX_PAIRS = [
|
|
45
|
-
[MOCK_EDGE_PREFIX, RESOLVED_MOCK_EDGE_PREFIX],
|
|
46
|
-
[MOCK_RUNTIME_PREFIX, RESOLVED_MOCK_RUNTIME_PREFIX],
|
|
47
|
-
[MOCK_BUILD_PREFIX, RESOLVED_MOCK_BUILD_PREFIX],
|
|
48
|
-
[MARKER_PREFIX, RESOLVED_MARKER_PREFIX]
|
|
49
|
-
];
|
|
50
|
-
/**
|
|
51
|
-
* Resolve import-protection's internal virtual module IDs.
|
|
52
|
-
*
|
|
53
|
-
* `resolveId(source)` sees *unresolved* ids/prefixes (no `\0`).
|
|
54
|
-
* Returning a resolved id (with `\0`) ensures Vite/Rollup route it to `load`.
|
|
55
|
-
*/
|
|
56
|
-
function resolveInternalVirtualModuleId(source) {
|
|
57
|
-
if (source.startsWith("/@id/__x00__")) return resolveInternalVirtualModuleId(`\0${source.slice(VITE_BROWSER_VIRTUAL_PREFIX.length)}`);
|
|
58
|
-
if (source === "tanstack-start-import-protection:mock" || source === RESOLVED_MOCK_MODULE_ID) return RESOLVED_MOCK_MODULE_ID;
|
|
59
|
-
for (const [unresolvedPrefix, resolvedPrefix] of RESOLVE_PREFIX_PAIRS) {
|
|
60
|
-
if (source.startsWith(unresolvedPrefix)) return resolveViteId(source);
|
|
61
|
-
if (source.startsWith(resolvedPrefix)) return source;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
4
|
+
import { isValidExportName } from "./analysis.js";
|
|
5
|
+
//#region src/import-protection/virtualModules.ts
|
|
64
6
|
function toBase64Url(input) {
|
|
65
7
|
return Buffer.from(input, "utf8").toString("base64url");
|
|
66
8
|
}
|
|
67
9
|
function fromBase64Url(input) {
|
|
68
10
|
return Buffer.from(input, "base64url").toString("utf8");
|
|
69
11
|
}
|
|
70
|
-
/**
|
|
71
|
-
* Compact runtime suggestion text for browser console, derived from
|
|
72
|
-
* {@link CLIENT_ENV_SUGGESTIONS} so there's a single source of truth.
|
|
73
|
-
*/
|
|
74
12
|
var RUNTIME_SUGGESTION_TEXT = "Fix: " + CLIENT_ENV_SUGGESTIONS.join(". ") + ". To disable these runtime diagnostics, set importProtection.mockAccess: \"off\".";
|
|
75
13
|
function mockRuntimeModuleIdFromViolation(info, mode, root) {
|
|
76
14
|
if (mode === "off") return MOCK_MODULE_ID;
|
|
77
|
-
if (info.env !==
|
|
15
|
+
if (info.env !== "client") return MOCK_MODULE_ID;
|
|
78
16
|
const rel = (p) => relativizePath(p, root);
|
|
79
17
|
const trace = info.trace.map((s) => {
|
|
80
18
|
const file = rel(s.file);
|
|
@@ -97,17 +35,6 @@ function makeMockEdgeModuleId(exports, runtimeId) {
|
|
|
97
35
|
};
|
|
98
36
|
return `${MOCK_EDGE_PREFIX}${toBase64Url(JSON.stringify(payload))}`;
|
|
99
37
|
}
|
|
100
|
-
/**
|
|
101
|
-
* Generate a recursive Proxy-based mock module.
|
|
102
|
-
*
|
|
103
|
-
* When `diagnostics` is provided, the generated code includes a `__report`
|
|
104
|
-
* function that logs runtime warnings/errors when the mock is actually used
|
|
105
|
-
* (property access for primitive coercion, calls, construction, sets).
|
|
106
|
-
*
|
|
107
|
-
* When `diagnostics` is omitted, the mock is completely silent — suitable
|
|
108
|
-
* for base mock modules (e.g. `MOCK_MODULE_ID` or per-violation build mocks)
|
|
109
|
-
* that are consumed by mock-edge modules providing explicit named exports.
|
|
110
|
-
*/
|
|
111
38
|
function generateMockCode(diagnostics) {
|
|
112
39
|
const fnName = diagnostics ? "__createMock" : "createMock";
|
|
113
40
|
const hasDiag = !!diagnostics;
|
|
@@ -194,18 +121,9 @@ export default mock;
|
|
|
194
121
|
function loadSilentMockModule() {
|
|
195
122
|
return { code: generateMockCode() };
|
|
196
123
|
}
|
|
197
|
-
/**
|
|
198
|
-
* Filter export names to valid, non-default names.
|
|
199
|
-
*/
|
|
200
124
|
function filterExportNames(exports) {
|
|
201
125
|
return exports.filter((n) => n.length > 0 && n !== "default");
|
|
202
126
|
}
|
|
203
|
-
/**
|
|
204
|
-
* Generate ESM export lines that re-export named properties from `mock`.
|
|
205
|
-
*
|
|
206
|
-
* Produces `export const foo = mock.foo;` for valid identifiers and
|
|
207
|
-
* string-keyed re-exports for non-identifier names.
|
|
208
|
-
*/
|
|
209
127
|
function generateExportLines(names) {
|
|
210
128
|
const lines = [];
|
|
211
129
|
const stringExports = [];
|
|
@@ -227,34 +145,11 @@ function generateExportLines(names) {
|
|
|
227
145
|
}
|
|
228
146
|
return lines;
|
|
229
147
|
}
|
|
230
|
-
/**
|
|
231
|
-
* Generate a self-contained mock module with explicit named exports.
|
|
232
|
-
*
|
|
233
|
-
* Used by the transform hook's "self-denial" check: when a denied file
|
|
234
|
-
* (e.g. `.server.ts` in the client environment) is transformed, its entire
|
|
235
|
-
* content is replaced with this mock module. This avoids returning virtual
|
|
236
|
-
* module IDs from `resolveId`, which prevents cross-environment cache
|
|
237
|
-
* contamination from third-party resolver plugins.
|
|
238
|
-
*
|
|
239
|
-
* The generated code is side-effect-free and tree-shakeable.
|
|
240
|
-
*/
|
|
241
148
|
function generateSelfContainedMockModule(exportNames) {
|
|
242
149
|
return { code: `${generateMockCode()}
|
|
243
150
|
${generateExportLines(filterExportNames(exportNames)).join("\n")}
|
|
244
151
|
` };
|
|
245
152
|
}
|
|
246
|
-
/**
|
|
247
|
-
* Generate a dev-mode mock module for self-denial transforms.
|
|
248
|
-
*
|
|
249
|
-
* Similar to `loadMockEdgeModule` but takes export names and a runtime ID
|
|
250
|
-
* directly (instead of parsing them from a base64url-encoded payload).
|
|
251
|
-
* Used by the transform hook when a denied file (e.g. `.server.ts` in
|
|
252
|
-
* the client environment) is replaced in dev mode.
|
|
253
|
-
*
|
|
254
|
-
* The generated module imports mock-runtime for runtime diagnostics
|
|
255
|
-
* (error/warn on property access) and re-exports explicit named exports
|
|
256
|
-
* so that `import { foo } from './denied.server'` works.
|
|
257
|
-
*/
|
|
258
153
|
function generateDevSelfDenialModule(exportNames, runtimeId) {
|
|
259
154
|
const exportLines = generateExportLines(filterExportNames(exportNames));
|
|
260
155
|
return { code: `import mock from ${JSON.stringify(runtimeId)};
|
|
@@ -299,14 +194,7 @@ var MARKER_MODULE_RESULT = { code: "export {}" };
|
|
|
299
194
|
function loadMarkerModule() {
|
|
300
195
|
return MARKER_MODULE_RESULT;
|
|
301
196
|
}
|
|
302
|
-
function loadResolvedVirtualModule(id) {
|
|
303
|
-
if (id === RESOLVED_MOCK_MODULE_ID) return loadSilentMockModule();
|
|
304
|
-
if (id.startsWith(RESOLVED_MOCK_BUILD_PREFIX)) return loadSilentMockModule();
|
|
305
|
-
if (id.startsWith(RESOLVED_MOCK_EDGE_PREFIX)) return loadMockEdgeModule(id.slice(RESOLVED_MOCK_EDGE_PREFIX.length));
|
|
306
|
-
if (id.startsWith(RESOLVED_MOCK_RUNTIME_PREFIX)) return loadMockRuntimeModule(id.slice(RESOLVED_MOCK_RUNTIME_PREFIX.length));
|
|
307
|
-
if (id.startsWith(RESOLVED_MARKER_PREFIX)) return loadMarkerModule();
|
|
308
|
-
}
|
|
309
197
|
//#endregion
|
|
310
|
-
export {
|
|
198
|
+
export { RUNTIME_SUGGESTION_TEXT, generateDevSelfDenialModule, generateSelfContainedMockModule, loadMarkerModule, loadMockEdgeModule, loadMockRuntimeModule, loadSilentMockModule, makeMockEdgeModuleId, mockRuntimeModuleIdFromViolation };
|
|
311
199
|
|
|
312
200
|
//# sourceMappingURL=virtualModules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"virtualModules.js","names":[],"sources":["../../../src/import-protection/virtualModules.ts"],"sourcesContent":["import {\n MARKER_PREFIX,\n MOCK_BUILD_PREFIX,\n MOCK_EDGE_PREFIX,\n MOCK_MODULE_ID,\n MOCK_RUNTIME_PREFIX,\n} from './constants'\nimport { isValidExportName } from './analysis'\nimport { CLIENT_ENV_SUGGESTIONS } from './trace'\nimport { relativizePath } from './utils'\nimport type { ViolationInfo } from './trace'\n\nfunction toBase64Url(input: string): string {\n return Buffer.from(input, 'utf8').toString('base64url')\n}\n\nfunction fromBase64Url(input: string): string {\n return Buffer.from(input, 'base64url').toString('utf8')\n}\n\ntype MockAccessMode = 'error' | 'warn' | 'off'\n\nexport const RUNTIME_SUGGESTION_TEXT =\n 'Fix: ' +\n CLIENT_ENV_SUGGESTIONS.join('. ') +\n '. To disable these runtime diagnostics, set importProtection.mockAccess: \"off\".'\n\nexport function mockRuntimeModuleIdFromViolation(\n info: ViolationInfo,\n mode: MockAccessMode,\n root: string,\n): string {\n if (mode === 'off') return MOCK_MODULE_ID\n if (info.env !== 'client') return MOCK_MODULE_ID\n\n const rel = (p: string) => relativizePath(p, root)\n const trace = info.trace.map((s) => {\n const file = rel(s.file)\n if (s.line == null) return file\n return `${file}:${s.line}:${s.column ?? 1}`\n })\n\n const payload = {\n env: info.env,\n importer: info.importer,\n specifier: info.specifier,\n trace,\n mode,\n }\n\n return `${MOCK_RUNTIME_PREFIX}${toBase64Url(JSON.stringify(payload))}`\n}\n\nexport function makeMockEdgeModuleId(\n exports: Array<string>,\n runtimeId: string,\n): string {\n const payload = { exports, runtimeId }\n return `${MOCK_EDGE_PREFIX}${toBase64Url(JSON.stringify(payload))}`\n}\n\nfunction generateMockCode(diagnostics?: {\n meta: {\n env: string\n importer: string\n specifier: string\n trace: Array<unknown>\n }\n mode: 'error' | 'warn' | 'off'\n}): string {\n const fnName = diagnostics ? '__createMock' : 'createMock'\n const hasDiag = !!diagnostics\n\n const preamble = hasDiag\n ? `const __meta = ${JSON.stringify(diagnostics.meta)};\nconst __mode = ${JSON.stringify(diagnostics.mode)};\n\nconst __seen = new Set();\nfunction __report(action, accessPath) {\n if (__mode === 'off') return;\n const key = action + ':' + accessPath;\n if (__seen.has(key)) return;\n __seen.add(key);\n\n const traceLines = Array.isArray(__meta.trace) && __meta.trace.length\n ? \"\\\\n\\\\nTrace:\\\\n\" + __meta.trace.map((t, i) => ' ' + (i + 1) + '. ' + String(t)).join('\\\\n')\n : '';\n\n const msg =\n '[import-protection] Mocked import used in dev client\\\\n\\\\n' +\n 'Denied import: \"' + __meta.specifier + '\"\\\\n' +\n 'Importer: ' + __meta.importer + '\\\\n' +\n 'Access: ' + accessPath + ' (' + action + ')' +\n traceLines +\n '\\\\n\\\\n' + ${JSON.stringify(RUNTIME_SUGGESTION_TEXT)};\n\n const err = new Error(msg);\n if (__mode === 'warn') {\n console.warn(err);\n } else {\n console.error(err);\n }\n}\n`\n : ''\n\n const diagGetTraps = hasDiag\n ? `\n if (prop === Symbol.toPrimitive) {\n return () => {\n __report('toPrimitive', name);\n return '[import-protection mock]';\n };\n }\n if (prop === 'toString' || prop === 'valueOf' || prop === 'toJSON') {\n return () => {\n __report(String(prop), name);\n return '[import-protection mock]';\n };\n }`\n : ''\n\n const applyBody = hasDiag\n ? `__report('call', name + '()');\n return ${fnName}(name + '()');`\n : `return ${fnName}(name + '()');`\n\n const constructBody = hasDiag\n ? `__report('construct', 'new ' + name);\n return ${fnName}('new ' + name);`\n : `return ${fnName}('new ' + name);`\n\n const setTrap = hasDiag\n ? `\n set(_target, prop) {\n __report('set', name + '.' + String(prop));\n return true;\n },`\n : ''\n\n return `\n${preamble}/* @__NO_SIDE_EFFECTS__ */\nfunction ${fnName}(name) {\n const fn = function () {};\n fn.prototype.name = name;\n const children = Object.create(null);\n const proxy = new Proxy(fn, {\n get(_target, prop) {\n if (prop === '__esModule') return true;\n if (prop === 'default') return proxy;\n if (prop === 'caller') return null;\n if (prop === 'then') return (f) => Promise.resolve(f(proxy));\n if (prop === 'catch') return () => Promise.resolve(proxy);\n if (prop === 'finally') return (f) => { f(); return Promise.resolve(proxy); };${diagGetTraps}\n if (typeof prop === 'symbol') return undefined;\n if (!(prop in children)) {\n children[prop] = ${fnName}(name + '.' + prop);\n }\n return children[prop];\n },\n apply() {\n ${applyBody}\n },\n construct() {\n ${constructBody}\n },${setTrap}\n });\n return proxy;\n}\nconst mock = /* @__PURE__ */ ${fnName}('mock');\nexport default mock;\n`\n}\n\nexport function loadSilentMockModule(): { code: string } {\n return { code: generateMockCode() }\n}\n\nfunction filterExportNames(exports: ReadonlyArray<string>): Array<string> {\n return exports.filter((n) => n.length > 0 && n !== 'default')\n}\n\nfunction generateExportLines(names: ReadonlyArray<string>): Array<string> {\n const lines: Array<string> = []\n const stringExports: Array<{ alias: string; name: string }> = []\n\n for (let i = 0; i < names.length; i++) {\n const n = names[i]!\n if (isValidExportName(n)) {\n lines.push(`export const ${n} = mock.${n};`)\n } else {\n const alias = `__tss_str_${i}`\n lines.push(`const ${alias} = mock[${JSON.stringify(n)}];`)\n stringExports.push({ alias, name: n })\n }\n }\n\n if (stringExports.length > 0) {\n const reexports = stringExports\n .map((s) => `${s.alias} as ${JSON.stringify(s.name)}`)\n .join(', ')\n lines.push(`export { ${reexports} };`)\n }\n\n return lines\n}\n\nexport function generateSelfContainedMockModule(exportNames: Array<string>): {\n code: string\n} {\n const mockCode = generateMockCode()\n const exportLines = generateExportLines(filterExportNames(exportNames))\n\n return {\n code: `${mockCode}\n${exportLines.join('\\n')}\n`,\n }\n}\n\nexport function generateDevSelfDenialModule(\n exportNames: Array<string>,\n runtimeId: string,\n): { code: string } {\n const names = filterExportNames(exportNames)\n const exportLines = generateExportLines(names)\n\n return {\n code: `import mock from ${JSON.stringify(runtimeId)};\n${exportLines.join('\\n')}\nexport default mock;\n`,\n }\n}\n\nexport function loadMockEdgeModule(encodedPayload: string): { code: string } {\n let payload: { exports?: Array<string>; runtimeId?: string }\n\n try {\n payload = JSON.parse(fromBase64Url(encodedPayload)) as typeof payload\n } catch {\n payload = { exports: [] }\n }\n\n const names = filterExportNames(payload.exports ?? [])\n const runtimeId =\n typeof payload.runtimeId === 'string' && payload.runtimeId.length > 0\n ? payload.runtimeId\n : MOCK_MODULE_ID\n const exportLines = generateExportLines(names)\n\n return {\n code: `import mock from ${JSON.stringify(runtimeId)};\n${exportLines.join('\\n')}\nexport default mock;\n`,\n }\n}\n\nexport function loadMockRuntimeModule(encodedPayload: string): {\n code: string\n} {\n let payload: {\n mode?: string\n env?: string\n importer?: string\n specifier?: string\n trace?: Array<unknown>\n }\n\n try {\n payload = JSON.parse(fromBase64Url(encodedPayload)) as typeof payload\n } catch {\n payload = {}\n }\n\n const mode: 'error' | 'warn' | 'off' =\n payload.mode === 'warn' || payload.mode === 'off' ? payload.mode : 'error'\n\n return {\n code: generateMockCode({\n meta: {\n env: String(payload.env ?? ''),\n importer: String(payload.importer ?? ''),\n specifier: String(payload.specifier ?? ''),\n trace: Array.isArray(payload.trace) ? payload.trace : [],\n },\n mode,\n }),\n }\n}\n\nconst MARKER_MODULE_RESULT = { code: 'export {}' } as const\n\nexport function loadMarkerModule(): { code: string } {\n return MARKER_MODULE_RESULT\n}\n\nexport {\n MARKER_PREFIX,\n MOCK_BUILD_PREFIX,\n MOCK_EDGE_PREFIX,\n MOCK_MODULE_ID,\n MOCK_RUNTIME_PREFIX,\n}\n"],"mappings":"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"}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
1
|
export type { TanStackStartInputConfig } from './schema.js';
|
|
2
2
|
export type { TanStackStartCoreOptions } from './types.js';
|
|
3
|
-
export
|
|
4
|
-
export type { TanStackStartViteInputConfig } from './vite/schema.js';
|
|
5
|
-
export { START_ENVIRONMENT_NAMES, VITE_ENVIRONMENT_NAMES } from './constants.js';
|
|
6
|
-
export { createVirtualModule } from './vite/createVirtualModule.js';
|
|
7
|
-
export { tanStackStartVite } from './vite/plugin.js';
|
|
3
|
+
export { START_ENVIRONMENT_NAMES } from './constants.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -1,4 +1,2 @@
|
|
|
1
|
-
import { START_ENVIRONMENT_NAMES
|
|
2
|
-
|
|
3
|
-
import { tanStackStartVite } from "./vite/plugin.js";
|
|
4
|
-
export { START_ENVIRONMENT_NAMES, VITE_ENVIRONMENT_NAMES, createVirtualModule, tanStackStartVite };
|
|
1
|
+
import { START_ENVIRONMENT_NAMES } from "./constants.js";
|
|
2
|
+
export { START_ENVIRONMENT_NAMES };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { TanStackStartOutputConfig } from './schema.js';
|
|
2
|
+
export interface StartPostBuildAdapter {
|
|
3
|
+
getClientOutputDirectory: () => string;
|
|
4
|
+
prerender: (startConfig: TanStackStartOutputConfig) => Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export declare function postBuild({ startConfig, adapter, }: {
|
|
7
|
+
startConfig: TanStackStartOutputConfig;
|
|
8
|
+
adapter: StartPostBuildAdapter;
|
|
9
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { buildSitemap } from "./build-sitemap.js";
|
|
2
|
+
import { HEADERS } from "@tanstack/start-server-core";
|
|
3
|
+
//#region src/post-build.ts
|
|
4
|
+
async function postBuild({ startConfig, adapter }) {
|
|
5
|
+
if (startConfig.prerender?.enabled !== false) startConfig.prerender = {
|
|
6
|
+
...startConfig.prerender,
|
|
7
|
+
enabled: startConfig.prerender?.enabled ?? startConfig.pages.some((d) => typeof d === "string" ? false : !!d.prerender?.enabled)
|
|
8
|
+
};
|
|
9
|
+
if (startConfig.spa?.enabled) {
|
|
10
|
+
startConfig.prerender = {
|
|
11
|
+
...startConfig.prerender,
|
|
12
|
+
enabled: true
|
|
13
|
+
};
|
|
14
|
+
const maskUrl = new URL(startConfig.spa.maskPath, "http://localhost");
|
|
15
|
+
if (maskUrl.origin !== "http://localhost") throw new Error("spa.maskPath must be a path (no protocol/host)");
|
|
16
|
+
startConfig.pages.push({
|
|
17
|
+
path: maskUrl.toString().replace("http://localhost", ""),
|
|
18
|
+
prerender: {
|
|
19
|
+
...startConfig.spa.prerender,
|
|
20
|
+
headers: {
|
|
21
|
+
...startConfig.spa.prerender.headers,
|
|
22
|
+
[HEADERS.TSS_SHELL]: "true"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
sitemap: { exclude: true }
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (startConfig.prerender.enabled) await adapter.prerender(startConfig);
|
|
29
|
+
if (startConfig.sitemap?.enabled) buildSitemap({
|
|
30
|
+
startConfig,
|
|
31
|
+
publicDir: adapter.getClientOutputDirectory()
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { postBuild };
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=post-build.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"post-build.js","names":[],"sources":["../../src/post-build.ts"],"sourcesContent":["import { HEADERS } from '@tanstack/start-server-core'\nimport { buildSitemap } from './build-sitemap'\nimport type { TanStackStartOutputConfig } from './schema'\n\nexport interface StartPostBuildAdapter {\n getClientOutputDirectory: () => string\n prerender: (startConfig: TanStackStartOutputConfig) => Promise<void>\n}\n\nexport async function postBuild({\n startConfig,\n adapter,\n}: {\n startConfig: TanStackStartOutputConfig\n adapter: StartPostBuildAdapter\n}) {\n if (startConfig.prerender?.enabled !== false) {\n startConfig.prerender = {\n ...startConfig.prerender,\n enabled:\n startConfig.prerender?.enabled ??\n startConfig.pages.some((d) =>\n typeof d === 'string' ? false : !!d.prerender?.enabled,\n ),\n }\n }\n\n if (startConfig.spa?.enabled) {\n startConfig.prerender = {\n ...startConfig.prerender,\n enabled: true,\n }\n\n const maskUrl = new URL(startConfig.spa.maskPath, 'http://localhost')\n if (maskUrl.origin !== 'http://localhost') {\n throw new Error('spa.maskPath must be a path (no protocol/host)')\n }\n\n startConfig.pages.push({\n path: maskUrl.toString().replace('http://localhost', ''),\n prerender: {\n ...startConfig.spa.prerender,\n headers: {\n ...startConfig.spa.prerender.headers,\n [HEADERS.TSS_SHELL]: 'true',\n },\n },\n sitemap: {\n exclude: true,\n },\n })\n }\n\n if (startConfig.prerender.enabled) {\n await adapter.prerender(startConfig)\n }\n\n if (startConfig.sitemap?.enabled) {\n buildSitemap({\n startConfig,\n publicDir: adapter.getClientOutputDirectory(),\n })\n }\n}\n"],"mappings":";;;AASA,eAAsB,UAAU,EAC9B,aACA,WAIC;AACD,KAAI,YAAY,WAAW,YAAY,MACrC,aAAY,YAAY;EACtB,GAAG,YAAY;EACf,SACE,YAAY,WAAW,WACvB,YAAY,MAAM,MAAM,MACtB,OAAO,MAAM,WAAW,QAAQ,CAAC,CAAC,EAAE,WAAW,QAChD;EACJ;AAGH,KAAI,YAAY,KAAK,SAAS;AAC5B,cAAY,YAAY;GACtB,GAAG,YAAY;GACf,SAAS;GACV;EAED,MAAM,UAAU,IAAI,IAAI,YAAY,IAAI,UAAU,mBAAmB;AACrE,MAAI,QAAQ,WAAW,mBACrB,OAAM,IAAI,MAAM,iDAAiD;AAGnE,cAAY,MAAM,KAAK;GACrB,MAAM,QAAQ,UAAU,CAAC,QAAQ,oBAAoB,GAAG;GACxD,WAAW;IACT,GAAG,YAAY,IAAI;IACnB,SAAS;KACP,GAAG,YAAY,IAAI,UAAU;MAC5B,QAAQ,YAAY;KACtB;IACF;GACD,SAAS,EACP,SAAS,MACV;GACF,CAAC;;AAGJ,KAAI,YAAY,UAAU,QACxB,OAAM,QAAQ,UAAU,YAAY;AAGtC,KAAI,YAAY,SAAS,QACvB,cAAa;EACX;EACA,WAAW,QAAQ,0BAA0B;EAC9C,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Page, TanStackStartOutputConfig } from './schema.js';
|
|
2
|
+
export interface PrerenderHandler {
|
|
3
|
+
getClientOutputDirectory: () => string;
|
|
4
|
+
request: (path: string, options?: RequestInit) => Promise<Response>;
|
|
5
|
+
close?: () => Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export declare function prerender({ startConfig, handler, }: {
|
|
8
|
+
startConfig: TanStackStartOutputConfig;
|
|
9
|
+
handler: PrerenderHandler;
|
|
10
|
+
}): Promise<void>;
|
|
11
|
+
export declare function validateAndNormalizePrerenderPages(pages: Array<Page>, routerBaseUrl: URL): Array<Page>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { createLogger } from "./utils.js";
|
|
2
|
+
import { Queue } from "./queue.js";
|
|
3
|
+
import path from "pathe";
|
|
4
|
+
import { joinURL, withBase, withTrailingSlash, withoutBase } from "ufo";
|
|
5
|
+
import { promises } from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
//#region src/prerender.ts
|
|
8
|
+
var DEFAULT_RETRY_DELAY = 500;
|
|
9
|
+
async function prerender({ startConfig, handler }) {
|
|
10
|
+
const logger = createLogger("prerender");
|
|
11
|
+
logger.info("Prerendering pages...");
|
|
12
|
+
if (startConfig.prerender?.enabled) {
|
|
13
|
+
let pages = startConfig.pages.length ? startConfig.pages : [{ path: "/" }];
|
|
14
|
+
if (startConfig.prerender.autoStaticPathsDiscovery ?? true) {
|
|
15
|
+
const pagesMap = new Map(pages.map((item) => [item.path, item]));
|
|
16
|
+
const discoveredPages = globalThis.TSS_PRERENDABLE_PATHS || [];
|
|
17
|
+
for (const page of discoveredPages) if (!pagesMap.has(page.path)) pagesMap.set(page.path, page);
|
|
18
|
+
pages = Array.from(pagesMap.values());
|
|
19
|
+
}
|
|
20
|
+
startConfig.pages = pages;
|
|
21
|
+
}
|
|
22
|
+
const routerBasePath = joinURL("/", startConfig.router.basepath ?? "");
|
|
23
|
+
const routerBaseUrl = new URL(routerBasePath, "http://localhost");
|
|
24
|
+
startConfig.pages = validateAndNormalizePrerenderPages(startConfig.pages, routerBaseUrl);
|
|
25
|
+
try {
|
|
26
|
+
const pages = await prerenderPages({ outputDir: handler.getClientOutputDirectory() });
|
|
27
|
+
logger.info(`Prerendered ${pages.length} pages:`);
|
|
28
|
+
pages.forEach((page) => {
|
|
29
|
+
logger.info(`- ${page}`);
|
|
30
|
+
});
|
|
31
|
+
} catch (error) {
|
|
32
|
+
logger.error(error);
|
|
33
|
+
throw error;
|
|
34
|
+
} finally {
|
|
35
|
+
await handler.close?.();
|
|
36
|
+
}
|
|
37
|
+
function extractLinks(html) {
|
|
38
|
+
const linkRegex = /<a[^>]+href=["']([^"']+)["'][^>]*>/g;
|
|
39
|
+
const links = [];
|
|
40
|
+
let match;
|
|
41
|
+
while ((match = linkRegex.exec(html)) !== null) {
|
|
42
|
+
const href = match[1];
|
|
43
|
+
if (href && (href.startsWith("/") || href.startsWith("./"))) links.push(href);
|
|
44
|
+
}
|
|
45
|
+
return links;
|
|
46
|
+
}
|
|
47
|
+
async function prerenderPages({ outputDir }) {
|
|
48
|
+
const seen = /* @__PURE__ */ new Set();
|
|
49
|
+
const prerendered = /* @__PURE__ */ new Set();
|
|
50
|
+
const retriesByPath = /* @__PURE__ */ new Map();
|
|
51
|
+
const concurrency = startConfig.prerender?.concurrency ?? os.cpus().length;
|
|
52
|
+
logger.info(`Concurrency: ${concurrency}`);
|
|
53
|
+
const queue = new Queue({ concurrency });
|
|
54
|
+
const routerBasePath = joinURL("/", startConfig.router.basepath ?? "");
|
|
55
|
+
const routerBaseUrl = new URL(routerBasePath, "http://localhost");
|
|
56
|
+
startConfig.pages = validateAndNormalizePrerenderPages(startConfig.pages, routerBaseUrl);
|
|
57
|
+
startConfig.pages.forEach((page) => addCrawlPageTask(page));
|
|
58
|
+
if (queue.isSettled()) {
|
|
59
|
+
logger.info("No pages matched prerender filter; skipping.");
|
|
60
|
+
return Array.from(prerendered);
|
|
61
|
+
}
|
|
62
|
+
await queue.start();
|
|
63
|
+
return Array.from(prerendered);
|
|
64
|
+
function addCrawlPageTask(page) {
|
|
65
|
+
if (seen.has(page.path)) return;
|
|
66
|
+
seen.add(page.path);
|
|
67
|
+
if (page.fromCrawl) startConfig.pages.push(page);
|
|
68
|
+
if (!(page.prerender?.enabled ?? true)) return;
|
|
69
|
+
if (startConfig.prerender?.filter && !startConfig.prerender.filter(page)) return;
|
|
70
|
+
const prerenderOptions = {
|
|
71
|
+
...startConfig.prerender,
|
|
72
|
+
...page.prerender
|
|
73
|
+
};
|
|
74
|
+
queue.add(async () => {
|
|
75
|
+
logger.info(`Crawling: ${page.path}`);
|
|
76
|
+
const retries = retriesByPath.get(page.path) || 0;
|
|
77
|
+
try {
|
|
78
|
+
const res = await requestWithRedirects(withTrailingSlash(withBase(page.path, routerBasePath)), { headers: { ...prerenderOptions.headers ?? {} } }, prerenderOptions.maxRedirects);
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
if (isRedirectResponse(res)) logger.warn(`Max redirects reached for ${page.path}`);
|
|
81
|
+
throw new Error(`Failed to fetch ${page.path}: ${res.statusText}`, { cause: res });
|
|
82
|
+
}
|
|
83
|
+
const cleanPagePath = (prerenderOptions.outputPath || page.path).split(/[?#]/)[0];
|
|
84
|
+
const contentType = res.headers.get("content-type") || "";
|
|
85
|
+
const isImplicitHTML = !cleanPagePath.endsWith(".html") && contentType.includes("html");
|
|
86
|
+
const routeWithIndex = cleanPagePath.endsWith("/") ? cleanPagePath + "index" : cleanPagePath;
|
|
87
|
+
const isSpaShell = startConfig.spa?.prerender.outputPath === cleanPagePath;
|
|
88
|
+
let htmlPath;
|
|
89
|
+
if (isSpaShell) htmlPath = cleanPagePath + ".html";
|
|
90
|
+
else if (cleanPagePath.endsWith("/") || (prerenderOptions.autoSubfolderIndex ?? true)) htmlPath = joinURL(cleanPagePath, "index.html");
|
|
91
|
+
else htmlPath = cleanPagePath + ".html";
|
|
92
|
+
const filename = withoutBase(isImplicitHTML ? htmlPath : routeWithIndex, routerBasePath);
|
|
93
|
+
const html = await res.text();
|
|
94
|
+
const filepath = path.join(outputDir, filename);
|
|
95
|
+
await promises.mkdir(path.dirname(filepath), { recursive: true });
|
|
96
|
+
await promises.writeFile(filepath, html);
|
|
97
|
+
prerendered.add(page.path);
|
|
98
|
+
const newPage = await prerenderOptions.onSuccess?.({
|
|
99
|
+
page,
|
|
100
|
+
html
|
|
101
|
+
});
|
|
102
|
+
if (newPage) Object.assign(page, newPage);
|
|
103
|
+
if (prerenderOptions.crawlLinks ?? true) {
|
|
104
|
+
const links = extractLinks(html);
|
|
105
|
+
for (const link of links) addCrawlPageTask({
|
|
106
|
+
path: link,
|
|
107
|
+
fromCrawl: true
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (retries < (prerenderOptions.retryCount ?? 0)) {
|
|
112
|
+
const retryDelay = normalizeRetryDelay(prerenderOptions.retryDelay);
|
|
113
|
+
logger.warn(`Encountered error, retrying: ${page.path} in ${retryDelay}ms`);
|
|
114
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
115
|
+
retriesByPath.set(page.path, retries + 1);
|
|
116
|
+
addCrawlPageTask(page);
|
|
117
|
+
} else if (prerenderOptions.failOnError ?? true) throw error;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function normalizeRetryDelay(value) {
|
|
123
|
+
const retryDelay = Number(value);
|
|
124
|
+
if (!Number.isFinite(retryDelay) || retryDelay < 0) return DEFAULT_RETRY_DELAY;
|
|
125
|
+
return Math.trunc(retryDelay);
|
|
126
|
+
}
|
|
127
|
+
async function requestWithRedirects(path, options, maxRedirects = 5) {
|
|
128
|
+
const response = await handler.request(path, options);
|
|
129
|
+
if (isRedirectResponse(response) && maxRedirects > 0) {
|
|
130
|
+
const location = response.headers.get("location");
|
|
131
|
+
if (location.startsWith("http://localhost") || location.startsWith("/")) return requestWithRedirects(location.replace("http://localhost", ""), options, maxRedirects - 1);
|
|
132
|
+
logger.warn(`Skipping redirect to external location: ${location}`);
|
|
133
|
+
}
|
|
134
|
+
return response;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function isRedirectResponse(res) {
|
|
138
|
+
return res.status >= 300 && res.status < 400 && res.headers.get("location");
|
|
139
|
+
}
|
|
140
|
+
function validateAndNormalizePrerenderPages(pages, routerBaseUrl) {
|
|
141
|
+
return pages.map((page) => {
|
|
142
|
+
let url;
|
|
143
|
+
try {
|
|
144
|
+
url = new URL(page.path, routerBaseUrl);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
throw new Error(`prerender page path must be relative: ${page.path}`, { cause: err });
|
|
147
|
+
}
|
|
148
|
+
if (url.origin !== "http://localhost") throw new Error(`prerender page path must be relative: ${page.path}`);
|
|
149
|
+
const decodedPathname = decodeURIComponent(url.pathname);
|
|
150
|
+
return {
|
|
151
|
+
...page,
|
|
152
|
+
path: decodedPathname + url.search + url.hash
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
export { prerender };
|
|
158
|
+
|
|
159
|
+
//# sourceMappingURL=prerender.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prerender.js","names":[],"sources":["../../src/prerender.ts"],"sourcesContent":["import { promises as fsp } from 'node:fs'\nimport os from 'node:os'\nimport path from 'pathe'\nimport { joinURL, withBase, withTrailingSlash, withoutBase } from 'ufo'\nimport { createLogger } from './utils'\nimport { Queue } from './queue'\nimport type { Page, TanStackStartOutputConfig } from './schema'\n\nconst DEFAULT_RETRY_DELAY = 500\n\nexport interface PrerenderHandler {\n getClientOutputDirectory: () => string\n request: (path: string, options?: RequestInit) => Promise<Response>\n close?: () => Promise<void>\n}\n\nexport async function prerender({\n startConfig,\n handler,\n}: {\n startConfig: TanStackStartOutputConfig\n handler: PrerenderHandler\n}) {\n const logger = createLogger('prerender')\n logger.info('Prerendering pages...')\n\n if (startConfig.prerender?.enabled) {\n let pages = startConfig.pages.length ? startConfig.pages : [{ path: '/' }]\n\n if (startConfig.prerender.autoStaticPathsDiscovery ?? true) {\n const pagesMap = new Map(pages.map((item) => [item.path, item]))\n const discoveredPages = globalThis.TSS_PRERENDABLE_PATHS || []\n\n for (const page of discoveredPages) {\n if (!pagesMap.has(page.path)) {\n pagesMap.set(page.path, page)\n }\n }\n\n pages = Array.from(pagesMap.values())\n }\n\n startConfig.pages = pages\n }\n\n const routerBasePath = joinURL('/', startConfig.router.basepath ?? '')\n const routerBaseUrl = new URL(routerBasePath, 'http://localhost')\n\n startConfig.pages = validateAndNormalizePrerenderPages(\n startConfig.pages,\n routerBaseUrl,\n )\n\n try {\n const pages = await prerenderPages({\n outputDir: handler.getClientOutputDirectory(),\n })\n\n logger.info(`Prerendered ${pages.length} pages:`)\n pages.forEach((page) => {\n logger.info(`- ${page}`)\n })\n } catch (error) {\n logger.error(error)\n throw error\n } finally {\n await handler.close?.()\n }\n\n function extractLinks(html: string): Array<string> {\n const linkRegex = /<a[^>]+href=[\"']([^\"']+)[\"'][^>]*>/g\n const links: Array<string> = []\n let match: RegExpExecArray | null\n\n while ((match = linkRegex.exec(html)) !== null) {\n const href = match[1]\n if (href && (href.startsWith('/') || href.startsWith('./'))) {\n links.push(href)\n }\n }\n\n return links\n }\n\n async function prerenderPages({ outputDir }: { outputDir: string }) {\n const seen = new Set<string>()\n const prerendered = new Set<string>()\n const retriesByPath = new Map<string, number>()\n const concurrency = startConfig.prerender?.concurrency ?? os.cpus().length\n logger.info(`Concurrency: ${concurrency}`)\n const queue = new Queue({ concurrency })\n const routerBasePath = joinURL('/', startConfig.router.basepath ?? '')\n const routerBaseUrl = new URL(routerBasePath, 'http://localhost')\n\n startConfig.pages = validateAndNormalizePrerenderPages(\n startConfig.pages,\n routerBaseUrl,\n )\n\n startConfig.pages.forEach((page) => addCrawlPageTask(page))\n\n if (queue.isSettled()) {\n logger.info('No pages matched prerender filter; skipping.')\n return Array.from(prerendered)\n }\n\n await queue.start()\n\n return Array.from(prerendered)\n\n function addCrawlPageTask(page: Page) {\n if (seen.has(page.path)) return\n\n seen.add(page.path)\n\n if (page.fromCrawl) {\n startConfig.pages.push(page)\n }\n\n if (!(page.prerender?.enabled ?? true)) return\n\n if (\n startConfig.prerender?.filter &&\n !startConfig.prerender.filter(page)\n ) {\n return\n }\n\n const prerenderOptions = {\n ...startConfig.prerender,\n ...page.prerender,\n }\n\n queue.add(async () => {\n logger.info(`Crawling: ${page.path}`)\n const retries = retriesByPath.get(page.path) || 0\n\n try {\n const res = await requestWithRedirects(\n withTrailingSlash(withBase(page.path, routerBasePath)),\n {\n headers: {\n ...(prerenderOptions.headers ?? {}),\n },\n },\n prerenderOptions.maxRedirects,\n )\n\n if (!res.ok) {\n if (isRedirectResponse(res)) {\n logger.warn(`Max redirects reached for ${page.path}`)\n }\n\n throw new Error(`Failed to fetch ${page.path}: ${res.statusText}`, {\n cause: res,\n })\n }\n\n const cleanPagePath = (\n prerenderOptions.outputPath || page.path\n ).split(/[?#]/)[0]!\n\n const contentType = res.headers.get('content-type') || ''\n const isImplicitHTML =\n !cleanPagePath.endsWith('.html') && contentType.includes('html')\n\n const routeWithIndex = cleanPagePath.endsWith('/')\n ? cleanPagePath + 'index'\n : cleanPagePath\n\n const isSpaShell =\n startConfig.spa?.prerender.outputPath === cleanPagePath\n\n let htmlPath: string\n if (isSpaShell) {\n htmlPath = cleanPagePath + '.html'\n } else if (\n cleanPagePath.endsWith('/') ||\n (prerenderOptions.autoSubfolderIndex ?? true)\n ) {\n htmlPath = joinURL(cleanPagePath, 'index.html')\n } else {\n htmlPath = cleanPagePath + '.html'\n }\n\n const filename = withoutBase(\n isImplicitHTML ? htmlPath : routeWithIndex,\n routerBasePath,\n )\n\n const html = await res.text()\n const filepath = path.join(outputDir, filename)\n\n await fsp.mkdir(path.dirname(filepath), {\n recursive: true,\n })\n\n await fsp.writeFile(filepath, html)\n\n prerendered.add(page.path)\n\n const newPage = await prerenderOptions.onSuccess?.({ page, html })\n\n if (newPage) {\n Object.assign(page, newPage)\n }\n\n if (prerenderOptions.crawlLinks ?? true) {\n const links = extractLinks(html)\n for (const link of links) {\n addCrawlPageTask({ path: link, fromCrawl: true })\n }\n }\n } catch (error) {\n if (retries < (prerenderOptions.retryCount ?? 0)) {\n const retryDelay = normalizeRetryDelay(prerenderOptions.retryDelay)\n logger.warn(\n `Encountered error, retrying: ${page.path} in ${retryDelay}ms`,\n )\n await new Promise((resolve) => setTimeout(resolve, retryDelay))\n retriesByPath.set(page.path, retries + 1)\n addCrawlPageTask(page)\n } else if (prerenderOptions.failOnError ?? true) {\n throw error\n }\n }\n })\n }\n }\n\n function normalizeRetryDelay(value: number | undefined): number {\n const retryDelay = Number(value)\n\n if (!Number.isFinite(retryDelay) || retryDelay < 0) {\n return DEFAULT_RETRY_DELAY\n }\n\n return Math.trunc(retryDelay)\n }\n\n async function requestWithRedirects(\n path: string,\n options?: RequestInit,\n maxRedirects: number = 5,\n ): Promise<Response> {\n const response = await handler.request(path, options)\n\n if (isRedirectResponse(response) && maxRedirects > 0) {\n const location = response.headers.get('location')!\n\n if (location.startsWith('http://localhost') || location.startsWith('/')) {\n const nextPath = location.replace('http://localhost', '')\n return requestWithRedirects(nextPath, options, maxRedirects - 1)\n }\n\n logger.warn(`Skipping redirect to external location: ${location}`)\n }\n\n return response\n }\n}\n\nfunction isRedirectResponse(res: Response) {\n return res.status >= 300 && res.status < 400 && res.headers.get('location')\n}\n\nexport function validateAndNormalizePrerenderPages(\n pages: Array<Page>,\n routerBaseUrl: URL,\n): Array<Page> {\n return pages.map((page) => {\n let url: URL\n try {\n url = new URL(page.path, routerBaseUrl)\n } catch (err) {\n throw new Error(`prerender page path must be relative: ${page.path}`, {\n cause: err,\n })\n }\n\n if (url.origin !== 'http://localhost') {\n throw new Error(`prerender page path must be relative: ${page.path}`)\n }\n\n const decodedPathname = decodeURIComponent(url.pathname)\n\n return {\n ...page,\n path: decodedPathname + url.search + url.hash,\n }\n })\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { RsbuildConfig } from '@rsbuild/core';
|
|
2
|
+
type ServerSetupFn = Extract<NonNullable<NonNullable<RsbuildConfig['server']>['setup']>, (...args: Array<any>) => any>;
|
|
3
|
+
/**
|
|
4
|
+
* Returns a `server.setup` function for rsbuild v2.
|
|
5
|
+
*
|
|
6
|
+
* Two middleware positions are used:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Setup body** (BEFORE built-ins): Intercepts `/_serverFn/` URLs so
|
|
9
|
+
* they never reach rsbuild's htmlFallback/htmlCompletion middleware,
|
|
10
|
+
* which can swallow long base64 function IDs.
|
|
11
|
+
*
|
|
12
|
+
* 2. **Returned callback** (AFTER built-ins, BEFORE fallback): Handles
|
|
13
|
+
* all remaining SSR requests (page navigations). This position lets
|
|
14
|
+
* rsbuild's asset middleware serve compiled JS/CSS first.
|
|
15
|
+
*
|
|
16
|
+
* See rsbuild source: devMiddlewares.ts `applyDefaultMiddlewares()`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createServerSetup(opts: {
|
|
19
|
+
serverFnBasePath: string;
|
|
20
|
+
}): ServerSetupFn;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { RSBUILD_ENVIRONMENT_NAMES } from "./planning.js";
|
|
2
|
+
import { NodeRequest, sendNodeResponse } from "srvx/node";
|
|
3
|
+
//#region src/rsbuild/dev-server.ts
|
|
4
|
+
/**
|
|
5
|
+
* Returns a `server.setup` function for rsbuild v2.
|
|
6
|
+
*
|
|
7
|
+
* Two middleware positions are used:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Setup body** (BEFORE built-ins): Intercepts `/_serverFn/` URLs so
|
|
10
|
+
* they never reach rsbuild's htmlFallback/htmlCompletion middleware,
|
|
11
|
+
* which can swallow long base64 function IDs.
|
|
12
|
+
*
|
|
13
|
+
* 2. **Returned callback** (AFTER built-ins, BEFORE fallback): Handles
|
|
14
|
+
* all remaining SSR requests (page navigations). This position lets
|
|
15
|
+
* rsbuild's asset middleware serve compiled JS/CSS first.
|
|
16
|
+
*
|
|
17
|
+
* See rsbuild source: devMiddlewares.ts `applyDefaultMiddlewares()`.
|
|
18
|
+
*/
|
|
19
|
+
function createServerSetup(opts) {
|
|
20
|
+
return (context) => {
|
|
21
|
+
if (context.action !== "dev") return () => {};
|
|
22
|
+
const serverFnBase = opts.serverFnBasePath;
|
|
23
|
+
const handleSSR = async (req, res, next) => {
|
|
24
|
+
const ssrEnv = context.server.environments[RSBUILD_ENVIRONMENT_NAMES.server];
|
|
25
|
+
if (!ssrEnv) {
|
|
26
|
+
console.error(`[tanstack-start] SSR environment "${RSBUILD_ENVIRONMENT_NAMES.server}" not found`);
|
|
27
|
+
return next();
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const serverEntry = await ssrEnv.loadBundle("index");
|
|
31
|
+
if (req.originalUrl) req.url = req.originalUrl;
|
|
32
|
+
const webReq = new NodeRequest({
|
|
33
|
+
req,
|
|
34
|
+
res
|
|
35
|
+
});
|
|
36
|
+
return sendNodeResponse(res, await serverEntry.default.fetch(webReq));
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.error("[tanstack-start] SSR error:", e);
|
|
39
|
+
if (new NodeRequest({
|
|
40
|
+
req,
|
|
41
|
+
res
|
|
42
|
+
}).headers.get("content-type")?.includes("application/json")) return sendNodeResponse(res, new Response(JSON.stringify({
|
|
43
|
+
status: 500,
|
|
44
|
+
error: "Internal Server Error",
|
|
45
|
+
message: "An unexpected error occurred. Please try again later.",
|
|
46
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
47
|
+
}, null, 2), {
|
|
48
|
+
status: 500,
|
|
49
|
+
headers: { "Content-Type": "application/json" }
|
|
50
|
+
}));
|
|
51
|
+
return sendNodeResponse(res, new Response(`<!DOCTYPE html>
|
|
52
|
+
<html lang="en">
|
|
53
|
+
<head><meta charset="UTF-8" /><title>Error</title></head>
|
|
54
|
+
<body>
|
|
55
|
+
<h1>Internal Server Error</h1>
|
|
56
|
+
<pre>${e instanceof Error ? e.message : String(e)}</pre>
|
|
57
|
+
</body>
|
|
58
|
+
</html>`, {
|
|
59
|
+
status: 500,
|
|
60
|
+
headers: { "Content-Type": "text/html" }
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
context.server.middlewares.use(async (req, res, next) => {
|
|
65
|
+
if ((req.url || "/").startsWith(serverFnBase)) return handleSSR(req, res, next);
|
|
66
|
+
return next();
|
|
67
|
+
});
|
|
68
|
+
return () => {
|
|
69
|
+
context.server.middlewares.use(handleSSR);
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
export { createServerSetup };
|
|
75
|
+
|
|
76
|
+
//# sourceMappingURL=dev-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-server.js","names":[],"sources":["../../../src/rsbuild/dev-server.ts"],"sourcesContent":["import { NodeRequest, sendNodeResponse } from 'srvx/node'\nimport { RSBUILD_ENVIRONMENT_NAMES } from './planning'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { RsbuildConfig } from '@rsbuild/core'\n\ntype ServerSetupFn = Extract<\n NonNullable<NonNullable<RsbuildConfig['server']>['setup']>,\n (...args: Array<any>) => any\n>\ntype SSRMiddleware = (\n req: IncomingMessage & { originalUrl?: string },\n res: ServerResponse,\n next: () => void,\n) => Promise<void>\n\n/**\n * Returns a `server.setup` function for rsbuild v2.\n *\n * Two middleware positions are used:\n *\n * 1. **Setup body** (BEFORE built-ins): Intercepts `/_serverFn/` URLs so\n * they never reach rsbuild's htmlFallback/htmlCompletion middleware,\n * which can swallow long base64 function IDs.\n *\n * 2. **Returned callback** (AFTER built-ins, BEFORE fallback): Handles\n * all remaining SSR requests (page navigations). This position lets\n * rsbuild's asset middleware serve compiled JS/CSS first.\n *\n * See rsbuild source: devMiddlewares.ts `applyDefaultMiddlewares()`.\n */\nexport function createServerSetup(opts: {\n serverFnBasePath: string\n}): ServerSetupFn {\n return (context) => {\n // Only install SSR middleware in dev mode\n if (context.action !== 'dev') {\n return () => {}\n }\n\n const serverFnBase = opts.serverFnBasePath\n\n const handleSSR: SSRMiddleware = async (req, res, next) => {\n const ssrEnv =\n context.server.environments[RSBUILD_ENVIRONMENT_NAMES.server]\n\n if (!ssrEnv) {\n console.error(\n `[tanstack-start] SSR environment \"${RSBUILD_ENVIRONMENT_NAMES.server}\" not found`,\n )\n return next()\n }\n\n try {\n const serverEntry = await ssrEnv.loadBundle<{\n default: { fetch: (req: Request) => Promise<Response> }\n }>('index')\n\n // Restore the original URL (rsbuild may rewrite to /index.html)\n if (req.originalUrl) {\n req.url = req.originalUrl\n }\n\n const webReq = new NodeRequest({ req, res })\n const webRes = await serverEntry.default.fetch(webReq)\n return sendNodeResponse(res, webRes)\n } catch (e) {\n console.error('[tanstack-start] SSR error:', e)\n\n const webReq = new NodeRequest({ req, res })\n if (webReq.headers.get('content-type')?.includes('application/json')) {\n return sendNodeResponse(\n res,\n new Response(\n JSON.stringify(\n {\n status: 500,\n error: 'Internal Server Error',\n message:\n 'An unexpected error occurred. Please try again later.',\n timestamp: new Date().toISOString(),\n },\n null,\n 2,\n ),\n {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n },\n ),\n )\n }\n\n return sendNodeResponse(\n res,\n new Response(\n `<!DOCTYPE html>\n<html lang=\"en\">\n <head><meta charset=\"UTF-8\" /><title>Error</title></head>\n <body>\n <h1>Internal Server Error</h1>\n <pre>${e instanceof Error ? e.message : String(e)}</pre>\n </body>\n</html>`,\n {\n status: 500,\n headers: { 'Content-Type': 'text/html' },\n },\n ),\n )\n }\n }\n\n // Position 1: BEFORE built-ins — intercept server function calls\n // early so they are not swallowed by htmlFallback or assetsMiddleware.\n context.server.middlewares.use(async (req, res, next) => {\n const url = req.url || '/'\n if (url.startsWith(serverFnBase)) {\n return handleSSR(req, res, next)\n }\n return next()\n })\n\n // Position 2: AFTER built-ins, before fallback — SSR catch-all for\n // page navigations. Assets are already handled by rsbuild middleware.\n return () => {\n context.server.middlewares.use(handleSSR)\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA8BA,SAAgB,kBAAkB,MAEhB;AAChB,SAAQ,YAAY;AAElB,MAAI,QAAQ,WAAW,MACrB,cAAa;EAGf,MAAM,eAAe,KAAK;EAE1B,MAAM,YAA2B,OAAO,KAAK,KAAK,SAAS;GACzD,MAAM,SACJ,QAAQ,OAAO,aAAa,0BAA0B;AAExD,OAAI,CAAC,QAAQ;AACX,YAAQ,MACN,qCAAqC,0BAA0B,OAAO,aACvE;AACD,WAAO,MAAM;;AAGf,OAAI;IACF,MAAM,cAAc,MAAM,OAAO,WAE9B,QAAQ;AAGX,QAAI,IAAI,YACN,KAAI,MAAM,IAAI;IAGhB,MAAM,SAAS,IAAI,YAAY;KAAE;KAAK;KAAK,CAAC;AAE5C,WAAO,iBAAiB,KADT,MAAM,YAAY,QAAQ,MAAM,OAAO,CAClB;YAC7B,GAAG;AACV,YAAQ,MAAM,+BAA+B,EAAE;AAG/C,QADe,IAAI,YAAY;KAAE;KAAK;KAAK,CAAC,CACjC,QAAQ,IAAI,eAAe,EAAE,SAAS,mBAAmB,CAClE,QAAO,iBACL,KACA,IAAI,SACF,KAAK,UACH;KACE,QAAQ;KACR,OAAO;KACP,SACE;KACF,4BAAW,IAAI,MAAM,EAAC,aAAa;KACpC,EACD,MACA,EACD,EACD;KACE,QAAQ;KACR,SAAS,EAAE,gBAAgB,oBAAoB;KAChD,CACF,CACF;AAGH,WAAO,iBACL,KACA,IAAI,SACF;;;;;WAKD,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,CAAC;;UAG1C;KACE,QAAQ;KACR,SAAS,EAAE,gBAAgB,aAAa;KACzC,CACF,CACF;;;AAML,UAAQ,OAAO,YAAY,IAAI,OAAO,KAAK,KAAK,SAAS;AAEvD,QADY,IAAI,OAAO,KACf,WAAW,aAAa,CAC9B,QAAO,UAAU,KAAK,KAAK,KAAK;AAElC,UAAO,MAAM;IACb;AAIF,eAAa;AACX,WAAQ,OAAO,YAAY,IAAI,UAAU"}
|