Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Support custom clone operation #12

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ const ignoreFormat = createFormat({
const unique = (array) => Array.from(new Set() < T[number] > array).sort()
await fs.writeFile(resolvedPath, unique(expected).join('\n'), 'utf8')
},
// Optional: define a 'clone' function to control how the result of 'read' is cloned before being passed to 'update'
// Defaults to `structuredClone`[1] if available, otherwise `v8.deserialize(v8.serialize(obj))`.
clone(actual) {
return myCustomClone(actual)
},
})

export default async (_workspaceDir) => {
Expand Down
1 change: 1 addition & 0 deletions __fixtures__/workspace-1/name.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root
1 change: 1 addition & 0 deletions __fixtures__/workspace-1/packages/bar/name.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bar
1 change: 1 addition & 0 deletions __fixtures__/workspace-1/packages/foo/name.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
old
66 changes: 62 additions & 4 deletions __tests__/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { jest } from '@jest/globals'
import { execa } from 'execa'
import fsx from 'fs-extra'
import { loadJsonFile } from 'load-json-file'
import path from 'path'
import tempy from 'tempy'
import { fileURLToPath } from 'url'
import { createUpdateOptions, performUpdates } from '../src/index.js'
import { createFormat, createUpdateOptions, performUpdates } from '../src/index.js'

const __dirname = path.dirname(fileURLToPath(import.meta.url))
const WORKSPACE1 = path.join(__dirname, '../__fixtures__/workspace-1')
Expand Down Expand Up @@ -40,7 +41,7 @@ test('updates are detected', async () => {
},
},
}),
{ test: true }
{ test: true },
)
expect(result).toEqual([
{
Expand Down Expand Up @@ -73,7 +74,7 @@ test('new config files are added', async () => {
return { foo: 1 }
},
},
})
}),
)
expect(result).toBe(null)
const fooConfig = await loadJsonFile<{ foo: number }>(path.join(tmp, 'packages/foo/config/config.json'))
Expand All @@ -96,9 +97,66 @@ test('config files are removed', async () => {
return config
},
},
})
}),
)
expect(result).toBe(null)
expect(fsx.existsSync(path.join(tmp, 'packages/foo/tsconfig.json'))).toBeFalsy()
expect(fsx.existsSync(path.join(tmp, 'packages/bar/tsconfig.json'))).toBeTruthy()
})

test('custom format plugins are used', async () => {
const tmp = tempy.directory()
await fsx.copy(WORKSPACE1, tmp)
const mockFormat = createFormat({
read(options) {
return fsx.readFile(options.resolvedPath, 'utf8')
},
clone(value) {
return value
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe also do some manipulations here and see if they were reflected

},
update(actual, updater, options) {
return updater(actual, options)
},
equal(expected, actual) {
return actual === expected
},
write(expected, options) {
return fsx.writeFile(options.resolvedPath, expected)
},
})
const spy = {
read: jest.spyOn(mockFormat, 'read'),
clone: jest.spyOn(mockFormat, 'clone'),
update: jest.spyOn(mockFormat, 'update'),
equal: jest.spyOn(mockFormat, 'equal'),
write: jest.spyOn(mockFormat, 'write'),
}
const updateOptions = createUpdateOptions({
files: {
['name.txt [#mock]']: (_, { manifest }) => {
return typeof manifest.name === 'string' ? `${manifest.name}\n` : null
},
},
formats: {
['#mock']: mockFormat,
},
})
const result = await performUpdates(tmp, updateOptions)
expect(result).toBe(null)
expect(spy.read).toHaveBeenCalledTimes(3)
expect(spy.clone).toHaveBeenCalledTimes(3)
expect(spy.clone).toHaveBeenCalledWith('old\n')
expect(spy.clone).toHaveBeenCalledWith('bar\n')
expect(spy.clone).toHaveBeenCalledWith('root\n')
expect(spy.update).toHaveBeenCalledTimes(3)
expect(spy.update).toHaveBeenCalledWith('old\n', expect.any(Function), expect.any(Object))
expect(spy.update).toHaveBeenCalledWith('bar\n', expect.any(Function), expect.any(Object))
expect(spy.update).toHaveBeenCalledWith('root\n', expect.any(Function), expect.any(Object))
expect(spy.equal).toHaveBeenCalledTimes(2)
expect(spy.equal).toHaveBeenCalledWith('foo\n', 'old\n', expect.any(Object))
expect(spy.equal).toHaveBeenCalledWith('bar\n', 'bar\n', expect.any(Object))
expect(spy.write).toHaveBeenCalledTimes(1)
expect(fsx.existsSync(path.join(tmp, 'packages/foo/name.txt'))).toBeTruthy()
expect(fsx.existsSync(path.join(tmp, 'packages/bar/name.txt'))).toBeTruthy()
expect(fsx.existsSync(path.join(tmp, 'name.txt'))).toBeFalsy()
})
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"write-json-file": "^5.0.0"
},
"devDependencies": {
"@jest/globals": "^29.6.2",
"@types/fs-extra": "^11.0.1",
"@types/jest": "^29.5.3",
"@types/node": "^20.4.5",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 14 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,14 @@ type UpdateError =

export async function performUpdates<
FileNameWithOptions extends string,
UserDefinedFormatPlugins extends BaseFormatPlugins
UserDefinedFormatPlugins extends BaseFormatPlugins,
>(
workspaceDir: string,
updateParam:
| UpdateOptionsLegacy<FileNameWithOptions>
| UpdateOptions<FileNameWithOptions>
| UpdateOptionsWithFormats<FileNameWithOptions, UserDefinedFormatPlugins>,
opts?: { test?: boolean }
opts?: { test?: boolean },
): Promise<null | UpdateError[]> {
const update = 'files' in updateParam ? updateParam : { files: updateParam }
let pkgs = await findWorkspacePackagesNoCheck(workspaceDir)
Expand All @@ -87,8 +87,13 @@ export async function performUpdates<
resolvedPath,
_writeProjectManifest: writeProjectManifest,
}
const formatPluginClone = formatPlugin.clone ?? clone
const actual = (await fileExists(resolvedPath)) ? await formatPlugin.read(formatHandlerOptions) : null
const expected = await formatPlugin.update(clone(actual), updateFile as any, formatHandlerOptions)
const expected = await formatPlugin.update(
actual != null ? formatPluginClone(actual) : null,
updateFile as any,
formatHandlerOptions,
)
const equal =
(actual == null && expected == null) ||
(actual != null && expected != null && (await formatPlugin.equal(expected, actual, formatHandlerOptions)))
Expand All @@ -107,8 +112,8 @@ export async function performUpdates<

errors.push({ actual, expected, path: resolvedPath })
} catch (error) {
const errorMessage = `Error while processing ${resolvedPath}: ${error.message}`;
errors.push({ exception: errorMessage });
const errorMessage = `Error while processing ${resolvedPath}: ${error.message}`
errors.push({ exception: errorMessage })
}
}
}
Expand All @@ -120,7 +125,7 @@ function printJsonDiff(actual: unknown, expected: unknown, out: NodeJS.WriteStre
printDiff(
typeof actual !== 'string' ? JSON.stringify(actual, null, 2) : actual,
typeof expected !== 'string' ? JSON.stringify(expected, null, 2) : expected,
out
out,
)
}

Expand All @@ -141,7 +146,7 @@ function parseFileKey(fileKey: string, formatPlugins: Record<string, FormatPlugi

if (!formatPlugin) {
throw new Error(
`Configuration error: there is no format plugin for fileKey "${fileKey}" with explicit format specifier "${extension}"`
`Configuration error: there is no format plugin for fileKey "${fileKey}" with explicit format specifier "${extension}"`,
)
}

Expand All @@ -156,8 +161,8 @@ function parseFileKey(fileKey: string, formatPlugins: Record<string, FormatPlugi
if (!extension) {
throw new Error(
`Configuration error: there is no format plugin for fileKey "${fileKey}", supported extensions are ${Object.keys(
formatPlugins
)}`
formatPlugins,
)}`,
)
}

Expand Down
9 changes: 8 additions & 1 deletion src/updater/formatPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,18 @@ export interface FormatPlugin<Content> {
/** Called only if file exists */
read(options: FormatPluginFnOptions): PromiseOrValue<Content>

/**
* Used to clone the object returned by `read` before passing it to `update`.
* Defaults to `structuredClone`[1] if available, otherwise `v8.deserialize(v8.serialize(obj))`.
* [1]: https://developer.mozilla.org/en-US/docs/web/api/structuredclone
*/
clone?(value: Content): Content

/** `actual` is `null` when file doesn't exist */
update(
actual: Content | null,
updater: Updater<Content>,
options: FormatPluginFnOptions
options: FormatPluginFnOptions,
): PromiseOrValue<Content | null>

/** Called only if check for equality is required (`actual != null` & `expected != null`) */
Expand Down