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
55 changes: 53 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ jobs:
strategy:
matrix:
node-version:
- 20.x
- 22.x
os:
- windows-latest
Expand All @@ -44,7 +43,7 @@ jobs:

- name: Archive npm failure logs
uses: actions/upload-artifact@v4
if: failure()
if: failure() && runner.os == 'Windows'
with:
name: npm-logs
path: C:\npm\cache\_logs\
Expand All @@ -60,6 +59,58 @@ jobs:
parallel: true
fail-on-error: false

e2e:
name: Browser E2E (Chromium)
runs-on: ubuntu-latest
needs:
- build

steps:
- name: git checkout
uses: actions/checkout@v4

- name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
node-version: 22.x
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Build node lib + UMD browser bundle
run: npm run build:dist

- name: Get Playwright version
id: playwright-version
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> $GITHUB_OUTPUT

- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}

- name: Install Playwright Chromium
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromium

- name: Install Playwright system deps only (cache hit)
if: steps.playwright-cache.outputs.cache-hit == 'true'
run: npx playwright install-deps chromium

- name: Run browser e2e tests
run: npx playwright test --config e2e/playwright.config.ts --reporter=list

- name: Upload Playwright report on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: e2e/playwright-report
retention-days: 7

notify:
needs:
- build
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
node_modules
coverage
dist
lib
umd
.DS_Store
scripts/external/tmp/
src/externalModels
Expand Down
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,47 @@ Note that this module is primarily intended for tool authors, or developers embe
npm install @accordproject/template-engine --save
```

Requires Node.js >= 22.

## Browser usage

The package is published with two entry points:

- `lib/index.js` — the Node.js build (`tsc` output, typed via `lib/index.d.ts`).
- `umd/template-engine.js` — a pre-built UMD browser bundle (referenced by the `browser`
field, so bundlers pick it up automatically for web builds).

The full author-and-run loop works **in the browser** — parse a template to TemplateMark,
type-check it, compile its embedded TypeScript logic to JavaScript, and evaluate it — which
is what powers the [Template Playground](https://playground.accordproject.org). For
convenience the bundle also re-exports `ModelManager` (concerto) and `TemplateMarkTransformer`
(markdown-template), so a single bundle can run the whole `model -> template -> agreement` flow
with one concerto instance:

```html
<script src="https://unpkg.com/@accordproject/template-engine/umd/template-engine.js"></script>
<script>
const { ModelManager, TemplateMarkTransformer, TemplateMarkInterpreter } = window['template-engine'];
const modelManager = new ModelManager();
modelManager.addCTOModel('namespace test@1.0.0\n@template\nconcept TemplateData { o String name }');
const templateMark = new TemplateMarkTransformer()
.fromMarkdownTemplate({ content: 'Hello {{name}}!' }, modelManager, 'contract');
const engine = new TemplateMarkInterpreter(modelManager, {});
const agreement = await engine.generate(templateMark, { $class: 'test@1.0.0.TemplateData', name: 'World' });
</script>
```

Notes on browser limitations:

- Logic that imports **arbitrary** 3rd-party packages must be shipped as a **compiled archive**
(JavaScript logic, dependencies already bundled — produced offline in Node.js). In the browser,
source archives (TypeScript logic) compile against a fixed set of supported runtime dependencies.
- `Template.fromDirectory()`/`fromUrl()` and child-process logic evaluation are Node-only; in the
browser load templates via `Template.fromArchive(buffer)` and use the default in-process evaluator.

The browser bundle is exercised by the Playwright tests in the `e2e/` workspace, which load
`umd/template-engine.js` into a headless Chromium and run a real `generate()`.

## License <a name="license"></a>
Accord Project source code files are made available under the Apache License, Version 2.0 (Apache-2.0), located in the LICENSE file. Accord Project documentation files are made available under the Creative Commons Attribution 4.0 International License (CC-BY-4.0), available at http://creativecommons.org/licenses/by/4.0/.

4 changes: 4 additions & 0 deletions e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
test-results/
playwright-report/
playwright/.cache/
15 changes: 15 additions & 0 deletions e2e/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "template-engine-e2e",
"version": "1.0.0",
"private": true,
"description": "End-to-end tests that load the template-engine UMD bundle into a real headless Chromium and exercise the public API.",
"scripts": {
"build:bundles": "npm --prefix .. run build:dist",
"pretest": "npm run build:bundles && npx --yes playwright install chromium",
"test": "playwright test"
},
"devDependencies": {
"@playwright/test": "^1.49.0",
"@types/node": "^20.11.30"
}
}
17 changes: 16 additions & 1 deletion src/markdown-transform.d.ts → e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,19 @@
* limitations under the License.
*/

declare module '@accordproject/markdown-transform';
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'list',
use: {
trace: 'on-first-retry',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
],
});
71 changes: 71 additions & 0 deletions e2e/tests/template-engine.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { test, expect, Page } from '@playwright/test';
import * as path from 'path';

const BUNDLE = path.resolve(__dirname, '../../umd/template-engine.js');

/**
* Loads about:blank and injects the UMD bundle, then waits for the global.
* @param page - the Playwright page
*/
async function inject(page: Page): Promise<void> {
await page.goto('about:blank');
await page.addScriptTag({ path: BUNDLE });
await page.waitForFunction(() => typeof (window as any)['template-engine'] !== 'undefined');
}

test.describe('@accordproject/template-engine UMD', () => {
test('exposes the public API on the global', async ({ page }) => {
await inject(page);
const api = await page.evaluate(() => {
const m = (window as any)['template-engine'];
return {
interpreter: typeof m.TemplateMarkInterpreter,
processor: typeof m.TemplateArchiveProcessor,
modelManager: typeof m.ModelManager,
transformer: typeof m.TemplateMarkTransformer,
};
});
expect(api.interpreter).toBe('function');
expect(api.processor).toBe('function');
expect(api.modelManager).toBe('function');
expect(api.transformer).toBe('function');
});

// Full browser flow: parse -> type-check -> compile the template's TypeScript logic to
// JS (twoslash, using the bundled TypeScript) -> evaluate, all in headless Chromium.
test('generates an agreement in the browser', async ({ page }) => {
await inject(page);
const json = await page.evaluate(async () => {
const { ModelManager, TemplateMarkTransformer, TemplateMarkInterpreter } =
(window as any)['template-engine'];
const MODEL = 'namespace test@1.0.0\n@template\nconcept TemplateData {\n o String name\n}';
const mm = new ModelManager();
mm.addCTOModel(MODEL, undefined, true);
const tmt = new TemplateMarkTransformer();
const templateMark = tmt.fromMarkdownTemplate(
{ content: 'Hello {{name}}!' }, mm, 'contract', { verbose: false });
const engine = new TemplateMarkInterpreter(mm, {});
const result = await engine.generate(
templateMark,
{ $class: 'test@1.0.0.TemplateData', name: 'World' },
{ now: '2023-03-17T00:00:00.000Z' });
return JSON.stringify(result.toJSON());
});
expect(json).toContain('Hello ');
expect(json).toContain('World');
});
});
8 changes: 8 additions & 0 deletions e2e/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": true,
"types": ["node"]
},
"include": ["tests/**/*.ts", "playwright.config.ts"]
}
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import js from "@eslint/js";
import tseslint from "typescript-eslint";

export default defineConfig([
globalIgnores(["dist/", "scripts/", "**/model-gen/**"]),
globalIgnores(["dist/", "lib/", "umd/", "coverage/", "scripts/", "**/model-gen/**", "**/node_modules/**", "e2e/", "webpack.config.js"]),
{ files: ["**/*.{js,mjs,cjs,ts}"] },
{
files: ["**/*.{js,mjs,cjs,ts}"],
Expand Down
Loading
Loading