@calmdown/rolldown-plugin-copy 1.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/index.js +200 -0
- package/package.json +14 -0
package/index.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
const PLUGIN_NAME = "Copy";
|
|
5
|
+
|
|
6
|
+
const SL_IGNORE = "ignore";
|
|
7
|
+
const SL_COPY_FILE = "copy-file";
|
|
8
|
+
const SL_LINK_ABSOLUTE = "link-absolute";
|
|
9
|
+
const SL_LINK_RELATIVE = "link-relative";
|
|
10
|
+
|
|
11
|
+
const EK_FILE = "file";
|
|
12
|
+
const EK_LINK = "link";
|
|
13
|
+
const EK_UNKNOWN = "unknown";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} CopySingleTarget
|
|
17
|
+
* @property {string} srcFile path to the file to be copied
|
|
18
|
+
* @property {string} dstFile path to where the file should be copied to
|
|
19
|
+
* @property {string} [baseDir] base directory for relative paths (defaults to current directory)
|
|
20
|
+
* @property {"before"|"after"} [trigger="after"] when to run the operation (defaults to "after")
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {Object} CopyManyTarget
|
|
25
|
+
* @property {string} dstDir directory to where files should be copied or linked
|
|
26
|
+
* @property {string|string[]} include glob pattern(s) of files to include
|
|
27
|
+
* @property {string|string[]} [exclude] glob pattern(s) to exclude (optional)
|
|
28
|
+
* @property {string} [baseDir] base directory for relative paths (defaults to current directory)
|
|
29
|
+
* @property {"before"|"after"} [trigger="after"] when to run the operation (defaults to "after")
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {Object} CopyPluginOptions
|
|
34
|
+
* @property {(CopySingleTarget | CopyManyTarget)[]} targets desired copy/link operations
|
|
35
|
+
* @property {boolean} [dryRun=false] whether to perform a dry run, only logging actions without executing them (defaults to false)
|
|
36
|
+
* @property {boolean} [runOnce=true] when in watch mode, controls whether to only delete files on the first build (defaults to true)
|
|
37
|
+
* @property {"ignore"|"copy-file"|"link-absolute"|"link-relative"} [symLinks="ignore"] how to handle symlinks (defaults to "ignore")
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {CopyPluginOptions} pluginOptions
|
|
42
|
+
*/
|
|
43
|
+
export default function CopyPlugin(pluginOptions) {
|
|
44
|
+
const targets = pluginOptions?.targets ?? [];
|
|
45
|
+
const symLinks = [ SL_IGNORE, SL_COPY_FILE, SL_LINK_ABSOLUTE, SL_LINK_RELATIVE ].find(it => pluginOptions?.symLinks === it) ?? SL_IGNORE;
|
|
46
|
+
|
|
47
|
+
const exec = (context, message, block) => {
|
|
48
|
+
if (pluginOptions?.dryRun) {
|
|
49
|
+
message && context.info({
|
|
50
|
+
plugin: PLUGIN_NAME,
|
|
51
|
+
pluginCode: "DRY_RUN",
|
|
52
|
+
message,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return block();
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const execTarget = async (context, cwd, target) => {
|
|
62
|
+
const baseDir = target.baseDir ? path.resolve(cwd, target.baseDir) : cwd;
|
|
63
|
+
const entries = [];
|
|
64
|
+
if (target.srcFile) {
|
|
65
|
+
// single file
|
|
66
|
+
const src = path.resolve(baseDir, target.srcFile);
|
|
67
|
+
const stats = await fs.stat(src);
|
|
68
|
+
entries.push({
|
|
69
|
+
src,
|
|
70
|
+
dst: path.resolve(baseDir, target.dstFile),
|
|
71
|
+
kind: getKind(stats),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// many files
|
|
76
|
+
const include = toArray(target.include);
|
|
77
|
+
const globOptions = {
|
|
78
|
+
cwd,
|
|
79
|
+
exclude: toArray(target.exclude ?? []),
|
|
80
|
+
withFileTypes: true,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const dstDir = path.resolve(baseDir, target.dstDir);
|
|
84
|
+
for (const includePattern of include) {
|
|
85
|
+
for await (const entry of fs.glob(includePattern, globOptions)) {
|
|
86
|
+
entries.push({
|
|
87
|
+
src: path.join(entry.parentPath, entry.name),
|
|
88
|
+
dst: path.join(dstDir, entry.name),
|
|
89
|
+
kind: getKind(entry),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
const dstDir = path.dirname(entry.dst);
|
|
97
|
+
if (entry.kind === EK_FILE) {
|
|
98
|
+
await exec(context, null, () => fs.mkdir(dstDir, { recursive: true }));
|
|
99
|
+
await exec(context, `would copy file ${entry.src} -> ${entry.dst}`, () => fs.copyFile(entry.src, entry.dst));
|
|
100
|
+
context.addWatchFile(entry.src);
|
|
101
|
+
}
|
|
102
|
+
else if (entry.kind === EK_LINK && symLinks !== SL_IGNORE) {
|
|
103
|
+
const linkedPath = await resolveSymLink(entry.src);
|
|
104
|
+
if (!linkedPath) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await exec(context, null, () => fs.mkdir(dstDir, { recursive: true }));
|
|
109
|
+
switch (symLinks) {
|
|
110
|
+
case SL_COPY_FILE:
|
|
111
|
+
await exec(context, `would copy file ${linkedPath} -> ${entry.dst} resolved from symlink ${entry.src}`, () => fs.copyFile(linkedPath, entry.dst));
|
|
112
|
+
break;
|
|
113
|
+
|
|
114
|
+
case SL_LINK_ABSOLUTE:
|
|
115
|
+
await exec(context, `would create symlink ${entry.dst} pointing to ${linkedPath} resolved from symlink ${entry.src}`, () => fs.symlink(linkedPath, entry.dst));
|
|
116
|
+
break;
|
|
117
|
+
|
|
118
|
+
case SL_LINK_RELATIVE: {
|
|
119
|
+
const linkTargetPath = path.relative(entry.dst, linkedPath);
|
|
120
|
+
await exec(context, `would create symlink ${entry.dst} pointing to ${linkTargetPath} resolved from symlink ${entry.src}`, () => fs.symlink(linkTargetPath, entry.dst));
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
context.addWatchFile(linkedPath);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
let cwd = undefined;
|
|
131
|
+
let isFirstBeforeRun = true;
|
|
132
|
+
let isFirstAfterRun = true;
|
|
133
|
+
return {
|
|
134
|
+
name: PLUGIN_NAME,
|
|
135
|
+
async buildStart() {
|
|
136
|
+
if (pluginOptions?.runOnce !== false && !isFirstBeforeRun) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
isFirstBeforeRun = false;
|
|
141
|
+
cwd = process.cwd();
|
|
142
|
+
for (const target of targets) {
|
|
143
|
+
if (target.trigger === "before") {
|
|
144
|
+
await execTarget(this, cwd, target);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
async closeBundle() {
|
|
149
|
+
if (pluginOptions?.runOnce !== false && !isFirstAfterRun) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
isFirstAfterRun = false;
|
|
154
|
+
for (const target of targets) {
|
|
155
|
+
const { trigger } = target;
|
|
156
|
+
if (trigger === "after" || trigger === undefined) {
|
|
157
|
+
await execTarget(this, cwd, target);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function toArray(oneOrMore) {
|
|
165
|
+
return Array.isArray(oneOrMore) ? oneOrMore : [ oneOrMore ];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getKind(entry) {
|
|
169
|
+
if (entry.isFile()) {
|
|
170
|
+
return EK_FILE;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (entry.isSymbolicLink()) {
|
|
174
|
+
return EK_LINK;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return EK_UNKNOWN;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function resolveSymLink(linkPath, maxDepth = 8) {
|
|
181
|
+
const visited = new Set();
|
|
182
|
+
|
|
183
|
+
let current = linkPath;
|
|
184
|
+
let depth = 0;
|
|
185
|
+
do {
|
|
186
|
+
const st = await fs.stat(current);
|
|
187
|
+
if (!st.isSymbolicLink()) {
|
|
188
|
+
return current;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const target = await fs.readlink(current, "utf8");
|
|
192
|
+
visited.add(current);
|
|
193
|
+
|
|
194
|
+
current = path.join(path.dirname(current), target);
|
|
195
|
+
}
|
|
196
|
+
while (!visited.has(current) && ++depth < maxDepth);
|
|
197
|
+
|
|
198
|
+
// cycle detected or depth exceeded
|
|
199
|
+
return null;
|
|
200
|
+
}
|