Skip to content

Commit

Permalink
feat(lint-synthesized): Lint synthesized files
Browse files Browse the repository at this point in the history
  • Loading branch information
langri-sha committed Apr 29, 2024
1 parent 51b1580 commit 149fb0f
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 6 deletions.
9 changes: 9 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,20 @@ const config: Config = {
...defaults,
transformIgnorePatterns: [
`node_modules/(?!(?:.pnpm/)?(${[
'execa',
'find-up',
'get-stream',
'human-signals',
'is-stream',
'locate-path',
'mimic-fn',
'npm-run-path',
'onetime',
'p-limit',
'p-locate',
'path-exists',
'path-key',
'strip-final-newline',
'unicorn-magic',
'yocto-queue',
].join('|')}))`,
Expand Down
48 changes: 43 additions & 5 deletions packages/projen-lint-synthesized/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { directorySnapshot } from 'projen/lib/util/synth'
import { expect, tempy, test } from '@langri-sha/jest-test'

import { Project } from 'projen'
import { LintSynthesized } from './index'
import { promises as fs } from 'node:fs'
import path from 'node:path'

const setup = () => {
import { Project, TextFile } from 'projen'
import { LintSynthesized, type LintSynthesizedOptions } from './index'

const setup = (options?: LintSynthesizedOptions) => {
const outdir = tempy.directory()

const project = new Project({
Expand All @@ -21,9 +24,9 @@ const setup = () => {
project.removeTask('pre-compile')
project.removeTask('test')

new LintSynthesized(project)
new LintSynthesized(project, options)

return { outdir, project }
return { project }
}

test('defaults', () => {
Expand All @@ -32,3 +35,38 @@ test('defaults', () => {
project.synth()
expect(directorySnapshot(project.outdir)).toMatchSnapshot()
})

test('lints synthesized files', async () => {
const { project } = setup({
'*': 'prettier --ignore-unknown --write',
})

let file

file = new TextFile(project, 'test.js')
file.addLine(`module.exports = ${JSON.stringify({ foo: 'bar' })}`)

project.synth()

file = await fs.readFile(path.join(project.outdir, 'test.js'))
const contents = file.toString('utf8')

expect(contents).toEqual(`module.exports = { foo: "bar" };\n`)
})

test('preserves file modes', async () => {
const { project } = setup({
'*': 'prettier --ignore-unknown',
})

new TextFile(project, 'test.sh', {
executable: true,
readonly: true,
})

project.synth()

await expect(
fs.stat(path.join(project.outdir, 'test.sh')),
).resolves.toHaveProperty('mode', 33_124)
})
86 changes: 85 additions & 1 deletion packages/projen-lint-synthesized/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,102 @@
import { Component } from 'projen'
import { debug as createDebug } from 'debug'

import * as fs from 'node:fs'

import { execaSync } from 'execa'
import { minimatch } from 'minimatch'

const debug = createDebug('projen-lint-synthesized')

export interface LintSynthesizedOptions {
[pattern: string]: string | ((files: string[]) => string)
}

/**
* A component that lints synthesized files.
*/
export class LintSynthesized extends Component {
constructor(scope: ConstructorParameters<typeof Component>[0]) {
#options?: LintSynthesizedOptions

constructor(
scope: ConstructorParameters<typeof Component>[0],
options?: LintSynthesizedOptions,
) {
super(scope, 'lint-synthesized')

this.#options = options

debug('Initialized')
}

override postSynthesize(): void {
super.postSynthesize()

debug('Commencing lints on synthesized files')

const fileInfo = Object.fromEntries(
this.project.files
.filter((file) => fs.existsSync(file.absolutePath))
.map((file) => [
file.path,
{
file,
absolutePath: file.absolutePath,
mode: fs.statSync(file.absolutePath).mode,
},
]),
)
const paths = Object.keys(fileInfo)

debug(`Found ${paths.length} synthesized files`)

const tasks = Object.entries(this.#options || {})
.map(([pattern, command]) => ({
pattern,
command,
files: minimatch.match(paths, pattern),
}))
.filter(({ files }) => files.length)

for (const { files } of tasks) {
for (const file of files) {
const { absolutePath } = fileInfo[file]

fs.chmodSync(absolutePath, 0o755)
}
}

for (const { pattern, command, files } of tasks) {
debug(`Running command ${files.length} files matching pattern ${pattern}`)

try {
this.#run(files, command)
} catch {
// Let `execa` log errors.
}
}

for (const { files } of tasks) {
for (const file of files) {
const { absolutePath, mode } = fileInfo[file]

fs.chmodSync(absolutePath, mode)
}
}
}

#run(files: string[], command: string | ((files: string[]) => string)) {
const task =
typeof command === 'string'
? `${command} ${files.join('')}`
: command(files)

debug(`Running command ${task}`)

execaSync(task, {
shell: true,
stdio: 'inherit',
cwd: this.project.outdir,
})
}
}

0 comments on commit 149fb0f

Please sign in to comment.