@gusnips/native 0.1.0 → 0.1.2

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
@@ -121,13 +121,16 @@ Five things worth knowing:
121
121
  of 48. `--rem` changes it if you really want another value.
122
122
  - **Tailwind finds your classes by itself** in the app's folder. Tailwind v4 has no `--content`
123
123
  flag. For a file outside the folder, add an `@source` line to `global.css`.
124
- - **Pin lightningcss to 1.30.1, or use a version after 1.30.2.** 1.30.2 breaks react-native-css's
125
- compiler, and react-native-css's own check for it is swallowed by an empty `catch`. The build
126
- refuses 1.30.2 instead. Pin it with `"overrides": { "lightningcss": "1.30.1" }` in the root
124
+ - **Pin lightningcss to exactly 1.30.1.** react-native-css 3.0.7's compiler breaks on 1.30.2, and
125
+ an app measured 1.32.0 and 1.33.0 breaking it too, so no later release is known to work.
126
+ react-native-css's own check is swallowed by an empty `catch`, so the build refuses every
127
+ release after 1.30.1 instead. Pin it with `"overrides": { "lightningcss": "1.30.1" }` in the root
127
128
  `package.json`: a devDependency is not enough, because react-native-css loads the copy under
128
129
  `@expo/metro-config` first.
129
130
  - **A `className` change hot-reloads.** Only a change to `global.css` or your tokens needs the
130
- build to run again.
131
+ build to run again. While you work on those, keep `gusnips-native-css --watch` running beside
132
+ Metro: it builds again when `global.css` or a file it imports changes (your token files, followed
133
+ through their own imports), and not when you edit a component.
131
134
  - **There is no global font.** react-native-css drops element and `*` selectors, so a text without
132
135
  a `font-*` class renders in the system font.
133
136
 
@@ -148,7 +151,8 @@ each class compiles to. The colours come from the stylesheet itself, so a change
148
151
  icons with the text. Tailwind writes only classes it finds in your files, so keep each palette
149
152
  class in your source; the build stops and names any it cannot find.
150
153
 
151
- From a script, the same build is `buildNativeCss({ palette })` from `@gusnips/native/css`.
154
+ From a script, the same build is `buildNativeCss({ palette })` from `@gusnips/native/css`, and the
155
+ watch is `watchNativeCss(options, { onBuild, onError })`.
152
156
 
153
157
  ## Streams
154
158
 
@@ -293,6 +297,20 @@ and under the default flow that URL carries the refresh token.
293
297
  resolver protects the bundle from bun's store layout; the catalog stops the versions drifting in
294
298
  the first place.
295
299
 
300
+ ## Testing an app that uses this
301
+
302
+ Mock the kit's import, not `react-native` under it:
303
+
304
+ ```ts
305
+ vi.mock("@gusnips/native/network", () => ({ useIsOffline: () => false }));
306
+ vi.mock("@gusnips/native/query", () => ({ installQueryManagers: () => {} }));
307
+ ```
308
+
309
+ Vitest runs a package from `node_modules` as it is, outside its own module graph, so a
310
+ `vi.mock("react-native")` or `vi.mock("@react-native-community/netinfo")` in your test never
311
+ reaches the imports inside `@gusnips/native`. Those load the real packages, which do not run under
312
+ Node. Mocking the subpath your code imports replaces the whole thing, and nothing under it loads.
313
+
296
314
  ## Why the dependencies are peers
297
315
 
298
316
  There are no `dependencies`. Each package here is one your app already has, or one that carries
@@ -8,7 +8,7 @@
8
8
  import { readFile } from "node:fs/promises";
9
9
  import { resolve } from "node:path";
10
10
  import { parseArgs } from "node:util";
11
- import { buildNativeCss } from "../css.js";
11
+ import { buildNativeCss, watchNativeCss } from "../css.js";
12
12
  const HELP = `gusnips-native-css: compile global.css for React Native, before Metro starts.
13
13
 
14
14
  Usage: gusnips-native-css [options]
@@ -20,9 +20,11 @@ Usage: gusnips-native-css [options]
20
20
  { "text": { "light": "text-ink", "dark": "dark:text-ink-dark" } }
21
21
  writes colors.ts beside stylesheet.ts
22
22
  --rem <px> pixels per rem (default: 16, as on the web)
23
+ --watch build again when global.css or a file it imports changes
23
24
  -h, --help show this
24
25
 
25
26
  Run it before Metro, in the same script: "start": "gusnips-native-css && react-native start"
27
+ While you edit the CSS or the tokens, keep "gusnips-native-css --watch" open beside Metro.
26
28
  `;
27
29
  async function main() {
28
30
  const { values } = parseArgs({
@@ -32,6 +34,7 @@ async function main() {
32
34
  out: { type: "string" },
33
35
  palette: { type: "string" },
34
36
  rem: { type: "string" },
37
+ watch: { type: "boolean" },
35
38
  help: { type: "boolean", short: "h" },
36
39
  },
37
40
  });
@@ -39,17 +42,29 @@ async function main() {
39
42
  process.stdout.write(HELP);
40
43
  return 0;
41
44
  }
42
- const root = resolve(values.root ?? ".");
43
- const result = await buildNativeCss({
44
- root,
45
+ const options = {
46
+ root: resolve(values.root ?? "."),
45
47
  input: values.input,
46
48
  outDir: values.out,
47
49
  rem: values.rem === undefined ? undefined : Number(values.rem),
48
50
  palette: values.palette === undefined ? undefined : await readPalette(resolve(values.palette)),
51
+ };
52
+ if (!values.watch) {
53
+ report(await buildNativeCss(options));
54
+ return 0;
55
+ }
56
+ // The folder watchers keep the process running until Ctrl-C. A failed build is printed and
57
+ // the watch goes on.
58
+ await watchNativeCss(options, {
59
+ onBuild: report,
60
+ onError: (error) => console.error(`native css: ${error.message}`),
49
61
  });
50
- console.log(`native css: ${result.classes} classes → ${result.files.join(", ")}`);
62
+ console.log("native css: watching global.css and the files it imports. Ctrl-C stops.");
51
63
  return 0;
52
64
  }
65
+ function report(result) {
66
+ console.log(`native css: ${result.classes} classes → ${result.files.join(", ")}`);
67
+ }
53
68
  /** The palette file is the app's own, but a typo in it should read as one, not as a crash. */
54
69
  async function readPalette(file) {
55
70
  const parsed = JSON.parse(await readFile(file, "utf-8"));
@@ -1 +1 @@
1
- {"version":3,"file":"gusnips-native-css.js","sourceRoot":"","sources":["../../../src/node/bin/gusnips-native-css.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"gusnips-native-css.js","sourceRoot":"","sources":["../../../src/node/bin/gusnips-native-css.ts"],"names":[],"mappings":"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"}
@@ -41,4 +41,40 @@ export interface NativeCssResult {
41
41
  * ```
42
42
  */
43
43
  export declare function buildNativeCss(options?: NativeCssOptions): Promise<NativeCssResult>;
44
+ /**
45
+ * Whether this lightningcss breaks react-native-css 3.0.7's compiler. 1.30.2 is the release
46
+ * react-native-css itself names, and an adopter's app then measured 1.32.0 and 1.33.0 failing the
47
+ * same way, so every release after 1.30.1 counts as broken until one is measured working.
48
+ */
49
+ export declare function isBrokenLightningcss(version: string): boolean;
50
+ /**
51
+ * The files whose change means the stylesheet must be built again: the entry, and every file of the
52
+ * app's it imports, followed through their own imports. That is `global.css` and the token files
53
+ * it pulls in. A package import (`tailwindcss/theme.css`, `nativewind/theme`) is left out, because it
54
+ * changes only on an install. Source files are left out on purpose too: a `className` edit
55
+ * hot-reloads without a rebuild, and a rebuild on every save would cost a compile per keystroke.
56
+ *
57
+ * An import is the app's own when it names a file that exists from the importing file's folder,
58
+ * which is how Tailwind tries it first.
59
+ */
60
+ export declare function cssInputs(entry: string): Promise<string[]>;
61
+ export interface NativeCssWatchHandlers {
62
+ /** A build finished. Also called for the first build. */
63
+ onBuild(result: NativeCssResult): void;
64
+ /** A build failed. Watching goes on, so fixing the file builds again. */
65
+ onError(error: Error): void;
66
+ }
67
+ /**
68
+ * Build, then build again whenever `global.css` or a file it imports changes. Stop it with
69
+ * `close()`. A first build that fails is reported and watched like any other, so saving the fix is
70
+ * enough.
71
+ *
72
+ * It watches each FOLDER and filters by name, not each file: an editor that saves by writing a new
73
+ * file and renaming it over the old one leaves a per-file watcher on the deleted file, which then
74
+ * never fires again. The import list is read again after every build, so a new token file starts
75
+ * being watched as soon as `global.css` imports it.
76
+ */
77
+ export declare function watchNativeCss(options: NativeCssOptions, { onBuild, onError }: NativeCssWatchHandlers): Promise<{
78
+ close(): void;
79
+ }>;
44
80
  //# sourceMappingURL=css.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"css.d.ts","sourceRoot":"","sources":["../../src/node/css.ts"],"names":[],"mappings":"AA8BA,4FAA4F;AAC5F,MAAM,WAAW,YAAY;IAC3B,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,uFAAuF;IACvF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qFAAqF;IACrF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,eAAe;IAC9B,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC,CA2C7F"}
1
+ {"version":3,"file":"css.d.ts","sourceRoot":"","sources":["../../src/node/css.ts"],"names":[],"mappings":"AA+BA,4FAA4F;AAC5F,MAAM,WAAW,YAAY;IAC3B,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,uFAAuF;IACvF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qFAAqF;IACrF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,eAAe;IAC9B,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC,CA2C7F;AAuDD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAK7D;AA+ED;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAehE;AAMD,MAAM,WAAW,sBAAsB;IACrC,yDAAyD;IACzD,OAAO,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAAC;IACvC,yEAAyE;IACzE,OAAO,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CAC7B;AAED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,gBAAgB,EACzB,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,sBAAsB,GAC3C,OAAO,CAAC;IAAE,KAAK,IAAI,IAAI,CAAA;CAAE,CAAC,CA0D5B"}
package/dist/node/css.js CHANGED
@@ -20,10 +20,11 @@
20
20
  * from where the app installed it.
21
21
  */
22
22
  import { spawnSync } from "node:child_process";
23
+ import { statSync, watch } from "node:fs";
23
24
  import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
24
25
  import { createRequire } from "node:module";
25
26
  import { tmpdir } from "node:os";
26
- import { dirname, join, relative, resolve } from "node:path";
27
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
27
28
  /**
28
29
  * ```ts
29
30
  * await buildNativeCss(); // global.css → src/generated/stylesheet.ts
@@ -115,10 +116,23 @@ function loadCompiler(fromApp) {
115
116
  return compiler;
116
117
  }
117
118
  /**
118
- * lightningcss 1.30.2 breaks react-native-css's compiler. react-native-css checks for it and means
119
- * to throw, but the check sits inside a `try` whose `catch` is empty, so the throw is swallowed and
120
- * the build goes on. This asks the same copy its loader picks: the one under `@expo/metro-config`
121
- * first (a peer of react-native-css, so bun installs it), then its own.
119
+ * Whether this lightningcss breaks react-native-css 3.0.7's compiler. 1.30.2 is the release
120
+ * react-native-css itself names, and an adopter's app then measured 1.32.0 and 1.33.0 failing the
121
+ * same way, so every release after 1.30.1 counts as broken until one is measured working.
122
+ */
123
+ export function isBrokenLightningcss(version) {
124
+ const [major = 0, minor = 0, patch = 0] = version.split(/[.+-]/, 3).map(Number);
125
+ if (major !== 1)
126
+ return major > 1;
127
+ if (minor !== 30)
128
+ return minor > 30;
129
+ return patch > 1;
130
+ }
131
+ /**
132
+ * react-native-css checks for a broken lightningcss and means to throw, but the check sits inside a
133
+ * `try` whose `catch` is empty, so the throw is swallowed and the build goes on. This asks the same
134
+ * copy its loader picks: the one under `@expo/metro-config` first (a peer of react-native-css, so
135
+ * bun installs it), then its own.
122
136
  *
123
137
  * That order is also why a devDependency on lightningcss does not fix it. Pin it with `overrides`
124
138
  * in the root `package.json`, which reaches every copy.
@@ -142,9 +156,10 @@ function refuseBrokenLightningcss(fromApp) {
142
156
  catch {
143
157
  continue;
144
158
  }
145
- if (version === "1.30.2")
146
- throw new Error("lightningcss 1.30.2 breaks react-native-css's compiler. Pin it to 1.30.1 with " +
147
- '"overrides": { "lightningcss": "1.30.1" } in the root package.json, or move to a newer one.');
159
+ if (isBrokenLightningcss(version))
160
+ throw new Error(`lightningcss ${version} breaks react-native-css's compiler (every release after 1.30.1 ` +
161
+ 'measured so far does). Pin it with "overrides": { "lightningcss": "1.30.1" } in the root ' +
162
+ "package.json, then run bun install.");
148
163
  return;
149
164
  }
150
165
  }
@@ -178,4 +193,105 @@ function missing(name) {
178
193
  function isNotFound(error) {
179
194
  return error instanceof Error && "code" in error && error.code === "MODULE_NOT_FOUND";
180
195
  }
196
+ // ── Watching ─────────────────────────────────────────────────────────────────
197
+ /**
198
+ * The files whose change means the stylesheet must be built again: the entry, and every file of the
199
+ * app's it imports, followed through their own imports. That is `global.css` and the token files
200
+ * it pulls in. A package import (`tailwindcss/theme.css`, `nativewind/theme`) is left out, because it
201
+ * changes only on an install. Source files are left out on purpose too: a `className` edit
202
+ * hot-reloads without a rebuild, and a rebuild on every save would cost a compile per keystroke.
203
+ *
204
+ * An import is the app's own when it names a file that exists from the importing file's folder,
205
+ * which is how Tailwind tries it first.
206
+ */
207
+ export async function cssInputs(entry) {
208
+ const found = new Set();
209
+ async function visit(file) {
210
+ if (found.has(file))
211
+ return;
212
+ found.add(file);
213
+ const source = await readFile(file, "utf-8").catch(() => "");
214
+ const code = source.replace(/\/\*[\s\S]*?\*\//g, "");
215
+ for (const [, specifier] of code.matchAll(/@import\s+(?:url\(\s*)?["']([^"']+)["']/g)) {
216
+ if (specifier === undefined)
217
+ continue;
218
+ const target = isAbsolute(specifier) ? specifier : resolve(dirname(file), specifier);
219
+ if (isFile(target))
220
+ await visit(target);
221
+ }
222
+ }
223
+ await visit(resolve(entry));
224
+ return [...found];
225
+ }
226
+ function isFile(path) {
227
+ return statSync(path, { throwIfNoEntry: false })?.isFile() ?? false;
228
+ }
229
+ /**
230
+ * Build, then build again whenever `global.css` or a file it imports changes. Stop it with
231
+ * `close()`. A first build that fails is reported and watched like any other, so saving the fix is
232
+ * enough.
233
+ *
234
+ * It watches each FOLDER and filters by name, not each file: an editor that saves by writing a new
235
+ * file and renaming it over the old one leaves a per-file watcher on the deleted file, which then
236
+ * never fires again. The import list is read again after every build, so a new token file starts
237
+ * being watched as soon as `global.css` imports it.
238
+ */
239
+ export async function watchNativeCss(options, { onBuild, onError }) {
240
+ const root = resolve(options.root ?? ".");
241
+ const entry = resolve(root, options.input ?? "global.css");
242
+ let watchers = [];
243
+ let watched = new Set();
244
+ let timer;
245
+ let running = null;
246
+ let again = false;
247
+ let closed = false;
248
+ async function rewatch() {
249
+ const inputs = await cssInputs(entry);
250
+ for (const watcher of watchers)
251
+ watcher.close();
252
+ watched = new Set(inputs);
253
+ watchers = [...new Set(inputs.map((file) => dirname(file)))].map((folder) => watch(folder, (_event, name) => {
254
+ if (name !== null && watched.has(join(folder, basename(name))))
255
+ schedule();
256
+ }));
257
+ }
258
+ // One build at a time: a save during a build marks it to run once more after, so the last save
259
+ // always lands and two compiles never write the same files at once.
260
+ async function build() {
261
+ if (running) {
262
+ again = true;
263
+ return running;
264
+ }
265
+ running = (async () => {
266
+ do {
267
+ again = false;
268
+ try {
269
+ onBuild(await buildNativeCss(options));
270
+ }
271
+ catch (error) {
272
+ onError(error instanceof Error ? error : new Error(String(error)));
273
+ }
274
+ if (!closed)
275
+ await rewatch();
276
+ } while (again && !closed);
277
+ })().finally(() => {
278
+ running = null;
279
+ });
280
+ return running;
281
+ }
282
+ // Editors fire several events for one save. One build per burst.
283
+ function schedule() {
284
+ clearTimeout(timer);
285
+ timer = setTimeout(() => void build(), 50);
286
+ }
287
+ await build();
288
+ return {
289
+ close() {
290
+ closed = true;
291
+ clearTimeout(timer);
292
+ for (const watcher of watchers)
293
+ watcher.close();
294
+ },
295
+ };
296
+ }
181
297
  //# sourceMappingURL=css.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"css.js","sourceRoot":"","sources":["../../src/node/css.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC3E,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA6C7D;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAAO,GAAqB,EAAE;IACjE,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC;IAC1C,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,IAAI,YAAY,CAAC,CAAC;IAC3D,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,MAAM,IAAI,eAAe,CAAC,CAAC;IAChE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,EAAE,CAAC;IAC9B,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,sDAAsD,GAAG,GAAG,CAAC,CAAC;IAE9F,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC;IAC1D,gGAAgG;IAChG,MAAM,QAAQ,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,GAAG,GAAG,MAAM,WAAW,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;IACpD,MAAM,UAAU,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC;IAC1E,cAAc,CAAC,UAAU,CAAC,CAAC;IAE3B,MAAM,KAAK,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,2CAA2C,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,gCAAgC,CAAC;IAChH,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACrD,MAAM,SAAS,CACb,cAAc,EACd,GAAG,MAAM,kFAAkF;QACzF,uDAAuD,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,KAAK,CACzF,CAAC;IACF,MAAM,KAAK,GAAG,CAAC,cAAc,CAAC,CAAC;IAE/B,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IAC5B,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,MAAM,GAAG,CAAC,IAAwB,EAAE,EAAE,CAC1C,MAAM,CAAC,WAAW,CAChB,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC;YAC7C,IAAI;YACJ,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC;SACvC,CAAC,CACH,CAAC;QACJ,MAAM,MAAM,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;QAChE,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QAC7C,MAAM,SAAS,CACb,UAAU,EACV,GAAG,MAAM,2BAA2B,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CACzE,CAAC;QACF,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACzB,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;AACnF,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,OAAoB,EAAE,IAAY,EAAE,KAAa;IAC1E,MAAM,GAAG,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;IACjC,2FAA2F;IAC3F,gFAAgF;IAChF,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,qBAAqB,CAAC,CAAC,CAAC;IAClE,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC;IACvC,MAAM,GAAG,GAAG,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,EAAE;QACrE,GAAG,EAAE,IAAI;QACT,QAAQ,EAAE,OAAO;QACjB,wFAAwF;QACxF,OAAO,EAAE,OAAO;KACjB,CAAC,CAAC;IACH,IAAI,GAAG,CAAC,KAAK,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;IACjG,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAClB,MAAM,IAAI,KAAK,CACb,iCAAiC,KAAK,EAAE;YACtC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,0BAA0B,IAAI,GAAG,CAC1F,CAAC;IACJ,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACzC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACjD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,WAAW,CAAC,OAAoB;IACvC,IAAI,YAAoB,CAAC;IACzB,IAAI,CAAC;QACH,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,+BAA+B,CAAC,CAAC;IAClE,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC;IAC/C,CAAC;IACD,MAAM,QAAQ,GAA8C,OAAO,CAAC,YAAY,CAAC,CAAC;IAClF,MAAM,GAAG,GAAG,OAAO,QAAQ,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,CAAC;IAC5F,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,YAAY,4BAA4B,CAAC,CAAC;IAC3F,OAAO,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,GAAG,CAAC,CAAC;AAC1C,CAAC;AAED;;;;GAIG;AACH,SAAS,YAAY,CAAC,OAAoB;IACxC,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACH,QAAQ,GAAG,OAAO,CAAC,2BAA2B,CAAC,CAAC;IAClD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,UAAU,CAAC,KAAK,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC;QACpE,MAAM,KAAK,CAAC;IACd,CAAC;IACD,wBAAwB,CAAC,OAAO,CAAC,CAAC;IAClC,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,wBAAwB,CAAC,OAAoB;IACpD,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,+BAA+B,CAAC,CAAC,CAAC;IAChF,MAAM,UAAU,GAAkB,EAAE,CAAC;IACrC,IAAI,CAAC;QACH,UAAU,CAAC,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,iCAAiC,CAAC,CAAC,CAAC,CAAC;IACrF,CAAC;IAAC,MAAM,CAAC;QACP,yEAAyE;IAC3E,CAAC;IACD,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACzB,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,IAAI,OAAe,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,QAAQ,GAAwB,IAAI,CAAC,2BAA2B,CAAC,CAAC;YACxE,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC;QAC7B,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QACD,IAAI,OAAO,KAAK,QAAQ;YACtB,MAAM,IAAI,KAAK,CACb,gFAAgF;gBAC9E,6FAA6F,CAChG,CAAC;QACJ,OAAO;IACT,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,cAAc,CAAC,UAAoC;IAC1D,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;QAAE,OAAO;IACzC,UAAU,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,MAAM,CAChC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,WAAW,CAAC,CAC/D,CAAC;AACJ,CAAC;AAED,SAAS,OAAO,CAAC,UAAoC,EAAE,SAAiB,EAAE,IAAY;IACpF,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;QACvC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC;QAC3E,CAAC,CAAC,SAAS,CAAC;IACd,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,qBAAqB,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC3F,IAAI,GAAG,KAAK,SAAS;QACnB,MAAM,IAAI,KAAK,CACb,kBAAkB,SAAS,iEAAiE;YAC1F,wCAAwC,SAAS,qBAAqB,IAAI,cAAc;YACxF,iFAAiF,CACpF,CAAC;IACJ,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,OAAO,CAAC,IAAY;IAC3B,OAAO,CACL,GAAG,IAAI,+EAA+E;QACtF,+EAA+E,CAChF,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,KAAc;IAChC,OAAO,KAAK,YAAY,KAAK,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,kBAAkB,CAAC;AACxF,CAAC"}
1
+ {"version":3,"file":"css.js","sourceRoot":"","sources":["../../src/node/css.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gusnips/native",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "What a bare React Native app needs beside @gusnips/react: streams over XHR, React Query on app focus and network, theme and language that remember the choice, the Supabase session storage, the Metro config for a bun monorepo, and the Tailwind to react-native-css build.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -8,7 +8,7 @@
8
8
  import { readFile } from "node:fs/promises";
9
9
  import { resolve } from "node:path";
10
10
  import { parseArgs } from "node:util";
11
- import { buildNativeCss, type PaletteEntry } from "../css.ts";
11
+ import { buildNativeCss, watchNativeCss, type NativeCssResult, type PaletteEntry } from "../css.ts";
12
12
 
13
13
  const HELP = `gusnips-native-css: compile global.css for React Native, before Metro starts.
14
14
 
@@ -21,9 +21,11 @@ Usage: gusnips-native-css [options]
21
21
  { "text": { "light": "text-ink", "dark": "dark:text-ink-dark" } }
22
22
  writes colors.ts beside stylesheet.ts
23
23
  --rem <px> pixels per rem (default: 16, as on the web)
24
+ --watch build again when global.css or a file it imports changes
24
25
  -h, --help show this
25
26
 
26
27
  Run it before Metro, in the same script: "start": "gusnips-native-css && react-native start"
28
+ While you edit the CSS or the tokens, keep "gusnips-native-css --watch" open beside Metro.
27
29
  `;
28
30
 
29
31
  async function main(): Promise<number> {
@@ -34,6 +36,7 @@ async function main(): Promise<number> {
34
36
  out: { type: "string" },
35
37
  palette: { type: "string" },
36
38
  rem: { type: "string" },
39
+ watch: { type: "boolean" },
37
40
  help: { type: "boolean", short: "h" },
38
41
  },
39
42
  });
@@ -42,18 +45,31 @@ async function main(): Promise<number> {
42
45
  return 0;
43
46
  }
44
47
 
45
- const root = resolve(values.root ?? ".");
46
- const result = await buildNativeCss({
47
- root,
48
+ const options = {
49
+ root: resolve(values.root ?? "."),
48
50
  input: values.input,
49
51
  outDir: values.out,
50
52
  rem: values.rem === undefined ? undefined : Number(values.rem),
51
53
  palette: values.palette === undefined ? undefined : await readPalette(resolve(values.palette)),
54
+ };
55
+ if (!values.watch) {
56
+ report(await buildNativeCss(options));
57
+ return 0;
58
+ }
59
+ // The folder watchers keep the process running until Ctrl-C. A failed build is printed and
60
+ // the watch goes on.
61
+ await watchNativeCss(options, {
62
+ onBuild: report,
63
+ onError: (error) => console.error(`native css: ${error.message}`),
52
64
  });
53
- console.log(`native css: ${result.classes} classes → ${result.files.join(", ")}`);
65
+ console.log("native css: watching global.css and the files it imports. Ctrl-C stops.");
54
66
  return 0;
55
67
  }
56
68
 
69
+ function report(result: NativeCssResult): void {
70
+ console.log(`native css: ${result.classes} classes → ${result.files.join(", ")}`);
71
+ }
72
+
57
73
  /** The palette file is the app's own, but a typo in it should read as one, not as a crash. */
58
74
  async function readPalette(file: string): Promise<Record<string, PaletteEntry>> {
59
75
  const parsed: unknown = JSON.parse(await readFile(file, "utf-8"));
@@ -0,0 +1,61 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import { cssInputs, isBrokenLightningcss } from "./css.ts";
6
+
7
+ describe("isBrokenLightningcss", () => {
8
+ it("lets 1.30.1 and earlier through", () => {
9
+ for (const version of ["1.30.1", "1.30.0", "1.29.3", "1.0.0", "0.9.9"])
10
+ expect(isBrokenLightningcss(version), version).toBe(false);
11
+ });
12
+
13
+ it("refuses every release after 1.30.1, the measured ones included", () => {
14
+ for (const version of ["1.30.2", "1.30.10", "1.31.0", "1.32.0", "1.33.0", "2.0.0"])
15
+ expect(isBrokenLightningcss(version), version).toBe(true);
16
+ });
17
+
18
+ it("reads a prerelease or build suffix by its numbers", () => {
19
+ expect(isBrokenLightningcss("1.30.1-beta.2")).toBe(false);
20
+ expect(isBrokenLightningcss("1.30.2+build.7")).toBe(true);
21
+ });
22
+ });
23
+
24
+ const made: string[] = [];
25
+ afterEach(async () => {
26
+ for (const dir of made.splice(0)) await rm(dir, { recursive: true, force: true });
27
+ });
28
+
29
+ describe("cssInputs: what makes --watch build again", () => {
30
+ it("is the entry and every local file it imports, followed through their own imports", async () => {
31
+ const app = await mkdtemp(join(tmpdir(), "native-css-inputs-"));
32
+ made.push(app);
33
+ const shared = join(app, "packages", "shared");
34
+ await mkdir(shared, { recursive: true });
35
+ await mkdir(join(app, "mobile"), { recursive: true });
36
+ await writeFile(
37
+ join(app, "mobile", "global.css"),
38
+ [
39
+ '@import "tailwindcss/theme.css" layer(theme);', // a package: changes only on install
40
+ '@import "nativewind/theme";',
41
+ '@import "../packages/shared/tokens.css";',
42
+ '/* @import "./commented-out.css"; */',
43
+ '@import url("./fonts.css");',
44
+ '@import "./missing.css";', // a typo is Tailwind's error to report, not a file to watch
45
+ ].join("\n"),
46
+ );
47
+ await writeFile(join(app, "mobile", "fonts.css"), ":root { --font-sans: Inter; }");
48
+ await writeFile(join(app, "mobile", "commented-out.css"), "");
49
+ await writeFile(join(shared, "tokens.css"), '@import "./colors.css";\n@theme {}');
50
+ await writeFile(join(shared, "colors.css"), '@import "./tokens.css";'); // a cycle ends
51
+
52
+ expect((await cssInputs(join(app, "mobile", "global.css"))).sort()).toEqual(
53
+ [
54
+ join(app, "mobile", "fonts.css"),
55
+ join(app, "mobile", "global.css"),
56
+ join(shared, "colors.css"),
57
+ join(shared, "tokens.css"),
58
+ ].sort(),
59
+ );
60
+ });
61
+ });
package/src/node/css.ts CHANGED
@@ -20,10 +20,11 @@
20
20
  * from where the app installed it.
21
21
  */
22
22
  import { spawnSync } from "node:child_process";
23
+ import { statSync, watch, type FSWatcher } from "node:fs";
23
24
  import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
24
25
  import { createRequire } from "node:module";
25
26
  import { tmpdir } from "node:os";
26
- import { dirname, join, relative, resolve } from "node:path";
27
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
27
28
  import type { ReactNativeCssStyleSheet } from "react-native-css/compiler";
28
29
 
29
30
  type Compiler = typeof import("react-native-css/compiler");
@@ -172,10 +173,22 @@ function loadCompiler(fromApp: NodeRequire): Compiler {
172
173
  }
173
174
 
174
175
  /**
175
- * lightningcss 1.30.2 breaks react-native-css's compiler. react-native-css checks for it and means
176
- * to throw, but the check sits inside a `try` whose `catch` is empty, so the throw is swallowed and
177
- * the build goes on. This asks the same copy its loader picks: the one under `@expo/metro-config`
178
- * first (a peer of react-native-css, so bun installs it), then its own.
176
+ * Whether this lightningcss breaks react-native-css 3.0.7's compiler. 1.30.2 is the release
177
+ * react-native-css itself names, and an adopter's app then measured 1.32.0 and 1.33.0 failing the
178
+ * same way, so every release after 1.30.1 counts as broken until one is measured working.
179
+ */
180
+ export function isBrokenLightningcss(version: string): boolean {
181
+ const [major = 0, minor = 0, patch = 0] = version.split(/[.+-]/, 3).map(Number);
182
+ if (major !== 1) return major > 1;
183
+ if (minor !== 30) return minor > 30;
184
+ return patch > 1;
185
+ }
186
+
187
+ /**
188
+ * react-native-css checks for a broken lightningcss and means to throw, but the check sits inside a
189
+ * `try` whose `catch` is empty, so the throw is swallowed and the build goes on. This asks the same
190
+ * copy its loader picks: the one under `@expo/metro-config` first (a peer of react-native-css, so
191
+ * bun installs it), then its own.
179
192
  *
180
193
  * That order is also why a devDependency on lightningcss does not fix it. Pin it with `overrides`
181
194
  * in the root `package.json`, which reaches every copy.
@@ -197,10 +210,11 @@ function refuseBrokenLightningcss(fromApp: NodeRequire): void {
197
210
  } catch {
198
211
  continue;
199
212
  }
200
- if (version === "1.30.2")
213
+ if (isBrokenLightningcss(version))
201
214
  throw new Error(
202
- "lightningcss 1.30.2 breaks react-native-css's compiler. Pin it to 1.30.1 with " +
203
- '"overrides": { "lightningcss": "1.30.1" } in the root package.json, or move to a newer one.',
215
+ `lightningcss ${version} breaks react-native-css's compiler (every release after 1.30.1 ` +
216
+ 'measured so far does). Pin it with "overrides": { "lightningcss": "1.30.1" } in the root ' +
217
+ "package.json, then run bun install.",
204
218
  );
205
219
  return;
206
220
  }
@@ -244,3 +258,116 @@ function missing(name: string): string {
244
258
  function isNotFound(error: unknown): boolean {
245
259
  return error instanceof Error && "code" in error && error.code === "MODULE_NOT_FOUND";
246
260
  }
261
+
262
+ // ── Watching ─────────────────────────────────────────────────────────────────
263
+
264
+ /**
265
+ * The files whose change means the stylesheet must be built again: the entry, and every file of the
266
+ * app's it imports, followed through their own imports. That is `global.css` and the token files
267
+ * it pulls in. A package import (`tailwindcss/theme.css`, `nativewind/theme`) is left out, because it
268
+ * changes only on an install. Source files are left out on purpose too: a `className` edit
269
+ * hot-reloads without a rebuild, and a rebuild on every save would cost a compile per keystroke.
270
+ *
271
+ * An import is the app's own when it names a file that exists from the importing file's folder,
272
+ * which is how Tailwind tries it first.
273
+ */
274
+ export async function cssInputs(entry: string): Promise<string[]> {
275
+ const found = new Set<string>();
276
+ async function visit(file: string): Promise<void> {
277
+ if (found.has(file)) return;
278
+ found.add(file);
279
+ const source = await readFile(file, "utf-8").catch(() => "");
280
+ const code = source.replace(/\/\*[\s\S]*?\*\//g, "");
281
+ for (const [, specifier] of code.matchAll(/@import\s+(?:url\(\s*)?["']([^"']+)["']/g)) {
282
+ if (specifier === undefined) continue;
283
+ const target = isAbsolute(specifier) ? specifier : resolve(dirname(file), specifier);
284
+ if (isFile(target)) await visit(target);
285
+ }
286
+ }
287
+ await visit(resolve(entry));
288
+ return [...found];
289
+ }
290
+
291
+ function isFile(path: string): boolean {
292
+ return statSync(path, { throwIfNoEntry: false })?.isFile() ?? false;
293
+ }
294
+
295
+ export interface NativeCssWatchHandlers {
296
+ /** A build finished. Also called for the first build. */
297
+ onBuild(result: NativeCssResult): void;
298
+ /** A build failed. Watching goes on, so fixing the file builds again. */
299
+ onError(error: Error): void;
300
+ }
301
+
302
+ /**
303
+ * Build, then build again whenever `global.css` or a file it imports changes. Stop it with
304
+ * `close()`. A first build that fails is reported and watched like any other, so saving the fix is
305
+ * enough.
306
+ *
307
+ * It watches each FOLDER and filters by name, not each file: an editor that saves by writing a new
308
+ * file and renaming it over the old one leaves a per-file watcher on the deleted file, which then
309
+ * never fires again. The import list is read again after every build, so a new token file starts
310
+ * being watched as soon as `global.css` imports it.
311
+ */
312
+ export async function watchNativeCss(
313
+ options: NativeCssOptions,
314
+ { onBuild, onError }: NativeCssWatchHandlers,
315
+ ): Promise<{ close(): void }> {
316
+ const root = resolve(options.root ?? ".");
317
+ const entry = resolve(root, options.input ?? "global.css");
318
+ let watchers: FSWatcher[] = [];
319
+ let watched = new Set<string>();
320
+ let timer: ReturnType<typeof setTimeout> | undefined;
321
+ let running: Promise<void> | null = null;
322
+ let again = false;
323
+ let closed = false;
324
+
325
+ async function rewatch(): Promise<void> {
326
+ const inputs = await cssInputs(entry);
327
+ for (const watcher of watchers) watcher.close();
328
+ watched = new Set(inputs);
329
+ watchers = [...new Set(inputs.map((file) => dirname(file)))].map((folder) =>
330
+ watch(folder, (_event, name) => {
331
+ if (name !== null && watched.has(join(folder, basename(name)))) schedule();
332
+ }),
333
+ );
334
+ }
335
+
336
+ // One build at a time: a save during a build marks it to run once more after, so the last save
337
+ // always lands and two compiles never write the same files at once.
338
+ async function build(): Promise<void> {
339
+ if (running) {
340
+ again = true;
341
+ return running;
342
+ }
343
+ running = (async () => {
344
+ do {
345
+ again = false;
346
+ try {
347
+ onBuild(await buildNativeCss(options));
348
+ } catch (error) {
349
+ onError(error instanceof Error ? error : new Error(String(error)));
350
+ }
351
+ if (!closed) await rewatch();
352
+ } while (again && !closed);
353
+ })().finally(() => {
354
+ running = null;
355
+ });
356
+ return running;
357
+ }
358
+
359
+ // Editors fire several events for one save. One build per burst.
360
+ function schedule(): void {
361
+ clearTimeout(timer);
362
+ timer = setTimeout(() => void build(), 50);
363
+ }
364
+
365
+ await build();
366
+ return {
367
+ close() {
368
+ closed = true;
369
+ clearTimeout(timer);
370
+ for (const watcher of watchers) watcher.close();
371
+ },
372
+ };
373
+ }