ntk 6.4.0 → 6.6.0

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/lib/app.js CHANGED
@@ -146,6 +146,26 @@ export default class App {
146
146
  return chooseGLXConfig(this, spec);
147
147
  }
148
148
 
149
+ /**
150
+ * Find a 32-bit TrueColor visual with an alpha channel, for per-pixel
151
+ * transparent windows (ARGB). Returns `{ visual, depth: 32 }` — pass
152
+ * these to `createWindow()` together with `backgroundPixel: 0`. Returns
153
+ * `null` when the server has no such visual (XQuartz, for example).
154
+ *
155
+ * The compositor blends the window's alpha channel automatically; no
156
+ * Composite extension calls or EWMH properties are needed from the client.
157
+ * Without a running compositor, transparent regions render black.
158
+ */
159
+ findArgbVisual(screen = 0) {
160
+ const depths = this.display.screen[screen].depths;
161
+ const visuals = depths?.[32];
162
+ if (!visuals) return null;
163
+ for (const [id, visual] of Object.entries(visuals)) {
164
+ if (visual.class === 4) return { visual: Number(id), depth: 32 };
165
+ }
166
+ return null;
167
+ }
168
+
149
169
  rootWindow(screen = 0) {
150
170
  return new Window(this, { id: this.display.screen[screen].root });
151
171
  }
package/lib/builtin.js ADDED
@@ -0,0 +1,47 @@
1
+ // Reach a Node builtin from ESM across Node versions.
2
+ //
3
+ // `process.getBuiltinModule(id)` is the browser-safe accessor — the string is
4
+ // a function argument, so no bundler treats it as a module specifier — but it
5
+ // only exists on Node >= 20.16. ntk's floor is lower (Node 18.19, for
6
+ // react-x11), and there is no browser-safe, synchronous way to reach a builtin
7
+ // from ESM on an older Node: `getBuiltinModule` is the API that was added to
8
+ // fill exactly that gap. So where it is absent we fall back to `createRequire`.
9
+ //
10
+ // The `node:module` import below is a real specifier and the one place in lib/
11
+ // allowed to statically import a builtin other than node:events — a sanctioned
12
+ // exception in test/packaging.test.js, scoped to this file. A *browser* bundle
13
+ // of the text stack must stub this one specifier, but the fallback that uses it
14
+ // never runs in a browser (guarded on process.versions.node) and ntk ships no
15
+ // browser build. It is not a top-level await, so the CJS/SEA bundle path (see
16
+ // docs/packaging.md) is unaffected.
17
+ import { createRequire } from 'node:module';
18
+
19
+ let _require;
20
+
21
+ // One createRequire bound to ntk, made lazily. Builtins ('node:*') resolve
22
+ // regardless of the base URL, so this also serves callers that resolve a
23
+ // package in ntk's own dependency graph (e.g. KaTeX's font directory).
24
+ function req() {
25
+ return (_require ??= createRequire(import.meta.url));
26
+ }
27
+
28
+ /**
29
+ * `process.getBuiltinModule(id)` with a Node-18 fallback.
30
+ * @param {string} id e.g. 'node:child_process'
31
+ * @returns the builtin module, or `undefined` outside Node (a browser
32
+ * embedding of the text stack — supply a custom FontSource / callback there).
33
+ */
34
+ export function builtin(id) {
35
+ const p = globalThis.process;
36
+ if (!p?.versions?.node) return undefined;
37
+ if (typeof p.getBuiltinModule === 'function') return p.getBuiltinModule(id);
38
+ return req()(id);
39
+ }
40
+
41
+ /**
42
+ * A `require` for resolving packages in ntk's dependency graph.
43
+ * @returns {NodeRequire|null} `null` outside Node.
44
+ */
45
+ export function nodeRequire() {
46
+ return globalThis.process?.versions?.node ? req() : null;
47
+ }
package/lib/fontconfig.js CHANGED
@@ -1,8 +1,10 @@
1
- // node:child_process is fetched lazily (not via static import) so that
2
- // browser bundles of the package never try to resolve it; in non-node
1
+ // node:child_process is fetched lazily (via builtin(), not a static import) so
2
+ // that browser bundles of the package never try to resolve it; in non-node
3
3
  // environments use a custom FontSource instead (see text/fontsource.js).
4
+ import { builtin } from './builtin.js';
5
+
4
6
  function childProcess() {
5
- return globalThis.process?.getBuiltinModule?.('node:child_process');
7
+ return builtin('node:child_process');
6
8
  }
7
9
 
8
10
  function execFileSync(...args) {
package/lib/image.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import jpeg from 'jpeg-js';
2
2
  import { PNG } from 'pngjs';
3
3
 
4
+ import { builtin } from './builtin.js';
4
5
  import { fromStraightRgba, pixelLayout } from './imagedata.js';
5
6
  import Picture from './picture.js';
6
7
 
@@ -126,7 +127,7 @@ export function decodeImage(buffer) {
126
127
  export async function loadImage(source) {
127
128
  if (typeof source === 'string' || source instanceof URL) {
128
129
  // lazy builtin lookup: browser bundles must not depend on node:fs
129
- const fsp = globalThis.process?.getBuiltinModule?.('node:fs/promises');
130
+ const fsp = builtin('node:fs/promises');
130
131
  if (!fsp) {
131
132
  throw new Error('loadImage: file paths need node — pass the encoded bytes instead');
132
133
  }
@@ -284,6 +284,9 @@ class RenderingContext2d {
284
284
  : depth === 8
285
285
  ? this.Render.a8
286
286
  : this.Render.rgb24;
287
+ // Does the target have a real alpha channel? Only then is "transparent"
288
+ // a colour it can hold, which is what clearRect turns on.
289
+ this._hasAlpha = depth === 32;
287
290
  this.picture = new Picture(this.window.app, {
288
291
  drawable: target,
289
292
  format,
@@ -325,14 +328,14 @@ class RenderingContext2d {
325
328
 
326
329
  this._clips = [];
327
330
  this._dropMasks();
328
- for (const gc of [this._gc, this._fillMaskGC]) {
331
+ for (const gc of [this._gc, this._fillMaskGC, this._clipMaskGC]) {
329
332
  if (!gc) continue;
330
333
  safeRelease(this.X, () => {
331
334
  this.X.FreeGC(gc);
332
335
  this.X.ReleaseID(gc);
333
336
  });
334
337
  }
335
- this._gc = this._fillMaskGC = null;
338
+ this._gc = this._fillMaskGC = this._clipMaskGC = null;
336
339
  this._gcs.length = 0;
337
340
 
338
341
  this._backgroundPicture = this._glyphSource = null;
@@ -1363,12 +1366,19 @@ class RenderingContext2d {
1363
1366
  // ------------------------------------------------------------------
1364
1367
  // drawing
1365
1368
 
1369
+ // Clearing means "back to nothing", and what nothing looks like depends on
1370
+ // whether the target can hold transparency. A depth-32 ARGB window gets
1371
+ // transparent black, the canvas spec's answer, and the compositor shows
1372
+ // whatever is behind it. Anything else has no alpha channel to write, so
1373
+ // clearing stays opaque white — the paper an opaque window starts from,
1374
+ // and what every caller predating ARGB windows expects.
1366
1375
  clearRect(x, y, w, h) {
1376
+ const alpha = this._hasAlpha;
1367
1377
  if (matIsIdentity(this._m) && !this._clips.length) {
1368
1378
  this.Render.FillRectangles(
1369
1379
  this.Render.PictOp.Src,
1370
1380
  this.picture.id,
1371
- [1, 1, 1, 1],
1381
+ alpha ? [0, 0, 0, 0] : [1, 1, 1, 1],
1372
1382
  [x, y, w, h],
1373
1383
  );
1374
1384
  this._markDirty();
@@ -1376,11 +1386,18 @@ class RenderingContext2d {
1376
1386
  }
1377
1387
  const tmp = new Path2D();
1378
1388
  tmp.rect(x, y, w, h);
1379
- // clear = opaque white, ignoring alpha and the composite op (but
1380
- // honoring the clip), matching the identity fast path above
1389
+ // Transformed or clipped, so the rect is a polygon and the erase has to
1390
+ // run through the coverage mask. OutReverse is `dst OUT src`: against an
1391
+ // opaque source it scales the destination by 1 - coverage, erasing to
1392
+ // transparent with an antialiased edge. PictOpSrc would take the whole
1393
+ // bounding box with it, coverage or not. Opaque targets keep the old
1394
+ // behaviour: paint white over the shape, ignoring alpha and the
1395
+ // composite op but honoring the clip, matching the fast path above.
1381
1396
  this._fillPolys(flattenPath(tmp._cmds, this._m), "nonzero", {
1382
- src: this.createSolidPicture(1, 1, 1, 1),
1383
- op: this.Render.PictOp.Over,
1397
+ src: alpha
1398
+ ? this.createSolidPicture(0, 0, 0, 1)
1399
+ : this.createSolidPicture(1, 1, 1, 1),
1400
+ op: alpha ? this.Render.PictOp.OutReverse : this.Render.PictOp.Over,
1384
1401
  alpha: 1,
1385
1402
  });
1386
1403
  }
@@ -1644,13 +1661,15 @@ class RenderingContext2d {
1644
1661
  drawable: tmpPixmap,
1645
1662
  format: R.a8,
1646
1663
  });
1647
- R.FillRectangles(
1648
- R.PictOp.Src,
1649
- tmpMask.id,
1650
- [0, 0, 0, 0],
1651
- [0, 0, bb.w, bb.h],
1652
- );
1653
- this._rasterizePolys(tmpMask, entry.polys, entry.rule, -bb.x, -bb.y);
1664
+ if (!this._uploadClipCoverage(tmpPixmap, entry, bb)) {
1665
+ R.FillRectangles(
1666
+ R.PictOp.Src,
1667
+ tmpMask.id,
1668
+ [0, 0, 0, 0],
1669
+ [0, 0, bb.w, bb.h],
1670
+ );
1671
+ this._rasterizePolys(tmpMask, entry.polys, entry.rule, -bb.x, -bb.y);
1672
+ }
1654
1673
  R.Composite(
1655
1674
  op,
1656
1675
  tmpMask.id,
@@ -1669,6 +1688,96 @@ class RenderingContext2d {
1669
1688
  tmpPixmap.destroy();
1670
1689
  }
1671
1690
 
1691
+ /**
1692
+ * The clip-mask twin of `_uploadCoverage`: rasterize one clip entry here
1693
+ * and PutImage the coverage into the bbox temp, instead of asking the
1694
+ * server for trapezoids. Returns false — caller falls back to AddTraps —
1695
+ * with no rasterizer, when the policy routes this shape to the server, or
1696
+ * when the rasterizer declines.
1697
+ *
1698
+ * Without this the routing was only half applied. `_uploadCoverage` covers
1699
+ * fills and strokes, so an app that set `rasterPolicy` to keep every
1700
+ * drawing local still emitted an AddTraps per non-rectangular clip, and on
1701
+ * glamor those were the whole remaining cost: a wall of 48 rounded cards
1702
+ * spent 12 AddTraps a frame on clips alone (react-x11#199), 116ms of
1703
+ * server drain per frame, with no policy able to reach them.
1704
+ *
1705
+ * PutImage writes Src, so it replaces the temp's clear as well.
1706
+ */
1707
+ _uploadClipCoverage(tmpPixmap, entry, bb) {
1708
+ const rasterizer = this.window.app.rasterizer;
1709
+ if (!rasterizer) return false;
1710
+ const flat = [];
1711
+ let edges = 0;
1712
+ for (const p of entry.polys) {
1713
+ if (p.pts.length < 6) continue;
1714
+ flat.push(p.pts);
1715
+ edges += p.pts.length / 2;
1716
+ }
1717
+ if (!flat.length) return false;
1718
+ if (
1719
+ routeRaster(bb.w, bb.h, edges, this.window.app.rasterPolicy) !== "local"
1720
+ ) {
1721
+ return false;
1722
+ }
1723
+
1724
+ const coverage = rasterizer.rasterize({
1725
+ polys: flat,
1726
+ rule: entry.rule,
1727
+ dx: -bb.x,
1728
+ dy: -bb.y,
1729
+ width: bb.w,
1730
+ height: bb.h,
1731
+ });
1732
+ if (!coverage) return false;
1733
+
1734
+ // scanlines padded to 4 bytes, as in _uploadCoverage — and as there, a
1735
+ // width that is already a multiple of 4 goes out as a view over the
1736
+ // rasterizer's own bytes with no copy
1737
+ const stride = (bb.w + 3) & ~3;
1738
+ let data;
1739
+ if (stride === bb.w) {
1740
+ data = Buffer.isBuffer(coverage)
1741
+ ? coverage
1742
+ : Buffer.from(coverage.buffer, coverage.byteOffset, coverage.length);
1743
+ } else {
1744
+ data = Buffer.alloc(stride * bb.h);
1745
+ for (let y = 0; y < bb.h; ++y) {
1746
+ Buffer.from(coverage.buffer, coverage.byteOffset + y * bb.w, bb.w).copy(
1747
+ data,
1748
+ y * stride,
1749
+ );
1750
+ }
1751
+ }
1752
+ this.X.PutImage(
1753
+ 2,
1754
+ tmpPixmap.id,
1755
+ this._clipGC(tmpPixmap),
1756
+ bb.w,
1757
+ bb.h,
1758
+ 0,
1759
+ 0,
1760
+ 0,
1761
+ 8,
1762
+ data,
1763
+ );
1764
+ return true;
1765
+ }
1766
+
1767
+ /**
1768
+ * GC for uploading coverage into a clip temp. Created against the first
1769
+ * temp and kept: every one of them is depth 8 on the same root, which is
1770
+ * all a GC binds to, and the temps themselves come and go per clip entry.
1771
+ */
1772
+ _clipGC(tmpPixmap) {
1773
+ if (!this._clipMaskGC) {
1774
+ this._clipMaskGC = this.X.AllocID();
1775
+ this._gcs.push(this._clipMaskGC);
1776
+ this.X.CreateGC(this._clipMaskGC, tmpPixmap.id);
1777
+ }
1778
+ return this._clipMaskGC;
1779
+ }
1780
+
1672
1781
  _rebuildClipMask() {
1673
1782
  // The stack changed shape (restore, resize): drop the mask rather than
1674
1783
  // rebuild it eagerly. A stack that went back to rectangles never needs
@@ -30,10 +30,11 @@
30
30
  // fontconfig: ntk ships no fonts, so the app has to hand over the ones it
31
31
  // ships, and the spec is the short way to say so.
32
32
  //
33
- // node:fs is fetched through `getBuiltinModule` inside the path branch rather
34
- // than imported, because this file is the one the browser bundle keeps — its
35
- // whole purpose is running the text stack without a filesystem. Do not turn
36
- // that into a static import (test/packaging.test.js fails if you do).
33
+ // node:fs is fetched lazily through builtin() inside the path branch, never a
34
+ // direct static import, because this file is the one the browser bundle keeps
35
+ // — its whole purpose is running the text stack without a filesystem. See
36
+ // builtin.js for how the lazy lookup stays version-tolerant.
37
+ import { builtin } from '../builtin.js';
37
38
  import { charsetHas, matchSortedSync, noFontsError, prewarm, supported } from '../fontconfig.js';
38
39
  import Font from './font.js';
39
40
 
@@ -267,7 +268,7 @@ const ACCEPTED =
267
268
  'docs/fonts.md';
268
269
 
269
270
  function fs() {
270
- const mod = globalThis.process?.getBuiltinModule?.('node:fs');
271
+ const mod = builtin('node:fs');
271
272
  if (!mod) {
272
273
  throw new Error(
273
274
  'ntk: a font path can only be read in node. In a browser, fetch the font ' +
@@ -1,6 +1,7 @@
1
1
  import { selectAll } from 'css-select';
2
2
  import { textContent } from 'domutils';
3
3
  import { parseDocument } from 'htmlparser2';
4
+ import { builtin } from '../builtin.js';
4
5
  import Yoga from '../yoga.js';
5
6
 
6
7
  import { decodeImage, Image } from '../image.js';
@@ -467,15 +468,14 @@ export default class HtmlView {
467
468
  // default policy: local files, only relative to an explicit baseUrl.
468
469
  // node builtins are fetched lazily — in environments without them
469
470
  // (browser bundles) pass a `loadResource` callback instead.
470
- const getBuiltin = globalThis.process?.getBuiltinModule;
471
- const fs = getBuiltin?.call(process, 'node:fs');
471
+ const fs = builtin('node:fs');
472
472
  if (!fs || !this.baseUrl) return null;
473
473
  let base = this.baseUrl;
474
474
  if (typeof base === 'string' && !/^[a-z][a-z0-9+.-]*:/i.test(base)) {
475
475
  // filesystem path; point at the directory itself when given one
476
476
  let p = base;
477
477
  if (!/[/\\]$/.test(p) && fs.existsSync(p) && fs.statSync(p).isDirectory()) p += '/';
478
- base = getBuiltin.call(process, 'node:url').pathToFileURL(p);
478
+ base = builtin('node:url').pathToFileURL(p);
479
479
  }
480
480
  const url = new URL(src, base);
481
481
  if (url.protocol !== 'file:') return null;
@@ -21,14 +21,11 @@
21
21
  import { parseSvgPath } from '../path.js';
22
22
  import { flatten } from '../rasterize.js';
23
23
  import { trapezoidize } from '../trapezoid.js';
24
+ import { builtin, nodeRequire } from '../builtin.js';
24
25
  import Font from '../text/font.js';
25
26
 
26
- // node builtins are fetched lazily so browser bundles never resolve them;
27
- // there, inject the assets with configureTex() instead.
28
- function nodeRequire() {
29
- const mod = globalThis.process?.getBuiltinModule?.('node:module');
30
- return mod ? mod.createRequire(import.meta.url) : null;
31
- }
27
+ // node builtins are fetched lazily (nodeRequire/builtin) so browser bundles
28
+ // never resolve them; there, inject the assets with configureTex() instead.
32
29
 
33
30
  // katex is sizable — load it on first formula, not on package import
34
31
  let katex = null;
@@ -82,8 +79,8 @@ function katexFontFiles() {
82
79
  'TeX rendering: KaTeX fonts cannot be read here — inject them with configureTex({ fonts })'
83
80
  );
84
81
  }
85
- const { readdirSync } = process.getBuiltinModule('node:fs');
86
- const { dirname, join } = process.getBuiltinModule('node:path');
82
+ const { readdirSync } = builtin('node:fs');
83
+ const { dirname, join } = builtin('node:path');
87
84
  fontDir = join(dirname(require.resolve('katex/package.json')), 'dist', 'fonts');
88
85
  fontFiles = new Set(readdirSync(fontDir).filter((f) => f.endsWith('.ttf')));
89
86
  }
package/lib/window.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import x11 from 'x11';
2
2
 
3
+ import { builtin } from './builtin.js';
3
4
  import { safeRelease } from './cleanup.js';
4
5
  import Drawable from './drawable.js';
5
6
  import { packIcons, unpackIcons } from './imagedata.js';
@@ -696,27 +697,25 @@ export default class Window extends Drawable {
696
697
  }
697
698
 
698
699
  // grow-only backing pixmap (re)allocation, with headroom (see
699
- // BACKING_GRANULARITY); new area is cleared to white
700
+ // BACKING_GRANULARITY); new area is cleared to the window's background
701
+ // (white for opaque windows, fully transparent for depth-32 ARGB ones)
700
702
  _allocBacking(w, h) {
701
703
  const cur = this._backing;
702
704
  if (cur && cur.width >= w && cur.height >= h) return;
703
705
  const roundUp = (v) => Math.ceil(v / BACKING_GRANULARITY) * BACKING_GRANULARITY;
704
706
  const newW = roundUp(Math.max(w, cur ? cur.width : 0));
705
707
  const newH = roundUp(Math.max(h, cur ? cur.height : 0));
708
+ const depth = this.depth || this.display.screen[0].root_depth;
706
709
  const pixmap = new Pixmap(this.app, {
707
710
  parent: this,
708
711
  width: newW,
709
712
  height: newH,
710
- // must match the window's depth: CopyArea between drawables of
711
- // different depths is a BadMatch
712
- depth: this.depth || this.display.screen[0].root_depth
713
+ depth
713
714
  });
714
715
  if (!this._clearGc) {
715
- // reusable across reallocs: a GC is valid for any drawable of the
716
- // same screen and depth, so one outlives every backing pixmap
717
716
  this._clearGc = this.X.AllocID();
718
717
  this.X.CreateGC(this._clearGc, pixmap.id, {
719
- foreground: this.display.screen[0].white_pixel,
718
+ foreground: depth === 32 ? 0 : this.display.screen[0].white_pixel,
720
719
  graphicsExposures: 0
721
720
  });
722
721
  }
@@ -1086,29 +1085,35 @@ export default class Window extends Drawable {
1086
1085
  }
1087
1086
 
1088
1087
  /**
1089
- * Whether the fence for the last frame is still unanswered — the server has
1090
- * not yet confirmed it consumed everything that frame drew.
1088
+ * Whether a blit this window already owes is still waiting to go out —
1089
+ * because the fence for the last frame is unanswered, or because a present
1090
+ * is deferred behind the minimum inter-blit interval.
1091
1091
  *
1092
1092
  * The one bit of frame-clock state worth publishing, because it is the
1093
1093
  * difference between the two ways a toolkit can answer a discrete input.
1094
1094
  * Drawing the response inside the event handler costs a frame less latency:
1095
1095
  * the blit goes out with the handler's own requests (see the `_present()`
1096
1096
  * at the end of the event dispatch) instead of waiting for the next paced
1097
- * frame — provided the last blit is at least `frameInterval` old, which is
1098
- * what a discrete input arriving out of the blue always is. Drawing it while
1099
- * a frame is in flight costs a frame's *work* for nothing: the present is
1100
- * deferred until the ack and coalesces with the one already pending, so only
1101
- * the last paint is ever seen. So `false`
1097
+ * frame. Drawing it when a blit is already queued costs a frame's *work* for
1098
+ * nothing: the paint lands in the same backing store and coalesces into the
1099
+ * present already pending, so only the last one is ever seen. So `false`
1102
1100
  * means "draw it now", `true` means "leave it to the frame clock" — which
1103
1101
  * is how a burst of discrete events (a spun wheel) paints its first notch
1104
1102
  * immediately and folds the rest into one catch-up frame.
1105
1103
  *
1106
- * Always `false` under `frameSync: false`: no fence is ever sent, so there
1107
- * is nothing to wait for, and a caller gating on this gets the unpaced
1108
- * behaviour that option asks for.
1104
+ * Both gates have to be reported, not just the fence. The fence is the one
1105
+ * that bites on a slow connection. The minimum inter-blit interval (#195) is
1106
+ * the one that bites on a fast local server, where the fence for a notch is
1107
+ * answered well before the next notch is even read, and what actually holds
1108
+ * the burst back is the interval. Reporting only the fence there answers
1109
+ * "draw it now" to every notch — the precise work this gate exists to skip.
1110
+ *
1111
+ * `false` whenever nothing gates blits at all: `frameSync: false` sends no
1112
+ * fence and `frameInterval: 0` keeps no interval, so a caller who asked for
1113
+ * both gets the unpaced behaviour those options ask for.
1109
1114
  */
1110
1115
  frameInFlight() {
1111
- return this._frame.inFlight;
1116
+ return this._frame.inFlight || this._presentPending;
1112
1117
  }
1113
1118
 
1114
1119
  _present() {
@@ -1911,7 +1916,7 @@ export default class Window extends Drawable {
1911
1916
  /** os.hostname(), looked up once, and absent in a browser bundle. */
1912
1917
  static _hostname() {
1913
1918
  if (Window._cachedHostname === undefined) {
1914
- const os = globalThis.process?.getBuiltinModule?.('node:os');
1919
+ const os = builtin('node:os');
1915
1920
  Window._cachedHostname = os?.hostname?.() ?? null;
1916
1921
  }
1917
1922
  return Window._cachedHostname;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "6.4.0",
3
+ "version": "6.6.0",
4
4
  "description": "Desktop UI toolkit for X11 with canvas-like 2d and OpenGL rendering",
5
5
  "author": "Andrey Sidorov <sidorares@yandex.ru>",
6
6
  "license": "MIT",
@@ -30,7 +30,7 @@
30
30
  "lib"
31
31
  ],
32
32
  "engines": {
33
- "node": ">=20.19.0"
33
+ "node": ">=18.19.0"
34
34
  },
35
35
  "dependencies": {
36
36
  "bidi-js": "^1.0.3",