@multiplatform.one/config 7.7.1 → 7.7.3

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.
Files changed (2) hide show
  1. package/lint.mjs +104 -1
  2. package/package.json +2 -2
package/lint.mjs CHANGED
@@ -37,7 +37,18 @@
37
37
  * target resolves, where react-native-web exports no X. The alias makes
38
38
  * that a module-evaluation SyntaxError, which takes the whole preview
39
39
  * bundle down rather than one component.
40
- * 9. Fail on an `import.meta` TOKEN in public/<pkg>/src (MPO-204) — in a
40
+ * 9. Fail on a `.react-flow__` SELECTOR outside the token bridge (MPO-191) —
41
+ * React Flow paints through `var(--xy-thing, var(--xy-thing-default))`, so
42
+ * every colour, width and shadow is settable without touching its
43
+ * stylesheet. A hand-written `.react-flow__` rule anywhere else forks that
44
+ * paint: it wins or loses on specificity rather than on the knob, and the
45
+ * next library upgrade silently changes which. The whole point of the
46
+ * bridge is that a designer never touches React Flow CSS — including us —
47
+ * so the rule is mechanical. `theme/graphVars.ts` (the map) and the
48
+ * package's own `css/` tree (outside `src`) are the two places it may live.
49
+ * Comments and specs are exempt; strings are not, because a hand-written
50
+ * rule lives in a template literal.
51
+ * 10. Fail on an `import.meta` TOKEN in public/<pkg>/src (MPO-204) — in a
41
52
  * classic script it is a PARSE-time SyntaxError, and two bundles execute
42
53
  * mpo as one (the vxrn/rolldown Hermes bundle, and any Metro bundle for
43
54
  * platform=web, i.e. an Expo DOM component). Node-only build tooling is
@@ -1183,6 +1194,88 @@ function checkImportMetaInPublicSource() {
1183
1194
  return violations;
1184
1195
  }
1185
1196
 
1197
+ // ── React Flow selectors outside the token bridge (MPO-191) ───────────────
1198
+
1199
+ const REACT_FLOW_SELECTOR_RE = /\.react-flow__[\w-]+/g;
1200
+ /**
1201
+ * The two files a `.react-flow__` string may live in, both DECLARED boundaries.
1202
+ *
1203
+ * `theme/graphVars.ts` owns the paint: every React Flow surface is set through
1204
+ * its own `--xy-*` variable there, never through a rule.
1205
+ *
1206
+ * `react-flow/xy.ts` owns the @xyflow DOM contract — it is already the one file
1207
+ * allowed to import the library at all (oxlint `no-restricted-imports`), and a
1208
+ * class NAME is part of that contract the same way an exported symbol is. The
1209
+ * long press has to find the pane to listen on it, which is a DOM lookup and
1210
+ * not a paint rule; putting the string at the call site would mean a library
1211
+ * rename lands in a `querySelector` that returns null in silence.
1212
+ */
1213
+ const GRAPH_VARS_EXEMPT = ["theme/graphVars.ts", "react-flow/xy.ts"];
1214
+ const REACT_FLOW_SCANNED_RE = /\.(tsx|ts|jsx|js|mjs|cjs|css)$/;
1215
+
1216
+ /**
1217
+ * Offsets that are NOT inside a comment.
1218
+ *
1219
+ * `codeOffsets` masks strings as well, which is the opposite of what this needs:
1220
+ * a hand-written React Flow rule lives in a template literal or a `className`,
1221
+ * so strings are exactly the interesting part. Comments are not — the map's own
1222
+ * allowlist explains itself by naming `.react-flow__node-input`, and that
1223
+ * explanation must not be the thing that fails the build.
1224
+ * @param {string} src
1225
+ */
1226
+ function nonCommentOffsets(src) {
1227
+ const mask = new Uint8Array(src.length).fill(1);
1228
+ let i = 0;
1229
+ while (i < src.length) {
1230
+ const char = src[i];
1231
+ const next = src[i + 1];
1232
+ if (char === "/" && next === "/") {
1233
+ while (i < src.length && src[i] !== "\n") mask[i++] = 0;
1234
+ continue;
1235
+ }
1236
+ if (char === "/" && next === "*") {
1237
+ while (i < src.length && !(src[i] === "*" && src[i + 1] === "/")) mask[i++] = 0;
1238
+ mask[i] = 0;
1239
+ mask[i + 1] = 0;
1240
+ i += 2;
1241
+ continue;
1242
+ }
1243
+ i++;
1244
+ }
1245
+ return mask;
1246
+ }
1247
+
1248
+ /**
1249
+ * MPO-191: a `.react-flow__` selector under public/<pkg>/src, outside the one
1250
+ * file that owns the mapping.
1251
+ */
1252
+ function checkGraphVarsCoverage() {
1253
+ /** @type {string[]} */
1254
+ const violations = [];
1255
+ if (!existsSync(PUBLIC_DIR)) return violations;
1256
+ for (const pkg of readdirSync(PUBLIC_DIR, { withFileTypes: true })) {
1257
+ if (!pkg.isDirectory() || pkg.name.startsWith(".") || pkg.name === "node_modules") continue;
1258
+ walkFiles(join(PUBLIC_DIR, pkg.name, "src"), (filePath) => {
1259
+ if (!REACT_FLOW_SCANNED_RE.test(filePath)) return;
1260
+ const posix = filePath.replace(/\\/g, "/");
1261
+ if (GRAPH_VARS_EXEMPT.some((exempt) => posix.endsWith(exempt))) return;
1262
+ // Specs are exempt for the reason check 10 exempts them: a spec that
1263
+ // asserts WHICH classes a variable binds to has to name those classes,
1264
+ // and it ships no paint. Nothing else is exempt, including stories.
1265
+ if (/\.(spec|test)\./.test(filePath)) return;
1266
+ const src = readFileSync(filePath, "utf8");
1267
+ if (!src.includes(".react-flow__")) return;
1268
+ const mask = nonCommentOffsets(src);
1269
+ for (const match of src.matchAll(REACT_FLOW_SELECTOR_RE)) {
1270
+ const index = match.index ?? 0;
1271
+ if (mask[index] !== 1) continue;
1272
+ violations.push(`${relative(ROOT, filePath)}:${lineAt(src, index)} — ${match[0]}`);
1273
+ }
1274
+ });
1275
+ }
1276
+ return violations;
1277
+ }
1278
+
1186
1279
  /**
1187
1280
  * Run structural convention checks over a consumer tree.
1188
1281
  *
@@ -1200,6 +1293,7 @@ export function runConventionChecks(options = {}) {
1200
1293
  const metroFallbacks = checkMetroSubpathFallbacks();
1201
1294
  const rnWebExports = checkReactNativeWebExports();
1202
1295
  const importMeta = checkImportMetaInPublicSource();
1296
+ const graphVars = checkGraphVarsCoverage();
1203
1297
  const tokens = checkTransitionTokens();
1204
1298
  const styleTokens = checkStyleTokens();
1205
1299
  const themeNames = checkThemeNames();
@@ -1293,6 +1387,15 @@ export function runConventionChecks(options = {}) {
1293
1387
  console.error("");
1294
1388
  }
1295
1389
 
1390
+ if (graphVars.length) {
1391
+ failed = true;
1392
+ console.error(
1393
+ "Convention (MPO-191 GRAPH VARS COVERAGE): a `.react-flow__` selector belongs in the token bridge, nowhere else. React Flow reads every paint through `var(--xy-thing, var(--xy-thing-default))`, so the whole surface is settable without touching its stylesheet; a rule written anywhere else wins or loses on specificity instead of on the knob, and the next @xyflow/react upgrade silently changes which. Set the variable in `theme/graphVars.ts`, or put the rule in the package's own `css/` tree (outside `src`) where a reviewer reads it whole. A DOM LOOKUP is the one other legitimate use, and it belongs in `react-flow/xy.ts` beside the rest of the @xyflow contract. A mention inside a comment is fine and is not flagged.\n",
1394
+ );
1395
+ for (const msg of graphVars) console.error(` ${msg}`);
1396
+ console.error("");
1397
+ }
1398
+
1296
1399
  if (importMeta.length) {
1297
1400
  failed = true;
1298
1401
  console.error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplatform.one/config",
3
- "version": "7.7.1",
3
+ "version": "7.7.3",
4
4
  "description": "Shared build and test configuration presets for multiplatform.one",
5
5
  "keywords": [
6
6
  "config",
@@ -102,7 +102,7 @@
102
102
  "vite-plugin-external": "^6.2.2",
103
103
  "vite-plugin-i18next-loader": "^3.1.3",
104
104
  "vitest": "^4.1.5",
105
- "@multiplatform.one/utils": "7.7.1"
105
+ "@multiplatform.one/utils": "7.7.3"
106
106
  },
107
107
  "devDependencies": {
108
108
  "tsdown": "^0.21.10",