feat: integrate PDF Tools with toolbox portal
This commit is contained in:
64
scripts/generate-toolbox-manifest.mjs
Normal file
64
scripts/generate-toolbox-manifest.mjs
Normal file
@@ -0,0 +1,64 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parseToolboxApp } from '@add-ideas/toolbox-contract';
|
||||
import { format } from 'prettier';
|
||||
|
||||
const projectRoot = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'..'
|
||||
);
|
||||
const packageJsonPath = path.join(projectRoot, 'package.json');
|
||||
const definitionPath = path.join(
|
||||
projectRoot,
|
||||
'src',
|
||||
'toolboxApp.definition.json'
|
||||
);
|
||||
const versionSourcePath = path.join(projectRoot, 'src', 'version.ts');
|
||||
const manifestPath = path.join(projectRoot, 'public', 'toolbox-app.json');
|
||||
|
||||
const [packageJson, definition, versionSource] = await Promise.all([
|
||||
readJson(packageJsonPath),
|
||||
readJson(definitionPath),
|
||||
readFile(versionSourcePath, 'utf8'),
|
||||
]);
|
||||
|
||||
const sourceVersion = versionSource.match(
|
||||
/APP_VERSION\s*=\s*['"]([^'"]+)['"]/
|
||||
)?.[1];
|
||||
|
||||
if (!sourceVersion) {
|
||||
throw new Error('Could not read APP_VERSION from src/version.ts.');
|
||||
}
|
||||
|
||||
if (sourceVersion !== packageJson.version) {
|
||||
throw new Error(
|
||||
`Version mismatch: package.json is ${packageJson.version}, while src/version.ts is ${sourceVersion}.`
|
||||
);
|
||||
}
|
||||
|
||||
const manifest = {
|
||||
$schema:
|
||||
'https://git.add-ideas.de/zemion/toolbox-sdk/raw/branch/main/schemas/toolbox-app.v1.schema.json',
|
||||
schemaVersion: 1,
|
||||
...definition,
|
||||
version: packageJson.version,
|
||||
};
|
||||
parseToolboxApp(manifest);
|
||||
const output = await format(JSON.stringify(manifest), { parser: 'json' });
|
||||
|
||||
if (process.argv.includes('--check')) {
|
||||
const existing = await readFile(manifestPath, 'utf8').catch(() => '');
|
||||
if (existing !== output) {
|
||||
throw new Error(
|
||||
'public/toolbox-app.json is out of date. Run npm run manifest:generate.'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await writeFile(manifestPath, output);
|
||||
}
|
||||
|
||||
async function readJson(filePath) {
|
||||
return JSON.parse(await readFile(filePath, 'utf8'));
|
||||
}
|
||||
100
scripts/package-release.mjs
Normal file
100
scripts/package-release.mjs
Normal file
@@ -0,0 +1,100 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
collectDirectoryFiles,
|
||||
createDeterministicZip,
|
||||
createThirdPartyLicenseBundle,
|
||||
} from './release-archive.mjs';
|
||||
|
||||
const projectRoot = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'..'
|
||||
);
|
||||
const packageJson = JSON.parse(
|
||||
await readFile(path.join(projectRoot, 'package.json'), 'utf8')
|
||||
);
|
||||
const releaseDirectory = path.join(projectRoot, 'release');
|
||||
const archiveName = `pdf-tools-${packageJson.version}.zip`;
|
||||
const archivePath = path.join(releaseDirectory, archiveName);
|
||||
const checksumPath = path.join(
|
||||
releaseDirectory,
|
||||
`pdf-tools-${packageJson.version}.sha256`
|
||||
);
|
||||
const force = process.argv.slice(2).includes('--force');
|
||||
const unknownArguments = process.argv
|
||||
.slice(2)
|
||||
.filter((argument) => argument !== '--force');
|
||||
if (unknownArguments.length > 0) {
|
||||
throw new Error(`Unknown release argument: ${unknownArguments.join(', ')}`);
|
||||
}
|
||||
|
||||
await mkdir(releaseDirectory, { recursive: true });
|
||||
if (!force) {
|
||||
await assertDoesNotExist(archivePath);
|
||||
await assertDoesNotExist(checksumPath);
|
||||
}
|
||||
|
||||
const files = await collectDirectoryFiles(path.join(projectRoot, 'dist'));
|
||||
|
||||
files['CHANGELOG.md'] = await readFile(path.join(projectRoot, 'CHANGELOG.md'));
|
||||
files['SOURCE.md'] = Buffer.from(
|
||||
[
|
||||
'# Corresponding source',
|
||||
'',
|
||||
`The source code corresponding to PDF Tools ${packageJson.version} is available at:`,
|
||||
'',
|
||||
`https://git.add-ideas.de/zemion/pdf-tools/src/tag/v${packageJson.version}`,
|
||||
'',
|
||||
'Third-party components remain available under the separate terms reproduced in LICENSES/.',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf8'
|
||||
);
|
||||
files['LICENSES/pdf-tools-LICENSE.txt'] = await readFile(
|
||||
path.join(projectRoot, 'LICENSE')
|
||||
);
|
||||
files['LICENSES/core-js-3.49.0-LICENSE.txt'] = await readFile(
|
||||
path.join(projectRoot, 'licenses', 'core-js-3.49.0-LICENSE.txt')
|
||||
);
|
||||
files['LICENSES/THIRD-PARTY-LICENSES.txt'] =
|
||||
await createThirdPartyLicenseBundle(projectRoot, [
|
||||
'@add-ideas/toolbox-contract',
|
||||
'@add-ideas/toolbox-shell-react',
|
||||
'@pdf-lib/standard-fonts',
|
||||
'@pdf-lib/upng',
|
||||
'fflate',
|
||||
'pako',
|
||||
'pdf-lib',
|
||||
'pdfjs-dist',
|
||||
'react',
|
||||
'react-dom',
|
||||
'scheduler',
|
||||
'tslib',
|
||||
]);
|
||||
|
||||
const archive = createDeterministicZip(files);
|
||||
const checksum = createHash('sha256').update(archive).digest('hex');
|
||||
|
||||
const writeFlag = force ? 'w' : 'wx';
|
||||
await writeFile(archivePath, archive, { flag: writeFlag });
|
||||
await writeFile(checksumPath, `${checksum} ${archiveName}\n`, {
|
||||
encoding: 'utf8',
|
||||
flag: writeFlag,
|
||||
});
|
||||
|
||||
console.log(`Created release/${archiveName}`);
|
||||
console.log(`SHA-256 ${checksum}`);
|
||||
|
||||
async function assertDoesNotExist(outputPath) {
|
||||
try {
|
||||
await access(outputPath);
|
||||
} catch (error) {
|
||||
if (error && typeof error === 'object' && error.code === 'ENOENT') return;
|
||||
throw error;
|
||||
}
|
||||
throw new Error(
|
||||
`${path.relative(projectRoot, outputPath)} already exists; pass --force to replace this exact version.`
|
||||
);
|
||||
}
|
||||
12
scripts/release-archive.d.ts
vendored
Normal file
12
scripts/release-archive.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
export function compareCodePoints(left: string, right: string): number;
|
||||
export function createDeterministicZip(
|
||||
files: Record<string, Uint8Array>
|
||||
): Uint8Array;
|
||||
export function collectDirectoryFiles(
|
||||
directory: string,
|
||||
prefix?: string
|
||||
): Promise<Record<string, Buffer>>;
|
||||
export function createThirdPartyLicenseBundle(
|
||||
projectRoot: string,
|
||||
packageNames: string[]
|
||||
): Promise<Buffer>;
|
||||
123
scripts/release-archive.mjs
Normal file
123
scripts/release-archive.mjs
Normal file
@@ -0,0 +1,123 @@
|
||||
import { readFile, readdir } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { zipSync } from 'fflate';
|
||||
|
||||
const LICENSE_FILE = /^(?:licen[cs]e|copying|notice)(?:[._-].*)?$/iu;
|
||||
|
||||
export function compareCodePoints(left, right) {
|
||||
return left < right ? -1 : left > right ? 1 : 0;
|
||||
}
|
||||
|
||||
export function createDeterministicZip(files) {
|
||||
const orderedFiles = Object.fromEntries(
|
||||
Object.entries(files).sort(([left], [right]) =>
|
||||
compareCodePoints(left, right)
|
||||
)
|
||||
);
|
||||
return zipSync(orderedFiles, {
|
||||
level: 9,
|
||||
// ZIP stores a wall-clock DOS timestamp. Constructing the epoch in local
|
||||
// time keeps its encoded bytes stable in every process timezone.
|
||||
mtime: new Date(1980, 0, 1, 0, 0, 0),
|
||||
});
|
||||
}
|
||||
|
||||
export async function collectDirectoryFiles(directory, prefix = '') {
|
||||
const result = {};
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
entries.sort((left, right) => compareCodePoints(left.name, right.name));
|
||||
|
||||
for (const entry of entries) {
|
||||
const absolutePath = path.join(directory, entry.name);
|
||||
const relativePath = path.posix.join(prefix, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
Object.assign(
|
||||
result,
|
||||
await collectDirectoryFiles(absolutePath, relativePath)
|
||||
);
|
||||
} else if (entry.isFile()) {
|
||||
result[relativePath] = await readFile(absolutePath);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unsupported filesystem entry in release input: ${relativePath}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function createThirdPartyLicenseBundle(projectRoot, packageNames) {
|
||||
const sections = [];
|
||||
|
||||
for (const packageName of [...new Set(packageNames)].sort(
|
||||
compareCodePoints
|
||||
)) {
|
||||
const packageDirectory = path.join(
|
||||
projectRoot,
|
||||
'node_modules',
|
||||
...packageName.split('/')
|
||||
);
|
||||
const packageJson = JSON.parse(
|
||||
await readFile(path.join(packageDirectory, 'package.json'), 'utf8')
|
||||
);
|
||||
const licenseFiles = await collectLicenseFiles(packageDirectory);
|
||||
if (licenseFiles.length === 0) {
|
||||
throw new Error(`No license text found for ${packageName}.`);
|
||||
}
|
||||
|
||||
const contents = [];
|
||||
for (const licenseFile of licenseFiles) {
|
||||
const text = await readFile(
|
||||
path.join(packageDirectory, ...licenseFile.split('/')),
|
||||
'utf8'
|
||||
);
|
||||
contents.push(
|
||||
`--- ${licenseFile} ---\n${text.replaceAll('\r\n', '\n').trim()}\n`
|
||||
);
|
||||
}
|
||||
sections.push(
|
||||
[
|
||||
`================================================================================`,
|
||||
`${packageJson.name ?? packageName}@${packageJson.version ?? 'unknown'}`,
|
||||
`Declared license: ${String(packageJson.license ?? 'unspecified')}`,
|
||||
`================================================================================`,
|
||||
...contents,
|
||||
].join('\n')
|
||||
);
|
||||
}
|
||||
|
||||
return Buffer.from(
|
||||
[
|
||||
'Third-party software license texts bundled with PDF Tools',
|
||||
'Generated deterministically from the installed production packages.',
|
||||
'',
|
||||
...sections,
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf8'
|
||||
);
|
||||
}
|
||||
|
||||
async function collectLicenseFiles(directory, prefix = '') {
|
||||
const matches = [];
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
entries.sort((left, right) => compareCodePoints(left.name, right.name));
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.name === 'node_modules') continue;
|
||||
const relativePath = path.posix.join(prefix, entry.name);
|
||||
const absolutePath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
matches.push(...(await collectLicenseFiles(absolutePath, relativePath)));
|
||||
} else if (entry.isFile() && LICENSE_FILE.test(entry.name)) {
|
||||
matches.push(relativePath);
|
||||
} else if (!entry.isFile()) {
|
||||
throw new Error(
|
||||
`Unsupported filesystem entry in license input: ${relativePath}`
|
||||
);
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
Reference in New Issue
Block a user