mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-11 05:26:15 -08:00
refactor(iframe): [PKG-IFRAME-04] preserve types across module consumers
This commit is contained in:
1 parent
1342de94ef
commit
047e2df945
24 files changed
+1496
-69
No files matched your search
@@ -0,0 +1,112 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import ts from 'typescript'
|
||||
import compat from 'typescript-compat'
|
||||
import { checkFiles, packedFiles } from '../../scripts/package-check.mjs'
|
||||
import { consumerDirectory, removeConsumer, run, workspace, writeJson } from '../../scripts/package-consumer.mjs'
|
||||
import { verifyIframeContract } from './iframe-contract.mjs'
|
||||
import { hash, readMember } from './releases.mjs'
|
||||
|
||||
async function main() {
|
||||
assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22', 'Use yarn test:iframe-types-package')
|
||||
const yarn = process.env.npm_execpath
|
||||
assert(yarn && fs.existsSync(yarn))
|
||||
const output = fs.mkdtempSync(path.join(workspace, 'refactor/.cache/iframe-package-types-'))
|
||||
console.log(`Iframe installed package evidence: ${output}`)
|
||||
const contract = await verifyIframeContract()
|
||||
const frozen = path.join(output, 'frozen-workspace')
|
||||
fs.mkdirSync(frozen)
|
||||
const prefix = 'packages/artplayer-tool-iframe/'
|
||||
for (const [file, bytes] of contract.sources) {
|
||||
if (!file.startsWith(prefix))
|
||||
continue
|
||||
const target = path.join(frozen, file.slice(prefix.length))
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true })
|
||||
fs.writeFileSync(target, bytes)
|
||||
}
|
||||
const packages = [{ label: 'published', name: contract.baseline.release.name, archive: contract.archive, sha256: contract.baseline.release.sha256, files: contract.baseline.release.files }]
|
||||
for (const [label, folder] of [['workspace', frozen], ['candidate', path.join(workspace, 'packages/artplayer-tool-iframe')]]) {
|
||||
const archive = path.join(output, `${label}.tgz`)
|
||||
fs.writeFileSync(path.join(output, `${label}-pack.log`), run([yarn, 'pack', '--filename', archive], folder))
|
||||
const manifest = JSON.parse(readMember(archive, 'package/package.json'))
|
||||
const files = packedFiles(archive)
|
||||
checkFiles(manifest, files)
|
||||
packages.push({ label, name: manifest.name, archive, sha256: hash(fs.readFileSync(archive)), files: Object.fromEntries(files.map(member => [member, hash(readMember(archive, member))])) })
|
||||
}
|
||||
const matrix = []
|
||||
const runtime = []
|
||||
for (const pkg of packages) {
|
||||
const consumer = consumerDirectory()
|
||||
try {
|
||||
writeJson(path.join(consumer, 'package.json'), { name: 'iframe-isolated-consumer', private: true, dependencies: { [pkg.name]: `file:${pkg.archive.replaceAll('\\', '/')}` } })
|
||||
fs.writeFileSync(path.join(output, `${pkg.label}-install.log`), run([yarn, 'install', '--offline', '--ignore-scripts', '--non-interactive'], consumer))
|
||||
const lock = fs.readFileSync(path.join(consumer, 'yarn.lock'))
|
||||
fs.writeFileSync(path.join(output, `${pkg.label}-frozen-install.log`), run([yarn, 'install', '--offline', '--frozen-lockfile', '--ignore-scripts', '--non-interactive'], consumer))
|
||||
assert.deepEqual(fs.readFileSync(path.join(consumer, 'yarn.lock')), lock)
|
||||
fs.writeFileSync(path.join(output, `${pkg.label}-yarn.lock`), lock)
|
||||
const installed = path.join(consumer, 'node_modules', pkg.name)
|
||||
assert.equal(fs.realpathSync(installed), installed, 'Consumer must not use workspace links')
|
||||
for (const [member, expected] of Object.entries(pkg.files))
|
||||
assert.equal(hash(fs.readFileSync(path.join(installed, member.slice(8)))), expected, member)
|
||||
const runtimeFile = path.join(consumer, 'runtime.cjs')
|
||||
fs.writeFileSync(runtimeFile, `const assert = require('node:assert/strict');
|
||||
const value = require('${pkg.name}');
|
||||
assert.equal(typeof value, '${pkg.label === 'published' ? 'object' : 'function'}');
|
||||
assert.equal(typeof value.default, '${pkg.label === 'published' ? 'function' : 'undefined'}');
|
||||
${pkg.label === 'published' ? `for (const suffix of ['js', 'legacy.js']) { const helper = require('${pkg.name}/dist/artplayer-helper-iframe.' + suffix); assert.equal(typeof helper.default, 'function'); assert.equal(typeof helper.default.destroy, 'function'); assert.equal(typeof value.default.destroy, 'undefined'); }` : `const legacy = require('${pkg.name}/legacy'); assert.equal(typeof legacy, 'function'); assert.equal(legacy.default, undefined);`}
|
||||
import('${pkg.name}').then(module => {
|
||||
assert.equal(typeof module.default, '${pkg.label === 'published' ? 'object' : 'function'}');
|
||||
${pkg.label === 'published' ? '' : 'assert.deepEqual(Object.keys(module), [\'default\']);'}
|
||||
console.log(JSON.stringify({ commonjs: typeof value, commonjsDefault: typeof value.default, esmDefault: typeof module.default, helperIsDistinct: ${pkg.label === 'published'}, ssrImport: true }));
|
||||
}).catch(error => { console.error(error); process.exitCode = 1; });`)
|
||||
const observation = run([runtimeFile], consumer)
|
||||
fs.writeFileSync(path.join(output, `${pkg.label}-runtime.log`), observation)
|
||||
runtime.push({ package: pkg.label, ...JSON.parse(observation.trim()) })
|
||||
const modes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]
|
||||
if (pkg.label === 'candidate')
|
||||
modes.push([ts, 'nodenext-cjs-no-interop'])
|
||||
for (const [compiler, mode] of modes) {
|
||||
const next = mode.startsWith('nodenext')
|
||||
const noInterop = mode.endsWith('no-interop')
|
||||
const filename = path.join(consumer, `consumer.${next ? mode.includes('-cjs') ? 'cts' : 'mts' : 'ts'}`)
|
||||
const options = { strict: true, noEmit: true, skipLibCheck: false, types: [], esModuleInterop: !noInterop, target: compiler.ScriptTarget.ES2020, lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'], module: next ? compiler.ModuleKind.NodeNext : mode === 'bundler-esm' ? compiler.ModuleKind.ESNext : compiler.ModuleKind.CommonJS, moduleResolution: next ? compiler.ModuleResolutionKind.NodeNext : mode === 'bundler-esm' ? compiler.ModuleResolutionKind.Bundler : compiler.ModuleResolutionKind.NodeJs }
|
||||
let source = fs.readFileSync(path.join(workspace, pkg.label === 'candidate' ? noInterop ? 'test/types/iframe-commonjs.cts' : 'test/types/iframe.ts' : 'refactor/fixtures/consumers/iframe-workspace.ts'), 'utf8')
|
||||
const files = [filename]
|
||||
if (pkg.label === 'published')
|
||||
source = source.replaceAll('artplayer-tool-iframe', pkg.name).replace('Record<number, { resove: (...args: any[]) => any, reject: (...args: any[]) => any }>', 'Record<number, { resove: Function, reject: Function }>')
|
||||
if (pkg.label === 'candidate' && !noInterop) {
|
||||
const oldFile = path.join(consumer, path.basename(filename).replace('consumer', 'historical'))
|
||||
fs.copyFileSync(path.join(workspace, 'refactor/fixtures/consumers/iframe-workspace.ts'), oldFile)
|
||||
files.push(oldFile)
|
||||
}
|
||||
function compile(code) {
|
||||
fs.writeFileSync(filename, code)
|
||||
const program = compiler.createProgram(files, options)
|
||||
for (const file of program.getSourceFiles()) {
|
||||
const real = fs.realpathSync(file.fileName)
|
||||
assert(real.startsWith(fs.realpathSync(consumer) + path.sep) || (program.isSourceFileDefaultLibrary(file) && path.dirname(real) === fs.realpathSync(path.dirname(compiler.sys.getExecutingFilePath()))), `Type escaped installed consumer: ${real}`)
|
||||
}
|
||||
return compiler.getPreEmitDiagnostics(program).map(item => ({ code: item.code, message: compiler.flattenDiagnosticMessageText(item.messageText, '\n').replaceAll(consumer.replaceAll('\\', '/'), '<consumer>') }))
|
||||
}
|
||||
const diagnostics = compile(source)
|
||||
const expected = pkg.label === 'workspace' && mode === 'nodenext-esm' ? [2344, 2344, 2344, 2709, 2339, 2344, 2709, 2344, 2709, 2709, 2351, 2339, 2339, 2339, 7006, 7006, 2339, 2339] : []
|
||||
writeJson(path.join(output, `${pkg.label}-${compiler.version}-${mode}.json`), diagnostics)
|
||||
assert.deepEqual(diagnostics.map(item => item.code), expected, `${pkg.label} ${compiler.version} ${mode}`)
|
||||
const negative = pkg.label === 'candidate' ? compile(source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')) : []
|
||||
if (pkg.label === 'candidate')
|
||||
assert.equal(negative.length, noInterop ? 1 : 16)
|
||||
matrix.push({ package: pkg.label, compiler: compiler.version, mode, diagnostics, negative })
|
||||
}
|
||||
}
|
||||
finally { removeConsumer(consumer) }
|
||||
}
|
||||
writeJson(path.join(output, 'report.json'), { task: 'PKG-IFRAME-04', scope: 'Actual npm archive, frozen Git workspace pack and candidate Yarn tarball installed outside workspace; byte verification, offline/frozen lock and strict positive/negative consumers. Old npm CJS namespace and distinct helper are preserved observations, not claims the candidate provides old package-name/helper aliases. Native player and distribution migration remain 05/06.', packages, matrix, runtime })
|
||||
console.log(`Iframe installed types verified: ${matrix.length} compiler cases; ${output}`)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
@@ -0,0 +1,105 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Compiler and historical declaration contract runner.
|
||||
import test from 'node:test'
|
||||
import { ESLint } from 'eslint'
|
||||
import ts from 'typescript'
|
||||
import compat from 'typescript-compat'
|
||||
import { checkPluginEditorDeclaration, generatePluginEditorDeclaration } from '../../scripts/plugin-editor-types.mjs'
|
||||
import { checkConsumer } from '../../scripts/typecheck.mjs'
|
||||
import { iframeCandidate, iframeEnvironment, iframeHistorical } from '../../test/helpers/iframe.js'
|
||||
import { verifyIframeContract } from './iframe-contract.mjs'
|
||||
import { readMember } from './releases.mjs'
|
||||
|
||||
const modes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]
|
||||
const invalid = code => code.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')
|
||||
|
||||
test('Iframe error envelopes preserve non-string thrown messages rather than claiming string-only payloads', async () => {
|
||||
const implementations = [...(await iframeHistorical()).filter(item => !item.name.includes('helper')), await iframeCandidate()]
|
||||
for (const implementation of implementations) {
|
||||
for (const [body, expected] of [['throw { message: 7 }', 7], ['throw 3', undefined]]) {
|
||||
const env = iframeEnvironment(implementation, true)
|
||||
await assert.rejects(env.Factory.onMessage({ data: { type: 'commit', data: body, id: 1 }, source: env.box.parent }), () => true)
|
||||
assert.equal(env.sent.length, 1)
|
||||
assert.equal(env.sent[0].packet.type, 'error')
|
||||
assert.equal(env.sent[0].packet.data, expected)
|
||||
assert.equal(env.sent[0].packet.id, 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('Iframe public types preserve legacy extraction while precise views reject invalid protocol calls', () => {
|
||||
const source = fs.readFileSync('test/types/iframe.ts', 'utf8')
|
||||
for (const [compiler, mode] of modes) {
|
||||
assert.deepEqual(checkConsumer(compiler, mode, source), [], `${compiler.version} ${mode}`)
|
||||
assert.equal(checkConsumer(compiler, mode, invalid(source)).length, 16)
|
||||
assert.deepEqual(checkConsumer(compiler, mode, fs.readFileSync('refactor/fixtures/consumers/iframe-workspace.ts', 'utf8')), [])
|
||||
}
|
||||
const commonjs = fs.readFileSync('test/types/iframe-commonjs.cts', 'utf8')
|
||||
assert.deepEqual(checkConsumer(ts, 'nodenext-cjs', commonjs), [])
|
||||
assert.equal(checkConsumer(ts, 'nodenext-cjs', invalid(commonjs)).length, 1)
|
||||
const implementationSource = fs.readFileSync('test/types/iframe-source.ts', 'utf8')
|
||||
assert.deepEqual(checkConsumer(ts, 'node10-commonjs', implementationSource), [])
|
||||
const implementation = checkConsumer(ts, 'node10-commonjs', invalid(implementationSource))
|
||||
assert.deepEqual(implementation.map(item => item.code), [2419], 'Only the preserved non-null legacy callback differs from the actual constructor')
|
||||
assert(implementation[0].message.includes('messageCallback') && implementation[0].message.includes('null'))
|
||||
})
|
||||
|
||||
test('Iframe frozen workspace keeps exact default signatures and published Function callbacks remain a separate legacy boundary', async () => {
|
||||
const contract = await verifyIframeContract()
|
||||
const source = fs.readFileSync('refactor/fixtures/consumers/iframe-workspace.ts', 'utf8')
|
||||
const filename = path.resolve('test/types/iframe-history.ts')
|
||||
const declaration = path.resolve('test/types/iframe-history.d.ts')
|
||||
for (const compiler of [ts, compat]) {
|
||||
function historical(code, definitions) {
|
||||
const options = { strict: true, noEmit: true, skipLibCheck: false, types: [], esModuleInterop: true, module: compiler.ModuleKind.CommonJS, moduleResolution: compiler.ModuleResolutionKind.NodeJs, target: compiler.ScriptTarget.ES2020, lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'] }
|
||||
const host = compiler.createCompilerHost(options)
|
||||
const original = host.getSourceFile.bind(host)
|
||||
host.getSourceFile = (file, language, ...rest) => path.resolve(file) === filename
|
||||
? compiler.createSourceFile(file, code.replaceAll('artplayer-tool-iframe', './iframe-history'), language, true)
|
||||
: path.resolve(file) === declaration ? compiler.createSourceFile(file, definitions, language, true) : original(file, language, ...rest)
|
||||
const exists = host.fileExists.bind(host)
|
||||
host.fileExists = file => path.resolve(file) === declaration || exists(file)
|
||||
const program = compiler.createProgram([filename, declaration], options, host)
|
||||
assert(program.getSourceFile(declaration))
|
||||
return compiler.getPreEmitDiagnostics(program).map(item => item.code)
|
||||
}
|
||||
const frozen = contract.sources.get('packages/artplayer-tool-iframe/types/artplayer-tool-iframe.d.ts')
|
||||
const published = readMember(contract.archive, 'package/types/artplayer-plugin-iframe.d.ts').toString()
|
||||
assert.deepEqual(historical(source, frozen), [])
|
||||
assert.deepEqual(historical(source, published), [2344], 'Published Function versus workspace callable field is a pre-refactor difference')
|
||||
const broad = `import Iframe from 'artplayer-tool-iframe'; declare const tool: Iframe; declare const callback: Function; tool.promises[1] = { resove: callback, reject: callback };`
|
||||
assert.deepEqual(historical(broad, published), [])
|
||||
assert.deepEqual(historical(broad, frozen), [2322, 2322])
|
||||
assert.deepEqual(checkConsumer(compiler, 'node10-commonjs', broad).map(item => item.code), [2322, 2322])
|
||||
assert.deepEqual(historical(`${source}\nIframe.postMessage({ type: 'custom' });`, frozen), [2345])
|
||||
assert.deepEqual(checkConsumer(compiler, 'node10-commonjs', `${source}\nIframe.postMessage({ type: 'custom' });`).map(item => item.code), [2345])
|
||||
}
|
||||
})
|
||||
|
||||
test('Iframe standalone editor generation fixes the frozen export errors and preserves class plus named types', async () => {
|
||||
const contract = await verifyIframeContract()
|
||||
const historical = contract.sources.get('docs/assets/ts/artplayer-tool-iframe.d.ts')
|
||||
const source = fs.readFileSync('packages/artplayer-tool-iframe/types/artplayer-tool-iframe.d.ts', 'utf8')
|
||||
const generated = generatePluginEditorDeclaration(source, 'ArtplayerToolIframe')
|
||||
const file = 'docs/assets/ts/artplayer-tool-iframe.d.ts'
|
||||
const [formatted] = await new ESLint({ fix: true, fixTypes: ['layout'] }).lintText(generated, { filePath: file })
|
||||
assert.equal(formatted.errorCount, 0)
|
||||
assert.equal(fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n'), formatted.output || generated)
|
||||
const consumer = `declare const frame: HTMLIFrameElement;
|
||||
const option: ArtplayerToolIframe.Option = { iframe: frame, url: '/frame' };
|
||||
const tool: ArtplayerToolIframe = new ArtplayerToolIframe(option);
|
||||
const runtime = ArtplayerToolIframe as ArtplayerToolIframe.RuntimeConstructor;
|
||||
runtime.postMessage({ type: 'custom' });
|
||||
const promise: Promise<void> = runtime.onMessage(new MessageEvent('message', { data: { type: 'custom' } }));
|
||||
// @ts-expect-error No lowercase runtime global exists.
|
||||
new artplayerToolIframe(option);
|
||||
// @ts-expect-error URL remains text.
|
||||
new ArtplayerToolIframe({ iframe: frame, url: 1 });`
|
||||
for (const compiler of [ts, compat]) {
|
||||
assert.deepEqual(checkPluginEditorDeclaration(historical, '', '', compiler).map(item => item.code).sort(), [2303, 2309, 2686])
|
||||
assert.deepEqual(checkPluginEditorDeclaration(generated, '', consumer, compiler), [])
|
||||
assert.equal(checkPluginEditorDeclaration(generated, '', invalid(consumer), compiler).length, 2)
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user