@multiplatform.one/config 7.11.0 → 7.15.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/README.md CHANGED
@@ -58,29 +58,54 @@ Available presets: `base.json`, `build.json`, `app.json`.
58
58
 
59
59
  ### Oxlint
60
60
 
61
- Convention rules travel with the package. Consuming apps add one line:
61
+ Convention rules travel with the package. A consuming app extends the preset
62
+ and needs no rules block of its own (MPO-23):
62
63
 
63
64
  ```json
64
65
  {
65
- "jsPlugins": ["@multiplatform.one/config/oxlint"]
66
+ "extends": ["./node_modules/@multiplatform.one/config/oxlint.json"]
66
67
  }
67
68
  ```
68
69
 
69
- Then enable `mpo-conventions/no-hex-literals` on the globs you want (mpo's
70
- own `oxlintrc.json` turns it on for `features/**`). Palette modules
71
- (`themes/base.ts`, `themes/accent.ts`, `*palette*`) are exempt. Comments
72
- are not visited, so a JSDoc `#025` is not a hit. Widening to `apps/**`
73
- and `packages/**` is 182 hits on this tree (153 outside tests) — record
74
- the count, then burn down; do not flip the glob until then.
70
+ oxlint resolves `extends` as a file path, not a package name, hence the
71
+ `node_modules` prefix. The preset loads the plugin, turns
72
+ `no-hex-literals` and `no-raw-typography` on as errors, turns
73
+ `no-chrome-props`, `no-raw-geometry` and `no-child-margin` on as warnings
74
+ while their burndowns run, and exempts specs and `tests/` trees, which assert
75
+ computed colours, from all but `no-raw-typography`. A project that wants its
76
+ own levels keeps the older one-line form,
77
+ `{ "jsPlugins": ["@multiplatform.one/config/oxlint"] }`, and writes its own
78
+ rules block (mpo's own `oxlintrc.json` turns `no-hex-literals` on for
79
+ `apps/**`, `packages/**` and `features/**`, and off for specs and `tests/`
80
+ trees). Palette modules (`themes/base.ts`, `themes/accent.ts`,
81
+ `*palette*`) are exempt. Comments are not visited, so a JSDoc `#025` is not
82
+ a hit. A file no theme token reaches opts out with a `// hex-escape: <reason>`
83
+ line comment; `lint` fails that comment unless the spec table
84
+ `## Colour-literal escapes` has its file and exact reason, and fails a row
85
+ whose comment is gone (MPO-326).
75
86
 
76
87
  `no-chrome-props` and `no-raw-geometry` ship in the same plugin (MPO-17).
77
88
  Enable them the same way when you start that burndown. Drawing-file
78
89
  exceptions come from the spec table `## Drawing files`, not from the code.
79
90
 
91
+ `no-child-margin` (MPO-23, `00` L9a) fails a non-zero `margin*` prop, long
92
+ or shorthand, on any element nested inside another element's JSX: the parent
93
+ owns the gap. `0` is always legal, the outermost element a file renders is
94
+ left alone, and slots come from the spec table `## Slot margins`. mpo's own
95
+ `oxlintrc.json` runs it as an error over `apps/`, `features/`, `packages/` and
96
+ `public/`, off for specs and `tests/` trees and for a named burndown list: the
97
+ 73 files that carried 133 hits when it was switched on. The list may only
98
+ shrink; `scripts/child-margin-ratchet.spec.mjs` fails if it grows, names a
99
+ pattern instead of a file, or keeps a file that no longer violates.
100
+
80
101
  ### Lint (node-script checks)
81
102
 
82
- CSS-in-apps, features-twin, silent token no-ops, and the LC-65 size-recipe
83
- pilot run through `runConventionChecks({ roots })`:
103
+ CSS-in-apps, features-twin, silent token no-ops, the LC-65 size-recipe
104
+ pilot, and the size-recipe escape registry run through
105
+ `runConventionChecks({ roots })`. Every `sizeRecipeEscape` site, and every
106
+ `size-recipe-escape:` comment, must be a row in the spec table
107
+ `## Size-recipe escapes` by file and exact reason, and a row whose site is
108
+ gone fails too (MPO-23, `00` L8 E8):
84
109
 
85
110
  ```ts
86
111
  import { runConventionChecks } from "@multiplatform.one/config/lint";
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { t as createViteConfig } from "./vite-D6vMjPwv.js";
2
- import { t as createStorybookViteConfig } from "./storybook-CGNSgSBS.js";
1
+ import { t as createViteConfig } from "./vite-DXXr5In0.js";
2
+ import { t as createStorybookViteConfig } from "./storybook-DFdvDJ_s.js";
3
3
  import { n as tamaguiWorkspacePathsFile, t as ensureTamaguiWorkspacePaths } from "./tamaguiWorkspacePaths-ZIcqybtn.js";
4
4
  import { createVitestConfig } from "./vitest.js";
5
5
 
@@ -1,4 +1,4 @@
1
- import { i as withSingletonDepAliases, r as resolvePackageMainSource, t as discoverPublicPackageRoots } from "./workspacePublicPackages-B4FGgRYG.js";
1
+ import { i as withSingletonDepAliases, r as resolvePackageMainSource, t as discoverPublicPackageRoots } from "./workspacePublicPackages-BuYRXxFs.js";
2
2
  import { t as ensureTamaguiWorkspacePaths } from "./tamaguiWorkspacePaths-ZIcqybtn.js";
3
3
  import { createRequire } from "node:module";
4
4
  import fs from "node:fs";
@@ -131,6 +131,8 @@ function createStorybookViteConfig(options = {}) {
131
131
  "@tamagui/web",
132
132
  "@tamagui/helpers-icon",
133
133
  "tamagui",
134
+ "@tamagui/animations-css",
135
+ "@tamagui/react-native-svg",
134
136
  "@tamagui/colors",
135
137
  "@tamagui/config",
136
138
  "@tamagui/config/v5",
package/lib/storybook.js CHANGED
@@ -1,3 +1,3 @@
1
- import { t as createStorybookViteConfig } from "./storybook-CGNSgSBS.js";
1
+ import { t as createStorybookViteConfig } from "./storybook-DFdvDJ_s.js";
2
2
 
3
3
  export { createStorybookViteConfig };
@@ -1,11 +1,12 @@
1
- import { i as withSingletonDepAliases, t as discoverPublicPackageRoots } from "./workspacePublicPackages-B4FGgRYG.js";
1
+ import { i as withSingletonDepAliases, t as discoverPublicPackageRoots } from "./workspacePublicPackages-BuYRXxFs.js";
2
2
  import { t as ensureTamaguiWorkspacePaths } from "./tamaguiWorkspacePaths-ZIcqybtn.js";
3
3
  import { createRequire } from "node:module";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { spawnSync } from "node:child_process";
7
- import { tamaguiPlugin } from "@tamagui/vite-plugin";
8
7
  import dotenv from "dotenv";
8
+ import { transformWithOxc } from "vite";
9
+ import { tamaguiPlugin } from "@tamagui/vite-plugin";
9
10
  import createExternal from "vite-plugin-external";
10
11
  import i18nextLoader from "vite-plugin-i18next-loader";
11
12
 
@@ -54,6 +55,148 @@ function lookupProjectRoot() {
54
55
  }
55
56
  }
56
57
 
58
+ //#endregion
59
+ //#region src/publicConfig.ts
60
+ const bakedConfigEnvKey = "VITE_MP_CONFIG";
61
+ const publicConfigKeysEnvKey = "VITE_MP_PUBLIC_CONFIG_KEYS";
62
+ /**
63
+ * The `public` key names of a config.json, read rather than imported: a
64
+ * static import puts the file on Vite's configFileDependencies, and every
65
+ * edit then restarts the dev server, re-optimizes deps and 504s each open tab
66
+ * (MPO-247). A read keeps it off that list.
67
+ */
68
+ function readPublicConfigKeys(file) {
69
+ const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
70
+ const keys = parsed && typeof parsed === "object" ? parsed.public : [];
71
+ if (!Array.isArray(keys)) return [];
72
+ return keys.filter((key) => typeof key === "string" && key.length > 0);
73
+ }
74
+ function mergePublicConfigKeys(...lists) {
75
+ return [...new Set(lists.flat())];
76
+ }
77
+ /**
78
+ * Put the bake on process.env: the values resolved from the environment under
79
+ * VITE_MP_CONFIG and the key NAMES under VITE_MP_PUBLIC_CONFIG_KEYS. Names,
80
+ * never a value, is what lets the SSR node process resolve them from its own
81
+ * environment at request time (@multiplatform.one/platform runtimeConfig.ts).
82
+ */
83
+ function bakePublicConfig(keys) {
84
+ process.env[bakedConfigEnvKey] = JSON.stringify(resolveConfig(keys));
85
+ process.env[publicConfigKeysEnvKey] = JSON.stringify(keys);
86
+ }
87
+ const bakedExpression = /\bprocess\??\.env\??\.(?:VITE_MP_CONFIG|VITE_MP_PUBLIC_CONFIG_KEYS)\b/;
88
+ const nonScriptRequest = /\.(?:json|css|html)(?:$|\?)/;
89
+ const nodeModulesPath = /[\\/]node_modules[\\/]/;
90
+ /**
91
+ * The same replacement vite:define makes, done through oxc so only real
92
+ * `process.env.<KEY>` reads change: a string, an assignment target or a
93
+ * `globalThis.process.env.<KEY>` is left as written.
94
+ */
95
+ function bakedDefine() {
96
+ return Object.fromEntries([bakedConfigEnvKey, publicConfigKeysEnvKey].map((key) => [`process.env.${key}`, JSON.stringify(process.env[key] ?? "")]));
97
+ }
98
+ /**
99
+ * The browser caches node_modules files and optimized deps as immutable under
100
+ * a ?v= hash that no define value feeds, so a value inlined there would
101
+ * outlive every reload and restart. Those keep reading the defines vite
102
+ * injects through /@vite/env instead.
103
+ */
104
+ function browserCached(environment, id) {
105
+ if (!environment || environment.config.consumer === "server") return false;
106
+ if (nodeModulesPath.test(id)) return true;
107
+ return "depsOptimizer" in environment && !!environment.depsOptimizer?.isOptimizedDepFile(id);
108
+ }
109
+ /**
110
+ * Dev-server half of `publicConfigFile`. vite:define freezes its replacement
111
+ * table per environment when the server starts, so this plugin inlines the
112
+ * two baked expressions itself (it runs ahead of vite:define), remembers the
113
+ * modules that carried them, and when config.json changes rebakes,
114
+ * invalidates those modules and reloads every environment. The server never
115
+ * restarts, so the dep optimizer never runs again.
116
+ */
117
+ function publicConfigPlugin(options) {
118
+ const file = path.resolve(options.file);
119
+ const envFile = path.resolve(options.projectRoot, ".env");
120
+ const inlineKeys = options.keys ?? [];
121
+ const bakedModules = /* @__PURE__ */ new Map();
122
+ function readKeys() {
123
+ return mergePublicConfigKeys(inlineKeys, readPublicConfigKeys(file));
124
+ }
125
+ function bake(next) {
126
+ dotenv.config({
127
+ path: envFile,
128
+ quiet: true
129
+ });
130
+ bakePublicConfig(next);
131
+ }
132
+ let keys = readKeys();
133
+ bake(keys);
134
+ function refresh(server) {
135
+ const label = path.relative(process.cwd(), file);
136
+ let next;
137
+ try {
138
+ next = readKeys();
139
+ } catch (err) {
140
+ server.config.logger.warn(`${label}: ${err.message}; keeping the previous public keys`, { timestamp: true });
141
+ return;
142
+ }
143
+ if (next.length === keys.length && next.every((key, i) => key === keys[i])) return;
144
+ keys = next;
145
+ bake(keys);
146
+ for (const [name, ids] of bakedModules) {
147
+ const environment = server.environments[name];
148
+ if (!environment) continue;
149
+ for (const id of ids) {
150
+ const mod = environment.moduleGraph.getModuleById(id);
151
+ if (mod) environment.moduleGraph.invalidateModule(mod);
152
+ }
153
+ }
154
+ for (const environment of Object.values(server.environments)) environment.hot.send({
155
+ type: "full-reload",
156
+ path: "*"
157
+ });
158
+ server.config.logger.info(`${label} changed, reloading with public keys ${keys.join(", ")}`, { timestamp: true });
159
+ }
160
+ return {
161
+ name: "multiplatform-public-config",
162
+ apply: "serve",
163
+ async transform(code, id) {
164
+ if (nonScriptRequest.test(id) || !bakedExpression.test(code)) return;
165
+ if (browserCached(this.environment, id)) return;
166
+ let result;
167
+ try {
168
+ result = await transformWithOxc(code, id, {
169
+ lang: "js",
170
+ sourceType: "module",
171
+ define: bakedDefine(),
172
+ tsconfig: false
173
+ });
174
+ } catch {
175
+ return;
176
+ }
177
+ const name = this.environment?.name ?? "client";
178
+ let ids = bakedModules.get(name);
179
+ if (!ids) {
180
+ ids = /* @__PURE__ */ new Set();
181
+ bakedModules.set(name, ids);
182
+ }
183
+ ids.add(id);
184
+ return {
185
+ code: result.code,
186
+ map: result.map
187
+ };
188
+ },
189
+ configureServer(server) {
190
+ server.watcher.add(file);
191
+ const onChange = (changed) => {
192
+ if (path.resolve(changed) === file) refresh(server);
193
+ };
194
+ server.watcher.on("change", onChange);
195
+ server.watcher.on("add", onChange);
196
+ }
197
+ };
198
+ }
199
+
57
200
  //#endregion
58
201
  //#region src/vite.ts
59
202
  /**
@@ -66,11 +209,11 @@ function lookupProjectRoot() {
66
209
  * @example
67
210
  * ```ts
68
211
  * // apps/one/vite.config.ts
212
+ * import path from "node:path";
69
213
  * import { createViteConfig } from "@multiplatform.one/config/vite";
70
- * import { public as publicConfigKeys } from "../../features/config.json";
71
214
  *
72
215
  * export default createViteConfig({
73
- * publicConfigKeys,
216
+ * publicConfigFile: path.resolve(__dirname, "../../packages/config/config.json"),
74
217
  * one: { web: { deploy: "node", defaultRenderMode: "ssr" } },
75
218
  * tamagui: { config: "./config/tamagui.config.ts" },
76
219
  * i18n: { paths: ["../../features/i18n"] },
@@ -79,15 +222,18 @@ function lookupProjectRoot() {
79
222
  */
80
223
  function createViteConfig(options = {}) {
81
224
  ensureTamaguiWorkspacePaths();
82
- const { projectRoot: _projectRoot, publicConfigKeys = [], tamagui = true, i18n = false, one = false, ssr, server, plugins: extraPlugins = [], externalReactRouter, ssrDeps, ssrEntries, ssrCjsExternal, nativeFixes } = options;
225
+ const { projectRoot: _projectRoot, publicConfigKeys: inlinePublicConfigKeys = [], publicConfigFile, tamagui = true, i18n = false, one = false, ssr, server, plugins: extraPlugins = [], externalReactRouter, ssrDeps, ssrEntries, ssrCjsExternal, nativeFixes } = options;
83
226
  const projectRoot = _projectRoot || findProjectRoot();
227
+ const publicConfigKeys = publicConfigFile ? mergePublicConfigKeys(inlinePublicConfigKeys, readPublicConfigKeys(publicConfigFile)) : inlinePublicConfigKeys;
84
228
  dotenv.config({ path: path.resolve(projectRoot, ".env") });
85
- if (publicConfigKeys.length > 0) {
86
- process.env.VITE_MP_CONFIG = JSON.stringify(resolveConfig(publicConfigKeys));
87
- process.env.VITE_MP_PUBLIC_CONFIG_KEYS = JSON.stringify(publicConfigKeys);
88
- }
229
+ if (publicConfigKeys.length > 0) bakePublicConfig(publicConfigKeys);
89
230
  const vitePlugins = [];
90
231
  vitePlugins.push(preferBuiltWorkspaceEntryPlugin());
232
+ if (publicConfigFile) vitePlugins.push(publicConfigPlugin({
233
+ file: publicConfigFile,
234
+ projectRoot,
235
+ keys: inlinePublicConfigKeys
236
+ }));
91
237
  if (one) {
92
238
  vitePlugins.push(ssrReactNativeAliasPlugin());
93
239
  vitePlugins.push(clientBrokenEsmPlugin());
@@ -257,7 +403,10 @@ function clientBrokenEsmPlugin() {
257
403
  "react-is",
258
404
  "fast-deep-equal",
259
405
  "color",
260
- "query-string"
406
+ "query-string",
407
+ "react/jsx-runtime",
408
+ "react/jsx-dev-runtime",
409
+ "tamagui"
261
410
  ];
262
411
  const include = clientEnv.optimizeDeps.include ?? [];
263
412
  for (const dep of extras) if (!include.includes(dep)) include.push(dep);
@@ -482,11 +631,17 @@ function ssrCjsExternalFixPlugin(extras) {
482
631
  configResolved(config) {
483
632
  const external = config.ssr.external ?? [];
484
633
  if (!Array.isArray(external)) return;
485
- for (const e of extras) if (!external.includes(e)) external.push(e);
634
+ for (const e of extras) {
635
+ if (config.command === "build" && requiresReact(e)) continue;
636
+ if (!external.includes(e)) external.push(e);
637
+ }
486
638
  config.ssr.external = external;
487
639
  }
488
640
  };
489
641
  }
642
+ function requiresReact(id) {
643
+ return id === "use-sync-external-store" || id.startsWith("use-sync-external-store/");
644
+ }
490
645
  /**
491
646
  * Restore Object.prototype methods shadowed by hoisted globals on NATIVE.
492
647
  *
@@ -626,4 +781,4 @@ function preferBuiltWorkspaceEntryPlugin() {
626
781
  }
627
782
 
628
783
  //#endregion
629
- export { createViteConfig as t };
784
+ export { publicConfigPlugin as n, readPublicConfigKeys as r, createViteConfig as t };
package/lib/vite.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as createViteConfig } from "./vite-D6vMjPwv.js";
2
- import { n as publicPackageViteSourceAliases, t as discoverPublicPackageRoots } from "./workspacePublicPackages-B4FGgRYG.js";
1
+ import { n as publicConfigPlugin, r as readPublicConfigKeys, t as createViteConfig } from "./vite-DXXr5In0.js";
2
+ import { n as publicPackageViteSourceAliases, t as discoverPublicPackageRoots } from "./workspacePublicPackages-BuYRXxFs.js";
3
3
 
4
- export { createViteConfig, discoverPublicPackageRoots, publicPackageViteSourceAliases };
4
+ export { createViteConfig, discoverPublicPackageRoots, publicConfigPlugin, publicPackageViteSourceAliases, readPublicConfigKeys };
@@ -75,9 +75,16 @@ import path from "node:path";
75
75
  * clause and not the whole rule: `@tanstack/db` references the hook nowhere
76
76
  * and is pinned anyway, for the collection registry it holds at module scope.
77
77
  *
78
- * `@tanstack/react-query` references the hook in 25 shipped files and is not
79
- * listed. That is recorded rather than settled — no split has been observed
80
- * there, and pinning it reaches further than this measurement does.
78
+ * MPO-290. `@tanstack/react-query` references the hook in 25 shipped files
79
+ * and is deliberately NOT listed. It passes the grep, but a pin only earns its
80
+ * place where a second record can exist, and here none does. Measured at
81
+ * 5.100.7 on 0af30157c: seven workspace packages declare it and the lockfile
82
+ * resolves all seven to one snapshot, `5.100.7(react@19.2.5)`. The hoisted
83
+ * install holds one physical copy. So does the isolated layout an unset
84
+ * NPM_AUTH_TOKEN produced while .npmrc held the linker keys, rebuilt by
85
+ * stripping them from pnpm-workspace.yaml: fourteen links, one realpath. The
86
+ * spec re-checks both halves, so a second version or peer set fails there
87
+ * before it splits an app.
81
88
  */
82
89
  const CONTEXT_SINGLETONS = [
83
90
  "react-cookie",
package/lint.d.ts CHANGED
@@ -19,3 +19,11 @@ export function resolveRoots(roots?: ConventionRoots): {
19
19
  };
20
20
 
21
21
  export function runConventionChecks(options?: RunConventionChecksOptions): number;
22
+
23
+ export type SizeRecipeEscapeSite = {
24
+ line: number;
25
+ form: "prop" | "call" | "comment";
26
+ reason: string | undefined;
27
+ };
28
+
29
+ export function findSizeRecipeEscapes(src: string): SizeRecipeEscapeSite[];