From bc8c10d15e4c8b893a159d14904c0ddb9fe68b7f Mon Sep 17 00:00:00 2001 From: Max M Date: Fri, 26 Sep 2025 14:45:07 +0100 Subject: [PATCH 1/4] Create temp --- .github/dusty/temp | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/dusty/temp diff --git a/.github/dusty/temp b/.github/dusty/temp new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/.github/dusty/temp @@ -0,0 +1 @@ + From e9a603217770869c5cd5a7cada3900fe7edf2ca3 Mon Sep 17 00:00:00 2001 From: Max M Date: Fri, 26 Sep 2025 14:46:21 +0100 Subject: [PATCH 2/4] Add files via upload --- .../dusty/lazy-import-heavy-js-deps/detect.ts | 188 +++++++++++++++++ .../fixtures/already_lazy.ts | 20 ++ .../fixtures/no_violation_module_level.ts | 21 ++ .../fixtures/violation.ts | 23 +++ .../heavy-modules-js.ts | 22 ++ .../lazy-import-heavy-js-deps/resolve.ts | 193 ++++++++++++++++++ .../dusty/lazy-import-heavy-js-deps/rule.md | 35 ++++ .../tests/expectations.yml | 35 ++++ 8 files changed, 537 insertions(+) create mode 100644 .github/dusty/lazy-import-heavy-js-deps/detect.ts create mode 100644 .github/dusty/lazy-import-heavy-js-deps/fixtures/already_lazy.ts create mode 100644 .github/dusty/lazy-import-heavy-js-deps/fixtures/no_violation_module_level.ts create mode 100644 .github/dusty/lazy-import-heavy-js-deps/fixtures/violation.ts create mode 100644 .github/dusty/lazy-import-heavy-js-deps/heavy-modules-js.ts create mode 100644 .github/dusty/lazy-import-heavy-js-deps/resolve.ts create mode 100644 .github/dusty/lazy-import-heavy-js-deps/rule.md create mode 100644 .github/dusty/lazy-import-heavy-js-deps/tests/expectations.yml diff --git a/.github/dusty/lazy-import-heavy-js-deps/detect.ts b/.github/dusty/lazy-import-heavy-js-deps/detect.ts new file mode 100644 index 00000000..13331f45 --- /dev/null +++ b/.github/dusty/lazy-import-heavy-js-deps/detect.ts @@ -0,0 +1,188 @@ +// Detect top-level heavy imports in TS/JS files (no edits) +// Usage: tsx detect.ts + +import fs from 'fs'; +import ts from 'typescript'; +import heavyModulesJs from './heavy-modules-js'; + +type ImportKind = 'es-import' | 'cjs-require'; +type ImportBinding = { + kind: ImportKind; + module: string; + defaultName?: string; + namespaceName?: string; + namedBindings?: Array<{ property: string; name: string }>; + cjsVarName?: string; + cjsDestructure?: Array<{ property: string; name: string }>; + node: ts.Node; +}; + +function getHeavyModules(): string[] { + const env = process.env.HEAVY_MODULES_JS; + if (env) return env.split(',').map((m) => m.trim()); + return heavyModulesJs; +} + +function isHeavyModule(mod: string, heavy: string[]): boolean { + const name = mod.toLowerCase(); + return heavy.some((hm) => { + const h = hm.toLowerCase(); + if (h.endsWith('*')) return name.startsWith(h.slice(0, -1)); + return name === h; + }); +} + +function collectTopLevelImports(sf: ts.SourceFile, heavy: string[]): ImportBinding[] { + const out: ImportBinding[] = []; + sf.forEachChild((node) => { + if (ts.isImportDeclaration(node) && node.importClause && ts.isStringLiteral(node.moduleSpecifier)) { + const mod = node.moduleSpecifier.text; + if (!isHeavyModule(mod, heavy)) return; + if (node.importClause.isTypeOnly) return; + const binding: ImportBinding = { kind: 'es-import', module: mod, node }; + if (node.importClause.name) binding.defaultName = node.importClause.name.text; + if (node.importClause.namedBindings) { + if (ts.isNamespaceImport(node.importClause.namedBindings)) { + binding.namespaceName = node.importClause.namedBindings.name.text; + } else if (ts.isNamedImports(node.importClause.namedBindings)) { + binding.namedBindings = node.importClause.namedBindings.elements.map((el) => ({ + property: el.propertyName ? el.propertyName.text : el.name.text, + name: el.name.text, + })); + } + } + if (!binding.defaultName && !binding.namespaceName && !binding.namedBindings?.length) return; + out.push(binding); + return; + } + if (ts.isVariableStatement(node)) { + for (const decl of node.declarationList.declarations) { + const init = decl.initializer; + if (!init || !ts.isCallExpression(init)) continue; + if (!ts.isIdentifier(init.expression) || init.expression.text !== 'require') continue; + if (init.arguments.length !== 1 || !ts.isStringLiteral(init.arguments[0])) continue; + const mod = (init.arguments[0] as ts.StringLiteral).text; + if (!isHeavyModule(mod, heavy)) continue; + const binding: ImportBinding = { kind: 'cjs-require', module: mod, node }; + if (ts.isIdentifier(decl.name)) { + binding.cjsVarName = decl.name.text; + } else if (ts.isObjectBindingPattern(decl.name)) { + binding.cjsDestructure = decl.name.elements.map((el) => ({ + property: el.propertyName ? (el.propertyName as ts.Identifier).text : (el.name as ts.Identifier).text, + name: (el.name as ts.Identifier).text, + })); + } else continue; + out.push(binding); + } + } + }); + return out; +} + +function isInTypePosition(node: ts.Node): boolean { + for (let p: ts.Node | undefined = node.parent; p; p = p.parent) { + if (ts.isTypeNode(p)) return true; + if (ts.isImportTypeNode && (ts as any).isImportTypeNode(p)) return true; + if (ts.isTypeAliasDeclaration(p) || ts.isInterfaceDeclaration(p)) return true; + } + return false; +} + +function isImportBindingIdentifier(node: ts.Identifier): boolean { + for (let p: ts.Node | undefined = node.parent; p; p = p.parent) { + if ( + ts.isImportDeclaration(p) || + ts.isImportClause(p) || + ts.isNamespaceImport(p) || + ts.isNamedImports(p) || + ts.isImportSpecifier(p) + ) + return true; + if (ts.isSourceFile(p)) break; + } + return false; +} + +function isRequireBindingIdentifier(node: ts.Identifier): boolean { + const parent = node.parent; + if (ts.isVariableDeclaration(parent) && parent.name === node) return true; + if (ts.isBindingElement(parent) && parent.name === node) return true; + return false; +} + +function collectDeclaredNames(binding: ImportBinding): string[] { + const names: string[] = []; + if (binding.kind === 'cjs-require') { + if (binding.cjsVarName) names.push(binding.cjsVarName); + if (binding.cjsDestructure) names.push(...binding.cjsDestructure.map((b) => b.name)); + return names; + } + if (binding.defaultName) names.push(binding.defaultName); + if (binding.namespaceName) names.push(binding.namespaceName); + if (binding.namedBindings) names.push(...binding.namedBindings.map((b) => b.name)); + return names; +} + +function findUsages(sf: ts.SourceFile, names: string[]) { + const usages: { topLevel?: true; fn?: ts.FunctionLikeDeclarationBase | ts.MethodDeclaration }[] = []; + function nearestFunction(node: ts.Node): ts.FunctionLikeDeclarationBase | ts.MethodDeclaration | undefined { + for (let p: ts.Node | undefined = node.parent; p; p = p.parent) { + if (ts.isFunctionLike(p) || ts.isMethodDeclaration(p)) return p as any; + } + return undefined; + } + function visit(node: ts.Node) { + if (ts.isIdentifier(node) && names.includes(node.text)) { + if (isImportBindingIdentifier(node) || isRequireBindingIdentifier(node) || isInTypePosition(node)) { + // skip + } else { + const fn = nearestFunction(node); + if (!fn) usages.push({ topLevel: true }); + else usages.push({ fn }); + } + } + ts.forEachChild(node, visit); + } + visit(sf); + return usages; +} + +function processFile(filePath: string) { + const heavy = getHeavyModules(); + const source = fs.readFileSync(filePath, 'utf8'); + const kind = filePath.endsWith('.tsx') + ? ts.ScriptKind.TSX + : filePath.endsWith('.ts') + ? ts.ScriptKind.TS + : filePath.endsWith('.jsx') + ? ts.ScriptKind.JSX + : ts.ScriptKind.JS; + const sf = ts.createSourceFile(filePath, source, ts.ScriptTarget.ES2022, true, kind); + const imports = collectTopLevelImports(sf, heavy); + for (const imp of imports) { + const names = collectDeclaredNames(imp); + if (names.length === 0) continue; + const usages = findUsages(sf, names); + const topLevel = usages.some((u) => u.topLevel); + const inFns = usages.filter((u) => !!u.fn).length > 0; + if (!topLevel && inFns) { + const loc = sf.getLineAndCharacterOfPosition(imp.node.getStart()).line + 1; + const detection = { + path: filePath, + description: `Top-level import of heavy module '${imp.module}' used only inside function/method scopes: consider lazy require inside those scopes`, + location: `import line ${loc}`, + }; + process.stdout.write(JSON.stringify(detection) + '\n'); + } + } +} + +if (require.main === module) { + const filePath = process.argv[2]; + if (!filePath) { + console.error('Usage: tsx detect.ts '); + process.exit(1); + } + processFile(filePath); +} + diff --git a/.github/dusty/lazy-import-heavy-js-deps/fixtures/already_lazy.ts b/.github/dusty/lazy-import-heavy-js-deps/fixtures/already_lazy.ts new file mode 100644 index 00000000..bd23f220 --- /dev/null +++ b/.github/dusty/lazy-import-heavy-js-deps/fixtures/already_lazy.ts @@ -0,0 +1,20 @@ +export function measure(): boolean { + // Already lazy via function-local require + const AWS = require('aws-sdk'); + return !!AWS; +} + +export function noHeavy(): string { + return 'no heavy module used'; +} + +export class Checker { + run() { + const AWS = require('aws-sdk'); + return new AWS.S3().listBuckets().promise(); + } + noop() { + return 'noop'; + } +} + diff --git a/.github/dusty/lazy-import-heavy-js-deps/fixtures/no_violation_module_level.ts b/.github/dusty/lazy-import-heavy-js-deps/fixtures/no_violation_module_level.ts new file mode 100644 index 00000000..5283233c --- /dev/null +++ b/.github/dusty/lazy-import-heavy-js-deps/fixtures/no_violation_module_level.ts @@ -0,0 +1,21 @@ +import { S3 } from 'aws-sdk'; + +const CLIENT = new S3(); + +export function process(): Promise { + return CLIENT.listBuckets().promise(); +} + +export function noHeavy(): string { + return 'no heavy module used'; +} + +export class Processor { + run() { + return CLIENT.listBuckets().promise(); + } + noop() { + return 'noop'; + } +} + diff --git a/.github/dusty/lazy-import-heavy-js-deps/fixtures/violation.ts b/.github/dusty/lazy-import-heavy-js-deps/fixtures/violation.ts new file mode 100644 index 00000000..ece7f8f5 --- /dev/null +++ b/.github/dusty/lazy-import-heavy-js-deps/fixtures/violation.ts @@ -0,0 +1,23 @@ +import * as AWS from 'aws-sdk'; + +export function upload(data: Buffer): Promise { + const s3 = new AWS.S3(); + return s3 + .upload({ Bucket: 'my-bucket', Key: 'file.bin', Body: data }) + .promise() + .then((r: any) => r.Location); +} + +export class Runner { + run(): any { + const s3 = new AWS.S3(); + return s3.listBuckets().promise(); + } + noop() { + return 'noop'; + } +} + +export function noHeavy(): string { + return 'no heavy module used'; +} diff --git a/.github/dusty/lazy-import-heavy-js-deps/heavy-modules-js.ts b/.github/dusty/lazy-import-heavy-js-deps/heavy-modules-js.ts new file mode 100644 index 00000000..8ee2270f --- /dev/null +++ b/.github/dusty/lazy-import-heavy-js-deps/heavy-modules-js.ts @@ -0,0 +1,22 @@ +// List of heavy JavaScript/TypeScript modules for lazy-import-heavy-js-deps rule +// You can override this list via the HEAVY_MODULES_JS environment variable (comma-separated) + +const heavyModulesJs: string[] = [ + // Cloud SDKs (often large) + "aws-sdk", "@aws-sdk/*", "firebase-admin", "firebase", "@google-cloud/*", "@azure/*", + // Headless browsers / automation + "puppeteer", "playwright", + // Native/binary-heavy + "sharp", "canvas", "opencv4nodejs", "nodegit", + // Data/ML in Node + "@tensorflow/tfjs-node", "onnxruntime-node", + // PDF/Excel and large utilities + "pdfkit", "pdf-lib", "xlsx", "exceljs", + // ORMs / DB clients (can be significant) + "typeorm", "sequelize", "prisma", "mongoose", "pg", "mysql2", + // 3D/Graphics + "three", "babylonjs", +]; + +export default heavyModulesJs; + diff --git a/.github/dusty/lazy-import-heavy-js-deps/resolve.ts b/.github/dusty/lazy-import-heavy-js-deps/resolve.ts new file mode 100644 index 00000000..596bd91b --- /dev/null +++ b/.github/dusty/lazy-import-heavy-js-deps/resolve.ts @@ -0,0 +1,193 @@ +// Resolve: move heavy imports to function-local require statements based on detections +// Usage: tsx resolve.ts + +import fs from 'fs'; +import ts from 'typescript'; + +type Detection = { path: string; description?: string; location?: string }; + +function computeIndentation(source: string, pos: number): string { + let i = pos; + while (i > 0 && source[i - 1] !== '\n' && source[i - 1] !== '\r') i--; + let j = i; + while (j < source.length && (source[j] === ' ' || source[j] === '\t')) j++; + return source.slice(i, j); +} + +function parseModuleFromDescription(desc: string | undefined): string | null { + if (!desc) return null; + const m = desc.match(/heavy module '([^']+)'/); + return m ? m[1] : null; +} + +function collectTopLevelImportForModule(sf: ts.SourceFile, moduleName: string) { + let found: { node: ts.Node; namespaceName?: string; named?: Array<{ property: string; name: string }>; defaultName?: string } | null = null; + sf.forEachChild((node) => { + if (found) return; + if (ts.isImportDeclaration(node) && node.importClause && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === moduleName) { + const rec: any = { node }; + if (node.importClause.name) rec.defaultName = node.importClause.name.text; + if (node.importClause.namedBindings) { + if (ts.isNamespaceImport(node.importClause.namedBindings)) { + rec.namespaceName = node.importClause.namedBindings.name.text; + } else if (ts.isNamedImports(node.importClause.namedBindings)) { + rec.named = node.importClause.namedBindings.elements.map((el) => ({ + property: el.propertyName ? el.propertyName.text : el.name.text, + name: el.name.text, + })); + } + } + found = rec; + return; + } + if (ts.isVariableStatement(node)) { + for (const decl of node.declarationList.declarations) { + const init = decl.initializer; + if (!init || !ts.isCallExpression(init)) continue; + if (!ts.isIdentifier(init.expression) || init.expression.text !== 'require') continue; + if (init.arguments.length !== 1 || !ts.isStringLiteral(init.arguments[0])) continue; + const mod = (init.arguments[0] as ts.StringLiteral).text; + if (mod !== moduleName) continue; + const rec: any = { node }; + if (ts.isIdentifier(decl.name)) { + rec.namespaceName = (decl.name as ts.Identifier).text; + } else if (ts.isObjectBindingPattern(decl.name)) { + rec.named = decl.name.elements.map((el) => ({ + property: el.propertyName ? (el.propertyName as ts.Identifier).text : (el.name as ts.Identifier).text, + name: (el.name as ts.Identifier).text, + })); + } + found = rec; + } + } + }); + return found; +} + +function buildRequireStmt(binding: { namespaceName?: string; named?: Array<{ property: string; name: string }>; defaultName?: string }, moduleName: string): string | null { + if (binding.namespaceName) return `const ${binding.namespaceName} = require('${moduleName}');`; + if (binding.named && binding.named.length) { + const inside = binding.named + .map((b) => (b.property === b.name ? b.name : `${b.property}: ${b.name}`)) + .join(', '); + return `const { ${inside} } = require('${moduleName}');`; + } + if (binding.defaultName) return null; // avoid ESM/CJS default ambiguity + return null; +} + +function processFile(filePath: string, detectionsJson: string) { + const detections: Detection[] = (() => { + try { + const parsed = JSON.parse(detectionsJson); + return Array.isArray(parsed) ? parsed : [parsed]; + } catch { + return []; + } + })(); + + if (!detections.length) return; + + const source = fs.readFileSync(filePath, 'utf8'); + const kind = filePath.endsWith('.tsx') + ? ts.ScriptKind.TSX + : filePath.endsWith('.ts') + ? ts.ScriptKind.TS + : filePath.endsWith('.jsx') + ? ts.ScriptKind.JSX + : ts.ScriptKind.JS; + const sf = ts.createSourceFile(filePath, source, ts.ScriptTarget.ES2022, true, kind); + + let newSource = source; + const resolutions: any[] = []; + + for (const det of detections) { + const mod = parseModuleFromDescription(det.description) || ''; + if (!mod) continue; + const found = collectTopLevelImportForModule(sf, mod); + if (!found) continue; + const requireStmt = buildRequireStmt(found, mod); + if (!requireStmt) continue; + + const remNode = (found as any).node as ts.Node; + // Edits: plan relative to original source, then apply in descending order + const edits: Array<{ start: number; end: number; insert: string }> = []; + + // Removal of the top-level import/require statement + let remStart = remNode.getFullStart(); + let remEnd = remNode.getEnd(); + while (remEnd < source.length && source[remEnd] !== '\n') remEnd++; + if (remEnd < source.length) remEnd++; + edits.push({ start: remStart, end: remEnd, insert: '' }); + + // Insert at start of every function where module is used + const names: string[] = []; + if (found.namespaceName) names.push(found.namespaceName); + if (found.named) names.push(...found.named.map((n) => n.name)); + + const fnBodies: ts.Block[] = []; + function nearestFunction(node: ts.Node): ts.FunctionLikeDeclarationBase | ts.MethodDeclaration | undefined { + for (let p: ts.Node | undefined = node.parent; p; p = p.parent) { + if (ts.isFunctionLike(p) || ts.isMethodDeclaration(p)) return p as any; + } + return undefined; + } + function isType(node: ts.Node): boolean { + for (let p: ts.Node | undefined = node.parent; p; p = p.parent) { + if (ts.isTypeNode(p)) return true; + if (ts.isImportTypeNode && (ts as any).isImportTypeNode(p)) return true; + } + return false; + } + function visit(n: ts.Node) { + if (ts.isIdentifier(n) && names.includes(n.text) && !isType(n)) { + const fn = nearestFunction(n); + if (fn && fn.body && ts.isBlock(fn.body)) fnBodies.push(fn.body); + } + ts.forEachChild(n, visit); + } + visit(sf); + + const uniqueBodies = Array.from(new Set(fnBodies)); + for (const b of uniqueBodies) { + const afterBrace = b.getStart() + 1; // position of '{' + 1 + const atLineBreak = source[afterBrace] === '\n'; + const insertPos = atLineBreak ? afterBrace + 1 : afterBrace; + const prefix = atLineBreak ? '' : '\n'; + const indent = computeIndentation(source, insertPos) + ' '; + edits.push({ start: insertPos, end: insertPos, insert: `${prefix}${indent}${requireStmt}\n` }); + } + + // Apply edits on the original source descending by position + edits.sort((a, b) => b.start - a.start); + let text = source; + for (const e of edits) { + text = text.slice(0, e.start) + e.insert + text.slice(e.end); + } + newSource = text; + + resolutions.push({ + detection: { path: filePath, description: det.description || '' }, + success: true, + description: `Moved '${mod}' import under function/method definitions where used (removed top-level import).`, + }); + } + + if (newSource !== source) { + fs.writeFileSync(filePath, newSource, 'utf8'); + } + + for (const r of resolutions) { + process.stdout.write(JSON.stringify(r) + '\n'); + } +} + +if (require.main === module) { + const filePath = process.argv[2]; + const detectionsJson = process.argv[3] || '[]'; + if (!filePath) { + console.error('Usage: tsx resolve.ts '); + process.exit(1); + } + processFile(filePath, detectionsJson); +} diff --git a/.github/dusty/lazy-import-heavy-js-deps/rule.md b/.github/dusty/lazy-import-heavy-js-deps/rule.md new file mode 100644 index 00000000..2ede74eb --- /dev/null +++ b/.github/dusty/lazy-import-heavy-js-deps/rule.md @@ -0,0 +1,35 @@ +# lazy-import-heavy-js-deps: Lazily load heavy, optional JS/TS dependencies only inside the code paths that need them to reduce cold-start time, memory footprint, and energy use + +## Qualify + +Applies to TypeScript/JavaScript application (non-generated, non-third-party) source files. + +Glob patterns: +- **/*.ts +- **/*.tsx +- **/*.js +- **/*.jsx +- !**/node_modules/** +- !**/dist/** +- !**/build/** +- !**/.next/** +- !**/.turbo/** +- !**/coverage/** +- !**/vendor/** +- !**/third_party/** +- !**/third-parties/** +- !**/external/** + +Most impactful for CLI entrypoints, serverless handlers, and feature-modular libraries. + +## Detect + +Script: `detect.ts` + +Identify files where a heavy module is imported at module scope (via `import … from 'pkg'` or `const … = require('pkg')`) but is only used inside functions/methods/blocks (i.e., no top-level usage). Prefer precise AST-based checks using the TypeScript compiler API. + +## Resolve + +Script: `resolve.ts` + +When safe, remove the top-level heavy import and insert a function-local `require('pkg')` statement at the beginning of each function/method that uses it. Preserve identifiers and aliases for namespace and named imports. Skip transforms that are unsafe for ESM-only semantics (e.g., default imports that cannot be accurately emulated with `require`) or when usages occur in top-level initializers. diff --git a/.github/dusty/lazy-import-heavy-js-deps/tests/expectations.yml b/.github/dusty/lazy-import-heavy-js-deps/tests/expectations.yml new file mode 100644 index 00000000..37b2d57a --- /dev/null +++ b/.github/dusty/lazy-import-heavy-js-deps/tests/expectations.yml @@ -0,0 +1,35 @@ +"violation.ts": + qualify: true + detect: true + resolve: true + contains: + - "export class Runner" + - "export function upload(" + - "export function noHeavy(" + regex: + - 'export function upload\([^)]*\):[^\n]*\{\n\s+const AWS = require\(''aws-sdk''\);' + - 'export class Runner[^{]*\{\n\s+run\(\):[^\n]*\{\n\s+const AWS = require\(''aws-sdk''\);' + +"no_violation_module_level.ts": + qualify: true + detect: false + resolve: false + contains: + - "const CLIENT = new S3()" + - "export class Processor" + - "export function process()" + - "export function noHeavy()" + not_contains: + - "const AWS = require('aws-sdk')" + +"already_lazy.ts": + qualify: true + detect: false + resolve: false + contains: + - "export function measure()" + - "export class Checker" + - "export function noHeavy()" + regex: + - 'export function measure\(\):[^\n]*\{\n\s+// Already lazy via function-local require\n\s+const AWS = require\(''aws-sdk''\);' + - 'export class Checker[^{]*\{\n\s+run\(\)[^\n]*\{\n\s+const AWS = require\(''aws-sdk''\);' From 021a3fdeb3d751e537098002f672faf23d8cadfe Mon Sep 17 00:00:00 2001 From: Max M Date: Fri, 26 Sep 2025 14:46:40 +0100 Subject: [PATCH 3/4] Delete .github/dusty/temp --- .github/dusty/temp | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .github/dusty/temp diff --git a/.github/dusty/temp b/.github/dusty/temp deleted file mode 100644 index 8b137891..00000000 --- a/.github/dusty/temp +++ /dev/null @@ -1 +0,0 @@ - From 96b10809771c11a0eec006d6087d79c2af70fb97 Mon Sep 17 00:00:00 2001 From: Max M Date: Fri, 26 Sep 2025 14:47:46 +0100 Subject: [PATCH 4/4] Add Dusty workflow for pull requests and pushes --- .github/workflows/dusty.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/dusty.yml diff --git a/.github/workflows/dusty.yml b/.github/workflows/dusty.yml new file mode 100644 index 00000000..2a7c6f96 --- /dev/null +++ b/.github/workflows/dusty.yml @@ -0,0 +1,23 @@ +# File: .github/workflows/dusty.yml +name: Dusty +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + push: + branches: [main] + schedule: + - cron: '0 12 * * 1' # optional + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +jobs: + dusty: + uses: githubnext/dusty/.github/workflows/dusty.yml@main + with: + # optional; default is .github/dusty + rules-path: .github/dusty + # for monorepos, point to a subdirectory containing rules + # rules-path: packages/app/.github/dusty