diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01ef398..6962a25 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,11 +54,22 @@ jobs: - name: Install dependencies run: yarn install --immutable + # Build before type check: the /node subpath type fixture resolves + # `ksef-client-ts/node` against the built dist (dist/node.d.ts / .d.cts). + - name: Build + run: yarn build + - name: Type check run: yarn lint - - name: Build - run: yarn build + # Resolves the /node subpath types against the built dist; requires the + # Build step above (kept separate from `lint` so a clean `yarn lint` + # needs no prior build). + - name: Check /node entry types + run: yarn check:node-types + + - name: Check fs-free core bundles + run: node packages/ksef-client-ts/scripts/check-fs-free-core.mjs - name: Run tests with coverage run: yarn test --coverage diff --git a/package.json b/package.json index c6af123..9f3c980 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "test:e2e": "yarn workspace ksef-client-ts test:e2e", "test:watch": "yarn workspace ksef-client-ts test:watch", "lint": "yarn workspace ksef-client-ts lint", + "check:node-types": "yarn workspace ksef-client-ts check:node-types", "lint:md": "yarn workspace ksef-client-ts lint:md", "lint:md:fix": "yarn workspace ksef-client-ts lint:md:fix", "docs:dev": "yarn workspace ksef-client-ts docs:dev", diff --git a/packages/ksef-client-ts/CHANGELOG.md b/packages/ksef-client-ts/CHANGELOG.md index 40329e8..738da80 100644 --- a/packages/ksef-client-ts/CHANGELOG.md +++ b/packages/ksef-client-ts/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. +## [0.10.0] - 2026-06-14 + +### Changed (breaking) + +- **Node.js-specific features moved to a separate entry point** — filesystem-dependent and other Node-only features are no longer exported from the main `ksef-client-ts` entry point, to improve portability and reduce bundle size for web/edge environments. They are now available via `ksef-client-ts/node`; see the [v0.10 migration guide](docs/migration-v0.10.md) for the affected exports and how to update imports. + +### Changed + +- **Smaller core bundle** — certificate-based authentication now loads its cryptographic signing modules on demand instead of bundling them upfront, reducing the core bundle size. This is transparent to callers; the public API is unchanged. + ## [0.9.1] - 2026-06-13 ### Changed diff --git a/packages/ksef-client-ts/README.md b/packages/ksef-client-ts/README.md index 9028828..e192c85 100644 --- a/packages/ksef-client-ts/README.md +++ b/packages/ksef-client-ts/README.md @@ -23,6 +23,7 @@ TypeScript client for the Polish National e-Invoice System (KSeF) API v2. - **Comprehensive test coverage** — unit + E2E tests across HTTP, crypto, services, workflows; CI on every change - **Interactive setup wizard** — `ksef setup` guides through environment selection, authentication, and token generation in one flow - **Zero HTTP dependencies** — native `fetch` (Node 18+); dual ESM/CJS via tsup +- **fs-free core** — the main entry point is free of filesystem access, so it runs on Node, Deno, and edge runtimes; Node-only features (filesystem storage, XSD validation) are available via the `ksef-client-ts/node` export Requires **Node.js 18+**. diff --git a/packages/ksef-client-ts/docs/.vitepress/config.ts b/packages/ksef-client-ts/docs/.vitepress/config.ts index cdf55e4..d1ea8b9 100644 --- a/packages/ksef-client-ts/docs/.vitepress/config.ts +++ b/packages/ksef-client-ts/docs/.vitepress/config.ts @@ -51,6 +51,7 @@ export default defineConfig({ items: [ { text: 'Home', link: '/' }, { text: 'Quick Start', link: '/quick-start' }, + { text: 'Migration Guide (v0.10)', link: '/migration-v0.10' }, { text: 'Architecture', link: '/architecture' }, { text: 'Authentication', link: '/authentication' }, ], diff --git a/packages/ksef-client-ts/docs/deno.md b/packages/ksef-client-ts/docs/deno.md index 7f40945..08fe53b 100644 --- a/packages/ksef-client-ts/docs/deno.md +++ b/packages/ksef-client-ts/docs/deno.md @@ -84,51 +84,28 @@ All cryptographic operations — RSA-OAEP token encryption, AES-256-CBC session --- -## What doesn't work: `validateAgainstXsd` +## What doesn't work: Node.js-specific Features -The one API that does not work on Deno is `validateAgainstXsd(xml, xsdPath)`, used for strict XSD-level invoice validation against the official KSeF schemas. It depends on `libxmljs2`, which wraps the native `libxml2` C library via N-API. Deno's Node compatibility layer cannot load that binding (any attempt to `require('libxmljs2')` or `import('libxmljs2')` will segfault the runtime — an upstream Deno limitation, not something fixable in the library). +As of v0.10.0, features that rely on Node.js-specific APIs (like the filesystem or native C++ add-ons) are no longer part of the main library bundle. They are now available via a separate `ksef-client-ts/node` entry point. -### What happens if you call it anyway +This means these features are definitively not available on Deno, as Deno cannot consume the Node.js-specific APIs they rely on. -Assuming `libxmljs2` is **not** installed (the expected state on Deno — you should not install it, see below), the call throws a clear, catchable error: +### `validateAgainstXsd` -```ts -try { - validateAgainstXsd(xml, xsdPath); -} catch (err) { - // Error: libxmljs2 is not installed; cannot run XSD validation. - // Install it as an optional peer dependency (e.g. `yarn add -O libxmljs2` or `npm i -O libxmljs2`). -} -``` +The primary example is `validateAgainstXsd(xml, xsdPath)`, used for strict XSD-level invoice validation. It depends on `libxmljs2`, which wraps the native `libxml2` C library via N-API. Deno's Node compatibility layer cannot load this binding. -The library exports a helper to identify this specific failure so you can fall back gracefully: +Attempting to import it on Deno will fail: -```ts -import { validateAgainstXsd, validate, isMissingLibxmljsError } from 'npm:ksef-client-ts@0.8.0'; - -try { - const result = validateAgainstXsd(xml, xsdPath); - // ... -} catch (err) { - if (isMissingLibxmljsError(err)) { - // Fall back to Zod-based schema validation — pure JS, works on Deno. - const result = await validate(xml); - // ... - } else { - throw err; - } -} +```typescript +// This will fail on Deno +import { validateAgainstXsd } from 'npm:ksef-client-ts/node'; ``` -### Zod validation is usually enough - -For the common case — "build an invoice XML, verify it's structurally correct before sending" — Zod-based validation (`validate()`, `validateSchema()`) is sufficient. It checks every required field, format (NIP, PESEL, KSeF numbers), enum values, multi-rate VAT group constraints, and business rules end-to-end. XSD validation adds pedantic structural checks against the official schema; it's useful for debugging hard-to-explain server-side rejections, not for routine validation. - -### Why you should not `install` libxmljs2 on Deno +The Zod-based validator (`validate()`) is the recommended alternative and works perfectly on Deno. -If `libxmljs2` somehow ends up in a `node_modules/` that Deno can see (for example, a shared folder with a Node project), the segfault happens **at library import time**, not at call time. Your whole app fails to start with a segmentation fault rather than a catchable error. +### Filesystem Storage -The library marks `libxmljs2` as an optional peer dependency, so a plain `deno add npm:ksef-client-ts` or `npm install ksef-client-ts` will never pull it in transitively. Only an explicit direct install will. On a Deno-only setup, there is no reason to do that. +Similarly, filesystem-based storage classes like `FileOfflineInvoiceStorage` and `FileHwmStore` are in the `ksef-client-ts/node` entry point and will not work in Deno's environment. Use `InMemoryOfflineInvoiceStorage` or implement a custom storage backend (e.g., using Deno's file APIs or a database). --- diff --git a/packages/ksef-client-ts/docs/index.md b/packages/ksef-client-ts/docs/index.md index e7819f1..76c2a1e 100644 --- a/packages/ksef-client-ts/docs/index.md +++ b/packages/ksef-client-ts/docs/index.md @@ -52,4 +52,6 @@ features: details: Get started in one command — ksef setup walks you through environment selection, NIP configuration, external signature authentication, and API token generation. Credentials are securely stored in ~/.ksef/credentials.json. - title: Zero HTTP Dependencies details: Uses native fetch (Node 18+) with no external HTTP libraries. Dual ESM/CJS output via tsup. Resilient transport with exponential backoff retry, token bucket rate limiting, opt-in circuit breaker, and presigned URL validation. + - title: fs-free Core + details: The main entry point is free of filesystem access and runs on Node, Deno, and edge runtimes. Node.js-only features (filesystem access, XSD validation) are available via the `ksef-client-ts/node` export, keeping the core library small. --- diff --git a/packages/ksef-client-ts/docs/migration-v0.10.md b/packages/ksef-client-ts/docs/migration-v0.10.md new file mode 100644 index 0000000..8bedc20 --- /dev/null +++ b/packages/ksef-client-ts/docs/migration-v0.10.md @@ -0,0 +1,93 @@ +# Migration Guide: v0.10.0 + +Version `0.10.0` introduces a significant refactoring to improve portability and reduce the bundle size for web and edge environments. This guide outlines the breaking changes and how to update your code. + +## Node.js-specific Features Moved to a Separate Entry Point + +To keep the core `ksef-client-ts` library free of filesystem dependencies (so it runs on Deno and edge functions without Node-specific filesystem access), filesystem-dependent and other Node-only features have been moved to a separate entry point: `ksef-client-ts/node`. + +If your application runs on Node.js and uses any of the features listed below, you will need to update your import paths. + +### Affected Features + +1. **Offline Invoice Storage:** The `FileOfflineInvoiceStorage` class, which uses the filesystem to store offline invoices, is now exposed via the `ksef-client-ts/node` entry point. + + **Before:** + + ```typescript + import { FileOfflineInvoiceStorage } from 'ksef-client-ts'; + ``` + + **After:** + + ```typescript + import { FileOfflineInvoiceStorage } from 'ksef-client-ts/node'; + ``` + +2. **XSD Schema Validation:** The `validateAgainstXsd` function and its related helpers rely on `libxmljs2`, a native Node.js module. These are now available from the `ksef-client-ts/node` entry point. + + **Before:** + + ```typescript + import { validateAgainstXsd } from 'ksef-client-ts'; + ``` + + **After:** + + ```typescript + import { validateAgainstXsd } from 'ksef-client-ts/node'; + ``` + + This also applies to `FA_XSD_PATHS`, `PEF_XSD_PATHS`, `libxmljsAvailable`, `resolveXsdFor`, and `isMissingLibxmljsError`. + +3. **High-Water-Mark Storage for Incremental Export:** The `FileHwmStore`, used for persisting the high-water-mark in incremental invoice exports, has been moved. + + **Before:** + + ```typescript + import { FileHwmStore } from 'ksef-client-ts'; + ``` + + **After:** + + ```typescript + import { FileHwmStore } from 'ksef-client-ts/node'; + ``` + +### Rationale + +This change allows developers to use `ksef-client-ts` in a wider range of JavaScript environments without including unnecessary Node.js-specific code, leading to smaller bundles and better performance, especially in serverless and edge computing contexts. + +## Troubleshooting + +The `ksef-client-ts/node` entry point is published as a [subpath export](https://nodejs.org/api/packages.html#subpath-exports). Two toolchain setups need attention after you update an import. + +### TypeScript: `Cannot find module 'ksef-client-ts/node'` (TS2307) + +If TypeScript reports `TS2307` for the new import even though the code runs fine at runtime, your project is using the legacy module resolution that does not read the package `exports` map. Set `moduleResolution` to a value that supports subpath exports: + +```json +{ + "compilerOptions": { + "module": "node16", + "moduleResolution": "node16" + } +} +``` + +`nodenext` or `bundler` work too. The legacy `node` (a.k.a. `node10`) resolution cannot resolve subpath exports and will keep failing. + +### CommonJS: the moved symbol is `undefined` + +Under CommonJS, importing a moved symbol from the root entry point no longer throws at `require` time — it silently resolves to `undefined`, and you only see the failure later when you use it: + +```js +// ❌ FileHwmStore is undefined here — no error yet +const { FileHwmStore } = require('ksef-client-ts'); +new FileHwmStore(); // TypeError: FileHwmStore is not a constructor + +// ✅ require from the /node entry point instead +const { FileHwmStore } = require('ksef-client-ts/node'); +``` + +If you hit a `TypeError: X is not a constructor` (or a call on `undefined`) for any of the moved symbols, update the `require` path to `ksef-client-ts/node`. diff --git a/packages/ksef-client-ts/docs/offline-mode.md b/packages/ksef-client-ts/docs/offline-mode.md index df1d293..8e28223 100644 --- a/packages/ksef-client-ts/docs/offline-mode.md +++ b/packages/ksef-client-ts/docs/offline-mode.md @@ -183,7 +183,7 @@ const storage = new InMemoryOfflineInvoiceStorage(); For CLI and persistent use. Stores each invoice as `{uuid}.json` in a directory. Default: `~/.ksef/offline/`. ```typescript -import { FileOfflineInvoiceStorage } from 'ksef-client-ts'; +import { FileOfflineInvoiceStorage } from 'ksef-client-ts/node'; const storage = new FileOfflineInvoiceStorage(); // ~/.ksef/offline/ const custom = new FileOfflineInvoiceStorage('/tmp/offline'); // custom dir diff --git a/packages/ksef-client-ts/docs/workflows.md b/packages/ksef-client-ts/docs/workflows.md index 12426d3..daa0653 100644 --- a/packages/ksef-client-ts/docs/workflows.md +++ b/packages/ksef-client-ts/docs/workflows.md @@ -487,8 +487,8 @@ The KSeF export API has a limit on how many invoices it returns per export. If t ```typescript import { incrementalExportAndDownload, - FileHwmStore, } from 'ksef-client-ts'; +import { FileHwmStore } from 'ksef-client-ts/node'; const result = await incrementalExportAndDownload(client, { subjectType: 'Subject1', @@ -570,8 +570,8 @@ Two built-in implementations: | Class | Storage | Use case | |-------|---------|----------| -| `InMemoryHwmStore` | In-process memory | Tests, one-off exports | -| `FileHwmStore` | JSON file on disk | Persistent synchronization, cron jobs | +| `InMemoryHwmStore` | In-process memory | Tests, one-off exports, non-Node environments | +| `FileHwmStore` | JSON file on disk | Persistent synchronization, cron jobs (Node.js only) | `FileHwmStore` gracefully handles missing files (returns `{}` on `ENOENT`). diff --git a/packages/ksef-client-ts/docs/xml-serialization.md b/packages/ksef-client-ts/docs/xml-serialization.md index 3252506..513bd79 100644 --- a/packages/ksef-client-ts/docs/xml-serialization.md +++ b/packages/ksef-client-ts/docs/xml-serialization.md @@ -295,7 +295,7 @@ Schema inference mirrors the serializer: `Invoice` key → PEF, `CreditNote` → ## Limitations -- **Runtime XSD validation is opt-in and requires an optional peer dependency.** The CLI's `--validate-xsd` flag and the `validateAgainstXsd` helper both depend on `libxmljs2`, which the package declares as an optional peer dependency rather than bundling: it is ~2 MB, native, and historically flaky across Node versions. Install it with `npm install --save-optional libxmljs2` when you want XSD-level validation; otherwise the Zod-based [XML validator](./validation.md) (generated from the same XSDs) is always available. +- **Runtime XSD validation is opt-in, Node.js-only, and requires an optional peer dependency.** The CLI's `--validate-xsd` flag and the `validateAgainstXsd` helper both depend on `libxmljs2`, a native add-on. As of v0.10.0, these are exposed via the `ksef-client-ts/node` entry point and will not work in other runtimes (like Deno or browsers). The package declares `libxmljs2` as an optional peer dependency rather than bundling it: it is ~2 MB, native, and historically flaky across Node versions. Install it with `npm install --save-optional libxmljs2` when you want XSD-level validation; otherwise the Zod-based [XML validator](./validation.md) (generated from the same XSDs) is always available. - **No FA_RR structural builder.** The FA_RR v1-1E schema shipped in December 2025. No reference implementation has a structural builder for it yet. FA_RR XML is supported via the pass-through path — build the XML yourself (or with another tool) and pipe it through `serializeInvoiceXml` to get a `Buffer`. - **No round-trip parsing.** The serializer is one-way: typed object → XML. Parsing XML back into a `FakturaInput` is out of scope. - **No fluent builder DSL.** Plain typed objects only. A fluent wrapper can be layered on top later without breaking the object-based API; demand has not materialised yet. diff --git a/packages/ksef-client-ts/package.json b/packages/ksef-client-ts/package.json index 71be0aa..e26d740 100644 --- a/packages/ksef-client-ts/package.json +++ b/packages/ksef-client-ts/package.json @@ -1,8 +1,9 @@ { "name": "ksef-client-ts", - "version": "0.9.1", + "version": "0.10.0", "description": "TypeScript client for the Polish National e-Invoice System (KSeF) API", "type": "module", + "sideEffects": false, "main": "./dist/index.cjs", "module": "./dist/index.js", "types": "./dist/index.d.ts", @@ -16,6 +17,16 @@ "types": "./dist/index.d.cts", "default": "./dist/index.cjs" } + }, + "./node": { + "import": { + "types": "./dist/node.d.ts", + "default": "./dist/node.js" + }, + "require": { + "types": "./dist/node.d.cts", + "default": "./dist/node.cjs" + } } }, "bin": { @@ -33,6 +44,7 @@ "test:e2e:watch": "vitest tests/e2e --reporter=verbose", "test:watch": "vitest", "lint": "tsc --noEmit", + "check:node-types": "tsc --project tsconfig.node-check.json", "lint:md": "markdownlint-cli2", "lint:md:fix": "markdownlint-cli2 --fix", "link": "npm link", diff --git a/packages/ksef-client-ts/scripts/check-fs-free-core.mjs b/packages/ksef-client-ts/scripts/check-fs-free-core.mjs new file mode 100644 index 0000000..e1b81c6 --- /dev/null +++ b/packages/ksef-client-ts/scripts/check-fs-free-core.mjs @@ -0,0 +1,90 @@ +import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +const packageRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "..", +); + +export const coreBundlePaths = ["dist/index.js", "dist/index.cjs"]; +export const nodeBundlePaths = ["dist/node.js", "dist/node.cjs"]; + +export const fsLeakPatterns = [ + { label: "node:fs/promises", pattern: /node:fs\/promises/ }, + { label: "node:fs", pattern: /node:fs/ }, + { label: "fs/promises", pattern: /(? pattern.test(source)) + .map(({ label }) => label); +} + +async function readBundle(relativePath) { + const absolutePath = path.join(packageRoot, relativePath); + + try { + return await readFile(absolutePath, "utf8"); + } catch (error) { + if (error?.code === "ENOENT") { + throw new Error(`Missing built bundle: ${relativePath}. Run yarn build first.`); + } + + throw error; + } +} + +export async function checkFsFreeCore() { + const failures = []; + + for (const relativePath of coreBundlePaths) { + const source = await readBundle(relativePath); + const markers = findFsMarkers(source); + + if (markers.length > 0) { + failures.push(`${relativePath}: ${markers.join(", ")}`); + } + } + + for (const relativePath of nodeBundlePaths) { + const nodeBundleMarkers = findFsMarkers(await readBundle(relativePath)); + + if (nodeBundleMarkers.length === 0) { + failures.push(`${relativePath}: expected fs marker for node-only entry`); + } + } + + if (failures.length > 0) { + throw new Error(`fs cleanliness guard failed:\n- ${failures.join("\n- ")}`); + } +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + checkFsFreeCore().catch((error) => { + console.error(error.message); + process.exitCode = 1; + }); +} diff --git a/packages/ksef-client-ts/src/node.ts b/packages/ksef-client-ts/src/node.ts new file mode 100644 index 0000000..14c0e40 --- /dev/null +++ b/packages/ksef-client-ts/src/node.ts @@ -0,0 +1,16 @@ +/** + * Node-only entry point (`ksef-client-ts/node`). + * + * Re-exports the filesystem-backed symbols that the root barrel no longer + * ships, keeping the core import fs-free. Import these from `ksef-client-ts/node` + * when running on Node.js and you need disk-backed storage or XSD validation. + */ +export { FileOfflineInvoiceStorage } from './offline/file-storage.js'; + +export { + FA_XSD_PATHS, PEF_XSD_PATHS, libxmljsAvailable, resolveXsdFor, validateAgainstXsd, + isMissingLibxmljsError, + type InvoiceSchemaId, type ValidateAgainstXsdResult, +} from './validation/xsd-validator.js'; + +export { FileHwmStore } from './workflows/hwm-storage.js'; diff --git a/packages/ksef-client-ts/src/offline/index.ts b/packages/ksef-client-ts/src/offline/index.ts index 792fb00..0a6f1f9 100644 --- a/packages/ksef-client-ts/src/offline/index.ts +++ b/packages/ksef-client-ts/src/offline/index.ts @@ -25,4 +25,3 @@ export { getPolishHolidays, isPolishHoliday } from './holidays.js'; export type { OfflineInvoiceFilter, OfflineInvoiceStorage, OfflineInvoiceUpdates } from './storage.js'; export { InMemoryOfflineInvoiceStorage } from './storage.js'; -export { FileOfflineInvoiceStorage } from './file-storage.js'; diff --git a/packages/ksef-client-ts/src/validation/index.ts b/packages/ksef-client-ts/src/validation/index.ts index b4398ce..041205d 100644 --- a/packages/ksef-client-ts/src/validation/index.ts +++ b/packages/ksef-client-ts/src/validation/index.ts @@ -21,8 +21,3 @@ export { type InvoiceValidationErrorCode, type ValidateOptions, type BatchValidationResult, } from './invoice-validator.js'; export { validateCharValidity, DISCOURAGED_UNICODE_RANGES } from './char-validity.js'; -export { - FA_XSD_PATHS, PEF_XSD_PATHS, libxmljsAvailable, resolveXsdFor, validateAgainstXsd, - isMissingLibxmljsError, - type InvoiceSchemaId, type ValidateAgainstXsdResult, -} from './xsd-validator.js'; diff --git a/packages/ksef-client-ts/src/workflows/index.ts b/packages/ksef-client-ts/src/workflows/index.ts index d8c4611..b2b5d5f 100644 --- a/packages/ksef-client-ts/src/workflows/index.ts +++ b/packages/ksef-client-ts/src/workflows/index.ts @@ -10,7 +10,7 @@ export { incrementalExportAndDownload } from './incremental-export-workflow.js'; export type { IncrementalExportOptions, IncrementalExportResult } from './incremental-export-workflow.js'; export { updateContinuationPoint, getEffectiveStartDate, deduplicateByKsefNumber } from './hwm-coordinator.js'; export type { ContinuationPoints } from './hwm-coordinator.js'; -export { InMemoryHwmStore, FileHwmStore } from './hwm-storage.js'; +export { InMemoryHwmStore } from './hwm-storage.js'; export type { HwmStore } from './hwm-storage.js'; export { authenticateWithToken, authenticateWithCertificate, authenticateWithPkcs12, authenticateWithExternalSignature } from './auth-workflow.js'; export type { AuthResult, TokenAuthOptions, CertificateAuthOptions, Pkcs12AuthOptions, ExternalSignatureAuthOptions } from './auth-workflow.js'; diff --git a/packages/ksef-client-ts/tests/fixtures/node-types-check.ts b/packages/ksef-client-ts/tests/fixtures/node-types-check.ts new file mode 100644 index 0000000..c29783a --- /dev/null +++ b/packages/ksef-client-ts/tests/fixtures/node-types-check.ts @@ -0,0 +1,55 @@ +/** + * Type-only fixture for ksef-client-ts/node subpath (FLO-246). + * + * Compiled by `tsc --project tsconfig.node-check.json --noEmit` in the lint + * step. Proves that both node.d.ts (ESM import) and node.d.cts (CJS require) + * are wired correctly and that all moved symbols have the expected shapes. + * + * This file is NOT executed at runtime. + */ +import { + FileOfflineInvoiceStorage, + FileHwmStore, + validateAgainstXsd, + resolveXsdFor, + FA_XSD_PATHS, + PEF_XSD_PATHS, + libxmljsAvailable, + isMissingLibxmljsError, + type InvoiceSchemaId, + type ValidateAgainstXsdResult, +} from 'ksef-client-ts/node'; + +// FileOfflineInvoiceStorage is a class (constructable) +const _storage: InstanceType = new FileOfflineInvoiceStorage(); +void _storage; + +// FileHwmStore is a class (constructable) +const _hwm: InstanceType = new FileHwmStore('/tmp/test-hwm.json'); +void _hwm; + +// XSD path maps have string values +const _fa: string = FA_XSD_PATHS.FA2; +const _pef: string = PEF_XSD_PATHS.PEF; +void _fa; void _pef; + +// InvoiceSchemaId is a union of literal strings +const _schemaId: InvoiceSchemaId = 'FA3'; +void _schemaId; + +// validateAgainstXsd is synchronous → ValidateAgainstXsdResult +const _validateResult: ValidateAgainstXsdResult = validateAgainstXsd('', 'FA3'); +void _validateResult; + +// resolveXsdFor takes InvoiceSchemaId and returns string +const _xsdPath: string = resolveXsdFor('FA2'); +void _xsdPath; + +// libxmljsAvailable is a boolean constant +const _available: boolean = libxmljsAvailable; +void _available; + +// isMissingLibxmljsError is a predicate function +const _err = new Error('test'); +const _isMissing: boolean = isMissingLibxmljsError(_err); +void _isMissing; diff --git a/packages/ksef-client-ts/tests/unit/node-subpath.test.ts b/packages/ksef-client-ts/tests/unit/node-subpath.test.ts new file mode 100644 index 0000000..e13db92 --- /dev/null +++ b/packages/ksef-client-ts/tests/unit/node-subpath.test.ts @@ -0,0 +1,95 @@ +/** + * /node subpath resolution tests (FLO-246). + * + * Verifies that: + * 1. All moved symbols export correctly from `ksef-client-ts/node` (ESM + CJS). + * 2. The moved symbols are absent from the root `ksef-client-ts` entry. + */ +import { createRequire } from 'node:module'; +import { describe, it, expect } from 'vitest'; + +// ESM import of the /node subpath (exercises the "import" condition in exports map) +import { + FileOfflineInvoiceStorage, + FileHwmStore, + validateAgainstXsd, + resolveXsdFor, + FA_XSD_PATHS, + PEF_XSD_PATHS, + libxmljsAvailable, + isMissingLibxmljsError, +} from 'ksef-client-ts/node'; + +// CJS require of the /node subpath (exercises the "require" condition in exports map) +const nodeRequire = createRequire(import.meta.url); +const cjsNode = nodeRequire('ksef-client-ts/node') as Record; + +// Root barrel — moved symbols must be absent here +import * as rootExports from 'ksef-client-ts'; + +const MOVED_SYMBOLS = [ + 'FileOfflineInvoiceStorage', + 'FileHwmStore', + 'validateAgainstXsd', + 'resolveXsdFor', + 'FA_XSD_PATHS', + 'PEF_XSD_PATHS', + 'libxmljsAvailable', + 'isMissingLibxmljsError', +] as const; + +describe('ksef-client-ts/node subpath — ESM resolution', () => { + it('exports FileOfflineInvoiceStorage', () => { + expect(FileOfflineInvoiceStorage).toBeDefined(); + }); + + it('exports FileHwmStore', () => { + expect(FileHwmStore).toBeDefined(); + }); + + it('exports validateAgainstXsd', () => { + expect(validateAgainstXsd).toBeDefined(); + }); + + it('exports resolveXsdFor', () => { + expect(resolveXsdFor).toBeDefined(); + }); + + it('exports FA_XSD_PATHS', () => { + expect(FA_XSD_PATHS).toBeDefined(); + expect(FA_XSD_PATHS).toHaveProperty('FA2'); + expect(FA_XSD_PATHS).toHaveProperty('FA3'); + }); + + it('exports PEF_XSD_PATHS', () => { + expect(PEF_XSD_PATHS).toBeDefined(); + expect(PEF_XSD_PATHS).toHaveProperty('PEF'); + expect(PEF_XSD_PATHS).toHaveProperty('PEF_KOR'); + }); + + it('exports libxmljsAvailable', () => { + expect(libxmljsAvailable).toBeDefined(); + expect(typeof libxmljsAvailable).toBe('boolean'); + }); + + it('exports isMissingLibxmljsError', () => { + expect(isMissingLibxmljsError).toBeDefined(); + expect(typeof isMissingLibxmljsError).toBe('function'); + }); +}); + +describe('ksef-client-ts/node subpath — CJS resolution', () => { + for (const symbol of MOVED_SYMBOLS) { + it(`exports ${symbol}`, () => { + expect(cjsNode[symbol]).toBeDefined(); + }); + } +}); + +describe('ksef-client-ts root barrel — moved symbols absent', () => { + for (const symbol of MOVED_SYMBOLS) { + it(`does not export ${symbol}`, () => { + expect((rootExports as Record)[symbol]).toBeUndefined(); + }); + } +}); diff --git a/packages/ksef-client-ts/tests/unit/utils/fs-free-core.test.ts b/packages/ksef-client-ts/tests/unit/utils/fs-free-core.test.ts new file mode 100644 index 0000000..4a994b9 --- /dev/null +++ b/packages/ksef-client-ts/tests/unit/utils/fs-free-core.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; + +import { + checkFsFreeCore, + findFsMarkers, +} from "../../../scripts/check-fs-free-core.mjs"; + +describe("fs-free core bundle guard", () => { + it("rejects quote- and whitespace-varied fs markers", () => { + expect(findFsMarkers("require ( 'fs' );")).toContain('require("fs")'); + expect(findFsMarkers('import { readFile } from "node:fs/promises";')).toEqual( + expect.arrayContaining(["node:fs/promises", "from \"fs/promises\"", "readFile"]), + ); + }); + + it("keeps built core bundles fs-free and confirms the node bundle is discriminating", async () => { + await expect(checkFsFreeCore()).resolves.toBeUndefined(); + }); +}); diff --git a/packages/ksef-client-ts/tsconfig.node-check.json b/packages/ksef-client-ts/tsconfig.node-check.json new file mode 100644 index 0000000..184947e --- /dev/null +++ b/packages/ksef-client-ts/tsconfig.node-check.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "noEmit": true, + "module": "node16", + "moduleResolution": "node16" + }, + "include": ["tests/fixtures/node-types-check.ts"], + "exclude": [] +} diff --git a/packages/ksef-client-ts/tsup.config.ts b/packages/ksef-client-ts/tsup.config.ts index 0374602..03ec654 100644 --- a/packages/ksef-client-ts/tsup.config.ts +++ b/packages/ksef-client-ts/tsup.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from "tsup"; export default defineConfig([ { - entry: ["src/index.ts"], + entry: ["src/index.ts", "src/node.ts"], format: ["esm", "cjs"], dts: true, clean: true,