@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.
- package/package.json +1 -1
- package/src/infra/app/__snapshots__/generator.spec.ts.snap +42 -0
- package/src/open-api/ts-client/generator.js.map +1 -1
- package/src/open-api/ts-hooks/generator.js.map +1 -1
- package/src/preset/__snapshots__/generator.spec.ts.snap +4 -1
- package/src/py/fast-api/__snapshots__/generator.spec.ts.snap +1 -1
- package/src/py/project/generator.js +27 -0
- package/src/py/project/generator.js.map +1 -1
- package/src/smithy/ts/api/__snapshots__/generator.spec.ts.snap +17 -0
- package/src/ts/lib/__snapshots__/generator.spec.ts.snap +51 -0
- package/src/ts/lib/biome.js +36 -3
- package/src/ts/lib/biome.js.map +1 -1
- package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +572 -195
- package/src/ts/sync/generator.js +0 -2
- package/src/ts/sync/generator.js.map +1 -1
- package/src/utils/ast.js.map +1 -1
- package/src/utils/files/terraform/src/core/asset-bucket/asset-bucket.tf.template +61 -70
- package/src/utils/format.d.ts +6 -0
- package/src/utils/format.js +62 -28
- package/src/utils/format.js.map +1 -1
- package/src/utils/versions.d.ts +1 -0
- package/src/utils/versions.js +1 -0
- package/src/utils/versions.js.map +1 -1
- package/src/utils/warm-ruff-cache.d.ts +8 -0
- package/src/utils/warm-ruff-cache.js +30 -0
- package/src/utils/warm-ruff-cache.js.map +1 -0
- package/src/utils/website-constructs/files/cdk/core/static-website.ts.template +127 -36
- package/src/utils/website-constructs/files/terraform/core/static-website/static-website.tf.template +77 -74
package/src/ts/sync/generator.js
CHANGED
|
@@ -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 {
|
|
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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"}
|
package/src/utils/ast.js.map
CHANGED
|
@@ -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
|
-
|
|
38
|
-
|
|
39
|
-
#
|
|
40
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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 = "
|
|
84
|
-
Effect = "
|
|
85
|
-
Principal = "
|
|
86
|
-
Action = "
|
|
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 = "
|
|
60
|
+
Sid = "AllowCloudWatchLogs"
|
|
99
61
|
Effect = "Allow"
|
|
100
|
-
Principal = {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
"aws:
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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 "
|
|
196
|
-
description = "Name of the
|
|
197
|
-
value =
|
|
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 "
|
|
201
|
-
description = "ARN of the access
|
|
202
|
-
value =
|
|
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
|
}
|
package/src/utils/format.d.ts
CHANGED
|
@@ -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;
|
package/src/utils/format.js
CHANGED
|
@@ -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
|
|
216
|
-
*
|
|
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
|
|
220
|
-
return _ruffCommand
|
|
233
|
+
if (_ruffCommand) {
|
|
234
|
+
return _ruffCommand;
|
|
221
235
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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 {
|