@vzn/vx 0.0.639 → 0.0.640
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/README.md +1 -1
- package/docs/caching.md +111 -83
- package/docs/cli.md +70 -40
- package/docs/execution.md +4 -4
- package/docs/features.md +9 -8
- package/docs/patterns.md +1 -1
- package/docs/schema.md +235 -180
- package/docs/security.md +1 -1
- package/package.json +5 -5
- package/schemas/show.json +14 -14
- package/skills/vx/SKILL.md +4 -3
- package/src/cache/cache.ts +4 -1
- package/src/cache/file-hashes.ts +7 -12
- package/src/cache/fs-clock.ts +62 -0
- package/src/cache/git-inputs.ts +2 -2
- package/src/cache/index.ts +3 -0
- package/src/cache/inputs.ts +154 -83
- package/src/cache/key-fold.ts +23 -3
- package/src/cache/layer.ts +8 -7
- package/src/cache/linked-inputs.ts +87 -0
- package/src/cli/foreign-flags.ts +1 -1
- package/src/cli/init.ts +23 -11
- package/src/cli/last.ts +24 -1
- package/src/cli/plugin-templates.ts +2 -2
- package/src/cli/select.ts +16 -6
- package/src/cli/show.ts +9 -6
- package/src/cli/upgrade.ts +95 -19
- package/src/cli/watch-filter.ts +22 -6
- package/src/cli/watch-judge.ts +49 -5
- package/src/cli/watch-set.ts +18 -8
- package/src/cli/watch.ts +74 -2
- package/src/cli/why.ts +5 -3
- package/src/config.ts +19 -15
- package/src/exec/executor.ts +4 -4
- package/src/graph/dependency-spec.ts +2 -2
- package/src/graph/folds.ts +236 -0
- package/src/graph/index.ts +8 -1
- package/src/graph/scheduler.ts +41 -15
- package/src/graph/task-graph.ts +135 -73
- package/src/index.ts +1 -1
- package/src/orchestrator/affected-tasks.ts +20 -19
- package/src/orchestrator/download-policy.ts +16 -22
- package/src/orchestrator/excluded-keys.ts +4 -5
- package/src/orchestrator/execute-task.ts +29 -25
- package/src/orchestrator/index.ts +1 -0
- package/src/orchestrator/keyed-projects.ts +12 -51
- package/src/orchestrator/local-shortcircuit.ts +1 -1
- package/src/orchestrator/persistent-task.ts +3 -3
- package/src/orchestrator/placement.ts +2 -2
- package/src/orchestrator/plan.ts +1 -1
- package/src/orchestrator/plugin-host.ts +62 -22
- package/src/orchestrator/plugin.ts +12 -2
- package/src/orchestrator/prepare.ts +12 -5
- package/src/orchestrator/projects.ts +7 -10
- package/src/orchestrator/run-report.ts +124 -46
- package/src/orchestrator/run-steps.ts +57 -0
- package/src/orchestrator/run.ts +51 -21
- package/src/orchestrator/sandbox-fix.ts +1 -1
- package/src/orchestrator/sandbox-request.ts +2 -2
- package/src/orchestrator/stable-keys.ts +59 -26
- package/src/orchestrator/steps.ts +26 -6
- package/src/orchestrator/task-hash.ts +31 -9
- package/src/orchestrator/telemetry-host.ts +3 -3
- package/src/orchestrator/telemetry.ts +9 -4
- package/src/orchestrator/upstream.ts +4 -181
- package/src/report/framed-output.ts +1 -1
- package/src/report/status-line.ts +7 -3
- package/src/settings.ts +11 -17
- package/src/util/paths.ts +1 -1
- package/src/workspace/affected.ts +81 -2
- package/src/workspace/config-imports.ts +1 -1
- package/src/workspace/config-schema.ts +142 -143
- package/src/workspace/lockfile.ts +1 -1
- package/src/workspace/migration.ts +19 -3
|
@@ -567,7 +567,7 @@ export function validateProjectConfig(
|
|
|
567
567
|
)
|
|
568
568
|
}
|
|
569
569
|
// `VITE_*` passed nothing through and loaded clean (Turbo's
|
|
570
|
-
// passThroughEnv expands it), where `
|
|
570
|
+
// passThroughEnv expands it), where `inputs.env` refused the
|
|
571
571
|
// same text below: one refusal for the class (item 1090).
|
|
572
572
|
assertNoEnvNegation(passThrough as string[], `${where}.exec.env.passThrough`)
|
|
573
573
|
const wild = (passThrough as string[]).find((n) => BUN_GLOB_WILDCARDS.test(n))
|
|
@@ -691,11 +691,11 @@ export function validateProjectConfig(
|
|
|
691
691
|
// so a dependant's `^build` finds it and waits on nothing
|
|
692
692
|
// (the quickstart). The comment here used to call that case
|
|
693
693
|
// "almost certainly a config mistake" while the docs taught it.
|
|
694
|
-
// A keyed group (`
|
|
694
|
+
// A keyed group (`inputs` with no `exec`) is a declaration on its own:
|
|
695
695
|
// its inputs are a key that dependants fold, with no process.
|
|
696
|
-
if (dependsOn === undefined &&
|
|
696
|
+
if (dependsOn === undefined && (task as { inputs?: unknown }).inputs === undefined) {
|
|
697
697
|
throw new UserError(
|
|
698
|
-
`${where}: a task with no \`exec\` must declare \`dependsOn\` or \`
|
|
698
|
+
`${where}: a task with no \`exec\` must declare \`dependsOn\` or \`inputs\` ` +
|
|
699
699
|
`(group tasks exist to chain dependencies or carry a key)`,
|
|
700
700
|
)
|
|
701
701
|
}
|
|
@@ -712,31 +712,14 @@ export function validateProjectConfig(
|
|
|
712
712
|
)
|
|
713
713
|
}
|
|
714
714
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
)
|
|
720
|
-
}
|
|
721
|
-
assertKnownFields(cache, CACHE_FIELDS, `${where}.cache`)
|
|
722
|
-
const stability = (cache as { inputStability?: unknown }).inputStability
|
|
723
|
-
if (stability !== undefined)
|
|
724
|
-
validateInputStability(stability, `${where}.cache.inputStability`)
|
|
725
|
-
const inputs = (cache as { inputs?: unknown }).inputs
|
|
726
|
-
const outputs = (cache as { outputs?: unknown }).outputs
|
|
727
|
-
// A present field of the wrong shape (`outputs: 'dist'`) was told it
|
|
728
|
-
// was missing; an array is named in `assertKnownFields`.
|
|
729
|
-
if (inputs === undefined || inputs === null) {
|
|
730
|
-
throw new UserError(`${where}.cache.inputs is required when \`cache\` is set`)
|
|
715
|
+
const inputs = (task as { inputs?: unknown }).inputs
|
|
716
|
+
if (inputs !== undefined) {
|
|
717
|
+
if (typeof inputs !== 'object' || inputs === null) {
|
|
718
|
+
throw new UserError(`${where}.inputs must be an object — \`inputs: { files: [...] }\``)
|
|
731
719
|
}
|
|
732
|
-
|
|
733
|
-
throw new UserError(
|
|
734
|
-
`${where}.cache.inputs must be an object — \`inputs: { files: [...] }\``,
|
|
735
|
-
)
|
|
736
|
-
}
|
|
737
|
-
assertKnownFields(inputs, CACHE_INPUT_FIELDS, `${where}.cache.inputs`)
|
|
720
|
+
assertKnownFields(inputs, INPUT_FIELDS, `${where}.inputs`)
|
|
738
721
|
if (!Array.isArray((inputs as { files?: unknown }).files)) {
|
|
739
|
-
throw new UserError(`${where}.
|
|
722
|
+
throw new UserError(`${where}.inputs.files must be an array of glob strings`)
|
|
740
723
|
}
|
|
741
724
|
const envList = (inputs as { env?: unknown }).env
|
|
742
725
|
if (envList !== undefined) {
|
|
@@ -746,10 +729,10 @@ export function validateProjectConfig(
|
|
|
746
729
|
envList.some((s) => !isEnvName(s))
|
|
747
730
|
) {
|
|
748
731
|
throw new UserError(
|
|
749
|
-
`${where}.
|
|
732
|
+
`${where}.inputs.env must be an array of env var names (non-empty, no '=' or NUL)`,
|
|
750
733
|
)
|
|
751
734
|
}
|
|
752
|
-
assertNoEnvNegation(envList as string[], `${where}.
|
|
735
|
+
assertNoEnvNegation(envList as string[], `${where}.inputs.env`)
|
|
753
736
|
for (const name of envList as string[]) {
|
|
754
737
|
// Reject wildcards explicitly so users don't silently miss
|
|
755
738
|
// env vars they thought they were tracking. Turbo supports
|
|
@@ -761,107 +744,35 @@ export function validateProjectConfig(
|
|
|
761
744
|
// whole `Bun.Glob` alphabet stays refused here.
|
|
762
745
|
if (BUN_GLOB_WILDCARDS.test(name)) {
|
|
763
746
|
throw new UserError(
|
|
764
|
-
`${where}.
|
|
747
|
+
`${where}.inputs.env: wildcards in env names are not supported ` +
|
|
765
748
|
`(got "${name}") — list explicit env var names instead`,
|
|
766
749
|
)
|
|
767
750
|
}
|
|
768
751
|
}
|
|
769
752
|
}
|
|
770
|
-
if (outputs === undefined || outputs === null) {
|
|
771
|
-
throw new UserError(`${where}.cache.outputs is required when \`cache\` is set`)
|
|
772
|
-
}
|
|
773
|
-
if (typeof outputs !== 'object') {
|
|
774
|
-
throw new UserError(
|
|
775
|
-
`${where}.cache.outputs must be an object — \`outputs: { files: [...] }\``,
|
|
776
|
-
)
|
|
777
|
-
}
|
|
778
|
-
assertKnownFields(outputs, CACHE_OUTPUT_FIELDS, `${where}.cache.outputs`)
|
|
779
|
-
const clean = (outputs as { clean?: unknown }).clean
|
|
780
|
-
if (clean !== undefined && typeof clean !== 'boolean') {
|
|
781
|
-
throw new UserError(`${where}.cache.outputs.clean must be true or false`)
|
|
782
|
-
}
|
|
783
|
-
const outFiles = (outputs as { files?: unknown }).files
|
|
784
|
-
if (!Array.isArray(outFiles)) {
|
|
785
|
-
throw new UserError(`${where}.cache.outputs.files must be an array of glob strings`)
|
|
786
|
-
}
|
|
787
|
-
// Reject zero-length strings and absolute paths up front.
|
|
788
|
-
// Both reach `resolveOutputs` as undefined behavior; the error
|
|
789
|
-
// surfaces deep inside the glob resolver with no line pointing
|
|
790
|
-
// at the user's config. Fail loud at load time.
|
|
791
|
-
for (const g of outFiles as unknown[]) {
|
|
792
|
-
if (typeof g !== 'string' || g.length === 0) {
|
|
793
|
-
throw new UserError(`${where}.cache.outputs.files must be an array of non-empty strings`)
|
|
794
|
-
}
|
|
795
|
-
if (isAbsoluteGlob(g)) {
|
|
796
|
-
throw new UserError(
|
|
797
|
-
`${where}.cache.outputs.files: absolute paths are not allowed (got "${g}") — ` +
|
|
798
|
-
`outputs must be project-relative globs`,
|
|
799
|
-
)
|
|
800
|
-
}
|
|
801
|
-
if (hasParentSegment(g)) {
|
|
802
|
-
throw new UserError(
|
|
803
|
-
`${where}.cache.outputs.files: '..' path segments are not allowed (got "${g}") — ` +
|
|
804
|
-
`outputs must stay within the project (a glob that escapes the project dir would ` +
|
|
805
|
-
`let cleanOutputs delete files outside it)`,
|
|
806
|
-
)
|
|
807
|
-
}
|
|
808
|
-
assertPosixGlob(g, `${where}.cache.outputs.files`, 'project')
|
|
809
|
-
assertNoForeignToken(g, `${where}.cache.outputs.files`)
|
|
810
|
-
if (namesDirItself(g)) {
|
|
811
|
-
throw new UserError(
|
|
812
|
-
`${where}.cache.outputs.files: "${g}" names the project directory itself and selects nothing — ` +
|
|
813
|
-
`name the directory the task writes, such as "dist/**"`,
|
|
814
|
-
)
|
|
815
|
-
}
|
|
816
|
-
// An artifact stores workspace outputs under `workspace-outputs/`,
|
|
817
|
-
// and its index read a project output there as one: every hit was
|
|
818
|
-
// a "corrupt artifact", dropped and run again.
|
|
819
|
-
if (!g.startsWith('!') && normalizeGlob(g).split('/')[0] === 'workspace-outputs') {
|
|
820
|
-
throw new UserError(
|
|
821
|
-
`${where}.cache.outputs.files: "${g}" is under workspace-outputs/, a name vx's ` +
|
|
822
|
-
`artifacts reserve for outputs.workspaceFiles — write the task's files to another directory`,
|
|
823
|
-
)
|
|
824
|
-
}
|
|
825
|
-
// A `!` entry takes a path back from the outputs (A-44): it is not
|
|
826
|
-
// cleaned, saved or restored, and it stays an input.
|
|
827
|
-
assertNotDoubleNegated(g, `${where}.cache.outputs.files`)
|
|
828
|
-
// Taken back by a `!`, the file is never cleaned or restored.
|
|
829
|
-
const own = g.startsWith('!')
|
|
830
|
-
? undefined
|
|
831
|
-
: ownFilesCovered(g, ownConfig).find((f) => !takenBack(outFiles as unknown[], f))
|
|
832
|
-
if (own !== undefined) {
|
|
833
|
-
throw new UserError(
|
|
834
|
-
`${where}.cache.outputs.files: "${g}" covers the project's own ${own} — vx deletes a ` +
|
|
835
|
-
`task's outputs before it runs and restores them on a hit, so the project would lose ` +
|
|
836
|
-
`its config. Name the directory the task writes, such as "dist/**", ` +
|
|
837
|
-
`or take the file back with "!${own}".`,
|
|
838
|
-
)
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
assertOutputsNotNegationOnly(outFiles as string[], `${where}.cache.outputs.files`)
|
|
842
753
|
// Same for inputs.files.
|
|
843
754
|
for (const g of (inputs as { files: unknown[] }).files) {
|
|
844
755
|
if (typeof g !== 'string' || g.length === 0) {
|
|
845
|
-
throw new UserError(`${where}.
|
|
756
|
+
throw new UserError(`${where}.inputs.files must be an array of non-empty strings`)
|
|
846
757
|
}
|
|
847
758
|
if (!g.startsWith('!/') && isAbsoluteGlob(g)) {
|
|
848
759
|
throw new UserError(
|
|
849
|
-
`${where}.
|
|
760
|
+
`${where}.inputs.files: absolute paths are not allowed (got "${g}") — ` +
|
|
850
761
|
`inputs must be project-relative globs`,
|
|
851
762
|
)
|
|
852
763
|
}
|
|
853
764
|
if (hasParentSegment(g)) {
|
|
854
765
|
throw new UserError(
|
|
855
|
-
`${where}.
|
|
766
|
+
`${where}.inputs.files: '..' path segments are not allowed (got "${g}") — ` +
|
|
856
767
|
`inputs must be project-relative (a '..' glob silently matches nothing; ` +
|
|
857
|
-
`use
|
|
768
|
+
`use inputs.workspaceFiles for workspace-root-relative inputs)`,
|
|
858
769
|
)
|
|
859
770
|
}
|
|
860
|
-
assertPosixGlob(g, `${where}.
|
|
861
|
-
assertNoForeignToken(g, `${where}.
|
|
771
|
+
assertPosixGlob(g, `${where}.inputs.files`, 'project')
|
|
772
|
+
assertNoForeignToken(g, `${where}.inputs.files`)
|
|
862
773
|
if (namesDirItself(g)) {
|
|
863
774
|
throw new UserError(
|
|
864
|
-
`${where}.
|
|
775
|
+
`${where}.inputs.files: "${g}" names the project directory itself and selects nothing — use "**" for everything under it`,
|
|
865
776
|
)
|
|
866
777
|
}
|
|
867
778
|
// A negated absolute path subtracts nothing (the positive globs are
|
|
@@ -869,11 +780,11 @@ export function validateProjectConfig(
|
|
|
869
780
|
// `workspaceFiles` refuses both spellings the same way (item 679).
|
|
870
781
|
if (g.startsWith('!/')) {
|
|
871
782
|
throw new UserError(
|
|
872
|
-
`${where}.
|
|
783
|
+
`${where}.inputs.files: absolute paths are not allowed (got "${g}") — ` +
|
|
873
784
|
`inputs must be project-relative globs`,
|
|
874
785
|
)
|
|
875
786
|
}
|
|
876
|
-
assertNotDoubleNegated(g, `${where}.
|
|
787
|
+
assertNotDoubleNegated(g, `${where}.inputs.files`)
|
|
877
788
|
}
|
|
878
789
|
// A list of ONLY negations selects nothing at all. `resolveFiles`
|
|
879
790
|
// builds the file set from the POSITIVE globs and uses the `!` entries
|
|
@@ -885,9 +796,9 @@ export function validateProjectConfig(
|
|
|
885
796
|
// "everything except specs", and Turbo makes exactly this a hard config
|
|
886
797
|
// error for the same reason. Refusing costs nothing: such a config was
|
|
887
798
|
// already silently broken, so no working key changes.
|
|
888
|
-
assertNotNegationOnly((inputs as { files: string[] }).files, `${where}.
|
|
889
|
-
assertNoLiteralTakenBack((inputs as { files: string[] }).files, `${where}.
|
|
890
|
-
// The only
|
|
799
|
+
assertNotNegationOnly((inputs as { files: string[] }).files, `${where}.inputs.files`)
|
|
800
|
+
assertNoLiteralTakenBack((inputs as { files: string[] }).files, `${where}.inputs.files`)
|
|
801
|
+
// The only InputsConfig field that reached the run unvalidated: a
|
|
891
802
|
// non-string entry crashes deep in `filterUpstreamHashes` /
|
|
892
803
|
// `parseDependencySpec` with a raw TypeError naming neither the task
|
|
893
804
|
// nor the config. Entry SYNTAX is already checked downstream (a
|
|
@@ -900,7 +811,7 @@ export function validateProjectConfig(
|
|
|
900
811
|
taskFilters.some((s) => typeof s !== 'string' || s.length === 0)
|
|
901
812
|
) {
|
|
902
813
|
throw new UserError(
|
|
903
|
-
`${where}.
|
|
814
|
+
`${where}.inputs.tasks must be an array of non-empty strings ` +
|
|
904
815
|
`(Turbo/Nx micro-syntax: 'name', '^name', 'pkg#name', '*', '^*', '!name')`,
|
|
905
816
|
)
|
|
906
817
|
}
|
|
@@ -916,7 +827,7 @@ export function validateProjectConfig(
|
|
|
916
827
|
list.some((s) => typeof s !== 'string' || s.trim() === '' || s.includes('\0'))
|
|
917
828
|
) {
|
|
918
829
|
throw new UserError(
|
|
919
|
-
`${where}.
|
|
830
|
+
`${where}.inputs.${field} must be an array of non-empty shell command strings with no NUL`,
|
|
920
831
|
)
|
|
921
832
|
}
|
|
922
833
|
}
|
|
@@ -925,17 +836,107 @@ export function validateProjectConfig(
|
|
|
925
836
|
// never absolute (they're workspace-root-relative by definition).
|
|
926
837
|
const wsInputs = (inputs as { workspaceFiles?: unknown }).workspaceFiles
|
|
927
838
|
if (wsInputs !== undefined) {
|
|
928
|
-
validateWorkspaceGlobs(wsInputs, `${where}.
|
|
839
|
+
validateWorkspaceGlobs(wsInputs, `${where}.inputs.workspaceFiles`, true)
|
|
929
840
|
}
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
841
|
+
}
|
|
842
|
+
if (cache !== undefined) {
|
|
843
|
+
if (typeof cache !== 'object' || cache === null) {
|
|
844
|
+
throw new UserError(
|
|
845
|
+
`${where}.cache must be an object when present — \`inputs: { files: [...] }, cache: { outputs: { files: [...] } }\`, or no \`cache\` for a task that never caches`,
|
|
846
|
+
)
|
|
847
|
+
}
|
|
848
|
+
assertKnownFields(cache, CACHE_FIELDS, `${where}.cache`)
|
|
849
|
+
const stability = (cache as { inputStability?: unknown }).inputStability
|
|
850
|
+
if (stability !== undefined)
|
|
851
|
+
validateInputStability(stability, `${where}.cache.inputStability`)
|
|
852
|
+
// The key is the task's inputs, and a group runs nothing to cache.
|
|
853
|
+
if (exec === undefined) {
|
|
854
|
+
throw new UserError(
|
|
855
|
+
`${where}: \`cache\` needs \`exec\` — a task with no command produces nothing to cache`,
|
|
856
|
+
)
|
|
933
857
|
}
|
|
934
|
-
if (
|
|
858
|
+
if (inputs === undefined) {
|
|
935
859
|
throw new UserError(
|
|
936
|
-
`${where}
|
|
860
|
+
`${where}: \`cache\` needs \`inputs\` — the key is the task's inputs, \`inputs: { files: [...] }\``,
|
|
937
861
|
)
|
|
938
862
|
}
|
|
863
|
+
const outputs = (cache as { outputs?: unknown }).outputs
|
|
864
|
+
if (outputs === undefined || outputs === null) {
|
|
865
|
+
throw new UserError(`${where}.cache.outputs is required when \`cache\` is set`)
|
|
866
|
+
}
|
|
867
|
+
if (typeof outputs !== 'object') {
|
|
868
|
+
throw new UserError(
|
|
869
|
+
`${where}.cache.outputs must be an object — \`outputs: { files: [...] }\``,
|
|
870
|
+
)
|
|
871
|
+
}
|
|
872
|
+
assertKnownFields(outputs, CACHE_OUTPUT_FIELDS, `${where}.cache.outputs`)
|
|
873
|
+
const clean = (outputs as { clean?: unknown }).clean
|
|
874
|
+
if (clean !== undefined && typeof clean !== 'boolean') {
|
|
875
|
+
throw new UserError(`${where}.cache.outputs.clean must be true or false`)
|
|
876
|
+
}
|
|
877
|
+
const outFiles = (outputs as { files?: unknown }).files
|
|
878
|
+
if (!Array.isArray(outFiles)) {
|
|
879
|
+
throw new UserError(`${where}.cache.outputs.files must be an array of glob strings`)
|
|
880
|
+
}
|
|
881
|
+
// Reject zero-length strings and absolute paths up front.
|
|
882
|
+
// Both reach `resolveOutputs` as undefined behavior; the error
|
|
883
|
+
// surfaces deep inside the glob resolver with no line pointing
|
|
884
|
+
// at the user's config. Fail loud at load time.
|
|
885
|
+
for (const g of outFiles as unknown[]) {
|
|
886
|
+
if (typeof g !== 'string' || g.length === 0) {
|
|
887
|
+
throw new UserError(`${where}.cache.outputs.files must be an array of non-empty strings`)
|
|
888
|
+
}
|
|
889
|
+
if (isAbsoluteGlob(g)) {
|
|
890
|
+
throw new UserError(
|
|
891
|
+
`${where}.cache.outputs.files: absolute paths are not allowed (got "${g}") — ` +
|
|
892
|
+
`outputs must be project-relative globs`,
|
|
893
|
+
)
|
|
894
|
+
}
|
|
895
|
+
if (hasParentSegment(g)) {
|
|
896
|
+
throw new UserError(
|
|
897
|
+
`${where}.cache.outputs.files: '..' path segments are not allowed (got "${g}") — ` +
|
|
898
|
+
`outputs must stay within the project (a glob that escapes the project dir would ` +
|
|
899
|
+
`let cleanOutputs delete files outside it)`,
|
|
900
|
+
)
|
|
901
|
+
}
|
|
902
|
+
assertPosixGlob(g, `${where}.cache.outputs.files`, 'project')
|
|
903
|
+
assertNoForeignToken(g, `${where}.cache.outputs.files`)
|
|
904
|
+
if (namesDirItself(g)) {
|
|
905
|
+
throw new UserError(
|
|
906
|
+
`${where}.cache.outputs.files: "${g}" names the project directory itself and selects nothing — ` +
|
|
907
|
+
`name the directory the task writes, such as "dist/**"`,
|
|
908
|
+
)
|
|
909
|
+
}
|
|
910
|
+
// An artifact stores workspace outputs under `workspace-outputs/`,
|
|
911
|
+
// and its index read a project output there as one: every hit was
|
|
912
|
+
// a "corrupt artifact", dropped and run again.
|
|
913
|
+
if (!g.startsWith('!') && normalizeGlob(g).split('/')[0] === 'workspace-outputs') {
|
|
914
|
+
throw new UserError(
|
|
915
|
+
`${where}.cache.outputs.files: "${g}" is under workspace-outputs/, a name vx's ` +
|
|
916
|
+
`artifacts reserve for outputs.workspaceFiles — write the task's files to another directory`,
|
|
917
|
+
)
|
|
918
|
+
}
|
|
919
|
+
// A `!` entry takes a path back from the outputs (A-44): it is not
|
|
920
|
+
// cleaned, saved or restored, and it stays an input.
|
|
921
|
+
assertNotDoubleNegated(g, `${where}.cache.outputs.files`)
|
|
922
|
+
// Taken back by a `!`, the file is never cleaned or restored.
|
|
923
|
+
const own = g.startsWith('!')
|
|
924
|
+
? undefined
|
|
925
|
+
: ownFilesCovered(g, ownConfig).find((f) => !takenBack(outFiles as unknown[], f))
|
|
926
|
+
if (own !== undefined) {
|
|
927
|
+
throw new UserError(
|
|
928
|
+
`${where}.cache.outputs.files: "${g}" covers the project's own ${own} — vx deletes a ` +
|
|
929
|
+
`task's outputs before it runs and restores them on a hit, so the project would lose ` +
|
|
930
|
+
`its config. Name the directory the task writes, such as "dist/**", ` +
|
|
931
|
+
`or take the file back with "!${own}".`,
|
|
932
|
+
)
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
assertOutputsNotNegationOnly(outFiles as string[], `${where}.cache.outputs.files`)
|
|
936
|
+
const wsOutputs = (outputs as { workspaceFiles?: unknown }).workspaceFiles
|
|
937
|
+
if (wsOutputs !== undefined) {
|
|
938
|
+
validateWorkspaceGlobs(wsOutputs, `${where}.cache.outputs.workspaceFiles`, false)
|
|
939
|
+
}
|
|
939
940
|
}
|
|
940
941
|
}
|
|
941
942
|
}
|
|
@@ -949,7 +950,7 @@ export function validateProjectConfig(
|
|
|
949
950
|
// `tasks` first: `nearest` breaks a tie by order, and `taks` is one edit
|
|
950
951
|
// from both.
|
|
951
952
|
const PROJECT_FIELDS = new Set(['name', 'tasks', 'tags'])
|
|
952
|
-
const TASK_FIELDS = new Set(['description', 'exec', 'dependsOn', 'cache'])
|
|
953
|
+
const TASK_FIELDS = new Set(['description', 'exec', 'dependsOn', 'inputs', 'cache'])
|
|
953
954
|
const EXEC_FIELDS = new Set([
|
|
954
955
|
'command',
|
|
955
956
|
'env',
|
|
@@ -962,8 +963,8 @@ const EXEC_FIELDS = new Set([
|
|
|
962
963
|
])
|
|
963
964
|
const PERSISTENT_FIELDS = new Set(['readyWhen'])
|
|
964
965
|
const ENV_FIELDS = new Set(['passThrough', 'define', 'secret'])
|
|
965
|
-
const CACHE_FIELDS = new Set(['
|
|
966
|
-
const
|
|
966
|
+
const CACHE_FIELDS = new Set(['outputs', 'inputStability'])
|
|
967
|
+
const INPUT_FIELDS = new Set([
|
|
967
968
|
'files',
|
|
968
969
|
'workspaceFiles',
|
|
969
970
|
'env',
|
|
@@ -983,9 +984,8 @@ const FOREIGN_FIELDS: ReadonlyMap<ReadonlySet<string>, Readonly<Record<string, s
|
|
|
983
984
|
[
|
|
984
985
|
TASK_FIELDS,
|
|
985
986
|
{
|
|
986
|
-
inputs: '`cache.inputs.files`',
|
|
987
987
|
outputs: '`cache.outputs.files`',
|
|
988
|
-
env: '`
|
|
988
|
+
env: '`inputs.env` (to key the task on a variable) or `exec.env` (to pass one)',
|
|
989
989
|
passThroughEnv: '`exec.env.passThrough`',
|
|
990
990
|
command: '`exec.command`',
|
|
991
991
|
cmd: '`exec.command`',
|
|
@@ -1027,15 +1027,15 @@ const FOREIGN_FIELDS: ReadonlyMap<ReadonlySet<string>, Readonly<Record<string, s
|
|
|
1027
1027
|
[
|
|
1028
1028
|
CACHE_FIELDS,
|
|
1029
1029
|
{
|
|
1030
|
-
files: '`inputs.files`
|
|
1030
|
+
files: "the task's `inputs.files`",
|
|
1031
1031
|
output: '`outputs.files`',
|
|
1032
|
-
env: '`inputs.env`
|
|
1032
|
+
env: "the task's `inputs.env`",
|
|
1033
1033
|
dependencies: "the task's `dependsOn` (an upstream's key folds into this one)",
|
|
1034
1034
|
enabled: 'no field: a task with a `cache` block caches, one without runs every time',
|
|
1035
1035
|
},
|
|
1036
1036
|
],
|
|
1037
1037
|
[
|
|
1038
|
-
|
|
1038
|
+
INPUT_FIELDS,
|
|
1039
1039
|
{
|
|
1040
1040
|
globs: '`files`',
|
|
1041
1041
|
include: '`files`',
|
|
@@ -1065,19 +1065,18 @@ const FOREIGN_FIELDS: ReadonlyMap<ReadonlySet<string>, Readonly<Record<string, s
|
|
|
1065
1065
|
scripts:
|
|
1066
1066
|
"`tasks` (a script is a task's `exec.command`; `vx init` writes them from package.json)",
|
|
1067
1067
|
pipeline: '`tasks` (`bunx @vzn/vx-migrate` writes them from turbo.json)',
|
|
1068
|
-
namedInputs: 'a module the vx.config imports, spread into `
|
|
1068
|
+
namedInputs: 'a module the vx.config imports, spread into `inputs.files`',
|
|
1069
1069
|
root: "no field: a project's directory is where its vx.config sits",
|
|
1070
|
-
sourceRoot: "a task's `
|
|
1070
|
+
sourceRoot: "a task's `inputs.files`",
|
|
1071
1071
|
projectType: 'no field: vx runs every project alike',
|
|
1072
1072
|
...Object.fromEntries(
|
|
1073
|
-
['dependsOn', 'cache', 'exec', 'description'].map((k) => [
|
|
1073
|
+
['dependsOn', 'inputs', 'cache', 'exec', 'description'].map((k) => [
|
|
1074
1074
|
k,
|
|
1075
1075
|
`\`tasks.<name>.${k}\`: a project holds tasks, and each task its own \`${k}\``,
|
|
1076
1076
|
]),
|
|
1077
1077
|
),
|
|
1078
|
-
inputs: '`tasks.<name>.cache.inputs.files`',
|
|
1079
1078
|
outputs: '`tasks.<name>.cache.outputs.files`',
|
|
1080
|
-
env: '`tasks.<name>.
|
|
1079
|
+
env: '`tasks.<name>.inputs.env` (to key a task on it) or `tasks.<name>.exec.env`',
|
|
1081
1080
|
},
|
|
1082
1081
|
],
|
|
1083
1082
|
// Turbo's turbo.json and Nx's nx.json keys, written into .vx/workspace
|
|
@@ -1091,9 +1090,9 @@ const FOREIGN_FIELDS: ReadonlyMap<ReadonlySet<string>, Readonly<Record<string, s
|
|
|
1091
1090
|
"each package's `vx.config` `tasks` (`bunx @vzn/vx-migrate` writes them from turbo.json)",
|
|
1092
1091
|
targetDefaults:
|
|
1093
1092
|
"each package's `vx.config` `tasks`, sharing defaults through an imported module",
|
|
1094
|
-
namedInputs: 'a module each `vx.config` imports, spread into `
|
|
1095
|
-
globalEnv: '`
|
|
1096
|
-
globalDependencies: '`
|
|
1093
|
+
namedInputs: 'a module each `vx.config` imports, spread into `inputs.files`',
|
|
1094
|
+
globalEnv: '`inputs.env` on the tasks it keys',
|
|
1095
|
+
globalDependencies: '`inputs.workspaceFiles` on the tasks it keys',
|
|
1097
1096
|
remoteCache:
|
|
1098
1097
|
'a cache plugin in `plugins` (`turboCache()` or `nxCache()` from `@vzn/vx-migrate`)',
|
|
1099
1098
|
parallel: '`run.concurrency`',
|
|
@@ -1113,7 +1112,7 @@ const FOREIGN_FIELDS: ReadonlyMap<ReadonlySet<string>, Readonly<Record<string, s
|
|
|
1113
1112
|
"pnpm-workspace.yaml's or package.json's `catalog`, where the package manager reads it",
|
|
1114
1113
|
npmClient: "no field: a task's command runs as written, with no package manager in between",
|
|
1115
1114
|
remote: 'a cache plugin in `plugins`',
|
|
1116
|
-
env: '`
|
|
1115
|
+
env: '`inputs.env` on the tasks it keys, or `exec.env` on the tasks it passes to',
|
|
1117
1116
|
},
|
|
1118
1117
|
],
|
|
1119
1118
|
])
|
|
@@ -1367,7 +1366,7 @@ function assertNotDoubleNegated(glob: string, where: string): void {
|
|
|
1367
1366
|
}
|
|
1368
1367
|
|
|
1369
1368
|
/**
|
|
1370
|
-
* An exact `
|
|
1369
|
+
* An exact `inputs.tasks` entry that no `dependsOn` entry names would
|
|
1371
1370
|
* match nothing at hash time and fold no upstream hash — the task silently
|
|
1372
1371
|
* decoupled from its dependencies, which is a stale hit waiting for the
|
|
1373
1372
|
* next upstream change. `['buidl']` for `['build']` is the shape. Patterns,
|
|
@@ -1407,7 +1406,7 @@ function assertFilterNamesDeclaredDeps(
|
|
|
1407
1406
|
// `where` opens with the config's path; the message names it once.
|
|
1408
1407
|
const task = where.slice(where.lastIndexOf(': tasks.') + 2)
|
|
1409
1408
|
throw new UserError(
|
|
1410
|
-
`${where}.
|
|
1409
|
+
`${where}.inputs.tasks: "${raw}" names no task in ${task}.dependsOn ` +
|
|
1411
1410
|
`(${declared.length === 0 ? 'none declared' : dependsOn!.map((d) => `'${d}'`).join(', ')}) — ` +
|
|
1412
1411
|
`it would match nothing and fold no upstream hash, decoupling the task from its ` +
|
|
1413
1412
|
`dependencies. \`name\` is this project's task, \`^name\` its dependencies', ` +
|
|
@@ -1450,7 +1449,7 @@ function isEnvName(name: unknown): name is string {
|
|
|
1450
1449
|
}
|
|
1451
1450
|
|
|
1452
1451
|
/**
|
|
1453
|
-
* A task name `dependsOn`, `
|
|
1452
|
+
* A task name `dependsOn`, `inputs.tasks` and the CLI can name. Each
|
|
1454
1453
|
* refused shape loaded and could not be referenced: `x#y` read as project
|
|
1455
1454
|
* `x`, `^gen` as every dependency's `gen`, a `*` as a pattern, `''` ran
|
|
1456
1455
|
* under `--all` as `a#` and `vx run a#` refused it, and a padded name was
|
|
@@ -1460,7 +1459,7 @@ function assertTaskName(name: string, configPath: string): void {
|
|
|
1460
1459
|
const why = taskNameProblem(name)
|
|
1461
1460
|
if (why === null) return
|
|
1462
1461
|
throw new UserError(
|
|
1463
|
-
`${configPath}: task name ${JSON.stringify(name)} ${why} — dependsOn,
|
|
1462
|
+
`${configPath}: task name ${JSON.stringify(name)} ${why} — dependsOn, inputs.tasks and ` +
|
|
1464
1463
|
`the CLI could not name it. Rename the task.`,
|
|
1465
1464
|
)
|
|
1466
1465
|
}
|
|
@@ -21,7 +21,7 @@ import { validateProjectConfig } from './config-schema.js'
|
|
|
21
21
|
import { PROJECT_CONFIG_FILENAMES } from './workspace.js'
|
|
22
22
|
|
|
23
23
|
export const LOCKFILE_NAME = '.vx/projects.lock.json'
|
|
24
|
-
export const LOCKFILE_VERSION =
|
|
24
|
+
export const LOCKFILE_VERSION = 4
|
|
25
25
|
|
|
26
26
|
/** One project as the lock holds it. */
|
|
27
27
|
export interface LockedProject {
|
|
@@ -67,7 +67,7 @@ const QUOTE_ARGS =
|
|
|
67
67
|
`vx_r="$vx_r\${vx_s%%"$vx_q"*}'\\''"; vx_s=\${vx_s#*"$vx_q"};; *) break;; esac; done; ` +
|
|
68
68
|
`vx_a="$vx_a '$vx_r$vx_s'"; done`
|
|
69
69
|
|
|
70
|
-
const isCacheTodo = (todo: string): boolean => todo.startsWith('cache: add `
|
|
70
|
+
const isCacheTodo = (todo: string): boolean => todo.startsWith('cache: add `inputs: {')
|
|
71
71
|
|
|
72
72
|
/** The one wording every mapper emits for a task it made persistent. */
|
|
73
73
|
export const PERSISTENT_TODO =
|
|
@@ -426,7 +426,7 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
|
|
|
426
426
|
}
|
|
427
427
|
|
|
428
428
|
// The lock's own words become a row; a refusal prints above the report.
|
|
429
|
-
const lock = dry || args.lock === undefined ? undefined : await args.lock(root)
|
|
429
|
+
const lock = dry || args.lock === undefined ? undefined : lockAnswer(await args.lock(root))
|
|
430
430
|
if (lock !== undefined && lock.code !== 0) process.stderr.write(lock.said)
|
|
431
431
|
|
|
432
432
|
const rows: ui.FieldRow[] = [['from', source]]
|
|
@@ -488,6 +488,22 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
|
|
|
488
488
|
return 0
|
|
489
489
|
}
|
|
490
490
|
|
|
491
|
+
/**
|
|
492
|
+
* The lock step's answer, checked: the tool calling this seam is another
|
|
493
|
+
* package and may be another version (vx-migrate 0.0.637's answered its
|
|
494
|
+
* exit code, which crashed the report).
|
|
495
|
+
*/
|
|
496
|
+
function lockAnswer(answer: unknown): { code: number; said: string } {
|
|
497
|
+
const a = answer as { code?: unknown; said?: unknown } | null
|
|
498
|
+
if (
|
|
499
|
+
typeof a === 'object' &&
|
|
500
|
+
a !== null &&
|
|
501
|
+
typeof a.code === 'number' &&
|
|
502
|
+
typeof a.said === 'string'
|
|
503
|
+
)
|
|
504
|
+
return { code: a.code, said: a.said }
|
|
505
|
+
return { code: 1, said: 'vx lock: the migration tool answered no result: run `vx lock`\n' }
|
|
506
|
+
}
|
|
491
507
|
/**
|
|
492
508
|
* What to do about written files .gitignore drops. A `!file` line cures a
|
|
493
509
|
* file, but git re-includes nothing inside an ignored directory: an
|
|
@@ -624,7 +640,7 @@ const EXAMPLE_BODY = `export default {
|
|
|
624
640
|
build: {
|
|
625
641
|
exec: { command: 'tsc -b' },
|
|
626
642
|
dependsOn: ['^build'],
|
|
627
|
-
|
|
643
|
+
inputs: { files: ['src/**'] }, cache: { outputs: { files: ['dist/**'] } },
|
|
628
644
|
},
|
|
629
645
|
},
|
|
630
646
|
}`
|