feat(postinstall): Add robust postinstall resource download with version checking
- Implemented version checking to avoid unnecessary re-downloads - Added retry logic with 3 attempts for network failures - Included network connectivity detection - Graceful error handling that never fails the install - Support for CI environment variable (EINVOICE_SKIP_RESOURCES) - Successfully tested all functionality including version checking and error handling
This commit is contained in:
1
assets/schematron/.version
Normal file
1
assets/schematron/.version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1.0.0
|
@@ -3,5 +3,5 @@
|
|||||||
"version": "1.3.14",
|
"version": "1.3.14",
|
||||||
"url": "https://github.com/ConnectingEurope/eInvoicing-EN16931/raw/master/cii/schematron/EN16931-CII-validation.sch",
|
"url": "https://github.com/ConnectingEurope/eInvoicing-EN16931/raw/master/cii/schematron/EN16931-CII-validation.sch",
|
||||||
"format": "CII",
|
"format": "CII",
|
||||||
"downloadDate": "2025-08-11T11:05:40.209Z"
|
"downloadDate": "2025-08-12T05:01:07.919Z"
|
||||||
}
|
}
|
@@ -3,5 +3,5 @@
|
|||||||
"version": "1.3.14",
|
"version": "1.3.14",
|
||||||
"url": "https://github.com/ConnectingEurope/eInvoicing-EN16931/raw/master/edifact/schematron/EN16931-EDIFACT-validation.sch",
|
"url": "https://github.com/ConnectingEurope/eInvoicing-EN16931/raw/master/edifact/schematron/EN16931-EDIFACT-validation.sch",
|
||||||
"format": "CII",
|
"format": "CII",
|
||||||
"downloadDate": "2025-08-11T11:05:40.547Z"
|
"downloadDate": "2025-08-12T05:01:08.258Z"
|
||||||
}
|
}
|
@@ -3,5 +3,5 @@
|
|||||||
"version": "1.3.14",
|
"version": "1.3.14",
|
||||||
"url": "https://github.com/ConnectingEurope/eInvoicing-EN16931/raw/master/ubl/schematron/EN16931-UBL-validation.sch",
|
"url": "https://github.com/ConnectingEurope/eInvoicing-EN16931/raw/master/ubl/schematron/EN16931-UBL-validation.sch",
|
||||||
"format": "UBL",
|
"format": "UBL",
|
||||||
"downloadDate": "2025-08-11T11:05:39.868Z"
|
"downloadDate": "2025-08-12T05:01:07.598Z"
|
||||||
}
|
}
|
@@ -3,5 +3,5 @@
|
|||||||
"version": "3.0.17",
|
"version": "3.0.17",
|
||||||
"url": "https://github.com/OpenPEPPOL/peppol-bis-invoice-3/raw/master/rules/sch/PEPPOL-EN16931-UBL.sch",
|
"url": "https://github.com/OpenPEPPOL/peppol-bis-invoice-3/raw/master/rules/sch/PEPPOL-EN16931-UBL.sch",
|
||||||
"format": "UBL",
|
"format": "UBL",
|
||||||
"downloadDate": "2025-08-11T11:05:40.954Z"
|
"downloadDate": "2025-08-12T05:01:08.635Z"
|
||||||
}
|
}
|
@@ -1 +0,0 @@
|
|||||||
404: Not Found
|
|
@@ -12,6 +12,7 @@
|
|||||||
"test": "(tstest test/ --verbose --logfile --timeout 60)",
|
"test": "(tstest test/ --verbose --logfile --timeout 60)",
|
||||||
"build": "(tsbuild tsfolders --allowimplicitany)",
|
"build": "(tsbuild tsfolders --allowimplicitany)",
|
||||||
"buildDocs": "(tsdoc)",
|
"buildDocs": "(tsdoc)",
|
||||||
|
"postinstall": "node dist_ts_install/index.js 2>/dev/null || true",
|
||||||
"download-schematron": "tsx ts_install/download-schematron.ts",
|
"download-schematron": "tsx ts_install/download-schematron.ts",
|
||||||
"download-test-samples": "tsx ts_install/download-test-samples.ts",
|
"download-test-samples": "tsx ts_install/download-test-samples.ts",
|
||||||
"test:conformance": "tstest test/test.conformance-harness.ts"
|
"test:conformance": "tstest test/test.conformance-harness.ts"
|
||||||
|
@@ -57,8 +57,12 @@ async function main() {
|
|||||||
console.log('\n✅ Schematron download complete!');
|
console.log('\n✅ Schematron download complete!');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the script
|
// Run if executed directly
|
||||||
main().catch(error => {
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
console.error('❌ Script failed:', error);
|
main().catch(error => {
|
||||||
process.exit(1);
|
console.error('❌ Script failed:', error);
|
||||||
});
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default main;
|
@@ -202,4 +202,5 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|||||||
main().catch(console.error);
|
main().catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { downloadTestSamples, TEST_SAMPLE_SOURCES };
|
export default main;
|
||||||
|
export { TEST_SAMPLE_SOURCES };
|
@@ -171,8 +171,12 @@ async function downloadXRechnungRules(): Promise<void> {
|
|||||||
console.log('3. Add XRechnung-specific TypeScript validators');
|
console.log('3. Add XRechnung-specific TypeScript validators');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the script
|
// Run if executed directly
|
||||||
downloadXRechnungRules().catch(error => {
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
console.error('Failed to download XRechnung rules:', error);
|
downloadXRechnungRules().catch(error => {
|
||||||
process.exit(1);
|
console.error('Failed to download XRechnung rules:', error);
|
||||||
});
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default downloadXRechnungRules;
|
275
ts_install/index.ts
Normal file
275
ts_install/index.ts
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post-install script to download required validation resources
|
||||||
|
* This script is automatically run after npm/pnpm install
|
||||||
|
* All users need validation capabilities, so this is mandatory
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SchematronDownloader } from '../dist_ts/formats/validation/schematron.downloader.js';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
|
// Version for cache invalidation
|
||||||
|
const RESOURCES_VERSION = '1.0.0';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we're in a proper npm install context
|
||||||
|
*/
|
||||||
|
function isValidInstallContext(): boolean {
|
||||||
|
// Skip if we're in a git install or similar
|
||||||
|
if (process.env.npm_lifecycle_event !== 'postinstall') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip in CI if explicitly disabled
|
||||||
|
if (process.env.CI && process.env.EINVOICE_SKIP_RESOURCES) {
|
||||||
|
console.log('⏭️ Skipping resource download (EINVOICE_SKIP_RESOURCES set)');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a checksum for a file
|
||||||
|
*/
|
||||||
|
function getFileChecksum(filePath: string): string | null {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(filePath)) return null;
|
||||||
|
const content = fs.readFileSync(filePath);
|
||||||
|
return crypto.createHash('sha256').update(content).digest('hex');
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if resources are already downloaded and valid
|
||||||
|
*/
|
||||||
|
function checkExistingResources(): boolean {
|
||||||
|
const versionFile = path.join('assets', 'schematron', '.version');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(versionFile)) return false;
|
||||||
|
|
||||||
|
const version = fs.readFileSync(versionFile, 'utf-8').trim();
|
||||||
|
if (version !== RESOURCES_VERSION) {
|
||||||
|
console.log('📦 Resource version mismatch, re-downloading...');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if key files exist
|
||||||
|
const keyFiles = [
|
||||||
|
'assets/schematron/EN16931-UBL-v1.3.14.sch',
|
||||||
|
'assets/schematron/EN16931-CII-v1.3.14.sch',
|
||||||
|
'assets/schematron/PEPPOL-EN16931-UBL-v3.0.17.sch'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of keyFiles) {
|
||||||
|
if (!fs.existsSync(file)) {
|
||||||
|
console.log(`📦 Missing ${file}, re-downloading resources...`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save version file after successful download
|
||||||
|
*/
|
||||||
|
function saveVersionFile(): void {
|
||||||
|
const versionFile = path.join('assets', 'schematron', '.version');
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.dirname(versionFile), { recursive: true });
|
||||||
|
fs.writeFileSync(versionFile, RESOURCES_VERSION);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ Could not save version file:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadSchematron() {
|
||||||
|
console.log('📥 Downloading Schematron validation files...\n');
|
||||||
|
|
||||||
|
const downloader = new SchematronDownloader('assets/schematron');
|
||||||
|
await downloader.initialize();
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let failCount = 0;
|
||||||
|
|
||||||
|
// Download EN16931 Schematron files
|
||||||
|
console.log('🔵 Downloading EN16931 Schematron files...');
|
||||||
|
try {
|
||||||
|
const en16931Files = await downloader.downloadStandard('EN16931');
|
||||||
|
console.log(`✅ Downloaded ${en16931Files.length} EN16931 files`);
|
||||||
|
successCount += en16931Files.length;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`⚠️ Failed to download EN16931: ${error.message}`);
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download PEPPOL Schematron files
|
||||||
|
console.log('\n🔵 Downloading PEPPOL Schematron files...');
|
||||||
|
try {
|
||||||
|
const peppolFiles = await downloader.downloadStandard('PEPPOL');
|
||||||
|
console.log(`✅ Downloaded ${peppolFiles.length} PEPPOL files`);
|
||||||
|
successCount += peppolFiles.length;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`⚠️ Failed to download PEPPOL: ${error.message}`);
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download XRechnung Schematron files
|
||||||
|
console.log('\n🔵 Downloading XRechnung Schematron files...');
|
||||||
|
try {
|
||||||
|
const xrechnungFiles = await downloader.downloadStandard('XRECHNUNG');
|
||||||
|
console.log(`✅ Downloaded ${xrechnungFiles.length} XRechnung files`);
|
||||||
|
successCount += xrechnungFiles.length;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`⚠️ Failed to download XRechnung: ${error.message}`);
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report results
|
||||||
|
if (successCount > 0) {
|
||||||
|
saveVersionFile();
|
||||||
|
console.log(`\n✅ Successfully downloaded ${successCount} validation files`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failCount > 0) {
|
||||||
|
console.log(`⚠️ Failed to download ${failCount} resource sets`);
|
||||||
|
console.log(' Some validation features may be limited');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { successCount, failCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Check if we should run
|
||||||
|
if (!isValidInstallContext()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('='.repeat(60));
|
||||||
|
console.log('🚀 @fin.cx/einvoice - Validation Resources Setup');
|
||||||
|
console.log('='.repeat(60));
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if resources already exist and are current
|
||||||
|
if (checkExistingResources()) {
|
||||||
|
console.log('✅ Validation resources already installed and up-to-date');
|
||||||
|
console.log();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we're in the right directory
|
||||||
|
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
||||||
|
if (!fs.existsSync(packageJsonPath)) {
|
||||||
|
console.error('❌ Error: package.json not found');
|
||||||
|
console.error(' Installation context issue - skipping resource download');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if dist_ts exists (module should be built)
|
||||||
|
const distPath = path.join(process.cwd(), 'dist_ts');
|
||||||
|
if (!fs.existsSync(distPath)) {
|
||||||
|
console.log('⚠️ Module not yet built - skipping resource download');
|
||||||
|
console.log(' Resources will be downloaded on first use');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check network connectivity (simple DNS check)
|
||||||
|
try {
|
||||||
|
await import('dns').then(dns =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
dns.lookup('github.com', (err) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(true);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
console.log('⚠️ No network connectivity detected');
|
||||||
|
console.log(' Validation resources will be downloaded on first use');
|
||||||
|
console.log(' when network is available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download resources with retry logic
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 3;
|
||||||
|
let lastError;
|
||||||
|
|
||||||
|
while (attempts < maxAttempts) {
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
if (attempts > 1) {
|
||||||
|
console.log(`\n🔄 Retry attempt ${attempts}/${maxAttempts}...`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { successCount, failCount } = await downloadSchematron();
|
||||||
|
|
||||||
|
if (successCount > 0) {
|
||||||
|
console.log();
|
||||||
|
console.log('='.repeat(60));
|
||||||
|
console.log('✅ Validation resources installed successfully!');
|
||||||
|
console.log('='.repeat(60));
|
||||||
|
console.log();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failCount > 0 && attempts < maxAttempts) {
|
||||||
|
console.log(`\n⚠️ Some downloads failed, retrying...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2s before retry
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
if (attempts < maxAttempts) {
|
||||||
|
console.log(`\n⚠️ Download failed: ${error.message}`);
|
||||||
|
console.log(' Retrying...');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000)); // Wait 3s before retry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here, downloads failed after retries
|
||||||
|
console.error();
|
||||||
|
console.error('⚠️ Could not download all validation resources');
|
||||||
|
console.error(' The library will work but validation features may be limited');
|
||||||
|
console.error(' Resources will be attempted again on first use');
|
||||||
|
console.error();
|
||||||
|
|
||||||
|
if (lastError) {
|
||||||
|
console.error(' Last error:', lastError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// Catch-all for unexpected errors
|
||||||
|
console.error();
|
||||||
|
console.error('⚠️ Unexpected error during resource setup:', error.message);
|
||||||
|
console.error(' This won\'t affect library installation');
|
||||||
|
console.error(' Resources will be downloaded on first use');
|
||||||
|
console.error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only run if this is the main module
|
||||||
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
main().catch(error => {
|
||||||
|
console.error('⚠️ Resource setup error:', error.message);
|
||||||
|
// Never fail the install
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default main;
|
Reference in New Issue
Block a user