@aws/nx-plugin 1.0.0-rc.40 → 1.0.0-rc.42

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.
@@ -6,7 +6,6 @@ import { relative } from "path";
6
6
  import PackageJson from "../../../package.json" with {
7
7
  type: 'json'
8
8
  };
9
- import { formatFilesInSubtree } from "../../utils/format.js";
10
9
  import { getGeneratorInfo } from "../../utils/nx.js";
11
10
  export const TS_SYNC_GENERATOR_INFO = getGeneratorInfo(import.meta.filename);
12
11
  export const SYNC_GENERATOR_NAME = `${PackageJson.name}:${TS_SYNC_GENERATOR_INFO.id}`;
@@ -35,7 +34,6 @@ export const tsSyncGeneratorGenerator = async (tree)=>{
35
34
  if (Object.keys(changesByConfigFile).length === 0) {
36
35
  return {};
37
36
  }
38
- await formatFilesInSubtree(tree);
39
37
  return {
40
38
  outOfSyncMessage: buildOutOfSyncMessage(changesByConfigFile)
41
39
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../packages/nx-plugin/src/ts/sync/generator.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport {\n getProjects,\n joinPathFragments,\n readJson,\n type Tree,\n updateJson,\n} from '@nx/devkit';\nimport type { SyncGeneratorResult } from 'nx/src/utils/sync-generators';\nimport { relative } from 'path';\nimport PackageJson from '../../../package.json' with { type: 'json' };\nimport { formatFilesInSubtree } from '../../utils/format';\nimport { getGeneratorInfo, type NxGeneratorInfo } from '../../utils/nx';\n\nexport const TS_SYNC_GENERATOR_INFO: NxGeneratorInfo = getGeneratorInfo(\n import.meta.filename,\n);\n\nexport const SYNC_GENERATOR_NAME = `${PackageJson.name}:${TS_SYNC_GENERATOR_INFO.id}`;\n\nexport const tsSyncGeneratorGenerator = async (\n tree: Tree,\n): Promise<SyncGeneratorResult> => {\n const basePaths = readBaseTsConfigPaths(tree);\n\n if (!basePaths) {\n return {};\n }\n\n const changesByConfigFile: Record<string, PathChange[]> = {};\n\n for (const project of getProjects(tree).values()) {\n for (const tsConfigFileName of [\n 'tsconfig.json',\n 'tsconfig.lib.json',\n 'tsconfig.app.json',\n ]) {\n const tsConfigPath = joinPathFragments(project.root, tsConfigFileName);\n if (!tree.exists(tsConfigPath)) {\n continue;\n }\n\n const { changes, updated } = syncPathsWithBase(\n tree,\n tsConfigPath,\n basePaths,\n project.root,\n );\n\n if (updated) {\n changesByConfigFile[tsConfigPath] = changes;\n }\n }\n }\n\n if (Object.keys(changesByConfigFile).length === 0) {\n return {};\n }\n\n await formatFilesInSubtree(tree);\n\n return {\n outOfSyncMessage: buildOutOfSyncMessage(changesByConfigFile),\n };\n};\n\nexport default tsSyncGeneratorGenerator;\n\nconst readBaseTsConfigPaths = (\n tree: Tree,\n): Record<string, string[]> | undefined => {\n const baseConfigPath = ['tsconfig.base.json', 'tsconfig.json'].find((path) =>\n tree.exists(path),\n );\n\n if (!baseConfigPath) {\n return undefined;\n }\n\n const baseConfigJson = readJson<Record<string, any>>(tree, baseConfigPath);\n return baseConfigJson?.compilerOptions?.paths ?? {};\n};\n\ntype PathChangeType = 'added' | 'updated';\n\ntype PathChange = {\n alias: string;\n type: PathChangeType;\n};\n\nconst arePathArraysEqual = (first: string[], second: string[]): boolean => {\n if (first.length !== second.length) {\n return false;\n }\n\n return first.every((value, index) => value === second[index]);\n};\n\n/**\n * Rebase a path from the workspace root to a project root.\n * e.g. \"./packages/foo/src/index.ts\" from project \"packages/bar\"\n * becomes \"../../packages/foo/src/index.ts\"\n */\nconst rebasePath = (pathValue: string, projectRoot: string): string => {\n // Strip leading ./ if present\n const normalized = pathValue.startsWith('./')\n ? pathValue.slice(2)\n : pathValue;\n const relPrefix = relative(projectRoot, '.') || '.';\n return joinPathFragments(relPrefix, normalized);\n};\n\nconst syncPathsWithBase = (\n tree: Tree,\n tsConfigPath: string,\n basePaths: Record<string, string[]>,\n projectRoot: string,\n): { updated: boolean; changes: PathChange[] } => {\n const changes: PathChange[] = [];\n\n const tsConfigJson = readJson(tree, tsConfigPath);\n const paths = tsConfigJson?.compilerOptions?.paths;\n\n if (!paths) {\n return { updated: false, changes };\n }\n\n const updatedAliases: Record<string, string[]> = {\n ...(tsConfigJson.compilerOptions?.paths ?? {}),\n };\n let updated = false;\n\n // Add or update aliases that exist in the base config, rebasing paths\n // relative to the project root since baseUrl is no longer set\n for (const [alias, baseValue] of Object.entries(basePaths)) {\n const rebasedValue = (baseValue as string[]).map((p) =>\n rebasePath(p, projectRoot),\n );\n const existingValue = updatedAliases[alias];\n if (!existingValue) {\n changes.push({ alias, type: 'added' });\n updated = true;\n } else if (!arePathArraysEqual(existingValue, rebasedValue)) {\n changes.push({ alias, type: 'updated' });\n updated = true;\n }\n\n updatedAliases[alias] = rebasedValue;\n }\n\n if (updated) {\n updateJson(tree, tsConfigPath, (json) => ({\n ...json,\n compilerOptions: {\n ...json.compilerOptions,\n paths: updatedAliases,\n },\n }));\n }\n\n return { updated, changes };\n};\n\n/**\n * Build the message to display when the sync generator would make changes to the tree\n */\nconst buildOutOfSyncMessage = (\n changesByConfig: Record<string, PathChange[]>,\n): string =>\n `TypeScript path aliases are out of sync with the base tsconfig. The following configs will be updated:\\n${Object.entries(\n changesByConfig,\n )\n .map(\n ([config, changes]) =>\n `${config}:\\n${changes\n .slice()\n .sort((left, right) => left.alias.localeCompare(right.alias))\n .map(({ alias, type }) => `- ${alias} (${type})`)\n .join('\\n')}`,\n )\n .join('\\n\\n')}`;\n"],"names":["getProjects","joinPathFragments","readJson","updateJson","relative","PackageJson","type","formatFilesInSubtree","getGeneratorInfo","TS_SYNC_GENERATOR_INFO","filename","SYNC_GENERATOR_NAME","name","id","tsSyncGeneratorGenerator","tree","basePaths","readBaseTsConfigPaths","changesByConfigFile","project","values","tsConfigFileName","tsConfigPath","root","exists","changes","updated","syncPathsWithBase","Object","keys","length","outOfSyncMessage","buildOutOfSyncMessage","baseConfigPath","find","path","undefined","baseConfigJson","compilerOptions","paths","arePathArraysEqual","first","second","every","value","index","rebasePath","pathValue","projectRoot","normalized","startsWith","slice","relPrefix","tsConfigJson","updatedAliases","alias","baseValue","entries","rebasedValue","map","p","existingValue","push","json","changesByConfig","config","sort","left","right","localeCompare","join"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../packages/nx-plugin/src/ts/sync/generator.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport {\n getProjects,\n joinPathFragments,\n readJson,\n type Tree,\n updateJson,\n} from '@nx/devkit';\nimport type { SyncGeneratorResult } from 'nx/src/utils/sync-generators';\nimport { relative } from 'path';\nimport PackageJson from '../../../package.json' with { type: 'json' };\nimport { getGeneratorInfo, type NxGeneratorInfo } from '../../utils/nx';\n\nexport const TS_SYNC_GENERATOR_INFO: NxGeneratorInfo = getGeneratorInfo(\n import.meta.filename,\n);\n\nexport const SYNC_GENERATOR_NAME = `${PackageJson.name}:${TS_SYNC_GENERATOR_INFO.id}`;\n\nexport const tsSyncGeneratorGenerator = async (\n tree: Tree,\n): Promise<SyncGeneratorResult> => {\n const basePaths = readBaseTsConfigPaths(tree);\n\n if (!basePaths) {\n return {};\n }\n\n const changesByConfigFile: Record<string, PathChange[]> = {};\n\n for (const project of getProjects(tree).values()) {\n for (const tsConfigFileName of [\n 'tsconfig.json',\n 'tsconfig.lib.json',\n 'tsconfig.app.json',\n ]) {\n const tsConfigPath = joinPathFragments(project.root, tsConfigFileName);\n if (!tree.exists(tsConfigPath)) {\n continue;\n }\n\n const { changes, updated } = syncPathsWithBase(\n tree,\n tsConfigPath,\n basePaths,\n project.root,\n );\n\n if (updated) {\n changesByConfigFile[tsConfigPath] = changes;\n }\n }\n }\n\n if (Object.keys(changesByConfigFile).length === 0) {\n return {};\n }\n\n return {\n outOfSyncMessage: buildOutOfSyncMessage(changesByConfigFile),\n };\n};\n\nexport default tsSyncGeneratorGenerator;\n\nconst readBaseTsConfigPaths = (\n tree: Tree,\n): Record<string, string[]> | undefined => {\n const baseConfigPath = ['tsconfig.base.json', 'tsconfig.json'].find((path) =>\n tree.exists(path),\n );\n\n if (!baseConfigPath) {\n return undefined;\n }\n\n const baseConfigJson = readJson<Record<string, any>>(tree, baseConfigPath);\n return baseConfigJson?.compilerOptions?.paths ?? {};\n};\n\ntype PathChangeType = 'added' | 'updated';\n\ntype PathChange = {\n alias: string;\n type: PathChangeType;\n};\n\nconst arePathArraysEqual = (first: string[], second: string[]): boolean => {\n if (first.length !== second.length) {\n return false;\n }\n\n return first.every((value, index) => value === second[index]);\n};\n\n/**\n * Rebase a path from the workspace root to a project root.\n * e.g. \"./packages/foo/src/index.ts\" from project \"packages/bar\"\n * becomes \"../../packages/foo/src/index.ts\"\n */\nconst rebasePath = (pathValue: string, projectRoot: string): string => {\n // Strip leading ./ if present\n const normalized = pathValue.startsWith('./')\n ? pathValue.slice(2)\n : pathValue;\n const relPrefix = relative(projectRoot, '.') || '.';\n return joinPathFragments(relPrefix, normalized);\n};\n\nconst syncPathsWithBase = (\n tree: Tree,\n tsConfigPath: string,\n basePaths: Record<string, string[]>,\n projectRoot: string,\n): { updated: boolean; changes: PathChange[] } => {\n const changes: PathChange[] = [];\n\n const tsConfigJson = readJson(tree, tsConfigPath);\n const paths = tsConfigJson?.compilerOptions?.paths;\n\n if (!paths) {\n return { updated: false, changes };\n }\n\n const updatedAliases: Record<string, string[]> = {\n ...(tsConfigJson.compilerOptions?.paths ?? {}),\n };\n let updated = false;\n\n // Add or update aliases that exist in the base config, rebasing paths\n // relative to the project root since baseUrl is no longer set\n for (const [alias, baseValue] of Object.entries(basePaths)) {\n const rebasedValue = (baseValue as string[]).map((p) =>\n rebasePath(p, projectRoot),\n );\n const existingValue = updatedAliases[alias];\n if (!existingValue) {\n changes.push({ alias, type: 'added' });\n updated = true;\n } else if (!arePathArraysEqual(existingValue, rebasedValue)) {\n changes.push({ alias, type: 'updated' });\n updated = true;\n }\n\n updatedAliases[alias] = rebasedValue;\n }\n\n if (updated) {\n updateJson(tree, tsConfigPath, (json) => ({\n ...json,\n compilerOptions: {\n ...json.compilerOptions,\n paths: updatedAliases,\n },\n }));\n }\n\n return { updated, changes };\n};\n\n/**\n * Build the message to display when the sync generator would make changes to the tree\n */\nconst buildOutOfSyncMessage = (\n changesByConfig: Record<string, PathChange[]>,\n): string =>\n `TypeScript path aliases are out of sync with the base tsconfig. The following configs will be updated:\\n${Object.entries(\n changesByConfig,\n )\n .map(\n ([config, changes]) =>\n `${config}:\\n${changes\n .slice()\n .sort((left, right) => left.alias.localeCompare(right.alias))\n .map(({ alias, type }) => `- ${alias} (${type})`)\n .join('\\n')}`,\n )\n .join('\\n\\n')}`;\n"],"names":["getProjects","joinPathFragments","readJson","updateJson","relative","PackageJson","type","getGeneratorInfo","TS_SYNC_GENERATOR_INFO","filename","SYNC_GENERATOR_NAME","name","id","tsSyncGeneratorGenerator","tree","basePaths","readBaseTsConfigPaths","changesByConfigFile","project","values","tsConfigFileName","tsConfigPath","root","exists","changes","updated","syncPathsWithBase","Object","keys","length","outOfSyncMessage","buildOutOfSyncMessage","baseConfigPath","find","path","undefined","baseConfigJson","compilerOptions","paths","arePathArraysEqual","first","second","every","value","index","rebasePath","pathValue","projectRoot","normalized","startsWith","slice","relPrefix","tsConfigJson","updatedAliases","alias","baseValue","entries","rebasedValue","map","p","existingValue","push","json","changesByConfig","config","sort","left","right","localeCompare","join"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../packages/nx-plugin/src/utils/ast.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { QueryBuilder } from '@getgrit/gritql';\nimport type { Tree } from '@nx/devkit';\nimport * as path from 'path';\nimport { updateGitIgnore } from './git';\nimport { isEsmWorkspace } from './module-format';\n\nconst GRIT_DIR = '.grit';\n\n/**\n * Normalize a relative module specifier for the workspace's module format.\n * ESM (`nodenext`) requires an explicit `.js` extension on relative imports;\n * CommonJS (`node16`) resolves them extensionless. Callers always pass the ESM\n * form (with `.js`); this strips the extension for CommonJS workspaces.\n */\nconst normalizeModuleSpecifier = (tree: Tree, from: string): string => {\n if (isEsmWorkspace(tree)) {\n return from;\n }\n const isRelative = from.startsWith('./') || from.startsWith('../');\n return isRelative && from.endsWith('.js') ? from.slice(0, -'.js'.length) : from;\n};\n\n// Pin the gritql native library's \"global\" stdlib directory to <workspace>/.grit so it doesn't try to write to /usr/local/.grit (or wherever node's grandparent resolves to).\nconst ensureGritDir = (tree: Tree) => {\n process.env.GRIT_GLOBAL_DIR ??= path.join(tree.root, GRIT_DIR);\n updateGitIgnore(tree, '.', (patterns) => [...patterns, GRIT_DIR]);\n};\n\nconst assertFilePath = (tree: Tree, filePath: string) => {\n if (!tree.exists(filePath)) {\n throw new Error(`No file located at ${filePath}`);\n }\n};\n\nexport const addDestructuredImport = async (\n tree: Tree,\n filePath: string,\n variableNames: string[],\n from: string,\n) => {\n assertFilePath(tree, filePath);\n from = normalizeModuleSpecifier(tree, from);\n\n // Check if there's an existing import from this module\n const hasExistingImport = await matchGritQL(\n tree,\n filePath,\n `\\`import { $_ } from '${from}'\\``,\n );\n\n if (hasExistingImport) {\n // For each new variable, use GritQL rewrite to add it if not already present\n for (const variableName of variableNames) {\n const localName = variableName.includes(' as ')\n ? variableName.split(' as ')[1]\n : variableName;\n // Use rewrite (=>) which correctly handles comma-separated import specifier lists\n await applyGritQL(\n tree,\n filePath,\n `\\`import { $imports } from '${from}'\\` => \\`import { $imports, ${variableName} } from '${from}'\\` where { $imports <: not contains \\`${localName}\\` }`,\n );\n }\n } else {\n // No existing import — prepend a new one (after shebang if present)\n const specifiers = variableNames.join(', ');\n const contents = tree.read(filePath)!.toString();\n const newImport = `import { ${specifiers} } from '${from}';\\n`;\n if (contents.startsWith('#!')) {\n const newlineIndex = contents.indexOf('\\n');\n tree.write(\n filePath,\n `${contents.substring(0, newlineIndex + 1)}${newImport}${contents.substring(newlineIndex + 1)}`,\n );\n } else {\n tree.write(filePath, `${newImport}${contents}`);\n }\n }\n};\n\n/**\n * Ensure a Python `from <module> import <name1>, <name2>, ...` statement\n * exists in the given file. If the module already has a `from <module> import`\n * line, any missing names are appended; otherwise a fresh import statement is\n * prepended to the file (ruff's import sorter will reorder it into place on\n * the next format pass).\n *\n * We try to append first (which only succeeds if an import-from for the\n * module already exists AND the name isn't yet in the list). If nothing\n * was rewritten after trying every requested name, we fall back to checking\n * whether the module is imported at all; if not, we prepend a fresh\n * `from <module> import ...` line.\n */\nexport const addPythonDestructuredImport = async (\n tree: Tree,\n filePath: string,\n variableNames: string[],\n from: string,\n) => {\n assertFilePath(tree, filePath);\n\n // Determine whether there's any `from <module> import ...` line in the file.\n // We detect this by attempting a no-op transformation — the file is unchanged,\n // but a successful match tells us the line exists.\n const beforeContents = tree.read(filePath)!.toString();\n let moduleAlreadyImported = false;\n for (const variableName of variableNames) {\n // Appends the name if the import-from exists and the name isn't already there.\n const appended = await applyGritQL(\n tree,\n filePath,\n `language python\\n\\`from ${from} import $names\\` where { $names <: not contains \\`${variableName}\\`, $names += \\`, ${variableName}\\` }`,\n );\n if (appended) {\n moduleAlreadyImported = true;\n }\n }\n\n // If the append succeeded for at least one name, we know the module line\n // exists; we're done. If nothing was appended we need to distinguish\n // between \"module not imported\" (need to add a new line) and \"module\n // imported but all names already present\" (no-op).\n if (moduleAlreadyImported) {\n return;\n }\n\n const allAlreadyPresent = await matchGritQL(\n tree,\n filePath,\n `language python\\n\\`from ${from} import $names\\` where { ${variableNames\n .map((n) => `$names <: contains \\`${n}\\``)\n .join(', ')} }`,\n );\n if (allAlreadyPresent) {\n return;\n }\n\n // No existing `from <module> import` line — prepend one. Ruff's import\n // sorter will place it in the right group on the next formatter pass.\n const specifiers = variableNames.join(', ');\n tree.write(filePath, `from ${from} import ${specifiers}\\n${beforeContents}`);\n};\n\n/**\n * Adds an `import <variableName> from '<from>'; statement to the beginning of the file,\n * if it doesn't already exist\n */\nexport const addSingleImport = async (\n tree: Tree,\n filePath: string,\n variableName: string,\n from: string,\n) => {\n assertFilePath(tree, filePath);\n from = normalizeModuleSpecifier(tree, from);\n\n // Check if default import already exists using GritQL\n const alreadyImported = await matchGritQL(\n tree,\n filePath,\n `\\`import ${variableName} from '${from}'\\``,\n );\n if (alreadyImported) {\n return;\n }\n\n // Prepend new import to file\n const contents = tree.read(filePath)!.toString();\n tree.write(filePath, `import ${variableName} from '${from}';\\n${contents}`);\n};\n\n/**\n * Adds an `export * from '<from>'; statement to the given TypeScript file.\n * Note that this will create the file if it does not exist in the tree.\n */\nexport const addStarExport = async (\n tree: Tree,\n filePath: string,\n from: string,\n) => {\n from = normalizeModuleSpecifier(tree, from);\n const contents = tree.read(filePath)?.toString() ?? '';\n\n // For empty/non-existent files, just write the export\n if (!contents.trim()) {\n tree.write(filePath, `export * from '${from}';\\n`);\n return;\n }\n\n // Check if already exported using GritQL\n const alreadyExported = await matchGritQL(\n tree,\n filePath,\n `\\`export * from '${from}'\\``,\n );\n if (alreadyExported) {\n return;\n }\n\n // Prepend new export to file\n tree.write(filePath, `export * from '${from}';\\n${contents}`);\n};\n\n/**\n * Return whether or not the given identifier is exported in the source file.\n * Checks for both `export { Identifier }` and `export type Identifier = ...`.\n */\nexport const hasExportDeclaration = async (\n tree: Tree,\n source: string,\n identifierName: string,\n): Promise<boolean> => {\n ensureGritDir(tree);\n const patterns = [\n `\\`export type ${identifierName} = $_\\``,\n `\\`export { ${identifierName} }\\``,\n `\\`export type { ${identifierName} } from $_\\``,\n ];\n\n for (const pattern of patterns) {\n try {\n const q = new QueryBuilder(`$p => $p where { $p <: ${pattern} }`);\n const result = await q.applyToFile({\n path: 'check.ts',\n content: source,\n });\n if (result !== null) return true;\n } catch {\n // Pattern didn't match, try next\n }\n }\n\n return false;\n};\n\n/**\n * Apply a GritQL pattern to a file in the Nx tree.\n */\nexport const applyGritQL = async (\n tree: Tree,\n filePath: string,\n pattern: string,\n): Promise<boolean> => {\n if (!tree.exists(filePath)) throw new Error(`No file at ${filePath}`);\n ensureGritDir(tree);\n const source = tree.read(filePath)!.toString();\n const query = new QueryBuilder(pattern);\n const result = await query.applyToFile({ path: filePath, content: source });\n if (result && result.content !== source) {\n tree.write(filePath, result.content);\n return true;\n }\n return false;\n};\n\n/**\n * Check whether a GritQL pattern matches anywhere in a file.\n * Returns true if the pattern matches at least once.\n *\n * Accepts raw GritQL, including patterns that start with a `language <name>`\n * header (e.g. `language python\\n\\`print($_)\\``) — the pattern is passed\n * straight through to QueryBuilder without any wrapping rewrite.\n */\nexport const matchGritQL = async (\n tree: Tree,\n filePath: string,\n pattern: string,\n): Promise<boolean> => {\n if (!tree.exists(filePath)) return false;\n ensureGritDir(tree);\n const source = tree.read(filePath)!.toString();\n let matched = false;\n try {\n const query = new QueryBuilder(pattern);\n query.filter(() => {\n matched = true;\n return true;\n });\n await query.applyToFile({ path: filePath, content: source });\n } catch {\n return false;\n }\n return matched;\n};\n\n/**\n * Capture the source text of the first match of a GritQL pattern.\n *\n * For example `` `dependencyCheck: $_` `` returns the whole\n * `dependencyCheck: { ... }` property text. Returns undefined if nothing\n * matches. The file is not modified.\n */\nexport const captureGritQL = async (\n tree: Tree,\n filePath: string,\n pattern: string,\n): Promise<string | undefined> => {\n if (!tree.exists(filePath)) return undefined;\n ensureGritDir(tree);\n const source = tree.read(filePath)!.toString();\n let captured: string | undefined;\n try {\n const query = new QueryBuilder(pattern);\n query.filter((node) => {\n captured ??= node.text();\n return true;\n });\n await query.applyToFile({ path: filePath, content: source });\n } catch {\n return undefined;\n }\n return captured;\n};\n\n/**\n * Return the text of every node matching the GritQL pattern, in document order.\n */\nexport const captureAllGritQL = async (\n tree: Tree,\n filePath: string,\n pattern: string,\n): Promise<string[]> => {\n if (!tree.exists(filePath)) return [];\n ensureGritDir(tree);\n const source = tree.read(filePath)!.toString();\n const captured: string[] = [];\n try {\n const query = new QueryBuilder(pattern);\n query.filter((node) => {\n captured.push(node.text());\n return true;\n });\n await query.applyToFile({ path: filePath, content: source });\n } catch {\n return [];\n }\n return captured;\n};\n\n/**\n * A unique token that is a valid identifier, so it can stand in for an array\n * element or object property inside a GritQL rewrite.\n */\nexport const GRIT_INSERT_PLACEHOLDER = '__GRIT_INSERT_PLACEHOLDER__';\n\n/**\n * Apply a GritQL rewrite that inserts {@link GRIT_INSERT_PLACEHOLDER}, then\n * replace the placeholder with `text`. Routing user-provided text through a\n * placeholder keeps it out of the GritQL pattern, where quotes, backticks or\n * `${...}` would otherwise break parsing.\n *\n * Any separator (e.g. a leading `, ` when appending to an array) must be part\n * of the GritQL pattern, not `text`. The one adjustment made here: if the\n * placeholder lands after a trailing line comment (`// ...`) — which would\n * comment out the text — the text is moved to its own line.\n *\n * The caller formats afterwards. Returns true if the pattern matched and the\n * file changed.\n */\nexport const insertViaGritQL = async (\n tree: Tree,\n filePath: string,\n pattern: string,\n text: string,\n): Promise<boolean> => {\n if (!(await applyGritQL(tree, filePath, pattern))) return false;\n const content = tree.read(filePath)!.toString();\n const at = content.indexOf(GRIT_INSERT_PLACEHOLDER);\n const lineSoFar = content.slice(content.lastIndexOf('\\n', at) + 1, at);\n const replacement = lineSoFar.includes('//') ? `\\n${text}` : text;\n\n // Function replacer so `$` in `text` isn't treated as a replacement pattern.\n tree.write(\n filePath,\n content.replace(GRIT_INSERT_PLACEHOLDER, () => replacement),\n );\n return true;\n};\n"],"names":["QueryBuilder","path","updateGitIgnore","isEsmWorkspace","GRIT_DIR","normalizeModuleSpecifier","tree","from","isRelative","startsWith","endsWith","slice","length","ensureGritDir","process","env","GRIT_GLOBAL_DIR","join","root","patterns","assertFilePath","filePath","exists","Error","addDestructuredImport","variableNames","hasExistingImport","matchGritQL","variableName","localName","includes","split","applyGritQL","specifiers","contents","read","toString","newImport","newlineIndex","indexOf","write","substring","addPythonDestructuredImport","beforeContents","moduleAlreadyImported","appended","allAlreadyPresent","map","n","addSingleImport","alreadyImported","addStarExport","trim","alreadyExported","hasExportDeclaration","source","identifierName","pattern","q","result","applyToFile","content","query","matched","filter","captureGritQL","undefined","captured","node","text","captureAllGritQL","push","GRIT_INSERT_PLACEHOLDER","insertViaGritQL","at","lineSoFar","lastIndexOf","replacement","replace"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../packages/nx-plugin/src/utils/ast.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { QueryBuilder } from '@getgrit/gritql';\nimport type { Tree } from '@nx/devkit';\nimport * as path from 'path';\nimport { updateGitIgnore } from './git';\nimport { isEsmWorkspace } from './module-format';\n\nconst GRIT_DIR = '.grit';\n\n/**\n * Normalize a relative module specifier for the workspace's module format.\n * ESM (`nodenext`) requires an explicit `.js` extension on relative imports;\n * CommonJS (`node16`) resolves them extensionless. Callers always pass the ESM\n * form (with `.js`); this strips the extension for CommonJS workspaces.\n */\nconst normalizeModuleSpecifier = (tree: Tree, from: string): string => {\n if (isEsmWorkspace(tree)) {\n return from;\n }\n const isRelative = from.startsWith('./') || from.startsWith('../');\n return isRelative && from.endsWith('.js')\n ? from.slice(0, -'.js'.length)\n : from;\n};\n\n// Pin the gritql native library's \"global\" stdlib directory to <workspace>/.grit so it doesn't try to write to /usr/local/.grit (or wherever node's grandparent resolves to).\nconst ensureGritDir = (tree: Tree) => {\n process.env.GRIT_GLOBAL_DIR ??= path.join(tree.root, GRIT_DIR);\n updateGitIgnore(tree, '.', (patterns) => [...patterns, GRIT_DIR]);\n};\n\nconst assertFilePath = (tree: Tree, filePath: string) => {\n if (!tree.exists(filePath)) {\n throw new Error(`No file located at ${filePath}`);\n }\n};\n\nexport const addDestructuredImport = async (\n tree: Tree,\n filePath: string,\n variableNames: string[],\n from: string,\n) => {\n assertFilePath(tree, filePath);\n from = normalizeModuleSpecifier(tree, from);\n\n // Check if there's an existing import from this module\n const hasExistingImport = await matchGritQL(\n tree,\n filePath,\n `\\`import { $_ } from '${from}'\\``,\n );\n\n if (hasExistingImport) {\n // For each new variable, use GritQL rewrite to add it if not already present\n for (const variableName of variableNames) {\n const localName = variableName.includes(' as ')\n ? variableName.split(' as ')[1]\n : variableName;\n // Use rewrite (=>) which correctly handles comma-separated import specifier lists\n await applyGritQL(\n tree,\n filePath,\n `\\`import { $imports } from '${from}'\\` => \\`import { $imports, ${variableName} } from '${from}'\\` where { $imports <: not contains \\`${localName}\\` }`,\n );\n }\n } else {\n // No existing import — prepend a new one (after shebang if present)\n const specifiers = variableNames.join(', ');\n const contents = tree.read(filePath)!.toString();\n const newImport = `import { ${specifiers} } from '${from}';\\n`;\n if (contents.startsWith('#!')) {\n const newlineIndex = contents.indexOf('\\n');\n tree.write(\n filePath,\n `${contents.substring(0, newlineIndex + 1)}${newImport}${contents.substring(newlineIndex + 1)}`,\n );\n } else {\n tree.write(filePath, `${newImport}${contents}`);\n }\n }\n};\n\n/**\n * Ensure a Python `from <module> import <name1>, <name2>, ...` statement\n * exists in the given file. If the module already has a `from <module> import`\n * line, any missing names are appended; otherwise a fresh import statement is\n * prepended to the file (ruff's import sorter will reorder it into place on\n * the next format pass).\n *\n * We try to append first (which only succeeds if an import-from for the\n * module already exists AND the name isn't yet in the list). If nothing\n * was rewritten after trying every requested name, we fall back to checking\n * whether the module is imported at all; if not, we prepend a fresh\n * `from <module> import ...` line.\n */\nexport const addPythonDestructuredImport = async (\n tree: Tree,\n filePath: string,\n variableNames: string[],\n from: string,\n) => {\n assertFilePath(tree, filePath);\n\n // Determine whether there's any `from <module> import ...` line in the file.\n // We detect this by attempting a no-op transformation — the file is unchanged,\n // but a successful match tells us the line exists.\n const beforeContents = tree.read(filePath)!.toString();\n let moduleAlreadyImported = false;\n for (const variableName of variableNames) {\n // Appends the name if the import-from exists and the name isn't already there.\n const appended = await applyGritQL(\n tree,\n filePath,\n `language python\\n\\`from ${from} import $names\\` where { $names <: not contains \\`${variableName}\\`, $names += \\`, ${variableName}\\` }`,\n );\n if (appended) {\n moduleAlreadyImported = true;\n }\n }\n\n // If the append succeeded for at least one name, we know the module line\n // exists; we're done. If nothing was appended we need to distinguish\n // between \"module not imported\" (need to add a new line) and \"module\n // imported but all names already present\" (no-op).\n if (moduleAlreadyImported) {\n return;\n }\n\n const allAlreadyPresent = await matchGritQL(\n tree,\n filePath,\n `language python\\n\\`from ${from} import $names\\` where { ${variableNames\n .map((n) => `$names <: contains \\`${n}\\``)\n .join(', ')} }`,\n );\n if (allAlreadyPresent) {\n return;\n }\n\n // No existing `from <module> import` line — prepend one. Ruff's import\n // sorter will place it in the right group on the next formatter pass.\n const specifiers = variableNames.join(', ');\n tree.write(filePath, `from ${from} import ${specifiers}\\n${beforeContents}`);\n};\n\n/**\n * Adds an `import <variableName> from '<from>'; statement to the beginning of the file,\n * if it doesn't already exist\n */\nexport const addSingleImport = async (\n tree: Tree,\n filePath: string,\n variableName: string,\n from: string,\n) => {\n assertFilePath(tree, filePath);\n from = normalizeModuleSpecifier(tree, from);\n\n // Check if default import already exists using GritQL\n const alreadyImported = await matchGritQL(\n tree,\n filePath,\n `\\`import ${variableName} from '${from}'\\``,\n );\n if (alreadyImported) {\n return;\n }\n\n // Prepend new import to file\n const contents = tree.read(filePath)!.toString();\n tree.write(filePath, `import ${variableName} from '${from}';\\n${contents}`);\n};\n\n/**\n * Adds an `export * from '<from>'; statement to the given TypeScript file.\n * Note that this will create the file if it does not exist in the tree.\n */\nexport const addStarExport = async (\n tree: Tree,\n filePath: string,\n from: string,\n) => {\n from = normalizeModuleSpecifier(tree, from);\n const contents = tree.read(filePath)?.toString() ?? '';\n\n // For empty/non-existent files, just write the export\n if (!contents.trim()) {\n tree.write(filePath, `export * from '${from}';\\n`);\n return;\n }\n\n // Check if already exported using GritQL\n const alreadyExported = await matchGritQL(\n tree,\n filePath,\n `\\`export * from '${from}'\\``,\n );\n if (alreadyExported) {\n return;\n }\n\n // Prepend new export to file\n tree.write(filePath, `export * from '${from}';\\n${contents}`);\n};\n\n/**\n * Return whether or not the given identifier is exported in the source file.\n * Checks for both `export { Identifier }` and `export type Identifier = ...`.\n */\nexport const hasExportDeclaration = async (\n tree: Tree,\n source: string,\n identifierName: string,\n): Promise<boolean> => {\n ensureGritDir(tree);\n const patterns = [\n `\\`export type ${identifierName} = $_\\``,\n `\\`export { ${identifierName} }\\``,\n `\\`export type { ${identifierName} } from $_\\``,\n ];\n\n for (const pattern of patterns) {\n try {\n const q = new QueryBuilder(`$p => $p where { $p <: ${pattern} }`);\n const result = await q.applyToFile({\n path: 'check.ts',\n content: source,\n });\n if (result !== null) return true;\n } catch {\n // Pattern didn't match, try next\n }\n }\n\n return false;\n};\n\n/**\n * Apply a GritQL pattern to a file in the Nx tree.\n */\nexport const applyGritQL = async (\n tree: Tree,\n filePath: string,\n pattern: string,\n): Promise<boolean> => {\n if (!tree.exists(filePath)) throw new Error(`No file at ${filePath}`);\n ensureGritDir(tree);\n const source = tree.read(filePath)!.toString();\n const query = new QueryBuilder(pattern);\n const result = await query.applyToFile({ path: filePath, content: source });\n if (result && result.content !== source) {\n tree.write(filePath, result.content);\n return true;\n }\n return false;\n};\n\n/**\n * Check whether a GritQL pattern matches anywhere in a file.\n * Returns true if the pattern matches at least once.\n *\n * Accepts raw GritQL, including patterns that start with a `language <name>`\n * header (e.g. `language python\\n\\`print($_)\\``) — the pattern is passed\n * straight through to QueryBuilder without any wrapping rewrite.\n */\nexport const matchGritQL = async (\n tree: Tree,\n filePath: string,\n pattern: string,\n): Promise<boolean> => {\n if (!tree.exists(filePath)) return false;\n ensureGritDir(tree);\n const source = tree.read(filePath)!.toString();\n let matched = false;\n try {\n const query = new QueryBuilder(pattern);\n query.filter(() => {\n matched = true;\n return true;\n });\n await query.applyToFile({ path: filePath, content: source });\n } catch {\n return false;\n }\n return matched;\n};\n\n/**\n * Capture the source text of the first match of a GritQL pattern.\n *\n * For example `` `dependencyCheck: $_` `` returns the whole\n * `dependencyCheck: { ... }` property text. Returns undefined if nothing\n * matches. The file is not modified.\n */\nexport const captureGritQL = async (\n tree: Tree,\n filePath: string,\n pattern: string,\n): Promise<string | undefined> => {\n if (!tree.exists(filePath)) return undefined;\n ensureGritDir(tree);\n const source = tree.read(filePath)!.toString();\n let captured: string | undefined;\n try {\n const query = new QueryBuilder(pattern);\n query.filter((node) => {\n captured ??= node.text();\n return true;\n });\n await query.applyToFile({ path: filePath, content: source });\n } catch {\n return undefined;\n }\n return captured;\n};\n\n/**\n * Return the text of every node matching the GritQL pattern, in document order.\n */\nexport const captureAllGritQL = async (\n tree: Tree,\n filePath: string,\n pattern: string,\n): Promise<string[]> => {\n if (!tree.exists(filePath)) return [];\n ensureGritDir(tree);\n const source = tree.read(filePath)!.toString();\n const captured: string[] = [];\n try {\n const query = new QueryBuilder(pattern);\n query.filter((node) => {\n captured.push(node.text());\n return true;\n });\n await query.applyToFile({ path: filePath, content: source });\n } catch {\n return [];\n }\n return captured;\n};\n\n/**\n * A unique token that is a valid identifier, so it can stand in for an array\n * element or object property inside a GritQL rewrite.\n */\nexport const GRIT_INSERT_PLACEHOLDER = '__GRIT_INSERT_PLACEHOLDER__';\n\n/**\n * Apply a GritQL rewrite that inserts {@link GRIT_INSERT_PLACEHOLDER}, then\n * replace the placeholder with `text`. Routing user-provided text through a\n * placeholder keeps it out of the GritQL pattern, where quotes, backticks or\n * `${...}` would otherwise break parsing.\n *\n * Any separator (e.g. a leading `, ` when appending to an array) must be part\n * of the GritQL pattern, not `text`. The one adjustment made here: if the\n * placeholder lands after a trailing line comment (`// ...`) — which would\n * comment out the text — the text is moved to its own line.\n *\n * The caller formats afterwards. Returns true if the pattern matched and the\n * file changed.\n */\nexport const insertViaGritQL = async (\n tree: Tree,\n filePath: string,\n pattern: string,\n text: string,\n): Promise<boolean> => {\n if (!(await applyGritQL(tree, filePath, pattern))) return false;\n const content = tree.read(filePath)!.toString();\n const at = content.indexOf(GRIT_INSERT_PLACEHOLDER);\n const lineSoFar = content.slice(content.lastIndexOf('\\n', at) + 1, at);\n const replacement = lineSoFar.includes('//') ? `\\n${text}` : text;\n\n // Function replacer so `$` in `text` isn't treated as a replacement pattern.\n tree.write(\n filePath,\n content.replace(GRIT_INSERT_PLACEHOLDER, () => replacement),\n );\n return true;\n};\n"],"names":["QueryBuilder","path","updateGitIgnore","isEsmWorkspace","GRIT_DIR","normalizeModuleSpecifier","tree","from","isRelative","startsWith","endsWith","slice","length","ensureGritDir","process","env","GRIT_GLOBAL_DIR","join","root","patterns","assertFilePath","filePath","exists","Error","addDestructuredImport","variableNames","hasExistingImport","matchGritQL","variableName","localName","includes","split","applyGritQL","specifiers","contents","read","toString","newImport","newlineIndex","indexOf","write","substring","addPythonDestructuredImport","beforeContents","moduleAlreadyImported","appended","allAlreadyPresent","map","n","addSingleImport","alreadyImported","addStarExport","trim","alreadyExported","hasExportDeclaration","source","identifierName","pattern","q","result","applyToFile","content","query","matched","filter","captureGritQL","undefined","captured","node","text","captureAllGritQL","push","GRIT_INSERT_PLACEHOLDER","insertViaGritQL","at","lineSoFar","lastIndexOf","replacement","replace"],"mappings":"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"}
@@ -34,82 +34,59 @@ resource "random_string" "suffix" {
34
34
  upper = false
35
35
  }
36
36
 
37
- # Access logs bucket for the asset bucket.
38
- resource "aws_s3_bucket" "access_logs" {
39
- #checkov:skip=CKV2_AWS_61:Lifecycle configuration not required for access log bucket
40
- #checkov:skip=CKV_AWS_144:Cross-region replication not required for access log bucket
41
- #checkov:skip=CKV2_AWS_62:Event notifications not required for access log bucket
42
- #checkov:skip=CKV_AWS_18:Access logging the access log bucket would create a cycle
43
- #checkov:skip=CKV_AWS_145:AES256 (S3-managed) encryption is sufficient for access logs
44
- bucket = "${var.bucket_name_prefix}-logs-${data.aws_caller_identity.current.account_id}-${data.aws_region.current.region}-${random_string.suffix.result}"
45
- force_destroy = true
46
-
47
- tags = var.tags
37
+ locals {
38
+ # Delivery source/destination names have a 60 character maximum. Truncate the
39
+ # bucket name prefix so the delivery names stay within the limit.
40
+ access_logs_name_prefix = substr(var.bucket_name_prefix, 0, 16)
48
41
  }
49
42
 
50
- resource "aws_s3_bucket_versioning" "access_logs" {
51
- bucket = aws_s3_bucket.access_logs.id
52
- versioning_configuration {
53
- status = "Enabled"
54
- }
55
- }
56
-
57
- resource "aws_s3_bucket_server_side_encryption_configuration" "access_logs" {
58
- bucket = aws_s3_bucket.access_logs.id
59
-
60
- rule {
61
- apply_server_side_encryption_by_default {
62
- sse_algorithm = "AES256"
63
- }
64
- }
65
- }
66
-
67
- resource "aws_s3_bucket_public_access_block" "access_logs" {
68
- bucket = aws_s3_bucket.access_logs.id
69
-
70
- block_public_acls = true
71
- block_public_policy = true
72
- ignore_public_acls = true
73
- restrict_public_buckets = true
74
- }
75
-
76
- resource "aws_s3_bucket_policy" "access_logs" {
77
- bucket = aws_s3_bucket.access_logs.id
43
+ # KMS key encrypting the server access logs delivered to CloudWatch Logs.
44
+ resource "aws_kms_key" "access_logs" {
45
+ description = "KMS key for ${var.bucket_name_prefix} asset bucket access logs"
46
+ enable_key_rotation = true
47
+ deletion_window_in_days = 7
78
48
 
79
49
  policy = jsonencode({
80
50
  Version = "2012-10-17"
81
51
  Statement = [
82
52
  {
83
- Sid = "DenyInsecureConnections"
84
- Effect = "Deny"
85
- Principal = "*"
86
- Action = "s3:*"
87
- Resource = [
88
- aws_s3_bucket.access_logs.arn,
89
- "${aws_s3_bucket.access_logs.arn}/*"
90
- ]
91
- Condition = {
92
- Bool = {
93
- "aws:SecureTransport" = "false"
94
- }
95
- }
53
+ Sid = "EnableIAMUserPermissions"
54
+ Effect = "Allow"
55
+ Principal = { AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" }
56
+ Action = "kms:*"
57
+ Resource = "*"
96
58
  },
97
59
  {
98
- Sid = "AllowS3LogDelivery"
60
+ Sid = "AllowCloudWatchLogs"
99
61
  Effect = "Allow"
100
- Principal = {
101
- Service = "logging.s3.amazonaws.com"
102
- }
103
- Action = "s3:PutObject"
104
- Resource = "${aws_s3_bucket.access_logs.arn}/*"
62
+ Principal = { Service = "logs.${data.aws_region.current.region}.amazonaws.com" }
63
+ Action = [
64
+ "kms:Encrypt",
65
+ "kms:Decrypt",
66
+ "kms:ReEncrypt*",
67
+ "kms:GenerateDataKey*",
68
+ "kms:DescribeKey"
69
+ ]
70
+ Resource = "*"
105
71
  Condition = {
106
- StringEquals = {
107
- "aws:SourceAccount" = data.aws_caller_identity.current.account_id
72
+ ArnLike = {
73
+ "kms:EncryptionContext:aws:logs:arn" = "arn:aws:logs:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:log-group:*"
108
74
  }
109
75
  }
110
76
  }
111
77
  ]
112
78
  })
79
+
80
+ tags = var.tags
81
+ }
82
+
83
+ # CloudWatch log group receiving the asset bucket server access logs.
84
+ resource "aws_cloudwatch_log_group" "access_logs" {
85
+ name = "/aws/s3/${var.bucket_name_prefix}-access-logs-${random_string.suffix.result}"
86
+ retention_in_days = 365
87
+ kms_key_id = aws_kms_key.access_logs.arn
88
+
89
+ tags = var.tags
113
90
  }
114
91
 
115
92
  # Shared asset bucket
@@ -118,6 +95,7 @@ resource "aws_s3_bucket" "assets" {
118
95
  #checkov:skip=CKV_AWS_144:Cross-region replication not required for asset bucket
119
96
  #checkov:skip=CKV2_AWS_62:Event notifications not required for asset bucket
120
97
  #checkov:skip=CKV_AWS_145:AES256 (S3-managed) encryption is sufficient for build artefacts
98
+ #checkov:skip=CKV_AWS_18:Server access logs are delivered to CloudWatch Logs (see aws_cloudwatch_log_delivery.assets)
121
99
  bucket = "${var.bucket_name_prefix}-${data.aws_caller_identity.current.account_id}-${data.aws_region.current.region}-${random_string.suffix.result}"
122
100
  force_destroy = true
123
101
 
@@ -150,11 +128,24 @@ resource "aws_s3_bucket_public_access_block" "assets" {
150
128
  restrict_public_buckets = true
151
129
  }
152
130
 
153
- resource "aws_s3_bucket_logging" "assets" {
154
- bucket = aws_s3_bucket.assets.id
131
+ # Deliver the asset bucket's server access logs to CloudWatch Logs.
132
+ resource "aws_cloudwatch_log_delivery_source" "assets" {
133
+ name = "${local.access_logs_name_prefix}-access-logs-source-${random_string.suffix.result}"
134
+ log_type = "S3_SERVER_ACCESS_LOGS"
135
+ resource_arn = aws_s3_bucket.assets.arn
136
+ }
137
+
138
+ resource "aws_cloudwatch_log_delivery_destination" "assets" {
139
+ name = "${local.access_logs_name_prefix}-access-logs-dest-${random_string.suffix.result}"
140
+
141
+ delivery_destination_configuration {
142
+ destination_resource_arn = aws_cloudwatch_log_group.access_logs.arn
143
+ }
144
+ }
155
145
 
156
- target_bucket = aws_s3_bucket.access_logs.id
157
- target_prefix = "s3-access-logs/"
146
+ resource "aws_cloudwatch_log_delivery" "assets" {
147
+ delivery_source_name = aws_cloudwatch_log_delivery_source.assets.name
148
+ delivery_destination_arn = aws_cloudwatch_log_delivery_destination.assets.arn
158
149
  }
159
150
 
160
151
  resource "aws_s3_bucket_policy" "assets" {
@@ -192,12 +183,12 @@ output "bucket_arn" {
192
183
  value = aws_s3_bucket.assets.arn
193
184
  }
194
185
 
195
- output "access_logs_bucket_name" {
196
- description = "Name of the access-logs bucket that receives S3 access logs for the asset bucket."
197
- value = aws_s3_bucket.access_logs.id
186
+ output "access_logs_log_group_name" {
187
+ description = "Name of the CloudWatch log group that receives S3 server access logs for the asset bucket."
188
+ value = aws_cloudwatch_log_group.access_logs.name
198
189
  }
199
190
 
200
- output "access_logs_bucket_arn" {
201
- description = "ARN of the access-logs bucket"
202
- value = aws_s3_bucket.access_logs.arn
191
+ output "access_logs_log_group_arn" {
192
+ description = "ARN of the CloudWatch log group that receives S3 server access logs for the asset bucket."
193
+ value = aws_cloudwatch_log_group.access_logs.arn
203
194
  }
@@ -52,3 +52,9 @@ export declare const DEFAULT_BIOME_CONFIG: {
52
52
  * See https://github.com/nrwl/nx/blob/4cd640a9187954505d12de5b6d76a90d8ce4c2eb/packages/devkit/src/generators/format-files.ts#L11
53
53
  */
54
54
  export declare function formatFilesInSubtree(tree: Tree, dir?: string): Promise<void>;
55
+ /**
56
+ * Derive ruff's `target-version` (eg `py314`) from a PEP 508
57
+ * `requires-python` specifier (eg `>=3.14`). Ruff targets the minimum
58
+ * supported version, so take the lowest `major.minor` mentioned.
59
+ */
60
+ export declare const requiresPythonToRuffTarget: (requiresPython: unknown) => string | undefined;
@@ -7,6 +7,7 @@ import { execFileSync, execSync } from "child_process";
7
7
  import { existsSync, readFileSync } from "fs";
8
8
  import { createRequire } from "module";
9
9
  import path from "path";
10
+ import { uvxCommand } from "./py.js";
10
11
  import { readToml } from "./toml.js";
11
12
  const require = createRequire(import.meta.url);
12
13
  export const DEFAULT_BIOME_CONFIG = {
@@ -56,7 +57,10 @@ export const DEFAULT_BIOME_CONFIG = {
56
57
  '!**/node_modules',
57
58
  '!**/.nx',
58
59
  '!**/.venv',
59
- '!**/*.css'
60
+ '!**/*.css',
61
+ '!**/*.gen.*',
62
+ '!**/generated/**',
63
+ '!**/tsconfig*.json'
60
64
  ]
61
65
  }
62
66
  };
@@ -73,6 +77,7 @@ const BIOME_FORMATTABLE_EXTENSIONS = new Set([
73
77
  '.jsonc',
74
78
  '.css'
75
79
  ]);
80
+ /** Matches `tsconfig.json` and variants like `tsconfig.lib.json`. */ const isTsConfig = (filePath)=>/(^|\/)tsconfig[^/]*\.json$/.test(filePath);
76
81
  /**
77
82
  * Format files in the given directory within the tree.
78
83
  * Handles both TypeScript/JavaScript/JSON (via biome) and Python (via ruff) files.
@@ -80,7 +85,12 @@ const BIOME_FORMATTABLE_EXTENSIONS = new Set([
80
85
  */ export async function formatFilesInSubtree(tree, dir) {
81
86
  const changedFiles = tree.listChanges().filter((file)=>file.type !== 'DELETE').filter((file)=>dir ? file.path.startsWith(dir) : true);
82
87
  const pyFiles = changedFiles.filter((file)=>file.path.endsWith('.py'));
83
- const otherFiles = changedFiles.filter((file)=>BIOME_FORMATTABLE_EXTENSIONS.has(path.extname(file.path)));
88
+ const otherFiles = changedFiles.filter((file)=>BIOME_FORMATTABLE_EXTENSIONS.has(path.extname(file.path)) && // tsconfigs are not biome-managed: they're excluded from the vended
89
+ // format target (Nx's typescript-sync rewrites them without formatting),
90
+ // so formatting them at generation would only diverge from the form
91
+ // written on later runs. Leave them as updateJson/writeJson emit them so
92
+ // repeated generation stays idempotent.
93
+ !isTsConfig(file.path));
84
94
  // Resolve each project's ruff settings (module names, line-length) so files
85
95
  // are formatted to match the on-disk build (see getPythonProjectRuffConfigs).
86
96
  const pythonProjectConfigs = pyFiles.length ? getPythonProjectRuffConfigs(tree) : [];
@@ -212,34 +222,32 @@ function getBiomeCommand(root) {
212
222
  }
213
223
  }
214
224
  /**
215
- * Find the ruff command. Tries 'uv run ruff', then 'uvx ruff'.
216
- * Matches how @nxlv/python runs ruff via the UV provider.
225
+ * Find the ruff command: `uvx --from ruff==<version> ruff`. uvx works
226
+ * regardless of workspace resolution state (unlike `uv run ruff`, which fails
227
+ * while installs are deferred), and the version pin matches the project's
228
+ * `format` target (PY_VERSIONS) so generation and check format identically.
229
+ * Only a successful probe is cached — ruff can become available mid-run in the
230
+ * long-lived Nx daemon, so a cached failure would skip formatting thereafter.
217
231
  */ let _ruffCommand;
218
232
  function getRuffCommand() {
219
- if (_ruffCommand !== undefined) {
220
- return _ruffCommand || undefined;
233
+ if (_ruffCommand) {
234
+ return _ruffCommand;
221
235
  }
222
- for (const cmd of [
223
- 'uv run ruff',
224
- 'uvx ruff'
225
- ]){
226
- try {
227
- execSync(`${cmd} --version`, {
228
- encoding: 'utf-8',
229
- stdio: [
230
- 'pipe',
231
- 'pipe',
232
- 'pipe'
233
- ]
234
- });
235
- _ruffCommand = cmd;
236
- return cmd;
237
- } catch {
238
- // Try next command
239
- }
236
+ const cmd = uvxCommand('ruff');
237
+ try {
238
+ execSync(`${cmd} --version`, {
239
+ encoding: 'utf-8',
240
+ stdio: [
241
+ 'pipe',
242
+ 'pipe',
243
+ 'pipe'
244
+ ]
245
+ });
246
+ _ruffCommand = cmd;
247
+ return cmd;
248
+ } catch {
249
+ return undefined;
240
250
  }
241
- _ruffCommand = '';
242
- return undefined;
243
251
  }
244
252
  /**
245
253
  * Whether ruff would discover a config on disk for a file, by walking from its
@@ -268,6 +276,27 @@ function getRuffCommand() {
268
276
  dir = parent;
269
277
  }
270
278
  }
279
+ /**
280
+ * Derive ruff's `target-version` (eg `py314`) from a PEP 508
281
+ * `requires-python` specifier (eg `>=3.14`). Ruff targets the minimum
282
+ * supported version, so take the lowest `major.minor` mentioned.
283
+ */ export const requiresPythonToRuffTarget = (requiresPython)=>{
284
+ if (typeof requiresPython !== 'string') {
285
+ return undefined;
286
+ }
287
+ let min;
288
+ for (const match of requiresPython.matchAll(/(\d+)\.(\d+)/g)){
289
+ const major = Number(match[1]);
290
+ const minor = Number(match[2]);
291
+ if (!min || major < min.major || major === min.major && minor < min.minor) {
292
+ min = {
293
+ major,
294
+ minor
295
+ };
296
+ }
297
+ }
298
+ return min ? `py${min.major}${min.minor}` : undefined;
299
+ };
271
300
  /**
272
301
  * Map each Nx project with a `pyproject.toml` to the ruff settings the on-disk
273
302
  * build enforces for it: its top-level module names (from
@@ -284,11 +313,13 @@ function getRuffCommand() {
284
313
  // all `known-first-party` keys off.
285
314
  const modules = Array.isArray(wheelPackages) ? wheelPackages.filter((pkg)=>typeof pkg === 'string' && !!pkg).map((pkg)=>pkg.split('/')[0]) : [];
286
315
  const lineLength = pyproject?.tool?.ruff?.['line-length'];
287
- if (modules.length || typeof lineLength === 'number') {
316
+ const targetVersion = requiresPythonToRuffTarget(pyproject?.project?.['requires-python']);
317
+ if (modules.length || typeof lineLength === 'number' || targetVersion) {
288
318
  configs.push({
289
319
  root: project.root.split(path.sep).join('/'),
290
320
  modules,
291
- lineLength: typeof lineLength === 'number' ? lineLength : undefined
321
+ lineLength: typeof lineLength === 'number' ? lineLength : undefined,
322
+ targetVersion
292
323
  });
293
324
  }
294
325
  } catch {
@@ -342,6 +373,9 @@ function getRuffCommand() {
342
373
  if (typeof projectConfig?.lineLength === 'number') {
343
374
  configArgs.push(`line-length = ${projectConfig.lineLength}`);
344
375
  }
376
+ if (projectConfig?.targetVersion) {
377
+ configArgs.push(`target-version = "${projectConfig.targetVersion}"`);
378
+ }
345
379
  const config = configArgs.map((arg)=>` --config ${JSON.stringify(arg)}`).join('');
346
380
  // First apply lint fixes (import sorting, unused imports, etc.)
347
381
  try {