Skip to content

Add Worker-safe create manifest export#475

Merged
tannerlinsley merged 1 commit into
mainfrom
taren/create-edge-manifest
Jun 20, 2026
Merged

Add Worker-safe create manifest export#475
tannerlinsley merged 1 commit into
mainfrom
taren/create-edge-manifest

Conversation

@tannerlinsley

@tannerlinsley tannerlinsley commented Jun 19, 2026

Copy link
Copy Markdown
Member

Summary

Adds a Worker-safe @tanstack/create/edge export backed by a build-time generated framework/template/add-on manifest. The root @tanstack/create export keeps the existing Node/CLI behavior.

Details

  • Adds a manifest generator that scans React/Solid framework catalogs during build and precompiles template rendering into generated source.
  • Adds edge-safe framework lookup, add-on selection, memory environment, package JSON rendering, and app generation modules.
  • Exposes @tanstack/create/edge and @tanstack/create/manifest subpath exports.
  • Keeps generated manifest source out of git and regenerates it for build/test/dev flows.
  • Documents Worker-safe programmatic generation and adds a patch changeset for @tanstack/create.

Validation

  • npm test in packages/create
  • npm run build in packages/create
  • pnpm --filter @tanstack/cli build
  • pnpm --filter @tanstack/cli test
  • Commit hook: nx run-many --target=build --parallel 5, unit tests, and blocking e2e tests
  • Dist smoke check for @tanstack/create/edge
  • Built edge graph checked for no direct node:*, execa, ejs, or new Function markers

Summary by CodeRabbit

Release Notes

  • New Features

    • Added @tanstack/create/edge export enabling programmatic app generation in worker-safe runtimes without Node filesystem access (serverless workers, edge functions).
    • Generated applications can be built entirely in memory with output files accessible for packaging and distribution.
  • Documentation

    • Added CLI reference guide with TypeScript examples for using the edge export.

@tannerlinsley tannerlinsley requested a review from a team as a code owner June 19, 2026 23:53
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a @tanstack/create/edge entrypoint that generates apps without Node.js filesystem access. A build-time script (generate-manifest.mjs) scans framework source trees, compiles EJS templates into precompiled TypeScript renderers, and emits a single create-manifest.ts. New edge modules implement path utilities, an in-memory Environment, template rendering, framework/add-on resolution, package.json building, and full app-creation orchestration via createApp.

Changes

Edge Export and Manifest Generation

Layer / File(s) Summary
Build-time manifest generator and project wiring
packages/create/scripts/generate-manifest.mjs, packages/create/package.json, .gitignore, eslint.config.js
generate-manifest.mjs scans project/base and add-on catalog directories, inlines binary assets as base64, compiles EJS templates into hash-keyed TypeScript renderer functions, and writes src/generated/create-manifest.ts. Build/test scripts chain generate-manifest before tsc/vitest. The generated directory is added to .gitignore and ESLint ignores.
Schema and manifest type contracts
packages/create/src/types.ts, packages/create/src/manifest-types.ts
AddOnBaseSchema route fields path/jsName made optional, icon added, optional variables array added; phase enum gains 'example'. New ManifestFrameworkDefinition replaces addOns with Array<AddOnCompiled>.
Edge path utilities and file helpers
packages/create/src/edge-path.ts, packages/create/src/edge-file-helpers.ts
Node-free path module (normalizePath, joinPaths, basenamePath, dirnamePath, extnamePath) and file helper module (isBinaryFile, isBase64, toCleanPath, relativePath, isDemoFilePath, cleanUpFiles, cleanUpFileArray).
In-memory Environment implementation
packages/create/src/edge-environment.ts
createMemoryEnvironment builds a full Environment backed by a normalized-path files map. Implements all file/dir operations with directory semantics derived from key prefixes. finishRun optionally rewrites output paths relative to a base directory. Exports MemoryEnvironmentOutput interface.
Template rendering pipeline
packages/create/src/edge-render.ts, packages/create/src/edge-template-file.ts
render wraps renderManifestTemplate with default context variables and an ignoreFile throw. createTemplateFile normalizes paths (_dot_, TS extension rewriting, .append suffix), builds add-on context (integrations, routes, addOnEnabled), renders .ejs content, and writes/appends to the environment.
Framework and add-on resolution
packages/create/src/edge-frameworks.ts, packages/create/src/edge-add-ons.ts
createFrameworkFromManifest and createAddOn wrap compiled manifest data into Framework/AddOn objects with async file accessors. finalizeAddOns resolves IDs case-insensitively, loads remote URL add-ons via fetch, expands dependsOn dependencies, and suggests closest matches via Levenshtein distance.
Package.json builder and config file writer
packages/create/src/edge-package-json.ts, packages/create/src/edge-config-file.ts
mergePackageJSON merges deps/devDeps/scripts/pnpm fields. createPackageJSON starts from framework base, merges TypeScript/Tailwind/mode packages and add-on packageTemplate renders, handles routerOnly cleanup. writeConfigFileToEnvironment serializes normalized PersistedOptions to the target directory.
App creation orchestration
packages/create/src/edge-create-app.ts
Full createApp pipeline: strip examples → write files (framework/add-on/starter/package.json/config) → seed .env.local → write .env.example → run git init, special steps, install, per-add-on commands, route-tree generation, shadcn install, TanStack Intent setup → report warnings/errors/next steps.
Public API barrel and package exports
packages/create/src/edge.ts, packages/create/src/manifest.ts, packages/create/package.json
edge.ts re-exports all edge symbols. manifest.ts re-exports from edge.js. package.json adds explicit exports map for root, ./edge, ./manifest, and ./dist/*.
Tests, documentation, and changeset
packages/create/tests/edge-import.test.ts, packages/create/tests/edge-manifest.test.ts, docs/cli-reference.md, .changeset/slow-buses-build.md
edge-import.test.ts asserts Node-only modules are not imported by the edge entry. edge-manifest.test.ts verifies manifest parity with Node-scanned catalogs and a full React app generation with output assertions. CLI reference documents the @tanstack/create/edge programmatic API.

Sequence Diagram

sequenceDiagram
  participant User as User/Worker
  participant edge as `@tanstack/create/edge`
  participant finalizeAddOns
  participant createMemoryEnvironment
  participant createApp

  User->>edge: getFrameworkById('react')
  edge-->>User: Framework (from compiled manifest)
  User->>finalizeAddOns: framework, mode, chosenAddOnIDs[]
  finalizeAddOns-->>User: resolved AddOn[]
  User->>createMemoryEnvironment: returnPathsRelativeTo
  createMemoryEnvironment-->>User: { environment, output }
  User->>createApp: environment, options
  createApp->>createApp: writeFiles (render templates from manifest)
  createApp->>createApp: seedEnvValues / writeEnvExample
  createApp->>createApp: runCommandsAndInstallDependencies
  createApp-->>User: void (environment.output.files populated)
  User->>User: output.files → ZIP / response
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Suggested reviewers

  • jherr

Poem

🐇 Hop hop, no filesystem near,
Templates compiled so Workers won't fear!
A manifest baked at build-time tight,
Edge runtimes now generate apps just right.
No fs.readFile, no Node in sight —
Just pure in-memory, fluffy delight! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: adding a Worker-safe export to the create manifest. This is the central feature across all modifications.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch taren/create-edge-manifest

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

eslint.config.js

Parsing error: project was set to true but couldn't find any tsconfig.json relative to '/eslint.config.js' within ''.

packages/create/tests/edge-import.test.ts

Parsing error: "parserOptions.project" has been provided for @typescript-eslint/parser.
The file was not found in any of the provided project(s): packages/create/tests/edge-import.test.ts

packages/create/tests/edge-manifest.test.ts

Parsing error: "parserOptions.project" has been provided for @typescript-eslint/parser.
The file was not found in any of the provided project(s): packages/create/tests/edge-manifest.test.ts


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

🧹 Nitpick comments (1)
docs/cli-reference.md (1)

47-85: 💤 Low value

Clear Worker-safe API documentation.

The example demonstrates the complete workflow for programmatic app generation in edge runtimes. The code is concise and ready to use.

📝 Optional: Mention the includeExamples option

Users might want to know about the includeExamples option to control example code generation:

 await createApp(environment, {
   projectName: 'app',
   targetDir: '/app',
   framework,
   mode: 'file-router',
   typescript: true,
   tailwind: true,
   packageManager: 'pnpm',
   git: false,
   install: false,
   intent: false,
   chosenAddOns,
   addOnOptions,
+  includeExamples: false,
 })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/cli-reference.md` around lines 47 - 85, Add documentation for the
includeExamples option in the Worker-safe programmatic generation example.
Include the includeExamples property in the options object passed to the
createApp function call to demonstrate how users can control example code
generation. Add a brief comment or explanation in the documentation explaining
that this option controls whether example files are generated during app
creation, helping users understand they can set it to true or false based on
their needs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/create/package.json`:
- Around line 27-33: The test:coverage script is missing the manifest generation
prerequisite that other scripts include. Add "npm run generate-manifest &&" to
the beginning of the test:coverage script command to ensure the generated
manifest file exists before running coverage tests, following the same pattern
used in the build, dev, test, and test:watch scripts.

In `@packages/create/scripts/generate-manifest.mjs`:
- Around line 76-83: The readdirSync call in the visit function iterates over
directory entries in filesystem order, which is non-deterministic across
different environments and causes inconsistent manifest generation. Sort the
array returned by readdirSync before iterating over it with the for loop. Apply
the same sorting fix to the other location mentioned (around lines 113-116)
where readdirSync is also used without sorting to ensure deterministic manifest
ordering regardless of filesystem.

In `@packages/create/src/edge-add-ons.ts`:
- Around line 36-38: The code fetches remote add-ons via
loadRemoteAddOn(addOnID) without validating the URL, creating an SSRF
vulnerability if addOnID originates from external input. Additionally,
loadRemoteAddOn likely processes HTTP responses as JSON without checking status
codes. Add URL validation before calling loadRemoteAddOn to reject localhost
addresses, private IP ranges, and cloud metadata endpoints. Modify
loadRemoteAddOn to check the HTTP response status code and only parse JSON for
2xx responses, rejecting error responses. Apply these same hardening fixes to
the similar remote add-on loading code at lines 76-79.

In `@packages/create/src/edge-config-file.ts`:
- Around line 17-24: The return object in this function uses the `...rest`
spread operator which inadvertently includes sensitive fields like
`envVarValues` and `addOns` in the persisted config file, creating a security
risk. Instead of spreading rest, explicitly list only the safe fields that
should be persisted in the edge config file. Destructure out `envVarValues` and
`addOns` from the options parameter along with the other fields already being
destructured, then return only the intended config fields (version, framework as
an id, chosenAddOns as ids mapped from the objects, and starter as an id if
present) without using the spread operator for remaining fields.

In `@packages/create/src/edge-create-app.ts`:
- Around line 565-567: The cd instruction string being printed uses the
incorrect variable `projectName` instead of `targetDir`. In the conditional
expression around line 565-567 in edge-create-app.ts, replace the `projectName`
reference with `targetDir` in the template string to ensure users are directed
to the correct output directory, which may differ from the project name when the
output directory is specified separately.
- Around line 445-446: The regex pattern in the RegExp constructor directly
interpolates the key variable without escaping, which means regex metacharacters
in the key will be treated as operators rather than literals. Before
constructing the pattern, escape the key using a regex escape utility function
(such as escaping characters like . * + ? [ ] ( ) etc.) so that special
characters are treated as literal characters in the pattern match. This prevents
keys with regex metacharacters from unintentionally changing the regex behavior.
- Around line 396-397: The calls to installShadcnComponents and setupIntent
functions are executing without checking whether the install flag is disabled in
options. Add a conditional check before both function calls to verify that
install is not set to false (i.e., check if options.install is not explicitly
false) to prevent running these networked commands when skip-install mode is
enabled.

In `@packages/create/src/edge-environment.ts`:
- Around line 37-42: The startRun method clears the output object but does not
reset the persistent in-memory files map, causing state leakage when the same
environment instance is reused for multiple runs. In the startRun method, in
addition to clearing the output properties, also reset the files map to an empty
object to ensure each run starts with a clean state and prevent carryover of
files from previous runs.
- Around line 98-103: The `exists` and `isDirectory` methods fail to handle root
path cases (represented as `.`) because `hasDirectory` expects a `./` prefix
that normalized file keys never contain. Add explicit checks in both the
`exists` and `isDirectory` functions to detect when the input path represents
the root directory (normalize to `.` or empty string after normalization) and
return `true` immediately for these cases, since the root always exists and is a
directory. Only call `hasDirectory` for non-root paths.

In `@packages/create/src/edge-file-helpers.ts`:
- Around line 38-43: In the relativePath function, normalize the to parameter
the same way as from by applying the backslash-to-forward-slash replacement
before processing it. Currently, from is normalized with replace(/\\/g, '/') but
to is used directly without this normalization. Add a line to normalize to by
replacing backslashes with forward slashes, similar to how from is normalized,
to ensure consistent path handling regardless of whether paths use backslashes
or forward slashes as separators.
- Around line 18-25: The toCleanPath function uses startsWith for path matching
without ensuring path segment boundaries, which causes incorrect slicing when
there's a partial match (e.g., normalizing /apple/file with base /app
incorrectly yields le/file). Fix this by adding a boundary check after the
startsWith match in both the normalizedPath.startsWith(normalizedBase) condition
and the pathNoDrive.startsWith(baseNoDrive) condition. Ensure the character
immediately following the matched base is either a path separator or the end of
the string before performing the slice operation.

In `@packages/create/src/edge-package-json.ts`:
- Around line 114-118: The additions array in the createPackageJSON function is
hardcoding the inclusion of TypeScript and Tailwind optional packages regardless
of user selection. Modify the additions array to conditionally include
options.framework.optionalPackages.typescript and
options.framework.optionalPackages.tailwindcss only when those options have been
explicitly selected by the user (check options.typescript and
options.tailwindcss flags). Similarly, update any template rendering logic
(referenced in lines 131-137) to use the actual selected options instead of
hardcoded TypeScript/TSX values, ensuring the correct file extensions and
configurations are used based on what the user chose.
- Around line 153-160: The catch block in the try-catch statement around
JSON.parse(render(addOn.packageTemplate, templateValues)) only logs the error
and continues execution, which allows incomplete packageJSON data to persist and
cause failures later. After logging the error with console.error, throw the
caught error to fail fast at the source of the problem rather than silently
continuing with invalid addOnPackageJSON data.

In `@packages/create/src/edge-template-file.ts`:
- Line 183: The file.replace method in the line with convertDotFilesAndPaths
will remove all instances of `.ejs` from the filename, not just the file
extension at the end. Fix this by modifying the replace call to use a regular
expression pattern that matches `.ejs` only at the end of the string using the
`$` anchor, so that filenames containing `.ejs` in the middle are left
unchanged. This ensures only the actual `.ejs` template extension is removed.
- Around line 127-133: The templateValues object in the edge-template-file.ts
file has hardcoded TypeScript values (typescript: true, js: 'ts', jsx: 'tsx')
that ignore the user's language preference. Replace these hardcoded values with
conditional values based on options.typescript: set typescript to
options.typescript, js to either 'ts' or 'js' depending on whether
options.typescript is true, and jsx to either 'tsx' or 'jsx' depending on
whether options.typescript is true. This ensures the correct file extensions and
flags are used for both TypeScript and JavaScript projects.

---

Nitpick comments:
In `@docs/cli-reference.md`:
- Around line 47-85: Add documentation for the includeExamples option in the
Worker-safe programmatic generation example. Include the includeExamples
property in the options object passed to the createApp function call to
demonstrate how users can control example code generation. Add a brief comment
or explanation in the documentation explaining that this option controls whether
example files are generated during app creation, helping users understand they
can set it to true or false based on their needs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: affd348c-b1c2-4ad1-a65e-e44c84b3871c

📥 Commits

Reviewing files that changed from the base of the PR and between b0c8ec9 and 14dc2cc.

📒 Files selected for processing (22)
  • .changeset/slow-buses-build.md
  • .gitignore
  • docs/cli-reference.md
  • eslint.config.js
  • packages/create/package.json
  • packages/create/scripts/generate-manifest.mjs
  • packages/create/src/edge-add-ons.ts
  • packages/create/src/edge-config-file.ts
  • packages/create/src/edge-create-app.ts
  • packages/create/src/edge-environment.ts
  • packages/create/src/edge-file-helpers.ts
  • packages/create/src/edge-frameworks.ts
  • packages/create/src/edge-package-json.ts
  • packages/create/src/edge-path.ts
  • packages/create/src/edge-render.ts
  • packages/create/src/edge-template-file.ts
  • packages/create/src/edge.ts
  • packages/create/src/manifest-types.ts
  • packages/create/src/manifest.ts
  • packages/create/src/types.ts
  • packages/create/tests/edge-import.test.ts
  • packages/create/tests/edge-manifest.test.ts

Comment on lines 27 to +33
"scripts": {
"build": "tsc && npm run copy-assets",
"build": "npm run generate-manifest && tsc && npm run copy-assets",
"generate-manifest": "node ./scripts/generate-manifest.mjs",
"copy-assets": "node -e \"const fs=require('fs');const path=require('path');function copyDir(src,dest){if(!fs.existsSync(dest))fs.mkdirSync(dest,{recursive:true});for(const entry of fs.readdirSync(src,{withFileTypes:true})){const srcPath=path.join(src,entry.name);const destPath=path.join(dest,entry.name);if(entry.isDirectory())copyDir(srcPath,destPath);else fs.copyFileSync(srcPath,destPath)}}['react','solid'].forEach(fw=>{['add-ons','toolchains','hosts','examples','project'].forEach(dir=>{const src='src/frameworks/'+fw+'/'+dir;const dest='dist/frameworks/'+fw+'/'+dir;if(fs.existsSync(src))copyDir(src,dest)})})\"",
"dev": "tsc --watch",
"test": "eslint ./src && vitest run",
"test:watch": "vitest",
"dev": "npm run generate-manifest && tsc --watch",
"test": "npm run generate-manifest && eslint ./src && vitest run",
"test:watch": "npm run generate-manifest && vitest",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

test:coverage should also run manifest generation in clean environments.

This block correctly adds codegen pre-steps for build/dev/test flows, but coverage still misses that prerequisite and can fail when src/generated/create-manifest.ts is absent.

Proposed fix
-    "test:coverage": "vitest run --coverage"
+    "test:coverage": "npm run generate-manifest && vitest run --coverage"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/create/package.json` around lines 27 - 33, The test:coverage script
is missing the manifest generation prerequisite that other scripts include. Add
"npm run generate-manifest &&" to the beginning of the test:coverage script
command to ensure the generated manifest file exists before running coverage
tests, following the same pattern used in the build, dev, test, and test:watch
scripts.

Comment on lines +76 to +83
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const file = resolve(dir, entry.name)
if (entry.isDirectory()) {
visit(file)
} else {
files[toCleanPath(file, baseDir)] = readTemplateFile(file)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Sort directory entries before iterating to keep manifest generation deterministic.

Line [76] and Line [113] depend on raw readdirSync iteration order, which is not guaranteed to be stable across filesystems. That can produce different manifest/add-on ordering between environments.

Proposed fix
 function findFilesRecursively(baseDir) {
   const files = {}

   if (!existsSync(baseDir)) {
     return files
   }

   function visit(dir) {
-    for (const entry of readdirSync(dir, { withFileTypes: true })) {
+    const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) =>
+      a.name.localeCompare(b.name),
+    )
+    for (const entry of entries) {
       const file = resolve(dir, entry.name)
       if (entry.isDirectory()) {
         visit(file)
       } else {
         files[toCleanPath(file, baseDir)] = readTemplateFile(file)
@@
-  for (const entry of readdirSync(addOnsBase, { withFileTypes: true })) {
+  const entries = readdirSync(addOnsBase, { withFileTypes: true }).sort(
+    (a, b) => a.name.localeCompare(b.name),
+  )
+  for (const entry of entries) {
     if (!entry.isDirectory()) {
       continue
     }

Also applies to: 113-116

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/create/scripts/generate-manifest.mjs` around lines 76 - 83, The
readdirSync call in the visit function iterates over directory entries in
filesystem order, which is non-deterministic across different environments and
causes inconsistent manifest generation. Sort the array returned by readdirSync
before iterating over it with the for loop. Apply the same sorting fix to the
other location mentioned (around lines 113-116) where readdirSync is also used
without sorting to ensure deterministic manifest ordering regardless of
filesystem.

Comment on lines +36 to +38
} else if (addOnID.startsWith('http')) {
addOn = await loadRemoteAddOn(addOnID)
addOns.push(addOn)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the edge-add-ons.ts file
find . -name "edge-add-ons.ts" -type f

Repository: TanStack/cli

Length of output: 94


🏁 Script executed:

# Read the edge-add-ons.ts file to understand the context
cat -n packages/create/src/edge-add-ons.ts

Repository: TanStack/cli

Length of output: 4727


🏁 Script executed:

# Check file size first
wc -l packages/create/src/edge-add-ons.ts

Repository: TanStack/cli

Length of output: 96


🏁 Script executed:

# Read the entire file
cat packages/create/src/edge-add-ons.ts

Repository: TanStack/cli

Length of output: 3761


🏁 Script executed:

# Search for usages of finalizeAddOns to understand if chosenAddOnIDs comes from external input
rg "finalizeAddOns" --type ts --type js -B 2 -A 2

Repository: TanStack/cli

Length of output: 5808


🏁 Script executed:

# Search for callers of loadRemoteAddOn and finalizeAddOns
rg "finalizeAddOns|loadRemoteAddOn" --type ts --type js -B 3 -A 3

Repository: TanStack/cli

Length of output: 11282


🏁 Script executed:

# Check if there's also a non-edge add-ons.ts with similar code
head -100 packages/create/src/add-ons.ts

Repository: TanStack/cli

Length of output: 2904


🏁 Script executed:

# Look at loadRemoteAddOn in packages/create/src/custom-add-ons/add-on.ts to see if different
cat packages/create/src/custom-add-ons/add-on.ts

Repository: TanStack/cli

Length of output: 8273


Harden remote add-on URL fetching (SSRF risk in hosted edge flows).

Any http* add-on ID is fetched directly without URL validation. If chosenAddOnIDs originates from external request input (possible in hosted/edge deployments), this enables SSRF attacks against localhost services, cloud metadata endpoints, or internal IP ranges. Additionally, non-2xx responses trigger JSON parsing without status checks, allowing error responses to be processed as add-on data.

Suggested hardening baseline
 export async function loadRemoteAddOn(url: string): Promise<AddOn> {
-  const response = await fetch(url)
+  const parsed = new URL(url)
+  if (parsed.protocol !== 'https:') {
+    throw new Error(`Only https add-on URLs are allowed: ${url}`)
+  }
+  if (['localhost', '127.0.0.1', '::1'].includes(parsed.hostname)) {
+    throw new Error(`Local add-on URLs are not allowed: ${url}`)
+  }
+
+  const response = await fetch(parsed.toString())
+  if (!response.ok) {
+    throw new Error(`Failed to fetch add-on: ${url} (${response.status})`)
+  }
   const jsonContent = await response.json()

Also applies to: 76-79

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/create/src/edge-add-ons.ts` around lines 36 - 38, The code fetches
remote add-ons via loadRemoteAddOn(addOnID) without validating the URL, creating
an SSRF vulnerability if addOnID originates from external input. Additionally,
loadRemoteAddOn likely processes HTTP responses as JSON without checking status
codes. Add URL validation before calling loadRemoteAddOn to reject localhost
addresses, private IP ranges, and cloud metadata endpoints. Modify
loadRemoteAddOn to check the HTTP response status code and only parse JSON for
2xx responses, rejecting error responses. Apply these same hardening fixes to
the similar remote add-on loading code at lines 76-79.

Comment on lines +17 to +24
const { chosenAddOns, framework, targetDir: _targetDir, ...rest } = options
return {
...rest,
version: 1,
framework: framework.id,
chosenAddOns: chosenAddOns.map((addOn) => addOn.id),
starter: options.starter?.id ?? undefined,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid persisting secret/runtime-only fields into the config file.

Using ...rest here includes fields like envVarValues (potential secrets) and addOns in .cta.json, which violates the intended persisted shape and increases leak risk.

Suggested fix
 function createPersistedOptions(options: Options): PersistedOptions {
-  const { chosenAddOns, framework, targetDir: _targetDir, ...rest } = options
+  const {
+    chosenAddOns,
+    framework,
+    starter,
+    addOns: _addOns,
+    envVarValues: _envVarValues,
+    targetDir: _targetDir,
+    ...rest
+  } = options
   return {
     ...rest,
     version: 1,
     framework: framework.id,
     chosenAddOns: chosenAddOns.map((addOn) => addOn.id),
-    starter: options.starter?.id ?? undefined,
+    starter: starter?.id ?? undefined,
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { chosenAddOns, framework, targetDir: _targetDir, ...rest } = options
return {
...rest,
version: 1,
framework: framework.id,
chosenAddOns: chosenAddOns.map((addOn) => addOn.id),
starter: options.starter?.id ?? undefined,
}
function createPersistedOptions(options: Options): PersistedOptions {
const {
chosenAddOns,
framework,
starter,
addOns: _addOns,
envVarValues: _envVarValues,
targetDir: _targetDir,
...rest
} = options
return {
...rest,
version: 1,
framework: framework.id,
chosenAddOns: chosenAddOns.map((addOn) => addOn.id),
starter: starter?.id ?? undefined,
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/create/src/edge-config-file.ts` around lines 17 - 24, The return
object in this function uses the `...rest` spread operator which inadvertently
includes sensitive fields like `envVarValues` and `addOns` in the persisted
config file, creating a security risk. Instead of spreading rest, explicitly
list only the safe fields that should be persisted in the edge config file.
Destructure out `envVarValues` and `addOns` from the options parameter along
with the other fields already being destructured, then return only the intended
config fields (version, framework as an id, chosenAddOns as ids mapped from the
objects, and starter as an id if present) without using the spread operator for
remaining fields.

Comment on lines +396 to +397
await installShadcnComponents(environment, options.targetDir, options)
await setupIntent(environment, options.targetDir, options)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Honor install: false for post-generation command execution.

When install is disabled, the flow still runs shadcn@latest add and @tanstack/intent install --map, which triggers networked command execution despite skip-install mode.

Suggested fix
-  await installShadcnComponents(environment, options.targetDir, options)
-  await setupIntent(environment, options.targetDir, options)
+  if (options.install !== false) {
+    await installShadcnComponents(environment, options.targetDir, options)
+    await setupIntent(environment, options.targetDir, options)
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await installShadcnComponents(environment, options.targetDir, options)
await setupIntent(environment, options.targetDir, options)
if (options.install !== false) {
await installShadcnComponents(environment, options.targetDir, options)
await setupIntent(environment, options.targetDir, options)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/create/src/edge-create-app.ts` around lines 396 - 397, The calls to
installShadcnComponents and setupIntent functions are executing without checking
whether the install flag is disabled in options. Add a conditional check before
both function calls to verify that install is not set to false (i.e., check if
options.install is not explicitly false) to prevent running these networked
commands when skip-install mode is enabled.

Comment on lines +38 to +43
const normalized = from.replace(/\\/g, '/')
const cleanedFrom = normalized.startsWith('./')
? normalized.slice(2)
: normalized
const cleanedTo = to.startsWith('./') ? to.slice(2) : to

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize to in relativePath the same way as from.

Line [38] normalizes from, but Line [42] uses to raw. With backslash paths, segment splitting and extension stripping become inconsistent and can produce incorrect import paths.

Proposed fix
 export function relativePath(
   from: string,
   to: string,
   stripExtension: boolean = false,
 ) {
   const normalized = from.replace(/\\/g, '/')
+  const normalizedTo = to.replace(/\\/g, '/')
   const cleanedFrom = normalized.startsWith('./')
     ? normalized.slice(2)
     : normalized
-  const cleanedTo = to.startsWith('./') ? to.slice(2) : to
+  const cleanedTo = normalizedTo.startsWith('./')
+    ? normalizedTo.slice(2)
+    : normalizedTo
@@
-  const target = stripExtension ? to.replace(extnamePath(to), '') : to
+  const target = stripExtension
+    ? normalizedTo.replace(extnamePath(normalizedTo), '')
+    : normalizedTo

Also applies to: 61-61

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/create/src/edge-file-helpers.ts` around lines 38 - 43, In the
relativePath function, normalize the to parameter the same way as from by
applying the backslash-to-forward-slash replacement before processing it.
Currently, from is normalized with replace(/\\/g, '/') but to is used directly
without this normalization. Add a line to normalize to by replacing backslashes
with forward slashes, similar to how from is normalized, to ensure consistent
path handling regardless of whether paths use backslashes or forward slashes as
separators.

Comment on lines +114 to +118
const additions: Array<Record<string, unknown> | undefined> = [
options.framework.optionalPackages.typescript,
options.framework.optionalPackages.tailwindcss,
options.mode ? options.framework.optionalPackages[options.mode] : undefined,
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the selected TypeScript/Tailwind options instead of hardcoded values.

createPackageJSON currently always merges TS/Tailwind optional packages and always renders add-on package templates as TS/TSX. That produces incorrect package.json output for JS/no-tailwind configurations.

Suggested fix
-  const additions: Array<Record<string, unknown> | undefined> = [
-    options.framework.optionalPackages.typescript,
-    options.framework.optionalPackages.tailwindcss,
-    options.mode ? options.framework.optionalPackages[options.mode] : undefined,
-  ]
+  const additions: Array<Record<string, unknown> | undefined> = [
+    options.typescript ? options.framework.optionalPackages.typescript : undefined,
+    options.tailwind ? options.framework.optionalPackages.tailwindcss : undefined,
+    options.mode ? options.framework.optionalPackages[options.mode] : undefined,
+  ]
@@
-        typescript: true,
-        tailwind: true,
-        js: 'ts',
-        jsx: 'tsx',
+        typescript: options.typescript === true,
+        tailwind: options.tailwind === true,
+        js: options.typescript ? 'ts' : 'js',
+        jsx: options.typescript ? 'tsx' : 'jsx',

Also applies to: 131-137

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/create/src/edge-package-json.ts` around lines 114 - 118, The
additions array in the createPackageJSON function is hardcoding the inclusion of
TypeScript and Tailwind optional packages regardless of user selection. Modify
the additions array to conditionally include
options.framework.optionalPackages.typescript and
options.framework.optionalPackages.tailwindcss only when those options have been
explicitly selected by the user (check options.typescript and
options.tailwindcss flags). Similarly, update any template rendering logic
(referenced in lines 131-137) to use the actual selected options instead of
hardcoded TypeScript/TSX values, ensuring the correct file extensions and
configurations are used based on what the user chose.

Comment on lines +153 to +160
try {
addOnPackageJSON = JSON.parse(render(addOn.packageTemplate, templateValues))
} catch (error) {
console.error(
`Error processing package.json.ejs for add-on ${addOn.id}:`,
error,
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail fast when an add-on packageTemplate cannot be rendered/parsed.

The current catch block logs and continues, which can silently emit an incomplete package.json and fail much later in command execution.

Suggested fix
-      } catch (error) {
-        console.error(
-          `Error processing package.json.ejs for add-on ${addOn.id}:`,
-          error,
-        )
-      }
+      } catch (error) {
+        throw new Error(
+          `Failed to process package.json template for add-on "${addOn.id}"`,
+        )
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
addOnPackageJSON = JSON.parse(render(addOn.packageTemplate, templateValues))
} catch (error) {
console.error(
`Error processing package.json.ejs for add-on ${addOn.id}:`,
error,
)
}
try {
addOnPackageJSON = JSON.parse(render(addOn.packageTemplate, templateValues))
} catch (error) {
throw new Error(
`Failed to process package.json template for add-on "${addOn.id}"`,
)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/create/src/edge-package-json.ts` around lines 153 - 160, The catch
block in the try-catch statement around JSON.parse(render(addOn.packageTemplate,
templateValues)) only logs the error and continues execution, which allows
incomplete packageJSON data to persist and cause failures later. After logging
the error with console.error, throw the caught error to fail fast at the source
of the problem rather than silently continuing with invalid addOnPackageJSON
data.

Comment on lines +127 to +133
const templateValues = {
packageManager: options.packageManager,
projectName: options.projectName,
typescript: true,
tailwind: true,
js: 'ts',
jsx: 'tsx',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use options.typescript for template flags instead of hardcoded TS values.

Template rendering is currently always TS-flavored (typescript/js/jsx), even when JS mode is selected, which can generate incorrect rendered content for non-TS projects.

Suggested fix
     const templateValues = {
       packageManager: options.packageManager,
       projectName: options.projectName,
-      typescript: true,
+      typescript: options.typescript,
       tailwind: true,
-      js: 'ts',
-      jsx: 'tsx',
+      js: options.typescript ? 'ts' : 'js',
+      jsx: options.typescript ? 'tsx' : 'jsx',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const templateValues = {
packageManager: options.packageManager,
projectName: options.projectName,
typescript: true,
tailwind: true,
js: 'ts',
jsx: 'tsx',
const templateValues = {
packageManager: options.packageManager,
projectName: options.projectName,
typescript: options.typescript,
tailwind: true,
js: options.typescript ? 'ts' : 'js',
jsx: options.typescript ? 'tsx' : 'jsx',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/create/src/edge-template-file.ts` around lines 127 - 133, The
templateValues object in the edge-template-file.ts file has hardcoded TypeScript
values (typescript: true, js: 'ts', jsx: 'tsx') that ignore the user's language
preference. Replace these hardcoded values with conditional values based on
options.typescript: set typescript to options.typescript, js to either 'ts' or
'js' depending on whether options.typescript is true, and jsx to either 'tsx' or
'jsx' depending on whether options.typescript is true. This ensures the correct
file extensions and flags are used for both TypeScript and JavaScript projects.

return
}

let target = convertDotFilesAndPaths(file.replace('.ejs', ''))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Only strip .ejs when it is the file suffix.

Using file.replace('.ejs', '') can rename non-template files that merely contain .ejs in the middle of the filename.

Suggested fix
-    let target = convertDotFilesAndPaths(file.replace('.ejs', ''))
+    const sourcePath = file.endsWith('.ejs') ? file.slice(0, -4) : file
+    let target = convertDotFilesAndPaths(sourcePath)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let target = convertDotFilesAndPaths(file.replace('.ejs', ''))
const sourcePath = file.endsWith('.ejs') ? file.slice(0, -4) : file
let target = convertDotFilesAndPaths(sourcePath)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/create/src/edge-template-file.ts` at line 183, The file.replace
method in the line with convertDotFilesAndPaths will remove all instances of
`.ejs` from the filename, not just the file extension at the end. Fix this by
modifying the replace call to use a regular expression pattern that matches
`.ejs` only at the end of the string using the `$` anchor, so that filenames
containing `.ejs` in the middle are left unchanged. This ensures only the actual
`.ejs` template extension is removed.

@tannerlinsley tannerlinsley merged commit 87c718d into main Jun 20, 2026
7 checks passed
@tannerlinsley tannerlinsley deleted the taren/create-edge-manifest branch June 20, 2026 06:18
@github-actions github-actions Bot mentioned this pull request Jun 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants