mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-11 13:02:22 +08:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
334cf3ef37 | ||
|
|
cf20bcd76d | ||
|
|
84783abb73 |
@@ -191,7 +191,9 @@ jobs:
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
run: pnpm release-version autobuild-latest
|
||||
run: |
|
||||
pnpm release-version autobuild-latest
|
||||
pnpm polyfills:generate
|
||||
|
||||
- name: Tauri build for Windows-macOS-Linux
|
||||
uses: tauri-apps/tauri-action@v1
|
||||
@@ -271,7 +273,9 @@ jobs:
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
run: pnpm release-version autobuild-latest
|
||||
run: |
|
||||
pnpm release-version autobuild-latest
|
||||
pnpm polyfills:generate
|
||||
|
||||
- name: 'Setup for linux'
|
||||
run: |-
|
||||
@@ -404,7 +408,9 @@ jobs:
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
run: pnpm release-version autobuild-latest
|
||||
run: |
|
||||
pnpm release-version autobuild-latest
|
||||
pnpm polyfills:generate
|
||||
|
||||
- name: Download WebView2 Runtime
|
||||
run: |
|
||||
|
||||
@@ -115,7 +115,9 @@ jobs:
|
||||
|
||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
run: pnpm release-version ${{ env.TAG_NAME }}
|
||||
run: |
|
||||
pnpm release-version ${{ env.TAG_NAME }}
|
||||
pnpm polyfills:generate
|
||||
|
||||
- name: Add Rust Target
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
|
||||
@@ -58,6 +58,9 @@ jobs:
|
||||
- name: Run TypeScript typecheck
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: Verify modern polyfill inventory against automatic discovery
|
||||
run: pnpm polyfills:check
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm test
|
||||
|
||||
|
||||
@@ -101,6 +101,17 @@ For frontend-only development, use `pnpm web:dev`. It does not start the Rust ba
|
||||
|
||||
### Build the Project
|
||||
|
||||
`pnpm web:build` uses a generated modern-polyfill inventory and rejects changed
|
||||
emitted module inputs, output, targets, or locked dependencies before writing
|
||||
output. Inputs include dependency code and code later removed by minification.
|
||||
When it reports a stale inventory, run `pnpm polyfills:generate` and commit
|
||||
`scripts/modern-polyfills.json`. Generation preserves automatic discovery's import
|
||||
order and requires byte-identical automatic/explicit outputs for the macOS, Linux,
|
||||
and Windows production defines. CI repeats this comparison with
|
||||
`pnpm polyfills:check`; browser targets and worker configuration stay unchanged.
|
||||
Release build jobs regenerate after rewriting versions because version data is
|
||||
also emitted in frontend modules.
|
||||
|
||||
Standard build:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
"tauri": "tauri",
|
||||
"web:dev": "vite",
|
||||
"web:build": "tsrs -p . --noEmit && vite build",
|
||||
"polyfills:generate": "node scripts/generate-modern-polyfills.mjs --write",
|
||||
"polyfills:check": "node scripts/generate-modern-polyfills.mjs --check",
|
||||
"web:serve": "vite preview",
|
||||
"prebuild": "node scripts/prebuild.mjs",
|
||||
"updater": "node scripts/updater.mjs",
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
import { build } from 'vite'
|
||||
|
||||
const [command, ...extra] = process.argv.slice(2)
|
||||
if (!['--write', '--check'].includes(command) || extra.length) {
|
||||
throw new Error(
|
||||
'Usage: node scripts/generate-modern-polyfills.mjs --write|--check',
|
||||
)
|
||||
}
|
||||
const inventoryPath = 'scripts/modern-polyfills.json'
|
||||
const staleMessage =
|
||||
'Modern polyfill inventory is stale; run pnpm polyfills:generate and commit scripts/modern-polyfills.json'
|
||||
|
||||
function outputHashes(result) {
|
||||
assert(
|
||||
!Array.isArray(result) && 'output' in result,
|
||||
'Expected one app output',
|
||||
)
|
||||
return result.output
|
||||
.map((file) => [
|
||||
file.fileName,
|
||||
createHash('sha256')
|
||||
.update(file.type === 'chunk' ? file.code : file.source)
|
||||
.digest('hex'),
|
||||
])
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
}
|
||||
|
||||
const temporary = mkdtempSync(path.join(os.tmpdir(), 'modern-polyfills-'))
|
||||
const capture = path.join(temporary, 'capture.json')
|
||||
const candidate = path.join(temporary, 'inventory.json')
|
||||
const snapshots = []
|
||||
const fingerprints = []
|
||||
let imports
|
||||
try {
|
||||
process.env.MODERN_POLYFILLS_CAPTURE = capture
|
||||
// OS_PLATFORM changes emitted code; certify each production define without
|
||||
// launching a native app or normalizing away code the detector actually sees.
|
||||
for (const platform of ['darwin', 'linux', 'win32']) {
|
||||
const config = {
|
||||
logLevel: 'error',
|
||||
define: { OS_PLATFORM: JSON.stringify(platform) },
|
||||
build: { write: false },
|
||||
}
|
||||
const automatic = await build(config)
|
||||
const detected = JSON.parse(readFileSync(capture, 'utf8'))
|
||||
if (imports) assert.deepEqual(detected.imports, imports)
|
||||
imports = detected.imports
|
||||
fingerprints.push(detected.fingerprint)
|
||||
snapshots.push({ config, hashes: outputHashes(automatic) })
|
||||
}
|
||||
const content = `${JSON.stringify({ imports, fingerprints: [...new Set(fingerprints)].sort() }, null, 2)}\n`
|
||||
writeFileSync(candidate, content)
|
||||
delete process.env.MODERN_POLYFILLS_CAPTURE
|
||||
process.env.MODERN_POLYFILLS_INVENTORY = candidate
|
||||
for (const { config, hashes } of snapshots) {
|
||||
assert.deepEqual(
|
||||
outputHashes(await build(config)),
|
||||
hashes,
|
||||
'Explicit polyfills must be byte-identical to automatic discovery',
|
||||
)
|
||||
}
|
||||
if (command === '--check') {
|
||||
assert.equal(readFileSync(inventoryPath, 'utf8'), content, staleMessage)
|
||||
} else {
|
||||
writeFileSync(inventoryPath, content)
|
||||
}
|
||||
console.log(
|
||||
`Modern polyfills ${command === '--check' ? 'verified' : 'generated'}: ${imports.length} imports; all three OS outputs byte-identical to automatic discovery`,
|
||||
)
|
||||
} finally {
|
||||
delete process.env.MODERN_POLYFILLS_CAPTURE
|
||||
delete process.env.MODERN_POLYFILLS_INVENTORY
|
||||
rmSync(temporary, { recursive: true })
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"imports": [
|
||||
"core-js/modules/es.error.cause.js",
|
||||
"core-js/modules/es.array.at.js",
|
||||
"core-js/modules/es.array.includes.js",
|
||||
"core-js/modules/es.array.push.js",
|
||||
"core-js/modules/es.array.unshift.js",
|
||||
"core-js/modules/es.array-buffer.detached.js",
|
||||
"core-js/modules/es.array-buffer.transfer.js",
|
||||
"core-js/modules/es.array-buffer.transfer-to-fixed-length.js",
|
||||
"core-js/modules/es.iterator.constructor.js",
|
||||
"core-js/modules/es.iterator.every.js",
|
||||
"core-js/modules/es.iterator.filter.js",
|
||||
"core-js/modules/es.iterator.find.js",
|
||||
"core-js/modules/es.iterator.flat-map.js",
|
||||
"core-js/modules/es.iterator.for-each.js",
|
||||
"core-js/modules/es.iterator.map.js",
|
||||
"core-js/modules/es.iterator.reduce.js",
|
||||
"core-js/modules/es.iterator.some.js",
|
||||
"core-js/modules/es.iterator.to-array.js",
|
||||
"core-js/modules/es.json.parse.js",
|
||||
"core-js/modules/es.json.stringify.js",
|
||||
"core-js/modules/es.map.get-or-insert.js",
|
||||
"core-js/modules/es.map.get-or-insert-computed.js",
|
||||
"core-js/modules/es.regexp.flags.js",
|
||||
"core-js/modules/es.set.difference.v2.js",
|
||||
"core-js/modules/es.set.intersection.v2.js",
|
||||
"core-js/modules/es.set.is-disjoint-from.v2.js",
|
||||
"core-js/modules/es.set.is-subset-of.v2.js",
|
||||
"core-js/modules/es.set.is-superset-of.v2.js",
|
||||
"core-js/modules/es.set.symmetric-difference.v2.js",
|
||||
"core-js/modules/es.set.union.v2.js",
|
||||
"core-js/modules/es.string.at-alternative.js",
|
||||
"core-js/modules/es.typed-array.at.js",
|
||||
"core-js/modules/es.typed-array.fill.js",
|
||||
"core-js/modules/es.typed-array.find-last.js",
|
||||
"core-js/modules/es.typed-array.find-last-index.js",
|
||||
"core-js/modules/es.typed-array.set.js",
|
||||
"core-js/modules/es.typed-array.sort.js",
|
||||
"core-js/modules/es.typed-array.to-reversed.js",
|
||||
"core-js/modules/es.typed-array.to-sorted.js",
|
||||
"core-js/modules/es.typed-array.with.js",
|
||||
"core-js/modules/es.uint8-array.set-from-base64.js",
|
||||
"core-js/modules/es.uint8-array.set-from-hex.js",
|
||||
"core-js/modules/es.uint8-array.to-base64.js",
|
||||
"core-js/modules/es.uint8-array.to-hex.js",
|
||||
"core-js/modules/es.weak-map.get-or-insert.js",
|
||||
"core-js/modules/es.weak-map.get-or-insert-computed.js",
|
||||
"core-js/modules/esnext.array.group.js",
|
||||
"core-js/modules/web.dom-exception.stack.js",
|
||||
"core-js/modules/web.immediate.js",
|
||||
"core-js/modules/web.url-search-params.delete.js",
|
||||
"core-js/modules/web.url-search-params.has.js",
|
||||
"core-js/modules/web.url-search-params.size.js",
|
||||
"core-js/modules/es.object.has-own.js",
|
||||
"core-js/modules/es.array.with.js",
|
||||
"core-js/modules/es.symbol.async-dispose.js",
|
||||
"core-js/modules/es.async-iterator.async-dispose.js",
|
||||
"core-js/modules/es.symbol.dispose.js",
|
||||
"core-js/modules/es.iterator.dispose.js",
|
||||
"core-js/modules/es.aggregate-error.cause.js",
|
||||
"core-js/modules/es.array.find-last.js",
|
||||
"core-js/modules/es.iterator.drop.js",
|
||||
"core-js/modules/es.iterator.take.js",
|
||||
"core-js/modules/web.structured-clone.js"
|
||||
],
|
||||
"fingerprints": [
|
||||
"30dd6a7f16e7065fa9803260e11aa2f2db56cabb69664257c940ab05bae8505b",
|
||||
"5630fe8a486687073b6d9894dd39735b77d5d8177432a4ada81947a429ff6991",
|
||||
"ddbb980f9ad3fe92ee7c30248d46ed4cdeb421c714266e627b825b59042c30bc"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import legacy, { detectPolyfills } from '@vitejs/plugin-legacy'
|
||||
|
||||
const digest = (value) => createHash('sha256').update(value).digest('hex')
|
||||
const inventoryPath = 'scripts/modern-polyfills.json'
|
||||
const staleMessage =
|
||||
'Modern polyfill inventory is stale; run pnpm polyfills:generate and commit scripts/modern-polyfills.json'
|
||||
|
||||
// Bind complete emitted module inputs as well as output: minification can hide APIs.
|
||||
// Generation double-checks the exported detector against legacy's automatic build.
|
||||
export function guardedLegacy(options) {
|
||||
const capturePath = process.env.MODERN_POLYFILLS_CAPTURE
|
||||
const inventory = capturePath
|
||||
? null
|
||||
: JSON.parse(
|
||||
readFileSync(
|
||||
process.env.MODERN_POLYFILLS_INVENTORY || inventoryPath,
|
||||
'utf8',
|
||||
),
|
||||
)
|
||||
const plugins = legacy({
|
||||
...options,
|
||||
modernPolyfills: Boolean(capturePath),
|
||||
additionalModernPolyfills: [
|
||||
...options.additionalModernPolyfills,
|
||||
...(capturePath ? [] : inventory.imports),
|
||||
],
|
||||
})
|
||||
const postProcess = plugins.find(
|
||||
(plugin) => plugin.name === 'vite:legacy-post-process',
|
||||
)
|
||||
if (!postProcess?.renderChunk) {
|
||||
throw new Error('plugin-legacy must provide its post-process render hook')
|
||||
}
|
||||
const original = postProcess.renderChunk
|
||||
const render = typeof original === 'function' ? original : original.handler
|
||||
const discovered = new Map()
|
||||
let chunkNames
|
||||
let context
|
||||
if (capturePath) {
|
||||
postProcess.renderChunk = async function (raw, chunk, output, meta) {
|
||||
chunkNames ??= Object.keys(meta.chunks)
|
||||
const imports = new Set()
|
||||
await detectPolyfills(
|
||||
raw,
|
||||
options.modernTargets,
|
||||
options.assumptions || {},
|
||||
imports,
|
||||
)
|
||||
discovered.set(chunk.fileName, imports)
|
||||
return render.call(this, raw, chunk, output, meta)
|
||||
}
|
||||
}
|
||||
return [
|
||||
...plugins,
|
||||
{
|
||||
name: 'guard-modern-polyfills',
|
||||
enforce: 'post',
|
||||
apply: 'build',
|
||||
configResolved(config) {
|
||||
if (config.build.ssr || options.renderLegacyChunks !== false) {
|
||||
throw new Error(
|
||||
'Modern polyfill guard requires a modern-only app build',
|
||||
)
|
||||
}
|
||||
// Custom import paths must not bind the inventory to a checkout location.
|
||||
context = [
|
||||
{
|
||||
...options,
|
||||
additionalModernPolyfills: options.additionalModernPolyfills.map(
|
||||
(file) =>
|
||||
path.relative(process.cwd(), file).split(path.sep).join('/'),
|
||||
),
|
||||
},
|
||||
config.build.target,
|
||||
config.mode,
|
||||
...[
|
||||
'package.json',
|
||||
'pnpm-lock.yaml',
|
||||
'vite.config.mts',
|
||||
'scripts/modern-polyfills.mjs',
|
||||
'scripts/generate-modern-polyfills.mjs',
|
||||
].map((file) => digest(readFileSync(file))),
|
||||
...[
|
||||
'.browserslistrc',
|
||||
'browserslist',
|
||||
'src/.browserslistrc',
|
||||
'src/browserslist',
|
||||
].map((file) =>
|
||||
existsSync(file) ? digest(readFileSync(file)) : null,
|
||||
),
|
||||
digest(
|
||||
readFileSync(new URL(import.meta.resolve('@vitejs/plugin-legacy'))),
|
||||
),
|
||||
]
|
||||
},
|
||||
generateBundle(_output, bundle) {
|
||||
const imports = capturePath
|
||||
? [
|
||||
...new Set(
|
||||
chunkNames.flatMap((name) => [...discovered.get(name)]),
|
||||
),
|
||||
]
|
||||
: inventory.imports
|
||||
const chunks = Object.values(bundle).filter(
|
||||
(file) => file.type === 'chunk',
|
||||
)
|
||||
const moduleIds = new Set(
|
||||
chunks.flatMap((chunk) => Object.keys(chunk.modules)),
|
||||
)
|
||||
const inputs = [...moduleIds].map((id) => {
|
||||
// Rolldown's runtime is generated, not a transformed input module.
|
||||
if (id === '\0rolldown/runtime.js') return null
|
||||
// Vite's HTML entry code embeds an absolute import to the main module.
|
||||
if (id.endsWith('.html')) return digest(readFileSync(id))
|
||||
const code = this.getModuleInfo(id)?.code
|
||||
if (code == null) this.error(`Missing emitted module input: ${id}`)
|
||||
return digest(code)
|
||||
})
|
||||
const fingerprint = digest(
|
||||
JSON.stringify([
|
||||
context,
|
||||
inputs.sort(),
|
||||
chunks.map((chunk) => digest(chunk.code)).sort(),
|
||||
imports,
|
||||
]),
|
||||
)
|
||||
if (capturePath) {
|
||||
writeFileSync(capturePath, JSON.stringify({ fingerprint, imports }))
|
||||
} else if (!inventory.fingerprints.includes(fingerprint)) {
|
||||
this.error(staleMessage)
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
+13
-6
@@ -1,22 +1,29 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import legacy from '@vitejs/plugin-legacy'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig } from 'vite'
|
||||
import svgr from 'vite-plugin-svgr'
|
||||
|
||||
import { guardedLegacy } from './scripts/modern-polyfills.mjs'
|
||||
|
||||
const svgPlugin = svgr()
|
||||
const svgLoad = svgPlugin.load
|
||||
if (!svgLoad) throw new Error('vite-plugin-svgr must provide a load hook')
|
||||
// SVGR's internal include filter still crosses the JS bridge for every module.
|
||||
svgPlugin.load = {
|
||||
filter: { id: /\.svg\?react$/ },
|
||||
handler: typeof svgLoad === 'function' ? svgLoad : svgLoad.handler,
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
root: 'src',
|
||||
server: { port: 3000 },
|
||||
plugins: [
|
||||
svgr(),
|
||||
svgPlugin,
|
||||
react(),
|
||||
legacy({
|
||||
guardedLegacy({
|
||||
modernTargets: ['edge>=109', 'safari>=14'],
|
||||
renderLegacyChunks: false,
|
||||
// Auto-detected from the emitted bundle, dependencies included,
|
||||
// so post-Safari-14 APIs never depend on extending a manual list.
|
||||
modernPolyfills: true,
|
||||
additionalModernPolyfills: [
|
||||
path.resolve('./src/polyfills/matchMedia.js'),
|
||||
path.resolve('./src/polyfills/WeakRef.js'),
|
||||
|
||||
Reference in New Issue
Block a user