@raresportan/marram 0.1.0 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raresportan/marram",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "The gate, ratchet lints and journeys that keep agent-written code from drifting.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -4,8 +4,11 @@
4
4
  // A project names its layers by folder pattern (`features/*/client`, `*` being one folder). A file may import from its
5
5
  // own layer and from the layers it lists. It may reach another feature (`features/<name>`) only through an `index.ts`:
6
6
  // the feature's own, or that of one of its sides, as an app's main process needs its host and its renderer its client,
7
- // and one index for both would pull the host into the renderer. Only a layer with `builtins: true` may import `node:` modules or `electron`. Existing violations go on a
8
- // ratchet list, by `file -> import`.
7
+ // and one index for both would pull the host into the renderer. An index only opens the feature: the side it belongs
8
+ // to must still be a layer the file may import. A file in no layer, such as a feature's own index, is no way around
9
+ // that: importing it counts as importing what it imports. Only a layer with `builtins: true` may import Node's modules,
10
+ // as `node:fs` or `fs`, or `electron`. Existing violations go on a ratchet list, by `file -> import`.
11
+ import { isBuiltin } from "node:module";
9
12
  import { dirname, relative, resolve, sep } from "node:path";
10
13
  import ts from "typescript";
11
14
  import { lineOf, parse, ratchet, sourceFiles } from "./ratchet.mjs";
@@ -31,21 +34,36 @@ const featureOf = (path) => /^(features\/[^/]+)(\/|$)/.exec(path)?.[1];
31
34
  /** A feature's way in: its folder's `index.ts`, or the `index.ts` of one of its sides (`host/index.ts`, `client/index.ts`). */
32
35
  const isEntry = (target, feature) => target === feature || /^features\/[^/]+(\/[^/]+)?\/index(\.tsx?)?$/.test(target);
33
36
 
34
- const isBuiltin = (specifier) => specifier === "electron" || specifier.startsWith("node:");
37
+ const needsHost = (specifier) => /^electron(\/|$)/.test(specifier) || specifier.startsWith("node:") || isBuiltin(specifier);
35
38
 
36
- /** What's wrong with `from` (a path from the folder) importing `specifier`, if anything. */
37
- function problem(from, specifier, layers) {
38
- const mine = layerOf(from, layers);
39
- if (isBuiltin(specifier)) return layers[mine?.pattern]?.builtins === true || mine === undefined ? undefined : `${mine.pattern} may not import ${specifier}, which needs a host`;
39
+ /** The source file an import's target (a path from the folder) means, however it's spelled, if it's one of `sources`. */
40
+ const fileOf = (target, sources) => [target, target.replace(/\.jsx?$/, ".ts"), target.replace(/\.jsx?$/, ".tsx"), `${target}.ts`, `${target}.tsx`, `${target}/index.ts`, `${target}/index.tsx`].find((path) => sources.has(path));
41
+
42
+ /** What's wrong with `from` (a path from the folder) importing `specifier`, if anything, for a file in the layer `mine`. */
43
+ function problem(from, specifier, layers, sources, mine = layerOf(from, layers), seen = new Set()) {
44
+ if (needsHost(specifier)) return layers[mine?.pattern]?.builtins === true || mine === undefined ? undefined : `${mine.pattern} may not import ${specifier}, which needs a host`;
40
45
  if (!specifier.startsWith(".")) return undefined;
41
46
  const target = posix(relative(".", resolve(dirname(from), specifier)));
42
47
  const theirFeature = featureOf(target);
43
- if (theirFeature !== undefined && theirFeature !== featureOf(from)) return isEntry(target, theirFeature) ? undefined : `reaches inside ${theirFeature}: import an index.ts`;
48
+ if (theirFeature !== undefined && theirFeature !== featureOf(from) && !isEntry(target, theirFeature)) return `reaches inside ${theirFeature}: import an index.ts`;
44
49
  const theirs = layerOf(target, layers);
45
- if (mine === undefined || theirs === undefined || theirs.pattern === mine.pattern) return undefined;
50
+ if (mine === undefined) return undefined;
51
+ if (theirs === undefined) return throughFile(fileOf(target, sources), layers, sources, mine, seen);
52
+ if (theirs.pattern === mine.pattern) return undefined;
46
53
  return (layers[mine.pattern].may ?? []).includes(theirs.pattern) ? undefined : `${mine.pattern} may not import from ${theirs.pattern}`;
47
54
  }
48
55
 
56
+ /** What's wrong with a file in the layer `mine` importing `file`, which is in no layer: the first of `file`'s imports it may not have. */
57
+ function throughFile(file, layers, sources, mine, seen) {
58
+ if (file === undefined || seen.has(file)) return undefined;
59
+ seen.add(file);
60
+ for (const specifier of imports(parse(sources.get(file)))) {
61
+ const wrong = problem(file, specifier.text, layers, sources, mine, seen);
62
+ if (wrong !== undefined) return `${wrong} (through ${file})`;
63
+ }
64
+ return undefined;
65
+ }
66
+
49
67
  /** Import specifiers in `source`, including `export … from` and `import()`, each with its node; types count too. */
50
68
  function imports(source) {
51
69
  const found = [];
@@ -64,11 +82,13 @@ function imports(source) {
64
82
  */
65
83
  export function importBoundaries({ root, folder, layers, allowed = {}, list }) {
66
84
  const base = resolve(root, folder);
67
- const found = sourceFiles(base).flatMap((file) => {
85
+ const files = sourceFiles(base);
86
+ const sources = new Map(files.map((file) => [posix(relative(base, file)), file]));
87
+ const found = files.flatMap((file) => {
68
88
  const source = parse(file);
69
89
  const from = posix(relative(base, file));
70
90
  return imports(source).flatMap((specifier) => {
71
- const wrong = problem(from, specifier.text, layers);
91
+ const wrong = problem(from, specifier.text, layers, sources);
72
92
  return wrong === undefined ? [] : [{ key: `${from} -> ${specifier.text}`, at: `${folder}/${from}:${lineOf(source, specifier)}`, what: wrong }];
73
93
  });
74
94
  });