mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-19 19:51:20 -08:00
fix(thumbnail): [PKG-TOOL-THUMB-04] supply public types and installed module entries
This commit is contained in:
1 parent
15485b6420
commit
3bff236145
33 files changed
+2466
-164
No files matched your search
@@ -0,0 +1,114 @@
|
||||
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 { verifyThumbnailContract } from './thumbnail-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:thumbnail-types-package')
|
||||
const yarn = process.env.npm_execpath
|
||||
assert(yarn && fs.existsSync(yarn))
|
||||
const output = fs.mkdtempSync(path.join(workspace, 'refactor/.cache/thumbnail-package-types-'))
|
||||
console.log(`Thumbnail installed package evidence: ${output}`)
|
||||
const contract = verifyThumbnailContract()
|
||||
const frozen = path.join(output, 'frozen-workspace')
|
||||
fs.mkdirSync(frozen)
|
||||
const prefix = 'packages/artplayer-tool-thumbnail/'
|
||||
for (const [file, text] of contract.source) {
|
||||
if (!file.startsWith(prefix)) continue
|
||||
const target = path.join(frozen, file.slice(prefix.length))
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true })
|
||||
fs.writeFileSync(target, text)
|
||||
}
|
||||
const packages = []
|
||||
const matrix = []
|
||||
const runtime = []
|
||||
for (const [label, folder] of [['workspace-fixture', frozen], ['candidate', path.join(workspace, 'packages/artplayer-tool-thumbnail')]]) {
|
||||
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)
|
||||
if (label === 'candidate') {
|
||||
checkFiles(manifest, files, [...contract.source.keys()].filter(file => file.startsWith(`${prefix}dist/`)).map(file => `package/${file.slice(prefix.length)}`))
|
||||
assert(files.includes('package/THIRD_PARTY_NOTICES'))
|
||||
assert.equal(readMember(archive, 'package/THIRD_PARTY_NOTICES').toString(), fs.readFileSync(path.join(folder, 'THIRD_PARTY_NOTICES'), 'utf8'))
|
||||
}
|
||||
else {
|
||||
assert.equal(manifest.main, undefined)
|
||||
assert(!files.includes(`package/${manifest.module}`))
|
||||
assert(!files.includes(`package/${manifest.types}`))
|
||||
}
|
||||
const pkg = { label, name: manifest.name, archive, sha256: hash(fs.readFileSync(archive)), manifest, files: Object.fromEntries(files.map(member => [member, hash(readMember(archive, member))])) }
|
||||
packages.push(pkg)
|
||||
const consumer = consumerDirectory()
|
||||
try {
|
||||
writeJson(path.join(consumer, 'package.json'), { name: 'thumbnail-isolated-consumer', private: true, dependencies: { [pkg.name]: `file:${archive.replaceAll('\\', '/')}` } })
|
||||
fs.writeFileSync(path.join(output, `${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, `${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, `${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, 'function');
|
||||
assert.equal(value.default, undefined);
|
||||
assert.equal(value.DEFAULTS.number, 60);
|
||||
${label === 'candidate' ? `const legacy = require('${pkg.name}/legacy'); assert.equal(typeof legacy, 'function'); assert.equal(legacy.default, undefined);` : ''}
|
||||
(async () => {
|
||||
let imported;
|
||||
try { imported = await import('${pkg.name}'); }
|
||||
catch (error) { ${label === 'candidate' ? 'throw error;' : `assert.equal(error.code, 'ERR_MODULE_NOT_FOUND'); console.log(JSON.stringify({ commonjs: 'function', esm: error.code, typesMissing: true })); return;`} }
|
||||
${label === 'candidate' ? `assert.equal(typeof imported.default, 'function'); assert.deepEqual(Object.keys(imported), ['default']); const legacy = await import('${pkg.name}/legacy'); assert.equal(typeof legacy.default, 'function'); console.log(JSON.stringify({ commonjs: 'function', esm: 'function', legacy: 'function', ssrImport: true }));` : `throw new Error('Frozen missing ESM unexpectedly loaded');`}
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });`)
|
||||
const observation = run([runtimeFile], consumer)
|
||||
fs.writeFileSync(path.join(output, `${label}-runtime.log`), observation)
|
||||
runtime.push({ package: label, ...JSON.parse(observation.trim()) })
|
||||
const modes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]
|
||||
if (label === 'candidate') modes.push([ts, 'nodenext-cjs-no-interop'], [compat, 'node10-commonjs-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 }
|
||||
const source = label === 'candidate' ? fs.readFileSync(path.join(workspace, noInterop ? 'test/types/thumbnail-commonjs.cts' : 'test/types/thumbnail.ts'), 'utf8') : `import Thumbnail from '${pkg.name}'; new Thumbnail({ fileInput: document.createElement('input') });`
|
||||
function compile(code) {
|
||||
fs.writeFileSync(filename, code)
|
||||
const program = compiler.createProgram([filename], 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)
|
||||
writeJson(path.join(output, `${label}-${compiler.version}-${mode}.json`), diagnostics)
|
||||
if (label === 'candidate') assert.deepEqual(diagnostics, [], `${compiler.version} ${mode}`)
|
||||
else {
|
||||
assert.deepEqual(diagnostics.map(item => item.code), [mode === 'node10-commonjs' ? 2307 : 7016], 'Only the exact frozen missing entry/declaration diagnostic is expected')
|
||||
}
|
||||
const negative = label === 'candidate' ? compile(source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')) : []
|
||||
if (label === 'candidate') assert.equal(negative.length, noInterop ? 1 : 10)
|
||||
matrix.push({ package: label, compiler: compiler.version, mode, diagnostics, negative })
|
||||
}
|
||||
if (label === 'candidate') fs.cpSync(installed, path.join(output, 'installed-artifacts'), { recursive: true })
|
||||
}
|
||||
finally { removeConsumer(consumer) }
|
||||
}
|
||||
writeJson(path.join(output, 'report.json'), { task: 'PKG-TOOL-THUMB-04', scope: 'Frozen Git input fixture and candidate Yarn pack installed outside workspace. The frozen fixture is not an original npm archive; no recovered 3.5.31 complete archive is claimed. Candidate root/legacy runtime, offline frozen install, all files and strict consumers verified.', packages, matrix, runtime })
|
||||
console.log(`Thumbnail installed types verified: ${matrix.length} compiler cases; ${output}`)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Public type and editor contract runner.
|
||||
import test from 'node:test'
|
||||
import ts from 'typescript'
|
||||
import compat from 'typescript-compat'
|
||||
import { ESLint } from 'eslint'
|
||||
import { checkConsumer } from '../../scripts/typecheck.mjs'
|
||||
import { checkPluginEditorDeclaration, generatePluginEditorDeclaration } from '../../scripts/plugin-editor-types.mjs'
|
||||
|
||||
const modes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]
|
||||
const invalid = source => source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')
|
||||
|
||||
test('Thumbnail public declarations check actual callbacks, custom events and invalid calls in current and old TS', () => {
|
||||
const source = fs.readFileSync('test/types/thumbnail.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, 10, `${compiler.version} ${mode} negatives`)
|
||||
}
|
||||
const commonjs = fs.readFileSync('test/types/thumbnail-commonjs.cts', 'utf8')
|
||||
for (const [compiler, mode] of [[ts, 'nodenext-cjs'], [compat, 'node10-commonjs']]) {
|
||||
assert.deepEqual(checkConsumer(compiler, mode, commonjs), [])
|
||||
assert.equal(checkConsumer(compiler, mode, invalid(commonjs)).length, 1)
|
||||
}
|
||||
assert.deepEqual(checkConsumer(ts, 'node10-commonjs', fs.readFileSync('test/types/thumbnail-source.ts', 'utf8')), [], 'Source and declaration constructors must be mutually assignable')
|
||||
})
|
||||
|
||||
test('Thumbnail editor declaration is generated with its exact uppercase global and works in old TS', async () => {
|
||||
const source = fs.readFileSync('packages/artplayer-tool-thumbnail/types/artplayer-tool-thumbnail.d.ts', 'utf8')
|
||||
const code = generatePluginEditorDeclaration(source, 'ArtplayerToolThumbnail')
|
||||
const file = 'docs/assets/ts/artplayer-tool-thumbnail.d.ts'
|
||||
const [formatted] = await new ESLint({ fix: true, fixTypes: ['layout'] }).lintText(code, { filePath: file })
|
||||
assert.equal(formatted.errorCount, 0)
|
||||
assert.equal(fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n'), formatted.output || code)
|
||||
const consumer = `const input = document.createElement('input');
|
||||
const options: ArtplayerToolThumbnail.Option = { fileInput: input };
|
||||
const tool: ArtplayerToolThumbnail = new ArtplayerToolThumbnail(options);
|
||||
tool.on('update', (url, progress) => url + progress.toFixed());
|
||||
tool.on('custom', (value: number) => value.toFixed());
|
||||
const promise: Promise<void> = tool.start();
|
||||
// @ts-expect-error No lowercase global exists.
|
||||
const bad = new artplayerToolThumbnail(options);
|
||||
// @ts-expect-error Invalid known event payload.
|
||||
tool.emit('update', 12, 'url');`
|
||||
for (const compiler of [ts, compat]) {
|
||||
const core = fs.readFileSync('docs/assets/ts/artplayer.d.ts', 'utf8')
|
||||
assert.deepEqual(checkPluginEditorDeclaration(code, core, consumer, compiler), [])
|
||||
assert.equal(checkPluginEditorDeclaration(code, core, invalid(consumer), compiler).length, 2)
|
||||
}
|
||||
assert.throws(() => generatePluginEditorDeclaration(source.replace('declare class ArtplayerToolThumbnail', 'declare class WrongClass'), 'ArtplayerToolThumbnail'), /Unexpected CommonJS editor class/)
|
||||
})
|
||||
|
||||
test('Full editor generation bundles explicit local MediaBunny types instead of publishing an invalid media global', () => {
|
||||
const source = fs.readFileSync('packages/artplayer-proxy-mediabunny/types/artplayer-proxy-mediabunny.d.ts', 'utf8')
|
||||
const media = fs.readFileSync('packages/artplayer-proxy-mediabunny/types/media.d.ts', 'utf8')
|
||||
const core = fs.readFileSync('docs/assets/ts/artplayer.d.ts', 'utf8')
|
||||
const frozenGenerator = `${source.replace(/^import.*$/gm, '')}\nexport = artplayerProxyMediabunny;\nexport as namespace artplayerProxyMediabunny;`
|
||||
const code = generatePluginEditorDeclaration(source, 'artplayerProxyMediabunny', { './media': media })
|
||||
const consumer = `declare const player: artplayerProxyMediabunny.MediaBunnyPlayer;
|
||||
const canvas: (art: Artplayer) => HTMLCanvasElement = artplayerProxyMediabunny();
|
||||
const time: number | undefined = player.mediabunny?.currentTime;
|
||||
// @ts-expect-error HLS levels are read from state, not directly from the player alias.
|
||||
const bad = player.mediabunny?.levels;`
|
||||
for (const compiler of [ts, compat]) {
|
||||
assert(checkPluginEditorDeclaration(frozenGenerator, core, '', compiler).some(item => item.code === 2307), 'The old generator leaves a missing relative type import')
|
||||
assert.deepEqual(checkPluginEditorDeclaration(code, core, consumer, compiler), [])
|
||||
assert.equal(checkPluginEditorDeclaration(code, core, invalid(consumer), compiler).length, 1)
|
||||
}
|
||||
assert.throws(() => generatePluginEditorDeclaration(source, 'artplayerProxyMediabunny'), /Unsupported editor type re-export/)
|
||||
assert.throws(() => generatePluginEditorDeclaration(source, 'artplayerProxyMediabunny', { './media': media + '\nexport interface Extra {}' }), /explicitly cover/)
|
||||
const common = fs.readFileSync('docs/assets/js/common.js', 'utf8')
|
||||
assert(common.includes('./assets/ts/artplayer-tool-thumbnail.d.ts'))
|
||||
assert(!common.includes('./assets/ts/media.d.ts'))
|
||||
})
|
||||
Reference in new issue
Block a user