autonomous-sdlc-harness 0.1.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/LICENSE +201 -0
- package/NOTICE +7 -0
- package/README.md +24 -0
- package/dist/cli.js +194 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/config.js +561 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/daemon.js +791 -0
- package/dist/commands/daemon.js.map +1 -0
- package/dist/commands/doctor.js +336 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/init.js +2023 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/registry.js +42 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/config/check.js +505 -0
- package/dist/config/check.js.map +1 -0
- package/dist/config/io.js +177 -0
- package/dist/config/io.js.map +1 -0
- package/dist/config/model.js +406 -0
- package/dist/config/model.js.map +1 -0
- package/dist/core/errors.js +71 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/git.js +537 -0
- package/dist/core/git.js.map +1 -0
- package/dist/core/json.js +125 -0
- package/dist/core/json.js.map +1 -0
- package/dist/core/layerCoverage.js +141 -0
- package/dist/core/layerCoverage.js.map +1 -0
- package/dist/core/layerGapRemedy.js +62 -0
- package/dist/core/layerGapRemedy.js.map +1 -0
- package/dist/core/nameList.js +23 -0
- package/dist/core/nameList.js.map +1 -0
- package/dist/core/paths.js +153 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/prompt.js +206 -0
- package/dist/core/prompt.js.map +1 -0
- package/dist/core/repoPaths.js +55 -0
- package/dist/core/repoPaths.js.map +1 -0
- package/dist/core/report.js +150 -0
- package/dist/core/report.js.map +1 -0
- package/dist/core/templating.js +88 -0
- package/dist/core/templating.js.map +1 -0
- package/dist/core/writer.js +479 -0
- package/dist/core/writer.js.map +1 -0
- package/dist/daemon/backend.js +180 -0
- package/dist/daemon/backend.js.map +1 -0
- package/dist/daemon/units.js +380 -0
- package/dist/daemon/units.js.map +1 -0
- package/dist/detect/nestedApplication.js +79 -0
- package/dist/detect/nestedApplication.js.map +1 -0
- package/dist/detect/presets.js +2033 -0
- package/dist/detect/presets.js.map +1 -0
- package/dist/detect/signals.js +1368 -0
- package/dist/detect/signals.js.map +1 -0
- package/dist/doctor/checks.js +3530 -0
- package/dist/doctor/checks.js.map +1 -0
- package/dist/generators/claudeContext.js +588 -0
- package/dist/generators/claudeContext.js.map +1 -0
- package/dist/generators/githooks.js +446 -0
- package/dist/generators/githooks.js.map +1 -0
- package/dist/generators/harnessConfig.js +632 -0
- package/dist/generators/harnessConfig.js.map +1 -0
- package/dist/generators/notifications.js +191 -0
- package/dist/generators/notifications.js.map +1 -0
- package/dist/generators/outerLoopScripts.js +165 -0
- package/dist/generators/outerLoopScripts.js.map +1 -0
- package/dist/generators/permissionProfile.js +1172 -0
- package/dist/generators/permissionProfile.js.map +1 -0
- package/dist/generators/projectSettings.js +322 -0
- package/dist/generators/projectSettings.js.map +1 -0
- package/dist/generators/repoRoot.js +417 -0
- package/dist/generators/repoRoot.js.map +1 -0
- package/dist/generators/scripts.js +557 -0
- package/dist/generators/scripts.js.map +1 -0
- package/dist/generators/stateDir.js +221 -0
- package/dist/generators/stateDir.js.map +1 -0
- package/dist/machine/paths.js +111 -0
- package/dist/machine/paths.js.map +1 -0
- package/dist/machine/plugins.js +224 -0
- package/dist/machine/plugins.js.map +1 -0
- package/dist/machine/registry.js +330 -0
- package/dist/machine/registry.js.map +1 -0
- package/package.json +23 -0
- package/scripts/README.md +13 -0
- package/scripts/daemon/launchd.plist.template +59 -0
- package/scripts/daemon/systemd.service.template +58 -0
- package/templates/README.md +15 -0
- package/templates/claude/CLAUDE.md +54 -0
- package/templates/claude/README.md +5 -0
- package/templates/claude/context/api.md +29 -0
- package/templates/claude/context/conventions.md +23 -0
- package/templates/claude/context/data-layer.md +28 -0
- package/templates/claude/context/data-storage.md +29 -0
- package/templates/claude/context/docs-catalog.md +29 -0
- package/templates/claude/context/domain.md +28 -0
- package/templates/claude/context/layer.md +20 -0
- package/templates/claude/context/module.md +30 -0
- package/templates/claude/context/package.md +29 -0
- package/templates/claude/context/presentation.md +32 -0
- package/templates/claude/context/state-slices.md +28 -0
- package/templates/claude/context/tests.md +28 -0
- package/templates/claude/harness-task-offer.md +58 -0
- package/templates/claude/push-notify.env.example +21 -0
- package/templates/claude/qa-accounts.env.example +38 -0
- package/templates/claude/qa_test_scenarios.md +110 -0
- package/templates/claude/settings.autonomous.json +93 -0
- package/templates/claude/settings.autonomous.qa.json +36 -0
- package/templates/githooks/README.md +3 -0
- package/templates/githooks/pre-push +72 -0
- package/templates/repo/README.md +3 -0
- package/templates/repo/gitattributes +16 -0
- package/templates/repo/gitignore +61 -0
- package/templates/repo/gitignore.qa +25 -0
- package/templates/repo/mcp.json +17 -0
- package/templates/scripts/README.md +5 -0
- package/templates/scripts/autonomous-format-stream.sh +95 -0
- package/templates/scripts/autonomous-notify.sh +337 -0
- package/templates/scripts/autonomous-watcher.sh +3087 -0
- package/templates/scripts/cleanup-merged-worktrees.sh +327 -0
- package/templates/scripts/commit-on-branch.sh +288 -0
- package/templates/scripts/create-worktree.sh +360 -0
- package/templates/scripts/deploy.sh +47 -0
- package/templates/scripts/lib/harness-run-lib.sh +1481 -0
- package/templates/scripts/push-branch.sh +140 -0
- package/templates/scripts/refresh-branch.sh +244 -0
- package/templates/scripts/restart-watcher.sh +401 -0
- package/templates/scripts/scratch-run.sh +302 -0
- package/templates/scripts/setup-worktree.sh +262 -0
- package/templates/scripts/start-dev-server.sh +99 -0
- package/templates/scripts/test.sh +50 -0
- package/templates/scripts/typecheck.sh +50 -0
- package/templates/state-dir/README-root.md +13 -0
- package/templates/state-dir/README.md +9 -0
- package/templates/state-dir/architecture_branch_review_point_reviews/README.md +9 -0
- package/templates/state-dir/architecture_branch_reviews/README.md +9 -0
- package/templates/state-dir/architecture_reviews/README.md +9 -0
- package/templates/state-dir/architecture_user_review_reviews/README.md +9 -0
- package/templates/state-dir/autonomous_inbox/README.md +9 -0
- package/templates/state-dir/autonomous_logs/README.md +9 -0
- package/templates/state-dir/branch_statistics/README.md +9 -0
- package/templates/state-dir/business_parity_branch_review_point_reviews/README.md +9 -0
- package/templates/state-dir/business_parity_branch_reviews/README.md +9 -0
- package/templates/state-dir/business_parity_reviews/README.md +9 -0
- package/templates/state-dir/business_parity_user_review_reviews/README.md +9 -0
- package/templates/state-dir/clarification_digests/README.md +9 -0
- package/templates/state-dir/clarifications/README.md +9 -0
- package/templates/state-dir/code_reviews/README.md +9 -0
- package/templates/state-dir/dispatch_additions/README.md +19 -0
- package/templates/state-dir/docs_catalog/README.md +9 -0
- package/templates/state-dir/flow_progress/README.md +9 -0
- package/templates/state-dir/improvement_observations/README.md +19 -0
- package/templates/state-dir/improvement_suggestions.md +29 -0
- package/templates/state-dir/lessons.md +23 -0
- package/templates/state-dir/qa_review_point_reviews/README.md +9 -0
- package/templates/state-dir/qa_reviews/README.md +9 -0
- package/templates/state-dir/review_plan_point_reviews/README.md +9 -0
- package/templates/state-dir/review_plan_reviews/README.md +9 -0
- package/templates/state-dir/scratch/README.md +11 -0
- package/templates/state-dir/skeptic_review_plan_reviews/README.md +9 -0
- package/templates/state-dir/skeptic_review_point_reviews/README.md +9 -0
- package/templates/state-dir/skeptic_reviews/README.md +9 -0
- package/templates/state-dir/story_plans/README.md +9 -0
- package/templates/state-dir/task_plan_point_reviews/README.md +9 -0
- package/templates/state-dir/task_plan_reviews/README.md +9 -0
- package/templates/state-dir/task_plans/README.md +9 -0
- package/templates/state-dir/task_prompts/README.md +9 -0
- package/templates/state-dir/ui_test_plan_reviews/README.md +9 -0
- package/templates/state-dir/ui_test_plans/README.md +9 -0
- package/templates/state-dir/user_review_fix_plan_point_reviews/README.md +9 -0
- package/templates/state-dir/user_reviews/README.md +9 -0
|
@@ -0,0 +1,2033 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The layer and command presets: what a detected preset name is actually worth to `init`.
|
|
3
|
+
*
|
|
4
|
+
* `detect/signals.ts` answers *which* preset; this module answers *what that preset contains* —
|
|
5
|
+
* the `layers` array `harness.config.json` gets, and the **raw** command lines the project's
|
|
6
|
+
* verification commands are.
|
|
7
|
+
*
|
|
8
|
+
* **The rule this module exists to enforce: `rawCommands` holds the command line a human would
|
|
9
|
+
* type, never a wrapper invocation.** Three files are three views of one string and drift between
|
|
10
|
+
* them is invisible until an unattended run stalls on a command that is neither allowed nor
|
|
11
|
+
* denied, so each view has exactly one owner:
|
|
12
|
+
*
|
|
13
|
+
* 1. **here** — the raw line (`npm run build`, `pytest`), derived only from files that exist;
|
|
14
|
+
* 2. **the wrapper-script writer** — inlines a raw line into `<scriptsDir>/<name>.sh`;
|
|
15
|
+
* 3. **the config generator** — writes `bash <scriptsDir>/<name>.sh` into `commands.typecheck`,
|
|
16
|
+
* `commands.test` and `commands.devServer` (`examples/harness.config.json`).
|
|
17
|
+
*
|
|
18
|
+
* This module therefore never formats a `bash …/….sh` value and never reads `scriptsDir`.
|
|
19
|
+
* `build` and `depInstall` are the two keys that are never wrapped: they pass through to
|
|
20
|
+
* `commands.*` exactly as detected here, which is why the example config shows them raw.
|
|
21
|
+
*
|
|
22
|
+
* **Nothing here is invented.** A command that cannot be derived from a manifest that exists
|
|
23
|
+
* becomes {@link placeholderCommand} plus a warning naming the key, what was looked for and what the
|
|
24
|
+
* repository holds instead ({@link undetectedCommandWarning}) — a value that is obviously
|
|
25
|
+
* unfinished, that `config/check.ts` already warns about and that `doctor` already looks for.
|
|
26
|
+
* Guessing a plausible command instead would produce a run that passes its verification step by
|
|
27
|
+
* running something that never checked anything.
|
|
28
|
+
*/
|
|
29
|
+
import { placeholderCommand } from '../config/model.js';
|
|
30
|
+
import { appScriptNames, declaresNodeScript, findAndroidModule, findApiDir, findBundlerManifest, findCargoManifest, findCmakeManifest, findComposerManifest, findDotnetProject, findFlutterManifest, findGoManifest, findGradleManifest, findLayeredRoot, findMavenManifest, findNodeInstallSite, findPythonManifest, findPythonPackageDir, findReadManifests, findSharedSchemes, findSwiftPackageManifest, findAnyReadManifest, findUnreadManifest, findXcodeProject, isWorkspaceRoot, manifestDirectory, ANDROID_MANIFEST, ANDROID_MODULE_DIRS, BUNDLER_MANIFEST, CARGO_MANIFEST, CMAKE_MANIFEST, COMPOSER_MANIFEST, DOTNET_PROJECT_SUFFIXES, DOTNET_SOLUTION_SUFFIXES, FLAT_FALLBACK_SIGNAL_ID, GO_MANIFEST, GRADLE_MANIFESTS, MAVEN_MANIFEST, PACKAGE_LOCKFILE, PACKAGE_MANIFEST, PUBSPEC_MANIFEST, PYTHON_MANIFESTS, SWIFT_PACKAGE_MANIFEST, XCODE_PROJECT_SUFFIXES, XCODE_SHARED_SCHEMES_DIR, XCODE_WORKSPACE_SUFFIX, } from './signals.js';
|
|
31
|
+
/** The two command keys the schema requires, and therefore the only two that get a placeholder. */
|
|
32
|
+
const REQUIRED_COMMAND_KEYS = ['typecheck', 'test'];
|
|
33
|
+
/** Where an adopter's per-layer rules documents live, relative to their repository root. */
|
|
34
|
+
const CONVENTIONS_DIR = '.claude/context';
|
|
35
|
+
/** The rules document each layered directory is pointed at. */
|
|
36
|
+
const LAYERED_CONVENTIONS = {
|
|
37
|
+
data: 'data-layer.md',
|
|
38
|
+
domain: 'domain.md',
|
|
39
|
+
presentation: 'presentation.md',
|
|
40
|
+
};
|
|
41
|
+
const API_LAYER_CONVENTIONS = 'api.md';
|
|
42
|
+
const PACKAGE_LAYER_CONVENTIONS = 'package.md';
|
|
43
|
+
const GENERAL_LAYER_CONVENTIONS = 'conventions.md';
|
|
44
|
+
/**
|
|
45
|
+
* The rules document a preset's single source-root layer points at — the shape a stack whose whole
|
|
46
|
+
* application lives under one directory gets, where naming that directory is deterministic and
|
|
47
|
+
* decomposing it is a judgement call and therefore `/harness-analyze`'s.
|
|
48
|
+
*/
|
|
49
|
+
const MODULE_LAYER_CONVENTIONS = 'module.md';
|
|
50
|
+
/**
|
|
51
|
+
* The layer name every preset's test root takes, whatever the directory is spelled — `spec/`,
|
|
52
|
+
* `Tests/`, `src/test/`.
|
|
53
|
+
*
|
|
54
|
+
* The name is the routing tag an orchestrator dispatches work by and the path is the spelling on
|
|
55
|
+
* disk; {@link APPLE_LAYER_NAME} documents that split for `sources` → `MyApp/` and this is the same
|
|
56
|
+
* one. One name across every stack is what lets an instruction say "the `tests` layer" once.
|
|
57
|
+
*/
|
|
58
|
+
export const TESTS_LAYER_NAME = 'tests';
|
|
59
|
+
/** The rules document every preset's test layer points at — see {@link PRESET_TEST_ROOTS}. */
|
|
60
|
+
const TESTS_LAYER_CONVENTIONS = 'tests.md';
|
|
61
|
+
/** The source-root directory name a Dart or Flutter package puts its code in. */
|
|
62
|
+
const DART_SOURCE_DIR = 'lib';
|
|
63
|
+
/**
|
|
64
|
+
* The main source set of a JVM module — the directory {@link ANDROID_MANIFEST} sits in on Android,
|
|
65
|
+
* and the one Maven's and Gradle's own conventions put sources under everywhere else.
|
|
66
|
+
*
|
|
67
|
+
* One constant for both stacks because it is one convention: the Android Gradle plugin inherits the
|
|
68
|
+
* layout from the Java plugin, which inherits it from Maven.
|
|
69
|
+
*/
|
|
70
|
+
const MAIN_SOURCE_SET = 'src/main';
|
|
71
|
+
/**
|
|
72
|
+
* The language directories a main source set holds its code in, in preference order.
|
|
73
|
+
*
|
|
74
|
+
* `java` first, and a module carrying both gets it: it is the source root every JVM build tool
|
|
75
|
+
* creates whatever the language, so it is the one a mixed module is certain to compile from, while
|
|
76
|
+
* `src/main/kotlin` is added by convention in Kotlin-only modules — many of which keep their Kotlin
|
|
77
|
+
* under `src/main/java` regardless.
|
|
78
|
+
*/
|
|
79
|
+
const JVM_SOURCE_DIRS = ['java', 'kotlin'];
|
|
80
|
+
/** The Gradle wrapper script, whose presence chooses `./gradlew` over `gradle` — see {@link jvmRunner}. */
|
|
81
|
+
const GRADLE_WRAPPER = 'gradlew';
|
|
82
|
+
/** The Maven wrapper script, whose presence chooses `./mvnw` over `mvn` — see {@link jvmRunner}. */
|
|
83
|
+
const MAVEN_WRAPPER = 'mvnw';
|
|
84
|
+
/**
|
|
85
|
+
* The cross-layer rules document — the one every layer inherits, and the one the `general`
|
|
86
|
+
* layer points at in every preset.
|
|
87
|
+
*
|
|
88
|
+
* Exported because a second consumer needs the same string: the conventions-stub generator
|
|
89
|
+
* writes this file whether or not a layer points at it, and its routing table gives it a row of
|
|
90
|
+
* its own. Two spellings of it would put a stub at one path and a config pointer at another.
|
|
91
|
+
*/
|
|
92
|
+
export const SHARED_CONVENTIONS_PATH = `${CONVENTIONS_DIR}/${GENERAL_LAYER_CONVENTIONS}`;
|
|
93
|
+
/**
|
|
94
|
+
* The npm script names each script-derived command key accepts, most specific first.
|
|
95
|
+
*
|
|
96
|
+
* **One table for both readers of a candidate list**, which is what keeps them honest: the root
|
|
97
|
+
* manifest's derivation ({@link nodeCommands}) and the sentence naming what `<appDir>/package.json`
|
|
98
|
+
* declares ({@link nestedScriptFor}) select a script the same way, so the line a nested application is
|
|
99
|
+
* offered is the one detection would have derived had that manifest been the root's. `depInstall` has
|
|
100
|
+
* no row because it is a whole command line rather than a script name.
|
|
101
|
+
*
|
|
102
|
+
* Typed against {@link RawCommands} so a command key added to the schema has to gain a row here or
|
|
103
|
+
* fail to compile, rather than becoming a fifth key nothing looks for.
|
|
104
|
+
*/
|
|
105
|
+
const NODE_SCRIPT_CANDIDATES = {
|
|
106
|
+
typecheck: ['typecheck', 'check-types', 'tsc', 'lint'],
|
|
107
|
+
test: ['test'],
|
|
108
|
+
build: ['build'],
|
|
109
|
+
devServer: ['dev', 'start', 'serve'],
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* {@link NODE_SCRIPT_CANDIDATES} flattened — every script name that makes this family answer.
|
|
113
|
+
*
|
|
114
|
+
* Exported for the closure case that holds it equal to `NODE_ROW_GUARD_SCRIPTS` in
|
|
115
|
+
* `detect/signals.ts` (`test/stack-presets.test.mjs`): that guard has to fire exactly where this
|
|
116
|
+
* family answers, and the two lists cannot be one constant because `detect/presets.ts` imports
|
|
117
|
+
* `detect/signals.ts` and not the reverse.
|
|
118
|
+
*/
|
|
119
|
+
export const NODE_SCRIPT_CANDIDATE_NAMES = Object.values(NODE_SCRIPT_CANDIDATES).flat();
|
|
120
|
+
/**
|
|
121
|
+
* The dev-server candidates that **compile as they serve** — the ones a build in front of would be
|
|
122
|
+
* pure cost.
|
|
123
|
+
*
|
|
124
|
+
* `dev` is the whole list today: a watch-mode dev server compiles on start and recompiles on change,
|
|
125
|
+
* so pre-building it would pay a full build per QA phase for output nothing reads. Membership here is
|
|
126
|
+
* the exception, and the exception is what is enumerated: a candidate added to
|
|
127
|
+
* {@link NODE_SCRIPT_CANDIDATES}`.devServer` is treated as pre-compiled unless it is named here,
|
|
128
|
+
* which is the safe default of the two — a needless build wastes a minute, a missing one serves
|
|
129
|
+
* stale or absent output.
|
|
130
|
+
*/
|
|
131
|
+
const WATCH_MODE_DEV_SERVER_SCRIPTS = new Set(['dev']);
|
|
132
|
+
/**
|
|
133
|
+
* The dev-server script names that serve what was **last compiled** rather than compiling as they
|
|
134
|
+
* serve — {@link NODE_SCRIPT_CANDIDATES}`.devServer` less the watch-mode ones.
|
|
135
|
+
*
|
|
136
|
+
* Derived from that table rather than restated as a second list of script names, for the reason the
|
|
137
|
+
* table's own doc gives: one table for both readers is what keeps them honest, and a candidate added
|
|
138
|
+
* there cannot be silently missed here.
|
|
139
|
+
*/
|
|
140
|
+
const PRECOMPILED_DEV_SERVER_SCRIPTS = NODE_SCRIPT_CANDIDATES.devServer.filter((name) => !WATCH_MODE_DEV_SERVER_SCRIPTS.has(name));
|
|
141
|
+
/** The raw lines those scripts produce — `npm run start`, `npm run serve`. */
|
|
142
|
+
const PRECOMPILED_DEV_SERVER_LINES = new Set(PRECOMPILED_DEV_SERVER_SCRIPTS.map((name) => `npm run ${name}`));
|
|
143
|
+
/**
|
|
144
|
+
* **The raw line a dev-server wrapper must run to completion before it launches its server, or
|
|
145
|
+
* `undefined` when there is nothing to run.**
|
|
146
|
+
*
|
|
147
|
+
* The one fact only this module owns: whether the dev-server line it derived serves what was last
|
|
148
|
+
* compiled ({@link PRECOMPILED_DEV_SERVER_LINES}). A `npm run start` that is `node dist/server.js`
|
|
149
|
+
* silently tests stale output after an uncompiled edit and fails outright on a fresh worktree where
|
|
150
|
+
* `dist/` does not exist.
|
|
151
|
+
*
|
|
152
|
+
* **A pre-start line and not a compound `commands.devServer`**, which is the load-bearing half of the
|
|
153
|
+
* shape: the wrapper backgrounds its command and prints `$!`, and an `a && b` backgrounded as a list
|
|
154
|
+
* makes that pid a subshell — `docs/outer-loop-verification.md` §1.7's measured "killing the printed
|
|
155
|
+
* pid leaves no `sleep` behind" row is what a compound line would falsify.
|
|
156
|
+
*
|
|
157
|
+
* This module still formats no wrapper path and reads no `scriptsDir` (the module header): the value
|
|
158
|
+
* is a **raw** line, and rendering it is the wrapper-script writer's — `generators/scripts.ts`, the
|
|
159
|
+
* one consumer, which already imports {@link RawCommands} from here.
|
|
160
|
+
*/
|
|
161
|
+
export function devServerPrestart(rawCommands) {
|
|
162
|
+
const { devServer, build } = rawCommands;
|
|
163
|
+
if (devServer === undefined || build === undefined)
|
|
164
|
+
return undefined;
|
|
165
|
+
return PRECOMPILED_DEV_SERVER_LINES.has(devServer) ? build : undefined;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The type-check candidates that are accepted but never silently.
|
|
169
|
+
*
|
|
170
|
+
* A script named here resolves `commands.typecheck` exactly as any other candidate does — it is the
|
|
171
|
+
* **last** entry in {@link NODE_SCRIPT_CANDIDATES}`.typecheck` precisely because it may not type-check
|
|
172
|
+
* at all: a lint run without type-aware rules exits zero on a type error, which is this module's own
|
|
173
|
+
* "a run that passes its verification step by running something that never checked anything" reached
|
|
174
|
+
* by a different route. Dropping it from the candidate list is not the remedy — for a plain-JS
|
|
175
|
+
* repository it is the only static analysis there is, and the alternative is a placeholder. So the
|
|
176
|
+
* selection is kept and **reported** ({@link weakTypecheckMessage}); membership and order of the
|
|
177
|
+
* candidate list are unchanged, so nothing about which command is derived moves.
|
|
178
|
+
*/
|
|
179
|
+
const WEAK_TYPECHECK_SCRIPTS = new Set(['lint']);
|
|
180
|
+
/**
|
|
181
|
+
* The file-existence evidence that a repository is typed, and therefore the switch between reporting
|
|
182
|
+
* a weak type-check as a warning and reporting it as a note.
|
|
183
|
+
*
|
|
184
|
+
* Read at the **repository root and nowhere else**, because the line the message is about is derived
|
|
185
|
+
* at the repository root and nowhere else: `nodeCommands` reads `rootPackageJson()`, which is exactly
|
|
186
|
+
* `<repoRoot>/package.json`. Grading a nested `tsconfig.json` as evidence for a root-derived line
|
|
187
|
+
* would warn a repository whose root really is plain JS.
|
|
188
|
+
*/
|
|
189
|
+
const TYPED_REPOSITORY_EVIDENCE = 'tsconfig.json';
|
|
190
|
+
/**
|
|
191
|
+
* The two script-derived keys the schema leaves optional — the keys that get no placeholder, so an
|
|
192
|
+
* unresolved one is absent from the generated config rather than obviously unfinished in it.
|
|
193
|
+
*/
|
|
194
|
+
const OPTIONAL_COMMAND_KEYS = ['build', 'devServer'];
|
|
195
|
+
/** A layer, built from one repo-relative directory and the name of its rules document. */
|
|
196
|
+
function layer(name, path, conventionsFile) {
|
|
197
|
+
return { name, path, conventions: `${CONVENTIONS_DIR}/${conventionsFile}` };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The layer every preset ends with: the orchestrator always needs somewhere to put work that
|
|
201
|
+
* belongs to no layer — workflow artifacts, configuration, scripts — and a task is assigned to
|
|
202
|
+
* exactly one layer, so a profile without this one leaves that work homeless.
|
|
203
|
+
*
|
|
204
|
+
* Built fresh per call: the presets hand these objects to a generator that may edit them.
|
|
205
|
+
*/
|
|
206
|
+
function generalLayer() {
|
|
207
|
+
return { name: 'general', path: '.', conventions: SHARED_CONVENTIONS_PATH };
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The `lib/` directory of a Dart or Flutter package: the first manifest root carrying one.
|
|
211
|
+
*
|
|
212
|
+
* It follows `DetectContext.manifestRoots` rather than `candidateRoots`, because the manifest rows
|
|
213
|
+
* do — `findFlutterManifest` searches exactly those two roots — so the layer is named under the
|
|
214
|
+
* same root the signal matched its `pubspec.yaml` in.
|
|
215
|
+
*/
|
|
216
|
+
function flutterSourceDir(context) {
|
|
217
|
+
for (const root of context.manifestRoots) {
|
|
218
|
+
const path = root === '.' ? DART_SOURCE_DIR : `${root}/${DART_SOURCE_DIR}`;
|
|
219
|
+
if (context.dirExists(path))
|
|
220
|
+
return path;
|
|
221
|
+
}
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
/** A module's main source set, repo-relative — spelled once so both readers below join it alike. */
|
|
225
|
+
function mainSourceSet(module) {
|
|
226
|
+
return module === '.' ? MAIN_SOURCE_SET : `${module}/${MAIN_SOURCE_SET}`;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The source root of a JVM module: its main source set's `java/` or `kotlin/` directory, its main
|
|
230
|
+
* source set itself when it carries neither, or `undefined` when the module has no main source set
|
|
231
|
+
* at all.
|
|
232
|
+
*
|
|
233
|
+
* One resolver for both JVM arms, so an Android module and a Maven or Gradle project name their
|
|
234
|
+
* source root by the same rule rather than by two copies of it. The `undefined` arm is reachable
|
|
235
|
+
* only from the {@link jvmSourceDir} side: an Android module resolved because
|
|
236
|
+
* {@link ANDROID_MANIFEST} was found *inside* its main source set, so that directory exists by
|
|
237
|
+
* construction — which is why the Android arm's `??` fallback restates the old total answer rather
|
|
238
|
+
* than dropping the layer.
|
|
239
|
+
*/
|
|
240
|
+
function mainSourceDir(context, module) {
|
|
241
|
+
const main = mainSourceSet(module);
|
|
242
|
+
for (const name of JVM_SOURCE_DIRS) {
|
|
243
|
+
const path = `${main}/${name}`;
|
|
244
|
+
if (context.dirExists(path))
|
|
245
|
+
return path;
|
|
246
|
+
}
|
|
247
|
+
return context.dirExists(main) ? main : undefined;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* The source root of a Maven or Gradle JVM project: the first manifest root carrying a main source
|
|
251
|
+
* set, resolved through {@link mainSourceDir}.
|
|
252
|
+
*
|
|
253
|
+
* It follows `DetectContext.manifestRoots` for {@link flutterSourceDir}'s reason — the rows that
|
|
254
|
+
* select this preset resolve over exactly those roots — and it is deliberately **not** gated on
|
|
255
|
+
* having found the manifest again: the layer half answers what the tree looks like, so a
|
|
256
|
+
* `--preset jvm` run against a conventional tree names its source root whatever manifest it carries.
|
|
257
|
+
*/
|
|
258
|
+
function jvmSourceDir(context) {
|
|
259
|
+
for (const root of context.manifestRoots) {
|
|
260
|
+
const dir = mainSourceDir(context, root);
|
|
261
|
+
if (dir !== undefined)
|
|
262
|
+
return dir;
|
|
263
|
+
}
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
/** The source-root directory a .NET solution repository conventionally puts its projects under. */
|
|
267
|
+
const DOTNET_SOURCE_DIR = 'src';
|
|
268
|
+
/**
|
|
269
|
+
* The `src/` directory of a .NET repository: the first manifest root carrying one, or `undefined`.
|
|
270
|
+
*
|
|
271
|
+
* It follows `DetectContext.manifestRoots` for {@link flutterSourceDir}'s reason — the row that
|
|
272
|
+
* selects this preset resolves over exactly those roots — and, like {@link jvmSourceDir}, it is
|
|
273
|
+
* deliberately not gated on having found the solution again, so a `--preset dotnet` run against a
|
|
274
|
+
* conventional tree names its source root whatever manifest it carries.
|
|
275
|
+
*/
|
|
276
|
+
function dotnetSourceDir(context) {
|
|
277
|
+
for (const root of context.manifestRoots) {
|
|
278
|
+
const path = root === '.' ? DOTNET_SOURCE_DIR : `${root}/${DOTNET_SOURCE_DIR}`;
|
|
279
|
+
if (context.dirExists(path))
|
|
280
|
+
return path;
|
|
281
|
+
}
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
/** The source root Swift Package Manager puts a package's targets under, by convention. */
|
|
285
|
+
const SWIFT_SOURCES_DIR = 'Sources';
|
|
286
|
+
/**
|
|
287
|
+
* The layer **name** both Apple arms use — lowercase, whatever the directory is actually spelled.
|
|
288
|
+
*
|
|
289
|
+
* **This is the one place in this module where the name and the path deliberately differ.** A layer
|
|
290
|
+
* name is the routing tag an orchestrator assigns work by and the schema constrains it to
|
|
291
|
+
* `^[a-z][a-z0-9-]*$`, while `path` carries the real spelling on disk — and the Xcode arm's
|
|
292
|
+
* directory is named after the product (`MyApp.xcodeproj` → `MyApp/`), which that pattern rejects.
|
|
293
|
+
* So the name is fixed and the path varies, rather than the name being derived from the directory.
|
|
294
|
+
*/
|
|
295
|
+
const APPLE_LAYER_NAME = 'sources';
|
|
296
|
+
/**
|
|
297
|
+
* The source root of an Apple-native repository: a manifest root's `Sources/` — SwiftPM's own
|
|
298
|
+
* convention — else the directory named after the Xcode container's stem, else `undefined`.
|
|
299
|
+
*
|
|
300
|
+
* The stem arm is a suffix strip rather than a guess: `MyApp.xcodeproj` sits beside the `MyApp/`
|
|
301
|
+
* directory holding its sources in the layout `xcodebuild` itself generates, and the strip is total
|
|
302
|
+
* because the container was found by one of {@link XCODE_PROJECT_SUFFIXES}. It is still gated on the
|
|
303
|
+
* directory existing, so a repository that keeps its sources elsewhere drops the layer rather than
|
|
304
|
+
* pointing an implementer at a path that is not there.
|
|
305
|
+
*/
|
|
306
|
+
function appleSourceDir(context) {
|
|
307
|
+
for (const root of context.manifestRoots) {
|
|
308
|
+
const path = root === '.' ? SWIFT_SOURCES_DIR : `${root}/${SWIFT_SOURCES_DIR}`;
|
|
309
|
+
if (context.dirExists(path))
|
|
310
|
+
return path;
|
|
311
|
+
}
|
|
312
|
+
const project = findXcodeProject(context);
|
|
313
|
+
if (project === undefined)
|
|
314
|
+
return undefined;
|
|
315
|
+
const suffix = XCODE_PROJECT_SUFFIXES.find((candidate) => project.endsWith(candidate));
|
|
316
|
+
if (suffix === undefined)
|
|
317
|
+
return undefined;
|
|
318
|
+
const stem = project.slice(0, -suffix.length);
|
|
319
|
+
return context.dirExists(stem) ? stem : undefined;
|
|
320
|
+
}
|
|
321
|
+
/** The source root Cargo compiles a crate from, by convention and by its own default target paths. */
|
|
322
|
+
const CARGO_SOURCE_DIR = 'src';
|
|
323
|
+
/**
|
|
324
|
+
* The `src/` directory of a Rust crate: the first manifest root carrying one, or `undefined`.
|
|
325
|
+
*
|
|
326
|
+
* {@link dotnetSourceDir}'s shape and its reasons — the row that selects this preset resolves over
|
|
327
|
+
* exactly these roots, and it is not gated on having found the manifest again, so a
|
|
328
|
+
* `--preset rust-cargo` run against a conventional tree names its source root.
|
|
329
|
+
*/
|
|
330
|
+
function cargoSourceDir(context) {
|
|
331
|
+
for (const root of context.manifestRoots) {
|
|
332
|
+
const path = root === '.' ? CARGO_SOURCE_DIR : `${root}/${CARGO_SOURCE_DIR}`;
|
|
333
|
+
if (context.dirExists(path))
|
|
334
|
+
return path;
|
|
335
|
+
}
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* The two conventional Ruby source roots, in the order they are tried: an application's `app/`
|
|
340
|
+
* first, a library's `lib/` second.
|
|
341
|
+
*
|
|
342
|
+
* Ruby has no manifest key that declares either — a `Gemfile` lists gems, not directories — so
|
|
343
|
+
* existence is the whole test, and a repository carrying both is an application with a `lib/`
|
|
344
|
+
* beside it far more often than the reverse.
|
|
345
|
+
*/
|
|
346
|
+
const RUBY_SOURCE_DIRS = ['app', 'lib'];
|
|
347
|
+
/**
|
|
348
|
+
* The two conventional PHP source roots, in the order they are tried: a Laravel application's `app/`
|
|
349
|
+
* first, a PSR-4 library's `src/` second.
|
|
350
|
+
*
|
|
351
|
+
* `composer.json` *does* declare its PSR-4 roots, and this list deliberately does not read them: the
|
|
352
|
+
* `autoload` map is nested rather than top-level, so consulting it would take detection past the
|
|
353
|
+
* file-existence and top-level-key rule `detect/signals.ts` is defined by. Both names are fixed
|
|
354
|
+
* conventions, so existence answers the same question the map would.
|
|
355
|
+
*/
|
|
356
|
+
const PHP_SOURCE_DIRS = ['app', 'src'];
|
|
357
|
+
/**
|
|
358
|
+
* The conventional C++ source root — one name, and the list shape only because
|
|
359
|
+
* {@link firstSourceDir} is the locator it is searched with.
|
|
360
|
+
*
|
|
361
|
+
* `include/` is **not** an entry here because it is a second row rather than an alternative to this
|
|
362
|
+
* one — {@link CMAKE_HEADER_DIRS} says why. `tests/` is not one either: a conventional test root is
|
|
363
|
+
* declared as the `tests` row from {@link PRESET_TEST_ROOTS}, which is where every preset's test
|
|
364
|
+
* tree is named.
|
|
365
|
+
*
|
|
366
|
+
* The split is between **kinds** of directory, not between one row and many: `include/` is a second
|
|
367
|
+
* *source* row on {@link MODULE_LAYER_CONVENTIONS}, and a test tree is a `tests` row on
|
|
368
|
+
* {@link TESTS_LAYER_CONVENTIONS}.
|
|
369
|
+
*/
|
|
370
|
+
const CMAKE_SOURCE_DIRS = ['src'];
|
|
371
|
+
/**
|
|
372
|
+
* The conventional C++ public-header root, searched with {@link firstSourceDir} like the source
|
|
373
|
+
* root above.
|
|
374
|
+
*
|
|
375
|
+
* It earns a row, and a **source** one: a CMake target's `include/` is its public API — the half of
|
|
376
|
+
* the same module other targets compile against — so it is implementation work under the same
|
|
377
|
+
* {@link MODULE_LAYER_CONVENTIONS} rules as `src/`, and the two are siblings no single path covers.
|
|
378
|
+
*/
|
|
379
|
+
const CMAKE_HEADER_DIRS = ['include'];
|
|
380
|
+
/**
|
|
381
|
+
* The first of `names` that exists as a directory at a manifest root, or `undefined`.
|
|
382
|
+
*
|
|
383
|
+
* {@link cargoSourceDir}'s shape with a list of names instead of one, and its reasons: the row that
|
|
384
|
+
* selects the preset resolves over exactly these roots, and this is not gated on having found the
|
|
385
|
+
* manifest again, so a `--preset` run against a conventional tree still names its source root. It
|
|
386
|
+
* reports the **name** beside the path because the two differ below the root, and the layer takes
|
|
387
|
+
* the name — a nested `services/api/lib` is still the `lib` layer.
|
|
388
|
+
*
|
|
389
|
+
* One function for {@link RUBY_SOURCE_DIRS} and {@link PHP_SOURCE_DIRS} because the two differ only
|
|
390
|
+
* in that list: which names a stack considers conventional is the stack's fact, and the search over
|
|
391
|
+
* manifest roots is not.
|
|
392
|
+
*/
|
|
393
|
+
function firstSourceDir(context, names) {
|
|
394
|
+
for (const root of context.manifestRoots) {
|
|
395
|
+
for (const name of names) {
|
|
396
|
+
const path = root === '.' ? name : `${root}/${name}`;
|
|
397
|
+
if (context.dirExists(path))
|
|
398
|
+
return { name, path };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* The presets whose `general`-only profile is a **decision** rather than a failure to resolve.
|
|
405
|
+
*
|
|
406
|
+
* A `monorepo`'s packages are a judgement call this table declines to make, so one catch-all row is
|
|
407
|
+
* the correct and final answer for it. Every other preset resolves a source root and answers
|
|
408
|
+
* `general` alone only when that root was not found — a state {@link buildPreset} warns about once
|
|
409
|
+
* at init and which `doctor` has to keep grading, since nothing else repeats it.
|
|
410
|
+
*
|
|
411
|
+
* The `flat` fallback (`config/model.ts`'s `FALLBACK_PRESET`) belongs to the same class and is
|
|
412
|
+
* deliberately **not** listed: `doctor`'s `layer-profile` check tests it with the clause beside
|
|
413
|
+
* this set, because a fallback profile is the one thing that check exists to keep warning about.
|
|
414
|
+
*
|
|
415
|
+
* Exported for that check, its only consumer: a second list of names there would drift the moment a
|
|
416
|
+
* preset is added.
|
|
417
|
+
*/
|
|
418
|
+
export const LAYERLESS_BY_DESIGN_PRESETS = new Set(['monorepo']);
|
|
419
|
+
/**
|
|
420
|
+
* The conventional test root of each preset's toolchain, in preference order.
|
|
421
|
+
*
|
|
422
|
+
* Every list is that stack's **convention** rather than a guess — Maven's and Gradle's `src/test`,
|
|
423
|
+
* Dart's `test/`, SwiftPM's `Tests/`, RSpec's `spec/`, PHPUnit's and .NET's `tests/` — which is why
|
|
424
|
+
* existence alone decides, exactly as it does for the source roots above. Where a stack has two, the
|
|
425
|
+
* first is the one a repository carrying both is more likely to be running: `spec` before `test` for
|
|
426
|
+
* the reason {@link bundlerCommands} gives, since such a repository is an RSpec suite with a
|
|
427
|
+
* leftover `test/` far more often than the reverse. The Android candidates mirror
|
|
428
|
+
* `ANDROID_MODULE_DIRS`' `app`-then-root search order.
|
|
429
|
+
*
|
|
430
|
+
* `monorepo` and `flat` are absent **deliberately**, not unresolved: their layer-less profile is a
|
|
431
|
+
* stated decision ({@link LAYERLESS_BY_DESIGN_PRESETS}, and the arm below for `flat`), and a test row
|
|
432
|
+
* would contradict it.
|
|
433
|
+
*
|
|
434
|
+
* The first two entries carry `src/`-nested spellings as well as the root ones because their
|
|
435
|
+
* **source** rows resolve over `DetectContext.candidateRoots` (`findLayeredRoot`, `findApiDir`)
|
|
436
|
+
* while {@link testsLayer} searches the narrower `manifestRoots`: without them, a tree whose source
|
|
437
|
+
* row lands at `src/routes` gets no `tests` row for its `src/test`. A candidate two segments deep is
|
|
438
|
+
* the shape `android-gradle`'s entry already uses, so the table stays one kind of thing.
|
|
439
|
+
*/
|
|
440
|
+
const PRESET_TEST_ROOTS = {
|
|
441
|
+
'layered-clean-arch': ['test', 'tests', 'src/test', 'src/tests'],
|
|
442
|
+
'api-service': ['test', 'tests', 'src/test', 'src/tests'],
|
|
443
|
+
'python-package': ['tests', 'test'],
|
|
444
|
+
flutter: ['test'],
|
|
445
|
+
'android-gradle': ['app/src/test', 'src/test'],
|
|
446
|
+
jvm: ['src/test'],
|
|
447
|
+
dotnet: ['tests'],
|
|
448
|
+
'apple-native': ['Tests'],
|
|
449
|
+
'rust-cargo': ['tests'],
|
|
450
|
+
'ruby-bundler': ['spec', 'test'],
|
|
451
|
+
'php-composer': ['tests'],
|
|
452
|
+
'cmake-cpp': ['tests', 'test'],
|
|
453
|
+
};
|
|
454
|
+
/**
|
|
455
|
+
* The `tests` row for a preset — one or none, dropped when the directory does not exist, which is
|
|
456
|
+
* the rule every source row above follows.
|
|
457
|
+
*
|
|
458
|
+
* Resolved with {@link firstSourceDir} over {@link PRESET_TEST_ROOTS}, so the search runs over
|
|
459
|
+
* `DetectContext.manifestRoots` and a `--preset` run against a conventional tree still names its
|
|
460
|
+
* test root. Two presets' source rows resolve over the wider `candidateRoots` instead, which is why
|
|
461
|
+
* their entries in {@link PRESET_TEST_ROOTS} carry the nested spellings explicitly.
|
|
462
|
+
*/
|
|
463
|
+
function testsLayer(preset, context) {
|
|
464
|
+
const names = PRESET_TEST_ROOTS[preset];
|
|
465
|
+
const dir = names === undefined ? undefined : firstSourceDir(context, names);
|
|
466
|
+
return dir === undefined ? undefined : layer(TESTS_LAYER_NAME, dir.path, TESTS_LAYER_CONVENTIONS);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* The layers for a preset, in the order they are written: the preset's own source rows, then
|
|
470
|
+
* `tests` where the toolchain's conventional test root exists, then the catch-all `general` row.
|
|
471
|
+
*
|
|
472
|
+
* **The test row is inserted here rather than in twelve switch arms** because it is one rule with
|
|
473
|
+
* one exception table, and an arm that forgot it would be invisible — the profile would simply route
|
|
474
|
+
* test work to `general`. {@link presetSourceLayers} always ends with the catch-all, which is what
|
|
475
|
+
* the splice relies on to keep `general` last.
|
|
476
|
+
*
|
|
477
|
+
* **A `tests` row never clears the unresolved-source-root report, on either channel.** The source
|
|
478
|
+
* rows are passed in rather than derived here so {@link buildPreset} gates its general-only warning
|
|
479
|
+
* on `sourceRows`, not on the spliced length; `doctor`'s `layer-profile` check excludes the `tests`
|
|
480
|
+
* row from the scoped rows that clear it, for the same reason. A repository that routes only its
|
|
481
|
+
* test tree below the root has an unresolved **implementation** profile, which is what both channels
|
|
482
|
+
* grade.
|
|
483
|
+
*/
|
|
484
|
+
function presetLayers(sourceRows, preset, context) {
|
|
485
|
+
const rows = [...sourceRows];
|
|
486
|
+
const tests = testsLayer(preset, context);
|
|
487
|
+
if (tests === undefined)
|
|
488
|
+
return rows;
|
|
489
|
+
return [...rows.slice(0, -1), tests, rows[rows.length - 1]];
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* The source layers for a preset, catch-all row last.
|
|
493
|
+
*
|
|
494
|
+
* **A layer whose directory does not exist is dropped rather than written**, so a generated config
|
|
495
|
+
* never points an implementer at a path that is not there. A preset can therefore come back as
|
|
496
|
+
* `general` alone; {@link buildPreset} says so rather than leaving it unexplained.
|
|
497
|
+
*/
|
|
498
|
+
function presetSourceLayers(preset, context) {
|
|
499
|
+
switch (preset) {
|
|
500
|
+
case 'layered-clean-arch': {
|
|
501
|
+
const match = findLayeredRoot(context);
|
|
502
|
+
const layers = (match?.dirs ?? []).map((dir) => layer(dir.name, dir.path, LAYERED_CONVENTIONS[dir.name]));
|
|
503
|
+
return [...layers, generalLayer()];
|
|
504
|
+
}
|
|
505
|
+
case 'api-service': {
|
|
506
|
+
const dir = findApiDir(context);
|
|
507
|
+
return dir === undefined ? [generalLayer()] : [layer('api', dir, API_LAYER_CONVENTIONS), generalLayer()];
|
|
508
|
+
}
|
|
509
|
+
case 'python-package': {
|
|
510
|
+
const dir = findPythonPackageDir(context);
|
|
511
|
+
return dir === undefined ? [generalLayer()] : [layer('package', dir, PACKAGE_LAYER_CONVENTIONS), generalLayer()];
|
|
512
|
+
}
|
|
513
|
+
case 'flutter': {
|
|
514
|
+
// A layered Flutter tree never reaches here: `layered-clean-arch:layer-directories` is
|
|
515
|
+
// evaluated first and gives it `data`, `domain` and `presentation` pointed at the per-layer
|
|
516
|
+
// documents. What arrives here is the feature-first `lib/`, whose decomposition into features
|
|
517
|
+
// is a judgement call, so it gets the one directory that is certain.
|
|
518
|
+
const dir = flutterSourceDir(context);
|
|
519
|
+
return dir === undefined
|
|
520
|
+
? // `buildPreset`'s general-only warning then fires, which is the right answer: a pubspec
|
|
521
|
+
// with no `lib/` has no source root to name.
|
|
522
|
+
[generalLayer()]
|
|
523
|
+
: [layer(DART_SOURCE_DIR, dir, MODULE_LAYER_CONVENTIONS), generalLayer()];
|
|
524
|
+
}
|
|
525
|
+
case 'android-gradle': {
|
|
526
|
+
// The single-module Android application this row exists for: one source root under the
|
|
527
|
+
// module's main source set, named because it is deterministic, and pointed at the
|
|
528
|
+
// single-module document for `flutter`'s reason — decomposing it into features is a
|
|
529
|
+
// judgement call and therefore `/harness-analyze`'s. A **multi-module** Android repository
|
|
530
|
+
// laid out as root-level `data/`, `domain/`, `presentation/` never reaches here: it matches
|
|
531
|
+
// `layered-clean-arch:layer-directories`, which is evaluated well above this preset's row.
|
|
532
|
+
const found = findAndroidModule(context);
|
|
533
|
+
return found === undefined
|
|
534
|
+
? // `buildPreset`'s general-only warning then fires, which is the right answer: no module
|
|
535
|
+
// resolved, so there is no source root to name.
|
|
536
|
+
[generalLayer()]
|
|
537
|
+
: [
|
|
538
|
+
layer('app', mainSourceDir(context, found.module) ?? mainSourceSet(found.module), MODULE_LAYER_CONVENTIONS),
|
|
539
|
+
generalLayer(),
|
|
540
|
+
];
|
|
541
|
+
}
|
|
542
|
+
case 'jvm': {
|
|
543
|
+
// Both build tools reach here, and both get the same one layer: a Maven or Gradle project's
|
|
544
|
+
// sources live under one main source set, naming it is deterministic, and decomposing it into
|
|
545
|
+
// modules or packages is a judgement call and therefore `/harness-analyze`'s. A **multi-module**
|
|
546
|
+
// JVM repository laid out as root-level `data/`, `domain/`, `presentation/` never reaches
|
|
547
|
+
// here — it matches `layered-clean-arch:layer-directories`, far above these rows.
|
|
548
|
+
const dir = jvmSourceDir(context);
|
|
549
|
+
return dir === undefined
|
|
550
|
+
? // `buildPreset`'s general-only warning then fires, which is the right answer: a project
|
|
551
|
+
// with no `src/main` has no source root this table can name.
|
|
552
|
+
[generalLayer()]
|
|
553
|
+
: [layer('main', dir, MODULE_LAYER_CONVENTIONS), generalLayer()];
|
|
554
|
+
}
|
|
555
|
+
case 'dotnet': {
|
|
556
|
+
// One layer at the source root, and **not one per project file**, which is the deliberate
|
|
557
|
+
// line here. Naming a layer per `.csproj` would take its name from the project file
|
|
558
|
+
// (`MyApp.Infrastructure` → `infrastructure`) and thereby decide which project is which
|
|
559
|
+
// layer — a judgement about the adopter's architecture, and therefore `/harness-analyze`'s,
|
|
560
|
+
// exactly as the `monorepo` and `flat` cases below say for packages and modules. Naming
|
|
561
|
+
// `src/` is deterministic; naming its contents is not.
|
|
562
|
+
const dir = dotnetSourceDir(context);
|
|
563
|
+
return dir === undefined
|
|
564
|
+
? // `buildPreset`'s general-only warning then fires, which is the right answer: a solution
|
|
565
|
+
// whose projects sit somewhere other than `src/` has no source root this table can name.
|
|
566
|
+
[generalLayer()]
|
|
567
|
+
: [layer(DOTNET_SOURCE_DIR, dir, MODULE_LAYER_CONVENTIONS), generalLayer()];
|
|
568
|
+
}
|
|
569
|
+
case 'apple-native': {
|
|
570
|
+
// One source root, pointed at the single-module document for `flutter`'s reason: naming
|
|
571
|
+
// `Sources/` — or the directory the Xcode container is named after — is deterministic, while
|
|
572
|
+
// decomposing it into targets or feature modules is a judgement call and therefore
|
|
573
|
+
// `/harness-analyze`'s. The layer is called `sources` in both arms even where the directory is
|
|
574
|
+
// `MyApp/`; {@link APPLE_LAYER_NAME} says why the two are allowed to differ here.
|
|
575
|
+
const dir = appleSourceDir(context);
|
|
576
|
+
return dir === undefined
|
|
577
|
+
? // `buildPreset`'s general-only warning then fires, which is the right answer: a container
|
|
578
|
+
// whose sources sit somewhere this table cannot name has no source root to name.
|
|
579
|
+
[generalLayer()]
|
|
580
|
+
: [layer(APPLE_LAYER_NAME, dir, MODULE_LAYER_CONVENTIONS), generalLayer()];
|
|
581
|
+
}
|
|
582
|
+
case 'rust-cargo': {
|
|
583
|
+
// One layer at the crate's `src/`, for {@link dotnetSourceDir}'s reason: naming the source
|
|
584
|
+
// root is deterministic, while splitting it into modules — or a workspace into its members —
|
|
585
|
+
// is a judgement call and therefore `/harness-analyze`'s.
|
|
586
|
+
const dir = cargoSourceDir(context);
|
|
587
|
+
return dir === undefined
|
|
588
|
+
? // `buildPreset`'s general-only warning then fires, which is the right answer: a manifest
|
|
589
|
+
// whose sources sit somewhere this table cannot name has no source root to name.
|
|
590
|
+
[generalLayer()]
|
|
591
|
+
: [layer(CARGO_SOURCE_DIR, dir, MODULE_LAYER_CONVENTIONS), generalLayer()];
|
|
592
|
+
}
|
|
593
|
+
case 'ruby-bundler': {
|
|
594
|
+
// One layer at whichever conventional source root exists, for {@link cargoSourceDir}'s
|
|
595
|
+
// reason: naming `app/` or `lib/` is deterministic, while splitting either into the
|
|
596
|
+
// sub-directories Rails puts there — `models/`, `controllers/`, `views/` — would decide the
|
|
597
|
+
// adopter's architecture, which is a judgement call and therefore `/harness-analyze`'s. A
|
|
598
|
+
// Rails-shaped repository does not arrive here at all: `api-service:route-directory` matches
|
|
599
|
+
// its `app/controllers` far above this preset's row and gives it the better profile.
|
|
600
|
+
const dir = firstSourceDir(context, RUBY_SOURCE_DIRS);
|
|
601
|
+
return dir === undefined
|
|
602
|
+
? // `buildPreset`'s general-only warning then fires, which is the right answer: a `Gemfile`
|
|
603
|
+
// with neither conventional source root has no source root this table can name.
|
|
604
|
+
[generalLayer()]
|
|
605
|
+
: [layer(dir.name, dir.path, MODULE_LAYER_CONVENTIONS), generalLayer()];
|
|
606
|
+
}
|
|
607
|
+
case 'php-composer': {
|
|
608
|
+
// One layer at whichever conventional source root exists, for the `ruby-bundler` arm's
|
|
609
|
+
// reason: naming Laravel's `app/` or PSR-4's `src/` is deterministic, while splitting either
|
|
610
|
+
// into the sub-directories a framework puts there — `Http/`, `Models/`, `Providers/` — would
|
|
611
|
+
// decide the adopter's architecture, which is `/harness-analyze`'s. A Laravel-shaped
|
|
612
|
+
// repository does not arrive here at all: `api-service:route-directory` matches its `routes/`
|
|
613
|
+
// far above this preset's row and gives it the better profile.
|
|
614
|
+
const dir = firstSourceDir(context, PHP_SOURCE_DIRS);
|
|
615
|
+
return dir === undefined
|
|
616
|
+
? // `buildPreset`'s general-only warning then fires, which is the right answer: a
|
|
617
|
+
// `composer.json` with neither conventional source root has none this table can name.
|
|
618
|
+
[generalLayer()]
|
|
619
|
+
: [layer(dir.name, dir.path, MODULE_LAYER_CONVENTIONS), generalLayer()];
|
|
620
|
+
}
|
|
621
|
+
case 'cmake-cpp': {
|
|
622
|
+
// The two conventional source roots, for the `rust-cargo` arm's reason: naming them is
|
|
623
|
+
// deterministic, while splitting a C++ project into modules, targets or libraries is a
|
|
624
|
+
// judgement call and therefore `/harness-analyze`'s. {@link CMAKE_HEADER_DIRS} says why
|
|
625
|
+
// `include/` is a row of its own and why it is a *source* row.
|
|
626
|
+
const source = firstSourceDir(context, CMAKE_SOURCE_DIRS);
|
|
627
|
+
const headers = firstSourceDir(context, CMAKE_HEADER_DIRS);
|
|
628
|
+
// Each absent root drops its row, the rule every arm above follows — so a project with
|
|
629
|
+
// headers and no `src/` names what it has instead of falling to the catch-all, and one with
|
|
630
|
+
// neither comes back as `general` alone and `buildPreset` warns about it.
|
|
631
|
+
const rows = [
|
|
632
|
+
...(source === undefined ? [] : [layer(source.name, source.path, MODULE_LAYER_CONVENTIONS)]),
|
|
633
|
+
...(headers === undefined ? [] : [layer(headers.name, headers.path, MODULE_LAYER_CONVENTIONS)]),
|
|
634
|
+
];
|
|
635
|
+
return [...rows, generalLayer()];
|
|
636
|
+
}
|
|
637
|
+
case 'monorepo':
|
|
638
|
+
case 'flat':
|
|
639
|
+
// Both are deliberately layer-less: the packages of a monorepo and the shape of an
|
|
640
|
+
// unrecognised tree are judgement calls, and judgement is `/harness-analyze`'s.
|
|
641
|
+
return [generalLayer()];
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* The dependency-install line anchored at an install site below the repository root, recorded in the
|
|
646
|
+
* notes as it is built.
|
|
647
|
+
*
|
|
648
|
+
* One constructor for both sites that can produce it — {@link nodeCommands}' anchoring block and
|
|
649
|
+
* {@link buildPreset}'s fallback for a repository with no root manifest — so the command line and the
|
|
650
|
+
* sentence describing it cannot come to disagree. `because` is the only thing that differs: why the
|
|
651
|
+
* root's own install is not what this repository gets.
|
|
652
|
+
*
|
|
653
|
+
* A note rather than a warning, for the reason `DetectContext.note` states — this is what a correct
|
|
654
|
+
* nested adoption looks like.
|
|
655
|
+
*/
|
|
656
|
+
function anchoredDepInstall(context, site, because) {
|
|
657
|
+
const anchored = `npm --prefix ${site.dir} ${site.hasLockfile ? 'ci' : 'install'}`;
|
|
658
|
+
context.note(`commands.depInstall was derived from \`${site.manifest}\` and runs \`${anchored}\` from the repository root, because commands are executed there and ${because}`);
|
|
659
|
+
return anchored;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Raw commands from the repository root's npm manifest, or `undefined` when there is none **or when
|
|
663
|
+
* it declares none of the scripts this harness runs**.
|
|
664
|
+
*
|
|
665
|
+
* The family gate and the four **script-derived** keys still read the root manifest, for the reason
|
|
666
|
+
* `DetectContext.rootPackageJson` states. `depInstall` is the exception and the only one:
|
|
667
|
+
* it follows the manifest that was actually found, because it is a whole command line rather than a
|
|
668
|
+
* script name resolved against the manifest the runner reads. Returning `undefined` from the gate
|
|
669
|
+
* therefore does not cost the install — {@link buildPreset} derives it from the same site when no
|
|
670
|
+
* family resolved one.
|
|
671
|
+
*
|
|
672
|
+
* **The gate is a resolved script, not a manifest that exists**, because a family whose search stops
|
|
673
|
+
* the loop has to have something to offer. The fixture that forced it: a Go module carrying a
|
|
674
|
+
* scripts-less root `package.json` — for a documentation site, or a lint-only tooling manifest —
|
|
675
|
+
* answered `typecheck` and `test` as placeholders and `depInstall: npm install`, because this
|
|
676
|
+
* function returned a truthy `depInstall`-only object and {@link buildPreset}'s loop broke before
|
|
677
|
+
* `goCommands` ever ran. The same tree without that manifest answered `go vet ./...` and
|
|
678
|
+
* `go test ./...`. A manifest declaring nothing the harness runs now stops nothing.
|
|
679
|
+
*/
|
|
680
|
+
function nodeCommands(context) {
|
|
681
|
+
if (context.rootPackageJson() === undefined)
|
|
682
|
+
return undefined;
|
|
683
|
+
const scripts = context.scriptNames();
|
|
684
|
+
const first = (candidates) => candidates.find((name) => scripts.has(name));
|
|
685
|
+
const commands = {};
|
|
686
|
+
const typecheck = first(NODE_SCRIPT_CANDIDATES.typecheck);
|
|
687
|
+
if (typecheck !== undefined) {
|
|
688
|
+
commands.typecheck = `npm run ${typecheck}`;
|
|
689
|
+
// A weak candidate is accepted and said out loud, at the severity the evidence supports. A
|
|
690
|
+
// `tsconfig.json` at the root is file-existence evidence that the repository is typed, so a
|
|
691
|
+
// `lint` line is very unlikely to be its type check and the adopter is told loudly; without one,
|
|
692
|
+
// `lint` is plausibly exactly what they meant, and a warning on every correct plain-JS adoption
|
|
693
|
+
// is how a warning list stops being read (`DetectContext.note`).
|
|
694
|
+
if (WEAK_TYPECHECK_SCRIPTS.has(typecheck)) {
|
|
695
|
+
const message = weakTypecheckMessage(typecheck, commands.typecheck);
|
|
696
|
+
if (context.fileExists(TYPED_REPOSITORY_EVIDENCE))
|
|
697
|
+
context.warn(message);
|
|
698
|
+
else
|
|
699
|
+
context.note(message);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
// `npm test` rather than `npm run test`: it is the lifecycle script's own invocation.
|
|
703
|
+
if (first(NODE_SCRIPT_CANDIDATES.test) !== undefined)
|
|
704
|
+
commands.test = 'npm test';
|
|
705
|
+
const build = first(NODE_SCRIPT_CANDIDATES.build);
|
|
706
|
+
if (build !== undefined)
|
|
707
|
+
commands.build = `npm run ${build}`;
|
|
708
|
+
const devServer = first(NODE_SCRIPT_CANDIDATES.devServer);
|
|
709
|
+
if (devServer !== undefined)
|
|
710
|
+
commands.devServer = `npm run ${devServer}`;
|
|
711
|
+
// The one arm {@link devServerPrestart} cannot fix, so it is said instead: a dev-server line that
|
|
712
|
+
// serves pre-compiled output, and no `build` script to put in front of it. Where both resolve, the
|
|
713
|
+
// generated wrapper runs the build before it launches and there is nothing to report.
|
|
714
|
+
if (devServer !== undefined && build === undefined && PRECOMPILED_DEV_SERVER_SCRIPTS.includes(devServer)) {
|
|
715
|
+
context.note(unbuildableDevServerNote(`npm run ${devServer}`));
|
|
716
|
+
}
|
|
717
|
+
// Nothing script-derived resolved, so this family has nothing to supply: hand the search on rather
|
|
718
|
+
// than stopping it with a `depInstall`-only object. Everything below is unchanged for the case
|
|
719
|
+
// where a script *did* resolve — the anchoring block and its workspace carve-out included.
|
|
720
|
+
if (Object.keys(commands).length === 0)
|
|
721
|
+
return undefined;
|
|
722
|
+
// A `depInstall` derived from the root of a repository whose application nests under `appDir`
|
|
723
|
+
// installs nothing for that application, and says `up to date` while doing it — so it is anchored
|
|
724
|
+
// at the manifest that was actually found, with `--prefix` because commands run from the
|
|
725
|
+
// repository root.
|
|
726
|
+
//
|
|
727
|
+
// **The workspace carve-out, which is load-bearing rather than incidental:** a workspace monorepo
|
|
728
|
+
// keeps one lockfile at the root and a manifest per package, so `npm --prefix <member> ci` fails
|
|
729
|
+
// for want of a lockfile while the root `npm ci` installs every member. `isWorkspaceRoot()` is the
|
|
730
|
+
// test that separates the two, and it is the union of *both* the detection table's `monorepo` rows
|
|
731
|
+
// — the `workspaces` key (`monorepo:workspaces-key`) and a workspace-tool manifest at the root
|
|
732
|
+
// (`monorepo:workspace-manifest`, which a pnpm workspace matches without ever setting the key). One
|
|
733
|
+
// predicate for both so the carve-out and the table cannot disagree about what a workspace root is.
|
|
734
|
+
//
|
|
735
|
+
// **The guard residual the anchored form carries**, recorded here so it is not re-derived from one
|
|
736
|
+
// row of the table: the spelling is `allow` at `docs/guard-verification.md` row **a7**, and
|
|
737
|
+
// **SILENT — one prompt** at row **b1**, whose fixture's `appDir` carries a character outside the
|
|
738
|
+
// guard's path class (a scoped `packages/@acme/…`; **b4** is the bare form's `allow` and **b5** the
|
|
739
|
+
// control attributing b1's silence to that character rather than to the shape). The anchoring is
|
|
740
|
+
// left unconditional anyway: §(b) is an **open recorded residual** whose own bound is *do not widen
|
|
741
|
+
// the class to buy it back*, and declining the anchor there would restore the silent no-install —
|
|
742
|
+
// a prompt parks an unattended run, a green-but-empty install does not announce itself.
|
|
743
|
+
const site = findNodeInstallSite(context);
|
|
744
|
+
if (site !== undefined && site.dir !== '.' && !isWorkspaceRoot(context)) {
|
|
745
|
+
commands.depInstall = anchoredDepInstall(context, site, 'a bare `npm ci` at the root would install nothing for the application');
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
// The lockfile is the whole difference: `npm ci` needs one and installs exactly it.
|
|
749
|
+
commands.depInstall = context.fileExists(PACKAGE_LOCKFILE) ? 'npm ci' : 'npm install';
|
|
750
|
+
}
|
|
751
|
+
return commands;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Say which manifest an anchored family's command lines came from, and where they run.
|
|
755
|
+
*
|
|
756
|
+
* One note per family rather than one per key: both keys come from the same manifest, so a second
|
|
757
|
+
* line would repeat the only fact the first carries. It names both keys for that same reason — a
|
|
758
|
+
* reader checking either line has to find it here, and a note whose subject is one key leaves the
|
|
759
|
+
* other unexplained. A note rather than a warning, for the reason `DetectContext.note` states —
|
|
760
|
+
* this is what a correct nested adoption looks like.
|
|
761
|
+
*/
|
|
762
|
+
function noteAnchoredFamily(context, manifest, typecheck, test) {
|
|
763
|
+
context.note(`commands.typecheck and commands.test were derived from \`${manifest}\` and run \`${typecheck}\` and \`${test}\` from the repository root, which is where every configured command is executed — the directory in each line is what points the tool at the manifest's own tree`);
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Raw commands for a Python project, or `undefined` when no packaging manifest is present.
|
|
767
|
+
*
|
|
768
|
+
* **The directory argument is the fix, not a nicety.** Both lines are executed from the repository
|
|
769
|
+
* root, so a bare `pytest` in a repository whose packaging manifest is nested reads none of that
|
|
770
|
+
* manifest's configuration — its `testpaths`, its `rootdir`, its import path — and fails or, worse,
|
|
771
|
+
* collects nothing. The argument points each tool at the tree the manifest was found in, which is
|
|
772
|
+
* the same directory `findPythonManifest` already reported as the detection evidence.
|
|
773
|
+
*
|
|
774
|
+
* The root arm keeps today's exact strings, `pytest` bare rather than `pytest .`, so a repository
|
|
775
|
+
* that is its own application generates what it generated before.
|
|
776
|
+
*/
|
|
777
|
+
function pythonCommands(context) {
|
|
778
|
+
const manifest = findPythonManifest(context);
|
|
779
|
+
if (manifest === undefined)
|
|
780
|
+
return undefined;
|
|
781
|
+
const dir = manifestDirectory(manifest);
|
|
782
|
+
if (dir === '.')
|
|
783
|
+
return { typecheck: 'mypy .', test: 'pytest' };
|
|
784
|
+
const commands = { typecheck: `mypy ${dir}`, test: `pytest ${dir}` };
|
|
785
|
+
noteAnchoredFamily(context, manifest, commands.typecheck, commands.test);
|
|
786
|
+
return commands;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Raw commands for a Go module, or `undefined` when there is no `go.mod`.
|
|
790
|
+
*
|
|
791
|
+
* `go -C <dir>` — a `go` 1.20 flag, and the spelling that keeps the package pattern `./...` meaning
|
|
792
|
+
* the module rather than the repository root the command is executed from. Teaching the wrapper to
|
|
793
|
+
* `cd` instead was declined: it would put a repo-relative path inside `templates/scripts/*.sh`,
|
|
794
|
+
* where this module never writes and where a token-carrying template change owes the rendered
|
|
795
|
+
* example a re-render.
|
|
796
|
+
*/
|
|
797
|
+
function goCommands(context) {
|
|
798
|
+
const manifest = findGoManifest(context);
|
|
799
|
+
if (manifest === undefined)
|
|
800
|
+
return undefined;
|
|
801
|
+
const dir = manifestDirectory(manifest);
|
|
802
|
+
if (dir === '.')
|
|
803
|
+
return { typecheck: 'go vet ./...', test: 'go test ./...' };
|
|
804
|
+
const commands = { typecheck: `go -C ${dir} vet ./...`, test: `go -C ${dir} test ./...` };
|
|
805
|
+
noteAnchoredFamily(context, manifest, commands.typecheck, commands.test);
|
|
806
|
+
return commands;
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* The paths, relative to the directory holding the `pubspec.yaml`, whose existence makes the package
|
|
810
|
+
* an **application** and therefore selects the `flutter` tool over `dart`.
|
|
811
|
+
*
|
|
812
|
+
* Any one of them is enough. `lib/main.dart` is the entry point every Flutter application has;
|
|
813
|
+
* `android/` and `ios/` are the platform trees `flutter create` writes and a plain package never
|
|
814
|
+
* carries.
|
|
815
|
+
*
|
|
816
|
+
* Choosing wrong is not a cosmetic error in either direction: `dart analyze` on a Flutter
|
|
817
|
+
* application reports every `package:flutter` import as unresolved, and `flutter test` in a
|
|
818
|
+
* repository with no Flutter SDK on `PATH` cannot run at all.
|
|
819
|
+
*
|
|
820
|
+
* **This constant is now one of two positives, not the whole test.** An application with a
|
|
821
|
+
* relocated entry point (`lib/src/…`, a multi-flavour `lib/main_dev.dart`) and no platform trees
|
|
822
|
+
* checked in carries none of these markers and used to fall to `dart` — F68. The second positive is
|
|
823
|
+
* `DetectContext.declaresFlutterSdk`, the manifest's own Flutter SDK dependency; why that read does
|
|
824
|
+
* not reopen the no-content-heuristics rule is argued once, on `signals.ts`'s `PUBSPEC_MANIFEST`.
|
|
825
|
+
* The disjunction is deliberately additive: every tree that resolved `flutter` before still does,
|
|
826
|
+
* so the only repositories that move are the ones the finding names.
|
|
827
|
+
*/
|
|
828
|
+
const FLUTTER_APP_MARKERS = ['lib/main.dart', 'android', 'ios'];
|
|
829
|
+
/** The `build_runner` configuration file, whose presence means sources are generated before analysis. */
|
|
830
|
+
const DART_CODEGEN_MANIFEST = 'build.yaml';
|
|
831
|
+
/** The generator a {@link DART_CODEGEN_MANIFEST} configures, named in the note as the usual spelling. */
|
|
832
|
+
const DART_CODEGEN_COMMAND = 'dart run build_runner build';
|
|
833
|
+
/**
|
|
834
|
+
* Raw commands for a Dart or Flutter package, or `undefined` when there is no `pubspec.yaml`.
|
|
835
|
+
*
|
|
836
|
+
* **Two arms, two positives.** A package carrying any of {@link FLUTTER_APP_MARKERS}, or whose
|
|
837
|
+
* manifest declares a Flutter SDK dependency, is an application and gets the `flutter` tool;
|
|
838
|
+
* anything else is a plain Dart package and gets `dart`. The marker arm is evaluated first so the
|
|
839
|
+
* cheap `statSync` probes short-circuit the manifest read. The two tools are not interchangeable in
|
|
840
|
+
* either direction, for the reason that constant states.
|
|
841
|
+
*
|
|
842
|
+
* **`build` and `devServer` stay unset, deliberately.** `commands.build`'s only consumer is
|
|
843
|
+
* `setup-worktree.sh`, which runs it in every worktree the flow cuts and which no agent invokes for
|
|
844
|
+
* verification — so a `flutter build apk` would spend minutes per worktree and prove nothing that
|
|
845
|
+
* `flutter analyze` and `flutter test` have not already proved. A dev server needs a device or
|
|
846
|
+
* platform selection, which is a judgement call rather than something file existence answers; the
|
|
847
|
+
* key is left absent and `undeclaredOptionalCommandNote` covers the one population that can be told
|
|
848
|
+
* what to set.
|
|
849
|
+
*
|
|
850
|
+
* The directory argument on the anchored arm is `pythonCommands`' rule, for its reason: the lines
|
|
851
|
+
* run from the repository root, so a bare `dart analyze` in a repository whose pubspec is nested
|
|
852
|
+
* analyses the wrong tree or none.
|
|
853
|
+
*/
|
|
854
|
+
function dartCommands(context) {
|
|
855
|
+
const manifest = findFlutterManifest(context);
|
|
856
|
+
if (manifest === undefined)
|
|
857
|
+
return undefined;
|
|
858
|
+
const dir = manifestDirectory(manifest);
|
|
859
|
+
const prefix = (path) => (dir === '.' ? path : `${dir}/${path}`);
|
|
860
|
+
const tool = FLUTTER_APP_MARKERS.some((marker) => context.fileExists(prefix(marker)) || context.dirExists(prefix(marker))) || context.declaresFlutterSdk(manifest)
|
|
861
|
+
? 'flutter'
|
|
862
|
+
: 'dart';
|
|
863
|
+
const anchored = dir !== '.';
|
|
864
|
+
const typecheck = anchored ? `${tool} analyze ${dir}` : `${tool} analyze`;
|
|
865
|
+
const test = anchored ? `${tool} test ${dir}/test` : `${tool} test`;
|
|
866
|
+
// **This family resolves `depInstall`, which neither the Python nor the Go family does**, and that
|
|
867
|
+
// is what it is worth: `pub get` is the install a Dart repository actually needs, and resolving it
|
|
868
|
+
// here means `buildPreset`'s npm fallback is never reached — so a Flutter repository carrying a
|
|
869
|
+
// documentation-site `package.json` is not handed an `npm install` beside its Dart lines.
|
|
870
|
+
const depInstall = anchored ? `${tool} pub get --directory ${dir}` : `${tool} pub get`;
|
|
871
|
+
if (anchored)
|
|
872
|
+
noteAnchoredFamily(context, manifest, typecheck, test);
|
|
873
|
+
// A generated-source step is a **precondition of the analyzer**, not a fifth command key: the
|
|
874
|
+
// analyze line fails on a missing `.g.dart` until the generator has run, so it belongs on the line
|
|
875
|
+
// above it inside the wrapper `commands.typecheck` names. A `commands.codegen` would put a key in
|
|
876
|
+
// the schema that one stack sets and every other reader has to skip.
|
|
877
|
+
if (context.fileExists(prefix(DART_CODEGEN_MANIFEST))) {
|
|
878
|
+
context.note(`\`${prefix(DART_CODEGEN_MANIFEST)}\` sits beside \`${manifest}\`, so this repository generates sources before they analyse and \`${typecheck}\` fails on a missing generated file until the generator has run: add that generator — \`${DART_CODEGEN_COMMAND}\` for the usual build_runner setup — as the line above it inside the wrapper script commands.typecheck names, rather than as a command key of its own`);
|
|
879
|
+
}
|
|
880
|
+
return { typecheck, test, depInstall };
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* The runner for a JVM build tool: the project's own wrapper script where the tree ships one,
|
|
884
|
+
* `fallback` from `PATH` where it does not. One helper for all three JVM arms, because the rule is one
|
|
885
|
+
* rule and three copies of it drift.
|
|
886
|
+
*
|
|
887
|
+
* **Below the root there are two candidates, co-located first.** The wrapper beside the manifest
|
|
888
|
+
* belongs to the project these lines build, so it wins wherever it exists. A repository that keeps its
|
|
889
|
+
* wrapper at the root with the manifest below it still gets a wrapper rather than a `PATH` lookup that
|
|
890
|
+
* exits 127 on a machine with no system Maven or Gradle installed — the anchor these arms already emit
|
|
891
|
+
* (`-f` for Maven, `-p` for Gradle) names the tree either way, so the root wrapper builds the same
|
|
892
|
+
* project the co-located one would.
|
|
893
|
+
*/
|
|
894
|
+
function jvmRunner(context, dir, wrapper, fallback) {
|
|
895
|
+
const candidates = dir === '.' ? [wrapper] : [`${dir}/${wrapper}`, wrapper];
|
|
896
|
+
const found = candidates.find((candidate) => context.fileExists(candidate));
|
|
897
|
+
return found === undefined ? fallback : `./${found}`;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Raw commands for an Android Gradle project, or `undefined` when no Android module resolved.
|
|
901
|
+
*
|
|
902
|
+
* **The two task names are AGP's own, not invented ones.** `compile<Variant>Sources` and
|
|
903
|
+
* `test<Variant>UnitTest` are the per-variant tasks the Android Gradle plugin registers for every
|
|
904
|
+
* application and library module, over the `debug` build type AGP always creates — the same
|
|
905
|
+
* spellings on AGP 7 and 8 — so `compileDebugSources` type-checks the module's sources and
|
|
906
|
+
* `testDebugUnitTest` runs its JVM unit tests without a device. A task name that could not be
|
|
907
|
+
* established across both majors would have been left unresolved (placeholder plus the standing
|
|
908
|
+
* warning) rather than shipped: this module's header forbids inventing a command outright, and a
|
|
909
|
+
* Gradle invocation naming a task that does not exist fails with `Task not found`, which reads as a
|
|
910
|
+
* broken repository rather than as an undetected command.
|
|
911
|
+
*
|
|
912
|
+
* `--console=plain --quiet` because these lines run unattended inside a wrapper script: the rich
|
|
913
|
+
* console renders progress bars into a captured log, and `--quiet` leaves the failure output, which
|
|
914
|
+
* is the only part a run reads. The exit status is untouched by either flag.
|
|
915
|
+
*
|
|
916
|
+
* **The runner is the wrapper when the project ships one.** `gradlew` pins the Gradle version the
|
|
917
|
+
* project builds with, so `gradle` from `PATH` — whatever version that is — is the fallback for a
|
|
918
|
+
* project that ships none, not the preference. {@link jvmRunner} is where the two candidate locations
|
|
919
|
+
* and their order are argued.
|
|
920
|
+
*
|
|
921
|
+
* **`build`, `devServer` and `depInstall` are deliberately unset, each for its own reason.**
|
|
922
|
+
* `commands.build`'s only consumer is `setup-worktree.sh`, which runs it in every worktree the flow
|
|
923
|
+
* cuts and which no agent invokes for verification, so an `assembleDebug` would spend a multi-minute
|
|
924
|
+
* native build per worktree and buy no verification an agent will ever run. An Android application
|
|
925
|
+
* has no dev server to start. And Gradle resolves dependencies as part of the task being run, so a
|
|
926
|
+
* separate install step would pay the same download twice per worktree and verify nothing.
|
|
927
|
+
*/
|
|
928
|
+
function androidGradleCommands(context) {
|
|
929
|
+
const found = findAndroidModule(context);
|
|
930
|
+
if (found === undefined)
|
|
931
|
+
return undefined;
|
|
932
|
+
const runner = jvmRunner(context, found.root, GRADLE_WRAPPER, 'gradle');
|
|
933
|
+
const anchored = found.root !== '.';
|
|
934
|
+
// The lines run from the repository root, so a project below it is named with `-p` for
|
|
935
|
+
// `pythonCommands`' reason: an invocation made at one directory and run at another builds the
|
|
936
|
+
// wrong tree or none.
|
|
937
|
+
const project = anchored ? ` -p ${found.root}` : '';
|
|
938
|
+
const gradle = `${runner}${project} --console=plain --quiet`;
|
|
939
|
+
const commands = { typecheck: `${gradle} compileDebugSources`, test: `${gradle} testDebugUnitTest` };
|
|
940
|
+
if (anchored)
|
|
941
|
+
noteAnchoredFamily(context, joinModuleManifest(found), commands.typecheck, commands.test);
|
|
942
|
+
return commands;
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Raw commands for a Maven project, or `undefined` when there is no `pom.xml`.
|
|
946
|
+
*
|
|
947
|
+
* **Why a compiled stack still gets both required keys, and this is both JVM families' answer.**
|
|
948
|
+
* `test` recompiles what `typecheck` compiled, on Maven and on Gradle alike, so the pair overlaps by
|
|
949
|
+
* construction. It is kept anyway: `typecheck` is the cheaper of the two, it runs first on every
|
|
950
|
+
* task, and it fails faster — a compile error is reported without waiting for a test tree to build
|
|
951
|
+
* and run. Collapsing them would make every task pay the test run to learn it does not compile.
|
|
952
|
+
*
|
|
953
|
+
* `-q -B` because these lines run unattended inside a wrapper script: `-B` drops the ANSI progress
|
|
954
|
+
* rendering that a captured log turns into noise, and `-q` leaves the failure output, which is the
|
|
955
|
+
* only part a run reads. Neither touches the exit status.
|
|
956
|
+
*
|
|
957
|
+
* `compile` for `typecheck` and `test` for `test` are Maven's own lifecycle phases, present in every
|
|
958
|
+
* POM without a plugin declaration, so neither is an invented command line — the rule this module's
|
|
959
|
+
* header states. **`depInstall` is `dependency:go-offline`**, a real goal that resolves the whole
|
|
960
|
+
* dependency tree into the local repository once per worktree, which is exactly what `depInstall`
|
|
961
|
+
* exists for; `build` and `devServer` stay unset, because `commands.build`'s only consumer is
|
|
962
|
+
* `setup-worktree.sh` and a `package` per worktree would prove nothing `compile` and `test` have not,
|
|
963
|
+
* and a Maven project has no dev server file existence can start.
|
|
964
|
+
*
|
|
965
|
+
* `-f <dir>/pom.xml` below the root is `pythonCommands`' rule for its reason: every line is executed
|
|
966
|
+
* from the repository root, so a bare `mvn` in a repository whose POM is nested builds the wrong
|
|
967
|
+
* tree or none.
|
|
968
|
+
*
|
|
969
|
+
* **The runner is the wrapper when the project ships one**, for {@link androidGradleCommands}'
|
|
970
|
+
* reason: `mvnw` pins the Maven version the project builds with, so `mvn` from `PATH` is the
|
|
971
|
+
* fallback for a project that ships none. {@link jvmRunner} resolves it, preferring the wrapper in the
|
|
972
|
+
* manifest's own directory — the one `-f` names — over one at the repository root.
|
|
973
|
+
*
|
|
974
|
+
* **A co-located wrapper is invoked by path from the repository root, which assumes `mvnw` resolves
|
|
975
|
+
* its project base directory from its own location** (`find_maven_basedir "$(dirname "$0")"`) rather
|
|
976
|
+
* than from the working directory — the property `gradlew` has too, and the one current
|
|
977
|
+
* maven-wrapper releases ship, `mvn -N wrapper:wrapper` among them. A wrapper old enough to resolve
|
|
978
|
+
* from `$(pwd)` would look for `.mvn/wrapper/` at the repository root instead of beside the POM;
|
|
979
|
+
* `MAVEN_BASEDIR=<dir>` ahead of the line overrides that on both script generations. The
|
|
980
|
+
* root-wrapper fallback is unexposed: its own directory *is* the root.
|
|
981
|
+
*
|
|
982
|
+
* **`./mvnw`, not `sh mvnw`.** The Gradle arms carry the identical exposure to a lost executable bit
|
|
983
|
+
* and spell it `./gradlew`; `mvn -N wrapper:wrapper` writes `mvnw` executable and git records the
|
|
984
|
+
* bit, so the interpreter is the project's choice to make. `sh mvnw` would run, behind an
|
|
985
|
+
* interpreter this module picked, a wrapper the project's own tooling cannot — and a checkout that
|
|
986
|
+
* lost the bit would succeed silently instead of failing while naming the file, which is the
|
|
987
|
+
* actionable outcome.
|
|
988
|
+
*/
|
|
989
|
+
function mavenCommands(context) {
|
|
990
|
+
const manifest = findMavenManifest(context);
|
|
991
|
+
if (manifest === undefined)
|
|
992
|
+
return undefined;
|
|
993
|
+
const dir = manifestDirectory(manifest);
|
|
994
|
+
const runner = jvmRunner(context, dir, MAVEN_WRAPPER, 'mvn');
|
|
995
|
+
const mvn = dir === '.' ? `${runner} -q -B` : `${runner} -q -B -f ${manifest}`;
|
|
996
|
+
const commands = { typecheck: `${mvn} compile`, test: `${mvn} test`, depInstall: `${mvn} dependency:go-offline` };
|
|
997
|
+
if (dir !== '.')
|
|
998
|
+
noteAnchoredFamily(context, manifest, commands.typecheck, commands.test);
|
|
999
|
+
return commands;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Raw commands for a plain JVM Gradle project, or `undefined` when it carries no Gradle manifest.
|
|
1003
|
+
*
|
|
1004
|
+
* **The two task names are the Java plugin's own, not invented ones.** `classes` compiles the main
|
|
1005
|
+
* source set and `test` runs the test one; both are registered by the `java` plugin every JVM
|
|
1006
|
+
* Gradle build applies, so neither can fail with `Task not found` on a project this row matched.
|
|
1007
|
+
* `assemble` was **not** taken for `build`: `commands.build`'s only consumer is `setup-worktree.sh`,
|
|
1008
|
+
* so a jar per worktree would buy no verification an agent runs.
|
|
1009
|
+
*
|
|
1010
|
+
* The runner, the `--console=plain --quiet` pair and the absent `depInstall` are
|
|
1011
|
+
* {@link androidGradleCommands}' answers for its reasons — the wrapper pins the Gradle version the
|
|
1012
|
+
* project builds with, the flags keep an unattended log readable, and Gradle resolves dependencies
|
|
1013
|
+
* as part of the task being run, so a separate install would pay the same download twice per
|
|
1014
|
+
* worktree. A JVM service has no dev server file existence can start, so `devServer` stays unset too.
|
|
1015
|
+
*/
|
|
1016
|
+
function gradleJvmCommands(context) {
|
|
1017
|
+
const manifest = findGradleManifest(context);
|
|
1018
|
+
if (manifest === undefined)
|
|
1019
|
+
return undefined;
|
|
1020
|
+
const root = manifestDirectory(manifest);
|
|
1021
|
+
const runner = jvmRunner(context, root, GRADLE_WRAPPER, 'gradle');
|
|
1022
|
+
const anchored = root !== '.';
|
|
1023
|
+
const gradle = `${runner}${anchored ? ` -p ${root}` : ''} --console=plain --quiet`;
|
|
1024
|
+
const commands = { typecheck: `${gradle} classes`, test: `${gradle} test` };
|
|
1025
|
+
if (anchored)
|
|
1026
|
+
noteAnchoredFamily(context, manifest, commands.typecheck, commands.test);
|
|
1027
|
+
return commands;
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Raw commands for a .NET repository, or `undefined` when no solution or project file was found.
|
|
1031
|
+
*
|
|
1032
|
+
* **`<target>` is the found solution or project, and it is present only below the repository
|
|
1033
|
+
* root.** Every configured command runs from the root, so a bare `dotnet build` in a repository
|
|
1034
|
+
* whose solution is nested finds no project and fails; at the root, `dotnet` resolves the single
|
|
1035
|
+
* solution or project itself, and naming it would add nothing.
|
|
1036
|
+
*
|
|
1037
|
+
* `--nologo` for {@link mavenCommands}' `-B` reason: these lines run unattended inside a wrapper,
|
|
1038
|
+
* and the banner is noise in a captured log. It does not touch the exit status or the diagnostics.
|
|
1039
|
+
*
|
|
1040
|
+
* **`typecheck` is the compile, and `test` recompiles it** — the overlap both JVM families already
|
|
1041
|
+
* document, kept for the same reason: `typecheck` is the cheaper of the two, runs first on every
|
|
1042
|
+
* task and fails faster than a test tree that has to build before it can report a type error.
|
|
1043
|
+
*
|
|
1044
|
+
* **`depInstall` is `dotnet restore`**, a real command that resolves the whole dependency graph
|
|
1045
|
+
* once per worktree, which is what that key is for. **`build` and `devServer` stay unset.**
|
|
1046
|
+
* `commands.build`'s only consumer is `setup-worktree.sh` and it is never agent-run, so an MSBuild
|
|
1047
|
+
* line there would duplicate the compile `typecheck` already runs and buy no verification; and
|
|
1048
|
+
* `dotnet run` needs a project chosen from among the solution's, which is a judgement rather than
|
|
1049
|
+
* something file existence answers. Both absences are visible — the optional keys are written as
|
|
1050
|
+
* nothing at all rather than as a placeholder, and `doctor` reports them.
|
|
1051
|
+
*/
|
|
1052
|
+
function dotnetCommands(context) {
|
|
1053
|
+
const project = findDotnetProject(context);
|
|
1054
|
+
if (project === undefined)
|
|
1055
|
+
return undefined;
|
|
1056
|
+
const anchored = manifestDirectory(project) !== '.';
|
|
1057
|
+
const target = anchored ? ` ${project}` : '';
|
|
1058
|
+
const commands = {
|
|
1059
|
+
typecheck: `dotnet build --nologo${target}`,
|
|
1060
|
+
test: `dotnet test --nologo${target}`,
|
|
1061
|
+
depInstall: `dotnet restore${target}`,
|
|
1062
|
+
};
|
|
1063
|
+
if (anchored)
|
|
1064
|
+
noteAnchoredFamily(context, project, commands.typecheck, commands.test);
|
|
1065
|
+
return commands;
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* What an Xcode-arm run has to say about the key it did not derive, and — where no single shared
|
|
1069
|
+
* scheme decided it — about the other one too.
|
|
1070
|
+
*
|
|
1071
|
+
* **`commands.test` is the one entry on this matrix that stays hand-written, and this is the
|
|
1072
|
+
* sentence that says so.** `xcodebuild test` requires a `-destination`, which names a simulator or a
|
|
1073
|
+
* device on the machine that runs it; no file in the repository states which, so deriving one would
|
|
1074
|
+
* be the invention this module's header forbids. The placeholder plus its standing warning is the
|
|
1075
|
+
* visible outcome the design prefers, and this note is what turns it from a gap into a decision.
|
|
1076
|
+
*
|
|
1077
|
+
* A **note** rather than a warning because it is not a fault of the repository, and because
|
|
1078
|
+
* `buildPreset` already raises the warning for the placeholder itself — a second warning saying the
|
|
1079
|
+
* same thing at the same severity reads as a bug.
|
|
1080
|
+
*/
|
|
1081
|
+
function appleXcodeNote(project, containerFlag, schemes) {
|
|
1082
|
+
const derived = schemes.length === 1
|
|
1083
|
+
? `commands.typecheck was derived from the one scheme \`${project}\` shares, \`${schemes[0]}\`.`
|
|
1084
|
+
: `\`${project}\` shares ${schemes.length === 0 ? 'no scheme' : `${schemes.length} schemes (${schemes.join(', ')})`} under ` +
|
|
1085
|
+
`${XCODE_SHARED_SCHEMES_DIR}, so commands.typecheck was left unresolved as well: \`-scheme\` takes exactly one name, and ` +
|
|
1086
|
+
'picking among several — or inventing one where none is shared — is a judgement rather than something file existence answers.';
|
|
1087
|
+
const scheme = schemes.length === 1 ? schemes[0] : '<scheme>';
|
|
1088
|
+
return (`${derived} commands.test was not derived from \`${project}\`: \`xcodebuild test\` requires a \`-destination\`, which names a ` +
|
|
1089
|
+
'simulator or device on the machine that runs it and is therefore not a fact any file in this repository states — init wrote a ' +
|
|
1090
|
+
`placeholder rather than inventing one. Set commands.test to \`xcodebuild test -${containerFlag} ${project} -scheme ${scheme} ` +
|
|
1091
|
+
`-destination '<destination>'\` in harness.config.json, ${WRAPPED_KEY_SECOND_STEP}`);
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Raw commands for an Apple-native repository, or `undefined` when it holds neither a
|
|
1095
|
+
* `Package.swift` nor an Xcode container.
|
|
1096
|
+
*
|
|
1097
|
+
* **Two arms, and only one of them can serve both required keys.**
|
|
1098
|
+
*
|
|
1099
|
+
* *SwiftPM* — a `Package.swift` exists — yields `swift build` and `swift test`, both of which need
|
|
1100
|
+
* nothing but the manifest; below the repository root each carries `--package-path <dir>`, which is
|
|
1101
|
+
* `pythonCommands`' rule for its reason: the lines run from the root, and a bare `swift build` there
|
|
1102
|
+
* builds the wrong tree or none. **`depInstall` is `swift package resolve`**, carrying the same
|
|
1103
|
+
* `--package-path`, for {@link cargoCommands}' `cargo fetch` reason: {@link HarnessCommands.depInstall}
|
|
1104
|
+
* prepares a fresh checkout, its only consumer is `setup-worktree.sh`, so the resolve is paid once
|
|
1105
|
+
* per worktree instead of inside that worktree's first build. It is not paid twice — a later
|
|
1106
|
+
* `swift build` reads the `Package.resolved` and `.build/checkouts` the resolve wrote.
|
|
1107
|
+
* No `build` and no `devServer`: `commands.build`'s only consumer is `setup-worktree.sh`, so a
|
|
1108
|
+
* release build per worktree would prove nothing `swift build` and `swift test` have not, and a
|
|
1109
|
+
* package has no dev server file existence can start.
|
|
1110
|
+
*
|
|
1111
|
+
* *Xcode* — no `Package.swift`, but a project or workspace — yields `typecheck` **only when the
|
|
1112
|
+
* container shares exactly one scheme**, since `-scheme` takes one name and choosing among several
|
|
1113
|
+
* is not reproducible. `test` is left unresolved **in every case**, for the reason
|
|
1114
|
+
* {@link appleXcodeNote} states.
|
|
1115
|
+
*
|
|
1116
|
+
* **This arm stops the family search even where it resolved nothing**, which is the one departure
|
|
1117
|
+
* from the rule `nodeCommands` states, and the difference is a property of the manifest: an
|
|
1118
|
+
* `.xcodeproj` names the repository's own stack, where a `package.json` routinely appears in a
|
|
1119
|
+
* repository of another one. Handing the search on would pair an npm line with an Xcode repository
|
|
1120
|
+
* and leave {@link appleXcodeNote}'s explanation attached to a key some other family had filled —
|
|
1121
|
+
* so the placeholder stands and the note explains it.
|
|
1122
|
+
*/
|
|
1123
|
+
function appleCommands(context) {
|
|
1124
|
+
const manifest = findSwiftPackageManifest(context);
|
|
1125
|
+
if (manifest !== undefined) {
|
|
1126
|
+
const dir = manifestDirectory(manifest);
|
|
1127
|
+
const packagePath = dir === '.' ? '' : ` --package-path ${dir}`;
|
|
1128
|
+
const commands = {
|
|
1129
|
+
typecheck: `swift build${packagePath}`,
|
|
1130
|
+
test: `swift test${packagePath}`,
|
|
1131
|
+
depInstall: `swift package resolve${packagePath}`,
|
|
1132
|
+
};
|
|
1133
|
+
if (dir !== '.')
|
|
1134
|
+
noteAnchoredFamily(context, manifest, commands.typecheck, commands.test);
|
|
1135
|
+
return commands;
|
|
1136
|
+
}
|
|
1137
|
+
const project = findXcodeProject(context);
|
|
1138
|
+
if (project === undefined)
|
|
1139
|
+
return undefined;
|
|
1140
|
+
// `-workspace` when a workspace was found, `-project` otherwise: `xcodebuild` rejects the flag
|
|
1141
|
+
// that does not match the container it is handed.
|
|
1142
|
+
const containerFlag = project.endsWith(XCODE_WORKSPACE_SUFFIX) ? 'workspace' : 'project';
|
|
1143
|
+
const schemes = findSharedSchemes(context, project);
|
|
1144
|
+
const commands = {};
|
|
1145
|
+
if (schemes.length === 1) {
|
|
1146
|
+
commands.typecheck = `xcodebuild -${containerFlag} ${project} -scheme ${schemes[0]} build`;
|
|
1147
|
+
}
|
|
1148
|
+
context.note(appleXcodeNote(project, containerFlag, schemes));
|
|
1149
|
+
return commands;
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* What a Rust adoption's type-check key does that an adopter may not expect, and the softer line to
|
|
1153
|
+
* drop back to.
|
|
1154
|
+
*
|
|
1155
|
+
* A **note** rather than a warning, for the reason `DetectContext.note` states: a linting,
|
|
1156
|
+
* format-checking type check is what a correct Rust adoption looks like, not a fault of the
|
|
1157
|
+
* repository. It says the two things the line does beyond `cargo check` — lints fail rather than
|
|
1158
|
+
* print, and nothing the old line compiled goes uncompiled — and then the one-line way back, since a
|
|
1159
|
+
* crate adopted with existing lint or format debt would otherwise meet a red key on its first run.
|
|
1160
|
+
*/
|
|
1161
|
+
function cargoLintNote(typecheck) {
|
|
1162
|
+
return (`commands.typecheck runs \`${typecheck}\`: on this family the static-analysis key is the linter, and ` +
|
|
1163
|
+
"`-D warnings` is what makes clippy's lints fail the key rather than print. No type error escapes by " +
|
|
1164
|
+
'that route — `cargo clippy --all-targets` compiles exactly what `cargo check --all-targets` compiled. ' +
|
|
1165
|
+
'A crate carrying existing lint or format debt can set commands.typecheck to `cargo check --all-targets` ' +
|
|
1166
|
+
`in harness.config.json, ${WRAPPED_KEY_SECOND_STEP}`);
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Raw commands for a Rust crate, or `undefined` when no `Cargo.toml` was found.
|
|
1170
|
+
*
|
|
1171
|
+
* **`--all-targets` is on the type check deliberately.** A bare `cargo clippy`, like the bare
|
|
1172
|
+
* `cargo check` this key used to run, covers the library and binary targets only, so tests and
|
|
1173
|
+
* benches go unchecked and the cheap key passes on code the expensive one then fails to compile —
|
|
1174
|
+
* the one outcome a type check exists to prevent.
|
|
1175
|
+
*
|
|
1176
|
+
* **The linter *is* the static-analysis key here**, as it is for {@link bundlerCommands}, whose
|
|
1177
|
+
* `typecheck` is `bundle exec rubocop`; the difference is the gate — RuboCop is a gem the repository
|
|
1178
|
+
* has to declare, while clippy and rustfmt are components of the toolchain a `Cargo.toml` already
|
|
1179
|
+
* commits the adopter to, so there is no config file to condition the line on and none is needed.
|
|
1180
|
+
* **`-D warnings` rather than a bare `cargo clippy`**, because clippy exits zero on every lint it
|
|
1181
|
+
* reports: the softer line is this module header's step that passes its verification by running
|
|
1182
|
+
* something that never checked anything, reached by a different route. The one consequence for
|
|
1183
|
+
* callers is that the line is a compound ending in `--`, so the `"$@"` `templates/scripts/typecheck.sh`
|
|
1184
|
+
* appends lands among rustc's flags rather than reaching a filter — that wrapper names this family as
|
|
1185
|
+
* one whose forwarding is not a bare filter.
|
|
1186
|
+
*
|
|
1187
|
+
* **`depInstall` is `cargo fetch`**, which downloads the whole dependency graph once per worktree,
|
|
1188
|
+
* which is what that key is for and what the type check needs before it can run offline.
|
|
1189
|
+
* **`build` and `devServer` stay unset**: `commands.build`'s only consumer is `setup-worktree.sh`,
|
|
1190
|
+
* so a `cargo build` there would recompile what the type check already compiled in a key no agent
|
|
1191
|
+
* runs, and a crate has no dev server file existence can start.
|
|
1192
|
+
*
|
|
1193
|
+
* `--manifest-path <dir>/Cargo.toml` below the root is {@link pythonCommands}' rule for its reason:
|
|
1194
|
+
* every line runs from the repository root, so a bare `cargo` there reads the wrong manifest or none.
|
|
1195
|
+
* Both halves of the compound carry it — `cargo fmt` takes the same flag — since an anchor on one
|
|
1196
|
+
* half leaves the other reading the root manifest or failing outright.
|
|
1197
|
+
*/
|
|
1198
|
+
function cargoCommands(context) {
|
|
1199
|
+
const manifest = findCargoManifest(context);
|
|
1200
|
+
if (manifest === undefined)
|
|
1201
|
+
return undefined;
|
|
1202
|
+
const anchored = manifestDirectory(manifest) !== '.';
|
|
1203
|
+
const target = anchored ? ` --manifest-path ${manifest}` : '';
|
|
1204
|
+
// The format check runs first: it is the cheap half, and its failure is the mechanical one.
|
|
1205
|
+
const commands = {
|
|
1206
|
+
typecheck: `cargo fmt --check${target} && cargo clippy --all-targets${target} -- -D warnings`,
|
|
1207
|
+
test: `cargo test${target}`,
|
|
1208
|
+
depInstall: `cargo fetch${target}`,
|
|
1209
|
+
};
|
|
1210
|
+
context.note(cargoLintNote(commands.typecheck));
|
|
1211
|
+
if (anchored)
|
|
1212
|
+
noteAnchoredFamily(context, manifest, commands.typecheck, commands.test);
|
|
1213
|
+
return commands;
|
|
1214
|
+
}
|
|
1215
|
+
/** The RuboCop configuration file names, either of which selects the type-check line. */
|
|
1216
|
+
const RUBOCOP_CONFIGS = ['.rubocop.yml', '.rubocop.yaml'];
|
|
1217
|
+
/** The RSpec and Minitest directories, whose existence decides which test runner the line names. */
|
|
1218
|
+
const RSPEC_DIR = 'spec';
|
|
1219
|
+
const MINITEST_DIR = 'test';
|
|
1220
|
+
/** The file `rake` reads its tasks from — the precondition of the Minitest line, and its Ruby evidence. */
|
|
1221
|
+
const RAKEFILE = 'Rakefile';
|
|
1222
|
+
/**
|
|
1223
|
+
* Raw commands for a Bundler-managed Ruby repository, or `undefined` when there is no `Gemfile`.
|
|
1224
|
+
*
|
|
1225
|
+
* **`typecheck` is emitted only where the repository configures RuboCop, and is left unresolved
|
|
1226
|
+
* otherwise.** Ruby has no type checker in the general case — Sorbet and Steep are opt-in and each
|
|
1227
|
+
* needs its own configuration and a `srb`/`steep` gem — so there is no line that verifies anything
|
|
1228
|
+
* about an arbitrary Ruby repository. A `.rubocop.yml` is the repository's own declaration that it
|
|
1229
|
+
* has a static check to run, and where it is absent the key gets the placeholder and its warning,
|
|
1230
|
+
* which tells the adopter what to fill in rather than handing them a step that passes on anything.
|
|
1231
|
+
*
|
|
1232
|
+
* **`test` is decided by file and directory existence, not judgement**: `spec/` is RSpec's fixed
|
|
1233
|
+
* location, and Minitest's `test/` counts only where a `Rakefile` sits beside the manifest, since
|
|
1234
|
+
* `rake` reads its tasks from that file and `bundle exec rake test` aborts with `No Rakefile found`
|
|
1235
|
+
* without one. A repository evidencing neither leaves the key unresolved for the reason above.
|
|
1236
|
+
* RSpec is tried first because a repository carrying both directories is an RSpec suite with a
|
|
1237
|
+
* leftover `test/` more often than the reverse.
|
|
1238
|
+
*
|
|
1239
|
+
* That `Rakefile` precondition is load-bearing twice over, and the second time for the node gate
|
|
1240
|
+
* below: a `test/` directory is not Ruby evidence — it is `node:test`'s, Mocha's, Tape's and Ava's
|
|
1241
|
+
* conventional directory too — so on its own it must not resolve a verifier and thereby satisfy a
|
|
1242
|
+
* gate that exists to establish the stack is Ruby. The `Rakefile` is what makes it evidence, as
|
|
1243
|
+
* well as what makes the line runnable.
|
|
1244
|
+
*
|
|
1245
|
+
* **No `build` and no `devServer`.** `commands.build`'s only consumer is `setup-worktree.sh`, and
|
|
1246
|
+
* Ruby is interpreted — there is nothing to build — while a Rails server needs a port and a binding
|
|
1247
|
+
* to be chosen, which is judgement rather than something file existence answers; the unset key is
|
|
1248
|
+
* visible in the generated config, so nothing is hidden by leaving it out.
|
|
1249
|
+
*
|
|
1250
|
+
* Below the root each line is prefixed with `BUNDLE_GEMFILE=<dir>/Gemfile `, which is the anchoring
|
|
1251
|
+
* {@link pythonCommands} does with an argument: every line runs from the repository root, and
|
|
1252
|
+
* `bundle` has no `-C`/`--directory` flag, so the environment variable is the only spelling that
|
|
1253
|
+
* points it at the nested manifest. It is legitimate here because the line lands inside the
|
|
1254
|
+
* adopter's own wrapper script, where a shell prefix is an ordinary command line rather than a
|
|
1255
|
+
* change to anybody's environment — and the directory is passed to the runner as well, so the
|
|
1256
|
+
* runner reads the nested tree rather than the root — a path for `rubocop` and `rspec`, `rake`'s
|
|
1257
|
+
* own `-C` chdir flag for the Minitest line, since a `Rakefile` is found relative to the working
|
|
1258
|
+
* directory rather than named on the command line.
|
|
1259
|
+
*
|
|
1260
|
+
* The anchoring **note** is raised only where both required keys resolved, because the one sentence
|
|
1261
|
+
* {@link noteAnchoredFamily} writes names both: a repository that resolved only one gets the
|
|
1262
|
+
* placeholder warning on the other, which names the manifest roots that were searched.
|
|
1263
|
+
*
|
|
1264
|
+
* **It hands the search on where it would answer with an install and nothing else, in a repository
|
|
1265
|
+
* whose root manifest declares an npm script.** This is the family half of the
|
|
1266
|
+
* `ruby-bundler:gemfile` row's guard (`detect/signals.ts`, {@link declaresNodeScript}), and the row
|
|
1267
|
+
* guard alone does not reach it: this family is tried above `npm` whether or not that row matched,
|
|
1268
|
+
* and it used to return a truthy object unconditionally — so a React Native repository, whose root
|
|
1269
|
+
* `Gemfile` pins CocoaPods and fastlane rather than declaring a Ruby stack, stopped the search on
|
|
1270
|
+
* `bundle install` alone and left both required keys as placeholders with its own npm scripts
|
|
1271
|
+
* unread.
|
|
1272
|
+
*
|
|
1273
|
+
* The condition is narrow on both sides deliberately. `bundle install` is still the whole answer for
|
|
1274
|
+
* a Ruby repository that configures neither verifier, because {@link declaresNodeScript} is false
|
|
1275
|
+
* there and no other family is waiting to answer — the invariant that this declines only where
|
|
1276
|
+
* another derivation is ready. And a `Gemfile` repository that resolved **either** verifier keeps
|
|
1277
|
+
* every line it resolved, npm scripts beside it or not, which is what leaves a Rails application
|
|
1278
|
+
* with an asset-pipeline `package.json` on `bundle exec rspec` rather than on `npm run build`.
|
|
1279
|
+
* That second half holds only because every arm above resolves on a Ruby-specific file — a RuboCop
|
|
1280
|
+
* configuration, a `spec/` directory, a `Rakefile` beside `test/` — so a Node repository carrying a
|
|
1281
|
+
* root `Gemfile` for CocoaPods, fastlane or Jekyll resolves nothing here and the gate still reaches
|
|
1282
|
+
* it, whatever directories it shares with a Ruby tree.
|
|
1283
|
+
*/
|
|
1284
|
+
function bundlerCommands(context) {
|
|
1285
|
+
const manifest = findBundlerManifest(context);
|
|
1286
|
+
if (manifest === undefined)
|
|
1287
|
+
return undefined;
|
|
1288
|
+
const dir = manifestDirectory(manifest);
|
|
1289
|
+
const anchored = dir !== '.';
|
|
1290
|
+
const at = (name) => (anchored ? `${dir}/${name}` : name);
|
|
1291
|
+
const prefix = anchored ? `BUNDLE_GEMFILE=${manifest} ` : '';
|
|
1292
|
+
const commands = { depInstall: `${prefix}bundle install` };
|
|
1293
|
+
if (RUBOCOP_CONFIGS.some((name) => context.fileExists(at(name)))) {
|
|
1294
|
+
commands.typecheck = `${prefix}bundle exec rubocop${anchored ? ` ${dir}` : ''}`;
|
|
1295
|
+
}
|
|
1296
|
+
if (context.dirExists(at(RSPEC_DIR))) {
|
|
1297
|
+
commands.test = `${prefix}bundle exec rspec${anchored ? ` ${at(RSPEC_DIR)}` : ''}`;
|
|
1298
|
+
}
|
|
1299
|
+
else if (context.fileExists(at(RAKEFILE)) && context.dirExists(at(MINITEST_DIR))) {
|
|
1300
|
+
commands.test = `${prefix}bundle exec rake${anchored ? ` -C ${dir}` : ''} test`;
|
|
1301
|
+
}
|
|
1302
|
+
if (commands.typecheck === undefined && commands.test === undefined && declaresNodeScript(context))
|
|
1303
|
+
return undefined;
|
|
1304
|
+
if (anchored && commands.typecheck !== undefined && commands.test !== undefined) {
|
|
1305
|
+
noteAnchoredFamily(context, manifest, commands.typecheck, commands.test);
|
|
1306
|
+
}
|
|
1307
|
+
return commands;
|
|
1308
|
+
}
|
|
1309
|
+
/** The PHPStan configuration file names, either of which selects the type-check line. */
|
|
1310
|
+
const PHPSTAN_CONFIGS = ['phpstan.neon', 'phpstan.neon.dist'];
|
|
1311
|
+
/** The Psalm configuration file names — the second analyser, tried only where PHPStan configured none. */
|
|
1312
|
+
const PSALM_CONFIGS = ['psalm.xml', 'psalm.xml.dist'];
|
|
1313
|
+
/** The PHPUnit configuration file names, either of which selects the test line. */
|
|
1314
|
+
const PHPUNIT_CONFIGS = ['phpunit.xml', 'phpunit.xml.dist'];
|
|
1315
|
+
/**
|
|
1316
|
+
* Raw commands for a Composer-managed PHP repository, or `undefined` when there is no
|
|
1317
|
+
* `composer.json`.
|
|
1318
|
+
*
|
|
1319
|
+
* **Both verifiers are emitted only on the repository's own configuration file, and left unresolved
|
|
1320
|
+
* otherwise** — {@link bundlerCommands}' rule, for a sharper version of its reason. PHP has no
|
|
1321
|
+
* analyser every project has: PHPStan and Psalm are each opt-in, each installed as a dev dependency,
|
|
1322
|
+
* and each needs its own configuration file, so naming one the repository does not configure would
|
|
1323
|
+
* emit a line that fails on `vendor/bin` not existing rather than a check that verifies anything.
|
|
1324
|
+
* PHPUnit is dominant but equally optional, and the same holds. PHPStan is tried before Psalm
|
|
1325
|
+
* because a repository configuring both is a PHPStan project with a Psalm baseline more often than
|
|
1326
|
+
* the reverse, and first-match has to answer one.
|
|
1327
|
+
*
|
|
1328
|
+
* **`depInstall` is load-bearing here rather than a nicety.** Both lines above run out of
|
|
1329
|
+
* `vendor/bin`, which does not exist until `composer install` has run — so without this key every
|
|
1330
|
+
* command this family emits fails on a fresh worktree, where for an interpreted stack with global
|
|
1331
|
+
* tooling it would only be slower. `--no-interaction` because the line runs unattended inside a
|
|
1332
|
+
* wrapper script.
|
|
1333
|
+
*
|
|
1334
|
+
* **No `build` and no `devServer`.** `commands.build`'s only consumer is `setup-worktree.sh` and PHP
|
|
1335
|
+
* is interpreted, so there is nothing to build; `php artisan serve` needs a port and a host chosen,
|
|
1336
|
+
* which is judgement rather than something file existence answers.
|
|
1337
|
+
*
|
|
1338
|
+
* Below the root, `--working-dir <dir>` for composer and `-c <dir>/<config file>` for the analyser
|
|
1339
|
+
* and PHPUnit — {@link pythonCommands}' anchoring rule for its reason: every line runs from the
|
|
1340
|
+
* repository root, so a bare invocation there reads the wrong configuration or none. The anchoring
|
|
1341
|
+
* **note** is raised only where both required keys resolved, for {@link bundlerCommands}' reason.
|
|
1342
|
+
*/
|
|
1343
|
+
function composerCommands(context) {
|
|
1344
|
+
const manifest = findComposerManifest(context);
|
|
1345
|
+
if (manifest === undefined)
|
|
1346
|
+
return undefined;
|
|
1347
|
+
const dir = manifestDirectory(manifest);
|
|
1348
|
+
const anchored = dir !== '.';
|
|
1349
|
+
const at = (name) => (anchored ? `${dir}/${name}` : name);
|
|
1350
|
+
const found = (names) => names.find((name) => context.fileExists(at(name)));
|
|
1351
|
+
const config = (name) => (anchored ? ` -c ${at(name)}` : '');
|
|
1352
|
+
const bin = (name) => (anchored ? `${dir}/vendor/bin/${name}` : `vendor/bin/${name}`);
|
|
1353
|
+
const commands = {
|
|
1354
|
+
depInstall: `composer install --no-interaction${anchored ? ` --working-dir ${dir}` : ''}`,
|
|
1355
|
+
};
|
|
1356
|
+
const phpstan = found(PHPSTAN_CONFIGS);
|
|
1357
|
+
const psalm = phpstan === undefined ? found(PSALM_CONFIGS) : undefined;
|
|
1358
|
+
if (phpstan !== undefined) {
|
|
1359
|
+
commands.typecheck = `${bin('phpstan')} analyse --no-progress${config(phpstan)}`;
|
|
1360
|
+
}
|
|
1361
|
+
else if (psalm !== undefined) {
|
|
1362
|
+
commands.typecheck = `${bin('psalm')} --no-progress${config(psalm)}`;
|
|
1363
|
+
}
|
|
1364
|
+
const phpunit = found(PHPUNIT_CONFIGS);
|
|
1365
|
+
if (phpunit !== undefined)
|
|
1366
|
+
commands.test = `${bin('phpunit')}${config(phpunit)}`;
|
|
1367
|
+
if (anchored && commands.typecheck !== undefined && commands.test !== undefined) {
|
|
1368
|
+
noteAnchoredFamily(context, manifest, commands.typecheck, commands.test);
|
|
1369
|
+
}
|
|
1370
|
+
return commands;
|
|
1371
|
+
}
|
|
1372
|
+
/** The directory this family configures into — its own choice, not something the repository states. */
|
|
1373
|
+
const CMAKE_BUILD_DIR = 'build';
|
|
1374
|
+
/**
|
|
1375
|
+
* Raw commands for a CMake-configured C++ repository, or `undefined` when there is no
|
|
1376
|
+
* `CMakeLists.txt`.
|
|
1377
|
+
*
|
|
1378
|
+
* **`typecheck` is a compound line, and that is the point of it.** `ctest` runs the tests a build
|
|
1379
|
+
* tree already holds, so it has nothing to run until the project has been configured and built —
|
|
1380
|
+
* unlike every interpreted stack here, where the two keys are independent. So `typecheck` is
|
|
1381
|
+
* `cmake -S <dir> -B <build> && cmake --build <build>`: the configure step and the compile that is
|
|
1382
|
+
* the only static check a C++ project has, joined because the second is meaningless without the
|
|
1383
|
+
* first. The whole line lands inside `typecheck.sh`, where a compound is an ordinary command line
|
|
1384
|
+
* rather than two command keys — the same shape a code-generation precondition takes on the line
|
|
1385
|
+
* above an analyzer.
|
|
1386
|
+
*
|
|
1387
|
+
* **`test` therefore carries the same configure-and-build prefix**, composed from the same string
|
|
1388
|
+
* `typecheck` is given so the two cannot drift: `ctest --test-dir <build>` reads a build tree, and
|
|
1389
|
+
* nothing in the plugin corpus establishes an order between `commands.typecheck` and
|
|
1390
|
+
* `commands.test` — every site naming both names `<test_cmd>` first — so `test` has to stand on its
|
|
1391
|
+
* own. A worktree `setup-worktree.sh` provisioned carries no build tree either, since this family
|
|
1392
|
+
* emits no `build` key, so a bare `ctest` there fails on a missing directory before a test runs,
|
|
1393
|
+
* which reads as a broken checkout rather than as a step out of order. Configure and build are both
|
|
1394
|
+
* incremental, so the repeated pair costs near-nothing where `typecheck` has already run.
|
|
1395
|
+
*
|
|
1396
|
+
* **`build/` is this family's own choice, not the repository's**, and it is the one argument in
|
|
1397
|
+
* this module not read off a file that exists: no CMake project declares where its build tree
|
|
1398
|
+
* goes, so a name had to be picked, and `build/` is the one the ecosystem's own documentation and
|
|
1399
|
+
* default `.gitignore` entries use. Both keys are composed from the same value, so neither can come
|
|
1400
|
+
* to name a different tree than the other.
|
|
1401
|
+
*
|
|
1402
|
+
* **No `build` key**: the compile is already inside `typecheck`, and `commands.build`'s only
|
|
1403
|
+
* consumer is `setup-worktree.sh`, so a second compile there would repeat a multi-minute native
|
|
1404
|
+
* build in every worktree the flow cuts. **No `devServer`**: a C++ project has none file existence
|
|
1405
|
+
* could start. **No `depInstall`**: dependency acquisition in a CMake project is project-specific —
|
|
1406
|
+
* `FetchContent` at configure time, a git submodule, a system package manager, Conan or vcpkg — and
|
|
1407
|
+
* none of it is derivable from the manifest's existence, so any line here would be invented.
|
|
1408
|
+
*
|
|
1409
|
+
* Below the repository root every path carries `<dir>`, {@link pythonCommands}' anchoring rule for
|
|
1410
|
+
* its reason: each line runs from the repository root, so a bare `cmake` there configures the wrong
|
|
1411
|
+
* tree or none.
|
|
1412
|
+
*/
|
|
1413
|
+
function cmakeCommands(context) {
|
|
1414
|
+
const manifest = findCmakeManifest(context);
|
|
1415
|
+
if (manifest === undefined)
|
|
1416
|
+
return undefined;
|
|
1417
|
+
const dir = manifestDirectory(manifest);
|
|
1418
|
+
const build = dir === '.' ? CMAKE_BUILD_DIR : `${dir}/${CMAKE_BUILD_DIR}`;
|
|
1419
|
+
const configureAndBuild = `cmake -S ${dir} -B ${build} && cmake --build ${build}`;
|
|
1420
|
+
const commands = {
|
|
1421
|
+
typecheck: configureAndBuild,
|
|
1422
|
+
test: `${configureAndBuild} && ctest --test-dir ${build} --output-on-failure`,
|
|
1423
|
+
};
|
|
1424
|
+
if (dir !== '.')
|
|
1425
|
+
noteAnchoredFamily(context, manifest, commands.typecheck, commands.test);
|
|
1426
|
+
return commands;
|
|
1427
|
+
}
|
|
1428
|
+
/** Where {@link findAndroidModule} looks for an {@link ANDROID_MANIFEST}, repo-relative to a manifest root. */
|
|
1429
|
+
const ANDROID_MODULE_MANIFESTS = ANDROID_MODULE_DIRS.map((module) => module === '.' ? ANDROID_MANIFEST : `${module}/${ANDROID_MANIFEST}`);
|
|
1430
|
+
/** The manifest an Android family's note names: the file that resolved the module. */
|
|
1431
|
+
function joinModuleManifest(found) {
|
|
1432
|
+
return found.module === '.' ? ANDROID_MANIFEST : `${found.module}/${ANDROID_MANIFEST}`;
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* The manifests `init` reads, as the undetected-command warning has to enumerate them: one entry per
|
|
1436
|
+
* family, the Python family's three alternatives spelled out because any one of them is enough.
|
|
1437
|
+
*
|
|
1438
|
+
* Composed from the locators' own constants rather than restated, so a manifest name added to
|
|
1439
|
+
* detection cannot go missing from the sentence that says what was looked for.
|
|
1440
|
+
*
|
|
1441
|
+
* **Exported for the test that asserts the sentence**, which derives what it expects from this
|
|
1442
|
+
* constant — splitting on the `, ` that separates families, and asserting each family's entry whole
|
|
1443
|
+
* — instead of restating a list that every newly supported stack extends. Entries are asserted whole
|
|
1444
|
+
* because a family's entry may be a path (Android's), whose segments are not manifest names.
|
|
1445
|
+
*/
|
|
1446
|
+
export const READ_MANIFESTS = [
|
|
1447
|
+
PACKAGE_MANIFEST,
|
|
1448
|
+
PYTHON_MANIFESTS.join('/'),
|
|
1449
|
+
GO_MANIFEST,
|
|
1450
|
+
PUBSPEC_MANIFEST,
|
|
1451
|
+
// The one entry that is a path rather than a root-level name: an Android manifest is what tells a
|
|
1452
|
+
// module from a plain JVM one, and it is looked for under `ANDROID_MODULE_DIRS` rather than in a
|
|
1453
|
+
// manifest root directly, so the spelling here is the one `findAndroidModule` actually tests. Its
|
|
1454
|
+
// alternatives are separated by ` or ` where every other family's are separated by `/`, because a
|
|
1455
|
+
// `/` between two paths reads as one longer path — the same spelling the Android signal's own
|
|
1456
|
+
// `description` uses for this pair.
|
|
1457
|
+
ANDROID_MODULE_MANIFESTS.join(' or '),
|
|
1458
|
+
MAVEN_MANIFEST,
|
|
1459
|
+
GRADLE_MANIFESTS.join('/'),
|
|
1460
|
+
// Suffixes rather than names, spelled as a glob is: a .NET solution and project are named after
|
|
1461
|
+
// the product, so `*.sln` is what an adopter recognises where `MyApp.sln` would name nothing.
|
|
1462
|
+
DOTNET_SOLUTION_SUFFIXES.map((suffix) => `*${suffix}`).join('/'),
|
|
1463
|
+
DOTNET_PROJECT_SUFFIXES.map((suffix) => `*${suffix}`).join('/'),
|
|
1464
|
+
SWIFT_PACKAGE_MANIFEST,
|
|
1465
|
+
// Suffixes again, for the .NET names' reason: an Xcode container is named after the product.
|
|
1466
|
+
XCODE_PROJECT_SUFFIXES.map((suffix) => `*${suffix}`).join('/'),
|
|
1467
|
+
CARGO_MANIFEST,
|
|
1468
|
+
BUNDLER_MANIFEST,
|
|
1469
|
+
COMPOSER_MANIFEST,
|
|
1470
|
+
CMAKE_MANIFEST,
|
|
1471
|
+
].join(', ');
|
|
1472
|
+
/**
|
|
1473
|
+
* The manifest roots as a sentence names them: `.` is the repository root, never a bare dot.
|
|
1474
|
+
*
|
|
1475
|
+
* Exported so `init`'s command-source line spells the roots the way every message raised here does
|
|
1476
|
+
* (`commands/init.ts`, `commandSourceLine`), rather than growing a second vocabulary for one fact.
|
|
1477
|
+
*/
|
|
1478
|
+
export function searchedRoots(context) {
|
|
1479
|
+
return context.manifestRoots.map((root) => (root === '.' ? 'the repository root' : `\`${root}\``)).join(' or ');
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* What `<appDir>/package.json` declares for a script-derived key, or `undefined` when it declares
|
|
1483
|
+
* nothing for it — and `undefined` outright when the application is the repository.
|
|
1484
|
+
*
|
|
1485
|
+
* The candidate lists are {@link nodeCommands}' own, read from {@link NODE_SCRIPT_CANDIDATES} rather
|
|
1486
|
+
* than restated: this offers to paste exactly what detection would have derived had this manifest been
|
|
1487
|
+
* the root's, anchored with `--prefix` because commands are executed from the repository root. `npm
|
|
1488
|
+
* test` rather than `npm run test` for the same reason `nodeCommands` gives — it is the lifecycle
|
|
1489
|
+
* script's own invocation.
|
|
1490
|
+
*
|
|
1491
|
+
* **It takes any of the four keys, not only the required two**, because the optional pair is answered
|
|
1492
|
+
* with nothing at all where the required pair at least gets a placeholder — see
|
|
1493
|
+
* {@link undeclaredOptionalCommandNote}. Widening this reader widens no derivation: every caller is a
|
|
1494
|
+
* sentence, and `rootPackageJson`'s only-the-root rule still decides what `commands.*` holds.
|
|
1495
|
+
*/
|
|
1496
|
+
function nestedScriptFor(context, key) {
|
|
1497
|
+
if (context.appDir === '.')
|
|
1498
|
+
return undefined;
|
|
1499
|
+
const scripts = appScriptNames(context);
|
|
1500
|
+
const script = NODE_SCRIPT_CANDIDATES[key].find((name) => scripts.has(name));
|
|
1501
|
+
if (script === undefined)
|
|
1502
|
+
return undefined;
|
|
1503
|
+
return {
|
|
1504
|
+
manifest: `${context.appDir}/${PACKAGE_MANIFEST}`,
|
|
1505
|
+
script,
|
|
1506
|
+
command: `npm --prefix ${context.appDir} ${key === 'test' ? 'test' : `run ${script}`}`,
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* The second step for a key whose value is **inlined into a wrapper**, with the condition that re-run
|
|
1511
|
+
* carries — one spelling, because the wrapper-form mismatch the next `init` reports is the same path
|
|
1512
|
+
* (`generators/scripts.ts`, `wrappedKeyMismatchMessage`) and two wordings of it read as two remedies.
|
|
1513
|
+
*/
|
|
1514
|
+
const WRAPPED_KEY_SECOND_STEP = "then re-run init: it inlines that line into this key's wrapper script — wrappers are create-if-absent, " +
|
|
1515
|
+
'so one already on disk takes `init --force` (which backs it up to a `.bak` first) or deleting instead — ' +
|
|
1516
|
+
'and names the wrapper invocation to finish with';
|
|
1517
|
+
/**
|
|
1518
|
+
* What a {@link WEAK_TYPECHECK_SCRIPTS} selection means, and what to set instead.
|
|
1519
|
+
*
|
|
1520
|
+
* **One text for both channels**, because the fact does not change with the severity: the same
|
|
1521
|
+
* sentence is raised as a warning on a repository carrying {@link TYPED_REPOSITORY_EVIDENCE} and as a
|
|
1522
|
+
* note on one that does not, and only the channel differs. It reuses {@link WRAPPED_KEY_SECOND_STEP}
|
|
1523
|
+
* rather than restating it, so `init`, `config set` and `doctor` keep describing one remedy for a
|
|
1524
|
+
* wrapped key. It names no replacement command: deriving one from a `tsconfig.json` would invent a
|
|
1525
|
+
* line the manifest does not declare, against this module's own rule.
|
|
1526
|
+
*/
|
|
1527
|
+
function weakTypecheckMessage(script, command) {
|
|
1528
|
+
return (`commands.typecheck was derived from the \`${script}\` script and set to \`${command}\`: \`${script}\` is the last ` +
|
|
1529
|
+
'candidate for this key because it may not type-check at all — a lint run without type-aware rules exits zero on a type ' +
|
|
1530
|
+
'error, so the verification step passes having checked nothing. Set commands.typecheck to a command that type-checks this ' +
|
|
1531
|
+
`repository in harness.config.json, ${WRAPPED_KEY_SECOND_STEP}`);
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* What a pre-compiled dev-server line with no build script exposes, and the two ways out of it.
|
|
1535
|
+
*
|
|
1536
|
+
* A **note** rather than a warning: a repository whose start script needs no build — a static server,
|
|
1537
|
+
* an interpreted entry point — is not at fault, and file existence cannot tell that repository from
|
|
1538
|
+
* one whose `dist/` is simply missing (`DetectContext.note`). It names both remedies because they are
|
|
1539
|
+
* genuinely alternatives: declaring `build` makes {@link devServerPrestart} answer and the generated
|
|
1540
|
+
* wrapper run it before it launches, and pointing the key at a watch-mode script removes the exposure
|
|
1541
|
+
* instead of covering it.
|
|
1542
|
+
*/
|
|
1543
|
+
function unbuildableDevServerNote(command) {
|
|
1544
|
+
return (`commands.devServer was derived as \`${command}\`, which serves whatever was last compiled, and this manifest ` +
|
|
1545
|
+
'declares no `build` script to run before it: after an uncompiled edit the interactive test phase tests stale ' +
|
|
1546
|
+
'output, and on a fresh worktree where the build directory does not exist the server fails to start. Declare a ' +
|
|
1547
|
+
'`build` script — the generated dev-server wrapper then runs that line to completion before it launches — or ' +
|
|
1548
|
+
'point commands.devServer at a watch-mode script, which compiles as it serves');
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* The consequence of each unset optional key, named by the file that consumes it.
|
|
1552
|
+
*
|
|
1553
|
+
* `build` is one of the two keys that are never wrapped (this module's header), so its remedy ends at
|
|
1554
|
+
* the config value; `devServer` is wrapped, so its remedy is the same second step arm 1 of
|
|
1555
|
+
* {@link undetectedCommandWarning} names.
|
|
1556
|
+
*/
|
|
1557
|
+
const OPTIONAL_COMMAND_REMEDIES = {
|
|
1558
|
+
build: {
|
|
1559
|
+
cost: 'so `setup-worktree.sh` skips the build in every worktree the flow cuts',
|
|
1560
|
+
afterSetting: 'and nothing further: `build` is never wrapped, so that raw line is what runs',
|
|
1561
|
+
},
|
|
1562
|
+
devServer: {
|
|
1563
|
+
cost: 'so the interactive test phase has nothing to start the application with when `phases.qa` is on',
|
|
1564
|
+
afterSetting: WRAPPED_KEY_SECOND_STEP,
|
|
1565
|
+
},
|
|
1566
|
+
};
|
|
1567
|
+
/**
|
|
1568
|
+
* Why an optional command is absent from the generated config, where `<appDir>/package.json` declares
|
|
1569
|
+
* the script it would have come from.
|
|
1570
|
+
*
|
|
1571
|
+
* **This is the required keys' arm 1, for the two keys that get no placeholder.** An unresolved
|
|
1572
|
+
* `build` or `devServer` is simply not written — no key, no placeholder, and without this sentence no
|
|
1573
|
+
* line of output either — which is the silent gap the `depInstall` anchoring exists to remove, one key
|
|
1574
|
+
* over. It says the same three things arm 1 says (the manifest, the script it declares, the
|
|
1575
|
+
* root-anchored line to set) plus what the absence costs, because a key nothing warns about is a key
|
|
1576
|
+
* nobody sets.
|
|
1577
|
+
*
|
|
1578
|
+
* A **note** rather than a warning: an optional key is optional, and the repository is not at fault
|
|
1579
|
+
* for nesting its application (`DetectContext.note`).
|
|
1580
|
+
*/
|
|
1581
|
+
function undeclaredOptionalCommandNote(key, nested) {
|
|
1582
|
+
const keyPath = `commands.${key}`;
|
|
1583
|
+
const remedy = OPTIONAL_COMMAND_REMEDIES[key];
|
|
1584
|
+
return (`${keyPath} was not derived from \`${nested.manifest}\`, which declares a \`${nested.script}\` script: ` +
|
|
1585
|
+
`commands run from the repository root, where a nested script does not resolve — init left the key ` +
|
|
1586
|
+
`unset, ${remedy.cost}. Set ${keyPath} to \`${nested.command}\` in harness.config.json, ${remedy.afterSetting}`);
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* Why a required command was not detected, in four arms — **each of which has to be true of the
|
|
1590
|
+
* repository it is printed on**, which is the whole reason there is more than one.
|
|
1591
|
+
*
|
|
1592
|
+
* 1. **A nested manifest declares it.** `rootPackageJson`'s only-the-root rule is what left the key
|
|
1593
|
+
* unresolved, and the repository does hold the command; the true sentence names that manifest, its
|
|
1594
|
+
* script and the root-anchored line to paste. It then names the **second** step — a re-run, with the
|
|
1595
|
+
* condition that re-run carries: wrappers are `create-if-absent`, so a plain re-run inlines the line
|
|
1596
|
+
* only where the wrapper file is absent, which is the ordinary state of a key that held the
|
|
1597
|
+
* placeholder, and one left on disk by an earlier adoption takes `--force` or a delete. That is the
|
|
1598
|
+
* same pair `wrappedKeyMismatchMessage` names, so the remedy here and the wrapper-form mismatch the
|
|
1599
|
+
* next `init` reports are one path rather than two instructions that appear to contradict each other
|
|
1600
|
+
* (`generators/scripts.ts`).
|
|
1601
|
+
* 2. **A manifest `init` reads is present.** {@link commandFamilies} stops at the first family whose
|
|
1602
|
+
* derivation returns a command set, not at the first whose manifest exists, and a family that
|
|
1603
|
+
* resolves only some keys — `bundler` and `composer` derive a
|
|
1604
|
+
* `depInstall` alone, the Xcode arm a `typecheck` alone — leaves the rest unresolved beside a
|
|
1605
|
+
* manifest that could have answered them. Saying *no manifest `init` reads was found* is false
|
|
1606
|
+
* there, so this arm names the manifest that is there and the reason it did not answer instead.
|
|
1607
|
+
* It is asked **before** arm 3 as well as arm 4, because arm 3's sentence carries the same denial.
|
|
1608
|
+
* 3. **An unread manifest is present.** Saying *no manifest in this repository* is false on a
|
|
1609
|
+
* repository whose only manifest is a root `Makefile`; what is true is *no manifest `init` reads*.
|
|
1610
|
+
* Naming what is there costs one existence check.
|
|
1611
|
+
* 4. **None of them.** The plain case, still stated as what was looked for and where, rather than as a
|
|
1612
|
+
* claim about what the repository does not contain.
|
|
1613
|
+
*
|
|
1614
|
+
* Only the sentence differs: every arm has already written {@link placeholderCommand} into the key,
|
|
1615
|
+
* so the wrapper selection and the permission profile that follow from it are identical in all four.
|
|
1616
|
+
* Arm 1 is Node-only by construction — {@link findPythonManifest} and {@link findGoManifest} search
|
|
1617
|
+
* `manifestRoots`, so a nested Python or Go manifest is read and its commands are emitted.
|
|
1618
|
+
*/
|
|
1619
|
+
function undetectedCommandWarning(context, key) {
|
|
1620
|
+
const keyPath = `commands.${key}`;
|
|
1621
|
+
const nested = nestedScriptFor(context, key);
|
|
1622
|
+
if (nested !== undefined) {
|
|
1623
|
+
return (`${keyPath} was not derived from \`${nested.manifest}\`, which declares a \`${nested.script}\` script: ` +
|
|
1624
|
+
'commands run from the repository root, where a nested script does not resolve — init wrote a ' +
|
|
1625
|
+
`placeholder. Set ${keyPath} to \`${nested.command}\` in harness.config.json, ${WRAPPED_KEY_SECOND_STEP}`);
|
|
1626
|
+
}
|
|
1627
|
+
const remedy = `init wrote a placeholder; set ${keyPath} in harness.config.json before the first run`;
|
|
1628
|
+
const found = findAnyReadManifest(context);
|
|
1629
|
+
if (found !== undefined) {
|
|
1630
|
+
return (`${keyPath} could not be detected: \`${found}\` was found in ${searchedRoots(context)}, but the ` +
|
|
1631
|
+
'command family that answered for this repository derives no line for this key — the first family ' +
|
|
1632
|
+
`whose derivation returns a command set supplies every key it can and the search stops there. ${remedy}`);
|
|
1633
|
+
}
|
|
1634
|
+
const lookedFor = `no manifest init reads (${READ_MANIFESTS}) was found in ${searchedRoots(context)}`;
|
|
1635
|
+
const unread = findUnreadManifest(context);
|
|
1636
|
+
return unread === undefined
|
|
1637
|
+
? `${keyPath} could not be detected: ${lookedFor} — ${remedy}`
|
|
1638
|
+
: `${keyPath} could not be detected: ${lookedFor}; \`${unread}\` is present but is not one of them — ${remedy}`;
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* The command families, in the order they are tried.
|
|
1642
|
+
*
|
|
1643
|
+
* **The first family whose derivation returns a command set supplies everything it can, and the
|
|
1644
|
+
* search stops there** — the gate is a resolved command set rather than a manifest that exists
|
|
1645
|
+
* ({@link fallbackAnchorReason}), so a family whose manifest is present may still decline and hand
|
|
1646
|
+
* the search on ({@link PresetProfile.declinedFamilies} is the only record of that).
|
|
1647
|
+
* Falling through to a second family for a key the first could not resolve would mean
|
|
1648
|
+
* pairing, say, an npm type-check with a `pytest` run in a repository that has both manifests —
|
|
1649
|
+
* a combination nothing in the repository asked for. An unresolved key stays unresolved and says
|
|
1650
|
+
* so.
|
|
1651
|
+
*
|
|
1652
|
+
* **`npm` is last**, and the reason is a property of the manifest rather than a preference between
|
|
1653
|
+
* stacks: `package.json` is the manifest here that most often appears in a repository of another
|
|
1654
|
+
* stack — a documentation site, a lint-or-format tooling manifest, a bundled front end — while
|
|
1655
|
+
* `go.mod`, `pyproject.toml` and most manifests a later family adds name the repository's own stack.
|
|
1656
|
+
* Ordering it last costs the npm repositories nothing, because a repository whose stack really is
|
|
1657
|
+
* Node carries no `go.mod` or `pyproject.toml` for an earlier family to answer with. The two
|
|
1658
|
+
* manifests that share `package.json`'s property are handled by a gate on their own family instead —
|
|
1659
|
+
* the two paragraphs below.
|
|
1660
|
+
*
|
|
1661
|
+
* **`bundler` stays above `npm` and carries the guard on its own gate instead.** A `Gemfile` has
|
|
1662
|
+
* `package.json`'s "appears in a repository of another stack" property — the React Native template
|
|
1663
|
+
* ships one at the repository root — so the paragraph above would move it below `npm` as well, and
|
|
1664
|
+
* that is deliberately not what was done: a Rails application's preset comes from the
|
|
1665
|
+
* directory-shaped `api-service:route-directory` row, which hoists nothing, and Rails routinely
|
|
1666
|
+
* carries an asset-pipeline `package.json`, so the move would answer `npm run build` where `bundle
|
|
1667
|
+
* exec rspec` is the right line. `bundlerCommands` declines instead, and only where it would answer
|
|
1668
|
+
* with `bundle install` and nothing else in a repository whose root manifest declares a script —
|
|
1669
|
+
* which is the React Native population and not the Rails one.
|
|
1670
|
+
*
|
|
1671
|
+
* **`cmake` is the one family below `npm`, on npm's own argument taken one step further.** A
|
|
1672
|
+
* `CMakeLists.txt` routinely describes a *sub-component* of a Node repository — a `node-gyp` /
|
|
1673
|
+
* `cmake-js` addon — so between those two manifests the npm one is the more specific claim, and
|
|
1674
|
+
* `nodeCommands`' gate is what tells them apart: it answers only where the root manifest declares a
|
|
1675
|
+
* script, which an addon's does and a C++ tree's `package.json` (if it has one at all) does not.
|
|
1676
|
+
* Nothing a genuine C++ adopter gets moves, because `SIGNAL_COMMAND_FAMILY` hoists `cmake` whenever
|
|
1677
|
+
* the `cmake-cpp:cmake-lists` row decided the preset — and that row declines the addon for the same
|
|
1678
|
+
* reason this order does (`detect/signals.ts`, `declaresNodeScript`).
|
|
1679
|
+
*
|
|
1680
|
+
* A new family's row goes **immediately above the `npm` row**, and its signal id(s) go into
|
|
1681
|
+
* {@link SIGNAL_COMMAND_FAMILY}.
|
|
1682
|
+
*/
|
|
1683
|
+
const COMMAND_FAMILIES = [
|
|
1684
|
+
{ id: 'python', resolve: pythonCommands, manifest: findPythonManifest },
|
|
1685
|
+
{ id: 'go', resolve: goCommands, manifest: findGoManifest },
|
|
1686
|
+
{ id: 'dart', resolve: dartCommands, manifest: findFlutterManifest },
|
|
1687
|
+
{
|
|
1688
|
+
id: 'android-gradle',
|
|
1689
|
+
resolve: androidGradleCommands,
|
|
1690
|
+
// The module manifest rather than the Gradle project directory: the spelling
|
|
1691
|
+
// {@link noteAnchoredFamily} already prints for this family, and the file that resolved it.
|
|
1692
|
+
manifest: (context) => {
|
|
1693
|
+
const found = findAndroidModule(context);
|
|
1694
|
+
return found === undefined ? undefined : joinModuleManifest(found);
|
|
1695
|
+
},
|
|
1696
|
+
},
|
|
1697
|
+
// Both JVM families sit **below** `android-gradle`, which the Gradle one would otherwise answer
|
|
1698
|
+
// for: an Android project carries the Gradle manifests this family gates on, and `classes` is not
|
|
1699
|
+
// the task that compiles an Android module.
|
|
1700
|
+
{ id: 'maven', resolve: mavenCommands, manifest: findMavenManifest },
|
|
1701
|
+
{ id: 'gradle-jvm', resolve: gradleJvmCommands, manifest: findGradleManifest },
|
|
1702
|
+
{ id: 'dotnet', resolve: dotnetCommands, manifest: findDotnetProject },
|
|
1703
|
+
// Swift package first, Xcode container second — `appleCommands`' own order.
|
|
1704
|
+
{ id: 'apple-native', resolve: appleCommands, manifest: (c) => findSwiftPackageManifest(c) ?? findXcodeProject(c) },
|
|
1705
|
+
{ id: 'cargo', resolve: cargoCommands, manifest: findCargoManifest },
|
|
1706
|
+
{ id: 'bundler', resolve: bundlerCommands, manifest: findBundlerManifest },
|
|
1707
|
+
{ id: 'composer', resolve: composerCommands, manifest: findComposerManifest },
|
|
1708
|
+
// The **root** manifest only, which is `nodeCommands`' own gate: a `package.json` under a manifest
|
|
1709
|
+
// root below the repository root is not what this family reads.
|
|
1710
|
+
{
|
|
1711
|
+
id: 'npm',
|
|
1712
|
+
resolve: nodeCommands,
|
|
1713
|
+
manifest: (context) => (context.rootPackageJson() === undefined ? undefined : PACKAGE_MANIFEST),
|
|
1714
|
+
},
|
|
1715
|
+
{ id: 'cmake', resolve: cmakeCommands, manifest: findCmakeManifest },
|
|
1716
|
+
];
|
|
1717
|
+
/**
|
|
1718
|
+
* {@link COMMAND_FAMILIES}' ids, in evaluation order, exported so the document that describes the
|
|
1719
|
+
* order can be checked against it rather than paraphrasing it (`docs/cli.md` §4, closure case in
|
|
1720
|
+
* `test/stack-presets.test.mjs`). A family whose paragraph drifts out of order fails there.
|
|
1721
|
+
*/
|
|
1722
|
+
export const COMMAND_FAMILY_IDS = COMMAND_FAMILIES.map((family) => family.id);
|
|
1723
|
+
/**
|
|
1724
|
+
* The family a matched signal row hoists to the front, by `Signal.id`.
|
|
1725
|
+
*
|
|
1726
|
+
* The generalisation of the `python-package` special case this replaced: where the row that decided
|
|
1727
|
+
* the preset is a **manifest** row, that manifest is the repository's own declaration of its stack,
|
|
1728
|
+
* so its family answers first whatever the standing order is. A row with no entry here — every
|
|
1729
|
+
* directory-shaped row, and `FORCED_SIGNAL_ID`, which evaluated no row at all — hoists nothing and
|
|
1730
|
+
* takes {@link COMMAND_FAMILIES} as it stands.
|
|
1731
|
+
*/
|
|
1732
|
+
const SIGNAL_COMMAND_FAMILY = {
|
|
1733
|
+
'monorepo:workspaces-key': 'npm',
|
|
1734
|
+
'monorepo:workspace-manifest': 'npm',
|
|
1735
|
+
'python-package:packaging-manifest': 'python',
|
|
1736
|
+
'api-service:go-module': 'go',
|
|
1737
|
+
'flutter:pubspec-manifest': 'dart',
|
|
1738
|
+
'android-gradle:android-manifest': 'android-gradle',
|
|
1739
|
+
'jvm:maven-manifest': 'maven',
|
|
1740
|
+
'jvm:gradle-manifest': 'gradle-jvm',
|
|
1741
|
+
'dotnet:solution-or-project': 'dotnet',
|
|
1742
|
+
'apple-native:swift-package-or-xcode-project': 'apple-native',
|
|
1743
|
+
'rust-cargo:cargo-manifest': 'cargo',
|
|
1744
|
+
'ruby-bundler:gemfile': 'bundler',
|
|
1745
|
+
'php-composer:composer-manifest': 'composer',
|
|
1746
|
+
'cmake-cpp:cmake-lists': 'cmake',
|
|
1747
|
+
};
|
|
1748
|
+
/** {@link COMMAND_FAMILIES} with the family {@link SIGNAL_COMMAND_FAMILY} names for this run first. */
|
|
1749
|
+
function commandFamilies(detection) {
|
|
1750
|
+
const hoisted = COMMAND_FAMILIES.find((family) => family.id === SIGNAL_COMMAND_FAMILY[detection.matchedSignal]);
|
|
1751
|
+
return hoisted === undefined ? COMMAND_FAMILIES : [hoisted, ...COMMAND_FAMILIES.filter((f) => f !== hoisted)];
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Why {@link buildPreset}'s fallback anchors the install below the root — one sentence per population
|
|
1755
|
+
* it is reached on, because {@link anchoredDepInstall} prints the clause beside the line it returns
|
|
1756
|
+
* and the two must not come to disagree.
|
|
1757
|
+
*
|
|
1758
|
+
* Two populations since the family gate became a resolved script rather than a manifest that exists:
|
|
1759
|
+
* the repository with no root manifest at all, and the repository whose root manifest declares none
|
|
1760
|
+
* of the script names the four command keys accept.
|
|
1761
|
+
*/
|
|
1762
|
+
function fallbackAnchorReason(context) {
|
|
1763
|
+
return context.rootPackageJson() === undefined
|
|
1764
|
+
? 'this repository declares no npm manifest at its root to install from'
|
|
1765
|
+
: 'the root manifest declares no script this harness runs, so the install follows the manifest that does';
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* How every reporting string names where the command lines came from — the family that answered and
|
|
1769
|
+
* **its own** manifest, or the family alone when its locator found none.
|
|
1770
|
+
*
|
|
1771
|
+
* One composer, because the fact is one fact: two spellings of it would let one run's output name
|
|
1772
|
+
* the same source two ways. Its only caller is {@link commandSourceClaim}, which wraps it in the
|
|
1773
|
+
* subject clause the three reporting strings share; no caller pairs a family with a manifest itself,
|
|
1774
|
+
* since the pair is {@link PresetProfile.commandManifest}'s, recorded from the winning family's own
|
|
1775
|
+
* locator. Not exported: {@link commandSourceClaim} is the module's one door onto this sentence, and
|
|
1776
|
+
* a second door is how the source and what it supplied come to be reported independently again.
|
|
1777
|
+
*/
|
|
1778
|
+
function commandSource(family, manifest) {
|
|
1779
|
+
return manifest === undefined
|
|
1780
|
+
? `the \`${family}\` command family`
|
|
1781
|
+
: `the \`${family}\` command family, which read \`${manifest}\``;
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* Which of the two required keys the winning family actually supplied, and where they came from —
|
|
1785
|
+
* the subject clause all three reporting strings open on ({@link familyTiebreakNote},
|
|
1786
|
+
* {@link splitDetectionNote}, `commands/init.ts`'s `commandSourceLine`).
|
|
1787
|
+
*
|
|
1788
|
+
* **A family having answered is not both keys having resolved.** {@link buildPreset}'s loop stops at
|
|
1789
|
+
* the first family whose `resolve` returns anything that is not `undefined`, and the populations
|
|
1790
|
+
* where that set is partial or empty are this module's own documented ones
|
|
1791
|
+
* ({@link undetectedCommandWarning}): `composer` and `bundler` derive a `depInstall` alone, the
|
|
1792
|
+
* Xcode arm a `typecheck` alone or — with no single shared scheme — an empty object, and `npm`
|
|
1793
|
+
* answers off a `build` script with neither verifier. Naming both keys on that gate printed a
|
|
1794
|
+
* sentence the same run's warnings contradicted, so the subject is built from
|
|
1795
|
+
* {@link PresetProfile.resolvedRequired} instead.
|
|
1796
|
+
*
|
|
1797
|
+
* One composer for all three callers, for {@link commandSource}'s reason: two spellings would let
|
|
1798
|
+
* one run's output make two different claims about one fact.
|
|
1799
|
+
*
|
|
1800
|
+
* @param claim.resolvedRequired the keys the winner resolved, in {@link REQUIRED_COMMAND_KEYS}
|
|
1801
|
+
* order — never re-derived by a caller.
|
|
1802
|
+
* @param verb `detection-resolved` is the kept-re-run spelling: that run wrote no key, so it reports
|
|
1803
|
+
* what detection resolved rather than what a key holds (`commands/init.ts`, `commandSourceLine`).
|
|
1804
|
+
*/
|
|
1805
|
+
export function commandSourceClaim(claim, verb = 'came-from') {
|
|
1806
|
+
const source = commandSource(claim.family, claim.manifest);
|
|
1807
|
+
const resolved = claim.resolvedRequired;
|
|
1808
|
+
// Neither key: no source is claimed for the required pair at all, because there is none to claim.
|
|
1809
|
+
// Both keys carry `undetectedCommandWarning`, which holds the remedy.
|
|
1810
|
+
if (resolved.length === 0) {
|
|
1811
|
+
return `neither commands.typecheck nor commands.test was derived by ${source}`;
|
|
1812
|
+
}
|
|
1813
|
+
const named = resolved.map((key) => `commands.${key}`).join(' and ');
|
|
1814
|
+
const head = verb === 'came-from' ? `${named} came from ${source}` : `stack detection resolved ${named} from ${source}`;
|
|
1815
|
+
if (resolved.length === REQUIRED_COMMAND_KEYS.length)
|
|
1816
|
+
return head;
|
|
1817
|
+
const missing = REQUIRED_COMMAND_KEYS.filter((key) => !resolved.includes(key));
|
|
1818
|
+
// The placeholder is what a written key holds; a kept re-run wrote none, so it says only what the
|
|
1819
|
+
// family did — the same rule that chose the verb.
|
|
1820
|
+
const held = verb === 'came-from' ? ' and carries a placeholder' : '';
|
|
1821
|
+
return `${head}; ${missing.map((key) => `commands.${key}`).join(' and ')} was not derived by it${held}`;
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* Which manifests collided at this repository's roots, which family answered and what became of the
|
|
1825
|
+
* families above it — the choice F66 measured as leaving no trace in the run's output.
|
|
1826
|
+
*
|
|
1827
|
+
* **It states four things and no fifth, and three tempting sentences are false here.** The winner is
|
|
1828
|
+
* not the first family whose *manifest* is present ({@link fallbackAnchorReason}: the gate is a
|
|
1829
|
+
* resolved command set), a listed manifest was not necessarily left unread, and "nothing else was
|
|
1830
|
+
* consulted" is false on the React Native shape this note exists for — `bundlerCommands` reads the
|
|
1831
|
+
* `Gemfile`, builds `bundle install` and yields on {@link declaresNodeScript}. What is true about a
|
|
1832
|
+
* non-winning family is {@link PresetProfile.declinedFamilies}, so that is what is said.
|
|
1833
|
+
*
|
|
1834
|
+
* A **note** rather than a warning: the winning row was correct on every fixture measured, and a
|
|
1835
|
+
* warning that fires on every correct multi-manifest repository is how a warnings block stops being
|
|
1836
|
+
* read (`DetectContext.note`). Its remedy is {@link undetectedCommandWarning}'s shape — set the key
|
|
1837
|
+
* by hand and re-run — because taking a different lane is a configuration edit rather than a fault
|
|
1838
|
+
* to be fixed, and both keys here are wrapped ({@link WRAPPED_KEY_SECOND_STEP}).
|
|
1839
|
+
*/
|
|
1840
|
+
function familyTiebreakNote(profile) {
|
|
1841
|
+
const others = profile.readManifests.filter((entry) => entry !== profile.manifest);
|
|
1842
|
+
// "these lines" has no referent where the winner supplied neither required key, which is a
|
|
1843
|
+
// reachable population ({@link commandSourceClaim}) — so that arm says what is true of the others
|
|
1844
|
+
// without naming lines nobody got.
|
|
1845
|
+
const supplied = profile.resolvedRequired.length === 0 ? 'did not supply them either.' : 'did not supply these lines.';
|
|
1846
|
+
const present = others.length === 0
|
|
1847
|
+
? ''
|
|
1848
|
+
: ` ${others.map((entry) => `\`${entry}\``).join(', ')} ${others.length === 1 ? 'is' : 'are'} present too and ` +
|
|
1849
|
+
supplied;
|
|
1850
|
+
const declined = profile.declinedFamilies.length === 0
|
|
1851
|
+
? ''
|
|
1852
|
+
: ` ${profile.declinedFamilies.map((id) => `\`${id}\``).join(', ')} ` +
|
|
1853
|
+
`${profile.declinedFamilies.length === 1 ? 'was tried, read its manifest' : 'were tried, read their manifests'} ` +
|
|
1854
|
+
'and declined.';
|
|
1855
|
+
return (`${commandSourceClaim(profile)}, and this ` +
|
|
1856
|
+
`repository carries more than one manifest init reads.${present}${declined} Families are tried in a fixed order ` +
|
|
1857
|
+
`(${COMMAND_FAMILY_IDS.join(', ')}), with the family this run's matched detection signal names hoisted to the ` +
|
|
1858
|
+
'front, and the first family whose derivation returns a command set supplies every key it can — the search stops ' +
|
|
1859
|
+
"there. To put this repository on another family's line, set the key you want to change — " +
|
|
1860
|
+
`commands.typecheck or commands.test — in harness.config.json, ${WRAPPED_KEY_SECOND_STEP}`);
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* That `unrecognised layout` is a verdict on the **layer** half alone, where the command half
|
|
1864
|
+
* resolved — the second thing F66 measured as silent.
|
|
1865
|
+
*
|
|
1866
|
+
* `FLAT_FALLBACK_WARNING` (`detect/signals.ts`) is raised before any family has run and is left
|
|
1867
|
+
* exactly as it is: it belongs to detection and knows nothing about the command half. This note is
|
|
1868
|
+
* the half that does, so it narrows the scope of that warning instead of restating it. It is raised
|
|
1869
|
+
* only where **a required key resolved** — where none did, both required keys already carry
|
|
1870
|
+
* {@link undetectedCommandWarning}, and a second line saying the same thing is duplication. That is
|
|
1871
|
+
* the same gate as before on the same reasoning; what changed is that a family having answered no
|
|
1872
|
+
* longer stands in for a key having resolved ({@link commandSourceClaim}), so the "did resolve"
|
|
1873
|
+
* clause is now gated on what it asserts. Where exactly one key resolved the clause is true of that
|
|
1874
|
+
* key and the composer says so about the other.
|
|
1875
|
+
*/
|
|
1876
|
+
function splitDetectionNote(claim) {
|
|
1877
|
+
return ('no layout signal matched this repository, so the `flat` fallback supplied the layer profile and ' +
|
|
1878
|
+
`\`/harness-analyze\` is what refines it — the command half did resolve: ${commandSourceClaim(claim)}. ` +
|
|
1879
|
+
'The unrecognised-layout warning is about the layer profile ' +
|
|
1880
|
+
'alone, not about the command lines this run wrote');
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* Turn a detection result into the layers and raw commands `init` writes.
|
|
1884
|
+
*
|
|
1885
|
+
* Takes the {@link DetectionResult} rather than a preset name so it reuses the probe detection
|
|
1886
|
+
* already built: the repository is read once per run and a warning cannot be reported twice.
|
|
1887
|
+
*/
|
|
1888
|
+
export function buildPreset(detection) {
|
|
1889
|
+
const { preset, context } = detection;
|
|
1890
|
+
const sourceRows = presetSourceLayers(preset, context);
|
|
1891
|
+
const layers = presetLayers(sourceRows, preset, context);
|
|
1892
|
+
// Gated on the **source** rows, not on `layers`: a `tests` row is spliced in whenever the
|
|
1893
|
+
// toolchain's conventional test root exists, and gating on the spliced length would silence this
|
|
1894
|
+
// warning for a repository whose source root did not resolve but whose test root did.
|
|
1895
|
+
// `flat` is excluded because its own fallback warning already says to run `/harness-analyze`,
|
|
1896
|
+
// and the same advice twice in one summary reads as a bug rather than as emphasis.
|
|
1897
|
+
//
|
|
1898
|
+
// The trailing clause names what `doctor` will say about the same repository, gated on the set
|
|
1899
|
+
// `doctor` gates its arms on rather than on this warning's own population, which is far wider:
|
|
1900
|
+
// only `LAYERLESS_BY_DESIGN_PRESETS` reaches `LAYER_PROFILE_CHECK`'s by-design **pass**; every
|
|
1901
|
+
// other preset here resolved no source root and takes that check's **warn** arm, so blessing it
|
|
1902
|
+
// would be false. Neither clause claims the other command's grade for itself.
|
|
1903
|
+
if (sourceRows.length === 1 && preset !== 'flat') {
|
|
1904
|
+
const disposition = LAYERLESS_BY_DESIGN_PRESETS.has(preset)
|
|
1905
|
+
? '`doctor` grades this a decision rather than drift, and it does not grade `layer-drift` while the profile carries no row below the root, so the directories under it are not enumerated anywhere until a profile is proposed'
|
|
1906
|
+
: "`doctor` keeps warning about it as an unresolved source root until a profile is recorded, since nothing else repeats this line";
|
|
1907
|
+
context.warn(`the \`${preset}\` preset gives this repository the \`general\` layer only — per-package or per-module layers are \`/harness-analyze\`'s to propose. ${disposition}`);
|
|
1908
|
+
}
|
|
1909
|
+
// One `resolve` call per iteration and the `break` intact: a second call on an already-evaluated
|
|
1910
|
+
// family would re-emit its own notes and warnings, and evaluating a family below the winner would
|
|
1911
|
+
// put a note about a family that never answered into the run's output. The `manifest` calls beside
|
|
1912
|
+
// it only locate.
|
|
1913
|
+
const resolved = {};
|
|
1914
|
+
let commandFamily;
|
|
1915
|
+
let commandManifest;
|
|
1916
|
+
const declinedFamilies = [];
|
|
1917
|
+
for (const family of commandFamilies(detection)) {
|
|
1918
|
+
const commands = family.resolve(context);
|
|
1919
|
+
if (commands !== undefined) {
|
|
1920
|
+
Object.assign(resolved, commands);
|
|
1921
|
+
commandFamily = family.id;
|
|
1922
|
+
commandManifest = family.manifest(context);
|
|
1923
|
+
break;
|
|
1924
|
+
}
|
|
1925
|
+
if (family.manifest(context) !== undefined)
|
|
1926
|
+
declinedFamilies.push(family.id);
|
|
1927
|
+
}
|
|
1928
|
+
// The dependency install is a whole command line rather than a script name resolved against the
|
|
1929
|
+
// manifest the runner reads, so it is derivable from a nested manifest even where `nodeCommands`'
|
|
1930
|
+
// root-manifest gate never opened — the repository whose only npm manifest is
|
|
1931
|
+
// `<appDir>/package.json`, where no npm family answers and the script-derived keys are placeholders.
|
|
1932
|
+
// Leaving it unset there is the silent no-install the anchoring exists to remove:
|
|
1933
|
+
// `setup-worktree.sh` installs nothing into every worktree the flow cuts, and the commands the run
|
|
1934
|
+
// has just told the adopter to configure then fail for want of `node_modules`.
|
|
1935
|
+
//
|
|
1936
|
+
// **The root install site is included, deliberately.** `depInstall` is a whole command line rather
|
|
1937
|
+
// than a verification key, it is already derived independently of whichever family won, and it is
|
|
1938
|
+
// one of the two keys that are never wrapped and never allow-listed (this module's header) — so
|
|
1939
|
+
// emitting it beside a non-npm family's `typecheck` and `test` is not the family-mixing
|
|
1940
|
+
// {@link commandFamilies} forbids. Its price is stated rather than hidden: every repository whose
|
|
1941
|
+
// keys come from a non-npm family while a root `package.json` sits beside them now gains a root npm
|
|
1942
|
+
// install, which `setup-worktree.sh` runs in every worktree the flow cuts.
|
|
1943
|
+
//
|
|
1944
|
+
// **The workspace carve-out applies here too**, which it did not have to before: this fallback used
|
|
1945
|
+
// to be reachable only where the root declared no manifest at all, so there was no root install to
|
|
1946
|
+
// keep. `nodeCommands`' gate is now a resolved *script*, so the fallback is reached **with a root
|
|
1947
|
+
// manifest present** whenever that manifest declares none of the four keys' script names — and a
|
|
1948
|
+
// workspaces root with a scripts-less `package.json` under `--app-dir <member>` would otherwise get
|
|
1949
|
+
// `npm --prefix <member> ci`, the exact failure the carve-out exists to prevent, since a workspace
|
|
1950
|
+
// keeps its one lockfile at the root.
|
|
1951
|
+
if (resolved.depInstall === undefined) {
|
|
1952
|
+
const site = findNodeInstallSite(context);
|
|
1953
|
+
if (site !== undefined) {
|
|
1954
|
+
resolved.depInstall =
|
|
1955
|
+
site.dir !== '.' && !isWorkspaceRoot(context)
|
|
1956
|
+
? anchoredDepInstall(context, site, fallbackAnchorReason(context))
|
|
1957
|
+
: // The lockfile is the whole difference, exactly as in `nodeCommands`' root arm.
|
|
1958
|
+
context.fileExists(PACKAGE_LOCKFILE)
|
|
1959
|
+
? 'npm ci'
|
|
1960
|
+
: 'npm install';
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
// Read **before** the placeholder loop below, which is the only window in which it is readable:
|
|
1964
|
+
// that loop fills every unresolved required key, after which `resolved` no longer distinguishes a
|
|
1965
|
+
// derived line from a placeholder.
|
|
1966
|
+
const resolvedRequired = REQUIRED_COMMAND_KEYS.filter((key) => resolved[key] !== undefined);
|
|
1967
|
+
for (const key of REQUIRED_COMMAND_KEYS) {
|
|
1968
|
+
if (resolved[key] !== undefined)
|
|
1969
|
+
continue;
|
|
1970
|
+
resolved[key] = placeholderCommand(key);
|
|
1971
|
+
context.warn(undetectedCommandWarning(context, key));
|
|
1972
|
+
}
|
|
1973
|
+
// The optional pair gets no placeholder and therefore no warning, so an unresolved one leaves no
|
|
1974
|
+
// trace at all — the same silent gap the `depInstall` anchoring above exists to remove, on the two
|
|
1975
|
+
// keys `setup-worktree.sh` and the interactive test phase read. Reported only where
|
|
1976
|
+
// `<appDir>/package.json` declares the script:
|
|
1977
|
+
// a repository that has no build step is ordinary, and a note on every such run is how a notes block
|
|
1978
|
+
// stops being read.
|
|
1979
|
+
for (const key of OPTIONAL_COMMAND_KEYS) {
|
|
1980
|
+
if (resolved[key] !== undefined)
|
|
1981
|
+
continue;
|
|
1982
|
+
const nested = nestedScriptFor(context, key);
|
|
1983
|
+
if (nested !== undefined)
|
|
1984
|
+
context.note(undeclaredOptionalCommandNote(key, nested));
|
|
1985
|
+
}
|
|
1986
|
+
// One call, and above both notes below rather than beside the return literal: this locator raises
|
|
1987
|
+
// neither a warning nor a note, so reading it here keeps that independent of statement order.
|
|
1988
|
+
const readManifests = findReadManifests(context);
|
|
1989
|
+
// The two reporting lines, last of everything this function raises — the module reports what it
|
|
1990
|
+
// did, then what an adopter should know about it. Both are gated on a family having answered:
|
|
1991
|
+
// where none did, both required keys already carry `undetectedCommandWarning`. Neither derives a
|
|
1992
|
+
// fact — each reads the values recorded above and says what they are, `resolvedRequired` included,
|
|
1993
|
+
// so neither can claim a key the winner did not supply.
|
|
1994
|
+
//
|
|
1995
|
+
// The split-detection note carries the second gate: its subject is that the command half *did*
|
|
1996
|
+
// resolve, so a winner that resolved neither required key leaves it unraised, for the same reason
|
|
1997
|
+
// no family answering does — both keys already carry `undetectedCommandWarning`.
|
|
1998
|
+
if (commandFamily !== undefined) {
|
|
1999
|
+
const claim = { family: commandFamily, manifest: commandManifest, resolvedRequired };
|
|
2000
|
+
if (readManifests.length > 1) {
|
|
2001
|
+
context.note(familyTiebreakNote({ ...claim, readManifests, declinedFamilies }));
|
|
2002
|
+
}
|
|
2003
|
+
if (detection.matchedSignal === FLAT_FALLBACK_SIGNAL_ID && resolvedRequired.length > 0) {
|
|
2004
|
+
context.note(splitDetectionNote(claim));
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
// Assembled key by key rather than spread, so the two required keys are required in the type as
|
|
2008
|
+
// well as in the loop above, and an optional key that was not resolved is absent rather than
|
|
2009
|
+
// present-and-undefined.
|
|
2010
|
+
const rawCommands = {
|
|
2011
|
+
typecheck: resolved.typecheck ?? placeholderCommand('typecheck'),
|
|
2012
|
+
test: resolved.test ?? placeholderCommand('test'),
|
|
2013
|
+
};
|
|
2014
|
+
if (resolved.build !== undefined)
|
|
2015
|
+
rawCommands.build = resolved.build;
|
|
2016
|
+
if (resolved.devServer !== undefined)
|
|
2017
|
+
rawCommands.devServer = resolved.devServer;
|
|
2018
|
+
if (resolved.depInstall !== undefined)
|
|
2019
|
+
rawCommands.depInstall = resolved.depInstall;
|
|
2020
|
+
return {
|
|
2021
|
+
preset,
|
|
2022
|
+
layers,
|
|
2023
|
+
rawCommands,
|
|
2024
|
+
warnings: context.drainWarnings(),
|
|
2025
|
+
notes: context.drainNotes(),
|
|
2026
|
+
commandFamily,
|
|
2027
|
+
commandManifest,
|
|
2028
|
+
resolvedRequired,
|
|
2029
|
+
readManifests,
|
|
2030
|
+
declinedFamilies,
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
//# sourceMappingURL=presets.js.map
|