laymos 0.0.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/LICENSE +21 -0
- package/README.md +740 -0
- package/dist/cli/cli.d.ts +3 -0
- package/dist/cli/cli.d.ts.map +1 -0
- package/dist/cli/cli.js +6 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli/run.d.ts +11 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +242 -0
- package/dist/config/define-config.d.ts +3 -0
- package/dist/config/define-config.d.ts.map +1 -0
- package/dist/config/define-config.js +228 -0
- package/dist/config/index.d.ts +5 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +4 -0
- package/dist/config/layer-graph.d.ts +7 -0
- package/dist/config/layer-graph.d.ts.map +1 -0
- package/dist/config/layer-graph.js +53 -0
- package/dist/config/layer.d.ts +5 -0
- package/dist/config/layer.d.ts.map +1 -0
- package/dist/config/layer.js +21 -0
- package/dist/config/module.d.ts +9 -0
- package/dist/config/module.d.ts.map +1 -0
- package/dist/config/module.js +29 -0
- package/dist/config/path.d.ts +4 -0
- package/dist/config/path.d.ts.map +1 -0
- package/dist/config/path.js +32 -0
- package/dist/config/types.d.ts +36 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +1 -0
- package/dist/engine/1-extract/extract.d.ts +12 -0
- package/dist/engine/1-extract/extract.d.ts.map +1 -0
- package/dist/engine/1-extract/extract.js +160 -0
- package/dist/engine/1-extract/index.d.ts +3 -0
- package/dist/engine/1-extract/index.d.ts.map +1 -0
- package/dist/engine/1-extract/index.js +1 -0
- package/dist/engine/2-resolve/index.d.ts +3 -0
- package/dist/engine/2-resolve/index.d.ts.map +1 -0
- package/dist/engine/2-resolve/index.js +1 -0
- package/dist/engine/2-resolve/resolve.d.ts +24 -0
- package/dist/engine/2-resolve/resolve.d.ts.map +1 -0
- package/dist/engine/2-resolve/resolve.js +74 -0
- package/dist/engine/3-evaluate/evaluate.d.ts +10 -0
- package/dist/engine/3-evaluate/evaluate.d.ts.map +1 -0
- package/dist/engine/3-evaluate/evaluate.js +80 -0
- package/dist/engine/3-evaluate/index.d.ts +3 -0
- package/dist/engine/3-evaluate/index.d.ts.map +1 -0
- package/dist/engine/3-evaluate/index.js +1 -0
- package/dist/engine/4-emit/emit.d.ts +7 -0
- package/dist/engine/4-emit/emit.d.ts.map +1 -0
- package/dist/engine/4-emit/emit.js +77 -0
- package/dist/engine/4-emit/index.d.ts +2 -0
- package/dist/engine/4-emit/index.d.ts.map +1 -0
- package/dist/engine/4-emit/index.js +1 -0
- package/dist/engine/errors.d.ts +19 -0
- package/dist/engine/errors.d.ts.map +1 -0
- package/dist/engine/errors.js +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/node.d.ts +24 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +139 -0
- package/dist/report/architecture.d.ts +104 -0
- package/dist/report/architecture.d.ts.map +1 -0
- package/dist/report/architecture.js +1 -0
- package/dist/report/index.d.ts +3 -0
- package/dist/report/index.d.ts.map +1 -0
- package/dist/report/index.js +2 -0
- package/dist/report/stories.d.ts +22 -0
- package/dist/report/stories.d.ts.map +1 -0
- package/dist/report/stories.js +1 -0
- package/dist/story/artifact/artifact.d.ts +57 -0
- package/dist/story/artifact/artifact.d.ts.map +1 -0
- package/dist/story/artifact/artifact.js +234 -0
- package/dist/story/artifact/index.d.ts +2 -0
- package/dist/story/artifact/index.d.ts.map +1 -0
- package/dist/story/artifact/index.js +1 -0
- package/dist/story/artifact/storage.d.ts +10 -0
- package/dist/story/artifact/storage.d.ts.map +1 -0
- package/dist/story/artifact/storage.js +162 -0
- package/dist/story/artifact/types.d.ts +74 -0
- package/dist/story/artifact/types.d.ts.map +1 -0
- package/dist/story/artifact/types.js +1 -0
- package/dist/story/core/blocks.d.ts +20 -0
- package/dist/story/core/blocks.d.ts.map +1 -0
- package/dist/story/core/blocks.js +104 -0
- package/dist/story/core/declare.d.ts +63 -0
- package/dist/story/core/declare.d.ts.map +1 -0
- package/dist/story/core/declare.js +62 -0
- package/dist/story/core/index.d.ts +4 -0
- package/dist/story/core/index.d.ts.map +1 -0
- package/dist/story/core/index.js +1 -0
- package/dist/story/core/recorder.d.ts +38 -0
- package/dist/story/core/recorder.d.ts.map +1 -0
- package/dist/story/core/recorder.js +28 -0
- package/dist/story/core/runtime-context.d.ts +14 -0
- package/dist/story/core/runtime-context.d.ts.map +1 -0
- package/dist/story/core/runtime-context.js +19 -0
- package/dist/story/core/types.d.ts +21 -0
- package/dist/story/core/types.d.ts.map +1 -0
- package/dist/story/core/types.js +1 -0
- package/dist/story/effect/effect.d.ts +52 -0
- package/dist/story/effect/effect.d.ts.map +1 -0
- package/dist/story/effect/effect.js +99 -0
- package/dist/story/effect/index.d.ts +3 -0
- package/dist/story/effect/index.d.ts.map +1 -0
- package/dist/story/effect/index.js +1 -0
- package/dist/story/runner/index.d.ts +3 -0
- package/dist/story/runner/index.d.ts.map +1 -0
- package/dist/story/runner/index.js +1 -0
- package/dist/story/runner/runner.d.ts +44 -0
- package/dist/story/runner/runner.d.ts.map +1 -0
- package/dist/story/runner/runner.js +476 -0
- package/dist/story/story-runtime/index.d.ts +2 -0
- package/dist/story/story-runtime/index.d.ts.map +1 -0
- package/dist/story/story-runtime/index.js +1 -0
- package/dist/story/story-runtime/story-runtime.d.ts +52 -0
- package/dist/story/story-runtime/story-runtime.d.ts.map +1 -0
- package/dist/story/story-runtime/story-runtime.js +194 -0
- package/dist/story/vanilla/index.d.ts +29 -0
- package/dist/story/vanilla/index.d.ts.map +1 -0
- package/dist/story/vanilla/index.js +78 -0
- package/package.json +68 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli/cli.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Effect } from 'effect';
|
|
3
|
+
import * as NodeRuntime from '@effect/platform-node/NodeRuntime';
|
|
4
|
+
import * as NodeServices from '@effect/platform-node/NodeServices';
|
|
5
|
+
import { cli } from './run.js';
|
|
6
|
+
cli.pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain());
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { cli, command, lintPasses } from './run.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import { Command } from 'effect/unstable/cli';
|
|
3
|
+
import type { LaymosReport } from '../report/index.js';
|
|
4
|
+
declare class CliExit {
|
|
5
|
+
readonly _tag = "CliExit";
|
|
6
|
+
}
|
|
7
|
+
export declare function lintPasses(report: Pick<LaymosReport, 'violations'>): boolean;
|
|
8
|
+
export declare const command: Command.Command<"laymos", {} | {}, {}, CliExit, never>;
|
|
9
|
+
export declare const cli: Effect.Effect<void, CliExit | import("effect/unstable/cli/CliError").CliError, Command.Environment>;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/cli/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,MAAM,EAAU,MAAM,QAAQ,CAAC;AAClD,OAAO,EAAY,OAAO,EAAQ,MAAM,qBAAqB,CAAC;AAI9D,OAAO,KAAK,EAAE,YAAY,EAAa,MAAM,oBAAoB,CAAC;AAQlE,cAAM,OAAO;IACX,QAAQ,CAAC,IAAI,aAAa;CAC3B;AAaD,wBAAgB,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,GAAG,OAAO,CAE5E;AAqVD,eAAO,MAAM,OAAO,wDAEnB,CAAC;AAEF,eAAO,MAAM,GAAG,qGAAkD,CAAC"}
|
package/dist/cli/run.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { Duration, Effect, Option } from 'effect';
|
|
2
|
+
import { Argument, Command, Flag } from 'effect/unstable/cli';
|
|
3
|
+
import { analyzeProject, runStories, runStoryGroup } from '../node.js';
|
|
4
|
+
class CliExit {
|
|
5
|
+
_tag = 'CliExit';
|
|
6
|
+
}
|
|
7
|
+
const paint = (stream, code) => (text) => stream.isTTY && process.env['NO_COLOR'] === undefined
|
|
8
|
+
? `\x1b[${code}m${text}\x1b[0m`
|
|
9
|
+
: text;
|
|
10
|
+
const green = paint(process.stdout, '32');
|
|
11
|
+
const yellow = paint(process.stdout, '33');
|
|
12
|
+
const red = paint(process.stderr, '31');
|
|
13
|
+
export function lintPasses(report) {
|
|
14
|
+
return report.violations.length === 0;
|
|
15
|
+
}
|
|
16
|
+
function lint() {
|
|
17
|
+
return analyzeProject(process.cwd()).pipe(Effect.flatMap((report) => Effect.suspend(() => {
|
|
18
|
+
reportCoverage(report);
|
|
19
|
+
reportWarnings(report);
|
|
20
|
+
if (lintPasses(report)) {
|
|
21
|
+
process.stdout.write(green('No violations found.\n'));
|
|
22
|
+
return Effect.void;
|
|
23
|
+
}
|
|
24
|
+
reportViolations(report.violations);
|
|
25
|
+
return Effect.fail(new CliExit());
|
|
26
|
+
})), Effect.catch((error) => error instanceof CliExit
|
|
27
|
+
? Effect.fail(error)
|
|
28
|
+
: Effect.sync(() => {
|
|
29
|
+
process.stderr.write(red(`Error: ${errorMessage(error)}\n`));
|
|
30
|
+
}).pipe(Effect.andThen(Effect.fail(new CliExit())))));
|
|
31
|
+
}
|
|
32
|
+
function stories(files, timeout, group) {
|
|
33
|
+
return Effect.suspend(() => {
|
|
34
|
+
let options;
|
|
35
|
+
let selectedGroupPath;
|
|
36
|
+
try {
|
|
37
|
+
options = storyRunOptions(timeout);
|
|
38
|
+
selectedGroupPath = Option.isSome(group)
|
|
39
|
+
? groupPath(group.value)
|
|
40
|
+
: undefined;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
process.stderr.write(red(`Error: ${errorMessage(error)}\n`));
|
|
44
|
+
return Effect.fail(new CliExit());
|
|
45
|
+
}
|
|
46
|
+
if (Option.isSome(group) && files.length > 0) {
|
|
47
|
+
process.stderr.write(red('Error: --group cannot be combined with Story file arguments\n'));
|
|
48
|
+
return Effect.fail(new CliExit());
|
|
49
|
+
}
|
|
50
|
+
const run = selectedGroupPath
|
|
51
|
+
? runStoryGroup(process.cwd(), selectedGroupPath, options)
|
|
52
|
+
: runStories(process.cwd(), files, options);
|
|
53
|
+
return run.pipe(Effect.flatMap((result) => Effect.suspend(() => {
|
|
54
|
+
reportStoriesRun(result);
|
|
55
|
+
if (result.status === 'passed')
|
|
56
|
+
return Effect.void;
|
|
57
|
+
return Effect.fail(new CliExit());
|
|
58
|
+
})), Effect.catch((error) => error instanceof CliExit
|
|
59
|
+
? Effect.fail(error)
|
|
60
|
+
: Effect.sync(() => {
|
|
61
|
+
process.stderr.write(red(`Error: ${errorMessage(error)}\n`));
|
|
62
|
+
}).pipe(Effect.andThen(Effect.fail(new CliExit())))));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function groupPath(input) {
|
|
66
|
+
const path = input.split('/').map((segment) => segment.trim());
|
|
67
|
+
if (path.length === 0 || path.some((segment) => segment.length === 0)) {
|
|
68
|
+
throw new Error(`Invalid --group "${input}"; use a path such as "DynamoDB / Entities"`);
|
|
69
|
+
}
|
|
70
|
+
return path;
|
|
71
|
+
}
|
|
72
|
+
function storyRunOptions(timeout) {
|
|
73
|
+
if (Option.isNone(timeout))
|
|
74
|
+
return {};
|
|
75
|
+
const input = timeout.value;
|
|
76
|
+
let millis;
|
|
77
|
+
try {
|
|
78
|
+
millis = Duration.toMillis(input);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
millis = Number.NaN;
|
|
82
|
+
}
|
|
83
|
+
if (!Number.isFinite(millis) || millis <= 0) {
|
|
84
|
+
throw new Error(`Invalid --timeout "${timeout.value}"; use a duration such as "90 seconds"`);
|
|
85
|
+
}
|
|
86
|
+
return { timeout: input };
|
|
87
|
+
}
|
|
88
|
+
function reportStoriesRun(result) {
|
|
89
|
+
const stories = Object.entries(result.report.stories);
|
|
90
|
+
process.stdout.write(`${stories.length} ${stories.length === 1 ? 'Story' : 'Stories'} executed.\n`);
|
|
91
|
+
for (const [storyId, artifact] of stories) {
|
|
92
|
+
reportStory(storyId, artifact);
|
|
93
|
+
}
|
|
94
|
+
for (const failure of result.failures) {
|
|
95
|
+
const scope = failure.scenario === undefined
|
|
96
|
+
? failure.storyId
|
|
97
|
+
: `${failure.storyId} › ${failure.scenario}`;
|
|
98
|
+
const phase = failure.phase === undefined ? '' : ` [${failure.phase}]`;
|
|
99
|
+
process.stderr.write(red(` - ${scope}${phase}: ${failure.message}\n`));
|
|
100
|
+
}
|
|
101
|
+
if (result.status === 'passed') {
|
|
102
|
+
process.stdout.write(green('All Scenarios passed.\n'));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
process.stderr.write(red('Some Scenarios failed or were interrupted.\n'));
|
|
106
|
+
}
|
|
107
|
+
function reportStory(storyId, artifact) {
|
|
108
|
+
const passed = artifact.scenarios.every(({ outcome }) => outcome === 'succeeded' || outcome === 'skipped');
|
|
109
|
+
process.stdout.write(`\n${passed ? green('✓') : red('×')} ${artifact.name} ${paintMuted(`(${storyId})`)}\n`);
|
|
110
|
+
for (const scenario of artifact.scenarios) {
|
|
111
|
+
const icon = scenario.outcome === 'succeeded'
|
|
112
|
+
? green('✓')
|
|
113
|
+
: scenario.outcome === 'skipped'
|
|
114
|
+
? paintMuted('○')
|
|
115
|
+
: scenario.outcome === 'interrupted'
|
|
116
|
+
? yellow('!')
|
|
117
|
+
: red('×');
|
|
118
|
+
const timing = scenario.durationMillis === undefined
|
|
119
|
+
? ''
|
|
120
|
+
: paintMuted(` ${scenario.durationMillis}ms`);
|
|
121
|
+
process.stdout.write(` ${icon} ${scenario.name}${timing}\n`);
|
|
122
|
+
reportUnsuccessfulPaths(artifact, scenario.execution);
|
|
123
|
+
}
|
|
124
|
+
reportDecisionGaps(artifact);
|
|
125
|
+
}
|
|
126
|
+
function reportUnsuccessfulPaths(artifact, execution, parents = []) {
|
|
127
|
+
for (const item of execution) {
|
|
128
|
+
if ('parallel' in item) {
|
|
129
|
+
for (const branch of item.parallel) {
|
|
130
|
+
reportUnsuccessfulPaths(artifact, branch, parents);
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const name = artifact.blocks[item.blockId]?.name ?? item.blockId;
|
|
135
|
+
const path = [...parents, name];
|
|
136
|
+
if (item.outcome !== 'succeeded') {
|
|
137
|
+
process.stdout.write(` ${yellow(item.outcome)}: ${path.join(' › ')}\n`);
|
|
138
|
+
}
|
|
139
|
+
reportUnsuccessfulPaths(artifact, item.children, path);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function reportDecisionGaps(artifact) {
|
|
143
|
+
for (const [blockId, block] of Object.entries(artifact.blocks)) {
|
|
144
|
+
if (block.kind !== 'decision')
|
|
145
|
+
continue;
|
|
146
|
+
const observed = new Set();
|
|
147
|
+
for (const scenario of artifact.scenarios) {
|
|
148
|
+
collectSelectedArms(scenario.execution, blockId, observed);
|
|
149
|
+
}
|
|
150
|
+
const missing = block.arms.filter((arm) => !observed.has(selectedArmKey(arm)));
|
|
151
|
+
if (missing.length === 0)
|
|
152
|
+
continue;
|
|
153
|
+
const location = `${block.location.file}:${block.location.line}:${block.location.column}`;
|
|
154
|
+
process.stdout.write(` ${paintMuted('decision')} ${block.name}: ${block.arms.length - missing.length}/${block.arms.length} arms observed ${paintMuted(`(${location})`)}\n`);
|
|
155
|
+
process.stdout.write(` unobserved: ${missing.map((arm) => arm.name).join(', ')}\n`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function collectSelectedArms(execution, blockId, observed) {
|
|
159
|
+
for (const item of execution) {
|
|
160
|
+
if ('parallel' in item) {
|
|
161
|
+
for (const branch of item.parallel) {
|
|
162
|
+
collectSelectedArms(branch, blockId, observed);
|
|
163
|
+
}
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (item.blockId === blockId && item.selectedArm !== undefined) {
|
|
167
|
+
observed.add(selectedArmKey(item.selectedArm));
|
|
168
|
+
}
|
|
169
|
+
collectSelectedArms(item.children, blockId, observed);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function selectedArmKey(arm) {
|
|
173
|
+
return arm.kind === 'otherwise'
|
|
174
|
+
? 'otherwise'
|
|
175
|
+
: `literal:${typeof arm.value}:${String(arm.value)}`;
|
|
176
|
+
}
|
|
177
|
+
const paintMuted = paint(process.stdout, '2');
|
|
178
|
+
function reportCoverage(report) {
|
|
179
|
+
const ignored = Object.values(report.files).filter((file) => file.kind === 'ignored').length;
|
|
180
|
+
const layerCoverage = report.coverage.layers;
|
|
181
|
+
const moduleTotal = report.coverage.modules.reduce((total, coverage) => total + coverage.totalFiles, 0);
|
|
182
|
+
const moduleCovered = report.coverage.modules.reduce((total, coverage) => total + coverage.coveredFiles, 0);
|
|
183
|
+
const ignoredSuffix = ignored > 0 ? ` (${ignored} ignored)` : '';
|
|
184
|
+
process.stdout.write([
|
|
185
|
+
`Scanned ${layerCoverage.totalFiles} source file(s)${ignoredSuffix}.`,
|
|
186
|
+
`Layers: ${layerCoverage.coveredFiles}/${layerCoverage.totalFiles} file(s) covered.`,
|
|
187
|
+
`Modules: ${moduleCovered}/${moduleTotal} layer-covered file(s) covered.`,
|
|
188
|
+
].join('\n') + '\n');
|
|
189
|
+
if (layerCoverage.uncovered.length > 0) {
|
|
190
|
+
writeWarningList('file(s) not covered by any layer', layerCoverage.uncovered);
|
|
191
|
+
}
|
|
192
|
+
for (const coverage of report.coverage.modules) {
|
|
193
|
+
if (coverage.uncovered.length > 0) {
|
|
194
|
+
writeWarningList(`file(s) in layer "${coverage.layer}" not covered by a module`, coverage.uncovered);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function reportWarnings(report) {
|
|
199
|
+
for (const warning of report.warnings) {
|
|
200
|
+
const detail = warning.kind === 'missing-source-root'
|
|
201
|
+
? `source root does not exist: ${warning.path}`
|
|
202
|
+
: warning.kind === 'missing-layer-path'
|
|
203
|
+
? `layer "${warning.layer}" path does not exist: ${warning.path}`
|
|
204
|
+
: `module path does not exist: ${warning.path}`;
|
|
205
|
+
process.stdout.write(yellow(`warning: ${detail}\n`));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function writeWarningList(title, files) {
|
|
209
|
+
process.stdout.write(yellow(`warning: ${files.length} ${title}:\n`));
|
|
210
|
+
for (const file of files)
|
|
211
|
+
process.stdout.write(yellow(` - ${file}\n`));
|
|
212
|
+
}
|
|
213
|
+
function reportViolations(violations) {
|
|
214
|
+
process.stderr.write(red(`Found ${violations.length} architecture violation(s):\n`));
|
|
215
|
+
for (const violation of violations) {
|
|
216
|
+
if (violation.kind === 'layer') {
|
|
217
|
+
process.stderr.write(red(` - [layer] ${violation.from.layer} -> ${violation.to.layer}\n`));
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
process.stderr.write(red(` - [${violation.rule}] ${violation.from.module} -> ${violation.to.module}\n`));
|
|
221
|
+
}
|
|
222
|
+
process.stderr.write(red(` ${violation.from.file} -> ${violation.to.file}\n`));
|
|
223
|
+
}
|
|
224
|
+
process.stderr.write(red(`Lint failed: ${violations.length} violation(s).\n`));
|
|
225
|
+
}
|
|
226
|
+
function errorMessage(error) {
|
|
227
|
+
if (typeof error === 'object' &&
|
|
228
|
+
error !== null &&
|
|
229
|
+
'cause' in error &&
|
|
230
|
+
error.cause !== undefined) {
|
|
231
|
+
return errorMessage(error.cause);
|
|
232
|
+
}
|
|
233
|
+
return error instanceof Error ? error.message : String(error);
|
|
234
|
+
}
|
|
235
|
+
const lintCommand = Command.make('lint', {}, lint).pipe(Command.withDescription('Check the project against its declared architecture. Coverage gaps are warnings only.'));
|
|
236
|
+
const storiesCommand = Command.make('stories', {
|
|
237
|
+
stories: Argument.string('story').pipe(Argument.variadic()),
|
|
238
|
+
timeout: Flag.string('timeout').pipe(Flag.optional, Flag.withDescription('Default Scenario timeout for this run, e.g. "90 seconds".')),
|
|
239
|
+
group: Flag.string('group').pipe(Flag.optional, Flag.withDescription('Run one Story Group subtree, e.g. "DynamoDB / Entities".')),
|
|
240
|
+
}, ({ stories: files, timeout, group }) => stories(files, timeout, group)).pipe(Command.withDescription('Run Story files or one --group subtree (all Stories by default) and print fresh execution evidence.'));
|
|
241
|
+
export const command = Command.make('laymos', {}, () => Effect.void).pipe(Command.withSubcommands([lintCommand, storiesCommand]));
|
|
242
|
+
export const cli = command.pipe(Command.run({ version: '0.0.0' }));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-config.d.ts","sourceRoot":"","sources":["../../src/config/define-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAa,MAAM,YAAY,CAAC;AAG1D,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAuB/D"}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { normalizeConfigPath, pathContains } from './path.js';
|
|
2
|
+
export function defineConfig(config) {
|
|
3
|
+
const normalizedConfig = {
|
|
4
|
+
...config,
|
|
5
|
+
sourceRoots: config.sourceRoots.map(normalizeConfigPath),
|
|
6
|
+
...(config.ignore !== undefined
|
|
7
|
+
? { ignore: config.ignore.map(normalizeConfigPath) }
|
|
8
|
+
: {}),
|
|
9
|
+
};
|
|
10
|
+
const issues = [
|
|
11
|
+
...sourceRootIssues(normalizedConfig),
|
|
12
|
+
...duplicateGraphNames(normalizedConfig),
|
|
13
|
+
...duplicateLayerNames(normalizedConfig),
|
|
14
|
+
...duplicateLayerPaths(normalizedConfig),
|
|
15
|
+
...duplicateIgnoredPaths(normalizedConfig),
|
|
16
|
+
...unionCycles(normalizedConfig),
|
|
17
|
+
...moduleIssues(normalizedConfig),
|
|
18
|
+
];
|
|
19
|
+
if (issues.length > 0) {
|
|
20
|
+
throw new Error(`Invalid laymos config:\n${issues.map((issue) => ` - ${issue}`).join('\n')}`);
|
|
21
|
+
}
|
|
22
|
+
return normalizedConfig;
|
|
23
|
+
}
|
|
24
|
+
function sourceRootIssues(config) {
|
|
25
|
+
const issues = [];
|
|
26
|
+
for (let index = 0; index < config.sourceRoots.length; index++) {
|
|
27
|
+
const root = config.sourceRoots[index];
|
|
28
|
+
for (let otherIndex = 0; otherIndex < index; otherIndex++) {
|
|
29
|
+
const other = config.sourceRoots[otherIndex];
|
|
30
|
+
if (root === other) {
|
|
31
|
+
issues.push(`Source root "${root}" is declared more than once`);
|
|
32
|
+
}
|
|
33
|
+
else if (pathContains(root, other) || pathContains(other, root)) {
|
|
34
|
+
issues.push(`Source roots "${other}" and "${root}" overlap`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (config.sourceRoots.length === 0) {
|
|
39
|
+
issues.push('At least one source root is required');
|
|
40
|
+
}
|
|
41
|
+
const configuredPaths = [
|
|
42
|
+
...layerPathEntries(config).map(([path]) => ['Layer', path]),
|
|
43
|
+
...(config.modules ?? []).map(({ path }) => ['Module', path]),
|
|
44
|
+
...(config.ignore ?? []).map((path) => ['Ignored', path]),
|
|
45
|
+
];
|
|
46
|
+
for (const [kind, path] of configuredPaths) {
|
|
47
|
+
if (!config.sourceRoots.some((root) => pathContains(root, path))) {
|
|
48
|
+
issues.push(`${kind} path "${path}" is not inside any source root`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return issues;
|
|
52
|
+
}
|
|
53
|
+
function duplicateGraphNames(config) {
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
const issues = [];
|
|
56
|
+
for (const graph of config.graphs) {
|
|
57
|
+
if (seen.has(graph.name)) {
|
|
58
|
+
issues.push(`Duplicate graph name "${graph.name}"`);
|
|
59
|
+
}
|
|
60
|
+
seen.add(graph.name);
|
|
61
|
+
}
|
|
62
|
+
return issues;
|
|
63
|
+
}
|
|
64
|
+
function duplicateLayerNames(config) {
|
|
65
|
+
const seen = new Map();
|
|
66
|
+
const issues = [];
|
|
67
|
+
for (const graph of config.graphs) {
|
|
68
|
+
for (const layer of graph.layers) {
|
|
69
|
+
const existing = seen.get(layer.name);
|
|
70
|
+
if (existing && existing !== layer) {
|
|
71
|
+
issues.push(`Layer name "${layer.name}" has multiple definitions — define the layer once and reuse the value`);
|
|
72
|
+
}
|
|
73
|
+
seen.set(layer.name, existing ?? layer);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return issues;
|
|
77
|
+
}
|
|
78
|
+
function layerPathEntries(config) {
|
|
79
|
+
const layers = new Set(config.graphs.flatMap((g) => [...g.layers]));
|
|
80
|
+
return [...layers].flatMap((layer) => layer.paths.map((path) => [path, layer.name]));
|
|
81
|
+
}
|
|
82
|
+
function duplicateLayerPaths(config) {
|
|
83
|
+
const seen = new Map();
|
|
84
|
+
const issues = [];
|
|
85
|
+
for (const [path, layerName] of layerPathEntries(config)) {
|
|
86
|
+
const existing = seen.get(path);
|
|
87
|
+
if (existing !== undefined && existing !== layerName) {
|
|
88
|
+
issues.push(`Path "${path}" is declared by both layers "${existing}" and "${layerName}"`);
|
|
89
|
+
}
|
|
90
|
+
seen.set(path, existing ?? layerName);
|
|
91
|
+
}
|
|
92
|
+
return issues;
|
|
93
|
+
}
|
|
94
|
+
function duplicateIgnoredPaths(config) {
|
|
95
|
+
const seen = new Set();
|
|
96
|
+
const issues = [];
|
|
97
|
+
for (const path of config.ignore ?? []) {
|
|
98
|
+
if (seen.has(path))
|
|
99
|
+
issues.push(`Duplicate ignored path "${path}"`);
|
|
100
|
+
seen.add(path);
|
|
101
|
+
}
|
|
102
|
+
return issues;
|
|
103
|
+
}
|
|
104
|
+
function moduleIssues(config) {
|
|
105
|
+
const issues = [];
|
|
106
|
+
const layerPaths = layerPathEntries(config);
|
|
107
|
+
const modules = new Map();
|
|
108
|
+
for (const def of config.modules ?? []) {
|
|
109
|
+
const path = def.path;
|
|
110
|
+
const existing = modules.get(path);
|
|
111
|
+
if (existing !== undefined) {
|
|
112
|
+
issues.push(`Module "${path}" is declared more than once`);
|
|
113
|
+
}
|
|
114
|
+
else
|
|
115
|
+
modules.set(path, def);
|
|
116
|
+
}
|
|
117
|
+
const paths = [...modules.keys()];
|
|
118
|
+
for (let i = 0; i < paths.length; i++) {
|
|
119
|
+
for (let j = i + 1; j < paths.length; j++) {
|
|
120
|
+
const [a, b] = [paths[i], paths[j]];
|
|
121
|
+
if (pathContains(a, b) || pathContains(b, a)) {
|
|
122
|
+
issues.push(`Modules "${a}" and "${b}" nest — modules must be flat`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const path of paths) {
|
|
127
|
+
let best;
|
|
128
|
+
for (const [layerPath, layerName] of layerPaths) {
|
|
129
|
+
if (pathContains(layerPath, path) &&
|
|
130
|
+
(best === undefined || layerPath.length > best[0].length)) {
|
|
131
|
+
best = [layerPath, layerName];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (best === undefined) {
|
|
135
|
+
issues.push(`Module "${path}" is not inside any layer`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
for (const [layerPath, layerName] of layerPaths) {
|
|
139
|
+
if (path !== layerPath &&
|
|
140
|
+
pathContains(path, layerPath) &&
|
|
141
|
+
layerName !== best[1]) {
|
|
142
|
+
issues.push(`Layer "${layerName}" has path "${layerPath}" inside module "${path}" — the module would straddle layers`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const seenRules = new Set();
|
|
147
|
+
for (const rule of config.moduleRules ?? []) {
|
|
148
|
+
const from = rule.module.path;
|
|
149
|
+
if (seenRules.has(from)) {
|
|
150
|
+
issues.push(`Module "${from}" has more than one rules declaration`);
|
|
151
|
+
}
|
|
152
|
+
seenRules.add(from);
|
|
153
|
+
if (modules.get(from) !== rule.module) {
|
|
154
|
+
issues.push(`Rules module "${from}" must reuse a value declared in config.modules`);
|
|
155
|
+
}
|
|
156
|
+
for (const [ruleName, definitions] of [
|
|
157
|
+
['canImport', rule.canImport ?? []],
|
|
158
|
+
['canImportedBy', rule.canImportedBy ?? []],
|
|
159
|
+
]) {
|
|
160
|
+
const seenTargets = new Set();
|
|
161
|
+
for (const def of definitions) {
|
|
162
|
+
const to = def.path;
|
|
163
|
+
if (seenTargets.has(to)) {
|
|
164
|
+
issues.push(`Rule ${ruleName} on module "${from}" references module "${to}" more than once`);
|
|
165
|
+
}
|
|
166
|
+
seenTargets.add(to);
|
|
167
|
+
if (modules.get(to) !== def) {
|
|
168
|
+
issues.push(`Rule ${ruleName} on module "${from}" must reuse module "${to}" from config.modules`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return issues;
|
|
174
|
+
}
|
|
175
|
+
function unionCycles(config) {
|
|
176
|
+
const adjacency = new Map();
|
|
177
|
+
for (const graph of config.graphs) {
|
|
178
|
+
for (const edge of graph.edges) {
|
|
179
|
+
if (!adjacency.has(edge.from.name)) {
|
|
180
|
+
adjacency.set(edge.from.name, new Set());
|
|
181
|
+
}
|
|
182
|
+
adjacency.get(edge.from.name).add(edge.to.name);
|
|
183
|
+
if (!adjacency.has(edge.to.name)) {
|
|
184
|
+
adjacency.set(edge.to.name, new Set());
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const index = new Map();
|
|
189
|
+
const lowLink = new Map();
|
|
190
|
+
const onStack = new Set();
|
|
191
|
+
const stack = [];
|
|
192
|
+
const cycles = [];
|
|
193
|
+
let counter = 0;
|
|
194
|
+
const visit = (node) => {
|
|
195
|
+
index.set(node, counter);
|
|
196
|
+
lowLink.set(node, counter);
|
|
197
|
+
counter++;
|
|
198
|
+
stack.push(node);
|
|
199
|
+
onStack.add(node);
|
|
200
|
+
for (const next of adjacency.get(node) ?? []) {
|
|
201
|
+
if (!index.has(next)) {
|
|
202
|
+
visit(next);
|
|
203
|
+
lowLink.set(node, Math.min(lowLink.get(node), lowLink.get(next)));
|
|
204
|
+
}
|
|
205
|
+
else if (onStack.has(next)) {
|
|
206
|
+
lowLink.set(node, Math.min(lowLink.get(node), index.get(next)));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (lowLink.get(node) === index.get(node)) {
|
|
210
|
+
const component = [];
|
|
211
|
+
let popped;
|
|
212
|
+
do {
|
|
213
|
+
popped = stack.pop();
|
|
214
|
+
onStack.delete(popped);
|
|
215
|
+
component.push(popped);
|
|
216
|
+
} while (popped !== node);
|
|
217
|
+
if (component.length > 1) {
|
|
218
|
+
cycles.push(component.reverse());
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
for (const node of adjacency.keys()) {
|
|
223
|
+
if (!index.has(node)) {
|
|
224
|
+
visit(node);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return cycles.map((cycle) => `Union cycle: ${cycle.join(' -> ')} -> ${cycle[0]}`);
|
|
228
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Layer, LayerEdge, LayerGraph } from './types.js';
|
|
2
|
+
export declare function edge(from: Layer, to: Layer): LayerEdge;
|
|
3
|
+
export declare function edge(from: Layer, to: readonly Layer[]): LayerEdge[];
|
|
4
|
+
export declare function layerGraph(name: string, edges: ReadonlyArray<LayerEdge | LayerEdge[]>, options?: {
|
|
5
|
+
description?: string;
|
|
6
|
+
}): LayerGraph;
|
|
7
|
+
//# sourceMappingURL=layer-graph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layer-graph.d.ts","sourceRoot":"","sources":["../../src/config/layer-graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE/D,wBAAgB,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,GAAG,SAAS,CAAC;AACxD,wBAAgB,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,SAAS,KAAK,EAAE,GAAG,SAAS,EAAE,CAAC;AAqBrE,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,aAAa,CAAC,SAAS,GAAG,SAAS,EAAE,CAAC,EAC7C,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GACjC,UAAU,CA6CZ"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export function edge(from, to) {
|
|
2
|
+
if (Array.isArray(to)) {
|
|
3
|
+
if (to.length === 0) {
|
|
4
|
+
throw new Error(`Edge from "${from.name}" must have at least 1 target`);
|
|
5
|
+
}
|
|
6
|
+
return to.map((target) => singleEdge(from, target));
|
|
7
|
+
}
|
|
8
|
+
return singleEdge(from, to);
|
|
9
|
+
}
|
|
10
|
+
function singleEdge(from, to) {
|
|
11
|
+
if (from === to || from.name === to.name) {
|
|
12
|
+
throw new Error(`Edge "${from.name} -> ${to.name}" is a self-edge`);
|
|
13
|
+
}
|
|
14
|
+
return { from, to };
|
|
15
|
+
}
|
|
16
|
+
export function layerGraph(name, edges, options) {
|
|
17
|
+
if (name.length === 0) {
|
|
18
|
+
throw new Error('Graph name must not be empty');
|
|
19
|
+
}
|
|
20
|
+
const flatEdges = edges.flat();
|
|
21
|
+
if (flatEdges.length === 0) {
|
|
22
|
+
throw new Error(`Graph "${name}" must have at least 1 edge`);
|
|
23
|
+
}
|
|
24
|
+
const layers = [];
|
|
25
|
+
const seen = new Map();
|
|
26
|
+
const seenEdges = new Set();
|
|
27
|
+
for (const e of flatEdges) {
|
|
28
|
+
for (const l of [e.from, e.to]) {
|
|
29
|
+
const existing = seen.get(l.name);
|
|
30
|
+
if (existing && existing !== l) {
|
|
31
|
+
throw new Error(`Graph "${name}" has duplicate layer name "${l.name}" with different definitions`);
|
|
32
|
+
}
|
|
33
|
+
if (!existing) {
|
|
34
|
+
seen.set(l.name, l);
|
|
35
|
+
layers.push(l);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const key = `${e.from.name}\0${e.to.name}`;
|
|
39
|
+
if (seenEdges.has(key)) {
|
|
40
|
+
throw new Error(`Graph "${name}" has duplicate edge "${e.from.name} -> ${e.to.name}"`);
|
|
41
|
+
}
|
|
42
|
+
seenEdges.add(key);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
kind: 'layer-graph',
|
|
46
|
+
name,
|
|
47
|
+
layers,
|
|
48
|
+
edges: flatEdges,
|
|
49
|
+
...(options?.description !== undefined
|
|
50
|
+
? { description: options.description }
|
|
51
|
+
: {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layer.d.ts","sourceRoot":"","sources":["../../src/config/layer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGxC,wBAAgB,KAAK,CACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GACjC,KAAK,CAmBP"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { normalizeConfigPath } from './path.js';
|
|
2
|
+
export function layer(name, paths, options) {
|
|
3
|
+
if (name.length === 0) {
|
|
4
|
+
throw new Error('Layer name must not be empty');
|
|
5
|
+
}
|
|
6
|
+
if (paths.length === 0) {
|
|
7
|
+
throw new Error(`Layer "${name}" must have at least 1 path`);
|
|
8
|
+
}
|
|
9
|
+
const normalizedPaths = paths.map(normalizeConfigPath);
|
|
10
|
+
if (new Set(normalizedPaths).size !== normalizedPaths.length) {
|
|
11
|
+
throw new Error(`Layer "${name}" contains duplicate paths`);
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
kind: 'layer',
|
|
15
|
+
name,
|
|
16
|
+
paths: normalizedPaths,
|
|
17
|
+
...(options?.description !== undefined
|
|
18
|
+
? { description: options.description }
|
|
19
|
+
: {}),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ModuleDef, ModuleRules } from './types.js';
|
|
2
|
+
export declare function module(path: string, options?: {
|
|
3
|
+
description?: string;
|
|
4
|
+
}): ModuleDef;
|
|
5
|
+
export declare function rules(module: ModuleDef, constraints: {
|
|
6
|
+
canImport?: readonly ModuleDef[];
|
|
7
|
+
canImportedBy?: readonly ModuleDef[];
|
|
8
|
+
}): ModuleRules;
|
|
9
|
+
//# sourceMappingURL=module.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../../src/config/module.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAGzD,wBAAgB,MAAM,CACpB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GACjC,SAAS,CAWX;AAED,wBAAgB,KAAK,CACnB,MAAM,EAAE,SAAS,EACjB,WAAW,EAAE;IACX,SAAS,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IACjC,aAAa,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;CACtC,GACA,WAAW,CAmBb"}
|