Files
dsh-web/scripts/sync-shared.mjs
T

236 lines
8.7 KiB
JavaScript

#!/usr/bin/env node
'use strict'
/**
* sync-shared — mirror shared/client/settings and shared/host (the
* family-shared modules) into the consuming plugin packages.
*
* The trio lives exactly once under shared/client/settings and is copied,
* with a generated-file header, to each consumer's src/client/. The copies
* are committed (same policy as the dsh-skins built assets) so packages stay
* self-contained for typecheck, test, and publish. The --check mode fails on
* any drift and runs in CI through test:scripts.
*
* Usage:
* node scripts/sync-shared.mjs # rewrite every consumer copy
* node scripts/sync-shared.mjs --check # fail when any copy drifts
*/
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
export const REPO_ROOT = resolve(SCRIPT_DIR, '..')
/**
* The sync manifest: every shared-module copy this repo carries. Sources
* live exactly once under shared/; consumers import the committed copy.
*/
// Consumers of the settings card trio: one list, three derivations below.
const SETTINGS_CONSUMERS = ['dsh-pet', 'dsh-task-board', 'dsh-remote-web-ui', 'dsh-aionui-panel', 'dsh-tool-describe-image', 'dsh-desktop-launcher', 'dsh-doctor', 'dsh-market']
const MANIFEST = [
{
file: 'settings-form.ts',
source: 'shared/client/settings/settings-form.ts',
targets: SETTINGS_CONSUMERS.map(pkg => `packages/${pkg}/src/client/settings-form.ts`),
},
{
file: 'PluginSettingsCard.tsx',
source: 'shared/client/settings/PluginSettingsCard.tsx',
targets: SETTINGS_CONSUMERS.map(pkg => `packages/${pkg}/src/client/PluginSettingsCard.tsx`),
},
{
file: 'settings-card.module.css',
source: 'shared/client/settings/settings-card.module.css',
targets: SETTINGS_CONSUMERS.map(pkg => `packages/${pkg}/src/client/settings-card.module.css`),
},
{
file: 'poll-guard.ts',
source: 'shared/host/poll-guard.ts',
targets: [
'packages/dsh-git-graph/src/host/poll-guard.ts',
],
},
{
file: 'dsh-home.ts',
source: 'shared/host/dsh-home.ts',
targets: [
'packages/dsh-pet/src/dsh-home.ts',
'packages/dsh-liangshen/src/dsh-home.ts',
'packages/dsh-task-board/src/dsh-home.ts',
'packages/dsh-plugin-manager/src/host/dsh-home.ts',
'packages/dsh-remote-web-ui/src/dsh-home.ts',
'packages/dsh-ssh/src/dsh-home.ts',
'packages/dsh-desktop-launcher/src/dsh-home.ts',
'packages/dsh-web-ui-settings/src/dsh-home.ts',
'packages/dsh-market/src/dsh-home.ts',
],
},
{
file: 'git-runner.ts',
source: 'shared/host/git-runner.ts',
targets: ['packages/dsh-git-graph/src/host/git-runner.ts'],
},
{
file: 'mount-once.ts',
source: 'shared/host/mount-once.ts',
targets: [
'packages/dsh-pet/src/mount-once.ts',
'packages/dsh-ssh/src/mount-once.ts',
'packages/dsh-remote-web-ui/src/mount-once.ts',
'packages/dsh-liangshen/src/mount-once.ts',
'packages/dsh-task-board/src/mount-once.ts',
'packages/dsh-git-graph/src/mount-once.ts',
'packages/dsh-aionui-panel/src/mount-once.ts',
'packages/dsh-community-plugins/src/mount-once.ts',
'packages/dsh-plugin-manager/src/mount-once.ts',
'packages/dsh-web-ui-settings/src/mount-once.ts',
'packages/dsh-tool-describe-image/src/mount-once.ts',
'packages/dsh-desktop-launcher/src/mount-once.ts',
'packages/dsh-skill-explorer/src/mount-once.ts',
'packages/dsh-doctor/src/mount-once.ts',
'packages/skins/skin-center/src/mount-once.ts',
'packages/dsh-market/src/mount-once.ts',
],
},
{
file: 'telemetry.ts',
source: 'shared/client/telemetry.ts',
targets: [
'packages/dsh-market/src/client/telemetry.ts',
'packages/dsh-pet/src/client/telemetry.ts',
'packages/skins/skin-center/src/client/telemetry.ts',
],
},
{
file: 'sse-leader.ts',
source: 'shared/client/sse-leader.ts',
targets: ['packages/dsh-git-graph/src/client/sse-leader.ts'],
},
{
file: 'loopback.ts',
source: 'shared/host/loopback.ts',
targets: ['packages/dsh-ssh/src/loopback.ts', 'packages/dsh-git-graph/src/host/loopback.ts', 'packages/dsh-remote-web-ui/src/loopback.ts', 'packages/dsh-task-board/src/loopback.ts', 'packages/dsh-skill-explorer/src/loopback.ts', 'packages/dsh-pet/src/loopback.ts', 'packages/dsh-plugin-manager/src/host/loopback.ts', 'packages/dsh-tool-describe-image/src/loopback.ts', 'packages/dsh-desktop-launcher/src/loopback.ts', 'packages/dsh-doctor/src/host/loopback.ts', 'packages/dsh-market/src/loopback.ts'],
},
{
file: 'http.ts',
source: 'shared/host/http.ts',
targets: [
'packages/dsh-pet/src/http.ts',
'packages/dsh-market/src/http.ts',
'packages/dsh-skill-explorer/src/http.ts',
'packages/dsh-desktop-launcher/src/http.ts',
'packages/dsh-web-ui-settings/src/http.ts',
'packages/dsh-tool-describe-image/src/http.ts',
'packages/dsh-doctor/src/host/http.ts',
'packages/dsh-git-graph/src/host/http.ts',
'packages/dsh-ssh/src/http.ts',
'packages/dsh-plugin-manager/src/host/http.ts',
'packages/skins/skin-center/src/http.ts',
'packages/dsh-remote-web-ui/src/http.ts',
'packages/dsh-task-board/src/http.ts',
],
},
{
file: 'sidebar-entry-core.ts',
source: 'shared/client/sidebar-entry-core.ts',
targets: [
'packages/dsh-ssh/src/client/sidebar-entry-core.ts',
'packages/dsh-task-board/src/client/sidebar-entry-core.ts',
'packages/dsh-skill-explorer/src/client/sidebar-entry-core.ts',
],
},
]
/** One source-to-consumer copy step. */
export function copyEntries(root = REPO_ROOT) {
return MANIFEST.flatMap(entry => entry.targets.map(target => ({
source: join(root, entry.source),
target: join(root, target),
file: entry.file,
sourceRel: entry.source,
})))
}
/** Generated-file header prepended to every copy. */
export function headerFor(file, sourceRel = `shared/client/settings/${file}`) {
const body = `Generated by scripts/sync-shared.mjs from ${sourceRel}. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".`
return file.endsWith('.css') ? `/* ${body} */` : `// ${body}`
}
/** Render a consumer copy from its shared source. */
export function renderCopy(sourceText, file, sourceRel) {
return `${headerFor(file, sourceRel)}
${sourceText}`
}
/** Recover the shared source text from a rendered copy (undefined when the header is missing or mangled). */
export function stripHeader(rendered, file, sourceRel) {
const header = headerFor(file, sourceRel)
if (!rendered.startsWith(header)) return undefined
return rendered.slice(header.length + 1)
}
/** Apply every copy step. Returns the number of rewritten files. */
export async function applySync(root = REPO_ROOT) {
let written = 0
for (const { source, target, file, sourceRel } of copyEntries(root)) {
const sourceText = await readFile(source, 'utf8')
const rendered = renderCopy(sourceText, file, sourceRel)
await mkdir(dirname(target), { recursive: true })
await writeFile(target, rendered)
written += 1
}
return written
}
/** Compare every copy against its source. Returns the drifted entries. */
export async function checkSync(root = REPO_ROOT) {
const drift = []
for (const { source, target, file, sourceRel } of copyEntries(root)) {
const sourceText = await readFile(source, 'utf8')
let rendered
try {
rendered = await readFile(target, 'utf8')
} catch {
drift.push({ target, reason: 'missing' })
continue
}
const stripped = stripHeader(rendered, file, sourceRel)
if (stripped === undefined) {
drift.push({ target, reason: 'header missing or mangled' })
} else if (stripped !== sourceText) {
drift.push({ target, reason: 'content drifted from shared source' })
}
}
return drift
}
async function main() {
const mode = process.argv[2]
if (mode === '--check') {
const drift = await checkSync()
if (drift.length > 0) {
for (const entry of drift) {
console.error(`[sync-shared] drift: ${entry.target} (${entry.reason})`)
}
console.error(`[sync-shared] ${drift.length} consumer copy(ies) out of sync; run "node scripts/sync-shared.mjs"`)
process.exit(1)
}
console.log('[sync-shared] all consumer copies in sync')
return
}
if (mode !== undefined) {
console.error('usage: node scripts/sync-shared.mjs [--check]')
process.exit(2)
}
const written = await applySync()
console.log(`[sync-shared] wrote ${written} consumer copies`)
}
if (process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
await main()
}