@animus-ui/next-plugin 0.1.31 → 0.1.35

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
@@ -71,6 +71,47 @@ export default withAnimus({
71
71
  Under webpack (`next dev` / `next build` on 15, or `--webpack` on 16) the
72
72
  plugin behaves exactly as before.
73
73
 
74
+ ## Third-party stylesheets
75
+
76
+ A library's plain CSS outside any `@layer` beats every Animus layer. With
77
+ `quarantine`, [`@animus-ui/layergate`](../layergate/README.md) loads it into
78
+ `anm-vendor`, a layer below all of Animus's, so Animus's styles win and the
79
+ library's still apply where Animus sets nothing:
80
+
81
+ ```bash
82
+ npm install -D @animus-ui/layergate
83
+ ```
84
+
85
+ ```tsx
86
+ export default withAnimus({
87
+ system: './src/ds.ts',
88
+ quarantine: {
89
+ sheets: ['react-grid-layout/css/styles.css'], // delivered ahead of Animus's own CSS
90
+ dependencies: ['react-datepicker'], // CSS these packages' modules import
91
+ optOut: [], // exact files whose own rules stay unlayered
92
+ },
93
+ })(nextConfig);
94
+ ```
95
+
96
+ It needs webpack: `next dev` and `next build` on Next 15, `--webpack` on
97
+ Next 16. Turbopack runs no loader on the stylesheets a stylesheet imports,
98
+ so with `quarantine` set any Turbopack run fails config loading, whatever
99
+ the `turbopack` option says. Next marks a Turbopack run by setting
100
+ `TURBOPACK`, so Animus refuses whenever that variable is set, even empty.
101
+ The converse: a `TURBOPACK` inherited from the environment also refuses a
102
+ webpack build of Next 15, loudly, so clear stale values in CI.
103
+
104
+ Vendor stylesheets layergate finishes are not kept in webpack's persistent
105
+ cache, so a warm rebuild finishes them again: about 118 ms per 200 vendor
106
+ sheets in a synthetic measurement.
107
+
108
+ `sheets` arrive at the head of Animus's own stylesheet, on every page.
109
+ `!important` declarations and animations still win, and are reported. App
110
+ CSS keeps its own rules unlayered; where it imports a vendor file, that file
111
+ is layered. CSS Modules are left alone. A remote `@import` in a vendor
112
+ stylesheet is an error: vendor the sheet locally, or opt the file out if
113
+ unlayered is acceptable.
114
+
74
115
  ## Appearance bootstrap (no-FOUC color mode)
75
116
 
76
117
  Optional. Restores a persisted color mode before first paint with zero runtime
@@ -128,6 +169,49 @@ Two rules either way:
128
169
  you add to the script tag. The full CSP footguns are documented in the
129
170
  [`@animus-ui/vite-plugin` README](https://github.com/codecaaron/animus/tree/main/packages/vite-plugin#content-security-policy).
130
171
 
172
+ ## Font preloads
173
+
174
+ A font face declared with `preload: true` can start loading before the
175
+ stylesheet's `@font-face` applies. The plugin injects nothing into the
176
+ document, so render the component that delivers it. It renders nothing when
177
+ no face preloads.
178
+
179
+ **App Router:** render `AnimusFontPreloads` in the root layout. React hoists
180
+ each preload into `<head>` ahead of the stylesheets, and dedupes it by href,
181
+ against `next/font` too.
182
+
183
+ ```tsx
184
+ // app/layout.tsx
185
+ import { AnimusFontPreloads } from '@animus-ui/next-plugin/font-preloads';
186
+
187
+ export default function RootLayout({
188
+ children,
189
+ }: {
190
+ children: React.ReactNode;
191
+ }) {
192
+ return (
193
+ <html lang="en">
194
+ <body>
195
+ <AnimusFontPreloads />
196
+ {children}
197
+ </body>
198
+ </html>
199
+ );
200
+ }
201
+ ```
202
+
203
+ **Pages Router:** render `AnimusFontPreloadLinks` inside `<Head>` in
204
+ `pages/_document.tsx`. It renders plain `<link>` tags, which any React
205
+ version renders.
206
+
207
+ Each preload names the URL the stylesheet's `url()` loads (the hashed
208
+ `/_next/static/media/…` file, under webpack and Turbopack, in build and in
209
+ development), so the browser fetches the file once. `withAnimus` adds this
210
+ package to `transpilePackages`, so Next bundles the component and resolves
211
+ the module it reads. Nothing detects a layout that never renders the
212
+ component: the faces then load when the stylesheet applies, as without
213
+ `preload`.
214
+
131
215
  ## MDX
132
216
 
133
217
  Animus reads each `.mdx` file as the JSX it renders: its imports, exports,
@@ -0,0 +1,60 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_rolldown_runtime = require("./rolldown-runtime-VH7oDXx4.cjs");
3
+ let react = require("react");
4
+ let react_dom = require("react-dom");
5
+ let virtual_animus_font_preloads = require("virtual:animus/font-preloads");
6
+ virtual_animus_font_preloads = require_rolldown_runtime.__toESM(virtual_animus_font_preloads);
7
+ //#region src/font-preloads.ts
8
+ /**
9
+ * The font preloads the system's faces ask for (`preload: true`), rendered
10
+ * into the document. Server-only; it imports neither the engine nor Node.
11
+ */
12
+ /** The origins of the absolute hrefs, once each: an asset prefix on
13
+ * another host makes the bundler's URLs absolute. `fontPreloadOrigins`'s
14
+ * rule, restated: its module would bring the engine into the bundle. A
15
+ * malformed href warms no origin, and never fails the render. */
16
+ const ORIGINS = [...new Set(virtual_animus_font_preloads.default.flatMap(({ href }) => {
17
+ if (!/^(?:https?:)?\/\//i.test(href)) return [];
18
+ try {
19
+ const { origin } = new URL(href, "https://animus.invalid");
20
+ return [href.startsWith("//") ? `//${href.slice(2).split(/[/?#]/)[0]}` : origin];
21
+ } catch {
22
+ return [];
23
+ }
24
+ }))];
25
+ /**
26
+ * App Router: render it in the root layout. React hoists each preload into
27
+ * `<head>` ahead of the stylesheets and dedupes it by href, against
28
+ * `next/font` too. Renders nothing; a no-op when no face preloads.
29
+ */
30
+ function AnimusFontPreloads() {
31
+ for (const origin of ORIGINS) (0, react_dom.preconnect)(origin, { crossOrigin: "" });
32
+ for (const { href, type } of virtual_animus_font_preloads.default) (0, react_dom.preload)(href, {
33
+ as: "font",
34
+ type,
35
+ crossOrigin: ""
36
+ });
37
+ return null;
38
+ }
39
+ /**
40
+ * Pages Router: render it inside `<Head>` in `pages/_document`, as plain
41
+ * `<link>` tags any React version renders. Empty when no face preloads.
42
+ */
43
+ function AnimusFontPreloadLinks() {
44
+ return (0, react.createElement)(react.Fragment, null, ...ORIGINS.map((origin) => (0, react.createElement)("link", {
45
+ key: origin,
46
+ rel: "preconnect",
47
+ href: origin,
48
+ crossOrigin: ""
49
+ })), ...virtual_animus_font_preloads.default.map(({ href, type }) => (0, react.createElement)("link", {
50
+ key: href,
51
+ rel: "preload",
52
+ href,
53
+ as: "font",
54
+ type,
55
+ crossOrigin: ""
56
+ })));
57
+ }
58
+ //#endregion
59
+ exports.AnimusFontPreloadLinks = AnimusFontPreloadLinks;
60
+ exports.AnimusFontPreloads = AnimusFontPreloads;
@@ -0,0 +1,13 @@
1
+ import type { ReactElement } from 'react';
2
+ /**
3
+ * App Router: render it in the root layout. React hoists each preload into
4
+ * `<head>` ahead of the stylesheets and dedupes it by href, against
5
+ * `next/font` too. Renders nothing; a no-op when no face preloads.
6
+ */
7
+ export declare function AnimusFontPreloads(): null;
8
+ /**
9
+ * Pages Router: render it inside `<Head>` in `pages/_document`, as plain
10
+ * `<link>` tags any React version renders. Empty when no face preloads.
11
+ */
12
+ export declare function AnimusFontPreloadLinks(): ReactElement;
13
+ //# sourceMappingURL=font-preloads.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"font-preloads.d.ts","sourceRoot":"","sources":["../src/font-preloads.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AA4B1C;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAMzC;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,YAAY,CAuBrD"}
@@ -0,0 +1,56 @@
1
+ import { Fragment, createElement } from "react";
2
+ import { preconnect, preload } from "react-dom";
3
+ import preloads from "virtual:animus/font-preloads";
4
+ //#region src/font-preloads.ts
5
+ /**
6
+ * The font preloads the system's faces ask for (`preload: true`), rendered
7
+ * into the document. Server-only; it imports neither the engine nor Node.
8
+ */
9
+ /** The origins of the absolute hrefs, once each: an asset prefix on
10
+ * another host makes the bundler's URLs absolute. `fontPreloadOrigins`'s
11
+ * rule, restated: its module would bring the engine into the bundle. A
12
+ * malformed href warms no origin, and never fails the render. */
13
+ const ORIGINS = [...new Set(preloads.flatMap(({ href }) => {
14
+ if (!/^(?:https?:)?\/\//i.test(href)) return [];
15
+ try {
16
+ const { origin } = new URL(href, "https://animus.invalid");
17
+ return [href.startsWith("//") ? `//${href.slice(2).split(/[/?#]/)[0]}` : origin];
18
+ } catch {
19
+ return [];
20
+ }
21
+ }))];
22
+ /**
23
+ * App Router: render it in the root layout. React hoists each preload into
24
+ * `<head>` ahead of the stylesheets and dedupes it by href, against
25
+ * `next/font` too. Renders nothing; a no-op when no face preloads.
26
+ */
27
+ function AnimusFontPreloads() {
28
+ for (const origin of ORIGINS) preconnect(origin, { crossOrigin: "" });
29
+ for (const { href, type } of preloads) preload(href, {
30
+ as: "font",
31
+ type,
32
+ crossOrigin: ""
33
+ });
34
+ return null;
35
+ }
36
+ /**
37
+ * Pages Router: render it inside `<Head>` in `pages/_document`, as plain
38
+ * `<link>` tags any React version renders. Empty when no face preloads.
39
+ */
40
+ function AnimusFontPreloadLinks() {
41
+ return createElement(Fragment, null, ...ORIGINS.map((origin) => createElement("link", {
42
+ key: origin,
43
+ rel: "preconnect",
44
+ href: origin,
45
+ crossOrigin: ""
46
+ })), ...preloads.map(({ href, type }) => createElement("link", {
47
+ key: href,
48
+ rel: "preload",
49
+ href,
50
+ as: "font",
51
+ type,
52
+ crossOrigin: ""
53
+ })));
54
+ }
55
+ //#endregion
56
+ export { AnimusFontPreloadLinks, AnimusFontPreloads };
package/dist/index.cjs CHANGED
@@ -1,20 +1,21 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_turbopack_config = require("./turbopack-config-BoNfzhNu.cjs");
2
+ const require_turbopack_config = require("./turbopack-config-W5eoa_xN.cjs");
3
3
  let _animus_ui_extract_pipeline = require("@animus-ui/extract/pipeline");
4
4
  let _animus_ui_extract_session = require("@animus-ui/extract/session");
5
5
  let fs = require("fs");
6
6
  let path = require("path");
7
7
  let url = require("url");
8
+ let module$1 = require("module");
8
9
  //#region src/same-directory.ts
9
10
  /** Whether two paths name one directory: by real path, which resolves a
10
11
  * trailing slash, a symlink and the filesystem's own spelling, else by
11
12
  * device and inode. */
12
13
  function sameDirectory(a, b) {
13
- const real = (path$5) => {
14
+ const real = (path$12) => {
14
15
  try {
15
- return fs.realpathSync.native(path$5);
16
+ return fs.realpathSync.native(path$12);
16
17
  } catch {
17
- return (0, path.resolve)(path$5);
18
+ return (0, path.resolve)(path$12);
18
19
  }
19
20
  };
20
21
  if (real(a) === real(b)) return true;
@@ -27,11 +28,97 @@ function sameDirectory(a, b) {
27
28
  }
28
29
  }
29
30
  //#endregion
30
- //#region src/plugin.ts
31
+ //#region src/watch-ignored.ts
32
+ /**
33
+ * webpack's `watchOptions.ignored`, read as Watchpack reads it, so Animus can
34
+ * wrap it: its own epoch file stays ignored, and the files and directories it
35
+ * registers are watched whatever the host ignores (Next ignores every
36
+ * `node_modules` path, where an installed source kit lives).
37
+ */
31
38
  /** The value arrives from a consumer's `next.config`, so the union above is a
32
39
  * model of what webpack accepts, not a proof about the value in hand. */
33
40
  const isWatchIgnoreString = (ignored) => Object.prototype.toString.call(ignored) === "[object String]";
34
41
  const isWatchIgnoreMatcher = (ignored) => Object.prototype.toString.call(ignored) === "[object Function]";
42
+ /** A glob as Watchpack compiles it (`glob-to-regexp` with `globstar` and
43
+ * `extended`, then matching the path or anything under it). */
44
+ function globSource(glob) {
45
+ let source = "";
46
+ let inGroup = false;
47
+ for (let index = 0; index < glob.length; index++) {
48
+ const char = glob[index];
49
+ switch (char) {
50
+ case "/":
51
+ case "$":
52
+ case "^":
53
+ case "+":
54
+ case ".":
55
+ case "(":
56
+ case ")":
57
+ case "=":
58
+ case "!":
59
+ case "|":
60
+ source += `\\${char}`;
61
+ break;
62
+ case "?":
63
+ source += ".";
64
+ break;
65
+ case "{":
66
+ inGroup = true;
67
+ source += "(";
68
+ break;
69
+ case "}":
70
+ inGroup = false;
71
+ source += ")";
72
+ break;
73
+ case ",":
74
+ source += inGroup ? "|" : "\\,";
75
+ break;
76
+ case "*": {
77
+ const before = glob[index - 1];
78
+ let stars = 1;
79
+ while (glob[index + 1] === "*") {
80
+ stars += 1;
81
+ index += 1;
82
+ }
83
+ const after = glob[index + 1];
84
+ if (stars > 1 && (before === "/" || before === void 0) && (after === "/" || after === void 0)) {
85
+ source += "((?:[^/]*(?:/|$))*)";
86
+ index += 1;
87
+ } else source += "([^/]*)";
88
+ break;
89
+ }
90
+ default: source += char;
91
+ }
92
+ }
93
+ return `^${source}(?:$|\\/)`;
94
+ }
95
+ /** What `ignored` ignores, as Watchpack decides it; nothing when unset. */
96
+ function ignoredMatcher(ignored) {
97
+ const slashed = (path$6) => path$6.replace(/\\/g, "/");
98
+ if (ignored === void 0 || ignored === null) return () => false;
99
+ if (isWatchIgnoreMatcher(ignored)) return (path$7) => Boolean(ignored(path$7));
100
+ if (ignored instanceof RegExp) return (path$8) => ignored.test(slashed(path$8));
101
+ const sources = (isWatchIgnoreString(ignored) ? [ignored] : ignored).map((entry) => entry instanceof RegExp ? entry.source : entry.length > 0 ? globSource(entry) : null).filter((source) => source !== null);
102
+ if (sources.length === 0) return () => false;
103
+ const pattern = new RegExp(sources.join("|"));
104
+ return (path$9) => pattern.test(slashed(path$9));
105
+ }
106
+ /** Windows paths compare without case, as `path.win32.relative` does. */
107
+ const comparable = process.platform === "win32" ? (path$10) => path$10.toLowerCase() : (path$11) => path$11;
108
+ /** Whether a path is one of `directories` or lies under one where a kit's
109
+ * sources can live: never in the kit's own `node_modules` or another path
110
+ * ingestion excludes, which stay as the host decides. A prefix test:
111
+ * webpack's watcher asks for every path it watches. */
112
+ function withinDirectories(directories) {
113
+ const prefixes = directories.map((dir) => comparable(dir.endsWith(path.sep) ? dir : dir + path.sep)).sort((a, b) => b.length - a.length);
114
+ return (path$5) => {
115
+ const key = comparable(path$5) + path.sep;
116
+ const prefix = prefixes.find((candidate) => key.startsWith(candidate));
117
+ return prefix !== void 0 && !(0, _animus_ui_extract_pipeline.isExcludedPackageRelativePath)(key.slice(prefix.length));
118
+ };
119
+ }
120
+ //#endregion
121
+ //#region src/plugin.ts
35
122
  const UNSUPPORTED_WEBPACK_MESSAGE = "[animus-extract] Unsupported webpack: NormalModule.getCompilationHooks(compilation).needBuild is required for dev transform coherence and this webpack does not expose it. Use a Next.js version whose webpack exposes this hook.";
36
123
  const PLUGIN_NAME = "AnimusWebpackPlugin";
37
124
  var AnimusWebpackPlugin = class {
@@ -48,6 +135,16 @@ var AnimusWebpackPlugin = class {
48
135
  /** This turn's rejected analysis. A rejection must fail the compilation,
49
136
  * never `watchRun`: webpack stops watching after a failed `watchRun`. */
50
137
  watchFailure = null;
138
+ /** What the Session's watch plan last registered: `watchIgnored` never
139
+ * ignores it. */
140
+ registered = {
141
+ files: /* @__PURE__ */ new Set(),
142
+ directories: () => false
143
+ };
144
+ /** Other files the watcher must see, by the plugins that read them. */
145
+ alsoWatched = [];
146
+ /** The webpack config Next hands its watcher, when withAnimus made it. */
147
+ watchedConfig;
51
148
  /** `development` is `next dev`: an error-level diagnostic reports and the
52
149
  * compilation keeps publishing, whatever the emission mode. */
53
150
  constructor(options, { development = false, build } = {}) {
@@ -55,6 +152,7 @@ var AnimusWebpackPlugin = class {
55
152
  (0, _animus_ui_extract_pipeline.assertSupportedEngine)(options.engine);
56
153
  this.session = new _animus_ui_extract_session.ExtractionSession(options, { build });
57
154
  this.session.development = development;
155
+ this.session.writesFontPreloadsModule = true;
58
156
  this.session.delivery = {
59
157
  ...this.session.delivery,
60
158
  systemPropsModuleId: _animus_ui_extract_session.TURBOPACK_SYSTEM_PROPS_ID
@@ -67,19 +165,39 @@ var AnimusWebpackPlugin = class {
67
165
  if (!Array.isArray(loaders)) return false;
68
166
  return loaders.some((entry) => entry?.loader !== void 0 && this.animusLoaderPaths.has(entry.loader));
69
167
  }
70
- appendEpochWatchIgnore(compiler) {
71
- const options = compiler.options;
72
- if (!options) return;
73
- const epochPath = (0, _animus_ui_extract_session.replacementEpochPath)((0, _animus_ui_extract_session.sessionArtifactDir)(this.session.rootDir ?? compiler.context, this.session.sessionId));
74
- options.watchOptions ??= {};
75
- const watchOptions = options.watchOptions;
76
- const ignored = watchOptions.ignored;
77
- if (ignored === void 0 || ignored === null) watchOptions.ignored = [epochPath];
78
- else if (Array.isArray(ignored)) {
79
- if (!ignored.includes(epochPath)) ignored.push(epochPath);
80
- } else if (isWatchIgnoreString(ignored)) watchOptions.ignored = [ignored, epochPath];
81
- else if (ignored instanceof RegExp) watchOptions.ignored = (path$1) => path$1 === epochPath || ignored.test(path$1);
82
- else if (isWatchIgnoreMatcher(ignored)) watchOptions.ignored = (path$2) => path$2 === epochPath || Boolean(ignored(path$2));
168
+ /** The webpack config whose watch options the host's watcher reads,
169
+ * rewritten when the plugin is applied. */
170
+ watchConfig(config) {
171
+ this.watchedConfig = config;
172
+ }
173
+ /** Makes `watchIgnored` watch the paths `watches` names too: one matcher
174
+ * owns the host's watch options. */
175
+ watchAlso(watches) {
176
+ this.alsoWatched.push(watches);
177
+ }
178
+ /**
179
+ * `watchOptions.ignored` for the host's own `ignored`: this session's epoch
180
+ * file stays ignored, and every file and directory the Session's watch plan
181
+ * registers is watched, whatever the host ignores. Next ignores every
182
+ * `node_modules` path, where an installed source kit and its system live.
183
+ */
184
+ watchIgnored(ignored, context) {
185
+ const hostIgnores = ignoredMatcher(ignored);
186
+ let epochRoot;
187
+ let epochPath;
188
+ const epoch = () => {
189
+ const root = this.session.rootDir ?? context;
190
+ if (root !== epochRoot) {
191
+ epochRoot = root;
192
+ epochPath = root === void 0 ? void 0 : (0, _animus_ui_extract_session.replacementEpochPath)((0, _animus_ui_extract_session.sessionArtifactDir)(root, this.session.sessionId));
193
+ }
194
+ return epochPath;
195
+ };
196
+ return (path$2) => {
197
+ if (path$2 === epoch()) return true;
198
+ if (this.registered.files.has(path$2) || this.registered.directories(path$2)) return false;
199
+ return hostIgnores(path$2) && !this.alsoWatched.some((watches) => watches(path$2));
200
+ };
83
201
  }
84
202
  /** Webpack's export conditions select the system's kit files as they
85
203
  * select the bundle's; `...`, webpack's own defaults, names none. Its
@@ -127,7 +245,15 @@ var AnimusWebpackPlugin = class {
127
245
  if (compiler.options?.name === "edge-server") return;
128
246
  const NormalModule = compiler.webpack?.NormalModule;
129
247
  if (NormalModule?.getCompilationHooks === void 0) throw new Error(UNSUPPORTED_WEBPACK_MESSAGE);
130
- this.appendEpochWatchIgnore(compiler);
248
+ const options = compiler.options;
249
+ if (options) {
250
+ options.watchOptions ??= {};
251
+ options.watchOptions.ignored = this.watchIgnored(options.watchOptions.ignored, compiler.context);
252
+ }
253
+ if (this.watchedConfig) this.watchedConfig.watchOptions = {
254
+ ...this.watchedConfig.watchOptions,
255
+ ignored: this.watchIgnored(this.watchedConfig.watchOptions?.ignored, compiler.context)
256
+ };
131
257
  compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
132
258
  const epochMoved = this.epochMovedForNextCompilation;
133
259
  this.epochMovedForNextCompilation = false;
@@ -145,6 +271,10 @@ var AnimusWebpackPlugin = class {
145
271
  compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
146
272
  const registerSystemDependencies = () => {
147
273
  const plan = this.session.watchPlan();
274
+ this.registered = {
275
+ files: /* @__PURE__ */ new Set([...plan.files, ...plan.missing]),
276
+ directories: withinDirectories(plan.directories)
277
+ };
148
278
  for (const path$3 of plan.files) if ((0, fs.existsSync)(path$3)) compilation.fileDependencies.add(path$3);
149
279
  else compilation.missingDependencies.add(path$3);
150
280
  for (const path$4 of plan.missing) compilation.missingDependencies.add(path$4);
@@ -174,13 +304,13 @@ var AnimusWebpackPlugin = class {
174
304
  this.extractAliases(_compiler);
175
305
  this.extractConditions(_compiler);
176
306
  if (await this.rejoin()) {
177
- this.session.agreeWithSystemAuthority();
307
+ await this.session.agreeWithSystemAuthority();
178
308
  return;
179
309
  }
180
310
  const existing = (0, _animus_ui_extract_session.getAnalysisStartedPromise)(this.session.key);
181
311
  if (existing) {
182
312
  await existing;
183
- this.session.agreeWithSystemAuthority();
313
+ await this.session.agreeWithSystemAuthority();
184
314
  return;
185
315
  }
186
316
  const promise = this.session.runFullPipeline();
@@ -212,7 +342,7 @@ var AnimusWebpackPlugin = class {
212
342
  }
213
343
  async analyzeWatchTurn(compiler) {
214
344
  if (!this.initialized && await this.rejoin()) {
215
- this.session.agreeWithSystemAuthority();
345
+ await this.session.agreeWithSystemAuthority();
216
346
  this.initialized = true;
217
347
  this.lastBuiltEpoch = (0, _animus_ui_extract_session.getReplacementEpoch)(this.session.key);
218
348
  this.epochMovedForNextCompilation = true;
@@ -230,7 +360,7 @@ var AnimusWebpackPlugin = class {
230
360
  if ((0, _animus_ui_extract_session.getAnalysisStartedPromise)(this.session.key) === startup) (0, _animus_ui_extract_session.resetAnalysisStartedPromise)(this.session.key);
231
361
  throw err;
232
362
  }
233
- this.session.agreeWithSystemAuthority();
363
+ await this.session.agreeWithSystemAuthority();
234
364
  this.initialized = true;
235
365
  this.lastBuiltEpoch = (0, _animus_ui_extract_session.getReplacementEpoch)(this.session.key);
236
366
  this.epochMovedForNextCompilation = this.watchFailure !== null;
@@ -240,7 +370,7 @@ var AnimusWebpackPlugin = class {
240
370
  modifiedFiles: compiler.modifiedFiles,
241
371
  removedFiles: compiler.removedFiles
242
372
  });
243
- this.session.agreeWithSystemAuthority();
373
+ await this.session.agreeWithSystemAuthority();
244
374
  const epoch = (0, _animus_ui_extract_session.getReplacementEpoch)(this.session.key);
245
375
  this.epochMovedForNextCompilation = this.lastBuiltEpoch !== null && epoch !== null && epoch !== this.lastBuiltEpoch;
246
376
  if (epoch !== null) this.lastBuiltEpoch = epoch;
@@ -296,6 +426,29 @@ var AnimusWebpackPlugin = class {
296
426
  }
297
427
  };
298
428
  //#endregion
429
+ //#region src/quarantine.ts
430
+ /**
431
+ * layergate's webpack plugin for the `quarantine` option, loaded on first
432
+ * use: layergate is an optional peer. Only the client compiler builds
433
+ * stylesheets, so only it gets one.
434
+ */
435
+ /** The error Turbopack meets: it runs no loader on a stylesheet's
436
+ * imports, so layergate cannot load a graph there. */
437
+ const QUARANTINE_TURBOPACK_MESSAGE = "[animus-extract] the `quarantine` option needs webpack: Turbopack runs no loader on the stylesheets a stylesheet imports, so it cannot keep them in `anm-vendor`. Run `next build --webpack` and `next dev --webpack`, or remove `quarantine` [layergate.turbopack]";
438
+ function quarantinePlugin(options, order, declaredBy) {
439
+ let layergate;
440
+ try {
441
+ layergate = (0, module$1.createRequire)(require("url").pathToFileURL(__filename).href)("@animus-ui/layergate/webpack");
442
+ } catch (cause) {
443
+ throw (0, _animus_ui_extract_pipeline.layergateMissing)("@animus-ui/next-plugin", cause);
444
+ }
445
+ return new layergate.LayergatePlugin({
446
+ ...(0, _animus_ui_extract_pipeline.layergateOptions)(options, order),
447
+ declaredBy,
448
+ watchIgnored: false
449
+ });
450
+ }
451
+ //#endregion
299
452
  //#region src/with-animus.ts
300
453
  const __filename$1 = (0, url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
301
454
  const __dirname$1 = (0, path.dirname)(__filename$1);
@@ -313,7 +466,8 @@ function withAnimus(options) {
313
466
  "turbopack",
314
467
  "unstable_turbopack",
315
468
  "loaderPath",
316
- "phase"
469
+ "phase",
470
+ "quarantine"
317
471
  ], [], {
318
472
  onUnknownKey: "warn",
319
473
  warn: (message) => console.warn(`[animus-extract] ${message}`)
@@ -325,8 +479,10 @@ function withAnimus(options) {
325
479
  const build = (0, _animus_ui_extract_session.createBuildId)();
326
480
  const appRoot = options.root === void 0 ? process.cwd() : (0, path.resolve)(options.root);
327
481
  return (authoredConfig) => {
328
- const nextConfig = withSourceKits(authoredConfig, appRoot);
329
- if (require_turbopack_config.resolveTurbopackMode(options)) {
482
+ const nextConfig = withTranspiled(authoredConfig, transpiledPackages(appRoot));
483
+ const turbopack = require_turbopack_config.resolveTurbopackMode(options);
484
+ if (options.quarantine && (turbopack || "TURBOPACK" in process.env)) throw new _animus_ui_extract_pipeline.AnimusConfigError(QUARANTINE_TURBOPACK_MESSAGE);
485
+ if (turbopack) {
330
486
  warnTurbopackCwdRoot(options, appRoot);
331
487
  return turbopackConfig(nextConfig, options, build, appRoot);
332
488
  }
@@ -338,8 +494,11 @@ function withAnimus(options) {
338
494
  if (context.dir?.length && !sameDirectory(context.dir, appRoot)) throw new _animus_ui_extract_pipeline.AnimusConfigError(`[animus-extract] Next builds ${context.dir}, but Animus read the app's source kits from ${appRoot}. Run next from the app directory, or pass withAnimus({ root }) naming it.`);
339
495
  const rootDir = appRoot;
340
496
  const { mode } = (0, _animus_ui_extract_pipeline.resolveMode)(options.mode, () => context.dev === true ? "development" : "production");
497
+ const vendorOrder = options.quarantine ? (0, _animus_ui_extract_pipeline.withVendorLayer)(options.layers) : void 0;
498
+ const layers = vendorOrder ?? options.layers;
341
499
  const plugin = new AnimusWebpackPlugin({
342
500
  ...options,
501
+ layers,
343
502
  mode
344
503
  }, {
345
504
  development: context.dev === true,
@@ -352,13 +511,15 @@ function withAnimus(options) {
352
511
  const stubCssPath = (0, _animus_ui_extract_session.stylesPath)(sessionDir);
353
512
  if (!(0, fs.existsSync)(stubCssPath)) {
354
513
  const { declaration } = (0, _animus_ui_extract_pipeline.assembleStylesheet)({
355
- layers: options.layers,
514
+ layers,
356
515
  variableCss: "",
357
516
  globalCss: "",
358
517
  split: true
359
518
  });
360
519
  (0, fs.writeFileSync)(stubCssPath, declaration);
361
520
  }
521
+ const sessionFontPreloadsPath = (0, _animus_ui_extract_session.fontPreloadsPath)(sessionDir);
522
+ if (!(0, fs.existsSync)(sessionFontPreloadsPath)) (0, fs.writeFileSync)(sessionFontPreloadsPath, (0, _animus_ui_extract_pipeline.fontPreloadsModule)([]));
362
523
  if (!warnedGitignore) {
363
524
  warnedGitignore = true;
364
525
  try {
@@ -371,6 +532,12 @@ function withAnimus(options) {
371
532
  const isExternalPackageFile = (filePath) => plugin.getExternalPackageDirs().some((dir) => (0, _animus_ui_extract_pipeline.isPathWithinRoot)(dir, filePath));
372
533
  config.plugins = config.plugins || [];
373
534
  config.plugins.push(plugin);
535
+ plugin.watchConfig(config);
536
+ if (options.quarantine && vendorOrder && context.isServer !== true) {
537
+ const gate = quarantinePlugin(options.quarantine, vendorOrder, stubCssPath);
538
+ config.plugins.push(gate);
539
+ plugin.watchAlso((path$1) => gate.watches(path$1));
540
+ }
374
541
  const DefinePlugin = context.webpack?.DefinePlugin;
375
542
  if (DefinePlugin) config.plugins.push(new DefinePlugin({ __ANIMUS_DEV__: JSON.stringify(mode === "development") }));
376
543
  config.resolve = config.resolve || {};
@@ -382,6 +549,7 @@ function withAnimus(options) {
382
549
  compiler.hooks.normalModuleFactory.tap("AnimusVirtualResolve", (nmf) => {
383
550
  nmf.hooks.beforeResolve.tap("AnimusVirtualResolve", (resolveData) => {
384
551
  if (resolveData.request === "virtual:animus/system-props") resolveData.request = sessionSystemPropsPath;
552
+ if (resolveData.request === _animus_ui_extract_session.FONT_PRELOADS_MODULE_ID) resolveData.request = sessionFontPreloadsPath;
385
553
  if (resolveData.request === "virtual:animus/styles.css" || resolveData.request === _animus_ui_extract_session.ANIMUS_CSS_MODULE_ID) resolveData.request = (0, _animus_ui_extract_session.stylesPath)(sessionDir);
386
554
  const srcEntry = plugin.getExternalSourceEntries().get(resolveData.request);
387
555
  if (srcEntry) {
@@ -438,11 +606,7 @@ function bindTurbopackWatchDeathReport(outcome, rootDir) {
438
606
  console.error(`[animus-extract] dev watcher for ${rootDir} died — source edits are no longer extracted; restart the dev server`);
439
607
  };
440
608
  }
441
- /** The config with the source kits the app depends on added to
442
- * `transpilePackages`. The kit redirect points both bundles at a kit's
443
- * source, but Next's server keeps an installed package external, and so
444
- * loads its runtime entry, unless the package is transpiled; transpiling
445
- * also compiles its TypeScript source. */
609
+ const NEXT_PLUGIN_PACKAGE = "@animus-ui/next-plugin";
446
610
  let warnedTurbopackCwd = false;
447
611
  /** Turbopack's loaders get no app dir either, so a cwd that is not the app
448
612
  * cannot be detected later: warn at startup when the cwd holds no Next
@@ -459,15 +623,25 @@ function warnTurbopackCwdRoot(options, appRoot) {
459
623
  warnedTurbopackCwd = true;
460
624
  console.warn(`[animus-extract] the current directory (${appRoot}) holds no next.config: under Turbopack, Animus reads the app from the current directory. Run next from the app directory, or pass withAnimus({ root }) naming it.`);
461
625
  }
462
- function withSourceKits(nextConfig, rootDir) {
626
+ /** The config with `packages` added to `transpilePackages`. Next's server
627
+ * keeps an installed package external, and so loads its runtime entry,
628
+ * unless the package is transpiled. */
629
+ function withTranspiled(nextConfig, packages) {
463
630
  const declared = nextConfig.transpilePackages ?? [];
464
- const kits = (0, _animus_ui_extract_pipeline.sourceKitDependencies)(rootDir, (0, _animus_ui_extract_session.requireEngine)()).map((kit) => kit.name).filter((name) => !declared.includes(name));
465
- if (kits.length === 0) return nextConfig;
631
+ const added = packages.filter((name) => !declared.includes(name));
632
+ if (added.length === 0) return nextConfig;
466
633
  return {
467
634
  ...nextConfig,
468
- transpilePackages: [...declared, ...kits]
635
+ transpilePackages: [...declared, ...added]
469
636
  };
470
637
  }
638
+ /** The packages Next must transpile: the source kits the app depends on,
639
+ * since the kit redirect points both bundles at a kit's TypeScript source,
640
+ * and this package, whose font-preloads entry imports the session module
641
+ * only the bundle's alias resolves. */
642
+ function transpiledPackages(rootDir) {
643
+ return [...(0, _animus_ui_extract_pipeline.sourceKitDependencies)(rootDir, (0, _animus_ui_extract_session.requireEngine)()).map((kit) => kit.name), NEXT_PLUGIN_PACKAGE];
644
+ }
471
645
  function turbopackConfig(nextConfig, options, build, appRoot) {
472
646
  const resolveExtensions = nextConfig.turbopack?.resolveExtensions ?? nextConfig.experimental?.turbo?.resolveExtensions;
473
647
  const outputExclude = nextConfig.distDir ? (0, _animus_ui_extract_pipeline.outputExclusion)(appRoot, (0, path.resolve)(appRoot, nextConfig.distDir)) : null;
@@ -525,6 +699,7 @@ async function analyzeForTurbopack(options, phase, rootDir, resolveExtensions, o
525
699
  unbundledComputedImports: true,
526
700
  build: `${build}-${phase}`
527
701
  });
702
+ session.writesFontPreloadsModule = true;
528
703
  const owner = turbopackOwner(build);
529
704
  owner.session?.close();
530
705
  owner.watcher?.close();