catalyst-core 0.3.0-beta.1 → 0.3.0-beta.3
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/globals.d.ts +7 -0
- package/dist/native/androidProject/app/build.gradle.kts +45 -4
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +86 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +8 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FrameworkServerUtils.kt +173 -13
- package/dist/native/androidProject/settings.gradle.kts +10 -0
- package/dist/native/androidSetup.js +8 -6
- package/dist/native/bridge/constants/NativeInterfaces.js +15 -1
- package/dist/native/bridge/hooks/index.js +7 -0
- package/dist/native/bridge/hooks/useAI.js +94 -0
- package/dist/native/buildAndroid/assets.js +1 -1
- package/dist/native/buildAndroid/index.js +32 -0
- package/dist/native/buildIos/assets.js +1 -1
- package/dist/offline/catalyst-sw.js +1 -0
- package/dist/scripts/build.js +15 -11
- package/dist/scripts/generateOfflineManifest.js +1 -0
- package/dist/scripts/scriptUtils.js +1 -0
- package/dist/scripts/serve.js +24 -22
- package/dist/scripts/start.js +1 -3
- package/dist/server/expressServer.js +47 -0
- package/dist/server/renderer/document/Head.jsx +2 -0
- package/dist/server/renderer/extract.js +1 -0
- package/dist/server/renderer/handler.jsx +29 -11
- package/dist/vite/inject-cache-key-plugin.js +7 -0
- package/dist/vite/loadCustomViteConfig.js +1 -0
- package/dist/vite/manifest-categorization-plugin.js +2 -0
- package/dist/vite/node-loader.mjs +6 -0
- package/dist/vite/vite.config.js +83 -1
- package/mcp_v2/knowledge-base.json +52 -4
- package/mcp_v2/mcp.js +36 -4
- package/mcp_v2/tools/build.js +46 -18
- package/mcp_v2/tools/github.js +11 -0
- package/package.json +3 -3
|
@@ -494,7 +494,7 @@ function createAssetsPhase(ctx) {
|
|
|
494
494
|
let manifestContent = fs.readFileSync(manifestPath, "utf8");
|
|
495
495
|
const metadataNames = ["default_notification_channel_id", "default_notification_icon", "default_notification_color", "com.google.firebase.messaging.default_notification_channel_id", "com.google.firebase.messaging.default_notification_icon", "com.google.firebase.messaging.default_notification_sound"];
|
|
496
496
|
for (const metadataName of metadataNames) {
|
|
497
|
-
const metadataRegex = new RegExp(`\\s*<meta-data\\s+[
|
|
497
|
+
const metadataRegex = new RegExp(`\\s*<meta-data\\s+[^>]*?android:name="${metadataName}"[^>]*?\\/>`, "gi");
|
|
498
498
|
manifestContent = manifestContent.replace(metadataRegex, "");
|
|
499
499
|
}
|
|
500
500
|
const serviceRegex = /\s*<!--\s*Push Notification Service\s*-->\s*<service[^>]*android:name="[^"]*PushNotificationUtils"[\s\S]*?<\/service>/gi;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const path = require("path");
|
|
4
|
+
const fs = require("fs");
|
|
4
5
|
const {
|
|
5
6
|
runCommand,
|
|
6
7
|
runInteractiveCommand
|
|
@@ -75,6 +76,35 @@ function createAndroidBuild(config) {
|
|
|
75
76
|
moveApkToOutputPath,
|
|
76
77
|
buildSignedAAB
|
|
77
78
|
} = createBuildPhase(ctx);
|
|
79
|
+
function syncAIPackageIfEnabled(wvConfig, progress) {
|
|
80
|
+
const aiEnabled = wvConfig?.ai?.enabled === true;
|
|
81
|
+
if (!aiEnabled) return;
|
|
82
|
+
const appNodeModules = path.join(process.cwd(), "node_modules");
|
|
83
|
+
const aiPackageSrc = path.join(appNodeModules, "catalyst-ai");
|
|
84
|
+
if (!fs.existsSync(aiPackageSrc)) {
|
|
85
|
+
progress.log("ai.enabled=true but catalyst-ai not found in node_modules — skipping native AI sync", "warning");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const destDir = path.join(pwd, "node_modules", "catalyst-ai");
|
|
89
|
+
fs.mkdirSync(destDir, {
|
|
90
|
+
recursive: true
|
|
91
|
+
});
|
|
92
|
+
copyDirSync(aiPackageSrc, destDir);
|
|
93
|
+
progress.log("catalyst-ai synced to dist/native/node_modules for Gradle", "success");
|
|
94
|
+
}
|
|
95
|
+
function copyDirSync(src, dest) {
|
|
96
|
+
fs.mkdirSync(dest, {
|
|
97
|
+
recursive: true
|
|
98
|
+
});
|
|
99
|
+
for (const entry of fs.readdirSync(src, {
|
|
100
|
+
withFileTypes: true
|
|
101
|
+
})) {
|
|
102
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
103
|
+
const s = path.join(src, entry.name); // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
104
|
+
const d = path.join(dest, entry.name); // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
105
|
+
entry.isDirectory() ? copyDirSync(s, d) : fs.copyFileSync(s, d);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
78
108
|
async function buildAndroidApp() {
|
|
79
109
|
let androidConfig = null;
|
|
80
110
|
let targetDevice = null;
|
|
@@ -133,6 +163,7 @@ function createAndroidBuild(config) {
|
|
|
133
163
|
});
|
|
134
164
|
await processNotifications(wvConfig);
|
|
135
165
|
progress.log(`Build optimization: ${buildOptimisation ? "Enabled" : "Disabled"}`, "info");
|
|
166
|
+
syncAIPackageIfEnabled(wvConfig, progress);
|
|
136
167
|
progress.complete("copyAssets");
|
|
137
168
|
let movedApkPath = null;
|
|
138
169
|
if (buildType === "release") {
|
|
@@ -352,6 +383,7 @@ function createAndroidBuild(config) {
|
|
|
352
383
|
log: (message, status = "info") => progress.log(message, status)
|
|
353
384
|
});
|
|
354
385
|
await processNotifications(wvConfig);
|
|
386
|
+
syncAIPackageIfEnabled(wvConfig, progress);
|
|
355
387
|
progress.log("✅ buildAndroidForTesting complete — project ready for gradlew test", "success");
|
|
356
388
|
return {
|
|
357
389
|
success: true
|
|
@@ -123,7 +123,7 @@ module.exports = function createAssetsPhase(ctx) {
|
|
|
123
123
|
|
|
124
124
|
// ─── Notification assets ─────────────────────────────────────────────────
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
function validateNotificationAsset(filePath, assetType) {
|
|
127
127
|
const stats = fs.statSync(filePath);
|
|
128
128
|
const fileSizeKB = stats.size / 1024;
|
|
129
129
|
if (assetType === "icon" && fileSizeKB > 100) progress.log(`Warning: Notification icon ${path.basename(filePath)} is ${fileSizeKB.toFixed(1)}KB (recommended <100KB for optimal performance)`, "warning");else if (assetType === "sound" && fileSizeKB > 1024) progress.log(`Warning: Notification sound ${path.basename(filePath)} is ${fileSizeKB.toFixed(1)}KB (recommended <1MB)`, "warning");
|
|
@@ -99,6 +99,7 @@ function isOfflineRoute(url, manifest) {
|
|
|
99
99
|
const { pathname } = new URL(url)
|
|
100
100
|
return (manifest.routes || []).some((route) => {
|
|
101
101
|
try {
|
|
102
|
+
// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp - route.regex is generated at build time by generateOfflineManifest.js's routeRegex(), which escapes every regex metacharacter in literal path segments and only ever emits flat (non-nested, non-repeating) patterns for :param and * segments — it cannot produce a catastrophic-backtracking shape. The manifest itself is same-origin build output, not user-submitted at runtime.
|
|
102
103
|
return new RegExp(route.regex).test(pathname)
|
|
103
104
|
} catch (_) {
|
|
104
105
|
return false
|
package/dist/scripts/build.js
CHANGED
|
@@ -4,6 +4,7 @@ import { arrayToObject } from "./scriptUtils.js"
|
|
|
4
4
|
import { fileURLToPath } from "url"
|
|
5
5
|
import { dirname } from "path"
|
|
6
6
|
import { readFileSync, existsSync, rmSync } from "fs"
|
|
7
|
+
import { createRequire } from "module"
|
|
7
8
|
|
|
8
9
|
const __filename = fileURLToPath(import.meta.url)
|
|
9
10
|
const __dirname = dirname(__filename)
|
|
@@ -11,16 +12,19 @@ const loaderPath = path.resolve(__dirname, "../../dist/vite/node-loader.mjs")
|
|
|
11
12
|
const configPath = path.join(process.env.PWD, "config/config.json")
|
|
12
13
|
const configJSON = JSON.parse(readFileSync(configPath), "utf-8")
|
|
13
14
|
|
|
15
|
+
// Resolve vite's actual JS entry point rather than spawning the "vite" command name,
|
|
16
|
+
// which on Windows only resolves via the npm-installed .cmd shim (i.e. requires a shell).
|
|
17
|
+
// Invoking process.execPath + this path directly works cross-platform with no shell.
|
|
18
|
+
const require = createRequire(import.meta.url)
|
|
19
|
+
const viteBinPath = path.join(path.dirname(require.resolve("vite/package.json")), "bin", "vite.js")
|
|
20
|
+
|
|
14
21
|
/**
|
|
15
|
-
* @param {string}
|
|
22
|
+
* @param {string[]} args
|
|
16
23
|
* @param {import('child_process').SpawnOptions} options
|
|
17
24
|
*/
|
|
18
|
-
function runBuildStep(
|
|
25
|
+
function runBuildStep(args, options) {
|
|
19
26
|
return new Promise((resolve, reject) => {
|
|
20
|
-
const child = spawn(
|
|
21
|
-
...options,
|
|
22
|
-
shell: true,
|
|
23
|
-
})
|
|
27
|
+
const child = spawn(process.execPath, args, options)
|
|
24
28
|
child.on("close", (code) => {
|
|
25
29
|
if (code === 0) {
|
|
26
30
|
resolve()
|
|
@@ -69,8 +73,8 @@ async function build() {
|
|
|
69
73
|
]),
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
const
|
|
73
|
-
const
|
|
76
|
+
const serverBuildArgs = [viteBinPath, "build", "--config", "./dist/vite/vite.config.server.js", "--ssr"]
|
|
77
|
+
const clientBuildArgs = [viteBinPath, "build", "--config", "./dist/vite/vite.config.client.js"]
|
|
74
78
|
const spawnBase = {
|
|
75
79
|
cwd: dirname,
|
|
76
80
|
stdio: "inherit",
|
|
@@ -80,11 +84,11 @@ async function build() {
|
|
|
80
84
|
|
|
81
85
|
try {
|
|
82
86
|
await Promise.all([
|
|
83
|
-
runBuildStep(
|
|
87
|
+
runBuildStep(serverBuildArgs, {
|
|
84
88
|
...spawnBase,
|
|
85
89
|
env: { ...baseEnv, CATALYST_VITE_CACHE_ID: "ssr" },
|
|
86
90
|
}),
|
|
87
|
-
runBuildStep(
|
|
91
|
+
runBuildStep(clientBuildArgs, {
|
|
88
92
|
...spawnBase,
|
|
89
93
|
env: { ...baseEnv, CATALYST_VITE_CACHE_ID: "client" },
|
|
90
94
|
}),
|
|
@@ -96,7 +100,7 @@ async function build() {
|
|
|
96
100
|
|
|
97
101
|
console.log("✅ Server and client builds completed!")
|
|
98
102
|
|
|
99
|
-
await runBuildStep("
|
|
103
|
+
await runBuildStep(["./dist/scripts/generateOfflineManifest.js"], {
|
|
100
104
|
...spawnBase,
|
|
101
105
|
env: baseEnv,
|
|
102
106
|
})
|
|
@@ -68,6 +68,7 @@ async function loadAppRoutes(appRoot) {
|
|
|
68
68
|
})
|
|
69
69
|
|
|
70
70
|
try {
|
|
71
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - appRoot is the app's own build root and the remainder is a hardcoded literal path, not request input.
|
|
71
72
|
const routesModule = await vite.ssrLoadModule(path.join(appRoot, "src/js/routes/utils.js"))
|
|
72
73
|
return typeof routesModule.getRoutes === "function" ? routesModule.getRoutes() : []
|
|
73
74
|
} finally {
|
|
@@ -23,6 +23,7 @@ export const printBundleInformation = () => {
|
|
|
23
23
|
const files = fs.readdirSync(directoryPath)
|
|
24
24
|
files.forEach((file) => {
|
|
25
25
|
if (!file.includes("txt") && !file.includes("json")) {
|
|
26
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - file comes from fs.readdirSync(directoryPath), i.e. actual filenames already on disk in the build output, not request input.
|
|
26
27
|
const filePath = path.join(directoryPath, file)
|
|
27
28
|
const fileSize = getFileSizeSync(filePath)
|
|
28
29
|
if (fileSize !== null) {
|
package/dist/scripts/serve.js
CHANGED
|
@@ -24,28 +24,30 @@ function startProd() {
|
|
|
24
24
|
|
|
25
25
|
console.log("🚀 Starting production server...")
|
|
26
26
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
27
|
+
const inspectArgs = commandLineArguments.includes("--inspect") ? ["--inspect"] : []
|
|
28
|
+
spawnSync(
|
|
29
|
+
"node",
|
|
30
|
+
[...inspectArgs, "--import", preInitPath, "--loader", loaderPath, "./dist/server/expressServer.js"],
|
|
31
|
+
{
|
|
32
|
+
cwd: dirname,
|
|
33
|
+
stdio: "inherit",
|
|
34
|
+
env: {
|
|
35
|
+
...process.env,
|
|
36
|
+
src_path: process.env.PWD,
|
|
37
|
+
NODE_ENV: "production",
|
|
38
|
+
IS_DEV_COMMAND: false,
|
|
39
|
+
APPLICATION: name || "catalyst_app",
|
|
40
|
+
...argumentsObject,
|
|
41
|
+
filterKeys: JSON.stringify([
|
|
42
|
+
"src_path",
|
|
43
|
+
"NODE_ENV",
|
|
44
|
+
"IS_DEV_COMMAND",
|
|
45
|
+
"APPLICATION",
|
|
46
|
+
...Object.keys(argumentsObject),
|
|
47
|
+
]),
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
)
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
startProd()
|
package/dist/scripts/start.js
CHANGED
|
@@ -22,11 +22,9 @@ function start() {
|
|
|
22
22
|
const packageJson = JSON.parse(readFileSync(path.join(process.env.PWD, "package.json"), "utf-8"))
|
|
23
23
|
const { name } = packageJson
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
spawnSync(command, [], {
|
|
25
|
+
spawnSync("node", ["--import", preInitPath, "--loader", loaderPath, "./dist/server/expressServer.js"], {
|
|
27
26
|
cwd: dirname,
|
|
28
27
|
stdio: "inherit",
|
|
29
|
-
shell: true,
|
|
30
28
|
env: {
|
|
31
29
|
...process.env,
|
|
32
30
|
src_path: process.env.PWD,
|
|
@@ -12,8 +12,52 @@ const { cyan, yellow, green } = pc
|
|
|
12
12
|
|
|
13
13
|
import { validateMiddleware, safeCall } from "./utils/validator.js"
|
|
14
14
|
import { botDetectionMiddleware } from "./utils/botDetectionMiddleware.js"
|
|
15
|
+
import { cjsRequire } from "./utils/cjsRequire.js"
|
|
15
16
|
const { addMiddlewares } = await import(path.join(process.env.src_path, "server/server.js"))
|
|
16
17
|
|
|
18
|
+
// Mount AI route if catalyst-ai is installed
|
|
19
|
+
function mountAIRouter(app) {
|
|
20
|
+
let aiPackagePath
|
|
21
|
+
try {
|
|
22
|
+
aiPackagePath = cjsRequire.resolve("catalyst-ai/route", {
|
|
23
|
+
paths: [process.env.src_path || process.cwd()],
|
|
24
|
+
})
|
|
25
|
+
} catch (resolveErr) {
|
|
26
|
+
if (resolveErr.code !== "MODULE_NOT_FOUND") {
|
|
27
|
+
console.error("[catalyst-core/ai] Unexpected error resolving catalyst-ai:", resolveErr)
|
|
28
|
+
} else {
|
|
29
|
+
console.debug("[catalyst-core/ai] catalyst-ai not installed — AI routes unavailable")
|
|
30
|
+
}
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const aiRouter = cjsRequire(aiPackagePath)
|
|
36
|
+
let aiConfig = {}
|
|
37
|
+
try {
|
|
38
|
+
const parsedConfig = JSON.parse(process.env.AI_CONFIG || "{}")
|
|
39
|
+
if (parsedConfig && typeof parsedConfig === "object" && !Array.isArray(parsedConfig)) {
|
|
40
|
+
aiConfig = parsedConfig
|
|
41
|
+
} else {
|
|
42
|
+
console.warn("[catalyst-core/ai] AI_CONFIG must be a JSON object, ignoring it")
|
|
43
|
+
}
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.warn(`[catalyst-core/ai] Invalid AI_CONFIG JSON, ignoring: ${e.message}`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (aiConfig.enabled === false) {
|
|
49
|
+
console.log("[catalyst-core/ai] AI_CONFIG.enabled is false, skipping AI router")
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const aiBasePath = aiConfig.basePath || "/ai"
|
|
54
|
+
console.log(`[catalyst-core/ai] mounting AI router at ${aiBasePath}`)
|
|
55
|
+
app.use(aiBasePath, aiRouter)
|
|
56
|
+
} catch (mountErr) {
|
|
57
|
+
console.error("[catalyst-core/ai] Failed to mount AI router:", mountErr)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
17
61
|
// OpenTelemetry is opt-in (OTEL_ENABLE) — mirrors server/renderer/handler.jsx.
|
|
18
62
|
// Passthrough no-op middleware when disabled or packages aren't installed.
|
|
19
63
|
let responseFlushMiddleware = () => (_req, _res, next) => next()
|
|
@@ -82,6 +126,7 @@ const isProduction = process.env.NODE_ENV === "production"
|
|
|
82
126
|
|
|
83
127
|
function serveBuildFile(app, buildPath, urlPath, fileName, headers = {}) {
|
|
84
128
|
app.get(urlPath, (_req, res, next) => {
|
|
129
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - fileName is always a hardcoded literal passed at each serveBuildFile call site, never derived from the request.
|
|
85
130
|
const filePath = path.join(buildPath, fileName)
|
|
86
131
|
if (!fs.existsSync(filePath)) return next()
|
|
87
132
|
res.set(headers)
|
|
@@ -106,6 +151,8 @@ async function createServer() {
|
|
|
106
151
|
// All the middlewares defined by the user will run here.
|
|
107
152
|
if (validateMiddleware(addMiddlewares)) addMiddlewares(app)
|
|
108
153
|
|
|
154
|
+
mountAIRouter(app)
|
|
155
|
+
|
|
109
156
|
// response.compress + response.flush spans straddle compression — they
|
|
110
157
|
// attribute the time past the `handler` span (gzip/brotli, then egress).
|
|
111
158
|
// MUST be mounted immediately before compression() so its outer res.end
|
|
@@ -34,9 +34,11 @@ export function Head(props) {
|
|
|
34
34
|
{metaTags && metaTags}
|
|
35
35
|
|
|
36
36
|
{/* Inline critical CSS — prevents FOUC/CLS */}
|
|
37
|
+
{/* nosemgrep: typescript.react.security.audit.react-dangerouslysetinnerhtml.react-dangerouslysetinnerhtml - inlineCss is compiled CSS read from disk (readCssFromDisk), sourced from the app's own build output, never from request/user input. */}
|
|
37
38
|
{inlineCss && <style dangerouslySetInnerHTML={{ __html: inlineCss }} />}
|
|
38
39
|
|
|
39
40
|
{/* Deferred CSS from prior SSRs on this route — avoids late layout from post-body <style> */}
|
|
41
|
+
{/* nosemgrep: typescript.react.security.audit.react-dangerouslysetinnerhtml.react-dangerouslysetinnerhtml - deferredRouteInlineCss is compiled CSS read from disk (readCssFromDisk), sourced from the app's own build output, never from request/user input. */}
|
|
40
42
|
{deferredRouteInlineCss && <style dangerouslySetInnerHTML={{ __html: deferredRouteInlineCss }} />}
|
|
41
43
|
|
|
42
44
|
{/* JS modules */}
|
|
@@ -103,6 +103,7 @@ export const readCssFromDisk = (cssPaths = [], basePath) => {
|
|
|
103
103
|
seen.add(asset)
|
|
104
104
|
if (asset.startsWith("http")) continue
|
|
105
105
|
|
|
106
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - asset paths come from the Vite build manifest generated at build time, never from a request, so there is no user-controlled input here.
|
|
106
107
|
const filePath = path.isAbsolute(asset) ? asset : path.join(basePath, asset.replace(/^\/+/, ""))
|
|
107
108
|
|
|
108
109
|
try {
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
generateModulePreloadLinkElements,
|
|
24
24
|
} from "./extract.js"
|
|
25
25
|
import path from "path"
|
|
26
|
+
import { Transform } from "node:stream"
|
|
26
27
|
|
|
27
28
|
import CustomDocument from "@catalyst/template/server/document"
|
|
28
29
|
|
|
@@ -242,13 +243,17 @@ const _renderMarkUp = async (
|
|
|
242
243
|
res.status(status)
|
|
243
244
|
|
|
244
245
|
return new Promise((resolve, reject) => {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
246
|
+
// Single completion path: React's pipe() auto-ends `tail`, and
|
|
247
|
+
// `flush()` appends the deferred asset tags before that end
|
|
248
|
+
// signal propagates to `res` via the plain pipe below. This
|
|
249
|
+
// avoids the onShellReady/onAllReady race where React's own
|
|
250
|
+
// stream-end and a manual res.end() compete to close `res`
|
|
251
|
+
// (see issue #320).
|
|
252
|
+
const tail = new Transform({
|
|
253
|
+
transform(chunk, _enc, cb) {
|
|
254
|
+
cb(null, chunk)
|
|
249
255
|
},
|
|
250
|
-
|
|
251
|
-
onAllReady() {
|
|
256
|
+
flush(cb) {
|
|
252
257
|
// Deferred assets — injected after body (non-blocking)
|
|
253
258
|
const deferredAssets = chunkExtractor
|
|
254
259
|
? chunkExtractor.getDeferredAssets()
|
|
@@ -256,10 +261,11 @@ const _renderMarkUp = async (
|
|
|
256
261
|
|
|
257
262
|
// Tell client which components were SSR'd so split() can
|
|
258
263
|
// eagerly import them (prevents Suspense fallback flash)
|
|
259
|
-
|
|
264
|
+
this.push(`<script>window.__CATALYST_IS_BOT__=${isBot ? "true" : "false"};</script>`)
|
|
260
265
|
if (chunkExtractor) {
|
|
261
266
|
const renderedKeys = chunkExtractor.getRenderedComponentKeys()
|
|
262
|
-
|
|
267
|
+
this.push(
|
|
268
|
+
// nosemgrep: javascript.lang.security.audit.unknown-value-with-script-tag.unknown-value-with-script-tag - renderedKeys are internal bundler component-module keys tracked by ChunkExtractor, never request/user input, and are JSON.stringify-escaped before embedding.
|
|
263
269
|
`<script>window.__SSR_RENDERED_COMPONENTS__=new Set(${JSON.stringify(renderedKeys)})</script>`
|
|
264
270
|
)
|
|
265
271
|
}
|
|
@@ -270,13 +276,24 @@ const _renderMarkUp = async (
|
|
|
270
276
|
isBot
|
|
271
277
|
)
|
|
272
278
|
if (newCssPaths.length) {
|
|
273
|
-
|
|
279
|
+
this.push(`<style>${readCssFromDisk(newCssPaths, buildDir)}</style>`)
|
|
274
280
|
}
|
|
275
281
|
if (!isBot) {
|
|
276
|
-
|
|
282
|
+
this.push(generateScriptStrings(deferredAssets.js))
|
|
277
283
|
}
|
|
278
284
|
|
|
279
|
-
|
|
285
|
+
cb()
|
|
286
|
+
},
|
|
287
|
+
})
|
|
288
|
+
tail.pipe(res)
|
|
289
|
+
|
|
290
|
+
const { pipe } = renderToPipeableStream(<CompleteDocument />, {
|
|
291
|
+
onShellReady() {
|
|
292
|
+
res.setHeader("content-type", "text/html")
|
|
293
|
+
pipe(tail)
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
onAllReady() {
|
|
280
297
|
cleanupSafeArea()
|
|
281
298
|
resolve()
|
|
282
299
|
},
|
|
@@ -285,6 +302,7 @@ const _renderMarkUp = async (
|
|
|
285
302
|
console.error("Error in renderToPipeableStream:", error)
|
|
286
303
|
safeCall(onRenderError, { req, res, store, error })
|
|
287
304
|
cleanupSafeArea()
|
|
305
|
+
tail.destroy(error)
|
|
288
306
|
reject(error)
|
|
289
307
|
},
|
|
290
308
|
})
|
|
@@ -45,9 +45,11 @@ function loadAliases() {
|
|
|
45
45
|
for (const [alias, aliasPath] of Object.entries(allAliases)) {
|
|
46
46
|
if (aliasPath && typeof aliasPath === "string") {
|
|
47
47
|
try {
|
|
48
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - appRoot is the app's own build root and aliasPath comes from the developer's package.json config, not request/user input.
|
|
48
49
|
const resolvedPath = path.resolve(appRoot, ...aliasPath.split("/"))
|
|
49
50
|
aliasCache[alias] = resolvedPath
|
|
50
51
|
} catch (error) {
|
|
52
|
+
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - alias is a key from the developer's own package.json config, not attacker-controlled; console.warn here does no printf-style substitution.
|
|
51
53
|
console.warn(`Failed to resolve alias ${alias}:`, error.message)
|
|
52
54
|
}
|
|
53
55
|
}
|
|
@@ -87,6 +89,7 @@ function resolveWithExtensions(basePath) {
|
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
for (const ext of extensions) {
|
|
92
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - basePath is derived from module import specifiers resolved by this plugin's own alias/relative-import logic, never from a request.
|
|
90
93
|
const candidate = path.join(basePath, `index${ext}`)
|
|
91
94
|
if (existsSync(candidate)) {
|
|
92
95
|
return candidate
|
|
@@ -107,9 +110,11 @@ function resolveToAbsolutePath(importPath, importerId) {
|
|
|
107
110
|
for (const [alias, aliasPath] of Object.entries(aliases)) {
|
|
108
111
|
if (importPath === alias || importPath.startsWith(alias + "/")) {
|
|
109
112
|
const remainingPath = importPath === alias ? "" : importPath.slice(alias.length + 1)
|
|
113
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - importPath comes from the app's own module import graph and aliasPath from the developer's package.json config, never from a request.
|
|
110
114
|
const rawResolved = path.join(aliasPath, remainingPath)
|
|
111
115
|
const finalResolved = resolveWithExtensions(rawResolved)
|
|
112
116
|
if (finalResolved) {
|
|
117
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - finalResolved is derived from the same alias/import resolution above, not request input.
|
|
113
118
|
return path.resolve(finalResolved)
|
|
114
119
|
}
|
|
115
120
|
}
|
|
@@ -118,6 +123,7 @@ function resolveToAbsolutePath(importPath, importerId) {
|
|
|
118
123
|
// 2. Relative imports — resolve from the importer's directory
|
|
119
124
|
if (importPath.startsWith("./") || importPath.startsWith("../")) {
|
|
120
125
|
const importerDir = path.dirname(importerId)
|
|
126
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - importPath comes from the app's own module import graph and importerId from Vite's module resolution, never from a request.
|
|
121
127
|
const absolutePath = path.resolve(importerDir, importPath)
|
|
122
128
|
const resolved = resolveWithExtensions(absolutePath)
|
|
123
129
|
if (resolved) {
|
|
@@ -149,6 +155,7 @@ function resolveImportPath(importPath, importerId) {
|
|
|
149
155
|
// Fallback: strip leading ./ and return as-is
|
|
150
156
|
return importPath.replace(/^\.\//, "")
|
|
151
157
|
} catch (error) {
|
|
158
|
+
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - importPath comes from the app's own module import graph resolved by Vite at build time, not attacker-controlled; console.warn here does no printf-style substitution.
|
|
152
159
|
console.warn(`Error resolving import path ${importPath}:`, error.message)
|
|
153
160
|
return importPath.replace(/^\.\//, "")
|
|
154
161
|
}
|
|
@@ -10,6 +10,7 @@ const emptyConfig = {
|
|
|
10
10
|
export async function loadCustomViteConfig() {
|
|
11
11
|
const configNames = ["buildConfig.js", "webpackConfig.js"]
|
|
12
12
|
const configPath = configNames
|
|
13
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - configName is one of two hardcoded literals above, and process.env.src_path is the app root set by the framework's own build tooling, not request/user input.
|
|
13
14
|
.map((configName) => path.join(process.env.src_path, configName))
|
|
14
15
|
.find((candidatePath) => existsSync(candidatePath))
|
|
15
16
|
|
|
@@ -293,6 +293,7 @@ export function manifestCategorizationPlugin(options = {}) {
|
|
|
293
293
|
// branch of the import graph, the affected CSS would vanish from server responses
|
|
294
294
|
// with no runtime error — this check surfaces that as a build-time warning.
|
|
295
295
|
function validateCssCoverage(categorizedManifest, buildDir) {
|
|
296
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - buildDir is the build tool's own output directory and the remaining segments are hardcoded literals, not request input.
|
|
296
297
|
const cssDir = path.join(buildDir, "client", "assets", "css")
|
|
297
298
|
if (!fs.existsSync(cssDir)) return
|
|
298
299
|
|
|
@@ -403,6 +404,7 @@ export function manifestCategorizationPlugin(options = {}) {
|
|
|
403
404
|
})
|
|
404
405
|
}
|
|
405
406
|
} catch (err) {
|
|
407
|
+
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - importPath comes from the app's own module import graph resolved by Vite at build time, not attacker-controlled; console.log here does no printf-style substitution.
|
|
406
408
|
console.log(`❌ Error resolving ${importPath}:`, err.message)
|
|
407
409
|
}
|
|
408
410
|
})
|
|
@@ -56,6 +56,7 @@ function loadAliases() {
|
|
|
56
56
|
const resolvedPath = resolvePath(appRoot, ...aliasPath.split("/"))
|
|
57
57
|
aliasCache[alias] = resolvedPath
|
|
58
58
|
} catch (error) {
|
|
59
|
+
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - alias is a key from the developer's own package.json _moduleAliases config, not attacker-controlled; console.warn here does no printf-style substitution.
|
|
59
60
|
console.warn(`Failed to resolve alias ${alias}:`, error.message)
|
|
60
61
|
}
|
|
61
62
|
}
|
|
@@ -100,6 +101,7 @@ function resolveWithExtensions(basePath) {
|
|
|
100
101
|
|
|
101
102
|
// Try index files inside a directory
|
|
102
103
|
for (const ext of extensions) {
|
|
104
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - basePath is derived from module import specifiers resolved by the Node loader itself, never from a request.
|
|
103
105
|
const candidate = join(basePath, `index${ext}`)
|
|
104
106
|
if (existsSync(candidate)) {
|
|
105
107
|
return candidate
|
|
@@ -119,10 +121,12 @@ function resolveAlias(specifier, parentURL) {
|
|
|
119
121
|
for (const [alias, aliasPath] of Object.entries(aliases)) {
|
|
120
122
|
if (specifier.startsWith(alias + "/") || specifier === alias) {
|
|
121
123
|
const remainingPath = specifier === alias ? "" : specifier.slice(alias.length + 1)
|
|
124
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - specifier comes from an ES module import statement in the app's own source code, and aliasPath from the developer's package.json config, never from a request.
|
|
122
125
|
const rawResolvedPath = join(aliasPath, remainingPath)
|
|
123
126
|
const finalResolvedPath = resolveWithExtensions(rawResolvedPath)
|
|
124
127
|
|
|
125
128
|
if (finalResolvedPath) {
|
|
129
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - finalResolvedPath is derived from the same alias/import resolution above, not request input.
|
|
126
130
|
return resolvePath(finalResolvedPath)
|
|
127
131
|
}
|
|
128
132
|
}
|
|
@@ -140,6 +144,7 @@ function resolveRelative(specifier, parentURL) {
|
|
|
140
144
|
try {
|
|
141
145
|
const parentPath = fileURLToPath(parentURL)
|
|
142
146
|
const parentDir = dirname(parentPath)
|
|
147
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - specifier comes from an ES module import statement in the app's own source code, parentURL from the module loader itself, never from a request.
|
|
143
148
|
const absolutePath = resolvePath(parentDir, specifier)
|
|
144
149
|
return resolveWithExtensions(absolutePath)
|
|
145
150
|
} catch {
|
|
@@ -225,6 +230,7 @@ export async function resolve(specifier, context, defaultResolve) {
|
|
|
225
230
|
*/
|
|
226
231
|
function findNearestPackageType(dir) {
|
|
227
232
|
while (dir !== dirname(dir)) {
|
|
233
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - dir is walked up from the module's own filesystem location via dirname(), never from a request; "package.json" is a hardcoded literal.
|
|
228
234
|
const pkgPath = join(dir, "package.json")
|
|
229
235
|
if (existsSync(pkgPath)) {
|
|
230
236
|
try {
|