| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282 |
- import { constants } from 'node:fs'
- import { access, readFile } from 'node:fs/promises'
- import { dirname, join, resolve } from 'node:path'
- import { fileURLToPath } from 'node:url'
- import { loadEcosystemReleaseContext } from './lib/ecosystem-packages.mjs'
- const scriptDir = dirname(fileURLToPath(import.meta.url))
- const sourceRoot = resolve(scriptDir, '..')
- const { wrapperManifest } = await loadEcosystemReleaseContext(sourceRoot)
- const requiredViewerTypeExports = [
- 'FileRef',
- 'ViewerAiOptions',
- 'ViewerArchiveOptions',
- 'ViewerCadOptions',
- 'ViewerController',
- 'ViewerControllerAccessor',
- 'ViewerControllerHandle',
- 'ViewerDocxOptions',
- 'ViewerEvent',
- 'ViewerEventHandler',
- 'ViewerEventType',
- 'ViewerFetchFile',
- 'ViewerFetchInput',
- 'ViewerMountOptions',
- 'ViewerOptions',
- 'ViewerPdfOptions',
- 'ViewerSpreadsheetOptions',
- 'ViewerCoreOptions',
- 'ViewerSearchOptions',
- 'ViewerSourceInput',
- 'ViewerThemeMode',
- 'ViewerToolbarOptions',
- 'ViewerToolbarPosition',
- 'ViewerTypstOptions',
- 'ViewerWatermarkOptions'
- ]
- const requiredVue3TypeExports = [
- 'FileViewerVue3PluginOptions',
- 'FileViewerVue3Handle'
- ]
- const forbiddenLegacyOptionTypes = [
- 'CreateViewerFrameOptions',
- 'ViewerDirectFrameHandle',
- 'ViewerFrameComponentBridgeOptions',
- 'ViewerFrameComponentProps',
- 'ViewerFrameContainerComponentProps',
- 'ViewerFrameController',
- 'ViewerFrameControllerAccessor',
- 'ViewerFrameControllerHandle',
- 'ViewerFrameEventHandler',
- 'ViewerFrameEventPayload',
- 'ViewerFrameEventType',
- 'ViewerFrameFilePostController',
- 'ViewerFrameFilePostControllerOptions',
- 'ViewerFrameHostComponentProps',
- 'ViewerFrameIframeComponentProps',
- 'ViewerFrameOptions',
- 'ViewerFrameParamValue',
- 'ViewerMountedFrameHandle'
- ]
- const forbiddenDirectCoreOptionTypes = [
- 'FileViewerAiOptions',
- 'FileViewerArchiveOptions',
- 'FileViewerCadOptions',
- 'FileViewerDocxOptions',
- 'FileViewerOptions',
- 'FileViewerPdfOptions',
- 'FileViewerSpreadsheetOptions',
- 'FileViewerSearchOptions',
- 'FileViewerSerializableCadOptions',
- 'FileViewerSerializableOptions',
- 'FileViewerSerializableToolbarOptions',
- 'FileViewerThemeMode',
- 'FileViewerToolbarOptions',
- 'FileViewerToolbarPosition',
- 'FileViewerTypstOptions',
- 'FileViewerWatermarkOptions'
- ]
- const forbiddenLocalOptionFields = [
- 'ai',
- 'archive',
- 'beforeOperation',
- 'cad',
- 'docx',
- 'pdf',
- 'spreadsheet',
- 'search',
- 'theme',
- 'toolbar',
- 'typst',
- 'watermark'
- ]
- function assert(condition, message) {
- if (!condition) {
- throw new Error(message)
- }
- }
- async function exists(path) {
- try {
- await access(path, constants.F_OK)
- return true
- } catch {
- return false
- }
- }
- async function readWrapperSource(packageDir) {
- const candidates = [
- join(sourceRoot, packageDir, 'src', 'index.ts'),
- join(sourceRoot, packageDir, 'src', 'index.tsx'),
- join(sourceRoot, packageDir, 'src', 'package', 'index.ts')
- ]
- for (const candidate of candidates) {
- if (await exists(candidate)) {
- return {
- path: candidate,
- content: await readFile(candidate, 'utf8')
- }
- }
- }
- throw new Error(`${packageDir} must provide src/index.ts or src/index.tsx`)
- }
- async function readWrapperControllerSource(packageDir) {
- const candidates = [
- join(sourceRoot, packageDir, 'src', 'controller.ts'),
- join(sourceRoot, packageDir, 'src', 'package', 'controller.ts')
- ]
- for (const candidate of candidates) {
- if (await exists(candidate)) {
- return {
- path: candidate,
- content: await readFile(candidate, 'utf8')
- }
- }
- }
- throw new Error(`${packageDir} must provide a local src/controller.ts wrapper controller`)
- }
- function escapeRegExp(value) {
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
- }
- function hasTypeToken(source, typeName) {
- return new RegExp(`\\b${escapeRegExp(typeName)}\\b`).test(source)
- }
- function hasExportedType(source, typeName, specifier) {
- const directAlias = new RegExp(`export\\s+(?:interface|type)\\s+${escapeRegExp(typeName)}\\b`)
- const reexportBlock = new RegExp(
- `export\\s+type\\s*{[\\s\\S]*\\b${escapeRegExp(typeName)}\\b[\\s\\S]*}\\s+from\\s+['"]${escapeRegExp(specifier)}['"]`
- )
- return directAlias.test(source) || reexportBlock.test(source)
- }
- function hasForbiddenCoreOptionImport(source, typeName) {
- const coreTypeImport = new RegExp(
- `import\\s+type\\s*{[\\s\\S]*\\b${escapeRegExp(typeName)}\\b[\\s\\S]*}\\s+from\\s+['"]@file-viewer/core['"]`
- )
- const mixedCoreImport = new RegExp(
- `import\\s*{[\\s\\S]*\\btype\\s+${escapeRegExp(typeName)}\\b[\\s\\S]*}\\s+from\\s+['"]@file-viewer/core['"]`
- )
- return coreTypeImport.test(source) || mixedCoreImport.test(source)
- }
- function hasForbiddenLocalOptionField(source, fieldName) {
- const fieldDeclaration = new RegExp(`(^|\\n)\\s*${escapeRegExp(fieldName)}\\??\\s*:\\s*[^,\\n]+;`, 'm')
- return fieldDeclaration.test(source)
- }
- let checked = 0
- for (const wrapper of wrapperManifest.wrappers) {
- const { content, path } = await readWrapperSource(wrapper.packageDir)
- if (wrapper.flavor === 'full') {
- assert(wrapper.basePackage, `${wrapper.packageName} full package must declare basePackage`)
- assert(
- content.includes('\'@file-viewer/preset-all\'') || content.includes('"@file-viewer/preset-all"'),
- `${path} must import @file-viewer/preset-all to enable the complete format matrix`
- )
- assert(
- content.includes(`'${wrapper.basePackage}'`) || content.includes(`"${wrapper.basePackage}"`),
- `${path} must re-export its standard package ${wrapper.basePackage}`
- )
- for (const legacyType of forbiddenLegacyOptionTypes) {
- assert(!hasTypeToken(content, legacyType), `${path} must not expose legacy iframe option type ${legacyType}`)
- }
- checked += 1
- continue
- }
- const { content: controllerContent, path: controllerPath } = await readWrapperControllerSource(wrapper.packageDir)
- for (const legacyType of forbiddenLegacyOptionTypes) {
- assert(!hasTypeToken(content, legacyType), `${path} must not expose legacy iframe option type ${legacyType}`)
- assert(!hasTypeToken(controllerContent, legacyType), `${controllerPath} must not expose legacy iframe option type ${legacyType}`)
- }
- assert(
- controllerContent.includes('from \'@file-viewer/core\'') || controllerContent.includes('from "@file-viewer/core"'),
- `${controllerPath} must consume the shared @file-viewer/core low-level contract`
- )
- for (const forbiddenToken of [
- 'createFileViewerNativeController',
- 'resolveFileViewerNativeLoadSource',
- 'FileViewerNativeController',
- 'FileViewerNativeFetchFile',
- 'FileViewerNativeFetchInput',
- 'FileViewerNativeSource'
- ]) {
- assert(
- !controllerContent.includes(forbiddenToken),
- `${controllerPath} must keep browser mount/source orchestration inside the component package instead of importing ${forbiddenToken} from core`
- )
- }
- assert(
- !content.includes('from \'@file-viewer/web\'') && !content.includes('from "@file-viewer/web"'),
- `${path} must not consume another component package`
- )
- assert(
- !controllerContent.includes('from \'@file-viewer/web\'') && !controllerContent.includes('from "@file-viewer/web"'),
- `${controllerPath} must not consume another component package`
- )
- assert(
- content.includes('from \'./controller\'') ||
- content.includes('from "./controller"') ||
- content.includes('from \'./controller.js\'') ||
- content.includes('from "./controller.js"'),
- `${path} must consume its local component controller instead of core mount helpers`
- )
- if (wrapper.packageName !== '@file-viewer/vue3') {
- assert(
- content.includes('from \'@file-viewer/core\'') || content.includes('from "@file-viewer/core"'),
- `${path} must inject the shared core renderer registry explicitly`
- )
- }
- for (const typeName of requiredViewerTypeExports) {
- assert(
- hasExportedType(content, typeName, './controller') ||
- hasExportedType(content, typeName, './controller.js') ||
- hasTypeToken(content, typeName),
- `${path} must re-export ${typeName} from the local component controller`
- )
- }
- if (wrapper.packageName === '@file-viewer/vue3') {
- for (const typeName of requiredVue3TypeExports) {
- assert(hasTypeToken(content, typeName), `${path} must expose Vue 3 type ${typeName}`)
- }
- checked += 1
- continue
- }
- for (const typeName of forbiddenDirectCoreOptionTypes) {
- assert(
- !hasForbiddenCoreOptionImport(content, typeName),
- `${path} must not import ${typeName} directly from @file-viewer/core; use shared Viewer* aliases`
- )
- }
- for (const fieldName of forbiddenLocalOptionFields) {
- assert(
- !hasForbiddenLocalOptionField(content, fieldName),
- `${path} must not redeclare option field "${fieldName}"; pass it through ViewerMountOptions`
- )
- }
- checked += 1
- }
- console.log(`Verified ${checked} standard component package option surfaces against the shared core contracts.`)
|