diff --git a/source-archive.ts b/source-archive.ts deleted file mode 100644 index 0ef6d4b..0000000 --- a/source-archive.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 Coffey Labs - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* - * The AGPL's offer, for this build: the exact source it was built from, - * written next to the app as `source.tar.gz`, and an identity for it that the - * interface shows with the download link. - * - * "Exact" includes uncommitted work, new files too: every file git doesn't - * ignore is written into a throwaway index, never the real one, and the tree - * that makes is what gets archived. A clean tree is HEAD's tree. Outside a git - * checkout (a release tarball, say), the project files are packed as they are. - */ -import { execFileSync } from 'node:child_process'; -import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import path from 'node:path'; -import type { Plugin } from 'vite'; - -const EXCLUDE = ['node_modules', 'dist', '.git', '.ignore', 'coverage']; - -function git(args: string[], cwd: string, env?: NodeJS.ProcessEnv): string { - return execFileSync('git', args, { cwd, env: env ?? process.env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); -} - -/** The tree this build is made from: its git tree id, and a name that says whether it has uncommitted work. */ -export function sourceIdentity(root: string): { ref: string | null; id: string } { - try { - git(['rev-parse', '--is-inside-work-tree'], root); - } catch { - return { ref: null, id: 'unversioned' }; - } - const dir = mkdtempSync(path.join(tmpdir(), 'inbuxa-source-')); - try { - const env = { ...process.env, GIT_INDEX_FILE: path.join(dir, 'index') }; - git(['read-tree', 'HEAD'], root, env); - git(['add', '--all', '.'], root, env); - const tree = git(['write-tree'], root, env); - const headTree = git(['rev-parse', 'HEAD^{tree}'], root); - const head = git(['rev-parse', '--short=12', 'HEAD'], root); - return tree === headTree ? { ref: tree, id: head } : { ref: tree, id: `${head}+local-${tree.slice(0, 12)}` }; - } finally { - rmSync(dir, { recursive: true, force: true }); - } -} - -export function sourceArchive(root: string, name: string, identity: { ref: string | null; id: string }): Plugin { - return { - name: 'inbuxa-source-archive', - apply: 'build', - closeBundle() { - const outDir = path.join(root, 'dist'); - if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true }); - const out = path.join(outDir, 'source.tar.gz'); - const prefix = `${name}-${identity.id}/`; - if (identity.ref) { - execFileSync('git', ['archive', '--format=tar.gz', `--prefix=${prefix}`, '-o', out, identity.ref], { cwd: root }); - } else { - execFileSync( - 'tar', - [...EXCLUDE.map((e) => `--exclude=./${e}`), `--transform=s,^\\.,${prefix.slice(0, -1)},`, '-czf', out, '.'], - { cwd: root }, - ); - } - }, - }; -} diff --git a/src/components/common/SourceLink.tsx b/src/components/common/SourceLink.tsx index cdb79e4..5344a10 100644 --- a/src/components/common/SourceLink.tsx +++ b/src/components/common/SourceLink.tsx @@ -5,17 +5,17 @@ */ import { useTranslation } from 'react-i18next'; -import { sourceDownloadUrl } from '@/lib/sourceDownload'; +import { SOURCE_URL } from '@/lib/sourceDownload'; /** - * The AGPL's offer to everyone using this interface over the network: the - * exact source of the version running, with that version named. + * The AGPL's offer to everyone using this interface over the network: where + * the source is, with the running version named beside it. */ export function SourceLink({ className }: { className?: string }) { const { t } = useTranslation(); return ( - - {t('source.download', 'Source code of this version ({{id}}), AGPL-3.0', { id: __SOURCE_ID__ })} + + {t('source.download', 'Source code ({{version}}), AGPL-3.0', { version: __APP_VERSION__ })} ); } diff --git a/src/components/layout/TopBar.tsx b/src/components/layout/TopBar.tsx index 25ecf9f..fc091c3 100644 --- a/src/components/layout/TopBar.tsx +++ b/src/components/layout/TopBar.tsx @@ -31,7 +31,7 @@ import { isPaletteId, PALETTES } from '@/lib/palettes'; import Logo from '@/components/common/Logo'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { EnterpriseUpsell } from '@/components/common/EnterpriseUpsell'; -import { sourceDownloadUrl } from '@/lib/sourceDownload'; +import { SOURCE_URL } from '@/lib/sourceDownload'; import { visibleLayouts } from '@/lib/layout'; import { sectionLandingLink } from '@/lib/lastVisited'; import { cn } from '@/lib/utils'; @@ -104,7 +104,7 @@ export function TopBar() { - {t('version.label', 'INBUXA Admin {{version}}', { version: __APP_VERSION__ })} ยท {__SOURCE_ID__} + {t('version.label', 'INBUXA Admin {{version}}', { version: __APP_VERSION__ })} @@ -286,7 +286,7 @@ export function TopBar() { - + {t('source.menu', 'Source code (AGPL-3.0)')} diff --git a/src/lib/sourceDownload.ts b/src/lib/sourceDownload.ts index 93ad322..7a3a33e 100644 --- a/src/lib/sourceDownload.ts +++ b/src/lib/sourceDownload.ts @@ -4,9 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { getBasePath } from '@/lib/basePath'; - -/** Where this build's own source is: written next to the app at build time (see source-archive.ts). */ -export function sourceDownloadUrl(): string { - return `${getBasePath()}/source.tar.gz`; -} +/** + * Where to point someone who wants this interface's source. + * + * INBUXA Admin is a modified Stalwart web interface, so the AGPL's offer is + * this fork. The version shown beside the link names the build, which is what + * makes the offer something a person can act on. + */ +export const SOURCE_URL = 'https://github.com/inbuxa/inbuxa-admin'; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 490be7b..a9576d6 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -22,4 +22,3 @@ interface ImportMeta { } declare const __APP_VERSION__: string; -declare const __SOURCE_ID__: string; diff --git a/vite.config.ts b/vite.config.ts index a111d13..c90ea9e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,18 +9,13 @@ import { version } from './package.json' with { type: 'json' } // INBUXA's own dated version lives apart from package.json, whose version // follows upstream WebUI so its bumps merge without conflicts. import inbuxa from './inbuxa-version.json' with { type: 'json' } -import { sourceArchive, sourceIdentity } from './source-archive' - -const source = sourceIdentity(import.meta.dirname) export default defineConfig({ base: './', define: { __APP_VERSION__: JSON.stringify(`${inbuxa.version} (base ${version})`), - // The tree this build came from; source.tar.gz next to the app holds it. - __SOURCE_ID__: JSON.stringify(source.id), }, - plugins: [react(), tailwindcss(), sourceArchive(import.meta.dirname, 'inbuxa-admin', source)], + plugins: [react(), tailwindcss()], resolve: { alias: { '@': path.resolve(import.meta.dirname, './src'),