Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,24 @@ jobs:
- name: Build
run: pnpm build

# Packaging breakage — an unresolvable subpath, a missing types target —
# is invisible to lint, typecheck, and the test suite, and only surfaces
# for a consumer after publish. attw resolves every entry point the way
# node10, node16, and bundlers would; publint checks the manifest itself.
#
# This step is the only thing that can verify the node10 rows: TypeScript 7
# removed `moduleResolution: node10` (TS5108), so the repo's own compiler
# cannot express the scenario `typesVersions` exists to serve. Dropping
# this step would make that block unverifiable in-repo.
#
# `cjs-resolves-to-esm` is ignored because it is inherent to an ESM-only
# package, not a defect — and it fired before the `default` conditions
# existed, so reverting those would not retire the flag. Do not swap it for
# `--profile esm-only`: that profile suppresses whole resolution modes,
# including node10, and would report green on unresolvable subpath types.
- name: Check package publishing metadata
run: pnpm check:package

pandoc-integration:
name: Pandoc Integration
runs-on: ubuntu-latest
Expand Down
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ to the release entry by hand. This project follows
- Add SharePoint transport
- Add GoogleDriveTransport for uploading docx as Google Docs
- Add docx-to-markdown reverse conversion (Tier-2) ([#16](https://github.com/agentic-tooling/polydoc-core/pull/16))
- Split the public surface into subpath exports ([#13](https://github.com/agentic-tooling/polydoc-core/issues/13))

### Miscellaneous

Expand All @@ -36,6 +37,28 @@ to the release entry by hand. This project follows

### Notes

- The transports are subpath-only. `SharePointTransport` is imported from
`@agentic-tooling/polydoc-core/sharepoint` and `GoogleDriveTransport` from
`@agentic-tooling/polydoc-core/google`; neither is re-exported from the root
entry point, types included. The root entry point loads no third-party SDK —
Node builtins plus `execa` and `fflate` — so a Markdown-to-DOCX consumer never
pays for a cloud SDK it will not call. Both transport entry points re-export
the shared contract (`Transport`, `TransportUploadResult`, `TransportError`,
`TransportErrorCode`, `DOCX_MIME_TYPE`) as the same bindings the root entry
point exposes.
- `instanceof TransportError` holds across entry points under Node's resolver,
`require()` included, because every entry point resolves to one
`transport.js` module instance. That is a property of module identity, not of
the package layout: a bundler that emits the root and a transport entry point
into separate bundles with no shared chunk produces two distinct classes and
`instanceof` then evaluates false. `TransportError.code` is the
resolution-independent discriminant. If CJS output is ever added, `transport.js`
must remain a single shared artifact rather than shipping alongside a
`transport.cjs`, or this becomes the dual-package hazard.
- The package ships only the entry points listed above plus `./package.json`.
Subpath types are declared through both `exports` and `typesVersions`, so they
resolve under `moduleResolution: "node"` as well as `node16`/`nodenext` and
bundler resolution. `attw` and `publint` run in CI to keep that true.
- A Pandoc 3.x binary must be on `PATH`. It is not bundled. `doctor()` probes
for it and reports actionable failures.
- `PandocErrorCode` and `TransportErrorCode` are open unions that grow as
Expand Down
78 changes: 65 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,58 @@ experience, remote service integrations, and sidecar storage stay outside this
repository. The library does include the shared transport contract and a local
file transport for consumers that need deterministic DOCX writes.

## Entry Points

The package is ESM-only and publishes three entry points. Cloud transports are
opt-in: importing the root entry point never loads a cloud SDK.

| Entry point | Contents | Third-party load |
| ------------------------------------------ | ------------------------------------------------------------------------------------- | ------------------------------------------- |
| `@agentic-tooling/polydoc-core` | Conversion core, the `Transport` contract, `TransportError`, and `LocalFileTransport` | `execa` and `fflate` only |
| `@agentic-tooling/polydoc-core/sharepoint` | `SharePointTransport` and its auth helpers | `@azure/msal-node` |
| `@agentic-tooling/polydoc-core/google` | `GoogleDriveTransport` and its Drive helpers | `@googleapis/drive`, loaded on first upload |

A consumer doing pure Markdown-to-DOCX conversion should never pay to load a
SharePoint or Drive SDK it will not call, so the transports live behind their
own entry points rather than in the root barrel. The difference is measurable
and enforced: importing the root entry point loads a small fraction of the
modules it did when the cloud transports were part of the barrel, and the test
suite fails if the root entry point regains an SDK dependency — including a
type-only one, and including one behind a lazy `import()`.

Both transport entry points re-export the shared contract — `Transport`,
`TransportUploadResult`, `TransportError`, `TransportErrorCode`, and
`DOCX_MIME_TYPE` — so code that imports one transport never has to reach back
into the root entry point for the error type it needs to catch. These are
re-exports of the same bindings rather than copies. Under Node's resolver every
entry point resolves to a single `transport.js` module instance, so
`instanceof TransportError` holds for a `SharePointTransportError` or
`GoogleDriveTransportError` no matter which entry point it was imported from —
`require()` included, on Node versions that can require ESM.

That guarantee comes from module identity, and a build step can break it. If a
bundler emits the root entry point and a transport entry point into separate
bundles with no shared chunk — two independent Rollup builds, or a framework
that splits server and client graphs — a consumer ends up with two distinct
`TransportError` classes and `instanceof` silently evaluates false, letting the
error escape the handler. Where that is a possibility, branch on the error code,
which is stable and resolution-independent:

```ts
try {
await transport.upload(canonicalId, docx);
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "SHAREPOINT_AUTH_FAILED") {
// Handle it without depending on which module instance built the error.
}

throw error;
}
```

## API

The package is ESM-only and exports the core Pandoc contract from
`@agentic-tooling/polydoc-core`.
The root entry point exports the core Pandoc contract.

```ts
import {
Expand Down Expand Up @@ -50,7 +98,10 @@ library.
## Transports

Transports are side-effecting adapters that publish DOCX bytes for a canonical
document ID and return a stable destination handle for later consumers.
document ID and return a stable destination handle for later consumers. The
contract and `LocalFileTransport` live in the root entry point; the two cloud
transports are imported from `@agentic-tooling/polydoc-core/sharepoint` and
`@agentic-tooling/polydoc-core/google`.

```ts
import {
Expand Down Expand Up @@ -101,10 +152,8 @@ Microsoft Graph app-only auth. It implements the same create-or-update
`Transport.upload(canonicalId, docx)` contract as `LocalFileTransport`.

```ts
import {
SharePointTransport,
convertMarkdownToDocx,
} from "@agentic-tooling/polydoc-core";
import { convertMarkdownToDocx } from "@agentic-tooling/polydoc-core";
import { SharePointTransport } from "@agentic-tooling/polydoc-core/sharepoint";

const docx = await convertMarkdownToDocx({
markdown,
Expand All @@ -129,8 +178,10 @@ console.log(destination.webUrl); // optional Graph driveItem webUrl
```

Auth uses `@azure/msal-node` with the Microsoft Graph scope
`https://graph.microsoft.com/.default`. `Sites.Selected` is the required Entra
application permission to admin-consent on the app registration, and a separate
`https://graph.microsoft.com/.default`. The SDK is a static import here, which is
why this is a separate entry point: importing `@agentic-tooling/polydoc-core`
loads no part of it. `Sites.Selected` is the required Entra application
permission to admin-consent on the app registration, and a separate
site-specific `write` grant must be provisioned out of band. The library does
not request `Sites.Selected` as an OAuth scope, does not require
tenant-wide `Sites.ReadWrite.All`, does not provision site grants, and does not
Expand Down Expand Up @@ -206,11 +257,11 @@ contract as the other transports.

```ts
import { OAuth2Client } from "google-auth-library";
import { convertMarkdownToDocx } from "@agentic-tooling/polydoc-core";
import {
GOOGLE_DRIVE_FILE_SCOPE,
GoogleDriveTransport,
convertMarkdownToDocx,
} from "@agentic-tooling/polydoc-core";
} from "@agentic-tooling/polydoc-core/google";

const docx = await convertMarkdownToDocx({
markdown,
Expand Down Expand Up @@ -256,8 +307,9 @@ one, is your responsibility. Consumers that use a service account, workload
identity federation, or their own token cache can pass any auth client the Drive
v3 client accepts.

The `@googleapis/drive` SDK is imported lazily on the first upload, so consumers
that only use another transport never pay to load it.
The `@googleapis/drive` SDK is imported lazily on the first upload, so even
importing this entry point costs nothing until a document is published.
Consumers that only use another transport never import it at all.

Uploads use the Drive v3 files resource:

Expand Down
33 changes: 29 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,34 +28,59 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
"import": "./dist/index.js",
"default": "./dist/index.js"
},
"./google": {
"types": "./dist/google.d.ts",
"import": "./dist/google.js",
"default": "./dist/google.js"
},
"./sharepoint": {
"types": "./dist/sharepoint.d.ts",
"import": "./dist/sharepoint.js",
"default": "./dist/sharepoint.js"
},
"./package.json": "./package.json"
},
"types": "./dist/index.d.ts",
"typesVersions": {
"*": {
"google": [
"./dist/google.d.ts"
],
"sharepoint": [
"./dist/sharepoint.d.ts"
]
}
},
"files": [
"dist",
"README.md",
"LICENSE"
],
"scripts": {
"build": "tsc -p tsconfig.build.json",
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"lint": "biome check .",
"format": "biome format --write .",
"format:check": "biome format .",
"check": "biome check . && tsc -p tsconfig.json --noEmit && vitest run && tsc -p tsconfig.build.json",
"check:package": "pnpm build && attw --pack . --ignore-rules cjs-resolves-to-esm && publint --strict",
"check": "biome check . && tsc -p tsconfig.json --noEmit && vitest run && pnpm check:package",
"changelog": "git cliff -o CHANGELOG.md",
"changelog:preview": "git cliff --unreleased",
"changelog:release": "git cliff --unreleased --prepend CHANGELOG.md",
"prepack": "pnpm build"
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.18.5",
"@biomejs/biome": "^2.5.5",
"@types/node": "^20.19.43",
"@vitest/coverage-v8": "^4.1.10",
"google-auth-library": "10.5.0",
"publint": "^0.3.22",
"typescript": "^7.0.2",
"vitest": "^4.1.10"
},
Expand Down
Loading