From 2f597d79df3154557b9933ba6c0fe2292469d95b Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 12 Aug 2025 05:02:42 +0000 Subject: [PATCH] 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 --- assets/schematron/.version | 1 + .../schematron/EN16931-CII-v1.3.14.meta.json | 2 +- .../EN16931-EDIFACT-v1.3.14.meta.json | 2 +- .../schematron/EN16931-UBL-v1.3.14.meta.json | 2 +- .../PEPPOL-EN16931-UBL-v3.0.17.meta.json | 2 +- .../xrechnung/temp/xrechnung-schematron.zip | 1 - package.json | 1 + ts_install/download-schematron.ts | 14 +- ts_install/download-test-samples.ts | 3 +- ts_install/download-xrechnung-rules.ts | 14 +- ts_install/index.ts | 275 ++++++++++++++++++ 11 files changed, 301 insertions(+), 16 deletions(-) create mode 100644 assets/schematron/.version delete mode 100644 assets/schematron/xrechnung/temp/xrechnung-schematron.zip create mode 100644 ts_install/index.ts diff --git a/assets/schematron/.version b/assets/schematron/.version new file mode 100644 index 0000000..afaf360 --- /dev/null +++ b/assets/schematron/.version @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/assets/schematron/EN16931-CII-v1.3.14.meta.json b/assets/schematron/EN16931-CII-v1.3.14.meta.json index f518299..a3c7855 100644 --- a/assets/schematron/EN16931-CII-v1.3.14.meta.json +++ b/assets/schematron/EN16931-CII-v1.3.14.meta.json @@ -3,5 +3,5 @@ "version": "1.3.14", "url": "https://github.com/ConnectingEurope/eInvoicing-EN16931/raw/master/cii/schematron/EN16931-CII-validation.sch", "format": "CII", - "downloadDate": "2025-08-11T11:05:40.209Z" + "downloadDate": "2025-08-12T05:01:07.919Z" } \ No newline at end of file diff --git a/assets/schematron/EN16931-EDIFACT-v1.3.14.meta.json b/assets/schematron/EN16931-EDIFACT-v1.3.14.meta.json index 5466b86..7780287 100644 --- a/assets/schematron/EN16931-EDIFACT-v1.3.14.meta.json +++ b/assets/schematron/EN16931-EDIFACT-v1.3.14.meta.json @@ -3,5 +3,5 @@ "version": "1.3.14", "url": "https://github.com/ConnectingEurope/eInvoicing-EN16931/raw/master/edifact/schematron/EN16931-EDIFACT-validation.sch", "format": "CII", - "downloadDate": "2025-08-11T11:05:40.547Z" + "downloadDate": "2025-08-12T05:01:08.258Z" } \ No newline at end of file diff --git a/assets/schematron/EN16931-UBL-v1.3.14.meta.json b/assets/schematron/EN16931-UBL-v1.3.14.meta.json index 6448340..debb71c 100644 --- a/assets/schematron/EN16931-UBL-v1.3.14.meta.json +++ b/assets/schematron/EN16931-UBL-v1.3.14.meta.json @@ -3,5 +3,5 @@ "version": "1.3.14", "url": "https://github.com/ConnectingEurope/eInvoicing-EN16931/raw/master/ubl/schematron/EN16931-UBL-validation.sch", "format": "UBL", - "downloadDate": "2025-08-11T11:05:39.868Z" + "downloadDate": "2025-08-12T05:01:07.598Z" } \ No newline at end of file diff --git a/assets/schematron/PEPPOL-EN16931-UBL-v3.0.17.meta.json b/assets/schematron/PEPPOL-EN16931-UBL-v3.0.17.meta.json index 530863a..0f71da0 100644 --- a/assets/schematron/PEPPOL-EN16931-UBL-v3.0.17.meta.json +++ b/assets/schematron/PEPPOL-EN16931-UBL-v3.0.17.meta.json @@ -3,5 +3,5 @@ "version": "3.0.17", "url": "https://github.com/OpenPEPPOL/peppol-bis-invoice-3/raw/master/rules/sch/PEPPOL-EN16931-UBL.sch", "format": "UBL", - "downloadDate": "2025-08-11T11:05:40.954Z" + "downloadDate": "2025-08-12T05:01:08.635Z" } \ No newline at end of file diff --git a/assets/schematron/xrechnung/temp/xrechnung-schematron.zip b/assets/schematron/xrechnung/temp/xrechnung-schematron.zip deleted file mode 100644 index 1becba2..0000000 --- a/assets/schematron/xrechnung/temp/xrechnung-schematron.zip +++ /dev/null @@ -1 +0,0 @@ -404: Not Found \ No newline at end of file diff --git a/package.json b/package.json index ee4e0de..678db48 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "test": "(tstest test/ --verbose --logfile --timeout 60)", "build": "(tsbuild tsfolders --allowimplicitany)", "buildDocs": "(tsdoc)", + "postinstall": "node dist_ts_install/index.js 2>/dev/null || true", "download-schematron": "tsx ts_install/download-schematron.ts", "download-test-samples": "tsx ts_install/download-test-samples.ts", "test:conformance": "tstest test/test.conformance-harness.ts" diff --git a/ts_install/download-schematron.ts b/ts_install/download-schematron.ts index a44cf38..3f21354 100644 --- a/ts_install/download-schematron.ts +++ b/ts_install/download-schematron.ts @@ -57,8 +57,12 @@ async function main() { console.log('\n✅ Schematron download complete!'); } -// Run the script -main().catch(error => { - console.error('❌ Script failed:', error); - process.exit(1); -}); \ No newline at end of file +// Run if executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + main().catch(error => { + console.error('❌ Script failed:', error); + process.exit(1); + }); +} + +export default main; \ No newline at end of file diff --git a/ts_install/download-test-samples.ts b/ts_install/download-test-samples.ts index 12c1f2e..4e4aa64 100644 --- a/ts_install/download-test-samples.ts +++ b/ts_install/download-test-samples.ts @@ -202,4 +202,5 @@ if (import.meta.url === `file://${process.argv[1]}`) { main().catch(console.error); } -export { downloadTestSamples, TEST_SAMPLE_SOURCES }; \ No newline at end of file +export default main; +export { TEST_SAMPLE_SOURCES }; \ No newline at end of file diff --git a/ts_install/download-xrechnung-rules.ts b/ts_install/download-xrechnung-rules.ts index 522a0df..017f69e 100644 --- a/ts_install/download-xrechnung-rules.ts +++ b/ts_install/download-xrechnung-rules.ts @@ -171,8 +171,12 @@ async function downloadXRechnungRules(): Promise { console.log('3. Add XRechnung-specific TypeScript validators'); } -// Run the script -downloadXRechnungRules().catch(error => { - console.error('Failed to download XRechnung rules:', error); - process.exit(1); -}); \ No newline at end of file +// Run if executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + downloadXRechnungRules().catch(error => { + console.error('Failed to download XRechnung rules:', error); + process.exit(1); + }); +} + +export default downloadXRechnungRules; \ No newline at end of file diff --git a/ts_install/index.ts b/ts_install/index.ts new file mode 100644 index 0000000..79ef3e7 --- /dev/null +++ b/ts_install/index.ts @@ -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; \ No newline at end of file