From dbe1e081152868c94baba8420554fa7c1c2d83e6 Mon Sep 17 00:00:00 2001 From: jycouet Date: Fri, 31 Oct 2025 13:07:22 +0100 Subject: [PATCH 1/7] rmv console log! --- packages/addons/drizzle/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/addons/drizzle/index.ts b/packages/addons/drizzle/index.ts index 00495fc7..b455218e 100644 --- a/packages/addons/drizzle/index.ts +++ b/packages/addons/drizzle/index.ts @@ -94,7 +94,6 @@ export default defineAddon({ return cancel(`Preexisting ${fileType} file at '${filePath}'`); } } - console.log(`no preexisting files`); sv.devDependency('drizzle-orm', '^0.44.6'); sv.devDependency('drizzle-kit', '^0.31.5'); sv.devDependency('@types/node', getNodeTypesVersion()); From 335144f8746850f86db92a506bba1d70027c4cf2 Mon Sep 17 00:00:00 2001 From: jycouet Date: Fri, 31 Oct 2025 14:06:12 +0100 Subject: [PATCH 2/7] tmp test rolldown vs tsdown --- demo-script-tag-escape/README.md | 49 ++++++ demo-script-tag-escape/dist-rolldown/index.js | 29 ++++ demo-script-tag-escape/dist-tsdown/index.js | 26 ++++ demo-script-tag-escape/package.json | 14 ++ demo-script-tag-escape/rolldown.config.js | 9 ++ demo-script-tag-escape/src/index.ts | 38 +++++ demo-script-tag-escape/test.js | 142 ++++++++++++++++++ demo-script-tag-escape/tsdown.config.ts | 7 + 8 files changed, 314 insertions(+) create mode 100644 demo-script-tag-escape/README.md create mode 100644 demo-script-tag-escape/dist-rolldown/index.js create mode 100644 demo-script-tag-escape/dist-tsdown/index.js create mode 100644 demo-script-tag-escape/package.json create mode 100644 demo-script-tag-escape/rolldown.config.js create mode 100644 demo-script-tag-escape/src/index.ts create mode 100644 demo-script-tag-escape/test.js create mode 100644 demo-script-tag-escape/tsdown.config.ts diff --git a/demo-script-tag-escape/README.md b/demo-script-tag-escape/README.md new file mode 100644 index 00000000..55faec29 --- /dev/null +++ b/demo-script-tag-escape/README.md @@ -0,0 +1,49 @@ +# Script Tag Escaping Demo + +This demo project demonstrates how different bundlers (rolldown vs tsdown) handle template literals containing `` tags when using tagged template literal syntax vs function call syntax. + +## The Issue + +When using `dedent`...`` (tagged template literal syntax) with `` tags, some bundlers may escape them to `<\/script>` in the source code. While JavaScript correctly unescapes these at runtime in most cases, the escaping can sometimes cause issues depending on how the code is processed or written to files. + +## Setup + +```bash +npm install +``` + +## Run the Demo + +```bash +# Build with rolldown +npm run build:rolldown + +# Build with tsdown +npm run build:tsdown + +# Compare the outputs +npm test +``` + +## What You'll See + +Both bundlers escape `` to `<\/script>` in the **source code** (the bundled `.js` files), but JavaScript correctly unescapes them at **runtime** when the code executes. + +However, the key difference is: +- **In the bundled source**: Both show `<\/script>` (escaped) +- **At runtime**: Both produce correct `` strings +- **The risk**: If the escaped version gets written to files or processed in unexpected ways, you'd see `<\/script>` in the output + +## Code Being Tested + +The `src/index.ts` file contains a simple `dedent` function (no external dependency needed) and tests: + +1. **Tagged template literal**: `dedent`...`` - Shows escaping in source code +2. **Function call**: `dedent(...)` - Also shows escaping in source, but more predictable behavior + +## Why This Matters + +In the context of sv 0.9.7, when the CLI switched from `rolldown` to `tsdown`, the bundling behavior changed. While both technically work at runtime, using `dedent(...)` function call syntax is more predictable and avoids any edge cases where the escaped version might leak into file output. + +This demonstrates why using `dedent(...)` instead of `dedent`...`` is the safer approach when dealing with `` tags in code generators. + diff --git a/demo-script-tag-escape/dist-rolldown/index.js b/demo-script-tag-escape/dist-rolldown/index.js new file mode 100644 index 00000000..2853ab5b --- /dev/null +++ b/demo-script-tag-escape/dist-rolldown/index.js @@ -0,0 +1,29 @@ + +//#region src/index.ts +function dedent(strings, ...values) { + if (typeof strings === "string") return strings.trim().replace(/^\n+|\n+$/g, ""); + let result = strings[0]; + for (let i = 0; i < values.length; i++) result += values[i] + strings[i + 1]; + return result.trim().replace(/^\n+|\n+$/g, ""); +} +function testTaggedTemplateLiteral() { + const result = dedent` + + `; + return result; +} +function testFunctionCall() { + const result = dedent(` + + `); + return result; +} +const taggedResult = testTaggedTemplateLiteral(); +const functionResult = testFunctionCall(); + +//#endregion +export { functionResult, taggedResult, testFunctionCall, testTaggedTemplateLiteral }; \ No newline at end of file diff --git a/demo-script-tag-escape/dist-tsdown/index.js b/demo-script-tag-escape/dist-tsdown/index.js new file mode 100644 index 00000000..ae4b96e7 --- /dev/null +++ b/demo-script-tag-escape/dist-tsdown/index.js @@ -0,0 +1,26 @@ +//#region src/index.ts +function dedent(strings, ...values) { + if (typeof strings === "string") return strings.trim().replace(/^\n+|\n+$/g, ""); + let result = strings[0]; + for (let i = 0; i < values.length; i++) result += values[i] + strings[i + 1]; + return result.trim().replace(/^\n+|\n+$/g, ""); +} +function testTaggedTemplateLiteral() { + return dedent` + + `; + return result; +} + +// Test with function call syntax (dedent(...)) +export function testFunctionCall() { + const result = dedent(` + + `); + return result; +} + +// Export both to be used in test +export const taggedResult = testTaggedTemplateLiteral(); +export const functionResult = testFunctionCall(); diff --git a/demo-script-tag-escape/test.js b/demo-script-tag-escape/test.js new file mode 100644 index 00000000..f25ebc80 --- /dev/null +++ b/demo-script-tag-escape/test.js @@ -0,0 +1,142 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +console.log('=== Script Tag Escaping Demo ===\n'); + +// Read the built files +const rolldownPath = path.join(__dirname, 'dist-rolldown', 'index.js'); +const tsdownPath = path.join(__dirname, 'dist-tsdown', 'index.js'); + +if (!fs.existsSync(rolldownPath)) { + console.log('āŒ Rolldown build not found. Run: npm run build:rolldown'); + process.exit(1); +} + +if (!fs.existsSync(tsdownPath)) { + console.log('āŒ Tsdown build not found. Run: npm run build:tsdown'); + process.exit(1); +} + +const rolldownCode = fs.readFileSync(rolldownPath, 'utf-8'); +const tsdownCode = fs.readFileSync(tsdownPath, 'utf-8'); + +console.log('šŸ“¦ ROLLDOWN BUNDLED CODE (excerpt):'); +console.log('─'.repeat(80)); +// Find script tags in the rolldown output +const rolldownScriptMatches = [ + ...rolldownCode.matchAll(/]*>[\s\S]*?<\/script>/g), + ...rolldownCode.matchAll(/<\\\/script>/g) +]; +if (rolldownScriptMatches.length > 0) { + const match = rolldownScriptMatches[0][0]; + console.log(match.substring(0, 150) + (match.length > 150 ? '...' : '')); +} else { + // Show a snippet around dedent or script tag + const scriptIndex = rolldownCode.indexOf(''); + const escapedIndex = rolldownCode.indexOf('<\\/script>'); + const searchIndex = scriptIndex !== -1 ? scriptIndex : escapedIndex !== -1 ? escapedIndex : -1; + if (searchIndex !== -1) { + const snippet = rolldownCode.substring(Math.max(0, searchIndex - 50), searchIndex + 20); + console.log(snippet); + } else { + console.log('No script tag found'); + } +} + +console.log('\n'); + +console.log('šŸ“¦ TSDOWN BUNDLED CODE (excerpt):'); +console.log('─'.repeat(80)); +// Find script tags in the tsdown output +const tsdownScriptMatches = [ + ...tsdownCode.matchAll(/]*>[\s\S]*?<\/script>/g), + ...tsdownCode.matchAll(/<\\\/script>/g) +]; +if (tsdownScriptMatches.length > 0) { + const match = tsdownScriptMatches[0][0]; + console.log(match.substring(0, 150) + (match.length > 150 ? '...' : '')); +} else { + // Show a snippet around script tag + const scriptIndex = tsdownCode.indexOf(''); + const escapedIndex = tsdownCode.indexOf('<\\/script>'); + const searchIndex = scriptIndex !== -1 ? scriptIndex : escapedIndex !== -1 ? escapedIndex : -1; + if (searchIndex !== -1) { + const snippet = tsdownCode.substring(Math.max(0, searchIndex - 50), searchIndex + 20); + console.log(snippet); + } else { + console.log('No script tag found'); + } +} + +console.log('\n'); + +// Check for escaped script tags +console.log('šŸ” CHECKING FOR ESCAPED SCRIPT TAGS IN BUNDLED CODE:'); +console.log('─'.repeat(80)); + +const rolldownHasEscaped = + rolldownCode.includes('<\\/script>') || rolldownCode.includes(''.replace('/', '\\/')); +const tsdownHasEscaped = + tsdownCode.includes('<\\/script>') || tsdownCode.includes(''.replace('/', '\\/')); + +console.log( + `Rolldown: ${rolldownHasEscaped ? 'āŒ HAS ESCAPED (found <\\/script>)' : 'āœ… No escaping detected'}` +); +console.log( + `Tsdown: ${tsdownHasEscaped ? 'āŒ HAS ESCAPED (found <\\/script>)' : 'āœ… No escaping detected'}` +); + +console.log('\n'); + +// Try to run the actual code +console.log('šŸš€ RUNTIME BEHAVIOR:'); +console.log('─'.repeat(80)); + +try { + const rolldownModule = await import(path.join(__dirname, 'dist-rolldown', 'index.js')); + const tsdownModule = await import(path.join(__dirname, 'dist-tsdown', 'index.js')); + + console.log( + '\nRolldown taggedResult:', + rolldownModule.taggedResult.includes('<\\/script>') ? 'āŒ ESCAPED' : 'āœ… Not escaped' + ); + console.log( + 'Rolldown functionResult:', + rolldownModule.functionResult.includes('<\\/script>') ? 'āŒ ESCAPED' : 'āœ… Not escaped' + ); + console.log( + '\nTsdown taggedResult:', + tsdownModule.taggedResult.includes('<\\/script>') ? 'āŒ ESCAPED' : 'āœ… Not escaped' + ); + console.log( + 'Tsdown functionResult:', + tsdownModule.functionResult.includes('<\\/script>') ? 'āŒ ESCAPED' : 'āœ… Not escaped' + ); + + if ( + rolldownModule.taggedResult.includes('<\\/script>') || + tsdownModule.taggedResult.includes('<\\/script>') + ) { + console.log('\nšŸ’” DEMONSTRATION: Tagged template literal syntax causes escaping!'); + } + if ( + !rolldownModule.functionResult.includes('<\\/script>') && + !tsdownModule.functionResult.includes('<\\/script>') + ) { + console.log('āœ… Function call syntax prevents escaping!'); + } +} catch (e) { + console.log('Could not execute built code:', e.message); + console.log('(This is okay - we can still see the difference in the bundled code above)'); +} + +console.log('\n'); +console.log('šŸ’” CONCLUSION:'); +console.log('─'.repeat(80)); +console.log( + 'When bundlers process tagged template literals (dedent`...`), they may escape ' +); +console.log('Using function call syntax (dedent(...)) avoids this escaping behavior.'); diff --git a/demo-script-tag-escape/tsdown.config.ts b/demo-script-tag-escape/tsdown.config.ts new file mode 100644 index 00000000..e56dbeef --- /dev/null +++ b/demo-script-tag-escape/tsdown.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + outDir: 'dist-tsdown' +}); From 5e12ea1f15abf55860791ea22dbdb459a0840c05 Mon Sep 17 00:00:00 2001 From: jycouet Date: Fri, 31 Oct 2025 14:34:07 +0100 Subject: [PATCH 3/7] literal of function, both are failing! --- packages/core/tests/utils.ts | 82 ++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/packages/core/tests/utils.ts b/packages/core/tests/utils.ts index 01139fbc..d8eede73 100644 --- a/packages/core/tests/utils.ts +++ b/packages/core/tests/utils.ts @@ -269,3 +269,85 @@ describe('yaml', () => { `); }); }); + +test('tsdown escapes script tags in bundled source code', async () => { + const { execSync } = await import('node:child_process'); + const fs = await import('node:fs'); + const path = await import('node:path'); + + const testDir = path.join('../..', '.test-output', `tsdown-test`); + fs.rmSync(testDir, { recursive: true, force: true }); + fs.mkdirSync(testDir, { recursive: true }); + + // Create a test file that uses dedent with script tags + const testFileLiteral = path.join(testDir, 'testLiteral.ts'); + fs.writeFileSync( + testFileLiteral, + `import dedent from 'dedent'; + +export const result = dedent\` + +\`; +` + ); + + const testFileFunction = path.join(testDir, 'testFunction.ts'); + fs.writeFileSync( + testFileFunction, + `import dedent from 'dedent'; + +export const result = dedent(\` + +\`); +` + ); + + // Create a tsdown config + const configFile = path.join(testDir, 'tsdown.config.ts'); + fs.writeFileSync( + configFile, + `import { defineConfig } from 'tsdown'; + +export default defineConfig({ + entry: ['testLiteral.ts', 'testFunction.ts'], + format: ['esm'], + outDir: 'dist', +}); +` + ); + + // Create package.json with tsdown + const pkgJson = { + name: 'test', + type: 'module', + devDependencies: { + tsdown: '^0.15.2', + dedent: '^1.6.0' + } + }; + fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify(pkgJson, null, 2)); + + // Install dependencies and build + execSync('npm install', { cwd: testDir, stdio: 'pipe' }); + execSync('npx tsdown', { cwd: testDir, stdio: 'pipe' }); + + // Read the bundled output + const bundledFileLiteral = path.join(testDir, 'dist', 'testLiteral.js'); + const bundledFileFunction = path.join(testDir, 'dist', 'testFunction.js'); + const bundledCodeLiteral = fs.readFileSync(bundledFileLiteral, 'utf-8'); + const bundledCodeFunction = fs.readFileSync(bundledFileFunction, 'utf-8'); + + // Check if the bundled code contains escaped script tags + const hasEscapedScriptTagLiteral = bundledCodeLiteral.includes('<\\/script>'); + const hasEscapedScriptTagFunction = bundledCodeFunction.includes('<\\/script>'); + + // This test demonstrates the issue: tsdown escapes in the bundled source + // Expected: Bundled code should NOT contain escaped script tags + // Actual: Bundled code contains <\/script> when using dedent`...` syntax + expect(hasEscapedScriptTagLiteral).toBe(true); + expect(hasEscapedScriptTagFunction).toBe(false); +}, 30000); // 30s timeout for npm install and build From 7f9a388382f672214d5ba866f15c9ef99a7b5d86 Mon Sep 17 00:00:00 2001 From: jycouet Date: Fri, 31 Oct 2025 14:40:45 +0100 Subject: [PATCH 4/7] simplify --- demo-script-tag-escape/package.json | 5 +- demo-script-tag-escape/test.js | 150 +++++++++------------------- 2 files changed, 48 insertions(+), 107 deletions(-) diff --git a/demo-script-tag-escape/package.json b/demo-script-tag-escape/package.json index c83f05ea..9bc331bc 100644 --- a/demo-script-tag-escape/package.json +++ b/demo-script-tag-escape/package.json @@ -3,12 +3,13 @@ "version": "1.0.0", "type": "module", "scripts": { + "build": "npm run build:rolldown && npm run build:tsdown", "build:rolldown": "rolldown --config rolldown.config.js", "build:tsdown": "tsdown", - "test": "node test.js" + "test": "npm run build && node test.js" }, "devDependencies": { - "rolldown": "1.0.0-beta.45", + "rolldown": "1.0.0-beta.1", "tsdown": "^0.15.2" } } diff --git a/demo-script-tag-escape/test.js b/demo-script-tag-escape/test.js index f25ebc80..24baec00 100644 --- a/demo-script-tag-escape/test.js +++ b/demo-script-tag-escape/test.js @@ -23,117 +23,57 @@ if (!fs.existsSync(tsdownPath)) { const rolldownCode = fs.readFileSync(rolldownPath, 'utf-8'); const tsdownCode = fs.readFileSync(tsdownPath, 'utf-8'); -console.log('šŸ“¦ ROLLDOWN BUNDLED CODE (excerpt):'); +console.log('šŸ“Š SUMMARY: ESCAPING DETECTION IN BUNDLED CODE'); console.log('─'.repeat(80)); -// Find script tags in the rolldown output -const rolldownScriptMatches = [ - ...rolldownCode.matchAll(/]*>[\s\S]*?<\/script>/g), - ...rolldownCode.matchAll(/<\\\/script>/g) -]; -if (rolldownScriptMatches.length > 0) { - const match = rolldownScriptMatches[0][0]; - console.log(match.substring(0, 150) + (match.length > 150 ? '...' : '')); -} else { - // Show a snippet around dedent or script tag - const scriptIndex = rolldownCode.indexOf(''); - const escapedIndex = rolldownCode.indexOf('<\\/script>'); - const searchIndex = scriptIndex !== -1 ? scriptIndex : escapedIndex !== -1 ? escapedIndex : -1; - if (searchIndex !== -1) { - const snippet = rolldownCode.substring(Math.max(0, searchIndex - 50), searchIndex + 20); - console.log(snippet); +console.log(''); + +// More precise check: look for escaped script tags in specific functions +const checkEscaping = (code, isLiteral) => { + if (isLiteral) { + // Find testTaggedTemplateLiteral function - look for dedent` followed by escaped script tag + const funcStart = code.indexOf('function testTaggedTemplateLiteral()'); + if (funcStart === -1) return 'āœ… No escaping detected'; + const funcEnd = code.indexOf('function testFunctionCall()', funcStart); + const funcCode = + funcEnd !== -1 ? code.substring(funcStart, funcEnd) : code.substring(funcStart); + + // Check if it uses dedent` (tagged template) and has escaped script tag + const usesTagged = funcCode.includes('dedent`'); + const hasEscaped = funcCode.includes('<\\/script>'); + return usesTagged && hasEscaped ? 'āŒ found <\\/script>' : 'āœ… No escaping detected'; } else { - console.log('No script tag found'); + // Find testFunctionCall function - look for dedent( followed by script tag + const funcStart = code.indexOf('function testFunctionCall()'); + if (funcStart === -1) return 'āœ… No escaping detected'; + const funcEnd = code.indexOf('const taggedResult', funcStart); + const funcCode = + funcEnd !== -1 + ? code.substring(funcStart, funcEnd) + : code.substring(funcStart, funcStart + 200); + + // Check if it uses dedent( (function call) and has escaped script tag + // Function call syntax should NOT have escaping since it's a regular string argument + const usesFunction = funcCode.includes('dedent('); + const hasEscaped = funcCode.includes('<\\/script>'); + + // Note: Some bundlers may still escape even in function calls, but the issue + // is specifically with tagged template literals + if (!usesFunction) return 'āœ… No escaping detected'; + return hasEscaped ? 'āŒ found <\\/script>' : 'āœ… No escaping detected'; } -} - -console.log('\n'); - -console.log('šŸ“¦ TSDOWN BUNDLED CODE (excerpt):'); -console.log('─'.repeat(80)); -// Find script tags in the tsdown output -const tsdownScriptMatches = [ - ...tsdownCode.matchAll(/]*>[\s\S]*?<\/script>/g), - ...tsdownCode.matchAll(/<\\\/script>/g) -]; -if (tsdownScriptMatches.length > 0) { - const match = tsdownScriptMatches[0][0]; - console.log(match.substring(0, 150) + (match.length > 150 ? '...' : '')); -} else { - // Show a snippet around script tag - const scriptIndex = tsdownCode.indexOf(''); - const escapedIndex = tsdownCode.indexOf('<\\/script>'); - const searchIndex = scriptIndex !== -1 ? scriptIndex : escapedIndex !== -1 ? escapedIndex : -1; - if (searchIndex !== -1) { - const snippet = tsdownCode.substring(Math.max(0, searchIndex - 50), searchIndex + 20); - console.log(snippet); - } else { - console.log('No script tag found'); - } -} - -console.log('\n'); - -// Check for escaped script tags -console.log('šŸ” CHECKING FOR ESCAPED SCRIPT TAGS IN BUNDLED CODE:'); -console.log('─'.repeat(80)); +}; -const rolldownHasEscaped = - rolldownCode.includes('<\\/script>') || rolldownCode.includes(''.replace('/', '\\/')); -const tsdownHasEscaped = - tsdownCode.includes('<\\/script>') || tsdownCode.includes(''.replace('/', '\\/')); +console.log(' │ Literal (dedent`...`) │ Function (dedent(...))'); +console.log('────────────┼───────────────────────┼────────────────────────'); +const rolldownLiteral = checkEscaping(rolldownCode, true); +const rolldownFunction = checkEscaping(rolldownCode, false); +const tsdownLiteral = checkEscaping(tsdownCode, true); +const tsdownFunction = checkEscaping(tsdownCode, false); -console.log( - `Rolldown: ${rolldownHasEscaped ? 'āŒ HAS ESCAPED (found <\\/script>)' : 'āœ… No escaping detected'}` -); -console.log( - `Tsdown: ${tsdownHasEscaped ? 'āŒ HAS ESCAPED (found <\\/script>)' : 'āœ… No escaping detected'}` -); - -console.log('\n'); - -// Try to run the actual code -console.log('šŸš€ RUNTIME BEHAVIOR:'); -console.log('─'.repeat(80)); - -try { - const rolldownModule = await import(path.join(__dirname, 'dist-rolldown', 'index.js')); - const tsdownModule = await import(path.join(__dirname, 'dist-tsdown', 'index.js')); - - console.log( - '\nRolldown taggedResult:', - rolldownModule.taggedResult.includes('<\\/script>') ? 'āŒ ESCAPED' : 'āœ… Not escaped' - ); - console.log( - 'Rolldown functionResult:', - rolldownModule.functionResult.includes('<\\/script>') ? 'āŒ ESCAPED' : 'āœ… Not escaped' - ); - console.log( - '\nTsdown taggedResult:', - tsdownModule.taggedResult.includes('<\\/script>') ? 'āŒ ESCAPED' : 'āœ… Not escaped' - ); - console.log( - 'Tsdown functionResult:', - tsdownModule.functionResult.includes('<\\/script>') ? 'āŒ ESCAPED' : 'āœ… Not escaped' - ); - - if ( - rolldownModule.taggedResult.includes('<\\/script>') || - tsdownModule.taggedResult.includes('<\\/script>') - ) { - console.log('\nšŸ’” DEMONSTRATION: Tagged template literal syntax causes escaping!'); - } - if ( - !rolldownModule.functionResult.includes('<\\/script>') && - !tsdownModule.functionResult.includes('<\\/script>') - ) { - console.log('āœ… Function call syntax prevents escaping!'); - } -} catch (e) { - console.log('Could not execute built code:', e.message); - console.log('(This is okay - we can still see the difference in the bundled code above)'); -} +console.log(`Rolldown │ ${rolldownLiteral.padEnd(23)}│ ${rolldownFunction}`); +console.log(`Tsdown │ ${tsdownLiteral.padEnd(23)}│ ${tsdownFunction}`); +console.log(''); -console.log('\n'); console.log('šŸ’” CONCLUSION:'); console.log('─'.repeat(80)); console.log( From 708aec59a1bc23997cc2a2c208b584b4ba7f8bda Mon Sep 17 00:00:00 2001 From: jycouet Date: Fri, 31 Oct 2025 14:42:25 +0100 Subject: [PATCH 5/7] clean --- demo-script-tag-escape/test.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/demo-script-tag-escape/test.js b/demo-script-tag-escape/test.js index 24baec00..e310ab06 100644 --- a/demo-script-tag-escape/test.js +++ b/demo-script-tag-escape/test.js @@ -63,8 +63,8 @@ const checkEscaping = (code, isLiteral) => { } }; -console.log(' │ Literal (dedent`...`) │ Function (dedent(...))'); -console.log('────────────┼───────────────────────┼────────────────────────'); +console.log(' │ Literal (dedent`...`) │ Function (dedent(`...`))'); +console.log('─────────────┼─────────────────────────┼────────────────────────'); const rolldownLiteral = checkEscaping(rolldownCode, true); const rolldownFunction = checkEscaping(rolldownCode, false); const tsdownLiteral = checkEscaping(tsdownCode, true); @@ -73,10 +73,3 @@ const tsdownFunction = checkEscaping(tsdownCode, false); console.log(`Rolldown │ ${rolldownLiteral.padEnd(23)}│ ${rolldownFunction}`); console.log(`Tsdown │ ${tsdownLiteral.padEnd(23)}│ ${tsdownFunction}`); console.log(''); - -console.log('šŸ’” CONCLUSION:'); -console.log('─'.repeat(80)); -console.log( - 'When bundlers process tagged template literals (dedent`...`), they may escape ' -); -console.log('Using function call syntax (dedent(...)) avoids this escaping behavior.'); From 747fe5ab06ed457e1d97ec7872b577cfafed6c35 Mon Sep 17 00:00:00 2001 From: jycouet Date: Fri, 31 Oct 2025 17:13:20 +0100 Subject: [PATCH 6/7] rmv tmp stuff --- demo-script-tag-escape/README.md | 49 ------------ demo-script-tag-escape/dist-rolldown/index.js | 29 ------- demo-script-tag-escape/dist-tsdown/index.js | 26 ------- demo-script-tag-escape/package.json | 15 ---- demo-script-tag-escape/rolldown.config.js | 9 --- demo-script-tag-escape/src/index.ts | 38 ---------- demo-script-tag-escape/test.js | 75 ------------------- demo-script-tag-escape/tsdown.config.ts | 7 -- 8 files changed, 248 deletions(-) delete mode 100644 demo-script-tag-escape/README.md delete mode 100644 demo-script-tag-escape/dist-rolldown/index.js delete mode 100644 demo-script-tag-escape/dist-tsdown/index.js delete mode 100644 demo-script-tag-escape/package.json delete mode 100644 demo-script-tag-escape/rolldown.config.js delete mode 100644 demo-script-tag-escape/src/index.ts delete mode 100644 demo-script-tag-escape/test.js delete mode 100644 demo-script-tag-escape/tsdown.config.ts diff --git a/demo-script-tag-escape/README.md b/demo-script-tag-escape/README.md deleted file mode 100644 index 55faec29..00000000 --- a/demo-script-tag-escape/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# Script Tag Escaping Demo - -This demo project demonstrates how different bundlers (rolldown vs tsdown) handle template literals containing `` tags when using tagged template literal syntax vs function call syntax. - -## The Issue - -When using `dedent`...`` (tagged template literal syntax) with `` tags, some bundlers may escape them to `<\/script>` in the source code. While JavaScript correctly unescapes these at runtime in most cases, the escaping can sometimes cause issues depending on how the code is processed or written to files. - -## Setup - -```bash -npm install -``` - -## Run the Demo - -```bash -# Build with rolldown -npm run build:rolldown - -# Build with tsdown -npm run build:tsdown - -# Compare the outputs -npm test -``` - -## What You'll See - -Both bundlers escape `` to `<\/script>` in the **source code** (the bundled `.js` files), but JavaScript correctly unescapes them at **runtime** when the code executes. - -However, the key difference is: -- **In the bundled source**: Both show `<\/script>` (escaped) -- **At runtime**: Both produce correct `` strings -- **The risk**: If the escaped version gets written to files or processed in unexpected ways, you'd see `<\/script>` in the output - -## Code Being Tested - -The `src/index.ts` file contains a simple `dedent` function (no external dependency needed) and tests: - -1. **Tagged template literal**: `dedent`...`` - Shows escaping in source code -2. **Function call**: `dedent(...)` - Also shows escaping in source, but more predictable behavior - -## Why This Matters - -In the context of sv 0.9.7, when the CLI switched from `rolldown` to `tsdown`, the bundling behavior changed. While both technically work at runtime, using `dedent(...)` function call syntax is more predictable and avoids any edge cases where the escaped version might leak into file output. - -This demonstrates why using `dedent(...)` instead of `dedent`...`` is the safer approach when dealing with `` tags in code generators. - diff --git a/demo-script-tag-escape/dist-rolldown/index.js b/demo-script-tag-escape/dist-rolldown/index.js deleted file mode 100644 index 2853ab5b..00000000 --- a/demo-script-tag-escape/dist-rolldown/index.js +++ /dev/null @@ -1,29 +0,0 @@ - -//#region src/index.ts -function dedent(strings, ...values) { - if (typeof strings === "string") return strings.trim().replace(/^\n+|\n+$/g, ""); - let result = strings[0]; - for (let i = 0; i < values.length; i++) result += values[i] + strings[i + 1]; - return result.trim().replace(/^\n+|\n+$/g, ""); -} -function testTaggedTemplateLiteral() { - const result = dedent` - - `; - return result; -} -function testFunctionCall() { - const result = dedent(` - - `); - return result; -} -const taggedResult = testTaggedTemplateLiteral(); -const functionResult = testFunctionCall(); - -//#endregion -export { functionResult, taggedResult, testFunctionCall, testTaggedTemplateLiteral }; \ No newline at end of file diff --git a/demo-script-tag-escape/dist-tsdown/index.js b/demo-script-tag-escape/dist-tsdown/index.js deleted file mode 100644 index ae4b96e7..00000000 --- a/demo-script-tag-escape/dist-tsdown/index.js +++ /dev/null @@ -1,26 +0,0 @@ -//#region src/index.ts -function dedent(strings, ...values) { - if (typeof strings === "string") return strings.trim().replace(/^\n+|\n+$/g, ""); - let result = strings[0]; - for (let i = 0; i < values.length; i++) result += values[i] + strings[i + 1]; - return result.trim().replace(/^\n+|\n+$/g, ""); -} -function testTaggedTemplateLiteral() { - return dedent` - - `; - return result; -} - -// Test with function call syntax (dedent(...)) -export function testFunctionCall() { - const result = dedent(` - - `); - return result; -} - -// Export both to be used in test -export const taggedResult = testTaggedTemplateLiteral(); -export const functionResult = testFunctionCall(); diff --git a/demo-script-tag-escape/test.js b/demo-script-tag-escape/test.js deleted file mode 100644 index e310ab06..00000000 --- a/demo-script-tag-escape/test.js +++ /dev/null @@ -1,75 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -console.log('=== Script Tag Escaping Demo ===\n'); - -// Read the built files -const rolldownPath = path.join(__dirname, 'dist-rolldown', 'index.js'); -const tsdownPath = path.join(__dirname, 'dist-tsdown', 'index.js'); - -if (!fs.existsSync(rolldownPath)) { - console.log('āŒ Rolldown build not found. Run: npm run build:rolldown'); - process.exit(1); -} - -if (!fs.existsSync(tsdownPath)) { - console.log('āŒ Tsdown build not found. Run: npm run build:tsdown'); - process.exit(1); -} - -const rolldownCode = fs.readFileSync(rolldownPath, 'utf-8'); -const tsdownCode = fs.readFileSync(tsdownPath, 'utf-8'); - -console.log('šŸ“Š SUMMARY: ESCAPING DETECTION IN BUNDLED CODE'); -console.log('─'.repeat(80)); -console.log(''); - -// More precise check: look for escaped script tags in specific functions -const checkEscaping = (code, isLiteral) => { - if (isLiteral) { - // Find testTaggedTemplateLiteral function - look for dedent` followed by escaped script tag - const funcStart = code.indexOf('function testTaggedTemplateLiteral()'); - if (funcStart === -1) return 'āœ… No escaping detected'; - const funcEnd = code.indexOf('function testFunctionCall()', funcStart); - const funcCode = - funcEnd !== -1 ? code.substring(funcStart, funcEnd) : code.substring(funcStart); - - // Check if it uses dedent` (tagged template) and has escaped script tag - const usesTagged = funcCode.includes('dedent`'); - const hasEscaped = funcCode.includes('<\\/script>'); - return usesTagged && hasEscaped ? 'āŒ found <\\/script>' : 'āœ… No escaping detected'; - } else { - // Find testFunctionCall function - look for dedent( followed by script tag - const funcStart = code.indexOf('function testFunctionCall()'); - if (funcStart === -1) return 'āœ… No escaping detected'; - const funcEnd = code.indexOf('const taggedResult', funcStart); - const funcCode = - funcEnd !== -1 - ? code.substring(funcStart, funcEnd) - : code.substring(funcStart, funcStart + 200); - - // Check if it uses dedent( (function call) and has escaped script tag - // Function call syntax should NOT have escaping since it's a regular string argument - const usesFunction = funcCode.includes('dedent('); - const hasEscaped = funcCode.includes('<\\/script>'); - - // Note: Some bundlers may still escape even in function calls, but the issue - // is specifically with tagged template literals - if (!usesFunction) return 'āœ… No escaping detected'; - return hasEscaped ? 'āŒ found <\\/script>' : 'āœ… No escaping detected'; - } -}; - -console.log(' │ Literal (dedent`...`) │ Function (dedent(`...`))'); -console.log('─────────────┼─────────────────────────┼────────────────────────'); -const rolldownLiteral = checkEscaping(rolldownCode, true); -const rolldownFunction = checkEscaping(rolldownCode, false); -const tsdownLiteral = checkEscaping(tsdownCode, true); -const tsdownFunction = checkEscaping(tsdownCode, false); - -console.log(`Rolldown │ ${rolldownLiteral.padEnd(23)}│ ${rolldownFunction}`); -console.log(`Tsdown │ ${tsdownLiteral.padEnd(23)}│ ${tsdownFunction}`); -console.log(''); diff --git a/demo-script-tag-escape/tsdown.config.ts b/demo-script-tag-escape/tsdown.config.ts deleted file mode 100644 index e56dbeef..00000000 --- a/demo-script-tag-escape/tsdown.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'tsdown'; - -export default defineConfig({ - entry: ['src/index.ts'], - format: ['esm'], - outDir: 'dist-tsdown' -}); From 0f82523a5b435dbb0f28cadf4eee898d129dea15 Mon Sep 17 00:00:00 2001 From: jycouet Date: Fri, 31 Oct 2025 17:28:08 +0100 Subject: [PATCH 7/7] fix(cli): generating closing tags now works correctly --- .changeset/wide-geese-smoke.md | 5 +++++ packages/cli/lib/install.ts | 3 ++- packages/core/tests/utils.ts | 5 +++-- 3 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 .changeset/wide-geese-smoke.md diff --git a/.changeset/wide-geese-smoke.md b/.changeset/wide-geese-smoke.md new file mode 100644 index 00000000..5c58af8a --- /dev/null +++ b/.changeset/wide-geese-smoke.md @@ -0,0 +1,5 @@ +--- +'sv': patch +--- + +fix(cli): generating closing tags now works correctly diff --git a/packages/cli/lib/install.ts b/packages/cli/lib/install.ts index aafd6446..e11c3057 100644 --- a/packages/cli/lib/install.ts +++ b/packages/cli/lib/install.ts @@ -142,7 +142,8 @@ async function runAddon({ addon, multiple, workspace }: RunAddon) { fileContent = content(fileContent); if (!fileContent) return fileContent; - writeFile(workspace, path, fileContent); + // TODO: fix https://github.com/rolldown/tsdown/issues/575 to remove the `replaceAll` + writeFile(workspace, path, fileContent.replaceAll('<\\/script>', '')); files.add(path); } catch (e) { if (e instanceof Error) { diff --git a/packages/core/tests/utils.ts b/packages/core/tests/utils.ts index d8eede73..10350d1a 100644 --- a/packages/core/tests/utils.ts +++ b/packages/core/tests/utils.ts @@ -270,7 +270,8 @@ describe('yaml', () => { }); }); -test('tsdown escapes script tags in bundled source code', async () => { +// TODO: fix https://github.com/rolldown/tsdown/issues/575 to remove the `skip` +test.skip('tsdown escapes script tags in bundled source code', async () => { const { execSync } = await import('node:child_process'); const fs = await import('node:fs'); const path = await import('node:path'); @@ -348,6 +349,6 @@ export default defineConfig({ // This test demonstrates the issue: tsdown escapes in the bundled source // Expected: Bundled code should NOT contain escaped script tags // Actual: Bundled code contains <\/script> when using dedent`...` syntax - expect(hasEscapedScriptTagLiteral).toBe(true); + expect(hasEscapedScriptTagLiteral).toBe(false); expect(hasEscapedScriptTagFunction).toBe(false); }, 30000); // 30s timeout for npm install and build