Skip to content

Commit

Permalink
feat: clean option is protected against deleting sensitive folders
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremy committed Jul 20, 2019
1 parent 4c9f5b2 commit cba911d
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 44 deletions.
16 changes: 12 additions & 4 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ yarn add -D tsc-prog

_Tsc-prog has no dependency. You just need typescript as a peer dependency._

### You simply need to build
### You simply need to build 👷‍

Use **`tsc.build`**.

Expand All @@ -34,7 +34,7 @@ tsc.build({

You can have a look at the parameters **[here](./src/interfaces.ts)**.

### You need more access
### You need more access 👨‍🏭

The `tsc.build` function is made of the two following steps, which you can have access to :

Expand All @@ -58,9 +58,9 @@ tsc.emit(program, { betterDiagnostics: true })

## Addons

### Clean
### Clean 🧹

We frequently need to clean the emmited files before a new build, so a `clean` option recursively removes folders and files :
We frequently need to clean the emmited files before a new build, so a **`clean`** option recursively removes folders and files :

```js
tsc.build({
Expand All @@ -83,3 +83,11 @@ tsc.build({
clean: { outDir: true, declarationDir: true },
})
```

##### Protections 🛡️

The `clean` option protects you against deleting the following folders :

- the specified `basePath` and all its parents (up to the root folder).
- the current working directory and all its parents (up to the root folder).
- the `rootDir` path if specified in the compiler options.
78 changes: 78 additions & 0 deletions src/clean-addon/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { EmitOptions, CompilerOptions } from '../interfaces'
import { ensureAbsolutePath, relativePath, normalizePath, parentPaths } from '../path.utils'
import rmrf from './rmrf'

/**
* @internal
*/
export default function cleanTargets(
targets: Required<EmitOptions>['clean'],
compilerOptions: CompilerOptions,
basePath: string = process.cwd()
) {
protectSensitiveFolders(targets, compilerOptions, basePath)

if (Array.isArray(targets)) {
for (let target of targets) {
target = ensureAbsolutePath(target, basePath)
console.log('Cleaning:', relativePath(target))
rmrf(target)
}
} else {
const { outDir, outFile, declarationDir } = compilerOptions

if (targets.outDir && outDir) {
console.log('Cleaning outDir:', relativePath(outDir))
rmrf(outDir)
}

if (targets.declarationDir && declarationDir) {
console.log('Cleaning declarationDir:', relativePath(declarationDir))
rmrf(declarationDir)
}

if (targets.outFile && outFile) {
console.log('Cleaning outFile:', relativePath(outFile))
rmrf(outFile)
}
}
}

/**
* @internal
*/
function protectSensitiveFolders(
targets: Required<EmitOptions>['clean'],
compilerOptions: CompilerOptions,
basePath: string
) {
const { rootDir } = compilerOptions
const cwd = process.cwd()

const protectedDirs: (string | undefined)[] = [rootDir, cwd, ...parentPaths(cwd)]

if (cwd !== basePath) {
protectedDirs.push(basePath, ...parentPaths(basePath))
}

const check = (target: string) => {
for (const dir of protectedDirs) {
// `compilerOptions` properties returns unix separators in windows paths so we must normalize
if (target === normalizePath(dir)) throw Error(`You cannot delete ${target}`)
}
}

if (Array.isArray(targets)) {
for (let target of targets) {
target = ensureAbsolutePath(target, basePath)
check(target)
}
} else {
for (const target of Object.keys(targets) as (keyof typeof targets)[]) {
if (compilerOptions[target]) {
// We can have outDir/declarationDir to be the same as rootDir
check(compilerOptions[target]!)
}
}
}
}
22 changes: 9 additions & 13 deletions src/addons.ts → src/clean-addon/rmrf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { join, relative } from 'path'
* Inspired from {@link https://github.com/isaacs/rimraf}
* @internal
*/
export function rmrf(path: string) {
export default function rmrf(path: string) {
// TODO: rely on ENOENT instead
// https://nodejs.org/dist/latest/docs/api/fs.html#fs_fs_exists_path_callback
if (!existsSync(path)) return
Expand All @@ -14,13 +14,12 @@ export function rmrf(path: string) {
else tryDeleteFile(path)

function deleteRecursive(dirPath: string) {
const fileNames = readdirSync(dirPath)
// if (fileNames && fileNames.length)
for (const fileName of fileNames) {
const filePath = join(dirPath, fileName)
const names = readdirSync(dirPath)
for (const name of names) {
const path_ = join(dirPath, name)

if (tryIsDirectory(filePath)) deleteRecursive(filePath)
else tryDeleteFile(filePath)
if (tryIsDirectory(path_)) deleteRecursive(path_)
else tryDeleteFile(path_)
}
tryDeleteEmptyDir(dirPath)
}
Expand Down Expand Up @@ -80,11 +79,8 @@ function fsSyncRetry<T extends (path: string) => any>(
// tslint:disable-next-line: no-empty
while (Date.now() < until) {}
}
}

/**
* @internal
*/
function fixWindowsEPERM(path: string) {
if (process.platform === 'win32') chmodSync(path, 0o666)
function fixWindowsEPERM(path_: string) {
if (process.platform === 'win32') chmodSync(path_, 0o666)
}
}
45 changes: 44 additions & 1 deletion src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { join, normalize } from 'path'
import ts from 'typescript'
import { createProgramFromConfig, build } from '.'

jest.mock('./clean-addon/rmrf')

const basePath = join(__dirname, '__fixtures__')
const configFilePath = 'tsconfig.fixture.json'

Expand All @@ -23,7 +25,7 @@ test('Create program by overriding config file', async () => {
strict: true,
// `compilerOptions` properties returns unix separators in windows paths
rootDir: normalize(join(basePath, 'src')),
declaration: false,
declaration: 'true',
})

expect(program.getRootFileNames()).toHaveLength(1)
Expand Down Expand Up @@ -52,3 +54,44 @@ test('Build without errors with config from scratch', async () => {
const distMainFile = join(basePath, 'dist', 'main.js')
expect(existsSync(distMainFile)).toBe(true)
})

describe('Cleaning protections', () => {
test('Forbid cleaning rootDir', async () => {
expect(() => {
build({
basePath,
configFilePath,
compilerOptions: { rootDir: 'src' },
clean: ['src'],
})
}).toThrow('cannot delete')
})

test('Forbid cleaning basePath and up', async () => {
expect(() => {
build({
basePath,
configFilePath,
clean: ['.'],
})
}).toThrow('cannot delete')

expect(() => {
build({
basePath,
configFilePath,
clean: ['..'],
})
}).toThrow('cannot delete')
})

test('Forbid cleaning cwd', async () => {
expect(() => {
build({
basePath,
configFilePath,
clean: [process.cwd()],
})
}).toThrow('cannot delete')
})
})
30 changes: 4 additions & 26 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import ts from 'typescript'
import { join, isAbsolute } from 'path'
import { rmrf } from './addons'
import { CreateProgramFromConfigOptions, TsConfig, EmitOptions } from './interfaces'
import { ensureAbsolutePath } from './path.utils'
import cleanTargets from './clean-addon'

/**
* Compile ts files by creating a compilation object using the compiler API and emitting js files.
Expand Down Expand Up @@ -72,30 +71,9 @@ export function createProgramFromConfig({
* @public
*/
export function emit(program: ts.Program, { betterDiagnostics, clean, basePath }: EmitOptions = {}) {
if (clean && Array.isArray(clean) && clean.length) {
console.log('Cleaning files')
if (basePath) {
clean.map((path) => (isAbsolute(path) ? path : join(basePath, path))).forEach(rmrf)
} else {
clean.forEach(rmrf)
}
} else if (clean && !Array.isArray(clean)) {
const { outDir, outFile, declarationDir } = program.getCompilerOptions()

if (clean.outDir && outDir) {
console.log('Cleaning outDir')
rmrf(outDir)
}

if (clean.outFile && outFile) {
console.log('Cleaning outFile')
rmrf(outFile)
}

if (clean.declarationDir && declarationDir) {
console.log('Cleaning declarationDir')
rmrf(declarationDir)
}
if (clean) {
const compilerOptions = program.getCompilerOptions()
cleanTargets(clean, compilerOptions, basePath)
}

console.log('Compilation started')
Expand Down

0 comments on commit cba911d

Please sign in to comment.