mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-11 13:56:15 -08:00
refactor(tooling): [MOD-PLUGIN-01] generate typed plugins with safe output handling
This commit is contained in:
1 parent
07d5bfef2e
commit
28c7abb2bb
32 files changed
+958
-161
No files matched your search
@@ -0,0 +1,86 @@
|
||||
# Plugin scaffold maintenance
|
||||
|
||||
Run `yarn create:plugin some-name` from the repository. The historical
|
||||
`node scripts/plugin/create.js some-name` path remains available and always targets
|
||||
the repository containing that script, even if the current directory differs.
|
||||
Use Node from `.node-version` and Yarn Classic 1.22.22. `--help` writes nothing.
|
||||
Names must be lowercase words separated by single hyphens; malformed names and
|
||||
extra arguments now fail before writing. Existing valid CLI names keep their
|
||||
package/global/example naming convention (`some-name` / `artplayerPluginSomeName`
|
||||
/ `some.name.js`). The README demo now uses the actual dotted example name.
|
||||
|
||||
## Ownership and write sequence
|
||||
|
||||
| Module | Responsibility |
|
||||
| --- | --- |
|
||||
| `create.js` | Checked JS compatibility shim; repository root, argv and failure exit status |
|
||||
| `cli.ts` | Validate command shape, render then publish, print next steps |
|
||||
| `render.ts` | Read text templates into a deterministic relative-path/content map; validate names, placeholders and output collisions |
|
||||
| `publish.ts` | Preflight destinations, stage complete text, exclusively reserve the package name and link files, roll back owned writes on failure |
|
||||
| `template/` | Generated package source, declarations, manifest, built-package tests and maintenance guide |
|
||||
|
||||
Templates ending in `.tpl` lose that suffix. `{{name}}`, `{{export}}` and
|
||||
`{{example}}` are the only replacements, including in filenames. Templates are
|
||||
UTF-8 text; links and unknown placeholders fail during rendering. Invalid TS
|
||||
placeholder identifiers are intentionally stored as `.tpl`; lint the renderer
|
||||
normally, and typecheck/build its rendered output through the integration test.
|
||||
|
||||
The writer refuses existing packages (including empty directories), examples and
|
||||
redirected destination paths. An exclusive temporary directory inside `packages/`
|
||||
holds the complete text before the final package is reserved. Hard links publish
|
||||
each complete file without overwriting another writer. Filesystem support for
|
||||
same-volume hard links is required; failure is reported and owned writes roll back.
|
||||
Staging never contains a top-level package.json and is ignored by project discovery.
|
||||
|
||||
On a caught failure, only files still matching this operation's written contents
|
||||
are removed, followed by empty directories created by this operation. Changed files
|
||||
and nonempty directories remain, with recovery paths in the error and its cause.
|
||||
The staged directory is removed only after checking its actual path and parent.
|
||||
The injected link function is an internal test seam for write failure/collision
|
||||
cases, not a CLI option. Publication is atomic per file, **not** a transaction over
|
||||
all files or crash recovery: a killed process may leave a partial new package or
|
||||
staging directory for manual inspection. This is not protection against an actor
|
||||
actively replacing filesystem ancestors between checks.
|
||||
|
||||
## Generated package contract
|
||||
|
||||
The synchronous TS factory returns its registration name, with a self `.default`
|
||||
alias for CommonJS users. The three existing build formats and `/legacy` entry
|
||||
remain. CommonJS/older-TS `.d.ts` and ESM `.d.mts` share `types/api.d.ts`, also used
|
||||
by the runtime source. The initial options type deliberately has no fields;
|
||||
implement explicit owned fields and behavior tests when developing the plugin.
|
||||
Styles are injected once per document at module evaluation, with a no-DOM import
|
||||
guard. During document loading this now inserts the style immediately instead of
|
||||
scheduling a DOMContentLoaded handler; imports after loading remain supported.
|
||||
These are templates for new packages;
|
||||
the command never rewrites an existing plugin's API or stylesheet behavior.
|
||||
|
||||
Generation does not install dependencies, change the root Yarn lock or inventories,
|
||||
or publish. New packages start at 1.0.0; the existing 22 packages' separate major
|
||||
upgrade policy remains unchanged. Before adding a new real workspace to CI, update
|
||||
the explicit package/demo/type/contract coverage and release scope through its own
|
||||
task, then update only the root lock. The scaffold's `artplayer: "*"` peer is a
|
||||
declaration of the host dependency, not evidence for every historical version.
|
||||
|
||||
## Validation
|
||||
|
||||
```sh
|
||||
yarn typecheck:scaffold
|
||||
yarn test:scaffold
|
||||
yarn lint
|
||||
```
|
||||
|
||||
`test/plugin-scaffold.test.js` freezes the former generator at
|
||||
`07d5bfef2e815e8038362bb836b9c496c7ef9862`, reproduces overwritten examples and
|
||||
incorrect demo names, then checks validation, concurrent collision, rollback,
|
||||
externally edited output retention and redirected directories. It runs the actual
|
||||
old CLI path from a different directory and builds a generated fixture using
|
||||
`scripts/build.js`. Built package tests exercise package export conditions, aliases
|
||||
and synchronous results; strict source, NodeNext ESM/CJS and TS 4.3.5 consumers are
|
||||
compiled. The DOM test checks duplicate stylesheet/import ownership with linkedom;
|
||||
it is not browser playback evidence for a future plugin's features.
|
||||
|
||||
Fixtures live under the ignored `refactor/.cache/` and are removed through checked
|
||||
paths. Tests do not add a 23rd workspace or alter real examples. Root `test:node`
|
||||
includes this suite, and `ci:check` includes the strict scaffold typecheck. Remote
|
||||
CI execution remains a separate release requirement.
|
||||
@@ -0,0 +1,16 @@
|
||||
import { publishPlugin } from './publish.ts'
|
||||
import { renderPlugin } from './render.ts'
|
||||
|
||||
export function createPlugin(root: string, args: string[]): void {
|
||||
if (args.length === 1 && args[0] === '--help') {
|
||||
console.log('Usage: yarn create:plugin <lowercase-plugin-name>')
|
||||
return
|
||||
}
|
||||
if (args.length !== 1 || !args[0])
|
||||
throw new Error('Provide exactly one plugin name. Use yarn create:plugin --help')
|
||||
const name = args[0]
|
||||
publishPlugin(root, name, renderPlugin(name))
|
||||
console.log(`Created artplayer-plugin-${name} and example ${name.replaceAll('-', '.')}.js`)
|
||||
console.log(`Next: yarn build artplayer-plugin-${name}; yarn workspace artplayer-plugin-${name} test`)
|
||||
console.log('Register the new workspace/demo in the refactor inventories and update the root Yarn lock before CI.')
|
||||
}
|
||||
+6
-102
@@ -1,107 +1,11 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { createPlugin } from './cli.ts'
|
||||
|
||||
// Get the current file path
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
// Get the plugin name from command line arguments
|
||||
const args = process.argv.slice(2)
|
||||
const pluginName = args[0]
|
||||
|
||||
if (!pluginName) {
|
||||
console.error('Please provide a plugin name, e.g., npm run create:plugin somePluginName')
|
||||
process.exit(1)
|
||||
try {
|
||||
createPlugin(fileURLToPath(new URL('../../', import.meta.url)), process.argv.slice(2))
|
||||
}
|
||||
|
||||
// Validate plugin name
|
||||
const pluginNameRegex = /^[a-z-]+$/
|
||||
if (!pluginNameRegex.test(pluginName)) {
|
||||
console.error('Invalid plugin name. Only lowercase letters and hyphens are allowed.')
|
||||
process.exit(1)
|
||||
catch (error) {
|
||||
console.error(error instanceof Error ? error.message : error)
|
||||
process.exitCode = 1
|
||||
}
|
||||
|
||||
// Convert plugin name to export name (PascalCase)
|
||||
const exportName = pluginName
|
||||
.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join('')
|
||||
const exportNameFull = `artplayerPlugin${exportName}`
|
||||
|
||||
// Define paths
|
||||
const templateDir = path.join(__dirname, 'template')
|
||||
const destDir = path.join(__dirname, '../../packages', `artplayer-plugin-${pluginName}`)
|
||||
const exampleDir = path.join(__dirname, '../../docs/assets/example')
|
||||
const exampleFileName = `${pluginName.replace(/-/g, '.')}.js`
|
||||
const exampleFile = path.join(exampleDir, exampleFileName)
|
||||
|
||||
// Check if a plugin with the same name already exists
|
||||
if (fs.existsSync(destDir)) {
|
||||
console.error(`Plugin ${pluginName} already exists. Please choose another name.`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Create the destination directory
|
||||
fs.mkdirSync(destDir, { recursive: true })
|
||||
|
||||
// Read files from the template directory and copy them to the destination directory
|
||||
function copyTemplateFiles(templateDir, destDir) {
|
||||
const files = fs.readdirSync(templateDir)
|
||||
|
||||
files.forEach((file) => {
|
||||
const templateFilePath = path.join(templateDir, file)
|
||||
let destFilePath = path.join(destDir, file)
|
||||
|
||||
const stats = fs.statSync(templateFilePath)
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
fs.mkdirSync(destFilePath, { recursive: true })
|
||||
copyTemplateFiles(templateFilePath, destFilePath)
|
||||
}
|
||||
else {
|
||||
// Replace placeholders in the file name
|
||||
destFilePath = destFilePath.replace(/\{\{name\}\}/g, pluginName)
|
||||
|
||||
let content = fs.readFileSync(templateFilePath, 'utf8')
|
||||
|
||||
// Replace placeholders in the file content
|
||||
content = content.replace(/\{\{name\}\}/g, pluginName)
|
||||
content = content.replace(/\{\{export\}\}/g, exportNameFull)
|
||||
|
||||
fs.writeFileSync(destFilePath, content)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Create the example file
|
||||
function createExampleFile() {
|
||||
const exampleContent = `
|
||||
// npm i artplayer-plugin-${pluginName}
|
||||
// import ${exportNameFull} from 'artplayer-plugin-${pluginName}';
|
||||
|
||||
var art = new Artplayer({
|
||||
container: '.artplayer-app',
|
||||
url: '/assets/sample/video.mp4',
|
||||
plugins: [
|
||||
${exportNameFull}({
|
||||
//
|
||||
}),
|
||||
],
|
||||
});
|
||||
`.trim()
|
||||
|
||||
if (!fs.existsSync(exampleDir)) {
|
||||
fs.mkdirSync(exampleDir, { recursive: true })
|
||||
}
|
||||
|
||||
fs.writeFileSync(exampleFile, exampleContent)
|
||||
console.log(`Example file created at ${exampleFile}`)
|
||||
}
|
||||
|
||||
// Start copying template files and creating example file
|
||||
copyTemplateFiles(templateDir, destDir)
|
||||
createExampleFile()
|
||||
|
||||
console.log(`Plugin ${pluginName} has been successfully created at ${destDir}`)
|
||||
@@ -0,0 +1,96 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
function checkedPath(root: string, relative: string): string {
|
||||
const target = path.resolve(root, relative)
|
||||
const within = path.relative(root, target)
|
||||
if (!within || within.startsWith('..') || path.isAbsolute(within))
|
||||
throw new Error(`Output escapes the repository: ${relative}`)
|
||||
let current = root
|
||||
for (const part of within.split(path.sep)) {
|
||||
current = path.join(current, part)
|
||||
try {
|
||||
if (fs.lstatSync(current).isSymbolicLink())
|
||||
throw new Error(`Refusing redirected output: ${current}`)
|
||||
}
|
||||
catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
function removeStage(stage: string, packages: string): void {
|
||||
if (fs.realpathSync(stage) !== stage || path.dirname(stage) !== packages || !path.basename(stage).startsWith('.artplayer-scaffold-'))
|
||||
throw new Error(`Refusing redirected scaffold cleanup: ${stage}`)
|
||||
fs.rmSync(stage, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
export function publishPlugin(root: string, name: string, files: ReadonlyMap<string, string>, link: typeof fs.linkSync = fs.linkSync): void {
|
||||
root = fs.realpathSync(root)
|
||||
const packages = checkedPath(root, 'packages')
|
||||
if (!fs.statSync(packages).isDirectory())
|
||||
throw new Error('The repository packages directory is missing')
|
||||
const destination = checkedPath(root, `packages/artplayer-plugin-${name}`)
|
||||
if (fs.existsSync(destination))
|
||||
throw new Error(`Plugin ${name} already exists`)
|
||||
for (const relative of files.keys()) {
|
||||
if (fs.existsSync(checkedPath(root, relative)))
|
||||
throw new Error(`Refusing to overwrite existing output: ${relative}`)
|
||||
}
|
||||
const stage = fs.mkdtempSync(path.join(packages, '.artplayer-scaffold-'))
|
||||
const createdDirectories: string[] = []
|
||||
const createdFiles: { target: string, content: string }[] = []
|
||||
function directory(target: string) {
|
||||
if (fs.existsSync(target))
|
||||
return
|
||||
directory(path.dirname(target))
|
||||
fs.mkdirSync(target)
|
||||
createdDirectories.push(target)
|
||||
}
|
||||
try {
|
||||
let index = 0
|
||||
for (const content of files.values())
|
||||
fs.writeFileSync(path.join(stage, String(index++)), content, { flag: 'wx' })
|
||||
// Reserve the package name without replacing even an empty concurrently-created directory.
|
||||
fs.mkdirSync(destination)
|
||||
createdDirectories.push(destination)
|
||||
index = 0
|
||||
for (const [relative, content] of files) {
|
||||
const target = checkedPath(root, relative)
|
||||
directory(path.dirname(target))
|
||||
link(path.join(stage, String(index++)), target)
|
||||
createdFiles.push({ target, content })
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
const retained: string[] = []
|
||||
for (const { target, content } of createdFiles.reverse()) {
|
||||
try {
|
||||
if (fs.lstatSync(target).isSymbolicLink() || fs.readFileSync(target, 'utf8') !== content)
|
||||
retained.push(target)
|
||||
else fs.unlinkSync(target)
|
||||
}
|
||||
catch (failure) {
|
||||
if ((failure as NodeJS.ErrnoException).code !== 'ENOENT')
|
||||
retained.push(target)
|
||||
}
|
||||
}
|
||||
for (const directory of createdDirectories.reverse()) {
|
||||
try {
|
||||
fs.rmdirSync(directory)
|
||||
}
|
||||
catch (failure) {
|
||||
if ((failure as NodeJS.ErrnoException).code !== 'ENOENT')
|
||||
retained.push(directory)
|
||||
}
|
||||
}
|
||||
if (retained.length)
|
||||
throw new Error(`Scaffolding failed; preserve changed outputs for recovery: ${retained.join(', ')}`, { cause: error })
|
||||
throw error
|
||||
}
|
||||
finally {
|
||||
removeStage(stage, packages)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
export function renderPlugin(name: string, template = fileURLToPath(new URL('./template', import.meta.url))): Map<string, string> {
|
||||
if (!/^[a-z]+(?:-[a-z]+)*$/.test(name))
|
||||
throw new Error('Use lowercase words separated by single hyphens for the plugin name')
|
||||
const replacements: Record<string, string> = {
|
||||
name,
|
||||
export: `artplayerPlugin${name.split('-').map(word => word[0]!.toUpperCase() + word.slice(1)).join('')}`,
|
||||
example: name.replaceAll('-', '.'),
|
||||
}
|
||||
const replace = (text: string) => text.replace(/\{\{(\w+)\}\}/g, (_, key: string) => {
|
||||
if (!Object.hasOwn(replacements, key))
|
||||
throw new Error(`Unknown template placeholder: ${key}`)
|
||||
return replacements[key]!
|
||||
})
|
||||
const files = new Map<string, string>()
|
||||
function visit(directory: string, relative = '') {
|
||||
for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
const source = path.join(directory, entry.name)
|
||||
const target = path.posix.join(relative, entry.name)
|
||||
if (entry.isSymbolicLink())
|
||||
throw new Error(`Template links are not supported: ${source}`)
|
||||
if (entry.isDirectory()) {
|
||||
visit(source, target)
|
||||
}
|
||||
else if (entry.isFile()) {
|
||||
const output = `packages/artplayer-plugin-${name}/${replace(target).replace(/\.tpl$/, '')}`
|
||||
if (files.has(output))
|
||||
throw new Error(`Duplicate template output: ${output}`)
|
||||
files.set(output, replace(fs.readFileSync(source, 'utf8')).replaceAll('\r\n', '\n'))
|
||||
}
|
||||
else {
|
||||
throw new Error(`Unsupported template entry: ${source}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
visit(template)
|
||||
files.set(`docs/assets/example/${replacements.example}.js`, `// npm i artplayer-plugin-${name}
|
||||
// import ${replacements.export} from 'artplayer-plugin-${name}';
|
||||
|
||||
var art = new Artplayer({
|
||||
container: '.artplayer-app',
|
||||
url: '/assets/sample/video.mp4',
|
||||
plugins: [${replacements.export}({})],
|
||||
});
|
||||
`)
|
||||
return files
|
||||
}
|
||||
@@ -4,7 +4,62 @@
|
||||
|
||||
## Demo
|
||||
|
||||
[https://artplayer.org](https://artplayer.org/?libs=./uncompiled/artplayer-plugin-{{name}}/index.js&example={{name}})
|
||||
[Local demo](http://localhost:8082/?libs=./uncompiled/artplayer-plugin-{{name}}/index.js&example={{example}})
|
||||
|
||||
Run `yarn dev artplayer-plugin-{{name}}` from the repository root first.
|
||||
The generated example is `docs/assets/example/{{example}}.js`.
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import {{export}} from 'artplayer-plugin-{{name}}'
|
||||
|
||||
const art = new Artplayer({
|
||||
container: '.artplayer-app',
|
||||
url: '/assets/sample/video.mp4',
|
||||
plugins: [{{export}}({})],
|
||||
})
|
||||
```
|
||||
|
||||
The factory also supports direct CommonJS `require`, its `.default` alias,
|
||||
the `/legacy` entry, and the browser global `{{export}}`.
|
||||
This scaffold only registers a name; implement and test the intended behavior.
|
||||
|
||||
## Maintenance
|
||||
|
||||
| File | Responsibility |
|
||||
| --- | --- |
|
||||
| `src/index.ts` | Synchronous plugin factory and registration result |
|
||||
| `src/stylesheet.ts` | One stylesheet per document on module evaluation; safe to import without a DOM |
|
||||
| `src/style.less` | Plugin styles; retain the package selector when adding UI |
|
||||
| `src/assets.d.ts` | Build-time inline stylesheet declaration |
|
||||
| `types/api.d.ts` | Owned option/result types shared by source and public entrypoints |
|
||||
| `types/artplayer-plugin-{{name}}.d.ts` | CommonJS and older TypeScript entry |
|
||||
| `types/artplayer-plugin-{{name}}.d.mts` | Native ESM declaration entry |
|
||||
| `test/plugin.test.mjs` | Built ESM/CommonJS/legacy synchronous factory and alias contracts |
|
||||
|
||||
Replace the empty `Option` contract with explicit fields when implementing behavior.
|
||||
Keep the return value synchronous unless the plugin explicitly requires an async contract.
|
||||
Keep source and both declaration entrypoints consistent; test real consumers when changing them.
|
||||
The stylesheet has document lifetime and is shared by instances. Resources added by the
|
||||
plugin (listeners, timers, requests, DOM, workers) need per-instance ownership and cleanup
|
||||
on `art.on('destroy', ...)`; guard late callbacks and source changes. Add behavior-specific
|
||||
normal, error, concurrency and cleanup tests, plus real browser checks for media/UI work.
|
||||
|
||||
Use the repository Node version from `.node-version` and Yarn Classic 1.22.22:
|
||||
|
||||
```sh
|
||||
yarn workspace artplayer-plugin-{{name}} typecheck
|
||||
yarn build artplayer-plugin-{{name}}
|
||||
yarn workspace artplayer-plugin-{{name}} test
|
||||
```
|
||||
|
||||
Build before testing; the tests consume the actual distribution files. ESM and modern
|
||||
UMD target es2020, legacy UMD targets es2015. The generator uses the existing toolchain;
|
||||
it does not install dependencies, publish, or update the root lockfile. Register this
|
||||
workspace/demo in the refactor inventories and validation mappings before CI, and update
|
||||
only the root `yarn.lock`. The declared ArtPlayer peer does not itself prove compatibility:
|
||||
test the core versions and devices required by the implemented features.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -19,20 +19,41 @@
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/artplayer-plugin-{{name}}.d.ts",
|
||||
"import": "./dist/artplayer-plugin-{{name}}.mjs",
|
||||
"require": "./dist/artplayer-plugin-{{name}}.js"
|
||||
"import": {
|
||||
"types": "./types/artplayer-plugin-{{name}}.d.mts",
|
||||
"default": "./dist/artplayer-plugin-{{name}}.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./types/artplayer-plugin-{{name}}.d.ts",
|
||||
"default": "./dist/artplayer-plugin-{{name}}.js"
|
||||
},
|
||||
"default": "./dist/artplayer-plugin-{{name}}.js"
|
||||
},
|
||||
"./legacy": {
|
||||
"types": "./types/artplayer-plugin-{{name}}.d.ts",
|
||||
"import": "./dist/artplayer-plugin-{{name}}.legacy.js",
|
||||
"require": "./dist/artplayer-plugin-{{name}}.legacy.js"
|
||||
|
||||
}
|
||||
"import": {
|
||||
"types": "./types/artplayer-plugin-{{name}}.d.mts",
|
||||
"default": "./dist/artplayer-plugin-{{name}}.legacy.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./types/artplayer-plugin-{{name}}.d.ts",
|
||||
"default": "./dist/artplayer-plugin-{{name}}.legacy.js"
|
||||
},
|
||||
"default": "./dist/artplayer-plugin-{{name}}.legacy.js"
|
||||
},
|
||||
"./dist/*": "./dist/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"main": "dist/artplayer-plugin-{{name}}.js",
|
||||
"module": "./dist/artplayer-plugin-{{name}}.mjs",
|
||||
"types": "types/artplayer-plugin-{{name}}.d.ts",
|
||||
"legacy": "dist/artplayer-plugin-{{name}}.legacy.js",
|
||||
"files": ["README.md", "dist", "types"],
|
||||
"scripts": {
|
||||
"typecheck": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json --noEmit",
|
||||
"test": "node --test test/plugin.test.mjs"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"artplayer": "*"
|
||||
},
|
||||
"browserslist": "last 1 Chrome version"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
declare module '*.less?inline' {
|
||||
const css: string
|
||||
export default css
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import style from './style.less?inline';
|
||||
|
||||
export default function {{export}}(option) {
|
||||
return (art) => {
|
||||
return {
|
||||
name: '{{export}}',
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
const id = 'artplayer-plugin-{{name}}';
|
||||
let $style = document.getElementById(id)
|
||||
if (!$style) {
|
||||
$style = document.createElement('style')
|
||||
$style.id = id
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.head.appendChild($style)
|
||||
})
|
||||
}
|
||||
else {
|
||||
(document.head || document.documentElement).appendChild($style)
|
||||
}
|
||||
}
|
||||
$style.textContent = style
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
import type { Option, Result } from '../types/api.js'
|
||||
import './stylesheet'
|
||||
|
||||
function {{export}}(_option: Option = {}) {
|
||||
return (_art: Artplayer): Result => ({ name: '{{export}}' })
|
||||
}
|
||||
|
||||
export default Object.assign({{export}}, { default: {{export}} })
|
||||
@@ -1,3 +1,3 @@
|
||||
.artplayer-plugin-{{name}} {
|
||||
//
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import css from './style.less?inline'
|
||||
|
||||
// The stylesheet belongs to the module, not individual player instances.
|
||||
if (typeof document !== 'undefined') {
|
||||
const id = 'artplayer-plugin-{{name}}'
|
||||
if (!document.getElementById(id)) {
|
||||
const style = document.createElement('style')
|
||||
style.id = id
|
||||
style.textContent = css
|
||||
;(document.head || document.documentElement).appendChild(style)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createRequire } from 'node:module'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Verify the generated distribution contracts.
|
||||
import test from 'node:test'
|
||||
import plugin from 'artplayer-plugin-{{name}}'
|
||||
import legacy from 'artplayer-plugin-{{name}}/legacy'
|
||||
|
||||
test('classic factory and historical default alias return the synchronous plugin result', () => {
|
||||
const require = createRequire(import.meta.url)
|
||||
for (const factory of [plugin, legacy, require('artplayer-plugin-{{name}}'), require('artplayer-plugin-{{name}}/legacy')]) {
|
||||
assert.equal(factory.default, factory)
|
||||
const result = factory({})({})
|
||||
assert.deepEqual(result, { name: '{{export}}' })
|
||||
assert.equal(result instanceof Promise, false)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["src", "types"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
|
||||
// Replace this empty option type with the documented options your plugin owns.
|
||||
export type Option = Record<string, never>
|
||||
export interface Result { name: '{{export}}' }
|
||||
export type Factory = (option?: Option) => (art: Artplayer) => Result
|
||||
export type RuntimeFactory = Factory & { default: Factory }
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { RuntimeFactory } from './api.js'
|
||||
|
||||
declare const {{export}}: RuntimeFactory
|
||||
export default {{export}}
|
||||
export type { Factory, Option, Result, RuntimeFactory } from './api.js'
|
||||
@@ -1,13 +0,0 @@
|
||||
import type Artplayer from 'artplayer';
|
||||
|
||||
interface Option {
|
||||
//
|
||||
}
|
||||
|
||||
interface Result {
|
||||
name: '{{export}}';
|
||||
}
|
||||
|
||||
declare const {{export}}: (option: Option) => (art: Artplayer) => Result;
|
||||
|
||||
export default {{export}}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { RuntimeFactory } from './api.js'
|
||||
|
||||
declare const {{export}}: RuntimeFactory
|
||||
// eslint-disable-next-line ts/no-redeclare -- Expose types on the CommonJS factory.
|
||||
declare namespace {{export}} {
|
||||
type Option = import('./api.js').Option
|
||||
type Result = import('./api.js').Result
|
||||
type Factory = import('./api.js').Factory
|
||||
type RuntimeFactory = import('./api.js').RuntimeFactory
|
||||
}
|
||||
export = {{export}}
|
||||
Reference in new issue
Block a user