Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions .github/scripts/prepare-docx-upstream.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import assert from 'node:assert/strict'
import { readFile, writeFile } from 'node:fs/promises'
import { spawnSync } from 'node:child_process'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'

const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
const manifestPath = 'packages/renderers/word/package.json'
const factsPaths = [
'apps/official-site/public/llms.txt',
'apps/official-site/public/llms-full.txt',
'docs/guide/faq.md',
'docs/zh/guide/faq.md'
]
export function prepareDocxTexts(texts, version) {
assert.match(
version,
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/,
'Pass an exact published stable DOCX version, not a range, URL or tag'
)
const manifest = JSON.parse(texts[manifestPath])
const previous = manifest.dependencies['@file-viewer/docx']
assert.match(previous, /^\d+\.\d+\.\d+$/)
const result = { ...texts }
manifest.dependencies['@file-viewer/docx'] = version
result[manifestPath] = JSON.stringify(manifest, null, 2) + '\n'
const path = 'packages/core/src/platform/assets.ts'
const before = `DEFAULT_FILE_VIEWER_DOCX_RUNTIME_VERSION = '${previous}'`
assert.ok(texts[path].includes(before), 'Core Worker provenance differs from the Word package')
result[path] = texts[path].replace(
before,
`DEFAULT_FILE_VIEWER_DOCX_RUNTIME_VERSION = '${version}'`
)
result['pnpm-workspace.yaml'] = texts['pnpm-workspace.yaml'].replace(
`'@file-viewer/docx@${previous}'`,
`'@file-viewer/docx@${version}'`
)
for (const path of factsPaths) {
result[path] = texts[path]
.replaceAll(`@file-viewer/docx@${previous}`, `@file-viewer/docx@${version}`)
.replaceAll(`file-viewer-docx=${previous}`, `file-viewer-docx=${version}`)
.replaceAll(`@file-viewer/docx\` ${previous}`, `@file-viewer/docx\` ${version}`)
assert.notEqual(result[path], texts[path], `Missing current DOCX version reference in ${path}`)
}
return { previous, result }
}
function run(command, args, capture = false) {
const executable =
process.platform === 'win32' && ['npm', 'pnpm'].includes(command) ? command + '.cmd' : command
const result = spawnSync(executable, args, {
cwd: root,
encoding: 'utf8',
stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
timeout: 600_000,
shell: false
})
if (result.error) throw result.error
assert.equal(
result.status,
0,
`${command} failed: ${result.stderr || result.signal || result.status}`
)
return result.stdout
}
async function main() {
const version = process.argv[2]
assert.equal(process.argv.length, 3, 'Usage: pnpm release:prepare-docx <published-version>')
assert.match(version || '', /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/)
const paths = [
manifestPath,
'packages/core/src/platform/assets.ts',
'pnpm-workspace.yaml',
...factsPaths,
'pnpm-lock.yaml'
]
assert.equal(
run('git', ['status', '--porcelain', '--', ...paths], true).trim(),
'',
'Commit or stash release metadata edits before updating DOCX'
)
// Registry presence is checked before touching source files. This never publishes.
const actual = JSON.parse(
run(
'npm',
[
'view',
`@file-viewer/docx@${version}`,
'version',
'--json',
'--registry=https://registry.npmjs.org/'
],
true
)
)
assert.equal(actual, version, 'The requested upstream version is not published')
const texts = Object.fromEntries(
await Promise.all(
paths.map(async (path) => [path, await readFile(resolve(root, path), 'utf8')])
)
)
const current = JSON.parse(texts[manifestPath]).dependencies['@file-viewer/docx']
if (current === version) {
run(process.execPath, ['.github/scripts/verify-docx-upstream.mjs'])
return
}
const { previous, result } = prepareDocxTexts(texts, version)
try {
await Promise.all(
Object.entries(result)
.filter(([path]) => path !== 'pnpm-lock.yaml')
.map(([path, text]) => writeFile(resolve(root, path), text))
)
run('pnpm', ['install', '--lockfile-only', '--ignore-scripts'])
run('pnpm', ['install', '--frozen-lockfile'])
run(process.execPath, ['.github/scripts/verify-docx-upstream.mjs'])
run(process.execPath, ['.github/scripts/verify-public-release-facts.mjs'])
console.log(
`[release-docx] ${previous} -> ${version}; manifest, lockfile, Worker provenance and current documentation synchronized. Run pnpm release:verify, then commit the changes. Nothing was published.`
)
} catch (error) {
await Promise.all(
Object.entries(texts).map(([path, text]) => writeFile(resolve(root, path), text))
)
console.error(
'[release-docx] Source metadata restored after failure. Run pnpm install --frozen-lockfile to restore installed dependencies.'
)
throw error
}
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await main()
41 changes: 41 additions & 0 deletions .github/scripts/prepare-docx-upstream.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { test } from 'node:test'
import { prepareDocxTexts } from './prepare-docx-upstream.mjs'
const paths = [
'packages/renderers/word/package.json',
'packages/core/src/platform/assets.ts',
'pnpm-workspace.yaml',
'apps/official-site/public/llms.txt',
'apps/official-site/public/llms-full.txt',
'docs/guide/faq.md',
'docs/zh/guide/faq.md'
]
const texts = Object.fromEntries(
await Promise.all(paths.map(async (path) => [path, await readFile(path, 'utf8')]))
)
test('upstream update keeps package, Worker provenance and current version facts synchronized', () => {
const { result, previous } = prepareDocxTexts(texts, '99.88.77')
assert.match(previous, /^\d+\.\d+\.\d+$/)
assert.equal(JSON.parse(result[paths[0]]).dependencies['@file-viewer/docx'], '99.88.77')
assert.match(result[paths[1]], /DEFAULT_FILE_VIEWER_DOCX_RUNTIME_VERSION = '99.88.77'/)
for (const path of paths.slice(2)) assert.ok(result[path].includes('99.88.77'), path)
assert.notEqual(texts[paths[0]], result[paths[0]], 'inputs remain immutable')
})
test('upstream update rejects ranges, URLs, tags and shell syntax', () => {
for (const value of [
'latest',
'^0.3.32',
'file:./x.tgz',
'1.2.3;echo bad',
'1.2.3-rc.1',
'01.2.3'
])
assert.throws(() => prepareDocxTexts(texts, value))
})
test('mismatched Worker version fails instead of changing unrelated release facts', () => {
assert.throws(
() => prepareDocxTexts({ ...texts, [paths[1]]: '' }, '99.88.77'),
/Worker provenance/
)
})
85 changes: 85 additions & 0 deletions .github/scripts/verify-docx-upstream.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import assert from 'node:assert/strict'
import { createRequire } from 'node:module'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import JSZip from 'jszip'
import { JSDOM } from 'jsdom'

// A behavioral release gate: an npm version string alone does not prove that
// the packed browser renderer contains the upstream table-border repair.
export async function verifyDocxUpstream(
root = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
) {
const require = createRequire(resolve(root, 'packages/renderers/word/package.json'))
const docx = require('@file-viewer/docx')
const manifest = require('@file-viewer/docx/package.json')
const dom = new JSDOM('<!doctype html><html><body><main id="root"></main></body></html>')
const names = [
'window',
'document',
'DOMParser',
'XMLSerializer',
'Node',
'HTMLElement',
'getComputedStyle'
]
const previous = new Map(
names.map((name) => [name, Object.getOwnPropertyDescriptor(globalThis, name)])
)
try {
for (const name of names)
Object.defineProperty(globalThis, name, {
configurable: true,
writable: true,
value:
name === 'getComputedStyle'
? dom.window.getComputedStyle.bind(dom.window)
: dom.window[name]
})
const zip = new JSZip()
zip.file(
'[Content_Types].xml',
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>'
)
zip.file(
'_rels/.rels',
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>'
)
zip.file(
'word/document.xml',
'<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:tbl><w:tblGrid><w:gridCol w:w="3000"/></w:tblGrid><w:tr><w:tc><w:tcPr><w:tcBorders><w:tl2br w:val="single" w:sz="8" w:color="FF0000"/></w:tcBorders></w:tcPr><w:p><w:r><w:t>UPSTREAM_RELEASE_GATE</w:t></w:r></w:p></w:tc></w:tr></w:tbl><w:sectPr/></w:body></w:document>'
)
const host = dom.window.document.getElementById('root')
await docx.renderAsync(await zip.generateAsync({ type: 'nodebuffer' }), host, null, {
useWorker: false,
breakPages: false,
awaitLayout: false,
ignoreFonts: true
})
assert.match(host.textContent, /UPSTREAM_RELEASE_GATE/)
const diagonal = host.querySelector('[data-docx-diagonal="tl2br"]')
assert.ok(
diagonal,
`Installed @file-viewer/docx@${manifest.version} lacks the upstream diagonal-border fix. Publish the merged upstream first, then run release:prepare-docx.`
)
assert.equal(diagonal.getAttribute('x1'), '0%')
assert.equal(diagonal.getAttribute('x2'), '100%')
assert.equal(diagonal.closest('svg').style.pointerEvents, 'none')
const result = {
package: manifest.name,
version: manifest.version,
diagonalCount: 1,
passed: true
}
console.log('[docx-upstream]', JSON.stringify(result))
return result
} finally {
for (const [name, descriptor] of previous) {
if (descriptor) Object.defineProperty(globalThis, name, descriptor)
else delete globalThis[name]
}
dom.window.close()
}
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url))
await verifyDocxUpstream()
8 changes: 8 additions & 0 deletions .github/workflows/public-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ jobs:
- name: Run the issue regression gates
run: pnpm test

- name: Verify optional IFC entry ownership and installed CLI execution
run: pnpm --filter @file-viewer/renderer-3d verify:ifc

- name: Verify bounded metadata parsing and Illustrator namespace hints
run: pnpm exec vitest run test/metadata-input-safety.spec.ts test/chm-renderer.spec.ts

Expand All @@ -84,6 +87,11 @@ jobs:
- name: Install Chromium and WebKit
run: pnpm exec playwright install --with-deps chromium webkit

- name: Verify official IFC4 and IFC4.3 rendering, picking and Worker cleanup
run: |
node packages/renderers/3d/scripts/download-ifc-fixtures.mjs /tmp/ifc-samples
pnpm --filter @file-viewer/renderer-3d verify:ifc-browser /tmp/ifc-samples

- name: Verify PDF Worker cancellation
run: |
pnpm exec vitest run test/pdf-worker-lifecycle.spec.ts test/pdf-runtime-provenance.spec.ts
Expand Down
7 changes: 4 additions & 3 deletions apps/viewer-demo/scripts/verify-issue-266-samples.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ try {
assert.equal(row.failure, undefined)
assert.equal(row.pages, 13)
assert.equal(await page.locator('table').count(), 4)
if (process.argv.includes('--require-diagonals')) assert.equal(await page.locator('[data-docx-diagonal="tl2br"]').count(), 1, 'Packed upstream diagonal fix must reach the actual Word renderer')
row.borders = await page.locator('table').first().locator('td').first().evaluate(cell =>
['Top', 'Right', 'Bottom', 'Left'].map(side => getComputedStyle(cell)[`border${side}Width`]))
assert.ok(row.borders.every(width => Number.parseFloat(width) > 0), 'Existing ordinary cell borders must be preserved')
Expand Down Expand Up @@ -133,9 +134,9 @@ try {
row.errors = errors; row.warnings = warnings; row.networkRequests = requests
assert.deepEqual(errors, [])
assert.deepEqual(requests, [], 'In-memory document rendering must not make HTTP requests')
// This invoice already has an unsupported SES signature envelope. Keep the
// warning visible in the report rather than presenting it as a new success.
assert.ok(warnings.every(warning => format === 'ofd' && row.pages === 1 && warning.includes('unsupported SES signature structure')), warnings.join('\n'))
// Only the unchanged baseline misclassifies the invoice SignedData as SES.
if (baseline) assert.ok(warnings.every(warning => format === 'ofd' && row.pages === 1 && warning.includes('unsupported SES signature structure')), warnings.join('\n'))
else assert.deepEqual(warnings, [], 'Patched samples must render without signature warnings')
console.log(`[issue-266] ${stage} ${format}: ${row.pages ?? 'expected failure'} pages; ${file.name}`)
} finally { await page.close() }
}
Expand Down
8 changes: 8 additions & 0 deletions docs/guide/on-demand-renderers.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,3 +288,11 @@ Use `copyAssets:true` or `npx --yes file-viewer-copy-assets ./public/file-viewer
- [x] The Rust/WASM parser runs in a dedicated Worker, and standard asset tooling self-hosts the Worker, JavaScript bridge, and WASM binary under `vendor/chm/`.
- [x] Contents, keyword index, text search, internal navigation, and packaged resources stay within the current archive.
- [x] Topic documents are sanitized, scripts remain disabled by sandbox and CSP, and remote active content is not loaded automatically.

## Optional IFC BIM viewer

The explicit `@file-viewer/renderer-3d/ifc` entry enhances only IFC files with
local That Open / Web-IFC visualization, selection, properties and a configuration
hook. Existing Full/Office entry points remain unchanged. Install the optional
peers and self-host their matching Worker/WASM assets with `file-viewer-ifc-assets`.
The exact setup and licensing notes are in the package's [IFC guide](https://github.com/flyfish-dev/file-viewer/blob/main/packages/renderers/3d/IFC.md).
51 changes: 51 additions & 0 deletions docs/maintenance/release-readiness-20260911.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Release preparation: sample regressions

## Source repairs

- File Viewer #272: literal PPTX chart data and horizontal-bar orientation (issue #268).
- File Viewer #273: Word 2003 XML containers, mixed DrawingML anchors and OFD TextCode positions (issue #266).
- docxjs #10: direct cell diagonal borders; merged upstream. The npm dependency must contain that repair before a File Viewer release is qualified.
- OFD SignedData: distinguish PKCS#7 / GM/T ContentInfo from an SES seal. The invoice in #266 contains a digital signature and its visible stamp is already an ordinary page resource. Do not try to extract an SES image from its certificate envelope. Neither SignedData nor a displayed SES image implies cryptographic verification; `VerifyRet` is now `null` and `VerificationStatus` is `not-verified`.

## Publication order

This preparation does not publish either package, create a release, or change issue state.

1. Publish the reviewed, merged `flyfish-dev/docxjs` source under a **new version**. Do not republish the existing 0.3.31 version.
2. In the clean File Viewer checkout, run `pnpm release:prepare-docx <the-published-version>`. This checks registry presence before mutation, synchronizes the dependency, lockfile, core Worker provenance/cache token and current version documentation, then behaviorally verifies the installed package. Metadata is restored if preparation fails.
3. Run `pnpm release:verify`, followed by the normal full Public CI/browser matrix and original-file checks below. Commit the preparation changes with the planned File Viewer version/release metadata before creating the immutable release assets.
4. Publish the new File Viewer packages using the existing release pipeline.

`pnpm verify:docx-upstream` deliberately fails against an old engine that does not render direct cell diagonals. A successful source build is not a substitute for this consumer check. Keeping the currently published dependency until step 2 allows ordinary frozen-lockfile installs to work before the new upstream exists.

## Packed upstream integration

The actual merged upstream source `6dbe15e347459f3707116d531fc9064f2d4c2a95`
was built and packed, then installed into a disposable checkout of File Viewer
`161d81379a8de7e2edf17a87e3f24eb0f3dc063f`. The old public package fails the
behavioral gate; the candidate tarball passes. Original #266 samples passed
through the actual Word renderer with `--require-diagonals`, four stamp anchor
positions and no OFD warnings. The source dependency and lockfile were not changed
by this test. Private upstream CI run: `34620182131` (successful).
This is pre-publication candidate evidence, not proof of a future npm artifact.

## Reproduction evidence

Supply the original public issue #266 ZIP (SHA-256 `57345ed8469bfae8ccb066abb726a551d0c322f5af7527828771035551cff4cb`) and issue #268 PPTX (SHA-256 `4b0dfef0400a6194f86c3deb1234fdf84828f696f2f3915fe940cbb43f5ed30c`). Scripts do not download or upload documents.

```sh
OFD_SIGNATURE_SAMPLE_ZIP=/path/to/issue-266.zip pnpm test:ofd-signatures
node apps/viewer-demo/scripts/verify-issue-266-samples.mjs /path/to/issue-266.zip
node packages/renderers/pptx/scripts/verify-github-268-browser.mjs /path/to/issue-268.pptx
```

The patched #266 gate now requires zero console warnings, not an allowlist for the old invoice warning. The deterministic SignedData tests cover both OIDs, malformed envelopes, unknown OIDs, truncated DER and missing entries. The original-file gate also checks that the existing SES image still contains 12,884 bytes.

## Scope that must not be called fixed without evidence

- #227: the original sensitive XLS is not available in the public issue. The existing MiniFAT regression is synthetic, not the original report.
- #248: the inline component lacks the attachment and the `downloadAttach` implementation. Existing cold Vue CLI/package regression coverage does not prove that private request/response path.
- #269: the screenshot does not include the CAD file or font resources needed for reproduction.
- #267: the optional IFC entry is documented in `packages/renderers/3d/IFC.md`; its official-model browser gate must pass before the optional feature is qualified. The frozen default Full/Office profiles are unchanged.

No issue is closed automatically by these changes.
Binary file added docs/regressions/issue-267/ifc4.ifc.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/regressions/issue-267/ifc43.ifc.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading