@raresportan/marram 0.1.0 → 0.2.1
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 +1 -1
- package/src/core/boundaries.mjs +31 -11
- package/src/core/config.mjs +1 -0
- package/src/core/gate.mjs +21 -4
package/package.json
CHANGED
package/src/core/boundaries.mjs
CHANGED
|
@@ -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.
|
|
8
|
-
//
|
|
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
|
|
37
|
+
const needsHost = (specifier) => /^electron(\/|$)/.test(specifier) || specifier.startsWith("node:") || isBuiltin(specifier);
|
|
35
38
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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)
|
|
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
|
|
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
|
|
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
|
});
|
package/src/core/config.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// gate: {
|
|
5
5
|
// logs: "where each run's logs go",
|
|
6
6
|
// progress: /lines from a stage worth showing as it runs/, // besides ✓ and ✗ lines
|
|
7
|
+
// sleepGapMs: 30000, // how long a pause counts as the machine sleeping, so a stage that failed runs again
|
|
7
8
|
// tiers: { fast: [stage, …], default: [stage, …], full: [stage, …] }, // stage: { name, run: [command, …args], env? }
|
|
8
9
|
// },
|
|
9
10
|
// review: { classes: "the mistake-classes doc", rungs: ["type", "lint", …] },
|
package/src/core/gate.mjs
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
// full The default gate and more (a build, the slow journeys). Before merging UI work, or nightly.
|
|
6
6
|
// What each tier runs is the project's: `gate.tiers` in marram.config.mjs. Stages run in order and the first failure
|
|
7
7
|
// stops the gate. Each stage's output is kept in its own log under `gate.logs`/gate-<tier>-<time>/, with report.json.
|
|
8
|
+
// A stage that fails after the machine slept during it runs again: timeouts ran out and frames stopped while it slept,
|
|
9
|
+
// so that failure says nothing about the code. A pause counts as sleep past `gate.sleepGapMs`, 30 seconds by default.
|
|
8
10
|
// The default and full gates record their verdict for the working tree (gate-state.mjs), which the hooks reuse.
|
|
9
11
|
import { spawn } from "node:child_process";
|
|
10
12
|
import { createWriteStream } from "node:fs";
|
|
11
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
13
|
+
import { mkdir, rename, writeFile } from "node:fs/promises";
|
|
12
14
|
import { join } from "node:path";
|
|
13
15
|
import { treeOf, updateState } from "./gate-state.mjs";
|
|
14
16
|
|
|
@@ -19,7 +21,7 @@ export async function gate(root, config, [tier = "default"]) {
|
|
|
19
21
|
console.error(`Unknown gate "${tier}". Usage: marram gate [fast | full]`);
|
|
20
22
|
process.exit(1);
|
|
21
23
|
}
|
|
22
|
-
const { logs, progress, tiers } = config.gate;
|
|
24
|
+
const { logs, progress, tiers, sleepGapMs = 30_000 } = config.gate;
|
|
23
25
|
const dir = join(root, logs, `gate-${tier}-${new Date().toISOString().replace(/[:.]/g, "-")}`);
|
|
24
26
|
await mkdir(dir, { recursive: true });
|
|
25
27
|
const report = { tier, startedAt: new Date().toISOString(), stages: [], passed: false };
|
|
@@ -50,16 +52,26 @@ export async function gate(root, config, [tier = "default"]) {
|
|
|
50
52
|
child.stdout.on("data", (chunk) => {
|
|
51
53
|
for (const line of chunk.toString().split("\n")) if (/^\s*[✓✗]/.test(line) || progress?.test(line)) console.log(` ${name}: ${line.trim()}`);
|
|
52
54
|
});
|
|
55
|
+
let last = Date.now();
|
|
56
|
+
let slept = 0;
|
|
57
|
+
const noticeSleep = () => {
|
|
58
|
+
const gap = Date.now() - last;
|
|
59
|
+
last += gap;
|
|
60
|
+
if (gap > sleepGapMs) slept += gap;
|
|
61
|
+
};
|
|
62
|
+
const clock = setInterval(noticeSleep, 1000);
|
|
53
63
|
const code = await new Promise((done) => {
|
|
54
64
|
child.on("error", () => done(1));
|
|
55
65
|
child.on("exit", (exit) => done(exit ?? 1));
|
|
56
66
|
});
|
|
67
|
+
clearInterval(clock);
|
|
68
|
+
noticeSleep();
|
|
57
69
|
log.end();
|
|
58
70
|
const seconds = Math.round((Date.now() - started) / 1000);
|
|
59
71
|
report.stages.push({ name, code, seconds });
|
|
60
72
|
await writeFile(join(dir, "report.json"), JSON.stringify(report, null, 2));
|
|
61
73
|
console.log(`${code === 0 ? "✓" : "✗"} ${name} (${seconds}s)${code === 0 ? "" : `: see ${join(dir, `${name}.log`)}`}`);
|
|
62
|
-
return code === 0;
|
|
74
|
+
return { passed: code === 0, slept };
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
// A fast pass doesn't make a change done, so only the default and full gates record a verdict.
|
|
@@ -70,7 +82,12 @@ export async function gate(root, config, [tier = "default"]) {
|
|
|
70
82
|
|
|
71
83
|
console.log(`Gate (${tier}). Logs: ${dir}`);
|
|
72
84
|
for (const stage of tiers[tier]) {
|
|
73
|
-
|
|
85
|
+
let { passed, slept } = await runStage(stage);
|
|
86
|
+
while (!passed && slept > 0 && !interrupted) {
|
|
87
|
+
console.log(` The machine slept for ${Math.round(slept / 1000)}s during ${stage.name}, so its failure says nothing about the code. Running it again.`);
|
|
88
|
+
await rename(join(dir, `${stage.name}.log`), join(dir, `${stage.name}-slept.log`));
|
|
89
|
+
({ passed, slept } = await runStage(stage));
|
|
90
|
+
}
|
|
74
91
|
if (interrupted) {
|
|
75
92
|
console.log(`\nGate interrupted at ${stage.name}.`);
|
|
76
93
|
process.exit(1);
|