@aws/nx-plugin 1.0.0-rc.89 → 1.0.0-rc.90
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 +22 -22
- package/executors.json +5 -0
- package/generators.json +2 -1
- package/migrations.json +47 -2
- package/package.json +1 -1
- package/src/infra/app/__snapshots__/generator.spec.ts.snap +24 -12
- package/src/infra/app/generator.js +9 -7
- package/src/infra/app/generator.js.map +1 -1
- package/src/mcp-server/generator-info.d.ts +5 -0
- package/src/mcp-server/generator-info.js +6 -2
- package/src/mcp-server/generator-info.js.map +1 -1
- package/src/migrations/latest/add-assemble-target/metadata.json +3 -0
- package/src/migrations/latest/add-assemble-target/migration.d.ts +6 -0
- package/src/migrations/latest/add-assemble-target/migration.js +241 -0
- package/src/migrations/latest/add-assemble-target/migration.js.map +1 -0
- package/src/migrations/latest/declare-cacheable-target-inputs/metadata.json +3 -0
- package/src/migrations/latest/declare-cacheable-target-inputs/migration.d.ts +6 -0
- package/src/migrations/latest/declare-cacheable-target-inputs/migration.js +94 -0
- package/src/migrations/latest/declare-cacheable-target-inputs/migration.js.map +1 -0
- package/src/migrations/latest/dev-target-depends-on-pull-image/metadata.json +3 -0
- package/src/migrations/latest/dev-target-depends-on-pull-image/migration.d.ts +6 -0
- package/src/migrations/latest/dev-target-depends-on-pull-image/migration.js +67 -0
- package/src/migrations/latest/dev-target-depends-on-pull-image/migration.js.map +1 -0
- package/src/migrations/latest/docker-target-cache-soundness/metadata.json +3 -0
- package/src/migrations/latest/docker-target-cache-soundness/migration.d.ts +6 -0
- package/src/migrations/latest/docker-target-cache-soundness/migration.js +95 -0
- package/src/migrations/latest/docker-target-cache-soundness/migration.js.map +1 -0
- package/src/migrations/latest/open-api-codegen-executor/metadata.json +3 -0
- package/src/migrations/latest/open-api-codegen-executor/migration.d.ts +6 -0
- package/src/migrations/latest/open-api-codegen-executor/migration.js +100 -0
- package/src/migrations/latest/open-api-codegen-executor/migration.js.map +1 -0
- package/src/migrations/latest/python-test-input-scoping/metadata.json +3 -0
- package/src/migrations/latest/python-test-input-scoping/migration.d.ts +6 -0
- package/src/migrations/latest/python-test-input-scoping/migration.js +142 -0
- package/src/migrations/latest/python-test-input-scoping/migration.js.map +1 -0
- package/src/migrations/latest/terraform-fmt-check/metadata.json +3 -0
- package/src/migrations/latest/terraform-fmt-check/migration.d.ts +6 -0
- package/src/migrations/latest/terraform-fmt-check/migration.js +165 -0
- package/src/migrations/latest/terraform-fmt-check/migration.js.map +1 -0
- package/src/migrations/latest/terraform-provider-plugin-cache/metadata.json +3 -0
- package/src/migrations/latest/terraform-provider-plugin-cache/migration.d.ts +6 -0
- package/src/migrations/latest/terraform-provider-plugin-cache/migration.js +164 -0
- package/src/migrations/latest/terraform-provider-plugin-cache/migration.js.map +1 -0
- package/src/migrations/latest/vitest-coverage-output-dir/metadata.json +3 -0
- package/src/migrations/latest/vitest-coverage-output-dir/migration.d.ts +6 -0
- package/src/migrations/latest/vitest-coverage-output-dir/migration.js +177 -0
- package/src/migrations/latest/vitest-coverage-output-dir/migration.js.map +1 -0
- package/src/open-api/codegen/executor-schema.d.js +6 -0
- package/src/open-api/codegen/executor-schema.d.js.map +1 -0
- package/src/open-api/codegen/executor-schema.d.ts +18 -0
- package/src/open-api/codegen/executor-schema.json +33 -0
- package/src/open-api/codegen/executor.d.ts +18 -0
- package/src/open-api/codegen/executor.js +56 -0
- package/src/open-api/codegen/executor.js.map +1 -0
- package/src/preset/__snapshots__/generator.spec.ts.snap +2 -1
- package/src/py/agent/generator.js +21 -22
- package/src/py/agent/generator.js.map +1 -1
- package/src/py/agent/react-connection/generator.js +9 -0
- package/src/py/agent/react-connection/generator.js.map +1 -1
- package/src/py/dynamodb/__snapshots__/generator.spec.ts.snap +0 -30
- package/src/py/fast-api/react/open-api.js +19 -7
- package/src/py/fast-api/react/open-api.js.map +1 -1
- package/src/py/mcp-server/generator.js +4 -4
- package/src/py/mcp-server/generator.js.map +1 -1
- package/src/py/project/generator.d.ts +9 -0
- package/src/py/project/generator.js +51 -1
- package/src/py/project/generator.js.map +1 -1
- package/src/py/rdb/generator.js +10 -4
- package/src/py/rdb/generator.js.map +1 -1
- package/src/smithy/project/__snapshots__/generator.spec.ts.snap +15 -0
- package/src/smithy/project/generator.js +10 -0
- package/src/smithy/project/generator.js.map +1 -1
- package/src/smithy/ts/api/__snapshots__/generator.spec.ts.snap +7 -0
- package/src/terraform/project/files/application/scripts/env.ts.template +11 -0
- package/src/terraform/project/files/application/scripts/init.ts.template +2 -1
- package/src/terraform/project/files/application/src/providers.tf.template +2 -2
- package/src/terraform/project/generator.d.ts +11 -1
- package/src/terraform/project/generator.js +104 -20
- package/src/terraform/project/generator.js.map +1 -1
- package/src/trpc/backend/generator.js +2 -2
- package/src/trpc/backend/generator.js.map +1 -1
- package/src/ts/agent/generator.js +4 -7
- package/src/ts/agent/generator.js.map +1 -1
- package/src/ts/astro-docs/generator.js +5 -0
- package/src/ts/astro-docs/generator.js.map +1 -1
- package/src/ts/dynamodb/__snapshots__/generator.spec.ts.snap +0 -30
- package/src/ts/dynamodb/generator.js +3 -0
- package/src/ts/dynamodb/generator.js.map +1 -1
- package/src/ts/lib/__snapshots__/generator.spec.ts.snap +10 -1
- package/src/ts/lib/__snapshots__/vitest.spec.ts.snap +1 -1
- package/src/ts/lib/generator.d.ts +16 -0
- package/src/ts/lib/generator.js +31 -5
- package/src/ts/lib/generator.js.map +1 -1
- package/src/ts/lib/vitest.d.ts +11 -0
- package/src/ts/lib/vitest.js +31 -2
- package/src/ts/lib/vitest.js.map +1 -1
- package/src/ts/mcp-server/generator.js +4 -7
- package/src/ts/mcp-server/generator.js.map +1 -1
- package/src/ts/rdb/generator.js +8 -5
- package/src/ts/rdb/generator.js.map +1 -1
- package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +27 -11
- package/src/ts/react-website/app/generator.js +9 -1
- package/src/ts/react-website/app/generator.js.map +1 -1
- package/src/ts/sync/generator.js +19 -2
- package/src/ts/sync/generator.js.map +1 -1
- package/src/utils/agent-core-constructs/agent-core-constructs.js +3 -3
- package/src/utils/agent-core-constructs/agent-core-constructs.js.map +1 -1
- package/src/utils/api-constructs/api-constructs.js +2 -2
- package/src/utils/api-constructs/api-constructs.js.map +1 -1
- package/src/utils/api-constructs/open-api-metadata.js +15 -36
- package/src/utils/api-constructs/open-api-metadata.js.map +1 -1
- package/src/utils/api-constructs/trpc-operations-metadata.js +9 -3
- package/src/utils/api-constructs/trpc-operations-metadata.js.map +1 -1
- package/src/utils/bundle/bundle.js +13 -4
- package/src/utils/bundle/bundle.js.map +1 -1
- package/src/utils/connection/open-api/react.js +7 -17
- package/src/utils/connection/open-api/react.js.map +1 -1
- package/src/utils/dcr-proxy-constructs/dcr-proxy-constructs.js +2 -2
- package/src/utils/dcr-proxy-constructs/dcr-proxy-constructs.js.map +1 -1
- package/src/utils/docker.d.ts +22 -10
- package/src/utils/docker.js +27 -17
- package/src/utils/docker.js.map +1 -1
- package/src/utils/dynamodb-constructs/dynamodb-constructs.js +2 -2
- package/src/utils/dynamodb-constructs/dynamodb-constructs.js.map +1 -1
- package/src/utils/files/common/scripts/src/dynamodb/start-container.ts.template +0 -15
- package/src/utils/files/common/scripts/src/rdb/shared/start-container.ts.template +0 -15
- package/src/utils/format.d.ts +5 -0
- package/src/utils/format.js +5 -0
- package/src/utils/format.js.map +1 -1
- package/src/utils/function-constructs/function-constructs.js +2 -2
- package/src/utils/function-constructs/function-constructs.js.map +1 -1
- package/src/utils/generators.d.ts +33 -0
- package/src/utils/generators.js +18 -3
- package/src/utils/generators.js.map +1 -1
- package/src/utils/init.js +4 -6
- package/src/utils/init.js.map +1 -1
- package/src/utils/mock-project-graph.js +5 -2
- package/src/utils/mock-project-graph.js.map +1 -1
- package/src/utils/nx.d.ts +19 -0
- package/src/utils/nx.js +23 -0
- package/src/utils/nx.js.map +1 -1
- package/src/utils/open-api-codegen-target.d.ts +28 -0
- package/src/utils/open-api-codegen-target.js +31 -0
- package/src/utils/open-api-codegen-target.js.map +1 -0
- package/src/utils/rdb-constructs/rdb-constructs.js +2 -2
- package/src/utils/rdb-constructs/rdb-constructs.js.map +1 -1
- package/src/utils/website-constructs/website-constructs.js +2 -2
- package/src/utils/website-constructs/website-constructs.js.map +1 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/ import { getProjects, joinPathFragments, updateProjectConfiguration } from "@nx/devkit";
|
|
5
|
+
import { addLicenseCheckToLintTarget } from "../../../license/config.js";
|
|
6
|
+
import { TERRAFORM_FORMAT_TARGET, TERRAFORM_PROJECT_GENERATOR_INFO } from "../../../terraform/project/generator.js";
|
|
7
|
+
import { applyGritQL } from "../../../utils/ast.js";
|
|
8
|
+
import { formatFilesInSubtree } from "../../../utils/format.js";
|
|
9
|
+
import { sortObjectKeys } from "../../../utils/object.js";
|
|
10
|
+
/**
|
|
11
|
+
* Converges a Terraform project's format target on the shape ts and py projects
|
|
12
|
+
* carry: a `format` target that checks, a `fix` configuration that writes, and a
|
|
13
|
+
* `lint` target orchestrating it so `run-many --target lint` reaches Terraform
|
|
14
|
+
* projects with its `fix` and `skip-lint` configurations propagating.
|
|
15
|
+
*
|
|
16
|
+
* The target was named `fmt` and ran `terraform fmt`, which rewrites the files it
|
|
17
|
+
* reads. Those files are its own declared `inputs`, so every run changed the hash
|
|
18
|
+
* it had just been computed from and it could never cache-hit — Nx reported it
|
|
19
|
+
* flaky on every build. That per-run write is also what kept the vended
|
|
20
|
+
* `providers.tf` backend block aligned, so this realigns it.
|
|
21
|
+
*/ /** The command the base target ran before it checked rather than wrote. */ const WRITING_COMMAND = 'terraform fmt';
|
|
22
|
+
/**
|
|
23
|
+
* The `backend "s3"` arguments the generator vends. Realignment only applies to a
|
|
24
|
+
* block holding exactly these.
|
|
25
|
+
*/ const BACKEND_ARGUMENTS = [
|
|
26
|
+
'encrypt',
|
|
27
|
+
'use_lockfile'
|
|
28
|
+
];
|
|
29
|
+
/** Matches the arguments of a `backend "s3"` block, and their alignment. */ const BACKEND_BLOCK = /backend\s+"s3"\s*\{([^}]*)\}/;
|
|
30
|
+
const BACKEND_ARGUMENT = /^[ \t]*([A-Za-z_][A-Za-z0-9_]*)([ \t]*)=/gm;
|
|
31
|
+
/**
|
|
32
|
+
* The arguments a `providers.tf` declares in its `backend "s3"` block, in order,
|
|
33
|
+
* each with the column its `=` currently sits at.
|
|
34
|
+
*/ const readBackendArguments = (contents)=>{
|
|
35
|
+
const block = BACKEND_BLOCK.exec(contents)?.[1];
|
|
36
|
+
if (block === undefined) return undefined;
|
|
37
|
+
return [
|
|
38
|
+
...block.matchAll(BACKEND_ARGUMENT)
|
|
39
|
+
].map(([, name, gap])=>({
|
|
40
|
+
name,
|
|
41
|
+
column: name.length + gap.length
|
|
42
|
+
}));
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Realigns the `backend "s3"` arguments in an application's `providers.tf`, which
|
|
46
|
+
* the checking target requires and the per-run write had been supplying.
|
|
47
|
+
*
|
|
48
|
+
* `terraform fmt` aligns `=` to the widest key in the whole block, so a block
|
|
49
|
+
* carrying arguments beyond the two vended ones has a width this cannot know.
|
|
50
|
+
* Such a block is left alone and reported: rewriting it to the vended width
|
|
51
|
+
* would push those two out of alignment with the user's own arguments and fail
|
|
52
|
+
* the very check this migration installs.
|
|
53
|
+
*/ const realignProviders = async (tree, projectName, projectRoot, nextSteps)=>{
|
|
54
|
+
const filePath = joinPathFragments(projectRoot, 'src', 'providers.tf');
|
|
55
|
+
if (!tree.exists(filePath)) return;
|
|
56
|
+
const declared = readBackendArguments(tree.read(filePath, 'utf-8') ?? '');
|
|
57
|
+
if (!declared?.length) return;
|
|
58
|
+
const isVendedBlock = declared.length === BACKEND_ARGUMENTS.length && declared.every(({ name })=>BACKEND_ARGUMENTS.includes(name));
|
|
59
|
+
if (!isVendedBlock) {
|
|
60
|
+
nextSteps.push(`${filePath}: its \`backend "s3"\` block declares arguments beyond the generated ones, so its alignment was left as it is. Run \`nx run ${projectName}:format --configuration=fix\` if the new format check reports it.`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const width = Math.max(...BACKEND_ARGUMENTS.map((name)=>name.length));
|
|
64
|
+
// Already at the width `terraform fmt` aligns this block to, so a re-run — and
|
|
65
|
+
// a workspace whose file is formatted — is a no-op.
|
|
66
|
+
if (declared.every(({ column })=>column === width)) return;
|
|
67
|
+
// Each argument is realigned around whatever value it holds, so a value the
|
|
68
|
+
// user changed is preserved. An argument whose name already fills the width
|
|
69
|
+
// rewrites to a textually identical pattern, which GritQL notes; the rewrite
|
|
70
|
+
// still normalises the whitespace around its `=`, which is the point.
|
|
71
|
+
for (const { name } of declared){
|
|
72
|
+
await applyGritQL(tree, filePath, `language hcl\n\`${name} = $value\` => \`${name.padEnd(width)} = $value\`` + ` where { $value <: within \`backend "s3" { $_ }\` }`);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
/** The target name this migration converges on, matching ts and py projects. */ const FORMAT_TARGET = 'format';
|
|
76
|
+
/** The name the generator vended this target under before the rename. */ const LEGACY_TARGET = 'fmt';
|
|
77
|
+
/**
|
|
78
|
+
* Repoints `dependsOn` entries naming the old target at the new one. A stale
|
|
79
|
+
* entry is not an error in Nx — it silently drops the edge — so `build` would
|
|
80
|
+
* stop checking formatting altogether.
|
|
81
|
+
*/ const repointDependsOn = (project)=>{
|
|
82
|
+
let changed = false;
|
|
83
|
+
for (const target of Object.values(project.targets ?? {})){
|
|
84
|
+
const dependsOn = target.dependsOn;
|
|
85
|
+
if (!Array.isArray(dependsOn) || !dependsOn.includes(LEGACY_TARGET)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
target.dependsOn = dependsOn.map((entry)=>entry === LEGACY_TARGET ? FORMAT_TARGET : entry);
|
|
89
|
+
changed = true;
|
|
90
|
+
}
|
|
91
|
+
return changed;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Renames the `fmt` target to `format`, moves it onto the checking form, and
|
|
95
|
+
* adds the `lint` target that orchestrates it.
|
|
96
|
+
*
|
|
97
|
+
* A target whose command is not the one the generator produced is the user's, so
|
|
98
|
+
* it is reported instead. Both the vended name and the new one are accepted as
|
|
99
|
+
* the starting point, so a workspace part-way through converges either way.
|
|
100
|
+
*/ const migrateProject = (projectName, project, nextSteps)=>{
|
|
101
|
+
const targets = project.targets ?? {};
|
|
102
|
+
const existingName = targets[FORMAT_TARGET] ? FORMAT_TARGET : targets[LEGACY_TARGET] ? LEGACY_TARGET : undefined;
|
|
103
|
+
if (!existingName) return false;
|
|
104
|
+
const format = targets[existingName];
|
|
105
|
+
let changed = false;
|
|
106
|
+
if (format.options?.command === WRITING_COMMAND) {
|
|
107
|
+
targets[existingName] = {
|
|
108
|
+
...format,
|
|
109
|
+
inputs: TERRAFORM_FORMAT_TARGET.inputs,
|
|
110
|
+
options: {
|
|
111
|
+
...format.options,
|
|
112
|
+
command: TERRAFORM_FORMAT_TARGET.options.command
|
|
113
|
+
},
|
|
114
|
+
// The user's own configurations win, as their `options` do above: someone
|
|
115
|
+
// who set `fix` to `terraform fmt -recursive` to cover nested modules
|
|
116
|
+
// keeps it.
|
|
117
|
+
configurations: {
|
|
118
|
+
...TERRAFORM_FORMAT_TARGET.configurations,
|
|
119
|
+
...format.configurations
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
changed = true;
|
|
123
|
+
} else if (!format.configurations?.fix) {
|
|
124
|
+
nextSteps.push(`${projectName}: its '${existingName}' target has been customised, so it was left as it is. Have it run \`${TERRAFORM_FORMAT_TARGET.options.command}\` and move the writing form to a 'fix' configuration — writing from the base target rewrites the inputs its cache key is computed from, so it can never cache-hit.`);
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (existingName === LEGACY_TARGET) {
|
|
128
|
+
targets[FORMAT_TARGET] = targets[LEGACY_TARGET];
|
|
129
|
+
delete targets[LEGACY_TARGET];
|
|
130
|
+
changed = true;
|
|
131
|
+
}
|
|
132
|
+
changed = repointDependsOn(project) || changed;
|
|
133
|
+
if (!targets.lint) {
|
|
134
|
+
targets.lint = {
|
|
135
|
+
dependsOn: [
|
|
136
|
+
FORMAT_TARGET
|
|
137
|
+
]
|
|
138
|
+
};
|
|
139
|
+
changed = true;
|
|
140
|
+
}
|
|
141
|
+
return changed;
|
|
142
|
+
};
|
|
143
|
+
export default async function migration(tree) {
|
|
144
|
+
const nextSteps = [];
|
|
145
|
+
for (const [projectName, project] of getProjects(tree)){
|
|
146
|
+
const generator = project.metadata?.generator;
|
|
147
|
+
if (generator !== TERRAFORM_PROJECT_GENERATOR_INFO.id) continue;
|
|
148
|
+
if (migrateProject(projectName, project, nextSteps)) {
|
|
149
|
+
updateProjectConfiguration(tree, projectName, {
|
|
150
|
+
...project,
|
|
151
|
+
targets: sortObjectKeys(project.targets)
|
|
152
|
+
});
|
|
153
|
+
// The new `lint` target checks licenses alongside formatting, as every
|
|
154
|
+
// other linting project's does in a workspace that has run `license`.
|
|
155
|
+
addLicenseCheckToLintTarget(tree, projectName);
|
|
156
|
+
}
|
|
157
|
+
await realignProviders(tree, projectName, project.root, nextSteps);
|
|
158
|
+
}
|
|
159
|
+
await formatFilesInSubtree(tree);
|
|
160
|
+
return {
|
|
161
|
+
nextSteps
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
//# sourceMappingURL=migration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/terraform-fmt-check/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n getProjects,\n joinPathFragments,\n type MigrationReturnObject,\n type ProjectConfiguration,\n type Tree,\n updateProjectConfiguration,\n} from '@nx/devkit';\nimport { addLicenseCheckToLintTarget } from '../../../license/config.js';\nimport {\n TERRAFORM_FORMAT_TARGET,\n TERRAFORM_PROJECT_GENERATOR_INFO,\n} from '../../../terraform/project/generator.js';\nimport { applyGritQL } from '../../../utils/ast.js';\nimport { formatFilesInSubtree } from '../../../utils/format.js';\nimport { sortObjectKeys } from '../../../utils/object.js';\n\n/**\n * Converges a Terraform project's format target on the shape ts and py projects\n * carry: a `format` target that checks, a `fix` configuration that writes, and a\n * `lint` target orchestrating it so `run-many --target lint` reaches Terraform\n * projects with its `fix` and `skip-lint` configurations propagating.\n *\n * The target was named `fmt` and ran `terraform fmt`, which rewrites the files it\n * reads. Those files are its own declared `inputs`, so every run changed the hash\n * it had just been computed from and it could never cache-hit — Nx reported it\n * flaky on every build. That per-run write is also what kept the vended\n * `providers.tf` backend block aligned, so this realigns it.\n */\n\n/** The command the base target ran before it checked rather than wrote. */\nconst WRITING_COMMAND = 'terraform fmt';\n\n/**\n * The `backend \"s3\"` arguments the generator vends. Realignment only applies to a\n * block holding exactly these.\n */\nconst BACKEND_ARGUMENTS = ['encrypt', 'use_lockfile'];\n\n/** Matches the arguments of a `backend \"s3\"` block, and their alignment. */\nconst BACKEND_BLOCK = /backend\\s+\"s3\"\\s*\\{([^}]*)\\}/;\nconst BACKEND_ARGUMENT = /^[ \\t]*([A-Za-z_][A-Za-z0-9_]*)([ \\t]*)=/gm;\n\n/**\n * The arguments a `providers.tf` declares in its `backend \"s3\"` block, in order,\n * each with the column its `=` currently sits at.\n */\nconst readBackendArguments = (\n contents: string,\n): { name: string; column: number }[] | undefined => {\n const block = BACKEND_BLOCK.exec(contents)?.[1];\n if (block === undefined) return undefined;\n return [...block.matchAll(BACKEND_ARGUMENT)].map(([, name, gap]) => ({\n name,\n column: name.length + gap.length,\n }));\n};\n\n/**\n * Realigns the `backend \"s3\"` arguments in an application's `providers.tf`, which\n * the checking target requires and the per-run write had been supplying.\n *\n * `terraform fmt` aligns `=` to the widest key in the whole block, so a block\n * carrying arguments beyond the two vended ones has a width this cannot know.\n * Such a block is left alone and reported: rewriting it to the vended width\n * would push those two out of alignment with the user's own arguments and fail\n * the very check this migration installs.\n */\nconst realignProviders = async (\n tree: Tree,\n projectName: string,\n projectRoot: string,\n nextSteps: string[],\n): Promise<void> => {\n const filePath = joinPathFragments(projectRoot, 'src', 'providers.tf');\n if (!tree.exists(filePath)) return;\n\n const declared = readBackendArguments(tree.read(filePath, 'utf-8') ?? '');\n if (!declared?.length) return;\n\n const isVendedBlock =\n declared.length === BACKEND_ARGUMENTS.length &&\n declared.every(({ name }) => BACKEND_ARGUMENTS.includes(name));\n if (!isVendedBlock) {\n nextSteps.push(\n `${filePath}: its \\`backend \"s3\"\\` block declares arguments beyond the generated ones, so its alignment was left as it is. Run \\`nx run ${projectName}:format --configuration=fix\\` if the new format check reports it.`,\n );\n return;\n }\n\n const width = Math.max(...BACKEND_ARGUMENTS.map((name) => name.length));\n\n // Already at the width `terraform fmt` aligns this block to, so a re-run — and\n // a workspace whose file is formatted — is a no-op.\n if (declared.every(({ column }) => column === width)) return;\n\n // Each argument is realigned around whatever value it holds, so a value the\n // user changed is preserved. An argument whose name already fills the width\n // rewrites to a textually identical pattern, which GritQL notes; the rewrite\n // still normalises the whitespace around its `=`, which is the point.\n for (const { name } of declared) {\n await applyGritQL(\n tree,\n filePath,\n `language hcl\\n\\`${name} = $value\\` => \\`${name.padEnd(width)} = $value\\`` +\n ` where { $value <: within \\`backend \"s3\" { $_ }\\` }`,\n );\n }\n};\n\n/** The target name this migration converges on, matching ts and py projects. */\nconst FORMAT_TARGET = 'format';\n/** The name the generator vended this target under before the rename. */\nconst LEGACY_TARGET = 'fmt';\n\n/**\n * Repoints `dependsOn` entries naming the old target at the new one. A stale\n * entry is not an error in Nx — it silently drops the edge — so `build` would\n * stop checking formatting altogether.\n */\nconst repointDependsOn = (project: ProjectConfiguration): boolean => {\n let changed = false;\n for (const target of Object.values(project.targets ?? {})) {\n const dependsOn = target.dependsOn;\n if (!Array.isArray(dependsOn) || !dependsOn.includes(LEGACY_TARGET)) {\n continue;\n }\n target.dependsOn = dependsOn.map((entry) =>\n entry === LEGACY_TARGET ? FORMAT_TARGET : entry,\n );\n changed = true;\n }\n return changed;\n};\n\n/**\n * Renames the `fmt` target to `format`, moves it onto the checking form, and\n * adds the `lint` target that orchestrates it.\n *\n * A target whose command is not the one the generator produced is the user's, so\n * it is reported instead. Both the vended name and the new one are accepted as\n * the starting point, so a workspace part-way through converges either way.\n */\nconst migrateProject = (\n projectName: string,\n project: ProjectConfiguration,\n nextSteps: string[],\n): boolean => {\n const targets = project.targets ?? {};\n const existingName = targets[FORMAT_TARGET]\n ? FORMAT_TARGET\n : targets[LEGACY_TARGET]\n ? LEGACY_TARGET\n : undefined;\n if (!existingName) return false;\n\n const format = targets[existingName];\n let changed = false;\n\n if (format.options?.command === WRITING_COMMAND) {\n targets[existingName] = {\n ...format,\n inputs: TERRAFORM_FORMAT_TARGET.inputs,\n options: {\n ...format.options,\n command: TERRAFORM_FORMAT_TARGET.options.command,\n },\n // The user's own configurations win, as their `options` do above: someone\n // who set `fix` to `terraform fmt -recursive` to cover nested modules\n // keeps it.\n configurations: {\n ...TERRAFORM_FORMAT_TARGET.configurations,\n ...format.configurations,\n },\n };\n changed = true;\n } else if (!format.configurations?.fix) {\n nextSteps.push(\n `${projectName}: its '${existingName}' target has been customised, so it was left as it is. Have it run \\`${TERRAFORM_FORMAT_TARGET.options.command}\\` and move the writing form to a 'fix' configuration — writing from the base target rewrites the inputs its cache key is computed from, so it can never cache-hit.`,\n );\n return false;\n }\n\n if (existingName === LEGACY_TARGET) {\n targets[FORMAT_TARGET] = targets[LEGACY_TARGET];\n delete targets[LEGACY_TARGET];\n changed = true;\n }\n\n changed = repointDependsOn(project) || changed;\n\n if (!targets.lint) {\n targets.lint = { dependsOn: [FORMAT_TARGET] };\n changed = true;\n }\n\n return changed;\n};\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n for (const [projectName, project] of getProjects(tree)) {\n const generator = (project.metadata as { generator?: string } | undefined)\n ?.generator;\n if (generator !== TERRAFORM_PROJECT_GENERATOR_INFO.id) continue;\n\n if (migrateProject(projectName, project, nextSteps)) {\n updateProjectConfiguration(tree, projectName, {\n ...project,\n targets: sortObjectKeys(project.targets),\n });\n // The new `lint` target checks licenses alongside formatting, as every\n // other linting project's does in a workspace that has run `license`.\n addLicenseCheckToLintTarget(tree, projectName);\n }\n\n await realignProviders(tree, projectName, project.root, nextSteps);\n }\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["getProjects","joinPathFragments","updateProjectConfiguration","addLicenseCheckToLintTarget","TERRAFORM_FORMAT_TARGET","TERRAFORM_PROJECT_GENERATOR_INFO","applyGritQL","formatFilesInSubtree","sortObjectKeys","WRITING_COMMAND","BACKEND_ARGUMENTS","BACKEND_BLOCK","BACKEND_ARGUMENT","readBackendArguments","contents","block","exec","undefined","matchAll","map","name","gap","column","length","realignProviders","tree","projectName","projectRoot","nextSteps","filePath","exists","declared","read","isVendedBlock","every","includes","push","width","Math","max","padEnd","FORMAT_TARGET","LEGACY_TARGET","repointDependsOn","project","changed","target","Object","values","targets","dependsOn","Array","isArray","entry","migrateProject","existingName","format","options","command","inputs","configurations","fix","lint","migration","generator","metadata","id","root"],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { type MigrationReturnObject, type Tree } from '@nx/devkit';
|
|
6
|
+
export default function migration(tree: Tree): Promise<MigrationReturnObject>;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/ import { generateFiles, getProjects, joinPathFragments, OverwriteStrategy, updateProjectConfiguration } from "@nx/devkit";
|
|
5
|
+
import { relative } from "path";
|
|
6
|
+
import { TERRAFORM_PROJECT_GENERATOR_INFO } from "../../../terraform/project/generator.js";
|
|
7
|
+
import { addDestructuredImport, applyGritQL, matchGritQL } from "../../../utils/ast.js";
|
|
8
|
+
import { formatFilesInSubtree } from "../../../utils/format.js";
|
|
9
|
+
import { kebabCase } from "../../../utils/names.js";
|
|
10
|
+
/**
|
|
11
|
+
* Terraform projects share their provider downloads through
|
|
12
|
+
* `TF_PLUGIN_CACHE_DIR`, so a `terraform init` whose `.terraform` was cleaned
|
|
13
|
+
* links the providers already on disk instead of re-downloading them. The AWS
|
|
14
|
+
* provider alone is several hundred megabytes, which dominated the `test`
|
|
15
|
+
* target's runtime on every cache miss.
|
|
16
|
+
*
|
|
17
|
+
* The cache lives under the already-gitignored `.terraform` at the workspace
|
|
18
|
+
* root, one directory per project. Terraform reports an error and silently falls
|
|
19
|
+
* back to downloading when the directory does not exist, so each target creates
|
|
20
|
+
* it first, and the vended `init` script gets an `env` helper that does the same.
|
|
21
|
+
*
|
|
22
|
+
* A directory per project rather than one shared: two `terraform init` runs
|
|
23
|
+
* filling one cache concurrently each compute a different hash for the same
|
|
24
|
+
* provider, and terraform then rejects the mismatch against the lock file.
|
|
25
|
+
*
|
|
26
|
+
* Nx does not interpolate `{workspaceRoot}` inside `env`, so the value is
|
|
27
|
+
* relative to the target's own `cwd` of `{projectRoot}/src`.
|
|
28
|
+
*/ const TF_PLUGIN_CACHE_DIR = 'TF_PLUGIN_CACHE_DIR';
|
|
29
|
+
const divergedStep = (projectName, targetName)=>`${projectName}: its '${targetName}' target no longer matches the shape the generator produced - left untouched. To reuse provider downloads, set the \`TF_PLUGIN_CACHE_DIR\` env var to \`.terraform/plugin-cache/<projectRoot>\` under the workspace root (relative to the target's \`cwd\`), creating that directory before \`terraform init\` runs.`;
|
|
30
|
+
/** Path to this project's cache from a target running in `{projectRoot}/src`. */ const pluginCacheDirFor = (projectRoot)=>joinPathFragments(relative(joinPathFragments(projectRoot, 'src'), '.') || '.', '.terraform', 'plugin-cache', '{projectRoot}').replace(/\\/g, '/');
|
|
31
|
+
/** Whether a target already runs the command, however it is expressed. */ const hasCommand = (commands, predicate)=>Array.isArray(commands) && commands.some((c)=>{
|
|
32
|
+
const command = typeof c === 'string' ? c : c?.command;
|
|
33
|
+
return typeof command === 'string' && predicate(command);
|
|
34
|
+
});
|
|
35
|
+
/**
|
|
36
|
+
* Prepend the `make-dir` that creates the cache directory. `forwardAllArgs` is
|
|
37
|
+
* off for it alone, so args meant for `terraform` are not passed to `make-dir`.
|
|
38
|
+
*/ const withMakeDir = (commands, pluginCacheDir)=>[
|
|
39
|
+
{
|
|
40
|
+
command: `make-dir ${pluginCacheDir}`,
|
|
41
|
+
forwardAllArgs: false
|
|
42
|
+
},
|
|
43
|
+
...commands
|
|
44
|
+
];
|
|
45
|
+
/**
|
|
46
|
+
* The `terraform init` call in the vended `init` script, matched structurally so
|
|
47
|
+
* formatting and argument layout don't affect whether it is recognised.
|
|
48
|
+
*/ const INIT_CALL = "`execFileSync('terraform', [$args], { cwd: srcDir, stdio: 'inherit' })`";
|
|
49
|
+
const INIT_CALL_WITH_ENV = "`execFileSync('terraform', [$args], { cwd: srcDir, stdio: 'inherit', env: pluginCacheEnv(projectRootRel) })`";
|
|
50
|
+
const divergedScriptStep = (filePath)=>`${filePath}: its \`terraform init\` call no longer matches the shape the generator produced - left untouched. Pass \`env: pluginCacheEnv(projectRootRel)\` to that \`execFileSync\` so it shares the provider cache in \`scripts/env.ts\`.`;
|
|
51
|
+
/**
|
|
52
|
+
* Route the vended `init` script's `terraform init` through the shared cache.
|
|
53
|
+
*
|
|
54
|
+
* Guarded on the helper not already being imported, so a re-run — and a project
|
|
55
|
+
* generated with the cache — is a no-op.
|
|
56
|
+
*/ const migrateInitScript = async (tree, filePath, nextSteps)=>{
|
|
57
|
+
if (!tree.exists(filePath)) return;
|
|
58
|
+
if (await matchGritQL(tree, filePath, '`pluginCacheEnv($_)`')) return;
|
|
59
|
+
if (!await matchGritQL(tree, filePath, INIT_CALL)) {
|
|
60
|
+
nextSteps.push(divergedScriptStep(filePath));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
await applyGritQL(tree, filePath, `${INIT_CALL} => ${INIT_CALL_WITH_ENV}`);
|
|
64
|
+
// Placed in the generator's import position rather than prepended, so a
|
|
65
|
+
// migrated workspace matches a freshly generated one.
|
|
66
|
+
const awsConfigImport = "`import { resolveAwsConfig } from './aws-config'`";
|
|
67
|
+
if (await matchGritQL(tree, filePath, awsConfigImport)) {
|
|
68
|
+
await applyGritQL(tree, filePath, `${awsConfigImport} => \`import { resolveAwsConfig } from './aws-config';\nimport { pluginCacheEnv } from './env'\``);
|
|
69
|
+
} else {
|
|
70
|
+
await addDestructuredImport(tree, filePath, [
|
|
71
|
+
'pluginCacheEnv'
|
|
72
|
+
], './env');
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
export default async function migration(tree) {
|
|
76
|
+
const nextSteps = [];
|
|
77
|
+
for (const [projectName, project] of getProjects(tree)){
|
|
78
|
+
const generator = project.metadata?.generator;
|
|
79
|
+
if (generator !== TERRAFORM_PROJECT_GENERATOR_INFO.id) continue;
|
|
80
|
+
const targets = project.targets ?? {};
|
|
81
|
+
const pluginCacheDir = pluginCacheDirFor(project.root);
|
|
82
|
+
let changed = false;
|
|
83
|
+
/**
|
|
84
|
+
* Wire the cache into a target that runs `terraform init`. Only the exact
|
|
85
|
+
* shape the generator produced is rewritten; anything else is the user's and
|
|
86
|
+
* is reported instead. Guarded on the env var being absent, so a re-run —
|
|
87
|
+
* and a project already generated with the cache — is a no-op.
|
|
88
|
+
*/ const migrateTarget = (targetName, target, /** Where the commands live: on the target, or under a configuration. */ commandsHolder, env)=>{
|
|
89
|
+
if (!target) return;
|
|
90
|
+
if (env?.[TF_PLUGIN_CACHE_DIR]) return;
|
|
91
|
+
if (!hasCommand(commandsHolder.commands, (c)=>c.includes('terraform init'))) {
|
|
92
|
+
nextSteps.push(divergedStep(projectName, targetName));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
commandsHolder.commands = withMakeDir(commandsHolder.commands, pluginCacheDir);
|
|
96
|
+
target.options = {
|
|
97
|
+
...target.options,
|
|
98
|
+
parallel: false,
|
|
99
|
+
env: {
|
|
100
|
+
...target.options?.env,
|
|
101
|
+
[TF_PLUGIN_CACHE_DIR]: pluginCacheDir
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
changed = true;
|
|
105
|
+
};
|
|
106
|
+
// `test` carries its `terraform init -backend=false` on the target itself.
|
|
107
|
+
const test = targets.test;
|
|
108
|
+
if (test?.options) {
|
|
109
|
+
const wasMigrated = test.options.env?.[TF_PLUGIN_CACHE_DIR] !== undefined;
|
|
110
|
+
migrateTarget('test', test, test.options, test.options.env);
|
|
111
|
+
// The data dir becomes symlinks into the shared cache, so it is no longer
|
|
112
|
+
// a self-contained artifact worth restoring on another machine. Only the
|
|
113
|
+
// exact output the generator declared is dropped.
|
|
114
|
+
if (!wasMigrated && test.outputs?.length === 1 && test.outputs[0] === '{workspaceRoot}/dist/{projectRoot}/terraform-test') {
|
|
115
|
+
test.outputs = [];
|
|
116
|
+
changed = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// `init` carries its command under the `dev` configuration. A library's
|
|
120
|
+
// runs `terraform init` directly; an application's delegates to the vended
|
|
121
|
+
// script, which reads the cache from its own helper rather than the target.
|
|
122
|
+
const init = targets.init;
|
|
123
|
+
const initDev = init?.configurations?.dev;
|
|
124
|
+
if (init?.options && initDev) {
|
|
125
|
+
const isScriptDriven = hasCommand(initDev.commands ?? init.options.commands, (c)=>c.includes('scripts/init.ts'));
|
|
126
|
+
if (!isScriptDriven) {
|
|
127
|
+
// The pre-fix generator vended a single `command`; normalise it to the
|
|
128
|
+
// `commands` array `make-dir` has to be prepended to.
|
|
129
|
+
if (typeof initDev.command === 'string' && !initDev.commands) {
|
|
130
|
+
initDev.commands = [
|
|
131
|
+
initDev.command
|
|
132
|
+
];
|
|
133
|
+
delete initDev.command;
|
|
134
|
+
}
|
|
135
|
+
migrateTarget('init', init, initDev, init.options.env);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (changed) {
|
|
139
|
+
updateProjectConfiguration(tree, projectName, {
|
|
140
|
+
...project,
|
|
141
|
+
targets
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
// An application's `init` target runs `terraform init` from the vended
|
|
145
|
+
// script rather than the target, so the cache reaches it via the `env`
|
|
146
|
+
// helper. The helper is new, so KeepExisting adds it while leaving a user's
|
|
147
|
+
// own copy alone; the script that calls it already exists, so it is
|
|
148
|
+
// rewritten below.
|
|
149
|
+
if (tree.exists(joinPathFragments(project.root, 'bootstrap'))) {
|
|
150
|
+
generateFiles(tree, joinPathFragments(import.meta.dirname, '../../../terraform/project/files/application/scripts'), joinPathFragments(project.root, 'scripts'), {
|
|
151
|
+
stateKeyPrefix: kebabCase(projectName)
|
|
152
|
+
}, {
|
|
153
|
+
overwriteStrategy: OverwriteStrategy.KeepExisting
|
|
154
|
+
});
|
|
155
|
+
await migrateInitScript(tree, joinPathFragments(project.root, 'scripts', 'init.ts'), nextSteps);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
await formatFilesInSubtree(tree);
|
|
159
|
+
return {
|
|
160
|
+
nextSteps
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
//# sourceMappingURL=migration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/terraform-provider-plugin-cache/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n generateFiles,\n getProjects,\n joinPathFragments,\n type MigrationReturnObject,\n OverwriteStrategy,\n type TargetConfiguration,\n type Tree,\n updateProjectConfiguration,\n} from '@nx/devkit';\nimport { relative } from 'path';\nimport { TERRAFORM_PROJECT_GENERATOR_INFO } from '../../../terraform/project/generator.js';\nimport {\n addDestructuredImport,\n applyGritQL,\n matchGritQL,\n} from '../../../utils/ast.js';\nimport { formatFilesInSubtree } from '../../../utils/format.js';\nimport { kebabCase } from '../../../utils/names.js';\n\n/**\n * Terraform projects share their provider downloads through\n * `TF_PLUGIN_CACHE_DIR`, so a `terraform init` whose `.terraform` was cleaned\n * links the providers already on disk instead of re-downloading them. The AWS\n * provider alone is several hundred megabytes, which dominated the `test`\n * target's runtime on every cache miss.\n *\n * The cache lives under the already-gitignored `.terraform` at the workspace\n * root, one directory per project. Terraform reports an error and silently falls\n * back to downloading when the directory does not exist, so each target creates\n * it first, and the vended `init` script gets an `env` helper that does the same.\n *\n * A directory per project rather than one shared: two `terraform init` runs\n * filling one cache concurrently each compute a different hash for the same\n * provider, and terraform then rejects the mismatch against the lock file.\n *\n * Nx does not interpolate `{workspaceRoot}` inside `env`, so the value is\n * relative to the target's own `cwd` of `{projectRoot}/src`.\n */\n\nconst TF_PLUGIN_CACHE_DIR = 'TF_PLUGIN_CACHE_DIR';\n\nconst divergedStep = (projectName: string, targetName: string) =>\n `${projectName}: its '${targetName}' target no longer matches the shape the generator produced - left untouched. To reuse provider downloads, set the \\`TF_PLUGIN_CACHE_DIR\\` env var to \\`.terraform/plugin-cache/<projectRoot>\\` under the workspace root (relative to the target's \\`cwd\\`), creating that directory before \\`terraform init\\` runs.`;\n\n/** Path to this project's cache from a target running in `{projectRoot}/src`. */\nconst pluginCacheDirFor = (projectRoot: string) =>\n joinPathFragments(\n relative(joinPathFragments(projectRoot, 'src'), '.') || '.',\n '.terraform',\n 'plugin-cache',\n '{projectRoot}',\n ).replace(/\\\\/g, '/');\n\n/** Whether a target already runs the command, however it is expressed. */\nconst hasCommand = (\n commands: unknown,\n predicate: (command: string) => boolean,\n): boolean =>\n Array.isArray(commands) &&\n commands.some((c) => {\n const command = typeof c === 'string' ? c : (c?.command as string);\n return typeof command === 'string' && predicate(command);\n });\n\n/**\n * Prepend the `make-dir` that creates the cache directory. `forwardAllArgs` is\n * off for it alone, so args meant for `terraform` are not passed to `make-dir`.\n */\nconst withMakeDir = (commands: unknown[], pluginCacheDir: string) => [\n { command: `make-dir ${pluginCacheDir}`, forwardAllArgs: false },\n ...commands,\n];\n\n/**\n * The `terraform init` call in the vended `init` script, matched structurally so\n * formatting and argument layout don't affect whether it is recognised.\n */\nconst INIT_CALL =\n \"`execFileSync('terraform', [$args], { cwd: srcDir, stdio: 'inherit' })`\";\nconst INIT_CALL_WITH_ENV =\n \"`execFileSync('terraform', [$args], { cwd: srcDir, stdio: 'inherit', env: pluginCacheEnv(projectRootRel) })`\";\n\nconst divergedScriptStep = (filePath: string) =>\n `${filePath}: its \\`terraform init\\` call no longer matches the shape the generator produced - left untouched. Pass \\`env: pluginCacheEnv(projectRootRel)\\` to that \\`execFileSync\\` so it shares the provider cache in \\`scripts/env.ts\\`.`;\n\n/**\n * Route the vended `init` script's `terraform init` through the shared cache.\n *\n * Guarded on the helper not already being imported, so a re-run — and a project\n * generated with the cache — is a no-op.\n */\nconst migrateInitScript = async (\n tree: Tree,\n filePath: string,\n nextSteps: string[],\n): Promise<void> => {\n if (!tree.exists(filePath)) return;\n if (await matchGritQL(tree, filePath, '`pluginCacheEnv($_)`')) return;\n\n if (!(await matchGritQL(tree, filePath, INIT_CALL))) {\n nextSteps.push(divergedScriptStep(filePath));\n return;\n }\n\n await applyGritQL(tree, filePath, `${INIT_CALL} => ${INIT_CALL_WITH_ENV}`);\n\n // Placed in the generator's import position rather than prepended, so a\n // migrated workspace matches a freshly generated one.\n const awsConfigImport = \"`import { resolveAwsConfig } from './aws-config'`\";\n if (await matchGritQL(tree, filePath, awsConfigImport)) {\n await applyGritQL(\n tree,\n filePath,\n `${awsConfigImport} => \\`import { resolveAwsConfig } from './aws-config';\\nimport { pluginCacheEnv } from './env'\\``,\n );\n } else {\n await addDestructuredImport(tree, filePath, ['pluginCacheEnv'], './env');\n }\n};\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n for (const [projectName, project] of getProjects(tree)) {\n const generator = (project.metadata as { generator?: string } | undefined)\n ?.generator;\n if (generator !== TERRAFORM_PROJECT_GENERATOR_INFO.id) continue;\n\n const targets = project.targets ?? {};\n const pluginCacheDir = pluginCacheDirFor(project.root);\n let changed = false;\n\n /**\n * Wire the cache into a target that runs `terraform init`. Only the exact\n * shape the generator produced is rewritten; anything else is the user's and\n * is reported instead. Guarded on the env var being absent, so a re-run —\n * and a project already generated with the cache — is a no-op.\n */\n const migrateTarget = (\n targetName: string,\n target: TargetConfiguration | undefined,\n /** Where the commands live: on the target, or under a configuration. */\n commandsHolder: { commands?: unknown; env?: Record<string, string> },\n env: Record<string, string> | undefined,\n ) => {\n if (!target) return;\n if (env?.[TF_PLUGIN_CACHE_DIR]) return;\n\n if (\n !hasCommand(commandsHolder.commands, (c) =>\n c.includes('terraform init'),\n )\n ) {\n nextSteps.push(divergedStep(projectName, targetName));\n return;\n }\n\n commandsHolder.commands = withMakeDir(\n commandsHolder.commands as unknown[],\n pluginCacheDir,\n );\n target.options = {\n ...target.options,\n parallel: false,\n env: { ...target.options?.env, [TF_PLUGIN_CACHE_DIR]: pluginCacheDir },\n };\n changed = true;\n };\n\n // `test` carries its `terraform init -backend=false` on the target itself.\n const test = targets.test;\n if (test?.options) {\n const wasMigrated = test.options.env?.[TF_PLUGIN_CACHE_DIR] !== undefined;\n migrateTarget('test', test, test.options, test.options.env);\n\n // The data dir becomes symlinks into the shared cache, so it is no longer\n // a self-contained artifact worth restoring on another machine. Only the\n // exact output the generator declared is dropped.\n if (\n !wasMigrated &&\n test.outputs?.length === 1 &&\n test.outputs[0] === '{workspaceRoot}/dist/{projectRoot}/terraform-test'\n ) {\n test.outputs = [];\n changed = true;\n }\n }\n\n // `init` carries its command under the `dev` configuration. A library's\n // runs `terraform init` directly; an application's delegates to the vended\n // script, which reads the cache from its own helper rather than the target.\n const init = targets.init;\n const initDev = init?.configurations?.dev;\n if (init?.options && initDev) {\n const isScriptDriven = hasCommand(\n initDev.commands ?? init.options.commands,\n (c) => c.includes('scripts/init.ts'),\n );\n if (!isScriptDriven) {\n // The pre-fix generator vended a single `command`; normalise it to the\n // `commands` array `make-dir` has to be prepended to.\n if (typeof initDev.command === 'string' && !initDev.commands) {\n initDev.commands = [initDev.command];\n delete initDev.command;\n }\n migrateTarget('init', init, initDev, init.options.env);\n }\n }\n\n if (changed) {\n updateProjectConfiguration(tree, projectName, { ...project, targets });\n }\n\n // An application's `init` target runs `terraform init` from the vended\n // script rather than the target, so the cache reaches it via the `env`\n // helper. The helper is new, so KeepExisting adds it while leaving a user's\n // own copy alone; the script that calls it already exists, so it is\n // rewritten below.\n if (tree.exists(joinPathFragments(project.root, 'bootstrap'))) {\n generateFiles(\n tree,\n joinPathFragments(\n import.meta.dirname,\n '../../../terraform/project/files/application/scripts',\n ),\n joinPathFragments(project.root, 'scripts'),\n { stateKeyPrefix: kebabCase(projectName) },\n { overwriteStrategy: OverwriteStrategy.KeepExisting },\n );\n\n await migrateInitScript(\n tree,\n joinPathFragments(project.root, 'scripts', 'init.ts'),\n nextSteps,\n );\n }\n }\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["generateFiles","getProjects","joinPathFragments","OverwriteStrategy","updateProjectConfiguration","relative","TERRAFORM_PROJECT_GENERATOR_INFO","addDestructuredImport","applyGritQL","matchGritQL","formatFilesInSubtree","kebabCase","TF_PLUGIN_CACHE_DIR","divergedStep","projectName","targetName","pluginCacheDirFor","projectRoot","replace","hasCommand","commands","predicate","Array","isArray","some","c","command","withMakeDir","pluginCacheDir","forwardAllArgs","INIT_CALL","INIT_CALL_WITH_ENV","divergedScriptStep","filePath","migrateInitScript","tree","nextSteps","exists","push","awsConfigImport","migration","project","generator","metadata","id","targets","root","changed","migrateTarget","target","commandsHolder","env","includes","options","parallel","test","wasMigrated","undefined","outputs","length","init","initDev","configurations","dev","isScriptDriven","dirname","stateKeyPrefix","overwriteStrategy","KeepExisting"],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { type MigrationReturnObject, type Tree } from '@nx/devkit';
|
|
6
|
+
export default function migration(tree: Tree): Promise<MigrationReturnObject>;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/ import { getProjects, joinPathFragments, updateJson } from "@nx/devkit";
|
|
5
|
+
import { isAbsolute } from "path";
|
|
6
|
+
import { GENERATED_REPORTS_DIRECTORY, getCoverageReportsDirectory } from "../../../ts/lib/vitest.js";
|
|
7
|
+
import { applyGritQL, matchGritQL } from "../../../utils/ast.js";
|
|
8
|
+
import { BIOME_TEST_OUTPUT_EXCLUDE, formatFilesInSubtree } from "../../../utils/format.js";
|
|
9
|
+
import { updateGitIgnore } from "../../../utils/git.js";
|
|
10
|
+
/**
|
|
11
|
+
* Point each project's vitest coverage directory at the workspace's `dist`.
|
|
12
|
+
*
|
|
13
|
+
* `@nx/vitest` writes `reportsDirectory: './test-output/vitest/coverage'`, which
|
|
14
|
+
* resolves inside the project. Coverage is off by default, so the directory stays
|
|
15
|
+
* dormant until someone runs `vitest --coverage` — at which point the HTML
|
|
16
|
+
* reporter's bundled third-party scripts land in the project, where the `format`
|
|
17
|
+
* target reformats them (failing every later build) and the `default` named input
|
|
18
|
+
* counts them, so no task in the project can be cached again.
|
|
19
|
+
*
|
|
20
|
+
* Alongside the rewrite, the coverage directory an older release already wrote
|
|
21
|
+
* into the project is deleted and `test-output` is added to the workspace
|
|
22
|
+
* `.gitignore`, so the reporter's vendored third-party scripts can neither be
|
|
23
|
+
* committed nor picked up by `license#sync`.
|
|
24
|
+
*
|
|
25
|
+
* Guardrails:
|
|
26
|
+
* - The pattern is anchored to the exact literal `@nx/vitest` generated, and
|
|
27
|
+
* scoped to the `coverage` block of the config's own `test` object, so a
|
|
28
|
+
* `reportsDirectory` elsewhere in the file is left alone.
|
|
29
|
+
* - A project whose directory has been repointed keeps its value, and is reported
|
|
30
|
+
* via `nextSteps` instead — as is one whose directory is set on a target, or a
|
|
31
|
+
* `biome.json` whose `files.includes` has diverged from the vended list.
|
|
32
|
+
* - Idempotent: the pattern no longer matches once rewritten.
|
|
33
|
+
*/ /**
|
|
34
|
+
* The config filenames to check. Generated projects carry the `.mts` forms; the
|
|
35
|
+
* other extensions vitest supports are included so a project whose config was
|
|
36
|
+
* renamed is still migrated. Safe to widen, because the pattern matched inside is
|
|
37
|
+
* anchored to the exact literal `@nx/vitest` generated.
|
|
38
|
+
*/ const CONFIG_FILES = [
|
|
39
|
+
'vitest.config.mts',
|
|
40
|
+
'vitest.config.ts',
|
|
41
|
+
'vitest.config.mjs',
|
|
42
|
+
'vitest.config.js',
|
|
43
|
+
'vite.config.mts',
|
|
44
|
+
'vite.config.ts',
|
|
45
|
+
'vite.config.mjs',
|
|
46
|
+
'vite.config.js'
|
|
47
|
+
];
|
|
48
|
+
/**
|
|
49
|
+
* The `coverage` block of the object the config factory returns, scoped so a
|
|
50
|
+
* `coverage` option elsewhere in the file is not matched.
|
|
51
|
+
*/ const coveragePattern = (body)=>`\`test: { $props }\` where {
|
|
52
|
+
$props <: within \`defineConfig($_)\`,
|
|
53
|
+
$props <: some \`coverage: { $cov }\`,
|
|
54
|
+
${body}
|
|
55
|
+
}`;
|
|
56
|
+
/** Matches a coverage block still carrying the directory `@nx/vitest` generated. */ const GENERATED_DIRECTORY_PATTERN = coveragePattern(`$cov <: some \`reportsDirectory: '${GENERATED_REPORTS_DIRECTORY}'\``);
|
|
57
|
+
/** Matches a coverage block that declares a `reportsDirectory` at all. */ const ANY_DIRECTORY_PATTERN = coveragePattern('$cov <: some `reportsDirectory: $_`');
|
|
58
|
+
/** Rewrites the generated directory to the one under `dist` for a project root. */ const rewriteDirectoryPattern = (projectRoot)=>coveragePattern(`$cov <: some \`reportsDirectory: '${GENERATED_REPORTS_DIRECTORY}'\` as $reportsDirectory,
|
|
59
|
+
$reportsDirectory => \`reportsDirectory: '${getCoverageReportsDirectory(projectRoot)}'\``);
|
|
60
|
+
/** Matches a coverage block already pointing at the directory under `dist`. */ const migratedDirectoryPattern = (projectRoot)=>coveragePattern(`$cov <: some \`reportsDirectory: '${getCoverageReportsDirectory(projectRoot)}'\``);
|
|
61
|
+
/** The directory an older release wrote coverage into, relative to a project. */ const OLD_COVERAGE_DIRECTORY = 'test-output';
|
|
62
|
+
/**
|
|
63
|
+
* Exclude test reports from Biome, so a coverage directory pointed anywhere
|
|
64
|
+
* inside the workspace is still never formatted or linted. Anchored to the
|
|
65
|
+
* excludes we vend, so a workspace that has rewritten `files.includes` keeps its
|
|
66
|
+
* own list — and is told to add the exclude itself.
|
|
67
|
+
*/ const excludeTestOutputFromBiome = (tree)=>{
|
|
68
|
+
if (!tree.exists('biome.json')) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
let unrecognised = false;
|
|
72
|
+
updateJson(tree, 'biome.json', (biome)=>{
|
|
73
|
+
const includes = biome?.files?.includes;
|
|
74
|
+
if (!Array.isArray(includes) || includes.includes(BIOME_TEST_OUTPUT_EXCLUDE)) {
|
|
75
|
+
return biome;
|
|
76
|
+
}
|
|
77
|
+
// Placed next to the other build-output excludes. Without that anchor the
|
|
78
|
+
// list has diverged from the one we vend, so leave it to the user.
|
|
79
|
+
if (!includes.includes('!**/out-tsc')) {
|
|
80
|
+
unrecognised = true;
|
|
81
|
+
return biome;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
...biome,
|
|
85
|
+
files: {
|
|
86
|
+
...biome.files,
|
|
87
|
+
includes: includes.flatMap((include)=>include === '!**/out-tsc' ? [
|
|
88
|
+
include,
|
|
89
|
+
BIOME_TEST_OUTPUT_EXCLUDE
|
|
90
|
+
] : [
|
|
91
|
+
include
|
|
92
|
+
])
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
return unrecognised ? [
|
|
97
|
+
`\`biome.json\`: add \`"${BIOME_TEST_OUTPUT_EXCLUDE}"\` to \`files.includes\` so test reports are never formatted or linted. Its \`files.includes\` has diverged from the vended list, so it was left as it is.`
|
|
98
|
+
] : [];
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Delete the coverage directory an older release wrote inside the project.
|
|
102
|
+
*
|
|
103
|
+
* Left in place it stays git-visible: the vended `.gitignore` covers `dist` but
|
|
104
|
+
* not `test-output`, so the HTML reporter's vendored third-party scripts can be
|
|
105
|
+
* committed, and `license#sync` — which builds its candidate set from
|
|
106
|
+
* git-visible files rather than from `biome.json` — would stamp an Apache-2.0
|
|
107
|
+
* header onto them. Only the reporter's own output is removed; anything else the
|
|
108
|
+
* user keeps there is left alone.
|
|
109
|
+
*/ const deleteOldCoverageDirectory = (tree, projectRoot)=>{
|
|
110
|
+
const oldDirectory = joinPathFragments(projectRoot, OLD_COVERAGE_DIRECTORY, 'vitest/coverage');
|
|
111
|
+
if (tree.exists(oldDirectory)) {
|
|
112
|
+
tree.delete(oldDirectory);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* A `reportsDirectory` set on a target rather than in the vitest config, which
|
|
117
|
+
* takes precedence over the config value. `@nx/vitest` supports this shape, so
|
|
118
|
+
* report any that still resolves inside the project — rewriting a target option
|
|
119
|
+
* is the user's call, not this migration's.
|
|
120
|
+
*/ const targetReportsDirectories = (project)=>{
|
|
121
|
+
const found = [];
|
|
122
|
+
for (const [target, config] of Object.entries(project.targets ?? {})){
|
|
123
|
+
for (const options of [
|
|
124
|
+
config.options,
|
|
125
|
+
...Object.values(config.configurations ?? {})
|
|
126
|
+
]){
|
|
127
|
+
const reportsDirectory = options?.reportsDirectory;
|
|
128
|
+
// A path escaping the project root is already outside it.
|
|
129
|
+
if (typeof reportsDirectory === 'string' && !reportsDirectory.startsWith('..') && !isAbsolute(reportsDirectory)) {
|
|
130
|
+
found.push({
|
|
131
|
+
target,
|
|
132
|
+
reportsDirectory
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return found;
|
|
138
|
+
};
|
|
139
|
+
export default async function migration(tree) {
|
|
140
|
+
const nextSteps = [
|
|
141
|
+
...excludeTestOutputFromBiome(tree)
|
|
142
|
+
];
|
|
143
|
+
// Test reports are build output, so ignore them wherever a project writes them.
|
|
144
|
+
updateGitIgnore(tree, '.', (patterns)=>patterns.includes(OLD_COVERAGE_DIRECTORY) ? patterns : [
|
|
145
|
+
...patterns,
|
|
146
|
+
OLD_COVERAGE_DIRECTORY
|
|
147
|
+
]);
|
|
148
|
+
for (const [name, project] of getProjects(tree)){
|
|
149
|
+
for (const { target, reportsDirectory } of targetReportsDirectories(project)){
|
|
150
|
+
nextSteps.push(`${name}: the \`${target}\` target sets \`reportsDirectory: '${reportsDirectory}'\`, which resolves inside the project and takes precedence over the vitest config. Point it outside the project (eg \`${getCoverageReportsDirectory(project.root)}\`).`);
|
|
151
|
+
}
|
|
152
|
+
for (const configFile of CONFIG_FILES){
|
|
153
|
+
const configPath = joinPathFragments(project.root, configFile);
|
|
154
|
+
if (!tree.exists(configPath)) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (await matchGritQL(tree, configPath, GENERATED_DIRECTORY_PATTERN)) {
|
|
158
|
+
await applyGritQL(tree, configPath, rewriteDirectoryPattern(project.root));
|
|
159
|
+
deleteOldCoverageDirectory(tree, project.root);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
// A directory the user repointed is theirs to keep, but it may still sit
|
|
163
|
+
// inside the project, so report it rather than rewriting it. A config
|
|
164
|
+
// declaring no directory at all relies on vitest's default, which this
|
|
165
|
+
// migration has no generated shape to match.
|
|
166
|
+
if (await matchGritQL(tree, configPath, ANY_DIRECTORY_PATTERN) && !await matchGritQL(tree, configPath, migratedDirectoryPattern(project.root))) {
|
|
167
|
+
nextSteps.push(`${name}: \`${configPath}\` sets its own \`test.coverage.reportsDirectory\`. Point it outside the project (eg \`${getCoverageReportsDirectory(project.root)}\`) so coverage output is not formatted, linted or treated as a task input.`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
await formatFilesInSubtree(tree);
|
|
172
|
+
return {
|
|
173
|
+
nextSteps
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
//# sourceMappingURL=migration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/vitest-coverage-output-dir/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n getProjects,\n joinPathFragments,\n type MigrationReturnObject,\n type ProjectConfiguration,\n type Tree,\n updateJson,\n} from '@nx/devkit';\nimport { isAbsolute } from 'path';\nimport {\n GENERATED_REPORTS_DIRECTORY,\n getCoverageReportsDirectory,\n} from '../../../ts/lib/vitest.js';\nimport { applyGritQL, matchGritQL } from '../../../utils/ast.js';\nimport {\n BIOME_TEST_OUTPUT_EXCLUDE,\n formatFilesInSubtree,\n} from '../../../utils/format.js';\nimport { updateGitIgnore } from '../../../utils/git.js';\n\n/**\n * Point each project's vitest coverage directory at the workspace's `dist`.\n *\n * `@nx/vitest` writes `reportsDirectory: './test-output/vitest/coverage'`, which\n * resolves inside the project. Coverage is off by default, so the directory stays\n * dormant until someone runs `vitest --coverage` — at which point the HTML\n * reporter's bundled third-party scripts land in the project, where the `format`\n * target reformats them (failing every later build) and the `default` named input\n * counts them, so no task in the project can be cached again.\n *\n * Alongside the rewrite, the coverage directory an older release already wrote\n * into the project is deleted and `test-output` is added to the workspace\n * `.gitignore`, so the reporter's vendored third-party scripts can neither be\n * committed nor picked up by `license#sync`.\n *\n * Guardrails:\n * - The pattern is anchored to the exact literal `@nx/vitest` generated, and\n * scoped to the `coverage` block of the config's own `test` object, so a\n * `reportsDirectory` elsewhere in the file is left alone.\n * - A project whose directory has been repointed keeps its value, and is reported\n * via `nextSteps` instead — as is one whose directory is set on a target, or a\n * `biome.json` whose `files.includes` has diverged from the vended list.\n * - Idempotent: the pattern no longer matches once rewritten.\n */\n\n/**\n * The config filenames to check. Generated projects carry the `.mts` forms; the\n * other extensions vitest supports are included so a project whose config was\n * renamed is still migrated. Safe to widen, because the pattern matched inside is\n * anchored to the exact literal `@nx/vitest` generated.\n */\nconst CONFIG_FILES = [\n 'vitest.config.mts',\n 'vitest.config.ts',\n 'vitest.config.mjs',\n 'vitest.config.js',\n 'vite.config.mts',\n 'vite.config.ts',\n 'vite.config.mjs',\n 'vite.config.js',\n];\n\n/**\n * The `coverage` block of the object the config factory returns, scoped so a\n * `coverage` option elsewhere in the file is not matched.\n */\nconst coveragePattern = (body: string): string =>\n `\\`test: { $props }\\` where {\n $props <: within \\`defineConfig($_)\\`,\n $props <: some \\`coverage: { $cov }\\`,\n ${body}\n}`;\n\n/** Matches a coverage block still carrying the directory `@nx/vitest` generated. */\nconst GENERATED_DIRECTORY_PATTERN = coveragePattern(\n `$cov <: some \\`reportsDirectory: '${GENERATED_REPORTS_DIRECTORY}'\\``,\n);\n\n/** Matches a coverage block that declares a `reportsDirectory` at all. */\nconst ANY_DIRECTORY_PATTERN = coveragePattern(\n '$cov <: some `reportsDirectory: $_`',\n);\n\n/** Rewrites the generated directory to the one under `dist` for a project root. */\nconst rewriteDirectoryPattern = (projectRoot: string): string =>\n coveragePattern(\n `$cov <: some \\`reportsDirectory: '${GENERATED_REPORTS_DIRECTORY}'\\` as $reportsDirectory,\n $reportsDirectory => \\`reportsDirectory: '${getCoverageReportsDirectory(projectRoot)}'\\``,\n );\n\n/** Matches a coverage block already pointing at the directory under `dist`. */\nconst migratedDirectoryPattern = (projectRoot: string): string =>\n coveragePattern(\n `$cov <: some \\`reportsDirectory: '${getCoverageReportsDirectory(projectRoot)}'\\``,\n );\n\n/** The directory an older release wrote coverage into, relative to a project. */\nconst OLD_COVERAGE_DIRECTORY = 'test-output';\n\n/**\n * Exclude test reports from Biome, so a coverage directory pointed anywhere\n * inside the workspace is still never formatted or linted. Anchored to the\n * excludes we vend, so a workspace that has rewritten `files.includes` keeps its\n * own list — and is told to add the exclude itself.\n */\nconst excludeTestOutputFromBiome = (tree: Tree): string[] => {\n if (!tree.exists('biome.json')) {\n return [];\n }\n let unrecognised = false;\n updateJson(tree, 'biome.json', (biome) => {\n const includes: unknown = biome?.files?.includes;\n if (\n !Array.isArray(includes) ||\n includes.includes(BIOME_TEST_OUTPUT_EXCLUDE)\n ) {\n return biome;\n }\n // Placed next to the other build-output excludes. Without that anchor the\n // list has diverged from the one we vend, so leave it to the user.\n if (!includes.includes('!**/out-tsc')) {\n unrecognised = true;\n return biome;\n }\n return {\n ...biome,\n files: {\n ...biome.files,\n includes: includes.flatMap((include) =>\n include === '!**/out-tsc'\n ? [include, BIOME_TEST_OUTPUT_EXCLUDE]\n : [include],\n ),\n },\n };\n });\n return unrecognised\n ? [\n `\\`biome.json\\`: add \\`\"${BIOME_TEST_OUTPUT_EXCLUDE}\"\\` to \\`files.includes\\` so test reports are never formatted or linted. Its \\`files.includes\\` has diverged from the vended list, so it was left as it is.`,\n ]\n : [];\n};\n\n/**\n * Delete the coverage directory an older release wrote inside the project.\n *\n * Left in place it stays git-visible: the vended `.gitignore` covers `dist` but\n * not `test-output`, so the HTML reporter's vendored third-party scripts can be\n * committed, and `license#sync` — which builds its candidate set from\n * git-visible files rather than from `biome.json` — would stamp an Apache-2.0\n * header onto them. Only the reporter's own output is removed; anything else the\n * user keeps there is left alone.\n */\nconst deleteOldCoverageDirectory = (tree: Tree, projectRoot: string): void => {\n const oldDirectory = joinPathFragments(\n projectRoot,\n OLD_COVERAGE_DIRECTORY,\n 'vitest/coverage',\n );\n if (tree.exists(oldDirectory)) {\n tree.delete(oldDirectory);\n }\n};\n\n/**\n * A `reportsDirectory` set on a target rather than in the vitest config, which\n * takes precedence over the config value. `@nx/vitest` supports this shape, so\n * report any that still resolves inside the project — rewriting a target option\n * is the user's call, not this migration's.\n */\nconst targetReportsDirectories = (\n project: ProjectConfiguration,\n): { target: string; reportsDirectory: string }[] => {\n const found: { target: string; reportsDirectory: string }[] = [];\n for (const [target, config] of Object.entries(project.targets ?? {})) {\n for (const options of [\n config.options,\n ...Object.values(config.configurations ?? {}),\n ]) {\n const reportsDirectory: unknown = options?.reportsDirectory;\n // A path escaping the project root is already outside it.\n if (\n typeof reportsDirectory === 'string' &&\n !reportsDirectory.startsWith('..') &&\n !isAbsolute(reportsDirectory)\n ) {\n found.push({ target, reportsDirectory });\n }\n }\n }\n return found;\n};\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [...excludeTestOutputFromBiome(tree)];\n\n // Test reports are build output, so ignore them wherever a project writes them.\n updateGitIgnore(tree, '.', (patterns) =>\n patterns.includes(OLD_COVERAGE_DIRECTORY)\n ? patterns\n : [...patterns, OLD_COVERAGE_DIRECTORY],\n );\n\n for (const [name, project] of getProjects(tree)) {\n for (const { target, reportsDirectory } of targetReportsDirectories(\n project,\n )) {\n nextSteps.push(\n `${name}: the \\`${target}\\` target sets \\`reportsDirectory: '${reportsDirectory}'\\`, which resolves inside the project and takes precedence over the vitest config. Point it outside the project (eg \\`${getCoverageReportsDirectory(project.root)}\\`).`,\n );\n }\n\n for (const configFile of CONFIG_FILES) {\n const configPath = joinPathFragments(project.root, configFile);\n if (!tree.exists(configPath)) {\n continue;\n }\n\n if (await matchGritQL(tree, configPath, GENERATED_DIRECTORY_PATTERN)) {\n await applyGritQL(\n tree,\n configPath,\n rewriteDirectoryPattern(project.root),\n );\n deleteOldCoverageDirectory(tree, project.root);\n continue;\n }\n\n // A directory the user repointed is theirs to keep, but it may still sit\n // inside the project, so report it rather than rewriting it. A config\n // declaring no directory at all relies on vitest's default, which this\n // migration has no generated shape to match.\n if (\n (await matchGritQL(tree, configPath, ANY_DIRECTORY_PATTERN)) &&\n !(await matchGritQL(\n tree,\n configPath,\n migratedDirectoryPattern(project.root),\n ))\n ) {\n nextSteps.push(\n `${name}: \\`${configPath}\\` sets its own \\`test.coverage.reportsDirectory\\`. Point it outside the project (eg \\`${getCoverageReportsDirectory(project.root)}\\`) so coverage output is not formatted, linted or treated as a task input.`,\n );\n }\n }\n }\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["getProjects","joinPathFragments","updateJson","isAbsolute","GENERATED_REPORTS_DIRECTORY","getCoverageReportsDirectory","applyGritQL","matchGritQL","BIOME_TEST_OUTPUT_EXCLUDE","formatFilesInSubtree","updateGitIgnore","CONFIG_FILES","coveragePattern","body","GENERATED_DIRECTORY_PATTERN","ANY_DIRECTORY_PATTERN","rewriteDirectoryPattern","projectRoot","migratedDirectoryPattern","OLD_COVERAGE_DIRECTORY","excludeTestOutputFromBiome","tree","exists","unrecognised","biome","includes","files","Array","isArray","flatMap","include","deleteOldCoverageDirectory","oldDirectory","delete","targetReportsDirectories","project","found","target","config","Object","entries","targets","options","values","configurations","reportsDirectory","startsWith","push","migration","nextSteps","patterns","name","root","configFile","configPath"],"mappings":"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"}
|