diff --git a/.github/workflows/nextjs-tracker.yml b/.github/workflows/nextjs-tracker.yml
index 5c2c87d45..3bafa8d00 100644
--- a/.github/workflows/nextjs-tracker.yml
+++ b/.github/workflows/nextjs-tracker.yml
@@ -39,8 +39,9 @@ jobs:
id: commits
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SINCE_HOURS: ${{ github.event.inputs.since_hours || '24' }}
run: |
- HOURS="${{ github.event.inputs.since_hours || '24' }}"
+ HOURS="$SINCE_HOURS"
SINCE=$(date -u -d "${HOURS} hours ago" +%Y-%m-%dT%H:%M:%SZ)
echo "Fetching Next.js canary commits since $SINCE"
@@ -70,8 +71,10 @@ jobs:
- name: Build tracker prompt
id: build_prompt
if: steps.commits.outputs.has_commits == 'true'
+ env:
+ DRY_RUN_INPUT: ${{ github.event.inputs.dry_run || 'false' }}
run: |
- DRY_RUN="${{ github.event.inputs.dry_run || 'false' }}"
+ DRY_RUN="$DRY_RUN_INPUT"
COMMITS=$(cat /tmp/nextjs-commits.json)
if [ "$DRY_RUN" = "true" ]; then
@@ -134,4 +137,6 @@ jobs:
- name: Skip (no commits)
if: steps.commits.outputs.has_commits == 'false'
- run: echo "No Next.js canary commits in the last ${{ github.event.inputs.since_hours || '24' }} hours. Nothing to do."
+ env:
+ SINCE_HOURS: ${{ github.event.inputs.since_hours || '24' }}
+ run: echo "No Next.js canary commits in the last $SINCE_HOURS hours. Nothing to do."
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 6db2a17e4..20064c796 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -44,13 +44,15 @@ jobs:
- name: Bump version
id: version
working-directory: packages/vinext
+ env:
+ BUMP: ${{ inputs.bump }}
run: |
# vp wraps package-manager subcommands, but versioning still needs the real npm CLI
# because we are mutating package.json in place before the publish step.
LATEST=$(npm view vinext version 2>/dev/null || echo "0.0.0")
IFS='.' read -r MAJOR MINOR PATCH <<< "$LATEST"
- case "${{ inputs.bump }}" in
+ case "$BUMP" in
major)
MAJOR=$((MAJOR + 1))
MINOR=0
diff --git a/examples/hackernews/components/comment.jsx b/examples/hackernews/components/comment.jsx
index 0176b5992..1e89094e1 100644
--- a/examples/hackernews/components/comment.jsx
+++ b/examples/hackernews/components/comment.jsx
@@ -1,6 +1,7 @@
'use client';
import { useState } from 'react';
+import DOMPurify from 'dompurify';
import timeAgo from '../lib/time-ago';
@@ -26,7 +27,7 @@ export default function Comment({ user, text, date, comments, commentsCount }) {
,
{comments.map((comment) => (
diff --git a/examples/hackernews/package.json b/examples/hackernews/package.json
index 1cd765f89..d98ba6433 100644
--- a/examples/hackernews/package.json
+++ b/examples/hackernews/package.json
@@ -1,26 +1,28 @@
{
- "name": "vinext-hackernews",
- "type": "module",
- "private": true,
- "scripts": {
- "dev": "vp dev",
- "build": "vp build",
- "preview": "vp preview"
- },
- "dependencies": {
- "@vitejs/plugin-react": "catalog:",
- "ms": "catalog:",
- "react": "catalog:",
- "react-dom": "catalog:",
- "server-only": "catalog:",
- "vite": "catalog:",
- "vinext": "workspace:*",
- "@vitejs/plugin-rsc": "catalog:",
- "react-server-dom-webpack": "catalog:",
- "@cloudflare/vite-plugin": "catalog:",
- "wrangler": "catalog:"
- },
- "devDependencies": {
- "vite-plus": "catalog:"
- }
+ "name": "vinext-hackernews",
+ "type": "module",
+ "private": true,
+ "scripts": {
+ "dev": "vp dev",
+ "build": "vp build",
+ "preview": "vp preview"
+ },
+ "dependencies": {
+ "@vitejs/plugin-react": "catalog:",
+ "dompurify": "catalog:",
+ "ms": "catalog:",
+ "react": "catalog:",
+ "react-dom": "catalog:",
+ "server-only": "catalog:",
+ "vite": "catalog:",
+ "vinext": "workspace:*",
+ "@vitejs/plugin-rsc": "catalog:",
+ "react-server-dom-webpack": "catalog:",
+ "@cloudflare/vite-plugin": "catalog:",
+ "wrangler": "catalog:"
+ },
+ "devDependencies": {
+ "@cloudflare/workers-types": "catalog:",
+ "vite-plus": "catalog:"
+ }
}
diff --git a/examples/hackernews/tsconfig.json b/examples/hackernews/tsconfig.json
index 7ccdce365..29b540b89 100644
--- a/examples/hackernews/tsconfig.json
+++ b/examples/hackernews/tsconfig.json
@@ -8,8 +8,15 @@
"esModuleInterop": true,
"skipLibCheck": true,
"allowJs": true,
- "baseUrl": ".",
- "types": ["@cloudflare/workers-types"]
+ "noEmit": true,
+ "types": [
+ "@cloudflare/workers-types"
+ ]
},
- "include": ["app", "components", "lib", "worker"]
-}
+ "include": [
+ "app",
+ "components",
+ "lib",
+ "worker"
+ ]
+}
\ No newline at end of file
diff --git a/packages/vinext/src/shims/head.ts b/packages/vinext/src/shims/head.ts
index cc7ecad21..0c9d4492d 100644
--- a/packages/vinext/src/shims/head.ts
+++ b/packages/vinext/src/shims/head.ts
@@ -272,7 +272,9 @@ export function escapeAttr(s: string): string {
* context but prevents the HTML parser from seeing a closing tag.
*/
export function escapeInlineContent(content: string, tag: string): string {
- // Build a pattern like `<\/script` or `<\/style`, case-insensitive
+ // Build a pattern like `<\/script` or `<\/style`, case-insensitive.
+ // `tag` is always a literal developer-controlled value ("script" or "style")
+ // guarded by the RAW_CONTENT_TAGS.has(tag) check at all call sites — never user input.
const pattern = new RegExp(`<\\/(${tag})`, "gi");
return content.replace(pattern, "<\\/$1");
}
diff --git a/packages/vinext/src/shims/script.tsx b/packages/vinext/src/shims/script.tsx
index 02bb7bbbd..aa3a6395c 100644
--- a/packages/vinext/src/shims/script.tsx
+++ b/packages/vinext/src/shims/script.tsx
@@ -156,6 +156,11 @@ function Script(props: ScriptProps): React.ReactElement | null {
}
if (dangerouslySetInnerHTML?.__html) {
+ // Intentional: mirrors the Next.js