feat: update changelog with Parcel to Vite refactor

feat: add danmuku plugin documentation and i18n setup guide

- Created a new documentation file for the danmuku plugin with code examples.
- Added a comprehensive guide for language settings (i18n) including default languages, importing languages, adding new languages, and modifying existing languages.
- Included code snippets for initializing the Artplayer with different language settings.

Add internationalization support for multiple languages in Artplayer

- Introduced Arabic (ar), Czech (cs), Spanish (es), Persian (fa), French (fr), Indonesian (id), Polish (pl), Russian (ru), Turkish (tr), Vietnamese (vi), and Traditional Chinese (zh-tw) translations.
- Created corresponding JavaScript and ES module files for each language containing translated strings for player controls and settings.
- Added a new documentation file for language settings in the project.

feat: add initial implementation of start option page with interactive code execution buttons

feat: add danmuku plugin documentation and i18n setup guide

- Created a new documentation file for the danmuku plugin, providing code examples for usage.
- Added a comprehensive guide for language settings, including default languages, importing languages, adding new languages, and modifying existing languages.
- Included code snippets for initializing the Artplayer with different language settings and importing language files.

refactor: update danmuku worker implementation and remove unused i18n files

- Changed the way the Danmuku class creates a Web Worker by using a worker loader instead of creating a blob.
- Removed Arabic, Czech, Spanish, Persian, French, Indonesian, Polish, Russian, Turkish, Vietnamese, and Traditional Chinese i18n files as they are no longer needed.
- Updated the Vite build configuration to disable banners for inline workers and added a plugin to remove worker banners from generated code.

feat: add danmuku plugin and i18n setup documentation

- Created a new documentation file for the danmuku plugin, providing code examples for usage.
- Added a new documentation file for language settings (i18n), detailing how to set default languages, import languages, and add new languages.
- Included code snippets for initializing the Artplayer with different language settings and examples for modifying existing languages.

feat: add danmuku plugin and i18n setup documentation

- Created a new documentation file for the danmuku plugin with code examples.
- Added a new documentation file for language settings (i18n) including import instructions and examples for setting default and custom languages.
- Updated the i18n documentation to include details on adding and modifying languages.

feat: add danmuku plugin and i18n setup documentation

- Created a new JavaScript file for the danmuku plugin documentation with multiple code execution buttons.
- Added a new JavaScript file for i18n (internationalization) setup documentation, including instructions for language settings, importing languages, and modifying language options.
- Included examples for setting default languages and adding new languages in the i18n documentation.

Refactor code structure for improved readability and maintainability
This commit is contained in:
Harvey Zhao committed 2026-03-07 15:21:34 +08:00
1 parent ddb78cf850
commit 699bf09577
397 files changed
+99776 -8638

No files matched your search

+18 -50
View File
@@ -1,9 +1,10 @@
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { Parcel } from '@parcel/core'
import cpy from 'cpy'
import { glob } from 'glob'
import { build as viteBuild } from 'vite'
import { getViteBuildConfig, toPascalCase } from './utils.js'
const basePath = 'packages/artplayer'
const i18nSrcDir = path.join(basePath, 'src/i18n')
@@ -21,63 +22,30 @@ async function buildI18n() {
}
fs.mkdirSync(distDir, { recursive: true })
const distJs = path.join(distDir, 'cjs')
const bundlerJs = new Parcel({
entries,
defaultConfig: '@parcel/config-default',
mode: 'production',
defaultTargetOptions: {
distDir: distJs,
outputFormat: 'global',
sourceMaps: false,
isLibrary: true,
engines: { browsers: ['last 1 Chrome version'] },
},
env: { NODE_ENV: 'production' },
})
await bundlerJs.run()
console.log('✅ Built i18n global (.js)')
for (const entry of entries) {
const baseName = path.basename(entry, '.js')
const globalName = `artplayerI18n${toPascalCase(baseName)}`
const distMjs = path.join(distDir, 'esm')
const bundlerMjs = new Parcel({
entries,
defaultConfig: '@parcel/config-default',
mode: 'production',
defaultTargetOptions: {
context: 'browser',
distDir: distMjs,
outputFormat: 'esmodule',
sourceMaps: false,
isLibrary: true,
engines: { browsers: ['last 1 Chrome version'] },
},
env: { NODE_ENV: 'production' },
})
const { bundleGraph } = await bundlerMjs.run()
const bundles = bundleGraph.getBundles()
for (const bundle of bundles) {
if (bundle.type === 'js') {
const oldPath = bundle.filePath
const newPath = oldPath.replace(/\.js$/, '.mjs')
fs.renameSync(oldPath, newPath)
// Build UMD and ESM formats
for (const [format, ext] of [['umd', '.js'], ['es', '.mjs']]) {
const config = getViteBuildConfig({
entry,
outDir: distDir,
name: globalName,
format,
fileName: `${baseName}${ext}`,
})
await viteBuild(config)
}
}
console.log('✅ Built i18n esm (.mjs)')
for (const f of fs.readdirSync(distJs)) {
fs.renameSync(path.join(distJs, f), path.join(distDir, f))
console.log(`✅ Built i18n: ${baseName}`)
}
for (const f of fs.readdirSync(distMjs)) {
fs.renameSync(path.join(distMjs, f), path.join(distDir, f))
}
fs.rmSync(distJs, { recursive: true, force: true })
fs.rmSync(distMjs, { recursive: true, force: true })
await cpy(distDir, compiledPath)
console.log('✨ Finished building i18n with both .js and .mjs')
console.log('✨ Finished building i18n')
}
buildI18n().catch((err) => {
console.error('❌ Build i18n failed', err)
console.error('❌ Build i18n failed:', err)
process.exitCode = 1
})
+71 -120
View File
@@ -1,152 +1,103 @@
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { Parcel } from '@parcel/core'
import cpy from 'cpy'
import prompts from 'prompts'
import { getProjects } from './utils.js'
import { build as viteBuild } from 'vite'
import { getProjects, getViteBuildConfig, toPascalCase } from './utils.js'
const projects = getProjects()
const compiledPath = path.resolve('docs/compiled')
function compressString(input) {
return input
.replace(/\s*(<[^>]+>)\s*/g, '$1')
.replace(/(\r\n|\n|\r)/g, ' ')
.replace(/\s+/g, ' ')
.trim()
}
async function build(name, targetName, clean = false) {
const projectPackageJson = path.join(projects[name], 'package.json')
const { version } = JSON.parse(fs.readFileSync(projectPackageJson, 'utf-8'))
process.chdir(projects[name])
const distDir = path.join(projects[name], 'dist')
if (clean && fs.existsSync(distDir)) {
fs.rmSync(distDir, { recursive: true, force: true })
console.log(`🗑️ Cleaned ${distDir}`)
}
const targets = {
main: {
context: 'browser',
distDir,
sourceMap: false,
outputFormat: 'global',
isLibrary: true,
engines: {
browsers: 'last 1 Chrome version',
},
},
legacy: {
context: 'browser',
distDir,
sourceMap: false,
outputFormat: 'global',
isLibrary: true,
engines: {
browsers: 'IE 11',
},
},
esm: {
context: 'browser',
distDir,
sourceMap: false,
outputFormat: 'esmodule',
isLibrary: true,
engines: {
browsers: 'last 1 Chrome version',
},
},
}
const names = {
main: `${name}.js`,
legacy: `${name}.legacy.js`,
esm: `${name}.mjs`,
}
const entryFile = path.join(projects[name], 'src/index.js')
const bundler = new Parcel({
entries: entryFile,
defaultConfig: '@parcel/config-default',
mode: 'production',
targets: {
[targetName]: targets[targetName],
},
env: {
NODE_ENV: 'production',
},
})
const banner = `
/*!
function getBanner(name, version) {
return `/*!
* ${name}.js v${version}
* Github: https://github.com/zhw2590582/ArtPlayer
* (c) 2017-${new Date().getFullYear()} Harvey Zhao
* Released under the MIT License.
*/
`
*/`
}
const { bundleGraph, buildTime } = await bundler.run()
const bundles = bundleGraph.getBundles()
const BUILD_FORMATS = {
main: { format: 'umd', ext: '.js', target: 'es2020', minify: 'terser' },
legacy: { format: 'umd', ext: '.legacy.js', target: 'es2015', minify: 'terser' },
esm: { format: 'es', ext: '.mjs', target: 'es2020', minify: 'terser' },
}
const filePath = path.join(distDir, 'index.js')
const newFilePath = path.join(distDir, names[targetName])
const code = fs.readFileSync(filePath, 'utf-8')
async function build(name, targetName, clean = false) {
const projectDir = projects[name]
const { version } = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf-8'))
const distDir = path.join(projectDir, 'dist')
const entryFile = path.join(projectDir, 'src/index.js')
fs.writeFileSync(filePath, banner + compressString(code))
fs.renameSync(filePath, newFilePath)
const size = fs.statSync(newFilePath).size / 1024
await cpy(newFilePath, compiledPath)
if (clean && fs.existsSync(distDir)) {
fs.rmSync(distDir, { recursive: true, force: true })
}
console.log(
`✨ Built@${targetName} ${name}@${version}`,
`Bundles@${bundles.length}`,
`Time@${buildTime}ms`,
`Size@${size.toFixed(2)}kb`,
)
const startTime = Date.now()
const { format, ext, target, minify } = BUILD_FORMATS[targetName]
const fileName = `${name}${ext}`
const config = getViteBuildConfig({
entry: entryFile,
outDir: distDir,
name: toPascalCase(name),
format,
fileName,
minify,
target,
banner: getBanner(name, version),
})
// Terser options for all builds (preserve banner comments starting with /*!)
const isLegacy = targetName === 'legacy'
config.build.terserOptions = {
ecma: isLegacy ? 5 : 2020,
compress: { ecma: isLegacy ? 5 : 2020, comparisons: false, inline: 2 },
mangle: { safari10: true },
format: {
ecma: isLegacy ? 5 : 2020,
comments: /^!/,
ascii_only: true,
max_line_len: false, // No line length limit
},
}
await viteBuild({ root: projectDir, ...config })
const outFile = path.join(distDir, fileName)
const size = (fs.statSync(outFile).size / 1024).toFixed(2)
await cpy(outFile, compiledPath)
console.log(`✨ Built@${targetName} ${name}@${version} Time@${Date.now() - startTime}ms Size@${size}kb`)
}
async function buildProject(name) {
await build(name, 'main', true)
await build(name, 'legacy')
await build(name, 'esm')
}
async function runBuild() {
if (process.argv.pop() === 'all') {
const bundles = Object.keys(projects).map(name => async () => {
await build(name, 'main', true)
await build(name, 'legacy')
await build(name, 'esm')
})
await runPromisesInSeries(bundles)
if (process.argv.includes('all')) {
for (const name of Object.keys(projects)) {
await buildProject(name)
}
console.log('✅ Finished building all packages!')
}
else {
const response = await prompts({
const { value } = await prompts({
type: 'select',
name: 'value',
message: 'Which project do you want to build?',
choices: Object.keys(projects).map(name => ({
title: name,
value: name,
})),
initial: 0,
choices: Object.keys(projects).map(name => ({ title: name, value: name })),
})
if (response.value) {
await build(response.value, 'main', true)
await build(response.value, 'legacy')
await build(response.value, 'esm')
}
if (value)
await buildProject(value)
}
}
function runPromisesInSeries(ps) {
return ps.reduce((p, next) => p.then(next), Promise.resolve())
}
runBuild().catch((error) => {
console.error('❌ Build script encountered an error:', error)
console.error('❌ Build failed:', error)
process.exitCode = 1
})
+43 -62
View File
@@ -1,11 +1,10 @@
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { Parcel } from '@parcel/core'
import prompts from 'prompts'
import servor from 'servor'
import openBrowser from 'servor/utils/openBrowser.js'
import { getProjects } from './utils.js'
import { build as viteBuild } from 'vite'
import { getProjects, getViteBuildConfig, toPascalCase } from './utils.js'
const projects = getProjects()
@@ -13,8 +12,7 @@ async function develop(name) {
const projectPath = projects[name]
const uncompiledPath = path.resolve(`docs/uncompiled/${name}`)
const entryFile = path.join(projectPath, 'src/index.js')
let isOpenBrowser = false
let browserOpened = false
const { url } = await servor({
root: 'docs',
@@ -23,72 +21,55 @@ async function develop(name) {
port: 8082,
})
process.chdir(projectPath)
fs.mkdirSync(uncompiledPath, { recursive: true })
const bundler = new Parcel({
entries: entryFile,
defaultConfig: '@parcel/config-default',
mode: 'development',
targets: {
main: {
distDir: uncompiledPath,
outputFormat: 'global',
isLibrary: true,
engines: {
browsers: ['last 1 Chrome version'],
},
},
},
env: {
NODE_ENV: 'development',
},
additionalReporters: [
{
packageName: '@parcel/reporter-cli',
resolveFrom: fileURLToPath(import.meta.url),
},
],
})
async function buildBundle() {
const startTime = Date.now()
try {
const config = getViteBuildConfig({
entry: entryFile,
outDir: uncompiledPath,
name: toPascalCase(name),
format: 'iife',
fileName: 'index.js',
minify: false,
emptyOutDir: true,
})
config.define['process.env.NODE_ENV'] = JSON.stringify('development')
bundler.watch((error, event) => {
if (error) {
console.error(`[${name}] ❌ Build error:`, error)
return
}
await viteBuild({ root: projectPath, ...config })
console.log(`[${name}] ✅ Built in ${Date.now() - startTime}ms`)
if (event.type === 'buildSuccess') {
const bundles = event.bundleGraph.getBundles()
console.log(`[${name}] ✅ Built ${bundles.length} bundles in ${event.buildTime}ms`)
if (!isOpenBrowser) {
isOpenBrowser = true
if (!browserOpened) {
browserOpened = true
openBrowser(url)
}
}
catch (error) {
console.error(`[${name}] ❌ Build error:`, error)
}
}
if (event.type === 'buildFailure') {
console.error(`[${name}] ❌ Build failure:`, event.diagnostics)
await buildBundle()
const srcPath = path.join(projectPath, 'src')
console.log(`[${name}] 👀 Watching ${srcPath}...`)
fs.watch(srcPath, { recursive: true }, async (_, filename) => {
if (filename) {
console.log(`[${name}] 📝 Changed: ${filename}`)
await buildBundle()
}
})
}
(async () => {
try {
const response = await prompts({
type: 'select',
name: 'value',
message: 'Which project do you want to develop?',
choices: Object.keys(projects).map(name => ({
title: name,
value: name,
})),
initial: 0,
})
if (response.value) {
await develop(response.value)
}
}
catch (error) {
console.error(`❌ Prompt error: ${error.message}`)
}
const { value } = await prompts({
type: 'select',
name: 'value',
message: 'Which project do you want to develop?',
choices: Object.keys(projects).map(name => ({ title: name, value: name })),
})
if (value)
await develop(value)
})()
+1 -1
View File
@@ -1,4 +1,4 @@
import style from 'bundle-text:./style.less';
import style from './style.less?inline';
export default function {{export}}(option) {
return (art) => {
+92 -14
View File
@@ -8,6 +8,7 @@ export function getProjects() {
.sync('packages/*')
.sort()
.filter(item => !item.endsWith('artplayer-vitepress'))
.filter(item => fs.existsSync(path.join(item, 'package.json')))
.reduce((result, item) => {
const name = item.split(/\/|\\/g).pop()
result[name] = path.resolve(process.cwd(), item)
@@ -15,18 +16,95 @@ export function getProjects() {
}, {})
}
export function removeDir(dir) {
if (fs.existsSync(dir)) {
const files = fs.readdirSync(dir)
files.forEach((file) => {
const curPath = path.join(dir, file)
if (fs.statSync(curPath).isDirectory()) {
removeDir(curPath)
}
else {
fs.unlinkSync(curPath)
}
})
fs.rmdirSync(dir)
}
// Convert kebab-case to PascalCase: artplayer-plugin-ads -> ArtplayerPluginAds
export function toPascalCase(name) {
return name.replace(/(^|-)([a-z])/g, (_, _p, c) => c.toUpperCase())
}
// Get Vite build config
export function getViteBuildConfig(options) {
const {
entry,
outDir,
name,
format,
fileName,
minify = 'esbuild',
target = 'es2020',
banner,
emptyOutDir = false,
} = options
const config = {
configFile: false,
logLevel: 'warn',
build: {
outDir,
emptyOutDir,
minify,
target,
lib: {
entry,
name,
formats: [format],
fileName: () => fileName,
},
rollupOptions: {
output: {
exports: 'default',
},
plugins: banner
? [{
name: 'add-banner-and-global',
generateBundle(_, bundle) {
for (const chunk of Object.values(bundle)) {
if (chunk.type === 'chunk') {
// Remove any inline worker banners (inside template literals)
// Match: `/*! ... */\n and replace with just `
let code = chunk.code.replace(/`\/\*![\s\S]*?\*\/\n/g, '`')
// Add main banner if not present
if (!code.startsWith('/*!')) {
code = `${banner}\n${code}`
}
// For UMD format, modify the wrapper to always expose global
// even when AMD loader is present (fix for RequireJS environments)
if (format === 'umd') {
// Match both original and minified UMD patterns:
// Original: typeof define === 'function' && define.amd ? define(factory) :
// Minified: "function"===typeof define&&define.amd?define(e):
// Change to: ... ? (global.Name = factory(), define(function() { return global.Name; })) :
code = code.replace(
/["']function["']\s*===?\s*typeof define\s*&&\s*define\.amd\s*\?\s*define\((\w+)\)\s*:/,
`"function"==typeof define&&define.amd?(t.${name}=$1(),define(function(){return t.${name}})):`,
)
}
chunk.code = code
}
}
},
}]
: [],
},
},
// Disable banner for inline workers
worker: {
format: 'es',
plugins: () => [{
name: 'remove-worker-banner',
generateBundle(_, bundle) {
for (const chunk of Object.values(bundle)) {
if (chunk.type === 'chunk' && chunk.code.startsWith('/*!')) {
// Remove banner from worker code
chunk.code = chunk.code.replace(/^\/\*![\s\S]*?\*\/\s*/, '')
}
}
},
}],
},
define: {
'process.env.NODE_ENV': JSON.stringify(minify ? 'production' : 'development'),
},
}
return config
}