Skip to content

Commit 5d3261f

Browse files
committed
chore(deps): upgrade docx to 9.15.2
docx 9.15 packs with fflate and encodes base64 with the global btoa, which the isolated-vm sandbox does not provide, so the docx-generate finalize (Packer.toBase64String + hand-rolled decode) threw ReferenceError. Pack with Packer.toArrayBuffer instead, like the pptx/pdf tasks, and cover the real bundle + bootstrap + finalize in a bare isolate-shaped context. docx now drops XML-1.0-illegal control characters itself, so the Word block's pre-strip before Markdown parsing is removed. Regenerated the docx sandbox bundle.
1 parent d7f3158 commit 5d3261f

10 files changed

Lines changed: 74 additions & 96 deletions

File tree

‎apps/sim/lib/execution/sandbox/bundles/build.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
*
1010
* Every bundle is evaluated in a bare context before it is written: the
1111
* bundler can emit a reference to a runtime helper it never defines (Bun does
12-
* this for docx's inlined CommonJS shim), and nothing else loads these files
12+
* this for an inlined CommonJS shim), and nothing else loads these files
1313
* before a production document generation does.
1414
*
1515
* Run via: `bun run build:sandbox-bundles`.

‎apps/sim/lib/execution/sandbox/bundles/docx.cjs‎

Lines changed: 9 additions & 39 deletions
Large diffs are not rendered by default.

‎apps/sim/lib/execution/sandbox/bundles/isolate-polyfills.ts‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,10 @@ if (typeof g.global === 'undefined') g.global = globalThis
2424

2525
/**
2626
* A library that inlines a CommonJS dependency ships esbuild's `__require`
27-
* helper around it (docx >= 9.7.1 does this for JSZip's UMD build). Bun's
28-
* browser/iife build rewrites the bare `require` references inside that helper
29-
* to its own `__require` runtime helper and then never emits it, so the bundle
30-
* throws `ReferenceError: __require is not defined` while it is still being
31-
* evaluated. The isolate has no `require` at all, so the only correct answer
27+
* helper around it. Bun's browser/iife build rewrites the bare `require`
28+
* references inside that helper to its own `__require` runtime helper and
29+
* then never emits it, so the bundle throws `ReferenceError: __require is not
30+
* defined` while it is still being evaluated. The isolate has no `require` at all, so the only correct answer
3231
* to a dynamic require is the one esbuild's helper gives when `require` is
3332
* absent: throw. Defining it here keeps every bundle self-contained; `build.ts`
3433
* evaluates each bundle in a bare context so a new variant of the defect fails

‎apps/sim/lib/execution/sandbox/bundles/verify.test.ts‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { readFileSync } from 'node:fs'
2+
import vm from 'node:vm'
3+
import JSZip from 'jszip'
24
import { describe, expect, it } from 'vitest'
3-
import { evaluateSandboxBundle } from '@/lib/execution/sandbox/bundles/verify'
5+
import {
6+
createBareSandboxContext,
7+
evaluateSandboxBundle,
8+
} from '@/lib/execution/sandbox/bundles/verify'
49
import type { SandboxBundleName } from '@/lib/execution/sandbox/types'
10+
import { docxGenerateTask } from '@/sandboxes/docx-generate'
511

612
function loadCheckedInBundle(name: SandboxBundleName): Record<string, unknown> {
713
const source = readFileSync(new URL(`./${name}.cjs`, import.meta.url), 'utf-8')
@@ -23,6 +29,26 @@ describe('sandbox bundles', () => {
2329
expect(typeof docx.Paragraph).toBe('function')
2430
})
2531

32+
it('docx-generate packs a document with the checked-in bundle in a bare context', async () => {
33+
const context = createBareSandboxContext()
34+
evaluateSandboxBundle(
35+
readFileSync(new URL('./docx.cjs', import.meta.url), 'utf-8'),
36+
'docx',
37+
context
38+
)
39+
const runAsync = (body: string) => vm.runInContext(`(async () => { ${body} })()`, context)
40+
41+
await runAsync(docxGenerateTask.bootstrap)
42+
await runAsync(
43+
"addSection({ children: [new docx.Paragraph({ children: [new docx.TextRun('R&D <ok>')] })] })"
44+
)
45+
const bytes: unknown = await runAsync(docxGenerateTask.finalize)
46+
47+
expect(ArrayBuffer.isView(bytes)).toBe(true)
48+
const zip = await JSZip.loadAsync(bytes as Uint8Array)
49+
expect(await zip.file('word/document.xml')?.async('string')).toContain('R&amp;D &lt;ok&gt;')
50+
})
51+
2652
it('pdf-lib evaluates in a bare context and exposes the pdf-generate surface', () => {
2753
const pdfLib = loadCheckedInBundle('pdf-lib')
2854
expect(typeof pdfLib.PDFDocument).toBe('function')

‎apps/sim/lib/execution/sandbox/bundles/verify.ts‎

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,12 @@ import vm from 'node:vm'
22
import type { SandboxBundleName } from '@/lib/execution/sandbox/types'
33

44
/**
5-
* Evaluates a built sandbox bundle the way the isolated-vm worker will: as a
6-
* classic script in a context that has timers, `console`, and the text codecs
7-
* but no `require`, `process`, or `Buffer` of its own. Returns the export the
8-
* bundle registered on `globalThis.__bundles`, or throws with the bundle's own
9-
* error, so a bundle that references a helper the bundler never emitted fails
10-
* at build time and in the test suite instead of on the first document
11-
* generated in production.
5+
* A context shaped like the isolated-vm worker's: timers, `console`, and the
6+
* text codecs, but no `require`, `process`, `Buffer`, `atob`, or `btoa` of its
7+
* own. Code that reaches for any other Node or browser global fails here the
8+
* same way it fails inside the isolate.
129
*/
13-
export function evaluateSandboxBundle(source: string, name: SandboxBundleName): unknown {
10+
export function createBareSandboxContext(): vm.Context {
1411
const context: Record<string, unknown> = {
1512
setTimeout,
1613
clearTimeout,
@@ -22,7 +19,22 @@ export function evaluateSandboxBundle(source: string, name: SandboxBundleName):
2219
TextDecoder,
2320
}
2421
context.globalThis = context
25-
vm.createContext(context)
22+
return vm.createContext(context)
23+
}
24+
25+
/**
26+
* Evaluates a built sandbox bundle the way the isolated-vm worker will: as a
27+
* classic script in a {@link createBareSandboxContext} context. Returns the
28+
* export the bundle registered on `globalThis.__bundles`, or throws with the
29+
* bundle's own error, so a bundle that references a helper the bundler never
30+
* emitted fails at build time and in the test suite instead of on the first
31+
* document generated in production.
32+
*/
33+
export function evaluateSandboxBundle(
34+
source: string,
35+
name: SandboxBundleName,
36+
context: vm.Context = createBareSandboxContext()
37+
): unknown {
2638
vm.runInContext(source, context, { filename: `sandbox/${name}.cjs` })
2739

2840
const bundles = context.__bundles

‎apps/sim/lib/microsoft-word/document.server.test.ts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,6 @@ describe('buildDocxFromContent', () => {
9191
})
9292

9393
it('strips control characters that would make the package unopenable', async () => {
94-
// The docx package writes run text into the XML verbatim, so a forbidden
95-
// character reaching it produces invalid XML 1.0 rather than a rendering bug.
9694
const buffer = await buildDocxFromContent(`before${String.fromCharCode(8)}after`)
9795
const xml = await readDocumentXml(buffer)
9896

‎apps/sim/lib/microsoft-word/document.server.ts‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,7 @@ function parseInlineRuns(line: string): ContentRun[] {
8383
function parseContentBlocks(content: string): ContentBlock[] {
8484
const blocks: ContentBlock[] = []
8585

86-
// Stripped once here rather than per run: the `docx` package writes run text
87-
// into the XML verbatim, so a stray control character from an upstream block
88-
// would produce a package Word refuses to open. Tab, newline, and carriage
89-
// return are legal XML and are outside the class, so the split below is
90-
// unaffected.
91-
for (const rawLine of stripInvalidXmlChars(content).replace(/\r\n?/g, '\n').split('\n')) {
86+
for (const rawLine of content.replace(/\r\n?/g, '\n').split('\n')) {
9287
const line = rawLine.trimEnd()
9388
if (line.trim().length === 0) continue
9489

‎apps/sim/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@
211211
"date-fns": "4.4.0",
212212
"decimal.js": "10.6.0",
213213
"diff": "9.0.0",
214-
"docx": "^9.6.1",
214+
"docx": "^9.15.2",
215215
"docx-preview": "^0.4.1",
216216
"drizzle-orm": "^0.45.4",
217217
"echarts": "6.1.0",

‎apps/sim/sandboxes/docx-generate.ts‎

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,6 @@ export const docxGenerateTask = defineSandboxTask<SandboxTaskInput>({
7878
}));
7979
};
8080
`,
81-
// JSZip's browser build doesn't support nodebuffer output, so we go through
82-
// base64 and decode back to bytes inside the isolate (avoids DataURL / Blob).
8381
finalize: `
8482
let doc = globalThis.doc;
8583
if (!doc && globalThis.__docxSections.length > 0) {
@@ -91,23 +89,7 @@ export const docxGenerateTask = defineSandboxTask<SandboxTaskInput>({
9189
if (!doc) {
9290
throw new Error('No document created. Use addSection({ children: [...] }) for chunked writes, or set globalThis.doc = new docx.Document({...}) for a single write.');
9391
}
94-
const b64 = await globalThis.docx.Packer.toBase64String(doc);
95-
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
96-
const lookup = new Uint8Array(128);
97-
for (let i = 0; i < alphabet.length; i++) lookup[alphabet.charCodeAt(i)] = i;
98-
const clean = b64.replace(/=+$/, '');
99-
const out = new Uint8Array(Math.floor((clean.length * 3) / 4));
100-
let pos = 0;
101-
for (let i = 0; i < clean.length; i += 4) {
102-
const c0 = lookup[clean.charCodeAt(i)];
103-
const c1 = lookup[clean.charCodeAt(i + 1)];
104-
const c2 = lookup[clean.charCodeAt(i + 2)];
105-
const c3 = lookup[clean.charCodeAt(i + 3)];
106-
out[pos++] = (c0 << 2) | (c1 >> 4);
107-
if (i + 2 < clean.length) out[pos++] = ((c1 & 0x0f) << 4) | (c2 >> 2);
108-
if (i + 3 < clean.length) out[pos++] = ((c2 & 0x03) << 6) | c3;
109-
}
110-
return out.subarray(0, pos);
92+
return new Uint8Array(await globalThis.docx.Packer.toArrayBuffer(doc));
11193
`,
11294
toResult: (bytes) => Buffer.from(bytes),
11395
})

‎bun.lock‎

Lines changed: 9 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)