@twip/web 0.0.0-canary-05bfdb9 → 0.0.0-canary-7dfb59b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -32,4 +32,28 @@ await editor.destroy();
32
32
 
33
33
  In Node, workers and tests, `HeadlessEditor.create()` gives the same API with no DOM.
34
34
 
35
+ ## Serving the engine's files yourself
36
+
37
+ The engine runs from files in the package's `dist/`: `worker.js`, `twip-wasm.js`, `twip_wasm.wasm`, and the
38
+ `transport/` and `worker/` folders the worker imports. By default the SDK finds them beside its own code, through
39
+ `new URL(..., import.meta.url)`. If your bundler doesn't follow those (Angular's esbuild builder doesn't), copy
40
+ `dist/` as it is to a folder you serve, and say where, once, before the first `FontLibrary` call or editor:
41
+
42
+ ```ts
43
+ import { configure } from '@twip/web';
44
+
45
+ configure({ assets: new URL('twip/', document.baseURI) }); // or a CDN: 'https://cdn.example.com/twip/'
46
+ ```
47
+
48
+ `assets` may be on another origin. The worker then starts from a same-origin `blob:` module that imports
49
+ `worker.js`, so the other origin must:
50
+
51
+ - send `Access-Control-Allow-Origin` (your page's origin, or `*`) on every file in the folder, as module scripts and
52
+ the WebAssembly are fetched with CORS;
53
+ - serve `twip_wasm.wasm` as `application/wasm`, or the engine compiles it after a full download instead of while
54
+ streaming.
55
+
56
+ With a Content Security Policy, allow `blob:` in `worker-src`, the other origin in `script-src` and `connect-src`, and
57
+ `'wasm-unsafe-eval'` in `script-src` (which the engine needs on any origin).
58
+
35
59
  Guides: https://jaxcksn.github.io/twip/docs/guides/web. Licensed under Apache-2.0.
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * @packageDocumentation
6
6
  */
7
7
  export { Editor, HeadlessEditor } from './editor.js';
8
+ export { configure } from './transport/module.js';
8
9
  export type { ExportOptions, ExportResult, MountOptions, PdfOptions, SessionOptions } from './editor.js';
9
10
  export type { CommandEntry, CommandId, CommandInfo, Commands, FieldTargets, NoArgumentCommandId, Op, OpArgs, PlaceholderAction, PlaceholderLook, } from './commands.js';
10
11
  export type { AutoCorrectChange, AutoCorrectEntry, AutoCorrectOptions, CharacterFormatting, CommandState, CommandValues, EditingTarget, EditorState, ElementContext, HighlightColor, HorizontalPosition, HorizontalReference, LineSpacing, MarkupView, Mixed, ObjectHorizontalAlignment, ObjectPlacement, ObjectSelection, ObjectVerticalAlignment, ObjectWrap, PageFormatting, ParagraphFormatting, ReviewState, RowHeight, SelectionSummary, TablePosition, TableSelection, TableStyleOptions, TableWidth, TextDistance, Tri, UnderlineStyle, VerticalPosition, VerticalReference, ViewState, WrapSide, } from './state.js';
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * @packageDocumentation
6
6
  */
7
7
  export { Editor, HeadlessEditor } from './editor.js';
8
+ export { configure } from './transport/module.js';
8
9
  export { EditorError } from './errors.js';
9
10
  export { FontLibrary } from './fonts/library.js';
10
11
  export { cm, inches, mm, pt } from './types.js';
@@ -47,6 +47,22 @@ export interface TwipModule {
47
47
  deleteContext(handle: number): void;
48
48
  };
49
49
  }
50
+ /**
51
+ * Sets where the package's runtime files (dist/: worker.js, twip-wasm.js, twip_wasm.wasm, transport/ and worker/) are
52
+ * served from, for an application whose bundler doesn't follow `new URL(..., import.meta.url)`. Call it once, before
53
+ * anything starts the engine: the first {@link FontLibrary} call, {@link Editor.mount} or
54
+ * {@link HeadlessEditor.create}. The engine is shared by every session, so it's not a per-session option.
55
+ *
56
+ * `assets` is the folder, resolved against the document's base URL. It may be on another origin (a CDN), which must
57
+ * then send CORS headers for every file; see the package README.
58
+ *
59
+ * @throws {@link EditorError} `not-allowed` when the engine has already loaded from another folder.
60
+ */
61
+ export declare function configure(options: {
62
+ assets: string | URL;
63
+ }): void;
64
+ /** The configured assets folder, if any, read to start the engine: it can't change after this. */
65
+ export declare function useAssets(): URL | undefined;
50
66
  /** Instantiates the engine module, with `dispatch` registered before any engine call can call back. */
51
67
  export declare function loadModule(dispatch: NonNullable<TwipModule['twipDispatch']>): Promise<TwipModule>;
52
68
  export {};
@@ -1,15 +1,49 @@
1
1
  // Loading the engine module: the one place that knows where the WebAssembly build is and what it exports.
2
2
  // The build copies the emscripten output (twip-wasm.js and twip_wasm.wasm) next to dist/index.js.
3
+ import { EditorError } from '../errors.js';
3
4
  /**
4
- * Where the engine module and its WebAssembly are, relative to this file in dist/transport/. Both are literal
5
- * `new URL(..., import.meta.url)`s, so a bundler copies both files into the application's build.
5
+ * Where the engine module and its WebAssembly are by default: relative to this file in dist/transport/. Both are
6
+ * literal `new URL(..., import.meta.url)`s, so a bundler that follows them copies both files into the application's
7
+ * build. {@link configure}'s `assets` replaces them.
6
8
  */
7
- const moduleUrl = new URL('../twip-wasm.js', import.meta.url);
8
- const wasmUrl = new URL('../twip_wasm.wasm', import.meta.url);
9
+ const defaultModuleUrl = new URL('../twip-wasm.js', import.meta.url);
10
+ const defaultWasmUrl = new URL('../twip_wasm.wasm', import.meta.url);
11
+ /** The folder of the package's dist/ files, when the application set one. */
12
+ let assets;
13
+ /** Whether this thread has read `assets` to start the engine: it can't change after. */
14
+ let used = false;
15
+ /**
16
+ * Sets where the package's runtime files (dist/: worker.js, twip-wasm.js, twip_wasm.wasm, transport/ and worker/) are
17
+ * served from, for an application whose bundler doesn't follow `new URL(..., import.meta.url)`. Call it once, before
18
+ * anything starts the engine: the first {@link FontLibrary} call, {@link Editor.mount} or
19
+ * {@link HeadlessEditor.create}. The engine is shared by every session, so it's not a per-session option.
20
+ *
21
+ * `assets` is the folder, resolved against the document's base URL. It may be on another origin (a CDN), which must
22
+ * then send CORS headers for every file; see the package README.
23
+ *
24
+ * @throws {@link EditorError} `not-allowed` when the engine has already loaded from another folder.
25
+ */
26
+ export function configure(options) {
27
+ const url = new URL(options.assets, globalThis.document?.baseURI ?? globalThis.location?.href);
28
+ if (!url.pathname.endsWith('/'))
29
+ url.pathname += '/';
30
+ if (used && url.href !== assets?.href) {
31
+ throw new EditorError('not-allowed', 'configure() must be called before the first FontLibrary call or editor: the engine has already loaded');
32
+ }
33
+ assets = url;
34
+ }
35
+ /** The configured assets folder, if any, read to start the engine: it can't change after this. */
36
+ export function useAssets() {
37
+ used = true;
38
+ return assets;
39
+ }
9
40
  /** Instantiates the engine module, with `dispatch` registered before any engine call can call back. */
10
41
  export async function loadModule(dispatch) {
42
+ const assets = useAssets();
43
+ const moduleUrl = assets ? new URL('twip-wasm.js', assets) : defaultModuleUrl;
44
+ const wasmUrl = assets ? new URL('twip_wasm.wasm', assets) : defaultWasmUrl;
11
45
  const { default: create } = (await import(moduleUrl.href));
12
- // In Node the module finds its file itself; in a browser it's wherever the bundler put it.
46
+ // In Node the module finds its file itself; in a browser it's wherever the bundler, or `assets`, put it.
13
47
  const locateFile = moduleUrl.protocol === 'file:' ? undefined : (file) => (file.endsWith('.wasm') ? wasmUrl : new URL(file, moduleUrl)).href;
14
48
  const module = await create({ twipDispatch: dispatch, locateFile });
15
49
  module.twipDispatch = dispatch;
@@ -5,6 +5,7 @@
5
5
  // cache before any event of the same drain.
6
6
  import { EditorError, unsupported } from '../errors.js';
7
7
  import { live } from '../host/dom.js';
8
+ import { useAssets } from './module.js';
8
9
  import { Method, toError } from './protocol.js';
9
10
  let modules;
10
11
  /** Whether this page can run the engine in a worker: module workers and `transferControlToOffscreen`. */
@@ -43,6 +44,26 @@ export function chooseExecution(execution = 'auto') {
43
44
  throw unsupported("execution: 'worker' on this browser");
44
45
  return 'main';
45
46
  }
47
+ /**
48
+ * Starts the engine worker. With no `assets`, it's beside this file's directory, in dist/: written out in one
49
+ * expression, as bundlers recognise it and build the worker and its imports into the application. With `assets`, it's
50
+ * there, told the folder in its URL's query (worker.ts); on another origin, through a same-origin module that imports
51
+ * it, as a worker's script must come from the page's origin.
52
+ */
53
+ function startWorker(assets) {
54
+ if (!assets)
55
+ return new Worker(new URL('../worker.js', import.meta.url), { type: 'module', name: 'twip-engine' });
56
+ const url = new URL(`worker.js?assets=${encodeURIComponent(assets.href)}`, assets);
57
+ if (url.origin === location.origin)
58
+ return new Worker(url, { type: 'module', name: 'twip-engine' });
59
+ const bootstrap = URL.createObjectURL(new Blob([`import ${JSON.stringify(url.href)};`], { type: 'text/javascript' }));
60
+ const worker = new Worker(bootstrap, { type: 'module', name: 'twip-engine' });
61
+ // Fetched by the time the worker answers, or fails.
62
+ const revoke = () => URL.revokeObjectURL(bootstrap);
63
+ worker.addEventListener('message', revoke, { once: true });
64
+ worker.addEventListener('error', revoke, { once: true });
65
+ return worker;
66
+ }
46
67
  let shared;
47
68
  /** The page's one worker transport. */
48
69
  export function workerTransport() {
@@ -139,9 +160,7 @@ export class WorkerTransport {
139
160
  #start() {
140
161
  if (this.#worker)
141
162
  return this.#worker;
142
- // The worker is beside this file's directory, in dist/. Written out in one expression, as bundlers recognise
143
- // it: they build the worker and its imports into the application.
144
- const worker = new Worker(new URL('../worker.js', import.meta.url), { type: 'module', name: 'twip-engine' });
163
+ const worker = startWorker(useAssets());
145
164
  live.workers++;
146
165
  worker.onmessage = ({ data }) => this.#receive(data);
147
166
  worker.onerror = (e) => {
Binary file
@@ -6,7 +6,6 @@
6
6
  import { EditorError } from '../errors.js';
7
7
  import { directTransport } from '../transport/direct.js';
8
8
  import { fromError } from '../transport/protocol.js';
9
- const engine = directTransport();
10
9
  const signals = new Map();
11
10
  /** Posts to the page, moving the buffer of exported bytes instead of copying it. */
12
11
  function post(message) {
@@ -35,8 +34,16 @@ function run(t, { id, op, args }) {
35
34
  return t[op].apply(t, args);
36
35
  }
37
36
  }
38
- // Session 0 is the engine's own: font requests.
39
- engine.then((t) => t.listen(0, post), () => { });
37
+ let started;
38
+ /** The engine, loaded on the first request: by then worker.ts has applied the page's `assets`. */
39
+ function engine() {
40
+ if (!started) {
41
+ started = directTransport();
42
+ // Session 0 is the engine's own: font requests.
43
+ started.then((t) => t.listen(0, post), () => { });
44
+ }
45
+ return started;
46
+ }
40
47
  onmessage = ({ data }) => {
41
48
  if (data.op === 'abort') {
42
49
  signals.get(data.id)?.abort();
@@ -44,7 +51,7 @@ onmessage = ({ data }) => {
44
51
  }
45
52
  // Every request waits on the same promise, so they reach the engine's queue in arrival order. Id 0 is a replayed
46
53
  // library change: nothing waits for its reply.
47
- engine.then((t) => run(t, data)).then((ok) => data.id && post({ id: data.id, ok }), (e) => {
54
+ engine().then((t) => run(t, data)).then((ok) => data.id && post({ id: data.id, ok }), (e) => {
48
55
  if (!data.id)
49
56
  return console.error(e);
50
57
  const error = e instanceof EditorError ? e : new EditorError('internal', String(e), { cause: e });
package/dist/worker.js CHANGED
@@ -1,2 +1,8 @@
1
1
  // The page's engine worker entry (`@twip/web/worker.js`). The SDK starts it itself; applications never load it.
2
2
  import './worker/entry.js';
3
+ import { configure } from './transport/module.js';
4
+ // The page's `assets` (configure()), in the query it started this worker with: the engine loads from there, not from
5
+ // this file's own URL. Applied before the first request, which loads the engine (worker/entry.ts).
6
+ const assets = new URL(import.meta.url).searchParams.get('assets');
7
+ if (assets)
8
+ configure({ assets });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twip/web",
3
- "version": "0.0.0-canary-05bfdb9",
3
+ "version": "0.0.0-canary-7dfb59b",
4
4
  "description": "The Twip DOCX editor for the browser and Node: a managed, framework-independent editor over the WebAssembly engine.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",