| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144 |
- import { existsSync, statSync } from 'node:fs'
- import { readFile } from 'node:fs/promises'
- import { dirname, join, resolve } from 'node:path'
- import { fileURLToPath } from 'node:url'
- import {
- collectPackageEntrypoints,
- loadEcosystemReleaseContext
- } from './lib/ecosystem-packages.mjs'
- const scriptDir = dirname(fileURLToPath(import.meta.url))
- const sourceRoot = resolve(scriptDir, '..')
- const { entries, rootPackage } = await loadEcosystemReleaseContext(sourceRoot)
- const expectedWorkspaceRange = `workspace:^${rootPackage.version}`
- const packageNamesByKind = entries.reduce((result, entry) => {
- result[entry.kind] ||= new Set()
- result[entry.kind].add(entry.packageName)
- return result
- }, {})
- const wrapperPackageNames = new Set([
- ...(packageNamesByKind['standard-wrapper'] || []),
- ...(packageNamesByKind.compatibility || [])
- ])
- const rendererEntries = entries.filter(entry => entry.kind === 'renderer')
- const pluginEnginePackages = new Set([
- '@file-viewer/pptx',
- '@file-viewer/eda-layout',
- '@file-viewer/eda-orcad',
- '@file-viewer/geometry-engine'
- ])
- function assert(condition, message) {
- if (!condition) {
- throw new Error(message)
- }
- }
- function assertFile(path, label = path) {
- assert(existsSync(path), `Missing ${label}`)
- assert(statSync(path).isFile(), `${label} must be a file`)
- }
- function assertDirectory(path, label = path) {
- assert(existsSync(path), `Missing ${label}`)
- assert(statSync(path).isDirectory(), `${label} must be a directory`)
- }
- function normalizeEntrypoint(value) {
- return String(value || '').replace(/^\.\//, '')
- }
- function dependencyFields(packageJson) {
- return {
- dependencies: packageJson.dependencies || {},
- optionalDependencies: packageJson.optionalDependencies || {},
- peerDependencies: packageJson.peerDependencies || {}
- }
- }
- function assertNoWrapperDependencies(entry) {
- for (const [field, dependencies] of Object.entries(dependencyFields(entry.packageJson))) {
- for (const packageName of Object.keys(dependencies)) {
- assert(
- !wrapperPackageNames.has(packageName),
- `${entry.packageName} ${field}.${packageName} must not depend on a component or compatibility wrapper`
- )
- }
- }
- }
- function assertCommonPackageContract(entry) {
- const packageJson = entry.packageJson
- assert(packageJson.name === entry.renderer.packageName, `${entry.id} package name drifted from wrappers.json`)
- assert(packageJson.private !== true, `${entry.packageName} must be publishable`)
- assert(packageJson.type === 'module', `${entry.packageName} must be an ESM package`)
- assert(packageJson.publishConfig?.access === 'public', `${entry.packageName} publishConfig.access must be public`)
- assert(packageJson.main === './dist/index.js', `${entry.packageName} main must be ./dist/index.js`)
- assert(packageJson.module === './dist/index.js', `${entry.packageName} module must be ./dist/index.js`)
- assert(packageJson.types === './dist/index.d.ts', `${entry.packageName} types must be ./dist/index.d.ts`)
- assert(packageJson.exports?.['.']?.import === './dist/index.js', `${entry.packageName} exports["."].import must be ./dist/index.js`)
- assert(packageJson.exports?.['.']?.types === './dist/index.d.ts', `${entry.packageName} exports["."].types must be ./dist/index.d.ts`)
- assert(packageJson.exports?.['./package.json'] === './package.json', `${entry.packageName} must export ./package.json`)
- assert(packageJson.scripts?.build, `${entry.packageName} must provide a build script`)
- assert(packageJson.scripts?.['type-check'], `${entry.packageName} must provide a type-check script`)
- const files = new Set(packageJson.files || [])
- for (const requiredFile of ['dist', 'README.md', 'README.en.md', 'LICENSE']) {
- assert(files.has(requiredFile), `${entry.packageName} package files must include ${requiredFile}`)
- }
- assertFile(join(entry.absoluteDir, 'README.md'), `${entry.packageName} README.md`)
- assertFile(join(entry.absoluteDir, 'README.en.md'), `${entry.packageName} README.en.md`)
- assertFile(join(entry.absoluteDir, 'LICENSE'), `${entry.packageName} LICENSE`)
- assertDirectory(join(entry.absoluteDir, 'dist'), `${entry.packageName} dist/`)
- collectPackageEntrypoints(packageJson).forEach(entrypoint => {
- assertFile(
- join(entry.absoluteDir, normalizeEntrypoint(entrypoint)),
- `${entry.packageName} ${entrypoint}`
- )
- })
- assertNoWrapperDependencies(entry)
- }
- async function assertPluginRendererContract(entry) {
- const sourceIndex = join(entry.absoluteDir, 'src', 'index.ts')
- assertFile(sourceIndex, `${entry.packageName} src/index.ts`)
- const source = await readFile(sourceIndex, 'utf8')
- const exportBaseName = entry.renderer.id.replace(/(^|-)([a-z0-9])/g, (_match, _dash, char) => char.toUpperCase())
- const camelBaseName = exportBaseName.charAt(0).toLowerCase() + exportBaseName.slice(1)
- assert(
- entry.packageJson.dependencies?.['@file-viewer/core'] === expectedWorkspaceRange,
- `${entry.packageName} must depend on @file-viewer/core@${expectedWorkspaceRange}`
- )
- assert(
- /export\s+default\s+\w+Renderer/.test(source),
- `${entry.packageName} src/index.ts must default-export a renderer plugin`
- )
- assert(
- source.includes(`export const ${camelBaseName}Renderer`) ||
- source.includes(`export const ${camelBaseName}RendererDefinitions`) ||
- source.includes(`export const ${camelBaseName}RendererDefinition`),
- `${entry.packageName} src/index.ts must expose a named renderer export`
- )
- assert(
- source.includes('renderFileViewer'),
- `${entry.packageName} src/index.ts must expose lazy renderFileViewer* handlers`
- )
- assert(
- source.includes('import('),
- `${entry.packageName} src/index.ts must keep heavy renderer implementation behind dynamic import()`
- )
- }
- for (const entry of rendererEntries) {
- assertCommonPackageContract(entry)
- if (!pluginEnginePackages.has(entry.packageName)) {
- await assertPluginRendererContract(entry)
- }
- }
- console.log(
- `[renderer-contracts] Verified ${rendererEntries.length} renderer packages, including ${rendererEntries.length - pluginEnginePackages.size} plugin renderer packages and ${pluginEnginePackages.size} engine package.`
- )
|