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.
Files changed (33) hide show
  1. package/dist/globals.d.ts +7 -0
  2. package/dist/native/androidProject/app/build.gradle.kts +45 -4
  3. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +86 -0
  4. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +8 -0
  5. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FrameworkServerUtils.kt +173 -13
  6. package/dist/native/androidProject/settings.gradle.kts +10 -0
  7. package/dist/native/androidSetup.js +8 -6
  8. package/dist/native/bridge/constants/NativeInterfaces.js +15 -1
  9. package/dist/native/bridge/hooks/index.js +7 -0
  10. package/dist/native/bridge/hooks/useAI.js +94 -0
  11. package/dist/native/buildAndroid/assets.js +1 -1
  12. package/dist/native/buildAndroid/index.js +32 -0
  13. package/dist/native/buildIos/assets.js +1 -1
  14. package/dist/offline/catalyst-sw.js +1 -0
  15. package/dist/scripts/build.js +15 -11
  16. package/dist/scripts/generateOfflineManifest.js +1 -0
  17. package/dist/scripts/scriptUtils.js +1 -0
  18. package/dist/scripts/serve.js +24 -22
  19. package/dist/scripts/start.js +1 -3
  20. package/dist/server/expressServer.js +47 -0
  21. package/dist/server/renderer/document/Head.jsx +2 -0
  22. package/dist/server/renderer/extract.js +1 -0
  23. package/dist/server/renderer/handler.jsx +29 -11
  24. package/dist/vite/inject-cache-key-plugin.js +7 -0
  25. package/dist/vite/loadCustomViteConfig.js +1 -0
  26. package/dist/vite/manifest-categorization-plugin.js +2 -0
  27. package/dist/vite/node-loader.mjs +6 -0
  28. package/dist/vite/vite.config.js +83 -1
  29. package/mcp_v2/knowledge-base.json +52 -4
  30. package/mcp_v2/mcp.js +36 -4
  31. package/mcp_v2/tools/build.js +46 -18
  32. package/mcp_v2/tools/github.js +11 -0
  33. 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+[\\s\\S]*?android:name="${metadataName}"[\\s\\S]*?\\/>`, "gi");
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
- async function validateNotificationAsset(filePath, assetType) {
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
@@ -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} command
22
+ * @param {string[]} args
16
23
  * @param {import('child_process').SpawnOptions} options
17
24
  */
18
- function runBuildStep(command, options) {
25
+ function runBuildStep(args, options) {
19
26
  return new Promise((resolve, reject) => {
20
- const child = spawn(command, [], {
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 serverBuildCommand = `vite build --config ./dist/vite/vite.config.server.js --ssr`
73
- const clientBuildCommand = `vite build --config ./dist/vite/vite.config.client.js`
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(serverBuildCommand, {
87
+ runBuildStep(serverBuildArgs, {
84
88
  ...spawnBase,
85
89
  env: { ...baseEnv, CATALYST_VITE_CACHE_ID: "ssr" },
86
90
  }),
87
- runBuildStep(clientBuildCommand, {
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("node ./dist/scripts/generateOfflineManifest.js", {
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) {
@@ -24,28 +24,30 @@ function startProd() {
24
24
 
25
25
  console.log("🚀 Starting production server...")
26
26
 
27
- const inspectFlag = commandLineArguments.includes("--inspect") ? "--inspect" : ""
28
- const command = `node ${inspectFlag} --import ${preInitPath} --loader ${loaderPath} ./dist/server/expressServer.js`
29
- spawnSync(command, [], {
30
- cwd: dirname,
31
- stdio: "inherit",
32
- shell: true,
33
- env: {
34
- ...process.env,
35
- src_path: process.env.PWD,
36
- NODE_ENV: "production",
37
- IS_DEV_COMMAND: false,
38
- APPLICATION: name || "catalyst_app",
39
- ...argumentsObject,
40
- filterKeys: JSON.stringify([
41
- "src_path",
42
- "NODE_ENV",
43
- "IS_DEV_COMMAND",
44
- "APPLICATION",
45
- ...Object.keys(argumentsObject),
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()
@@ -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
- const command = `node --import ${preInitPath} --loader ${loaderPath} ./dist/server/expressServer.js`
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
- const { pipe } = renderToPipeableStream(<CompleteDocument />, {
246
- onShellReady() {
247
- res.setHeader("content-type", "text/html")
248
- pipe(res)
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
- res.write(`<script>window.__CATALYST_IS_BOT__=${isBot ? "true" : "false"};</script>`)
264
+ this.push(`<script>window.__CATALYST_IS_BOT__=${isBot ? "true" : "false"};</script>`)
260
265
  if (chunkExtractor) {
261
266
  const renderedKeys = chunkExtractor.getRenderedComponentKeys()
262
- res.write(
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
- res.write(`<style>${readCssFromDisk(newCssPaths, buildDir)}</style>`)
279
+ this.push(`<style>${readCssFromDisk(newCssPaths, buildDir)}</style>`)
274
280
  }
275
281
  if (!isBot) {
276
- res.write(generateScriptStrings(deferredAssets.js))
282
+ this.push(generateScriptStrings(deferredAssets.js))
277
283
  }
278
284
 
279
- res.end()
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 {