mem-fs-editor 11.1.4 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,5 @@
1
1
  # mem-fs-editor
2
2
 
3
- [![Node.js CI](https://github.com/SBoudrias/mem-fs-editor/workflows/Node.js%20CI/badge.svg)](https://github.com/SBoudrias/mem-fs-editor/actions?query=workflow%3A%22Node.js+CI%22)
4
3
  [![NPM version](https://badge.fury.io/js/mem-fs-editor.svg)](http://badge.fury.io/js/mem-fs-editor)
5
4
  [![Coverage Status](https://codecov.io/gh/SBoudrias/mem-fs-editor/branch/master/graph/badge.svg)](https://codecov.io/gh/SBoudrias/mem-fs-editor)
6
5
 
@@ -55,12 +54,13 @@ Append the new contents to the current file contents.
55
54
  - `options.separator` (default `os.EOL`). Separator to insert between current and new contents.
56
55
  - `options.create` (default `false`). Create the file if doesn't exists.
57
56
 
58
- ### `#appendTpl(filepath, contents, context[, templateOptions[, [options]])`
57
+ ### `#appendTpl(filepath, contents, data[, options])`
59
58
 
60
- Append the new `contents` to the exsting `filepath` content and parse the new contents as an [ejs](http://ejs.co/) template where `context` is the template context (the variable names available inside the template).
59
+ Append the new `contents` to the existing `filepath` content and parse the new contents as an [ejs](http://ejs.co/) template where `data` is the template context (the variable names available inside the template).
61
60
 
62
61
  - `options.trimEnd` (default `true`). Trim trailing whitespace of the current file contents.
63
62
  - `options.separator` (default `os.EOL`). Separator to insert between current and new contents.
63
+ - `options.transformOptions`. Options passed to the EJS renderer when processing the template, similar to `copyTpl`.
64
64
 
65
65
  ### `#extendJSON(filepath, contents[, replacer [, space]])`
66
66
 
@@ -74,38 +74,45 @@ Delete a file or a directory.
74
74
 
75
75
  `filePath` can also be a `glob`. If `filePath` is glob, you can optionally pass in an `options.globOptions` object to change its pattern matching behavior. The full list of options are being described [here](https://github.com/mrmlnc/fast-glob#options-1). The `sync` flag is forced to be `true` in `globOptions`.
76
76
 
77
- ### `#copy(from, to, [options], context[, templateOptions ])`
77
+ ### `#copy(from, to, [options])`
78
78
 
79
79
  Copy file(s) from the `from` path to the `to` path.
80
80
  When passing array, you should pass `options.fromBasePath` to be used to calculate the `to` relative path. The common directory will be detected and used as `fromBasePath` otherwise.
81
81
 
82
- Optionally, pass an `options.process` function (`process(contents)`) returning a string or a buffer who'll become the new file content. The process function will take a single contents argument who is the copied file contents as a `Buffer`.
82
+ Optionally, pass an `options.fileTransform` function (`fileTransform(destinationPath, sourcePath, contents)`) that transforms both the destination path and file contents. The function takes two arguments:
83
+ - `destinationPath`: The resolved destination file path
84
+ - `sourcePath`: The resolved source file path
85
+ - `contents`: The file contents as a `Buffer`
86
+
87
+ It should return a tuple `[newFilepath, newContents]` where:
88
+ - `newFilepath`: The transformed destination path (string)
89
+ - `newContents`: The transformed file contents (Buffer)
83
90
 
84
91
  `options.ignoreNoMatch` can be used to silence the error thrown if no files match the `from` pattern.
85
92
  `options.append` can be used to append `from` contents to `to` instead of copying, when the file is already loaded in mem-fs (safe for regeneration).
86
93
 
87
94
  `from` can be a glob pattern that'll be match against the file system. If that's the case, then `to` must be an output directory. For a globified `from`, you can optionally pass in an `options.globOptions` object to change its pattern matching behavior. The full list of options are being described [here](https://github.com/mrmlnc/fast-glob#options-1). The `nodir` flag is forced to be `true` in `globOptions` to ensure a vinyl object representing each matching directory is marked as `deleted` in the `mem-fs` store.
88
95
 
89
- Optionally, when `from` is a glob pattern, pass an `options.processDestinationPath` function (`processDestinationPath(destinationFile)`) returning a string who'll become the new file name.
90
-
91
96
  `options.noGlob` can be used to by bypass glob matching entirely. In that case, `from` will directly match file paths against the file system.
92
97
 
93
- ### `#copyAsync(from, to, [options], context[, templateOptions ])`
98
+ ### `#copyAsync(from, to, [options])`
94
99
 
95
100
  Async version of `copy`.
96
101
 
97
102
  `copy` loads `from` to memory and copy its contents to `to`.
98
103
  `copyAsync` copies directly from the disk to `to`, falling back to `copy` behavior if the file doesn't exists on disk.
99
104
 
100
- Same parameters of `copy` (see [copy() documentation for more details](#copyfrom-to-options-context-templateoptions-)).
105
+ Same parameters of `copy`
106
+
107
+ See [copy() documentation for more details](#copyfrom-to-options).
101
108
 
102
- ### `#copyTpl(from, to, context[, templateOptions [, copyOptions]])`
109
+ ### `#copyTpl(from, to, data[, options])`
103
110
 
104
- Copy the `from` file and, if it is not a binary file, parse its content as an [ejs](http://ejs.co/) template where `context` is the template context (the variable names available inside the template).
111
+ Copy the `from` file and, if it is not a binary file, parse its content as an [ejs](http://ejs.co/) template where `data` is the template context (the variable names available inside the template).
105
112
 
106
- You can optionally pass a `templateOptions` object. `mem-fs-editor` automatically setup the filename option so you can easily use partials.
113
+ `options.transformOptions` replaced the old `tplOptions` parameter and is passed as ejs options. `mem-fs-editor` automatically setup the filename option so you can easily use partials.
107
114
 
108
- You can also optionally pass a `copyOptions` object (see [copy() documentation for more details](#copyfrom-to-options-context-templateoptions-)).
115
+ You can also optionally pass a `options` object (see [copy() documentation for more details](#copyfrom-to-options)).
109
116
 
110
117
  Templates syntax looks like this:
111
118
 
@@ -122,13 +129,13 @@ Dir syntax looks like this:
122
129
 
123
130
  Refer to the [ejs documentation](http://ejs.co/) for more details.
124
131
 
125
- ### `#copyTplAsync(from, to, [options], context[, templateOptions ])`
132
+ ### `#copyTplAsync(from, to, data[, options])`
126
133
 
127
134
  Async version of `copyTpl` that uses `copyAsync` instead of `copy`.
128
135
 
129
136
  Can be used for best performance. Reduces overheads.
130
137
 
131
- Same parameters of `copyTpl` (see [copyTpl() documentation for more details](#copyfrom-to-options-context-templateoptions-)).
138
+ Same parameters of `copyTpl` (see [copyTpl() documentation for more details](#copytplfrom-to-data-options)).
132
139
 
133
140
  ### `#move(from, to, [options])`
134
141
 
@@ -1,4 +1,5 @@
1
- import { Data, Options } from 'ejs';
1
+ import ejs from 'ejs';
2
2
  import type { MemFsEditor } from '../index.js';
3
- import { AppendOptions } from './append.js';
4
- export default function appendTpl(this: MemFsEditor, to: string, contents: string | Buffer, context?: Data, tplSettings?: Options, options?: AppendOptions): void;
3
+ export default function appendTpl(this: MemFsEditor, to: string, contents: string | Buffer, data?: ejs.Data, options?: NonNullable<Parameters<MemFsEditor['append']>[2]> & {
4
+ transformOptions?: ejs.Options;
5
+ }): void;
@@ -1,4 +1,7 @@
1
- import { render } from '../util.js';
2
- export default function appendTpl(to, contents, context, tplSettings, options) {
3
- this.append(to, render(contents.toString(), context, tplSettings), options);
1
+ import ejs from 'ejs';
2
+ export default function appendTpl(to, contents, data, options) {
3
+ if (options?.transformOptions?.async) {
4
+ throw new Error('Async EJS rendering is not supported');
5
+ }
6
+ this.append(to, ejs.render(contents.toString(), data, { ...options?.transformOptions, async: false }), options);
4
7
  }
@@ -1,7 +1,6 @@
1
1
  import type { MemFsEditor } from '../index.js';
2
- export type AppendOptions = {
2
+ export default function append(this: MemFsEditor, to: string, contents: string | Buffer, options?: {
3
3
  create?: boolean;
4
4
  trimEnd?: boolean;
5
5
  separator?: string;
6
- };
7
- export default function append(this: MemFsEditor, to: string, contents: string | Buffer, options?: AppendOptions): void;
6
+ }): void;
@@ -1,20 +1,18 @@
1
1
  import { EOL } from 'os';
2
2
  export default function append(to, contents, options) {
3
- options = {
3
+ const opts = {
4
+ create: false,
4
5
  trimEnd: true,
5
6
  separator: EOL,
6
7
  ...options,
7
8
  };
8
- if (!this.exists(to) && options.create) {
9
- this.write(to, contents);
10
- return;
9
+ if (!this.exists(to) && !opts.create) {
10
+ throw new Error(`${to} doesn't exist`);
11
11
  }
12
- let currentContents = this.read(to);
13
- if (!currentContents) {
14
- throw new Error(`Error appending to ${to}, file is empty.`);
12
+ let currentContent = this.read(to, { defaults: '' });
13
+ if (currentContent && opts.trimEnd) {
14
+ currentContent = currentContent.trimEnd();
15
15
  }
16
- if (currentContents && options.trimEnd) {
17
- currentContents = currentContents.replace(/\s+$/, '');
18
- }
19
- this.write(to, currentContents + options.separator + contents.toString());
16
+ const newContent = currentContent ? currentContent + opts.separator + contents.toString() : contents;
17
+ this.write(to, newContent);
20
18
  }
@@ -1,2 +1,2 @@
1
1
  import type { MemFsEditorFile } from '../index.js';
2
- export default function commitFileAsync<EditorFile extends MemFsEditorFile>(file: EditorFile): Promise<void>;
2
+ export default function commitFileAsync(file: MemFsEditorFile): Promise<void>;
@@ -23,16 +23,13 @@ async function write(file) {
23
23
  await fs.writeFile(file.path, file.contents, { mode: newMode });
24
24
  if (newMode !== undefined) {
25
25
  const { mode: existingMode } = await fs.stat(file.path);
26
+ /* c8 ignore next */
26
27
  // eslint-disable-next-line no-bitwise
27
28
  if ((existingMode & 0o777) !== (newMode & 0o777)) {
28
29
  await fs.chmod(file.path, newMode);
29
30
  }
30
31
  }
31
32
  }
32
- async function remove(file) {
33
- const remove = fs.rm || fs.rmdir;
34
- await remove(file.path, { recursive: true });
35
- }
36
33
  export default async function commitFileAsync(file) {
37
34
  if (isFileStateModified(file)) {
38
35
  setCommittedFile(file);
@@ -40,7 +37,7 @@ export default async function commitFileAsync(file) {
40
37
  }
41
38
  else if (isFileStateDeleted(file) && !isFileNew(file)) {
42
39
  setCommittedFile(file);
43
- await remove(file);
40
+ await fs.rm(file.path, { recursive: true });
44
41
  }
45
42
  clearFileState(file);
46
43
  }
@@ -1,16 +1,32 @@
1
- import type { Data, Options } from 'ejs';
2
- import { type Options as GlobbyOptions } from 'globby';
1
+ import { GlobOptions } from 'tinyglobby';
3
2
  import type { MemFsEditor } from '../index.js';
4
- import { AppendOptions } from './append.js';
5
- import { CopySingleOptions } from './copy.js';
6
- export type CopyAsyncOptions = CopySingleAsyncOptions & {
7
- globOptions?: GlobbyOptions;
8
- processDestinationPath?: (string: any) => string;
9
- ignoreNoMatch?: boolean;
10
- };
11
- export declare function copyAsync(this: MemFsEditor, from: string | string[], to: string, options?: CopyAsyncOptions, context?: Data, tplSettings?: Options): Promise<void>;
12
- export type CopySingleAsyncOptions = AppendOptions & CopySingleOptions & {
3
+ import type { Options as MultimatchOptions } from 'multimatch';
4
+ type CopySingleAsyncOptions = Parameters<MemFsEditor['append']>[2] & {
13
5
  append?: boolean;
14
- processFile?: (this: MemFsEditor, filepath: string) => string | Promise<string | Buffer>;
6
+ /**
7
+ * @experimental This API is experimental and may change without a major version bump.
8
+ *
9
+ * Transform both the file path and content during copy.
10
+ * @param destinationPath The destination file path
11
+ * @param sourcePath The source file path
12
+ * @param contents The file content as Buffer
13
+ * @returns Tuple of [new filepath, new content]
14
+ */
15
+ fileTransform?: (destinationPath: string, sourcePath: string, contents: Buffer) => [string, string | Buffer] | Promise<[string, string | Buffer]>;
16
+ };
17
+ type CopyAsyncOptions = CopySingleAsyncOptions & {
18
+ noGlob?: boolean;
19
+ /**
20
+ * Options for disk globbing.
21
+ * Glob options that should be compatible with minimatch results.
22
+ */
23
+ globOptions?: Pick<GlobOptions, 'caseSensitiveMatch' | 'cwd' | 'debug' | 'deep' | 'dot' | 'expandDirectories' | 'followSymbolicLinks'>;
24
+ /**
25
+ * Options for store files matching.
26
+ */
27
+ storeMatchOptions?: MultimatchOptions;
28
+ ignoreNoMatch?: boolean;
29
+ fromBasePath?: string;
15
30
  };
16
- export declare function _copySingleAsync(this: MemFsEditor, from: string, to: string, options?: CopySingleAsyncOptions): Promise<void>;
31
+ export declare function copyAsync(this: MemFsEditor, from: string | string[], to: string, options?: CopyAsyncOptions): Promise<void>;
32
+ export {};
@@ -1,95 +1,115 @@
1
1
  import assert from 'assert';
2
2
  import fs from 'fs';
3
3
  import fsPromises from 'fs/promises';
4
- import path, { resolve } from 'path';
5
- import { globbySync, isDynamicPattern } from 'globby';
4
+ import path from 'path';
5
+ import createDebug from 'debug';
6
+ import { glob, isDynamicPattern } from 'tinyglobby';
6
7
  import multimatch from 'multimatch';
7
- import { render, globify, getCommonPath } from '../util.js';
8
8
  import normalize from 'normalize-path';
9
9
  import File from 'vinyl';
10
- async function applyProcessingFileFunc(processFile, filename) {
11
- const output = await Promise.resolve(processFile.call(this, filename));
12
- return Buffer.isBuffer(output) ? output : Buffer.from(output);
13
- }
14
- function renderFilepath(filepath, context, tplSettings) {
15
- if (!context) {
16
- return filepath;
17
- }
18
- return render(filepath, context, tplSettings);
19
- }
20
- async function getOneFile(from) {
21
- let oneFile;
22
- if (typeof from === 'string') {
23
- oneFile = from;
24
- }
25
- else {
26
- return undefined;
27
- }
28
- const resolved = path.resolve(oneFile);
10
+ import { resolveFromPaths, getCommonPath, globify, resolveGlobOptions } from '../util.js';
11
+ import { writeInternal } from './write.js';
12
+ const debug = createDebug('mem-fs-editor:copy-async');
13
+ async function getOneFile(filepath) {
14
+ const resolved = path.resolve(filepath);
29
15
  try {
30
16
  if ((await fsPromises.stat(resolved)).isFile()) {
31
17
  return resolved;
32
18
  }
19
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
33
20
  }
34
- catch (_) { }
21
+ catch (error) { }
35
22
  return undefined;
36
23
  }
37
- export async function copyAsync(from, to, options, context, tplSettings) {
24
+ export async function copyAsync(from, to, options = {}) {
38
25
  to = path.resolve(to);
39
- options = options || {};
40
- const oneFile = await getOneFile(from);
41
- if (oneFile) {
42
- return this._copySingleAsync(oneFile, renderFilepath(to, context, tplSettings), options);
26
+ const { noGlob } = options;
27
+ const hasGlobOptions = Boolean(options.globOptions);
28
+ const hasMultimatchOptions = Boolean(options.storeMatchOptions);
29
+ assert(!noGlob || !hasGlobOptions, '`noGlob` and `globOptions` are mutually exclusive');
30
+ assert(!noGlob || !hasMultimatchOptions, '`noGlob` and `storeMatchOptions` are mutually exclusive');
31
+ if (typeof from === 'string') {
32
+ const oneFile = await getOneFile(from);
33
+ if (oneFile) {
34
+ return copySingleAsync(this, oneFile, to, options);
35
+ }
43
36
  }
44
- const fromGlob = globify(from);
45
- const globOptions = { ...options.globOptions, nodir: true };
46
- const diskFiles = globbySync(fromGlob, globOptions).map((filepath) => path.resolve(filepath));
37
+ const { fromBasePath = getCommonPath(from) } = options;
38
+ const resolvedFromPaths = resolveFromPaths({ from, fromBasePath });
39
+ const hasDynamicPattern = resolvedFromPaths.some((f) => isDynamicPattern(normalize(f.from)));
40
+ const { preferFiles } = resolveGlobOptions({
41
+ noGlob,
42
+ hasDynamicPattern,
43
+ hasGlobOptions,
44
+ });
47
45
  const storeFiles = [];
48
- this.store.each((file) => {
49
- const normalizedFilepath = normalize(file.path);
50
- // The store may have a glob path and when we try to copy it will fail because not real file
51
- if (!isDynamicPattern(normalizedFilepath) &&
52
- multimatch([normalizedFilepath], fromGlob).length !== 0 &&
53
- !diskFiles.includes(file.path)) {
54
- storeFiles.push(file.path);
46
+ const globResolved = [];
47
+ for (const resolvedFromPath of resolvedFromPaths) {
48
+ const { resolvedFrom } = resolvedFromPath;
49
+ if (this.store.existsInMemory(resolvedFrom)) {
50
+ storeFiles.push(resolvedFrom);
55
51
  }
56
- });
52
+ else {
53
+ globResolved.push(resolvedFromPath);
54
+ }
55
+ }
56
+ let diskFiles = [];
57
+ if (globResolved.length > 0) {
58
+ const patterns = globResolved.map((file) => globify(file.from)).flat();
59
+ diskFiles = (await glob(patterns, { cwd: fromBasePath, ...options.globOptions, absolute: true, onlyFiles: true })).map((file) => path.resolve(file));
60
+ const normalizedStoreFilePaths = this.store
61
+ .all()
62
+ .map((file) => file.path)
63
+ .filter((filePath) => !diskFiles.includes(filePath))
64
+ .map((filePath) => normalize(filePath))
65
+ // The store may have a glob path and when we try to copy it will fail because not real file
66
+ .filter((filePath) => !isDynamicPattern(filePath));
67
+ multimatch(normalizedStoreFilePaths, patterns, options.storeMatchOptions).forEach((filePath) => {
68
+ storeFiles.push(path.resolve(filePath));
69
+ });
70
+ }
71
+ // Sanity checks: Makes sure we copy at least one file.
72
+ assert(options.ignoreNoMatch || diskFiles.length > 0 || storeFiles.length > 0, 'Trying to copy from a source that does not exist: ' + String(from));
73
+ // If `from` is an array, or if it contains any dynamic patterns, or if it doesn't exist, `to` must be a directory.
74
+ const treatToAsDir = Array.isArray(from) || !preferFiles || globResolved.length > 0;
57
75
  let generateDestination = () => to;
58
- if (Array.isArray(from) || !this.exists(from) || isDynamicPattern(normalize(from))) {
76
+ if (treatToAsDir) {
59
77
  assert(!this.exists(to) || fs.statSync(to).isDirectory(), 'When copying multiple files, provide a directory as destination');
60
- const processDestinationPath = options.processDestinationPath || ((path) => path);
61
- const root = getCommonPath(from);
62
- generateDestination = (filepath) => {
63
- const toFile = path.relative(root, filepath);
64
- return processDestinationPath(path.join(to, toFile));
65
- };
78
+ generateDestination = (filepath) => path.join(to, path.relative(fromBasePath, filepath));
66
79
  }
67
- // Sanity checks: Makes sure we copy at least one file.
68
- assert(options.ignoreNoMatch || diskFiles.length > 0 || storeFiles.length > 0, 'Trying to copy from a source that does not exist: ' + from);
69
80
  await Promise.all([
70
- ...diskFiles.map((file) => this._copySingleAsync(file, renderFilepath(generateDestination(file), context, tplSettings), options)),
71
- ...storeFiles.map((file) => Promise.resolve(this._copySingle(file, renderFilepath(generateDestination(file), context, tplSettings), options))),
81
+ ...diskFiles.map((file) => copySingleAsync(this, file, generateDestination(file), options)),
82
+ ...storeFiles.map((file) => copySingleAsync(this, file, generateDestination(file), options)),
72
83
  ]);
73
84
  }
74
- export async function _copySingleAsync(from, to, options = {}) {
75
- if (!options.processFile) {
76
- return this._copySingle(from, to, options);
85
+ const defaultFileTransform = (destPath, _sourcePath, contents) => [
86
+ destPath,
87
+ contents,
88
+ ];
89
+ async function copySingleAsync(editor, from, to, options = {}) {
90
+ from = path.resolve(from);
91
+ debug('Copying %s to %s with %o', from, to, options);
92
+ const file = editor.store.get(from);
93
+ assert(file.contents, `Cannot copy empty file ${from}`);
94
+ const { fileTransform = defaultFileTransform } = options;
95
+ const transformPromise = fileTransform(path.resolve(to), from, file.contents);
96
+ let contents;
97
+ [to, contents] = await transformPromise;
98
+ if (options.append && editor.store.existsInMemory(to)) {
99
+ editor.append(to, contents, { create: true, ...options });
77
100
  }
78
- from = resolve(from);
79
- const contents = await applyProcessingFileFunc.call(this, options.processFile, from);
80
- if (options.append) {
81
- if (!this.store.existsInMemory) {
82
- throw new Error('Current mem-fs is not compatible with append');
83
- }
84
- if (this.store.existsInMemory(to)) {
85
- this.append(to, contents, { create: true, ...options });
86
- return;
87
- }
101
+ else if (File.isVinyl(file)) {
102
+ writeInternal(editor.store, Object.assign(file.clone({ contents: false, deep: false }), {
103
+ contents: Buffer.from(contents),
104
+ path: to,
105
+ }));
106
+ }
107
+ else {
108
+ writeInternal(editor.store, new File({
109
+ contents: Buffer.from(contents),
110
+ stat: file.stat,
111
+ path: to,
112
+ history: [file.path],
113
+ }));
88
114
  }
89
- this._write(new File({
90
- contents,
91
- stat: await fsPromises.stat(from),
92
- path: to,
93
- history: [from],
94
- }));
95
115
  }
@@ -1,4 +1,5 @@
1
1
  import type { MemFsEditor } from '../index.js';
2
- import { Data, Options } from 'ejs';
3
- import { CopyAsyncOptions } from './copy-async.js';
4
- export default function (this: MemFsEditor, from: string | string[], to: string, context?: Data, tplSettings?: Options, options?: CopyAsyncOptions): Promise<void>;
2
+ import ejs from 'ejs';
3
+ export default function (this: MemFsEditor, from: string | string[], to: string, data?: ejs.Data, options?: Omit<NonNullable<Parameters<MemFsEditor['copyAsync']>[2]>, 'fileTransform'> & {
4
+ transformOptions?: ejs.Options;
5
+ }): Promise<void>;
@@ -1,23 +1,24 @@
1
- import fs from 'fs/promises';
2
- import { isBinary, renderFile } from '../util.js';
3
- export default async function (from, to, context, tplSettings, options) {
4
- context = context || {};
5
- tplSettings = tplSettings || {};
1
+ import { isBinary } from '../util.js';
2
+ import ejs from 'ejs';
3
+ export default async function (from, to, data = {}, options) {
6
4
  await this.copyAsync(from, to, {
7
- processDestinationPath: (path) => path.replace(/.ejs$/, ''),
8
5
  ...options,
9
- async processFile(filename) {
10
- if (isBinary(filename, null)) {
11
- return fs.readFile(filename);
12
- }
13
- return renderFile(filename, context, tplSettings);
6
+ async fileTransform(destinationPath, sourcePath, contents) {
7
+ const { transformOptions } = options ?? {};
8
+ const processedPath = await ejs.render(destinationPath, data, {
9
+ ...transformOptions,
10
+ cache: false, // Cache uses filename as key, which is not provided in this case.
11
+ });
12
+ const processedContent = isBinary(sourcePath, contents)
13
+ ? contents
14
+ : await ejs.render(contents.toString(), data, {
15
+ // Setting filename by default allow including partials.
16
+ filename: sourcePath,
17
+ // Async option cannot be set to true because `include()` then also become async which change the behaviors of templates.
18
+ // Users must pass async value in transformOptions if they want to use async features of ejs.
19
+ ...transformOptions,
20
+ });
21
+ return [processedPath.replace(/.ejs$/, ''), processedContent];
14
22
  },
15
- process: (contents, filename, destination) => this._processTpl({
16
- contents,
17
- filename,
18
- destination,
19
- context,
20
- tplSettings,
21
- }),
22
- }, context, tplSettings);
23
+ });
23
24
  }
@@ -1,11 +1,5 @@
1
- import { Data, Options } from 'ejs';
1
+ import ejs from 'ejs';
2
2
  import type { MemFsEditor } from '../index.js';
3
- import { CopySingleOptions } from './copy.js';
4
- export declare function _processTpl(this: MemFsEditor, { contents, filename, context, tplSettings, }: {
5
- contents: string | Buffer;
6
- filename: string;
7
- destination?: string;
8
- context?: Data;
9
- tplSettings?: Options;
10
- }): string | Buffer;
11
- export declare function copyTpl(this: MemFsEditor, from: string | string[], to: string, context?: Data, tplSettings?: Options, options?: CopySingleOptions): void;
3
+ export declare function copyTpl(this: MemFsEditor, from: string | string[], to: string, data?: ejs.Data, options?: Omit<NonNullable<Parameters<MemFsEditor['copy']>[2]>, 'fileTransform'> & {
4
+ transformOptions?: ejs.Options;
5
+ }): void;
@@ -1,21 +1,27 @@
1
- import { isBinary, render } from '../util.js';
2
- export function _processTpl({ contents, filename, context, tplSettings, }) {
3
- if (isBinary(filename, contents)) {
4
- return contents;
5
- }
6
- return render(contents.toString(), context, {
7
- // Setting filename by default allow including partials.
8
- filename,
9
- cache: true,
10
- ...tplSettings,
11
- });
12
- }
13
- export function copyTpl(from, to, context, tplSettings, options) {
14
- context = context || {};
15
- tplSettings = tplSettings || {};
1
+ import ejs from 'ejs';
2
+ import { isBinary } from '../util.js';
3
+ export function copyTpl(from, to, data = {}, options) {
16
4
  this.copy(from, to, {
17
- processDestinationPath: (path) => path.replace(/.ejs$/, ''),
18
5
  ...options,
19
- process: (contents, filename) => this._processTpl({ contents, filename, context, tplSettings }),
20
- }, context, tplSettings);
6
+ fileTransform(destinationPath, sourcePath, contents) {
7
+ const { transformOptions } = options ?? {};
8
+ if (transformOptions?.async) {
9
+ throw new Error('Async EJS rendering is not supported');
10
+ }
11
+ const processedPath = ejs.render(destinationPath, data, {
12
+ ...transformOptions,
13
+ cache: false, // Cache uses filename as key, which is not provided in this case.
14
+ async: false,
15
+ });
16
+ const processedContent = isBinary(sourcePath, contents)
17
+ ? contents
18
+ : ejs.render(contents.toString(), data, {
19
+ // Setting filename by default allow including partials.
20
+ filename: sourcePath,
21
+ ...transformOptions,
22
+ async: false,
23
+ });
24
+ return [processedPath.replace(/.ejs$/, ''), processedContent];
25
+ },
26
+ });
21
27
  }
@@ -1,16 +1,33 @@
1
- import { type Options as GlobbyOptions } from 'globby';
2
- import { Data, Options } from 'ejs';
1
+ import { type GlobOptions } from 'tinyglobby';
2
+ import type { Options as MultimatchOptions } from 'multimatch';
3
3
  import type { MemFsEditor } from '../index.js';
4
- export type CopyOptions = CopySingleOptions & {
4
+ type CopySingleOptions = {
5
+ append?: boolean;
6
+ /**
7
+ * @experimental This API is experimental and may change without a major version bump.
8
+ *
9
+ * Transform both the file path and content during copy.
10
+ * @param destinationPath The destination file path
11
+ * @param sourcePath The source file path
12
+ * @param contents The file content as Buffer
13
+ * @returns Tuple of [new filepath, new content]
14
+ */
15
+ fileTransform?: (destinationPath: string, sourcePath: string, contents: Buffer) => [string, string | Buffer];
16
+ };
17
+ type CopyOptions = CopySingleOptions & {
5
18
  noGlob?: boolean;
6
- globOptions?: GlobbyOptions;
19
+ /**
20
+ * Options for disk globbing.
21
+ * Glob options that should be compatible with minimatch results.
22
+ */
23
+ globOptions?: Pick<GlobOptions, 'caseSensitiveMatch' | 'cwd' | 'debug' | 'deep' | 'dot' | 'expandDirectories' | 'followSymbolicLinks'>;
24
+ /**
25
+ * Options for store files matching.
26
+ */
27
+ storeMatchOptions?: MultimatchOptions;
7
28
  ignoreNoMatch?: boolean;
8
29
  fromBasePath?: string;
9
- processDestinationPath?: (string: any) => string;
10
- };
11
- export declare function copy(this: MemFsEditor, from: string | string[], to: string, options?: CopyOptions, context?: Data, tplSettings?: Options): void;
12
- export type CopySingleOptions = {
13
- append?: boolean;
14
- process?: (contents: string | Buffer, filepath: string, destination: string) => string | Buffer;
15
30
  };
16
- export declare function _copySingle(this: MemFsEditor, from: string, to: string, options?: CopySingleOptions): void;
31
+ export declare function copy(this: MemFsEditor, from: string | string[], to: string, options?: CopyOptions): void;
32
+ export declare function copySingle(editor: MemFsEditor, from: string, to: string, options?: CopySingleOptions): void;
33
+ export {};