ember.nvp 1.13.0 → 1.15.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.
@@ -0,0 +1,118 @@
1
+ import { packageJson, tsconfig } from "ember-apply";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { checkApp, migrateApp } from "../../app.js";
5
+ import { EMBER_CLI_BUILD } from "../../ember-cli-build.js";
6
+ import { firstExisting, listFiles, remove } from "../../files.js";
7
+ import { changedTestemFlags } from "../../layers.js";
8
+
9
+ /**
10
+ * Compiler options of the blueprint's tsconfig.json.
11
+ * `baseUrl` and `paths` map the app's module prefix to app/,
12
+ * which package.json `exports` does now.
13
+ */
14
+ const BLUEPRINT_COMPILER_OPTIONS = new Set(["allowJs", "baseUrl", "noEmit", "paths", "types"]);
15
+
16
+ /**
17
+ * The blueprint's types/global.d.ts, which sets up Glint 1 for loose-mode templates
18
+ */
19
+ const GLINT_ENVIRONMENT = /^\s*import\s+["']@glint\/environment-ember-loose["'];?\s*$/;
20
+
21
+ /**
22
+ * An app that builds with ember-cli:
23
+ * with broccoli, or with `@embroider/webpack` through `@embroider/compat`.
24
+ *
25
+ * This covers `@ember-tooling/classic-build-app-blueprint`,
26
+ * and the `app` blueprint that ember-cli had before it.
27
+ *
28
+ * The migration is the same as for `@ember/app-blueprint`,
29
+ * plus the moves that `ember-vite-codemod` makes:
30
+ * - app/index.html moves to index.html
31
+ *
32
+ * @type {import('#types').Migration}
33
+ */
34
+ export default {
35
+ label: "@ember-tooling/classic-build-app-blueprint",
36
+ type: "app",
37
+
38
+ detect(directory) {
39
+ let build = firstExisting(directory, EMBER_CLI_BUILD);
40
+
41
+ if (!build) return false;
42
+
43
+ let source = readFileSync(join(directory, build), "utf-8");
44
+
45
+ return (
46
+ source.includes("ember-cli/lib/broccoli/ember-app") && !source.includes("@embroider/vite")
47
+ );
48
+ },
49
+
50
+ async check(project) {
51
+ let report = await checkApp(project);
52
+ let root = project.directory;
53
+
54
+ if (listFiles(root, "server").length > 0) {
55
+ report.todo.push({
56
+ title: "Dev server middleware in server/",
57
+ where: listFiles(root, "server"),
58
+ action: "Move proxies and mocks to `server` in vite.config.mjs. Vite does not run server/.",
59
+ });
60
+ }
61
+
62
+ let testem = changedTestemFlags(root, "testem.js", "app");
63
+
64
+ if (testem) report.todo.push(testem);
65
+
66
+ let config = await blueprintTsconfig(root);
67
+ let custom = Object.keys(config?.compilerOptions ?? {}).filter(
68
+ (option) => !BLUEPRINT_COMPILER_OPTIONS.has(option),
69
+ );
70
+
71
+ if (config && custom.length > 0) {
72
+ report.todo.push({
73
+ title: "Compiler options that the new tsconfig.json does not have",
74
+ where: custom.map((option) => `tsconfig.json: ${option}`),
75
+ action: "Add the options that you still need to tsconfig.json.",
76
+ });
77
+ }
78
+
79
+ return report;
80
+ },
81
+
82
+ async run(project, report) {
83
+ let root = project.directory;
84
+ let indexHtml = project.hasFile("app/index.html") ? "app/index.html" : "index.html";
85
+
86
+ // Glint 1 and @tsconfig/ember do not check .gts, so the base writes a new tsconfig.json
87
+ if (await blueprintTsconfig(root)) {
88
+ await remove(project, ["tsconfig.json"]);
89
+ await packageJson.removeDevDependencies(["@tsconfig/ember"], root);
90
+
91
+ let types = (await project.read("types/global.d.ts")) ?? "";
92
+
93
+ if (GLINT_ENVIRONMENT.test(types)) {
94
+ await remove(project, ["types/global.d.ts"]);
95
+ }
96
+
97
+ if (listFiles(root, "types").length === 0) {
98
+ await remove(project, ["types"]);
99
+ }
100
+ }
101
+
102
+ await migrateApp(project, report, { indexHtml });
103
+ },
104
+ };
105
+
106
+ /**
107
+ * @param {string} root
108
+ * @returns {Promise<Record<string, any> | undefined>} tsconfig.json, when it is the blueprint's
109
+ */
110
+ async function blueprintTsconfig(root) {
111
+ if (!existsSync(join(root, "tsconfig.json"))) return;
112
+
113
+ let config = await tsconfig.read(join(root, "tsconfig.json")).catch(() => undefined);
114
+
115
+ if (config?.glint || String(config?.extends ?? "").startsWith("@tsconfig/ember")) {
116
+ return config;
117
+ }
118
+ }
@@ -0,0 +1,207 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { parseProgram, propertyValue, sourceOf, stringValue, walk } from "./javascript.js";
4
+
5
+ /**
6
+ * Babel plugins whose work ember-rolldown's defaults already do for a library:
7
+ * TypeScript, templates to `precompileTemplate`, and decorators.
8
+ */
9
+ const COVERED_BY_EMBER_ROLLDOWN = new Set([
10
+ "@babel/plugin-transform-typescript",
11
+ "babel-plugin-ember-template-compilation",
12
+ "decorator-transforms",
13
+ "@babel/plugin-proposal-decorators",
14
+ "@babel/plugin-proposal-class-properties",
15
+ "@babel/plugin-transform-class-properties",
16
+ "@babel/plugin-transform-class-static-block",
17
+ "@babel/plugin-proposal-private-methods",
18
+ "@babel/plugin-transform-private-methods",
19
+ "@embroider/addon-dev/template-colocation-plugin",
20
+ "ember-template-imports/src/babel-plugin",
21
+ ]);
22
+
23
+ /**
24
+ * Babel presets whose work ember-rolldown's defaults already do
25
+ */
26
+ const PRESETS_COVERED_BY_EMBER_ROLLDOWN = new Set(["@babel/preset-typescript"]);
27
+
28
+ /**
29
+ * @typedef {object} BabelConfig
30
+ * @property {string[]} plugins every plugin name that the config lists
31
+ * @property {boolean} hasCode whether plugins also come from code that cannot be read statically,
32
+ * such as `...macros.babelMacros`
33
+ * @property {string[]} presets every preset name that the config lists
34
+ * @property {string[]} templateTransforms the source of each AST transform that the template compiler runs
35
+ */
36
+
37
+ /**
38
+ * @param {string} root
39
+ * @param {string} file relative to `root`
40
+ * @returns {BabelConfig | undefined} undefined when the file is missing or does not parse
41
+ */
42
+ export function readBabelConfig(root, file) {
43
+ let path = join(root, file);
44
+
45
+ if (!existsSync(path)) return;
46
+
47
+ let source = readFileSync(path, "utf-8");
48
+
49
+ if (file.endsWith(".json") || file.endsWith(".babelrc")) {
50
+ return fromJSON(source);
51
+ }
52
+
53
+ return fromJavaScript(source, file);
54
+ }
55
+
56
+ /**
57
+ * @param {string} source
58
+ * @returns {BabelConfig | undefined}
59
+ */
60
+ function fromJSON(source) {
61
+ /** @type {any} */
62
+ let json;
63
+
64
+ try {
65
+ json = JSON.parse(source);
66
+ } catch {
67
+ return;
68
+ }
69
+
70
+ /** @type {string[]} */
71
+ let plugins = [];
72
+ /** @type {string[]} */
73
+ let templateTransforms = [];
74
+
75
+ for (let entry of json.plugins ?? []) {
76
+ let [name, options] = Array.isArray(entry) ? entry : [entry];
77
+
78
+ if (typeof name !== "string") continue;
79
+
80
+ plugins.push(name);
81
+
82
+ if (name === "babel-plugin-ember-template-compilation") {
83
+ for (let transform of options?.transforms ?? []) {
84
+ templateTransforms.push(JSON.stringify(transform));
85
+ }
86
+ }
87
+ }
88
+
89
+ /** @type {string[]} */
90
+ let presets = [];
91
+
92
+ for (let entry of json.presets ?? []) {
93
+ let [name] = Array.isArray(entry) ? entry : [entry];
94
+
95
+ if (typeof name === "string") presets.push(name);
96
+ }
97
+
98
+ return { plugins, presets, hasCode: false, templateTransforms };
99
+ }
100
+
101
+ /**
102
+ * @param {string} source
103
+ * @param {string} file
104
+ * @returns {BabelConfig | undefined}
105
+ */
106
+ function fromJavaScript(source, file) {
107
+ let program = parseProgram(source, file);
108
+
109
+ if (!program) return;
110
+
111
+ /** @type {BabelConfig} */
112
+ let config = { plugins: [], presets: [], hasCode: false, templateTransforms: [] };
113
+
114
+ walk(program, (node) => {
115
+ if (node.type !== "ObjectExpression") return;
116
+
117
+ let plugins = withoutFilter(propertyValue(node, "plugins"));
118
+ let presets = propertyValue(node, "presets");
119
+
120
+ if (presets?.type === "ArrayExpression") {
121
+ for (let element of presets.elements ?? []) {
122
+ let entry = element?.type === "ArrayExpression" ? element.elements?.[0] : element;
123
+
124
+ config.presets.push(pluginName(entry) ?? "...");
125
+ }
126
+ }
127
+
128
+ if (plugins?.type !== "ArrayExpression") return;
129
+
130
+ for (let element of plugins.elements ?? []) {
131
+ let entry = element?.type === "ArrayExpression" ? element.elements?.[0] : element;
132
+ let name = pluginName(entry);
133
+
134
+ if (name === undefined) {
135
+ config.hasCode = true;
136
+ continue;
137
+ }
138
+
139
+ config.plugins.push(name);
140
+
141
+ let options = element?.type === "ArrayExpression" ? element.elements?.[1] : undefined;
142
+ let transforms = propertyValue(options, "transforms");
143
+
144
+ if (
145
+ name === "babel-plugin-ember-template-compilation" &&
146
+ transforms?.type === "ArrayExpression"
147
+ ) {
148
+ for (let transform of transforms.elements ?? []) {
149
+ if (transform) config.templateTransforms.push(sourceOf(source, transform));
150
+ }
151
+ }
152
+ }
153
+ });
154
+
155
+ if (config.plugins.length === 0 && /plugins\s*:/.test(source)) {
156
+ config.hasCode = true;
157
+ }
158
+
159
+ return config;
160
+ }
161
+
162
+ /**
163
+ * @param {import('./javascript.js').Node | undefined} node
164
+ * @returns {string | undefined} `x` for `"x"`, `require.resolve("x")`, and `import.meta.resolve("x")`
165
+ */
166
+ function pluginName(node) {
167
+ let name = stringValue(node);
168
+
169
+ if (name !== undefined) return name;
170
+
171
+ if (node?.type === "CallExpression" && node.callee?.property?.name === "resolve") {
172
+ return stringValue(node.arguments?.[0]);
173
+ }
174
+ }
175
+
176
+ /**
177
+ * @param {import('./javascript.js').Node | undefined} node
178
+ * @returns {import('./javascript.js').Node | undefined} the array in `[...].filter(Boolean)`
179
+ */
180
+ function withoutFilter(node) {
181
+ if (node?.type === "CallExpression" && node.callee?.type === "MemberExpression") {
182
+ return node.callee.object;
183
+ }
184
+
185
+ return node;
186
+ }
187
+
188
+ /**
189
+ * @param {BabelConfig} config
190
+ * @returns {string[]} what the config does beyond ember-rolldown's defaults
191
+ */
192
+ export function beyondEmberRolldown(config) {
193
+ /** @type {string[]} */
194
+ let extra = [];
195
+
196
+ for (let plugin of config.plugins) {
197
+ if (!COVERED_BY_EMBER_ROLLDOWN.has(plugin.replace(/^module:/, ""))) extra.push(plugin);
198
+ }
199
+
200
+ for (let preset of config.presets) {
201
+ if (!PRESETS_COVERED_BY_EMBER_ROLLDOWN.has(preset)) extra.push(`preset ${preset}`);
202
+ }
203
+ if (config.templateTransforms.length > 0) extra.push("template transforms");
204
+ if (config.hasCode) extra.push("plugins from code");
205
+
206
+ return extra;
207
+ }
@@ -0,0 +1,238 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import {
4
+ importedNames,
5
+ parseProgram,
6
+ propertyNames,
7
+ propertyValue,
8
+ sourceOf,
9
+ walk,
10
+ } from "./javascript.js";
11
+
12
+ export const EMBER_CLI_BUILD = ["ember-cli-build.js", "ember-cli-build.mjs", "ember-cli-build.cjs"];
13
+
14
+ /**
15
+ * Modules that an ember-cli-build imports to build the way the blueprints do
16
+ */
17
+ const BLUEPRINT_IMPORTS = new Set([
18
+ "ember-cli/lib/broccoli/ember-app",
19
+ "ember-cli/lib/broccoli/ember-app.js",
20
+ "@embroider/compat",
21
+ "@embroider/vite",
22
+ "@embroider/webpack",
23
+ "@embroider/test-setup",
24
+ "@warp-drive/core/build-config",
25
+ "@warp-drive/build-config",
26
+ "@ember-data/private-build-infra",
27
+ "node:path",
28
+ "node:url",
29
+ "path",
30
+ "url",
31
+ ]);
32
+
33
+ /**
34
+ * `new EmberApp(defaults, { ... })` options that only tune the ember-cli build.
35
+ * Vite does the same work by itself.
36
+ */
37
+ const BUILD_ONLY_OPTIONS = new Set([
38
+ "ember-cli-babel",
39
+ "ember-cli-terser",
40
+ "ember-cli-uglify",
41
+ "ember-data",
42
+ "emberData",
43
+ "fingerprint",
44
+ "hinting",
45
+ "minifyCSS",
46
+ "minifyJS",
47
+ "sourcemaps",
48
+ "SRI",
49
+ "storeConfigInMeta",
50
+ "tests",
51
+ ]);
52
+
53
+ /**
54
+ * `compatBuild(app, packager, { ... })` options that do nothing without `@embroider/compat`
55
+ */
56
+ const COMPAT_ONLY_OPTIONS = new Set([
57
+ "allowUnsafeDynamicComponents",
58
+ "skipBabel",
59
+ "staticAddonTestSupportTrees",
60
+ "staticAddonTrees",
61
+ "staticComponents",
62
+ "staticEmberSource",
63
+ "staticHelpers",
64
+ "staticInvokables",
65
+ "staticModifiers",
66
+ ]);
67
+
68
+ /**
69
+ * @typedef {object} EmberCliBuild
70
+ * @property {string} file
71
+ * @property {string[]} options `new EmberApp(...)` options that change what the app does
72
+ * @property {string[]} compatOptions `compatBuild(...)` options that change what the app does
73
+ * @property {string[]} imports modules beyond the ones that the blueprints import
74
+ * @property {string[]} appImports the source of each `app.import(...)` call
75
+ * @property {{ from: string, options: string } | undefined} warpDrive the `setConfig(app, dir, options)` call
76
+ * @property {boolean} usesVite whether the build runs through `@embroider/vite`
77
+ */
78
+
79
+ /**
80
+ * @param {string} root
81
+ * @param {string} file relative to `root`
82
+ * @returns {EmberCliBuild | undefined} undefined when the file does not parse
83
+ */
84
+ export function readEmberCliBuild(root, file) {
85
+ let source = readFileSync(join(root, file), "utf-8");
86
+ let program = parseProgram(source, file);
87
+
88
+ if (!program) return;
89
+
90
+ let imports = importedNames(program);
91
+ let modules = new Set(imports.values());
92
+
93
+ /** @type {EmberCliBuild} */
94
+ let build = {
95
+ file,
96
+ options: [],
97
+ compatOptions: [],
98
+ imports: [],
99
+ appImports: [],
100
+ warpDrive: undefined,
101
+ usesVite: modules.has("@embroider/vite"),
102
+ };
103
+
104
+ for (let module of modules) {
105
+ if (!BLUEPRINT_IMPORTS.has(module)) build.imports.push(module);
106
+ }
107
+
108
+ walk(program, (node) => {
109
+ if (node.type === "NewExpression" && node.callee?.type === "Identifier") {
110
+ let from = imports.get(node.callee.name) ?? "";
111
+
112
+ if (!from.startsWith("ember-cli/lib/broccoli/ember-app")) return;
113
+
114
+ for (let name of propertyNames(node.arguments?.[1])) {
115
+ if (!BUILD_ONLY_OPTIONS.has(name)) build.options.push(name);
116
+ }
117
+
118
+ return;
119
+ }
120
+
121
+ if (node.type !== "CallExpression") return;
122
+
123
+ let callee = node.callee;
124
+
125
+ if (callee?.type === "MemberExpression" && callee.property?.name === "import") {
126
+ build.appImports.push(sourceOf(source, node).replace(/\s+/g, " "));
127
+ return;
128
+ }
129
+
130
+ if (callee?.type !== "Identifier") return;
131
+
132
+ let from = imports.get(callee.name);
133
+
134
+ if (callee.name === "compatBuild") {
135
+ for (let name of propertyNames(node.arguments?.[2])) {
136
+ if (!COMPAT_ONLY_OPTIONS.has(name)) build.compatOptions.push(name);
137
+ }
138
+ }
139
+
140
+ if (callee.name === "setConfig" && from && /build-config|private-build-infra/.test(from)) {
141
+ let options = node.arguments?.[2];
142
+
143
+ build.warpDrive = {
144
+ from,
145
+ options: options ? sourceOf(source, options) : "{}",
146
+ };
147
+ }
148
+ });
149
+
150
+ return build;
151
+ }
152
+
153
+ /**
154
+ * @param {EmberCliBuild} build
155
+ * @returns {string[]} everything that `readEmberCliBuild` found which ember.nvp cannot carry over
156
+ */
157
+ export function unsupportedIn(build) {
158
+ /** @type {string[]} */
159
+ let found = [];
160
+
161
+ for (let option of build.options) found.push(`${build.file}: new EmberApp option "${option}"`);
162
+ for (let option of build.compatOptions)
163
+ found.push(`${build.file}: compatBuild option "${option}"`);
164
+ for (let module of build.imports) found.push(`${build.file}: imports ${module}`);
165
+ for (let call of build.appImports) found.push(`${build.file}: ${call}`);
166
+
167
+ return found;
168
+ }
169
+
170
+ /**
171
+ * @param {string} root
172
+ * @param {string} file
173
+ * @returns {string[] | undefined} the paths below `ENV` that `config/environment.js` sets, such as `APP.rootElement`
174
+ */
175
+ export function readEnvironmentKeys(root, file) {
176
+ let source = readFileSync(join(root, file), "utf-8");
177
+ let program = parseProgram(source, file);
178
+
179
+ if (!program) return;
180
+
181
+ /** @type {Set<string>} */
182
+ let keys = new Set();
183
+
184
+ walk(program, (node) => {
185
+ if (node.type === "VariableDeclarator" && node.id?.name === "ENV") {
186
+ collectKeys(node.init, "", keys);
187
+ }
188
+
189
+ if (node.type === "AssignmentExpression" && node.left?.type === "MemberExpression") {
190
+ let path = memberPath(node.left);
191
+
192
+ if (path?.startsWith("ENV.")) {
193
+ keys.add(path.slice("ENV.".length));
194
+ collectKeys(node.right, path.slice("ENV.".length) + ".", keys);
195
+ }
196
+ }
197
+ });
198
+
199
+ return Array.from(keys);
200
+ }
201
+
202
+ /**
203
+ * @param {import('./javascript.js').Node | undefined} node
204
+ * @param {string} prefix
205
+ * @param {Set<string>} keys
206
+ */
207
+ function collectKeys(node, prefix, keys) {
208
+ if (node?.type !== "ObjectExpression") return;
209
+
210
+ for (let property of node.properties ?? []) {
211
+ let [name] = propertyNames({ type: "ObjectExpression", properties: [property] });
212
+
213
+ if (!name || name === "...") continue;
214
+
215
+ keys.add(prefix + name);
216
+ collectKeys(property.value, `${prefix}${name}.`, keys);
217
+ }
218
+ }
219
+
220
+ /**
221
+ * @param {import('./javascript.js').Node} node
222
+ * @returns {string | undefined} `ENV.APP.foo` for `ENV.APP.foo` and `ENV['APP'].foo`
223
+ */
224
+ function memberPath(node) {
225
+ if (node.type === "Identifier") return node.name;
226
+ if (node.type !== "MemberExpression") return;
227
+
228
+ let object = memberPath(node.object);
229
+ let property = node.computed
230
+ ? node.property?.type === "Literal"
231
+ ? String(node.property.value)
232
+ : undefined
233
+ : node.property?.name;
234
+
235
+ if (!object || property === undefined) return;
236
+
237
+ return `${object}.${property}`;
238
+ }
@@ -0,0 +1,120 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { mkdir, rename, rm } from "node:fs/promises";
3
+ import { dirname, join, relative, sep } from "node:path";
4
+
5
+ /**
6
+ * Never part of a project's own source
7
+ */
8
+ const SKIPPED_DIRECTORIES = new Set(["node_modules", "dist", "declarations", ".git"]);
9
+
10
+ /**
11
+ * @param {string} root the project directory
12
+ * @param {string} directory relative to `root`
13
+ * @returns {string[]} every file below `directory`, relative to `root`, sorted
14
+ */
15
+ export function listFiles(root, directory) {
16
+ let absolute = join(root, directory);
17
+
18
+ if (!existsSync(absolute)) return [];
19
+
20
+ /** @type {string[]} */
21
+ let files = [];
22
+
23
+ collect(absolute, root, files);
24
+
25
+ return files.sort();
26
+ }
27
+
28
+ /**
29
+ * @param {string} directory
30
+ * @param {string} root
31
+ * @param {string[]} files
32
+ */
33
+ function collect(directory, root, files) {
34
+ for (let entry of readdirSync(directory, { withFileTypes: true })) {
35
+ if (SKIPPED_DIRECTORIES.has(entry.name)) continue;
36
+
37
+ let path = join(directory, entry.name);
38
+
39
+ if (entry.isDirectory()) {
40
+ collect(path, root, files);
41
+ } else if (entry.isFile()) {
42
+ files.push(relative(root, path).split(sep).join("/"));
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * @param {string} root the project directory
49
+ * @param {string[]} directories relative to `root`
50
+ * @param {string[]} extensions such as `.hbs`
51
+ * @returns {string[]}
52
+ */
53
+ export function filesWithExtension(root, directories, extensions) {
54
+ /** @type {string[]} */
55
+ let result = [];
56
+
57
+ for (let directory of directories) {
58
+ for (let file of listFiles(root, directory)) {
59
+ if (extensions.some((extension) => file.endsWith(extension))) {
60
+ result.push(file);
61
+ }
62
+ }
63
+ }
64
+
65
+ return result;
66
+ }
67
+
68
+ /**
69
+ * Tests that render with `hbs` from ember-cli-htmlbars (or its older aliases).
70
+ *
71
+ * Those templates are loose mode:
72
+ * components are looked up by name at runtime,
73
+ * and the babel plugin that compiles them is part of ember-cli.
74
+ *
75
+ * @param {string} root the project directory
76
+ * @param {string[]} directories relative to `root`
77
+ * @returns {string[]}
78
+ */
79
+ export function testsWithHbs(root, directories) {
80
+ let candidates = filesWithExtension(root, directories, [".js", ".ts"]);
81
+
82
+ return candidates.filter((file) => {
83
+ let source = readFileSync(join(root, file), "utf-8");
84
+
85
+ return /from\s+["'](ember-cli-htmlbars|htmlbars-inline-precompile|ember-cli-htmlbars-inline-precompile)["']/.test(
86
+ source,
87
+ );
88
+ });
89
+ }
90
+
91
+ /**
92
+ * The first of `names` that exists in `root`
93
+ *
94
+ * @param {string} root
95
+ * @param {string[]} names
96
+ * @returns {string | undefined}
97
+ */
98
+ export function firstExisting(root, names) {
99
+ return names.find((name) => existsSync(join(root, name)));
100
+ }
101
+
102
+ /**
103
+ * @param {import('#utils/project.js').Project} project
104
+ * @param {string[]} paths files or directories, relative to the project
105
+ */
106
+ export async function remove(project, paths) {
107
+ for (let path of paths) {
108
+ await rm(project.path(path), { recursive: true, force: true });
109
+ }
110
+ }
111
+
112
+ /**
113
+ * @param {import('#utils/project.js').Project} project
114
+ * @param {string} from relative to the project
115
+ * @param {string} to relative to the project
116
+ */
117
+ export async function move(project, from, to) {
118
+ await mkdir(dirname(project.path(to)), { recursive: true });
119
+ await rename(project.path(from), project.path(to));
120
+ }