From 0c4bbe0cb9d8d755e07cb120058280396bfaaade Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <mmkal@users.noreply.github.com> Date: Thu, 15 Feb 2024 00:34:43 -0500 Subject: [PATCH] mark fs-syncer as moved --- README.md | 2 +- packages/fs-syncer/.eslintignore | 2 - packages/fs-syncer/.eslintrc.js | 10 - packages/fs-syncer/.gitignore | 2 - packages/fs-syncer/.npmignore | 12 - packages/fs-syncer/CHANGELOG.json | 128 --------- packages/fs-syncer/CHANGELOG.md | 55 ---- packages/fs-syncer/jest.config.js | 1 - packages/fs-syncer/package.json | 40 --- packages/fs-syncer/readme.md | 164 ------------ .../src/__tests__/generated/sync/a.txt | 1 - .../src/__tests__/generated/sync/b.txt | 1 - .../src/__tests__/generated/sync/c/d.txt | 1 - .../src/__tests__/generated/sync/c/e.txt | 1 - .../src/__tests__/generated/sync/c/f.txt | 1 - .../fs-syncer/src/__tests__/index.test.ts | 77 ------ .../src/__tests__/integration.test.ts | 214 ---------------- .../src/__tests__/jest-fixture.test.ts | 119 --------- .../fs-syncer/src/__tests__/jsonc.test.ts | 180 ------------- .../fs-syncer/src/__tests__/memfs.test.ts | 23 -- .../src/__tests__/merge-config.test.ts | 242 ------------------ packages/fs-syncer/src/__tests__/util.test.ts | 103 -------- packages/fs-syncer/src/__tests__/yaml.test.ts | 10 - packages/fs-syncer/src/index.ts | 127 --------- packages/fs-syncer/src/jest.ts | 37 --- packages/fs-syncer/src/jsonc.ts | 229 ----------------- packages/fs-syncer/src/merge-config.ts | 44 ---- packages/fs-syncer/src/types.ts | 105 -------- packages/fs-syncer/src/util.ts | 61 ----- packages/fs-syncer/src/yaml.ts | 36 --- packages/fs-syncer/tsconfig.json | 15 -- 31 files changed, 1 insertion(+), 2042 deletions(-) delete mode 100644 packages/fs-syncer/.eslintignore delete mode 100644 packages/fs-syncer/.eslintrc.js delete mode 100644 packages/fs-syncer/.gitignore delete mode 100644 packages/fs-syncer/.npmignore delete mode 100644 packages/fs-syncer/CHANGELOG.json delete mode 100644 packages/fs-syncer/CHANGELOG.md delete mode 100644 packages/fs-syncer/jest.config.js delete mode 100644 packages/fs-syncer/package.json delete mode 100644 packages/fs-syncer/readme.md delete mode 100644 packages/fs-syncer/src/__tests__/generated/sync/a.txt delete mode 100644 packages/fs-syncer/src/__tests__/generated/sync/b.txt delete mode 100644 packages/fs-syncer/src/__tests__/generated/sync/c/d.txt delete mode 100644 packages/fs-syncer/src/__tests__/generated/sync/c/e.txt delete mode 100644 packages/fs-syncer/src/__tests__/generated/sync/c/f.txt delete mode 100644 packages/fs-syncer/src/__tests__/index.test.ts delete mode 100644 packages/fs-syncer/src/__tests__/integration.test.ts delete mode 100644 packages/fs-syncer/src/__tests__/jest-fixture.test.ts delete mode 100644 packages/fs-syncer/src/__tests__/jsonc.test.ts delete mode 100644 packages/fs-syncer/src/__tests__/memfs.test.ts delete mode 100644 packages/fs-syncer/src/__tests__/merge-config.test.ts delete mode 100644 packages/fs-syncer/src/__tests__/util.test.ts delete mode 100644 packages/fs-syncer/src/__tests__/yaml.test.ts delete mode 100644 packages/fs-syncer/src/index.ts delete mode 100644 packages/fs-syncer/src/jest.ts delete mode 100644 packages/fs-syncer/src/jsonc.ts delete mode 100644 packages/fs-syncer/src/merge-config.ts delete mode 100644 packages/fs-syncer/src/types.ts delete mode 100644 packages/fs-syncer/src/util.ts delete mode 100644 packages/fs-syncer/src/yaml.ts delete mode 100644 packages/fs-syncer/tsconfig.json diff --git a/README.md b/README.md index 9c7b48d5..a4129028 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,10 @@ Monorepo of typescript projects. - [expect-type](https://github.com/mmkal/expect-type#readme) - Compile-time tests for types. Useful to make sure types don't regress into being overly-permissive as changes go in over time. - [eslint-plugin-codegen](https://github.com/mmkal/eslint-plugin-codegen#readme) - An eslint plugin for inline codegen, with presets for barrels, jsdoc to markdown and a monorepo workspace table of contents generator. Auto-fixes out of sync code. +- [fs-syncer](https://github.com/mmkal/fs-syncer) - A helper to recursively read and write text files to a specified directory. ### Still here <!-- codegen:start {preset: monorepoTOC, sort: package.name} --> -- [fs-syncer](./packages/fs-syncer) - A helper to recursively read and write text files to a specified directory. - [io-ts-extra](https://github.com/mmkal/ts/tree/main/packages/io-ts-extra#readme) - Adds pattern matching, optional properties, and several other helpers and types, to io-ts. - [memorable-moniker](https://github.com/mmkal/ts/tree/main/packages/memorable-moniker#readme) - Name generator with some in-built dictionaries and presets. <!-- codegen:end --> diff --git a/packages/fs-syncer/.eslintignore b/packages/fs-syncer/.eslintignore deleted file mode 100644 index 1e4aee4d..00000000 --- a/packages/fs-syncer/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -src/__tests__/generated -src/__tests__/fixtures diff --git a/packages/fs-syncer/.eslintrc.js b/packages/fs-syncer/.eslintrc.js deleted file mode 100644 index 4b434d49..00000000 --- a/packages/fs-syncer/.eslintrc.js +++ /dev/null @@ -1,10 +0,0 @@ -const baseConfig = require('@mmkal/rig/.eslintrc') - -module.exports = { - ...baseConfig, - - ignorePatterns: [ - ...baseConfig.ignorePatterns, - 'fixtures', // https://github.com/eslint/eslint/issues/8429#issuecomment-355967308 - ], -} diff --git a/packages/fs-syncer/.gitignore b/packages/fs-syncer/.gitignore deleted file mode 100644 index 1e4aee4d..00000000 --- a/packages/fs-syncer/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -src/__tests__/generated -src/__tests__/fixtures diff --git a/packages/fs-syncer/.npmignore b/packages/fs-syncer/.npmignore deleted file mode 100644 index c4ef50a2..00000000 --- a/packages/fs-syncer/.npmignore +++ /dev/null @@ -1,12 +0,0 @@ -node_modules -**/__tests__ -dist/buildinfo.json -.eslintcache -.eslintrc.js -.rush -.heft -*.log -tsconfig.json -config/jest.config.json -jest.config.js -coverage diff --git a/packages/fs-syncer/CHANGELOG.json b/packages/fs-syncer/CHANGELOG.json deleted file mode 100644 index 82e0755c..00000000 --- a/packages/fs-syncer/CHANGELOG.json +++ /dev/null @@ -1,128 +0,0 @@ -{ - "name": "fs-syncer", - "entries": [ - { - "version": "0.3.6", - "tag": "fs-syncer_v0.3.6", - "date": "Sat, 16 Oct 2021 13:17:24 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"expect-type\" to `0.13.0`" - } - ] - } - }, - { - "version": "0.3.4", - "tag": "fs-syncer_v0.3.4", - "date": "Tue, 29 Jun 2021 08:48:24 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"expect-type\" to `0.11.0`" - } - ] - } - }, - { - "version": "0.3.3", - "tag": "fs-syncer_v0.3.3", - "date": "Thu, 03 Dec 2020 19:10:22 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"expect-type\" to `0.10.0`" - } - ] - } - }, - { - "version": "0.3.2", - "tag": "fs-syncer_v0.3.2", - "date": "Sat, 28 Nov 2020 19:10:00 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"expect-type\" to `0.9.2`" - } - ] - } - }, - { - "version": "0.3.1", - "tag": "fs-syncer_v0.3.1", - "date": "Thu, 26 Nov 2020 17:06:36 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"expect-type\" from `0.9.0` to `0.9.1`" - } - ] - } - }, - { - "version": "0.3.0", - "tag": "fs-syncer_v0.3.0", - "date": "Tue, 27 Oct 2020 16:18:39 GMT", - "comments": { - "minor": [ - { - "comment": "Permalink readme urls before publishing (#211)" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"expect-type\" from `0.8.0` to `0.9.0`" - } - ] - } - }, - { - "version": "0.2.7", - "tag": "fs-syncer_v0.2.7", - "date": "Mon, 05 Oct 2020 22:38:33 GMT", - "comments": { - "dependency": [ - { - "comment": "Updating dependency \"expect-type\" from `0.7.11` to `0.8.0`" - } - ] - } - }, - { - "version": "0.2.6", - "tag": "fs-syncer_v0.2.6", - "date": "Thu, 01 Oct 2020 14:48:13 GMT", - "comments": { - "patch": [ - { - "comment": "chore: npmignore coverage folder (#184)" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"expect-type\" from `0.7.10` to `0.7.11`" - } - ] - } - }, - { - "version": "0.2.5", - "tag": "fs-syncer_v0.2.5", - "date": "Fri, 18 Sep 2020 16:56:41 GMT", - "comments": { - "patch": [ - { - "comment": "fix(npmignore): exclude rush files (#169)" - } - ], - "dependency": [ - { - "comment": "Updating dependency \"expect-type\" from `0.7.9` to `0.7.10`" - } - ] - } - } - ] -} diff --git a/packages/fs-syncer/CHANGELOG.md b/packages/fs-syncer/CHANGELOG.md deleted file mode 100644 index f5119b8d..00000000 --- a/packages/fs-syncer/CHANGELOG.md +++ /dev/null @@ -1,55 +0,0 @@ -# Change Log - fs-syncer - -This log was last generated on Sat, 16 Oct 2021 13:17:24 GMT and should not be manually modified. - -## 0.3.6 -Sat, 16 Oct 2021 13:17:24 GMT - -_Version update only_ - -## 0.3.4 -Tue, 29 Jun 2021 08:48:24 GMT - -_Version update only_ - -## 0.3.3 -Thu, 03 Dec 2020 19:10:22 GMT - -_Version update only_ - -## 0.3.2 -Sat, 28 Nov 2020 19:10:00 GMT - -_Version update only_ - -## 0.3.1 -Thu, 26 Nov 2020 17:06:36 GMT - -_Version update only_ - -## 0.3.0 -Tue, 27 Oct 2020 16:18:39 GMT - -### Minor changes - -- Permalink readme urls before publishing (#211) - -## 0.2.7 -Mon, 05 Oct 2020 22:38:33 GMT - -_Version update only_ - -## 0.2.6 -Thu, 01 Oct 2020 14:48:13 GMT - -### Patches - -- chore: npmignore coverage folder (#184) - -## 0.2.5 -Fri, 18 Sep 2020 16:56:41 GMT - -### Patches - -- fix(npmignore): exclude rush files (#169) - diff --git a/packages/fs-syncer/jest.config.js b/packages/fs-syncer/jest.config.js deleted file mode 100644 index ae9d9d4f..00000000 --- a/packages/fs-syncer/jest.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('@mmkal/rig/jest.config') diff --git a/packages/fs-syncer/package.json b/packages/fs-syncer/package.json deleted file mode 100644 index 5c8ab6c4..00000000 --- a/packages/fs-syncer/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "fs-syncer", - "version": "0.4.0", - "keywords": [ - "fs", - "file", - "file system", - "sync", - "syncer", - "copy", - "directory", - "recursive", - "read", - "write" - ], - "repository": { - "type": "git", - "url": "https://github.com/mmkal/ts.git", - "directory": "packages/fs-syncer" - }, - "main": "dist/index.js", - "types": "dist/index.d.ts", - "scripts": { - "prebuild": "npm run clean", - "build": "rig tsc -p .", - "clean": "rig rimraf dist", - "lint": "rig eslint --cache .", - "prepack": "rig permalink", - "postpack": "rig unpermalink", - "pretest": "rig rimraf src/__tests__/generated && rig rimraf src/__tests__/fixtures", - "test": "rig jest" - }, - "devDependencies": { - "@mmkal/rig": "workspace:*", - "expect-type2": "npm:expect-type@0.14.0", - "@types/lodash": "4.14.165", - "lodash": "^4.17.15", - "memfs": "~3.3.0" - } -} diff --git a/packages/fs-syncer/readme.md b/packages/fs-syncer/readme.md deleted file mode 100644 index b9cb402a..00000000 --- a/packages/fs-syncer/readme.md +++ /dev/null @@ -1,164 +0,0 @@ -# fs-syncer - -A helper to recursively read and write text files to a specified directory. - -<!-- codegen:start {preset: badges} --> -[](https://github.com/mmkal/ts/actions?query=workflow%3A%22Node+CI%22) -[](https://codecov.io/gh/mmkal/ts/tree/main/packages/fs-syncer) -[](https://npmjs.com/package/fs-syncer) -<!-- codegen:end --> - - -## The idea - -It's a pain to write tests for tools that interact with the filesystem. It would be useful to write assertions that look something like: - -```js -expect(someDirectory.read()).toEqual({ - 'file1.txt': 'some info', - 'file2.log': 'something logged', - nested: { - sub: { - directory: { - 'deeply-nested-file.sql': 'SELECT * FROM abc' - }, - }, - }, -}) -``` - -Similarly, as part of test setup, you might want to write several files, e.g.: - -```js -write({ - migrations: { - 'migration1.sql': 'create table one(id text)', - 'migration2.sql': 'create table two(id text)', - down: { - 'migration1.sql': 'drop table one', - 'migration2.sql': 'drop table two', - }, - }, -}) -``` - -The problem is that usually, you have to write a recursive directory-walker function, an object-to-filepath converter function, a nested-object-getter-function and a few more functions that tie them all together. - -Then, if you have the energy, you should also write a function that cleans up any extraneous files after tests have been run. Or, you can pull in several dependencies that do some of these things for you, then write some functions that tie them together. - -Now, you can just use `fs-syncer`, which does all of the above. Here's the API: - -```js -import {fsSyncer} from 'fs-syncer' - -const syncer = fsSyncer(__dirname + '/migrations', { - 'migration1.sql': 'create table one(id text)', - 'migration2.sql': 'create table two(id text)', - down: { - 'migration1.sql': 'drop table one', - 'migration2.sql': 'drop table two', - }, -}) - -syncer.sync() // replaces all content in `./migrations` with what's described in the target state - -syncer.read() // returns the filesystem state as an object, in the same format as the target state - -// write a file that's not in part of the target state -require('fs').writeFileSync(__dirname + '/migrations/extraneous.txt', 'abc', 'utf8') - -syncer.read() // includes `extraneous.txt: 'abc'` - -syncer.sync() // 'extraneous.txt' will now have been removed - -syncer.write() // like `syncer.sync()`, but doesn't remove extraneous files -``` - -## Usage with jest - -⚠️⚠️⚠️ This feature is new and experimental - if you try it out, be aware that the API is in flux. Feedback is welcome! ⚠️⚠️⚠️ - -If you happen to want to use this in a jest test, there's an opinionated helper which allows you to avoid supplying a `baseDir` parameter. - -Let's say you want to test a file modification tool, which appends `// comments` to all the files it finds under a certain directory, and also creates a log file: - -```js -import {jestFixture} from 'fs-syncer' - -import {fileModificationToolThatYouWantToTest} from '../src/your-library' - -test('files are modified', async () => { - const fixture = jestFixture({ - targetState: { - 'file1.txt': 'hello I am a file', - nested: { - 'file2.txt': 'I am also a file', - } - } - }) - - fixture.sync() - - await fileModificationToolThatYouWantToTest.run({ - directory: fixture.baseDir, - logFile: 'abc.log', - }) - - expect(fixture.yaml()).toMatchInlineSnapshot() -}) -``` - -`fixture.yaml()` is a helper that returns a yaml string representing the file tree. It's designed to be human-readable rather than a full implementation of the yaml spec though! - -Let's assume the test file containing this test is called `my-test-file.test.ts`. When run, the above test will generate a directory `fixtures/my-test-file.test.ts/files-are-modified` next to the test file. The directory structure described in `targetState` will be created inside that folder. The test above might end up looking something like when run: - -```js -import {jestFixture} from 'fs-syncer' - -import {fileModificationToolThatYouWantToTest} from '../src/your-library' - -test('files are modified', async () => { - const fixture = jestFixture({ - targetState: { - 'file1.txt': 'hello I am a file', - nested: { - 'file2.txt': 'I am also a file', - } - } - }) - - fixture.sync() - - await fileModificationToolThatYouWantToTest.run({ - directory: fixture.baseDir, - logFile: 'abc.log', - }) - - expect(fixture.yaml()).toMatchInlineSnapshot( - `"--- - abc.log: |- - added content to file1.txt - added content to nested/file2.txt - file1.txt: |- - hello I am a file - - // this content was auto-generated by the tool - nested: - file2.txt: |- - hello I am a file - - // this content was auto-generated by the tool - "` - ) -}) -``` - -## Not supported (right now) - -- File content other than text, e.g. `Buffer`s. The library assumes you are solely dealing with utf8 strings. -- Any performance optimisations - you will probably have a bad time if you try to use it to read or write a very large number of files. -- Any custom symlink behaviour. - -## Comparison with mock-fs - -This isn't a mocking library. There's no magic under the hood, it just calls `fs.readFileSync`, `fs.writeFileSync` and `fs.mkdirSync` directly. Which means you can use it anywhere - it could even be a runtime dependency as a wrapper for the `fs` module. And using it doesn't have any weird side-effects like [breaking jest snapshot testing](https://www.npmjs.com/package/mock-fs#using-with-jest-snapshot-testing). Not being a mocking library means you could use it in combination with mock-fs, if you really wanted. diff --git a/packages/fs-syncer/src/__tests__/generated/sync/a.txt b/packages/fs-syncer/src/__tests__/generated/sync/a.txt deleted file mode 100644 index 2e65efe2..00000000 --- a/packages/fs-syncer/src/__tests__/generated/sync/a.txt +++ /dev/null @@ -1 +0,0 @@ -a \ No newline at end of file diff --git a/packages/fs-syncer/src/__tests__/generated/sync/b.txt b/packages/fs-syncer/src/__tests__/generated/sync/b.txt deleted file mode 100644 index 84738b4a..00000000 --- a/packages/fs-syncer/src/__tests__/generated/sync/b.txt +++ /dev/null @@ -1 +0,0 @@ -bee \ No newline at end of file diff --git a/packages/fs-syncer/src/__tests__/generated/sync/c/d.txt b/packages/fs-syncer/src/__tests__/generated/sync/c/d.txt deleted file mode 100644 index 268487bf..00000000 --- a/packages/fs-syncer/src/__tests__/generated/sync/c/d.txt +++ /dev/null @@ -1 +0,0 @@ -dee \ No newline at end of file diff --git a/packages/fs-syncer/src/__tests__/generated/sync/c/e.txt b/packages/fs-syncer/src/__tests__/generated/sync/c/e.txt deleted file mode 100644 index efce686c..00000000 --- a/packages/fs-syncer/src/__tests__/generated/sync/c/e.txt +++ /dev/null @@ -1 +0,0 @@ -ee \ No newline at end of file diff --git a/packages/fs-syncer/src/__tests__/generated/sync/c/f.txt b/packages/fs-syncer/src/__tests__/generated/sync/c/f.txt deleted file mode 100644 index 9eb55121..00000000 --- a/packages/fs-syncer/src/__tests__/generated/sync/c/f.txt +++ /dev/null @@ -1 +0,0 @@ -ef \ No newline at end of file diff --git a/packages/fs-syncer/src/__tests__/index.test.ts b/packages/fs-syncer/src/__tests__/index.test.ts deleted file mode 100644 index 5ada311d..00000000 --- a/packages/fs-syncer/src/__tests__/index.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import {fsSyncer} from '..' - -import * as fs from 'fs' -import * as path from 'path' -import {expectTypeOf} from 'expect-type2' - -test('sync', () => { - const syncer = fsSyncer(path.join(__dirname, 'generated/sync'), { - 'a.txt': 'a', - 'b.txt': 'bee', - c: { - 'd.txt': 'dee', - 'e.txt': 'ee', - 'f.txt': 'ef', - }, - }) - - syncer.sync() - - expect(syncer.read()).toMatchInlineSnapshot(` - Object { - "a.txt": "a", - "b.txt": "bee", - "c": Object { - "d.txt": "dee", - "e.txt": "ee", - "f.txt": "ef", - }, - } - `) - - fs.writeFileSync(path.join(syncer.baseDir, 'unexpected.txt'), `shouldn't be here`, 'utf8') - - expect(syncer.read()).toMatchObject({'unexpected.txt': expect.any(String)}) - - syncer.write() - - expect(syncer.read()).toMatchObject({'unexpected.txt': expect.any(String)}) - - syncer.sync() - - expect(syncer.read()).toEqual(syncer.targetState) - expect(syncer.read()).not.toHaveProperty('unexpected.txt') -}) - -test('creates base dir if necessary', () => { - const syncer = fsSyncer(`${__dirname}/generated/create/${Math.random()}`, {}) - - expect(fs.existsSync(syncer.baseDir)).toBe(false) - expect(syncer.read()).toEqual({}) - - syncer.sync() - expect(syncer.read()).toEqual({}) - - expect(fs.statSync(syncer.baseDir).isDirectory()).toBe(true) -}) - -test('sync returns syncer', () => { - const syncer = fsSyncer(`${__dirname}/generated/return/${Math.random()}`, {}) - - expect(syncer.sync()).toBe(syncer) -}) - -test('types', () => { - const syncer = fsSyncer(`${__dirname}/generated/return/${Math.random()}`, { - filename: 'content', - }) - - expectTypeOf(syncer).toHaveProperty('baseDir').toBeString() - expectTypeOf(syncer).toHaveProperty('targetState').toEqualTypeOf({ - filename: 'content', - }) - - expectTypeOf(syncer.sync).returns.toEqualTypeOf(syncer) - expectTypeOf(syncer.read).returns.toBeAny() - expectTypeOf(syncer.write).returns.toEqualTypeOf<void>() -}) diff --git a/packages/fs-syncer/src/__tests__/integration.test.ts b/packages/fs-syncer/src/__tests__/integration.test.ts deleted file mode 100644 index a02f09f4..00000000 --- a/packages/fs-syncer/src/__tests__/integration.test.ts +++ /dev/null @@ -1,214 +0,0 @@ -import * as fsSyncer from '..' -import * as fs from 'fs' -import * as path from 'path' -import * as lodash from 'lodash' - -test('dedents by default', () => { - const syncer = fsSyncer.jestFixture({ - targetState: { - 'foo.js': ` - export const foo = () => { - if (Math.random() > 0.5) { - console.log('foo') - } - } - `, - }, - }) - - syncer.sync() - - expect(syncer.yaml()).toMatchInlineSnapshot(` - "--- - foo.js: |- - export const foo = () => { - if (Math.random() > 0.5) { - console.log('foo') - } - } - " - `) -}) - -test('adds newline by default', () => { - const syncer = fsSyncer.jestFixture({ - targetState: { - 'test.txt': `abc`, - }, - }) - - syncer.sync() - - expect(syncer.read()['test.txt']).toMatch(/^abc\r?\n$/) -}) - -test('dedent can be disabled using mergeStrategy', () => { - const syncer = fsSyncer.jestFixture({ - mergeStrategy: params => params.targetContent, - targetState: { - 'foo.js': ` - export const foo = () => { - if (Math.random() > 0.5) { - console.log('foo') - } - } - `, - }, - }) - - syncer.sync() - - expect(syncer.yaml()).toMatchInlineSnapshot(` - "--- - foo.js: |- - - export const foo = () => { - if (Math.random() > 0.5) { - console.log('foo') - } - } - " - `) -}) - -test('custom merge strategy', () => { - const syncer = fsSyncer.jestFixture({ - mergeStrategy: params => { - if (params.filepath.includes('.vscode') && params.filepath.endsWith('.json')) { - // IRL, you may want to use a parser which can handle comments in json - const existingConfig = JSON.parse(params.existingContent || '{}') - const defaultConfig = JSON.parse(params.targetContent || '{}') - - const mergedConfig = lodash.defaultsDeep(existingConfig, defaultConfig) - - return JSON.stringify(mergedConfig, null, 2) - } - return fsSyncer.defaultMergeStrategy(params) - }, - targetState: { - '.vscode': { - 'settings.json': ` - { - "custom.tool.settings": { - "teamSetting1": "default value 1", - "teamSetting2": "default value 2", - "teamSetting3": "default value 3" - } - } - `, - }, - }, - }) - - const settingsPath = path.join(syncer.baseDir, '.vscode/settings.json') - fs.mkdirSync(path.dirname(settingsPath), {recursive: true}) - fs.writeFileSync( - settingsPath, - JSON.stringify({ - 'custom.tool.settings': { - userSetting: 'xyz', - teamSetting2: 'default value overidden by user', - }, - }), - 'utf8' - ) - - syncer.sync() - - expect(syncer.yaml()).toMatchInlineSnapshot(` - "--- - .vscode: - settings.json: |- - { - \\"custom.tool.settings\\": { - \\"userSetting\\": \\"xyz\\", - \\"teamSetting2\\": \\"default value overidden by user\\", - \\"teamSetting1\\": \\"default value 1\\", - \\"teamSetting3\\": \\"default value 3\\" - } - }" - `) -}) - -describe('ignore paths', () => { - const setup = () => - fsSyncer.jestFixture({ - targetState: { - excluded: { - 'a.txt': 'aaa', - }, - nested: { - 'b.txt': 'bbb', - excluded: { - 'c.txt': 'ccc', - }, - }, - included: { - 'd.txt': 'ddd', - alsoincluded: { - 'e.txt': 'eee', - }, - }, - src: { - 'foo.js': `console.log('foo')`, - nestedjs: { - 'bar.js': `console.log('bar')`, - }, - }, - }, - }) - - test('ignore paths', () => { - setup().sync() - - const ignoreExcludedDirs = fsSyncer.jestFixture({ - targetState: {}, - exclude: ['excluded'], - }) - - expect(ignoreExcludedDirs.yaml()).toMatchInlineSnapshot(` - "--- - included: - d.txt: |- - ddd - - alsoincluded: - e.txt: |- - eee - - nested: - b.txt: |- - bbb - - src: - foo.js: |- - console.log('foo') - - nestedjs: - bar.js: |- - console.log('bar') - " - `) - }) - - test('can whitelist folders', () => { - setup().sync() - - const ignoreExcludedDirs = fsSyncer.jestFixture({ - targetState: {}, - exclude: [/^((?!src).)*$/], - }) - - expect(ignoreExcludedDirs.yaml()).toMatchInlineSnapshot(` - "--- - src: - foo.js: |- - console.log('foo') - - nestedjs: - bar.js: |- - console.log('bar') - " - `) - }) -}) diff --git a/packages/fs-syncer/src/__tests__/jest-fixture.test.ts b/packages/fs-syncer/src/__tests__/jest-fixture.test.ts deleted file mode 100644 index 1609577f..00000000 --- a/packages/fs-syncer/src/__tests__/jest-fixture.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import * as fsSyncer from '..' -import * as fs from 'fs' -import * as path from 'path' -import {wipe} from '../jest' - -test('fixture dir is created', () => { - const fixture = fsSyncer.jestFixture({ - targetState: {'one.txt': 'uno'}, - }) - - fixture.sync() - - expect(fs.existsSync(path.join(__dirname, 'fixtures', 'jest-fixture.test.ts', 'fixture-dir-is-created'))).toBe(true) - expect( - fs - .readFileSync(path.join(__dirname, 'fixtures', 'jest-fixture.test.ts', 'fixture-dir-is-created', 'one.txt')) - .toString() - .trim() - ).toBe('uno') -}) - -test('wipe() deletes existing files', () => { - const before = fsSyncer.jestFixture({ - targetState: {'one.txt': '1'}, - }) - - before.sync() - - expect( - fs.readdirSync(path.join(__dirname, 'fixtures', 'jest-fixture.test.ts', 'wipe-deletes-existing-files')) - ).toEqual(['one.txt']) - - wipe() - - expect( - fs.readdirSync(path.join(__dirname, 'fixtures', 'jest-fixture.test.ts', 'wipe-deletes-existing-files')) - ).toEqual([]) -}) - -describe('A suite', () => { - test(`another test (doesn't have nice path formatting!)`, () => { - const fixture = fsSyncer.jestFixture({ - targetState: {'two.txt': 'dos'}, - }) - - fixture.sync() - - expect( - fs.existsSync( - path.join( - __dirname, - 'fixtures', - 'jest-fixture.test.ts', - 'a-suite-another-test-doesn-t-have-nice-path-formatting' - ) - ) - ).toBe(true) - expect( - fs - .readFileSync( - path.join( - __dirname, - 'fixtures', - 'jest-fixture.test.ts', - 'a-suite-another-test-doesn-t-have-nice-path-formatting', - 'two.txt' - ) - ) - .toString() - .trim() - ).toBe('dos') - }) -}) - -test('yaml snapshot', () => { - const fixture = fsSyncer.jestFixture({ - targetState: { - 'singleline.js': `console.log('hello world')`, - 'multiline.py': - `if __name__ == "__main__":\n` + // prettier-break - ` print("hello world")`, - nested: { - directory: { - 'withfile.txt': 'hello world', - with: { - 'multiline.rs': - `fn main() {\n` + // prettier-break - ` println!("hello world");\n` + - `}`, - }, - }, - }, - }, - }) - - fixture.sync() - - expect(fixture.yaml()).toMatchInlineSnapshot(` - "--- - multiline.py: |- - if __name__ == \\"__main__\\": - print(\\"hello world\\") - - singleline.js: |- - console.log('hello world') - - nested: - directory: - withfile.txt: |- - hello world - - with: - multiline.rs: |- - fn main() { - println!(\\"hello world\\"); - } - " - `) -}) diff --git a/packages/fs-syncer/src/__tests__/jsonc.test.ts b/packages/fs-syncer/src/__tests__/jsonc.test.ts deleted file mode 100644 index 7cfb7d24..00000000 --- a/packages/fs-syncer/src/__tests__/jsonc.test.ts +++ /dev/null @@ -1,180 +0,0 @@ -import * as JSONC from '../jsonc' -import {dedent} from '../util' - -expect.addSnapshotSerializer({ - test: () => false, - print: val => JSON.stringify(val, null, 2), -}) - -test('parse jsonc', () => { - const jsonc = dedent(` - { - // inline comment - "a": 1, - /* block comment on single line */ - "b": 2, - /** - * multiline block comment - */ - "c": 3, - //1 comment starting with a number - "d": { - // nested comment - "e": 5 - }, - // comment on prop that is going to be deleted - "deleteable": "abc", - /** - * This value is "def" because of important reasons. - * You wouldn't want to see this comment if the value was changed; - * that would be quite confusing. - */ - "editable": "def", - "f": 6 - } - `) - - const parsed = JSONC.parse(jsonc as JSONC.JSONC) - - parsed.extraProp = 'added dynamically' - - delete parsed.deleteable // the comment for this prop should be removed too - - parsed.editable = 'xyz' // the comment for this prop will be removed; it may be out of date - - expect(JSONC.stringify(parsed)).toMatchInlineSnapshot(` - "{ - // inline comment - \\"a\\": 1, - /* block comment on single line */ - \\"b\\": 2, - /** - * multiline block comment - */ - \\"c\\": 3, - //1 comment starting with a number - \\"d\\": { - // nested comment - \\"e\\": 5 - }, - // comment on \\"deleteable\\" removed due to content change. - // comment on \\"editable\\" removed due to content change. - \\"editable\\": \\"xyz\\", - \\"f\\": 6, - \\"extraProp\\": \\"added dynamically\\" - }" - `) -}) - -test('edit jsonc', () => { - const jsonc = dedent(` - { - // foobar comment - "foo": "bar", - "nested": { - // nested comment - "a": 1, - "nestedMore": { - "x": "y", - "y": "z" - } - } - } - `) - - const edited = JSONC.edit(jsonc, (obj, comment) => { - obj.nested.nestedMore.newProp = 123 - - comment( - ['nested', 'nestedMore', 'newProp'], // break - 'This was added because of important reasons you should know about' - ) - - comment( - ['nested', 'nestedMore', 'y'], - dedent(` - A verbose, multiline comment - about 'y' - `) - ) - - expect(() => comment(['non', 'existent', 'path'], 'are ignored')).toThrowErrorMatchingInlineSnapshot( - `"Can't add comment to path [non,existent,path]. Parent path is not defined."` - ) - }) - - expect(edited).toMatchInlineSnapshot(` - "{ - // foobar comment - \\"foo\\": \\"bar\\", - \\"nested\\": { - // nested comment - \\"a\\": 1, - \\"nestedMore\\": { - \\"x\\": \\"y\\", - /** - * A verbose, multiline comment - * about 'y' - */ - \\"y\\": \\"z\\", - // This was added because of important reasons you should know about - \\"newProp\\": 123 - } - } - }" - `) -}) - -test('edit jsonc respects existing indentation', () => { - const jsonc = dedent(` - { - \t"a1": "b1" - } - `) - - const edited = JSONC.edit(jsonc, (obj, comment) => { - obj.a2 = 'b2' - obj.a3 = {x: 'y'} - - comment(['a1'], 'Comment 1') - comment(['a3', 'x'], 'Comment 2') - }) - - expect(edited).toEqual( - dedent(` - { - \t// Comment 1 - \t"a1": "b1", - \t"a2": "b2", - \t"a3": { - \t\t// Comment 2 - \t\t"x": "y" - \t} - }`) - ) -}) - -test('edit jsonc double-space indents by default', () => { - const jsonc = dedent(`{"a1": "b1"}`) - - const edited = JSONC.edit(jsonc, (obj, comment) => { - obj.a2 = 'b2' - - comment(['a1'], 'Comment for a1') - }) - - expect(edited).toMatchInlineSnapshot(` - "{ - // Comment for a1 - \\"a1\\": \\"b1\\", - \\"a2\\": \\"b2\\" - }" - `) -}) - -test('helpful errors for invalid syntax', () => { - expect(() => JSONC.parse(`{"foo': "bar"}` as JSONC.JSONC)).toThrowErrorMatchingInlineSnapshot(` - "Unexpected token b in JSON at position 9 - {\\"foo': \\" --> b <-- ar\\"}" - `) -}) diff --git a/packages/fs-syncer/src/__tests__/memfs.test.ts b/packages/fs-syncer/src/__tests__/memfs.test.ts deleted file mode 100644 index 46f08507..00000000 --- a/packages/fs-syncer/src/__tests__/memfs.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {Volume} from 'memfs' -import {createFSSyncer} from '..' -import * as fs from 'fs' - -test('can use memfs', () => { - const vol = Volume.fromJSON({'/tmp/root.txt': 'root'}) - const syncer = createFSSyncer({ - baseDir: '/tmp/this/should/not/be/created/on/the/real/filesystem', - targetState: {'one.txt': '1'}, - fs: vol, - }) - - syncer.sync() - - expect(fs.existsSync(syncer.baseDir)).toBeFalsy() - expect(vol.toJSON()).toMatchInlineSnapshot(` - Object { - "/tmp/root.txt": "root", - "/tmp/this/should/not/be/created/on/the/real/filesystem/one.txt": "1 - ", - } - `) -}) diff --git a/packages/fs-syncer/src/__tests__/merge-config.test.ts b/packages/fs-syncer/src/__tests__/merge-config.test.ts deleted file mode 100644 index 95ee6d9d..00000000 --- a/packages/fs-syncer/src/__tests__/merge-config.test.ts +++ /dev/null @@ -1,242 +0,0 @@ -import * as fsSyncer from '..' -import {mergeJsonConfigs} from '../merge-config' -import {dedent} from '../util' - -test('merge configs', () => { - const userVSCodeSettings = dedent(` - { - "eslint.packageManager": "yarn", - // User setting should be preserved - "eslint.debug": true, - "files.exclude": { - // vscode excludes this by default, but I like seeing it. - "**/.git": false - }, - "files.watcherExclude": { - /** - * If these aren't excluded from watcher tasks it can cause performance issues - * when trying to load large projects - */ - "**/.git/objects/**": true, - "**/.git/subtree-cache/**": true, - "**/node_modules/**": true - } - } - `) - - const teamVSCodeSettings = dedent(` - { - // Team tooling requires overriding package manager - "eslint.packageManager": "pnpm", - "custom.team.plugin": { - // very important setting that everyone should have - "activityBarColor": "#ff0000" - }, - "files.watcherExclude": { - // ignore file generated by the team's custom code generator - "**/team-code-generator/generated/**": true - } - } - `) - - const merged = mergeJsonConfigs({ - filepath: '.vscode/settings.json', - existingContent: userVSCodeSettings, - targetContent: teamVSCodeSettings, - }) - - expect(merged).toMatchInlineSnapshot(` - "{ - // Team tooling requires overriding package manager - \\"eslint.packageManager\\": \\"pnpm\\", - \\"custom.team.plugin\\": { - // very important setting that everyone should have - \\"activityBarColor\\": \\"#ff0000\\" - }, - \\"files.watcherExclude\\": { - // ignore file generated by the team's custom code generator - \\"**/team-code-generator/generated/**\\": true, - /** - * If these aren't excluded from watcher tasks it can cause performance issues - * when trying to load large projects - */ - \\"**/.git/objects/**\\": true, - \\"**/.git/subtree-cache/**\\": true, - \\"**/node_modules/**\\": true - }, - // User setting should be preserved - \\"eslint.debug\\": true, - \\"files.exclude\\": { - // vscode excludes this by default, but I like seeing it. - \\"**/.git\\": false - } - }" - `) -}) - -describe('config merge strategy', () => { - test('merge config merge strategy', () => { - const sharedSettings = { - '.vscode': { - 'settings.json': ` - { - // Team tooling requires overriding package manager - "eslint.packageManager": "pnpm", - "custom.team.plugin": { - // very important setting that everyone should have - "activityBarColor": "#ff0000" - }, - "files.watcherExclude": { - // ignore file generated by the team's custom code generator - "**/team-code-generator/generated/**": true - } - } - `, - }, - '.eslintrc.js': `module.exports = require('@your-company/shared-configs/eslint-config')`, - 'jest.config.js': `module.exports = require('@your-company/shared-configs/jest-config')`, - 'tsconfig.json': ` - { - "extends": "@your-company/shared-configs/typescript-config.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "dist" - } - } - `, - 'package.json': ` - { - "scripts": { - "build": "tsc -p .", - "lint": "eslint .", - "test": "jest" - }, - "devDependencies": { - "@your-company/shared-configs": "*", - "eslint": "*", - "jest": "*", - "typescript": "*" - } - } - `, - } - - const specificProjectfileTree = { - '.vscode': { - 'settings.json': ` - { - "eslint.packageManager": "yarn", - // User setting should be preserved - "eslint.debug": true, - "files.exclude": { - // vscode excludes this by default, but I like seeing it. - "**/.git": false - }, - "files.watcherExclude": { - /** - * If these aren't excluded from watcher tasks it can cause performance issues - * when trying to load large projects - */ - "**/.git/objects/**": true, - "**/.git/subtree-cache/**": true, - "**/node_modules/**": true - } - } - `, - }, - src: { - 'index.ts': 'console.log(123)', - }, - 'package.json': ` - { - "name": "@your-company/some-specific-project", - "version": "0.0.1" - } - `, - 'tsconfig.json': ` - { - "compilerOptions": { - // this project has a custom target but we still want to inherit other team settings - "target": "es2018" - } - } - `, - } - - // first write the existing user code, this is just test setup though - fsSyncer.jestFixture({targetState: specificProjectfileTree}).sync() - - // expect(1).toEqual(0) - - const syncer = fsSyncer.jestFixture({ - targetState: sharedSettings, - mergeStrategy: mergeJsonConfigs, - }) - - syncer.sync() - - // we should see the team settings merged into the user settings - expect(syncer.yaml()).toMatchInlineSnapshot(` - "--- - .eslintrc.js: module.exports = require('@your-company/shared-configs/eslint-config') - jest.config.js: module.exports = require('@your-company/shared-configs/jest-config') - package.json: |- - { - \\"scripts\\": { - \\"build\\": \\"tsc -p .\\", - \\"lint\\": \\"eslint .\\", - \\"test\\": \\"jest\\" - }, - \\"devDependencies\\": { - \\"@your-company/shared-configs\\": \\"*\\", - \\"eslint\\": \\"*\\", - \\"jest\\": \\"*\\", - \\"typescript\\": \\"*\\" - }, - \\"name\\": \\"@your-company/some-specific-project\\", - \\"version\\": \\"0.0.1\\" - } - tsconfig.json: |- - { - \\"extends\\": \\"@your-company/shared-configs/typescript-config.json\\", - \\"compilerOptions\\": { - \\"rootDir\\": \\"src\\", - \\"outDir\\": \\"dist\\", - // this project has a custom target but we still want to inherit other team settings - \\"target\\": \\"es2018\\" - } - } - .vscode: - settings.json: |- - { - // Team tooling requires overriding package manager - \\"eslint.packageManager\\": \\"pnpm\\", - \\"custom.team.plugin\\": { - // very important setting that everyone should have - \\"activityBarColor\\": \\"#ff0000\\" - }, - \\"files.watcherExclude\\": { - // ignore file generated by the team's custom code generator - \\"**/team-code-generator/generated/**\\": true, - /** - * If these aren't excluded from watcher tasks it can cause performance issues - * when trying to load large projects - */ - \\"**/.git/objects/**\\": true, - \\"**/.git/subtree-cache/**\\": true, - \\"**/node_modules/**\\": true - }, - // User setting should be preserved - \\"eslint.debug\\": true, - \\"files.exclude\\": { - // vscode excludes this by default, but I like seeing it. - \\"**/.git\\": false - } - } - src: - index.ts: |- - console.log(123) - " - `) - }) -}) diff --git a/packages/fs-syncer/src/__tests__/util.test.ts b/packages/fs-syncer/src/__tests__/util.test.ts deleted file mode 100644 index 95bfce6d..00000000 --- a/packages/fs-syncer/src/__tests__/util.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import {dedent, getPaths} from '../util' - -describe('getPaths', () => { - test('nested object', () => { - expect(getPaths({a: {b: {c: 1, d: {e: null}}, x: 'y'}})).toMatchInlineSnapshot(` - Array [ - Array [ - "a", - "b", - "c", - ], - Array [ - "a", - "b", - "d", - "e", - ], - Array [ - "a", - "x", - ], - ] - `) - }) - - test('empty object', () => { - expect(getPaths({})).toEqual([]) - }) -}) - -describe('dedent', () => { - test('calculates common indent', () => { - const dedented = dedent(` - def main(): - if 1 > 0: - print('hello') - elif: 2 < 1: - print('goodbye') - - if __name__ == '__main__': - main() - `) - - expect(dedented).toMatchInlineSnapshot(` - "def main(): - if 1 > 0: - print('hello') - elif: 2 < 1: - print('goodbye') - - if __name__ == '__main__': - main() - " - `) - }) - - test('respects tabs with tabs', () => { - const withTabs = ` -\t\t\tconst x = { -\t\t\t\ta: 'a', -\t\t\t\tb: { -\t\t\t\t\tc: 'c -\t\t\t\t} -\t\t\t} -\t\t` - - expect(dedent(withTabs)).toMatchInlineSnapshot(` - "const x = { - a: 'a', - b: { - c: 'c - } - } - " - `) - }) - - test('allows trailing line', () => { - const withTrailing = ` - foo - bar - ` - - const withoutTrailing = ` - foo - bar` - expect(dedent(withTrailing)).toEqual('foo\nbar\n') - - expect(dedent(withoutTrailing)).toEqual('foo\nbar') - }) - - test('allows single lines', () => { - expect(dedent(`foo bar`)).toEqual('foo bar') - }) - - test(`doesn't indent value with content on first line`, () => { - const withLeadingContent = `foo - bar - baz - ` - expect(dedent(withLeadingContent)).toEqual(withLeadingContent) - }) -}) diff --git a/packages/fs-syncer/src/__tests__/yaml.test.ts b/packages/fs-syncer/src/__tests__/yaml.test.ts deleted file mode 100644 index 910441ba..00000000 --- a/packages/fs-syncer/src/__tests__/yaml.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {yamlishPrinter} from '../yaml' - -test(`undefined is handled`, () => { - expect(yamlishPrinter({a: 1, b: undefined, c: null})).toMatchInlineSnapshot(` - "--- - a: 1 - b: - c: " - `) -}) diff --git a/packages/fs-syncer/src/index.ts b/packages/fs-syncer/src/index.ts deleted file mode 100644 index bc7619e6..00000000 --- a/packages/fs-syncer/src/index.ts +++ /dev/null @@ -1,127 +0,0 @@ -import * as realFs from 'fs' -import * as path from 'path' -import * as os from 'os' -import {getPaths, get, dedent, tryCatch} from './util' -import {fsSyncerFileTreeMarker, CreateSyncerParams, MergeStrategy} from './types' -import {yamlishPrinter} from './yaml' - -export * from './types' - -export {jestFixture} from './jest' - -export * as JSONC from './jsonc' - -export * from './merge-config' - -export const defaultMergeStrategy: MergeStrategy = params => { - return params.targetContent && dedent(params.targetContent).trim() + os.EOL -} - -/** - * @experimental - * More flexible alternative to `fsSyncer`. - */ -export const createFSSyncer = <T extends object>({ - baseDir, - targetState, - exclude = ['node_modules'], - mergeStrategy = defaultMergeStrategy, - fs: _fs = realFs, -}: CreateSyncerParams<T>) => { - const fs = _fs as typeof realFs - const write = () => { - fs.mkdirSync(baseDir, {recursive: true}) - const paths = getPaths(targetState) - paths.forEach(p => { - const filepath = path.join(baseDir, ...p) - fs.mkdirSync(path.dirname(filepath), {recursive: true}) - - let targetContent: string | undefined = `${get(targetState, p)}` - - const existingContent = tryCatch(() => fs.readFileSync(filepath).toString()) - targetContent = mergeStrategy({filepath, existingContent, targetContent}) - - if (typeof targetContent === 'string') { - fs.writeFileSync(filepath, targetContent) - } - }) - } - - const readdir = (dir: string): T => { - const result = fs - .readdirSync(dir, {withFileTypes: true}) - .sort((...entries) => { - const [left, right] = entries.map(e => Number(e.isDirectory())) - return left - right - }) - .reduce<T>((state, entry) => { - const subpath = path.join(dir, entry.name) - - const relativePath = path.relative(baseDir, subpath) - if (exclude.some(r => relativePath.match(r))) { - return state - } - - return { - ...state, - [entry.name]: fs.statSync(subpath).isFile() ? fs.readFileSync(subpath).toString() : readdir(subpath), - } - }, {} as T) - Object.defineProperty(result, fsSyncerFileTreeMarker, {value: 'directory', enumerable: false}) - return result - } - - const read = (): any => (fs.existsSync(baseDir) ? readdir(baseDir) : {}) - - const yaml = ({tab, path = []}: {tab?: string; path?: string[]} = {}): string => - yamlishPrinter(get(read(), path), tab) - - /** writes all target files to file system, and deletes files not in the target state object */ - const sync = () => { - write() - const fsState = read() - const fsPaths = getPaths(fsState) - fsPaths.forEach(p => { - const filepath = path.join(baseDir, ...p) - const targetContent = get(targetState, p) - const existingContent = tryCatch(() => fs.readFileSync(filepath).toString()) - const resolved = mergeStrategy({ - filepath, - targetContent, - existingContent, - }) - if (typeof resolved === 'string') { - // todo: make it necessary to write here - // we don't need to now because we already did in write() - // above, but that's weird and involves calling mergeStrategy twice. - // fs.writeFileSync(filepath, resolved) - } else { - tryCatch(() => fs.unlinkSync(filepath)) - } - }) - - return syncer - } - - const syncer = {read, yaml, write, sync, targetState, baseDir} - - return syncer -} - -// Backwards-compatible export, may be deprecated if the above one proves nice to work with: - -/** - * A helper to read and write text files to a specified directory. - * - * @param baseDir file paths relative to this - * @param targetState a nested dictionary. A string property is a file, with the key - * being the filename and the value the content. A nested object represents a directory. - */ -export const fsSyncer = <T extends object>(baseDir: string, targetState: T) => { - return createFSSyncer({ - baseDir, - targetState, - // legacy behaviour: no dedenting, so can't use defaultMergeStrategy - mergeStrategy: params => params.targetContent, - }) -} diff --git a/packages/fs-syncer/src/jest.ts b/packages/fs-syncer/src/jest.ts deleted file mode 100644 index 57aac1d3..00000000 --- a/packages/fs-syncer/src/jest.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as path from 'path' -import {createFSSyncer} from '.' -import {CreateSyncerParams} from './types' - -/** - * @experimental - * Call from a jest test to setup a syncer in a `baseDir` based on the current file, suite and test name. - * This reduces the risk of copy-paste errors resulting in two tests trying to write to the same directory. - * @param targetState target file tree - */ -// todo: give this the same signature as createFsSyncer -export const jestFixture = (params: Omit<CreateSyncerParams<any>, 'baseDir'>) => { - return createFSSyncer<any>({ - baseDir: baseDir(), - ...params, - }) -} - -export const baseDir = () => - path.join( - path.dirname(expect.getState().testPath), - 'fixtures', - path.basename(expect.getState().testPath), - expect - .getState() - .currentTestName.toLowerCase() - .replace(/[^\da-z]/g, '-') // convert everything non-alphanumeric to dashes - .replace(/-+/g, '-') // remove double-dashes - .replace(/^-*/, '') // remove dashes at the start - .replace(/-*$/, '') // remove dashes at the end - ) - -export const wipe = () => - createFSSyncer({ - baseDir: path.join(path.dirname(expect.getState().testPath), 'fixtures'), - targetState: {}, - }).sync() diff --git a/packages/fs-syncer/src/jsonc.ts b/packages/fs-syncer/src/jsonc.ts deleted file mode 100644 index c79227af..00000000 --- a/packages/fs-syncer/src/jsonc.ts +++ /dev/null @@ -1,229 +0,0 @@ -import {createHash} from 'crypto' -import {get} from './util' - -export const jsonC = Symbol('jsonc') -export type JSONC = string & {[jsonC]: true} - -/** - * Parses json, and handles **some** jsonc-style comments, preserving them as real properties. - * Note: the parsed output is usually **not** something you'd want to serialise or persist, but it - * is useful for modifying config files which include comments (e.g. https://github.com/microsoft/rushstack/tree/master/rush.json) - * - * Note: not all jsonc comments are handled: - * - Comments on the same line as other properties e.g. - * ``` - * { - * "foo": "bar" // this comment will cause an error - * } - * ``` - * - Comments that aren't preceding properties e.g. - * ``` - * { - * "foo": "bar" - * // this comment will cause an error - * } - * ``` - * - * @example - * ``` - * const oldContent `{ - * // this setting is set to true because of important reasons - * "originalSetting": true - * }` - * - * const config = JSONC.parse(oldContent) - * - * config.additionalSetting = 'abc' - * - * expect(JSONC.stringify(config)).toEqual(`{ - * // this setting is set to true because of important reasons - * "originalSetting": true, - * "additionalSetting": "abc" - * }`) - * ``` - */ -export const parse = (jsonc: JSONC) => { - const hash = createHash('md5').update(jsonc).digest('hex') - const clashes = jsonc.match(/\/\/\d+/g) - let commentCounter = 0 - for (const m of clashes || []) { - commentCounter = Math.max(commentCounter, Number.parseInt(m.replace('//', ''), 10) + 1) - } - const lines = jsonc.split('\n') - let blockStart = -1 - const withCommentValues = lines.map((s, i, arr) => { - const trimmed = s.trim() - let commentProp = '' - if (blockStart === -1 && trimmed.startsWith('/*')) { - blockStart = i - } - - if (blockStart > -1 && trimmed.endsWith('*/')) { - commentProp = lines - .slice(blockStart, i + 1) - .join('\n') - .trim() - blockStart = -1 - } else if (trimmed.startsWith('//')) { - commentProp = trimmed - } - - if (commentProp) { - const addedJson = { - [`//${++commentCounter} jsonc comment ${hash}`]: JSON.stringify({ - comment: commentProp, - nextLine: arr[i + 1], - }), - } - return JSON.stringify(addedJson).slice(1, -1) + ',' - } else if (blockStart > -1) { - return '' - } - - return s - }) - - const json = withCommentValues.join('\n') - try { - return JSON.parse(json) - } catch (e: unknown) { - if (e instanceof SyntaxError && e.message.match(/position \d+$/)) { - const position = Number.parseInt(e.message.split(' ').slice(-1)[0], 10) - e.message += `\n${json.slice(0, position)} --> ${json[position]} <-- ${json.slice(position + 1)}` - } - throw e - } -} - -// No 'z' - this a private variable so I'll spell it how I damn well please. -const normaliseJsonLine = (line: string) => line.trim().replace(/\s*,$/, '') - -export const stringify = (obj: any, replacer: Array<string | number> | null = null, space?: string | number): JSONC => { - const json = JSON.stringify(obj, replacer, space ?? 2) - const lines = json.split('\n') - const withComments = lines.map((s, i, arr) => { - const trimmed = s.trim() - if (trimmed.match(/^"\/\/\d+ jsonc comment \w+"/)) { - const margin = s.slice(0, s.indexOf(`"`)) - // json json json json - const jsonjson = JSON.parse(`{${trimmed.replace(/,?\r?$/, '')}}`) - const {comment, nextLine} = JSON.parse(jsonjson[Object.keys(jsonjson)[0]]) - if (normaliseJsonLine(nextLine) !== normaliseJsonLine(arr[i + 1])) { - return `${margin}// comment on ${nextLine.split(':')[0].trim()} removed due to content change.` - } - return margin + comment - } - return s - }) - - return withComments.join('\n') as JSONC -} - -/** - * Parses json, and handles **some** jsonc-style comments, preserving them as real properties. - * This is useful for modifying config files which include comments (e.g. https://github.com/microsoft/rushstack/tree/master/rush.json) - * - * The function should be called with a json-like string and an `edit` function, which will receive the parsed - * object, and a `comment` function. Mutate the parsed content to add, remove or change properties, and - * use the `comment` function to add comments which will be rendered above the specified property. - * - * @example - * ``` - * const oldContent `{ - * // this setting is set to true because of important reasons - * "originalSetting": true - * }` - * - * const newContent = JSONC.edit(oldContent, (config, comment) => { - * config.additionalSetting = {foo: 'bar'} - * - * comment( - * ['additionalSetting', 'foo'], - * 'This value must be either "bar" or "baz" or things will explode!', - * ) - * }) - * - * expect(newContent).toEqual(`{ - * // this setting is set to true because of important reasons - * "originalSetting": true, - * "additionalSetting": { - * // This value must be either "bar" or "baz" or things will explode! - * "foo": "bar" - * } - * }`) - * ``` - * - * Note: not all jsonc comments are handled: - * - Comments on the same line as other properties e.g. - * ``` - * { - * "foo": "bar" // this comment will cause an error - * } - * ``` - * - Comments that aren't above properties e.g. - * ``` - * { - * "foo": "bar" - * // this comment will cause an error - * } - * ``` - - * - * @param jsonc json-ish string input. This can include comments with some caveats. - * @param editor A function which modifies the parsed - */ -export const edit = <T = any>( - jsonc: string, - editor: (obj: T, addComment: (path: string[], comment: string) => void) => void -): JSONC => { - const obj = parse(jsonc as JSONC) - // const indent = typeof space === 'string' ? space : ' '.repeat(space ?? 2) - const indent = jsonc?.split('\n')[1]?.match(/^\s+/)?.[0] || ' ' - - const addComment = (path: string[], comment: string) => { - const parent = get(obj, path.slice(0, -1)) - const last = path[path.length - 1] - if (!parent) { - throw new TypeError(`Can't add comment to path [${path}]. Parent path is not defined.`) - } - const parentJson = stringify(parent, null, indent) - - // this searches for key that needs to be commented. It should be reliable because - // JSON doesn't allow newlines, and requires key-quoting. So if you see a newline, - // some whitespace and a then "thekey", you know that corresponds to the right property. - // todo: escape `JSON.stringify(last)` or don't use new RegExp - const keyToBeCommentedRegExp = new RegExp(`\\n\\s+${JSON.stringify(last)}`) - - const withExtraComment = parentJson.replace(keyToBeCommentedRegExp, r => { - const padding = r.split('"')[0] + indent.repeat(path.length - 1) - - const commentedOutComment = comment.includes('\n') - ? '/**' + - comment - .trim() - .split('\n') - .map(c => `${padding} * ${c}`) - .join('') + - padding + - ' */' - : `// ${comment}` - - return `${padding}${commentedOutComment}${r}` - }) - - // need to modify the object in place, so delete all the old keys and add all the new ones - - const parentReplacement = parse(withExtraComment as JSONC) - Object.keys(parent).forEach(k => { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete parent[k] - }) - Object.keys(parentReplacement).forEach(k => { - parent[k] = parentReplacement[k] - }) - } - - editor(obj, addComment) - - return stringify(obj, null, indent) -} diff --git a/packages/fs-syncer/src/merge-config.ts b/packages/fs-syncer/src/merge-config.ts deleted file mode 100644 index 1066790a..00000000 --- a/packages/fs-syncer/src/merge-config.ts +++ /dev/null @@ -1,44 +0,0 @@ -import {MergeStrategy} from './types' -import * as JSONC from './jsonc' -import {uniq} from './util' - -const isMergeable = (obj: unknown) => obj && typeof obj === 'object' && obj.toString() === '[object Object]' - -/** - * @experimental - * Deep merge two objects. The `right` object "wins" when values can't be merged (primitives, arrays etc.). - */ -export const mergeObjects = (left: any, right: any): any => { - if (isMergeable(left) && isMergeable(right)) { - const keys = uniq([...Object.keys(right), ...Object.keys(left)]) - return keys.reduce((acc, next) => ({...acc, [next]: mergeObjects(left[next], right[next])}), {} as any) - } - - return typeof right === 'undefined' ? left : right -} - -// todo: consider whether this is making too much of an assumption that we "prefer" the target content -// a lot of scenarios would need a more cautious algorithm which throws on conflicts, or a more agressive -// one which prefers "target" content (say, important settings like package naming conventions), or some -// kind of mixture based on filename (let users have their own vscode settings, but don't let them choose -// their own package naming conventions). -// so maybe some options are needed into these params? -/** - * @experimental - * Deep-merge two json-like config files. Comments will be _mostly_ preserved. The second argument - * "wins" when a property that can't be merged (primitives, arrays etc.) is found in both. - */ -export const mergeJsonConfigs: MergeStrategy = params => { - if (!params.filepath.endsWith('.json')) { - return params.targetContent || params.existingContent - } - let merged: any - JSONC.edit(params.existingContent || '{}', existing => { - JSONC.edit(params.targetContent || '{}', target => { - // todo: make this less callback-ish? would need `addComment` to push args to a queue instead of doing stuff right away - merged = mergeObjects(existing, target) - }) - }) - - return JSONC.stringify(merged) -} diff --git a/packages/fs-syncer/src/types.ts b/packages/fs-syncer/src/types.ts deleted file mode 100644 index 82136f3a..00000000 --- a/packages/fs-syncer/src/types.ts +++ /dev/null @@ -1,105 +0,0 @@ -// todo: make .read() have a more helpful return type -// todo: mergeStrategy. tried before -// todo: fixture - -export const fsSyncerFileTreeMarker = Symbol('fs-syncer-marker') - -/** - * Interface that's compatible with both `require('fs')` and a `memfs` volume. - * Annoyingly, memfs doesn't strictly conform to the `fs` interface, it deals with - * nullish values differently, etc. - * - * Note: this only includes methods that this library actually uses. - */ -export interface FSLike { - readFileSync(path: string): {toString(): string} - writeFileSync(path: string, content: string): void - existsSync(filepath: string): boolean - mkdirSync(path: string, opts?: {recursive?: boolean}): void - // memfs uses crazy generics here, just make sure some kinda function exists - readdirSync(path: string, opts?: {encoding: any; withFileTypes?: false}): any[] - readdirSync(path: string, opts: {withFileTypes?: true}): any[] - statSync(path: string): {isFile(): boolean} - unlinkSync(path: string): void -} - -export type MergeStrategy = (params: { - filepath: string - existingContent: string | undefined - targetContent: string | undefined -}) => string | undefined - -export interface CreateSyncerParams<T extends object> { - /** Path that all files will be synced relative to. */ - baseDir: string - /** - * Object representation of desired file tree. - * - * @example - * ``` - * { - * 'one.txt': 'uno', - * 'two.txt': 'dos', - * 'subfolder': { - * 'three.txt': 'tres' - * } - * } - * ``` - */ - targetState: T - /** - * Any file or folder matching one of these regexes will not be read from the file system - * - * @default - * ``` - * ['node_modules'] - * ``` - * - * @example - * ``` - * ['node_modules', 'dist'] - * ``` - * - * @example - * ``` - * ['node_modules', 'dist', /.*\.(test|spec)\.ts/] - * ``` - * - * @example - * ``` - * /^((?!src).)*$/ // ignore all paths not containing `src`. Note - this effectively means paths must _start_ with `src`, since they will be short-circuited otherwise. - * ``` - */ - exclude?: Array<string | RegExp> - - /** - * A function which takes a filepath, old content and new content strings, and returns a string. The returned string is written to disk. - * If `undefined` is returned, the file is deleted. - * - * You can use this to implement custom merges. e.g. a json config file with some default properties could use a strategy which merges existing and target contents: - * - * @example - * ``` - * ({filepath, targetContent, existingContent}) => { - * if (!existingContent) { - * return targetContent - * } - * const existingConfig = JSON.parse(existingContent) - * const targetConfig = JSON.parse(targetContent || '{}') - * - * return { - * ...targetConfig, - * ...existingConfig, - * } - * } - * ``` - * - * @default params => params.targetContent - */ - mergeStrategy?: MergeStrategy - /** - * Replacement file-system. Defaults to `require('fs')`. You can use `memfs` to perform in-memory operations. - */ - fs?: FSLike - // beforeWrites?: BeforeWrite[] -} diff --git a/packages/fs-syncer/src/util.ts b/packages/fs-syncer/src/util.ts deleted file mode 100644 index 6464279a..00000000 --- a/packages/fs-syncer/src/util.ts +++ /dev/null @@ -1,61 +0,0 @@ -type Route = string[] - -export const get = (obj: any, path: Route) => path.reduce((val, key) => val?.[key], obj) - -export const getPaths = (obj: unknown, route: Route = []): Route[] => { - if (typeof obj !== 'object' || !obj) { - return [route] - } - - const newRoutes = Object.entries(obj).map(e => { - return getPaths(e[1], [...route, e[0]]) - }) - - return ([] as Route[]).concat(...newRoutes) -} - -/** Along the lines of https://github.com/tc39/proposal-string-dedent */ -export const dedent = (str: string) => { - const lines = str.split('\n') - if (lines.length === 1 || lines[0]) { - return str - } - lines.shift() - if (lines[lines.length - 1].trim() === '') { - lines[lines.length - 1] = '' - } - - const commonMargin = - lines.filter(Boolean).reduce((common, next) => { - const lineMargin = next.split(/\S/)[0] - if (typeof common === 'string') { - return lineMargin.startsWith(common) ? common : common.startsWith(lineMargin) ? lineMargin : '' - } - return lineMargin - }, null as string | null) || '' - - return lines.map(line => line.replace(commonMargin, '')).join('\n') -} - -/** Dedupes a string array while preserving original ordering */ -export const uniq = (array: string[]) => { - const set = new Set<string>() - return array.filter(item => { - if (set.has(item)) { - return false - } - set.add(item) - return true - }) -} - -export const tryCatch = <T, U = undefined>( - fn: () => T, - onError: (error: unknown) => U = () => (undefined as any) as U -) => { - try { - return fn() - } catch (e: unknown) { - return onError(e) - } -} diff --git a/packages/fs-syncer/src/yaml.ts b/packages/fs-syncer/src/yaml.ts deleted file mode 100644 index b60de0b6..00000000 --- a/packages/fs-syncer/src/yaml.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @experimental Prints an object to yaml, or at least something like it. Not intended to be spec-compliant. - * It's really just used for outputting readable jest snapshots. - */ -export const yamlishPrinter = (val: any, tab = ' ') => { - const buffer: string[] = [] - const printNode = (node: any, indent: number) => { - if (typeof node === 'undefined') { - return - } - if (node && typeof node === 'object') { - const entries = Object.entries(node) - // .sort((...items) => { - // const keys = items.map(e => (e[1] && typeof e[1] === 'object' ? 'z' : typeof e[1])) - // return keys[0].localeCompare(keys[1]) - // }) - entries.forEach(e => { - buffer.push('\n' + tab.repeat(indent) + e[0] + ': ') - printNode(e[1], indent + 1) - }) - return - } - if (typeof node === 'string' && node.includes('\n')) { - buffer.push('|-\n') - node.split('\n').forEach((line, i, arr) => { - buffer.push(tab.repeat(indent) + line + (i === arr.length - 1 ? '' : '\n')) - }) - return - } - - buffer.push(node?.toString()) - } - - printNode(val, 0) - return '---\n' + buffer.join('').trimLeft() -} diff --git a/packages/fs-syncer/tsconfig.json b/packages/fs-syncer/tsconfig.json deleted file mode 100644 index 77d90b1f..00000000 --- a/packages/fs-syncer/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "./node_modules/@mmkal/rig/tsconfig.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/buildinfo.json", - "typeRoots": [ - "node_modules/@mmkal/rig/node_modules/@types" - ] - }, - "exclude": [ - "node_modules", - "dist" - ] -}