Skip to content
Open
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
31 changes: 16 additions & 15 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,24 @@ This repository is **Accord Project markdown-transform** — a TypeScript npm-wo
- Runtime: Node.js `>=22`
- Package manager: `npm` (workspace root + package-level scripts)
- Language: **TypeScript** (target `ES2020`, `module: commonjs`). Source lives in `packages/*/src/`; compiled `.js` + `.d.ts` are emitted to `packages/*/lib/`.
- Build: `tsc` per package (config extends `tsconfig.base.json`).
- Build: `tsc` per package (config extends `tsconfig.base.json`), then `scripts/build-esm.js` (esbuild) emits ES module builds to `lib/esm/` (Node, `import` condition) and `lib/esm-browser/` (bundlers targeting the web, `browser` condition).
- Linting: ESLint with `@typescript-eslint` (4-space indent, single quotes, semicolons).
- Unit testing: **Jest 29 + ts-jest** across every package. The legacy mocha+chai suites were removed during the TS migration.
- Browser E2E: **Playwright** under `e2e/` exercises the UMD bundles in headless Chromium.
- Bundling: `webpack 5` produces UMD bundles for `markdown-html`, `markdown-template`, `markdown-transform` (the three user-facing entry points). The other packages are CommonJS library deps consumed via bundlers.
- Browser E2E: **Playwright** under `e2e/` bundles the `lib/esm-browser` builds with esbuild and runs them in headless Chromium.
- No prebuilt browser bundles are published; browser consumers bundle the packages themselves. `scripts/smoke-esm.mjs` checks the ES module builds after every root `npm run build`.
- CI: GitHub Actions matrix on Ubuntu, macOS, and Windows for unit tests; Ubuntu-only for Playwright e2e.

## Repository layout

- `packages/` — eight publishable packages:
- `markdown-common`
- `markdown-cicero`
- `markdown-template` *(also UMD)*
- `markdown-html` *(also UMD)*
- `markdown-template`
- `markdown-html`
- `markdown-it-cicero`
- `markdown-it-template`
- `markdown-cli`
- `markdown-transform` *(umbrella, also UMD)*
- `markdown-cli` *(Node-only CLI, CommonJS only)*
- `markdown-transform` *(umbrella)*
- `e2e/` — browser end-to-end tests (Playwright). Not published.
- `scripts/` — repo-level utilities (model generation, version bumping, coverage aggregation).
- `tsconfig.base.json` — shared compiler options inherited by every package.
Expand Down Expand Up @@ -59,12 +59,12 @@ Concerto models for CommonMark/CiceroMark/TemplateMark are downloaded by `script

When changing code, run checks in this order:

1. `npm run build` — runs `tsc` per workspace (also rebuilds before tests via each package's `pretest`).
1. `npm run build` — runs `tsc` and the ES module builds per workspace, then the ESM smoke test (each package's `pretest` also rebuilds it).
2. `npm test` — runs the full Jest suite across every package.
3. `npm run -w markdown-transform-e2e test` — Playwright browser tests; only needed if you changed source that ends up in a UMD bundle.
3. `npm run -w markdown-transform-e2e test` — Playwright browser tests; needed if you changed source that runs in the browser, or the build.
4. `npm run coverage` — coverage aggregation (only if investigating coverage).

For package-level iteration, `cd packages/<name>` and run `npm run build`, `npm test`, etc. directly. For the umbrella package, also run `npm run webpack` after `npm run build` to refresh the UMD bundle.
For package-level iteration, `cd packages/<name>` and run `npm run build`, `npm test`, etc. directly.

When migrating Concerto: `@accordproject/concerto-core` is on **v4**. `new ModelManager({ strict: true })` is no longer valid — drop the option, don't cast to `any`. The model manager defaults are equivalent in v4.

Expand All @@ -91,12 +91,13 @@ These are based on merged PR review feedback in this repository:
- If bumping a shared dependency, align all affected package manifests and lockfiles in one change.

6. **Browser polyfills only when strictly needed**
- The webpack configs use `webpack.ProvidePlugin({ process: 'process/browser' })` and `resolve.alias = { jsdom: false }` to keep UMD bundles slim. Don't add Node polyfills unless a real test fails without them.
- The browser ES module build replaces Node builtins and `jsdom` with empty modules (`scripts/build-esm.js`) and must not reference `process`; the smoke test enforces both. Don't add Node polyfills unless a real test fails without them.

## Publishing & npm packages

- `package.json` `files` field for every publishable package is `["lib"]` (or `["lib", "umd"]` for the three UMD packages). `src/`, tests, snapshots, jest config, eslint config, and tsconfig stay out of the tarball.
- `main: "lib/index.js"`, `types: "lib/index.d.ts"`. The three UMD packages also set `browser: "umd/markdown-X.js"` so bundlers serving browser targets pick the UMD bundle automatically.
- `package.json` `files` field for every publishable package is `["lib"]`. `src/`, tests, snapshots, jest config, eslint config, and tsconfig stay out of the tarball.
- `main: "lib/index.js"`, `types: "lib/index.d.ts"`. The library packages also declare an `exports` map (`types` / `browser` / `import` / `require`) listing only `.` and `./package.json`; add a subpath explicitly if consumers need one — no `./lib/*` wildcard.
- Load dependencies with `import` statements, not `require()`: the ES module builds cannot follow a `require()` call, and the smoke test fails on one.
- Source maps (`*.js.map`) **are** shipped — keep `sourceMap: true` in `tsconfig.base.json` so consumer stack traces stay useful.

## AI review behavior (adapted from best-practice guidance)
Expand Down Expand Up @@ -129,7 +130,7 @@ Before proposing a PR-ready change:
- [ ] Change scope is minimal and focused
- [ ] New/updated behavior has tests (unit and, where relevant, Playwright e2e)
- [ ] Lint/build/tests pass
- [ ] `npm pack --dry-run` for any package whose contents changed shows only `lib/` (+ optional `umd/`) — no tests, snapshots, or configs leaking
- [ ] `npm pack --dry-run` for any package whose contents changed shows only `lib/` — no tests, snapshots, or configs leaking
- [ ] Dependency changes are justified and minimal
- [ ] No accidental downgrades or unnecessary added packages
- [ ] Commit(s) use DCO sign-off
Expand All @@ -138,7 +139,7 @@ Before proposing a PR-ready change:
## Common pitfalls in this repo

- Mixing `.js` and `.ts` in `src/` — the source tree is TypeScript only.
- Forgetting to rebuild UMD bundles (`npm run webpack -w …`) after source changes; the Playwright e2e tests will then test stale code.
- Forgetting to run `npm run build` after source changes; the Playwright e2e tests bundle `lib/esm-browser` and will then test stale code.
- Adding broad type tightening (`noImplicitAny`, `strict`) in unrelated files while fixing a small bug — out of scope, expand `any` only where the change is needed.
- Adding many dependency changes in one sweep without explaining each one.
- Switching from exact to ranged versions for core dependencies without team agreement.
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,6 @@ jobs:
- name: Build packages (dep order)
run: npm run build

- name: Build UMD bundles
run: npm run -w markdown-transform-e2e build:bundles

- name: Get Playwright version
id: playwright-version
run: |
Expand Down
65 changes: 65 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Migrating to markdown-transform 2.0

Version 2.0 changes how the packages in this repository are published. They now ship ES module builds selected through an `exports` map, and the prebuilt UMD browser bundles are gone. The aim is smaller browser bundles: a bundler now takes each package's browser build and shares one copy of every dependency with the rest of the application, where the UMD bundles each carried their own copy of Concerto and about 1 MB of Node polyfills.

**Who is affected:** if you import these packages by name, from Node or through a modern bundler, probably nobody:

```js
// CommonJS — unchanged
const { TemplateMarkTransformer } = require('@accordproject/markdown-template');

// ES modules — unchanged
import { transform } from '@accordproject/markdown-transform';
```

You are affected if you:

- load a UMD bundle (`umd/markdown-*.js`) with a `<script>` tag, or read `window['markdown-transform']` and similar globals;
- import a file inside a package, such as `@accordproject/markdown-common/lib/...`;
- build with a tool that does not read the `exports` field (webpack 4, browserify, Parcel 1);
- load the same package with both `import` and `require()` in one Node process.

These commands find the first two:

```bash
grep -rnE "@accordproject/markdown-[a-z-]+/(lib|umd)/" --include=*.js --include=*.ts --include=*.mjs --include=*.html .
grep -rnE "window\[['\"]markdown-(html|template|transform)['\"]\]" .
```

## Breaking changes

### The UMD bundles are removed

`@accordproject/markdown-html`, `@accordproject/markdown-template` and `@accordproject/markdown-transform` no longer publish `umd/markdown-*.js`, and the top-level `browser` field that pointed at them is gone.

- **Bundler users** (webpack 5, Vite, Rollup, esbuild): no change is needed. These bundlers read the `browser` condition of the `exports` map, which selects `lib/esm-browser`. That build already leaves out `jsdom` and Node builtins, so the `IgnorePlugin` for `jsdom`, `process` polyfills and `resolve.fallback` entries that the UMD era called for are no longer required by these packages.
- **`<script>` tag users:** add a bundler to your build and import the packages normally. `lib/esm-browser` is a module graph, not a single file, and it imports its dependencies by package name (`markdown-it`, `dayjs`, `@xmldom/xmldom`, `@accordproject/concerto-core`, ...). Some of those are CommonJS, so an import map alone cannot load the graph in a browser. A CDN that converts npm packages to browser-ready ES modules can, but check its behaviour before relying on it.
- **Tools that do not read `exports`** (webpack 4, browserify, Parcel 1): with the `browser` field gone, these fall back to `main`, the CommonJS build for Node. Bundling it pulls in `jsdom` through `markdown-html`, which is not built to run in a browser. Upgrade to a bundler that supports `exports`.

### Deep imports no longer resolve

Each package now declares an `exports` map exposing only the package root and `./package.json`. Importing a file inside a package, for example `@accordproject/markdown-template/lib/templatemarkutil`, fails with `ERR_PACKAGE_PATH_NOT_EXPORTED` in Node and an equivalent error in bundlers. Import from the package root instead; for example `templatemarkutil` is exported as `import { templatemarkutil } from '@accordproject/markdown-template'`. If something you need is not exported from the root, please open an issue.

### `import` and `require()` load different files

`require()` still loads the CommonJS build in `lib/`, but Node's `import` now loads the ES module build in `lib/esm/`. A process that reaches the same package both ways holds two copies of it, and an `instanceof` check fails on an object created by the other copy. Use one module system per package in a process. If a dependency of yours still uses `require()`, hand objects it created back to it rather than to your own `import`ed copy. If something fails in a way that looks impossible, run `npm ls @accordproject/markdown-template` (or the package concerned) and check that it is installed once.

### The `process` dependency is dropped

`markdown-html`, `markdown-template` and `markdown-transform` listed the `process` polyfill as a runtime dependency for the webpack builds. It is removed; if your application used it through these packages, add it to your own dependencies.

## Not breaking

- Imports from a package root, with `require()` or `import`.
- TypeScript types: `types` still points at `lib/index.d.ts`.
- The markdown-it plugins: `require('@accordproject/markdown-it-cicero')` still returns the plugin function, and `import plugin from '@accordproject/markdown-it-cicero'` gives it as the default export. The same holds for `markdown-it-template`.
- Formula names in TemplateMark. They are now computed with a pure JavaScript SHA-256 instead of Node's `crypto` module, and are identical.

## What you get

Bundling `{ TemplateMarkTransformer }` from `markdown-template` and `{ transform }` from `markdown-transform` for the browser with esbuild (`--bundle --minify`), Concerto included:

| | Minified | Gzipped |
|---|---:|---:|
| 1.1.0, through the UMD bundles | 4,571 KB | 1,341 KB |
| 2.0, through `lib/esm-browser` | 689 KB | 192 KB |
21 changes: 9 additions & 12 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,36 @@
# Browser End-to-End Tests

[Playwright](https://playwright.dev) tests that load the UMD bundles for `markdown-html`, `markdown-template` and `markdown-transform` into a real headless Chromium and call the public API. These tests exist to catch packaging/bundling regressions that unit tests miss — for example, accidentally pulling Node-only modules like `jsdom` into the browser bundle.
[Playwright](https://playwright.dev) tests that run the browser builds in a real headless Chromium and call the public API. These tests exist to catch packaging/bundling regressions that unit tests miss — for example, accidentally pulling Node-only modules like `jsdom` into a browser bundle.

Each package publishes an ES module build in `lib/esm-browser`, which bundlers select through the `browser` export condition. `esm-browser.spec.ts` bundles those builds with esbuild, as an application's bundler would, and loads the result into the page.

## Run

From the repository root:

```bash
npm install --workspaces
npm run build
npm run -w markdown-transform-e2e test
```

`npm test` from the e2e directory runs `pretest` first, which:
1. Builds each TS package (`tsc`)
2. Builds each UMD bundle (`webpack`)
3. Installs the Chromium browser used by Playwright (cached after first run)
`npm run build` compiles each package and emits its ES module builds. `npm test` from the e2e directory then runs `pretest`, which installs the Chromium browser used by Playwright (cached after first run).

## What's covered

| Spec | Asserts |
|------|---------|
| `markdown-html.spec.ts` | `HtmlTransformer` exported on the global; `toHtml`/`toCiceroMark` work using the native `DOMParser` (jsdom is **not** in the browser bundle) |
| `markdown-template.spec.ts` | `TemplateMarkTransformer` exported; `toTokens` and `normalizeNLs` work |
| `markdown-transform.spec.ts` | `transform`, `formatDescriptor`, `generateTransformationDiagram`, `TransformEngine` exported; markdown → commonmark and markdown → html transformations succeed |
| `esm-browser.spec.ts` | every package resolves to `lib/esm-browser`; jsdom and the crypto polyfills stay out of the bundle; the `markdown-transform` API is exported; markdown → commonmark and markdown → html succeed; `toHtml`, and `toCiceroMark` with the native `DOMParser`, work; a template with a formula parses (with a name matching Node's SHA-256); `toTokens` and `normalizeNLs` work |

## Adding a test

Each UMD bundle exports its API onto `window['<package-name>']` (e.g. `window['markdown-html']`). Spec pattern:
The bundle built in `beforeAll` exposes the exports of its entry module on `window.markdownTransform`. To test another API, export it from `ENTRY` in `esm-browser.spec.ts`, then:

```ts
await page.setContent('<!doctype html><html><body></body></html>');
await page.addScriptTag({ path: path.resolve(__dirname, '../../packages/<pkg>/umd/<pkg>.js') });
await loadBundle(page);

const result = await page.evaluate(() => {
const { Something } = (window as any)['<pkg>'];
const { Something } = (window as any).markdownTransform;
return new Something().doStuff();
});

Expand Down
5 changes: 2 additions & 3 deletions e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@
"name": "markdown-transform-e2e",
"version": "1.1.0",
"private": true,
"description": "End-to-end tests that load the UMD browser bundles into a real headless Chromium and exercise the public API.",
"description": "End-to-end tests that bundle the browser ES module builds and exercise the public API in a real headless Chromium.",
"scripts": {
"build:bundles": "npm run -w @accordproject/markdown-html webpack && npm run -w @accordproject/markdown-template webpack && npm run -w @accordproject/markdown-transform webpack",
"pretest": "npm run build:bundles && npx --yes playwright install chromium",
"pretest": "npx --yes playwright install chromium",
"test": "playwright test"
},
"devDependencies": {
Expand Down
Loading