@distilled.cloud/cloudflare-rolldown-plugin 0.3.0 → 0.4.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.
Files changed (51) hide show
  1. package/dist/factory.d.ts +19 -0
  2. package/dist/factory.d.ts.map +1 -0
  3. package/dist/factory.js +30 -0
  4. package/dist/factory.js.map +1 -0
  5. package/dist/options.d.ts +26 -0
  6. package/dist/options.d.ts.map +1 -0
  7. package/dist/options.js +1 -0
  8. package/dist/options.js.map +1 -0
  9. package/dist/plugin.d.ts +3 -6
  10. package/dist/plugin.d.ts.map +1 -1
  11. package/dist/plugin.js +10 -11
  12. package/dist/plugin.js.map +1 -1
  13. package/dist/plugins/additional-modules.d.ts +1 -2
  14. package/dist/plugins/additional-modules.d.ts.map +1 -1
  15. package/dist/plugins/additional-modules.js +67 -53
  16. package/dist/plugins/additional-modules.js.map +1 -1
  17. package/dist/plugins/cloudflare-externals.d.ts +1 -2
  18. package/dist/plugins/cloudflare-externals.d.ts.map +1 -1
  19. package/dist/plugins/cloudflare-externals.js +36 -15
  20. package/dist/plugins/cloudflare-externals.js.map +1 -1
  21. package/dist/plugins/index.d.ts +7 -0
  22. package/dist/plugins/index.d.ts.map +1 -0
  23. package/dist/plugins/index.js +7 -0
  24. package/dist/plugins/index.js.map +1 -0
  25. package/dist/plugins/nodejs-compat.d.ts +9 -3
  26. package/dist/plugins/nodejs-compat.d.ts.map +1 -1
  27. package/dist/plugins/nodejs-compat.js +178 -115
  28. package/dist/plugins/nodejs-compat.js.map +1 -1
  29. package/dist/plugins/options.d.ts +1 -5
  30. package/dist/plugins/options.d.ts.map +1 -1
  31. package/dist/plugins/options.js +128 -23
  32. package/dist/plugins/options.js.map +1 -1
  33. package/dist/plugins/virtual-modules.d.ts +3 -0
  34. package/dist/plugins/virtual-modules.d.ts.map +1 -0
  35. package/dist/plugins/virtual-modules.js +125 -0
  36. package/dist/plugins/virtual-modules.js.map +1 -0
  37. package/dist/plugins/wasm-init.d.ts +1 -2
  38. package/dist/plugins/wasm-init.d.ts.map +1 -1
  39. package/dist/plugins/wasm-init.js +15 -13
  40. package/dist/plugins/wasm-init.js.map +1 -1
  41. package/package.json +16 -1
  42. package/src/factory.ts +60 -0
  43. package/src/options.ts +25 -0
  44. package/src/plugin.ts +22 -18
  45. package/src/plugins/additional-modules.ts +89 -55
  46. package/src/plugins/cloudflare-externals.ts +36 -16
  47. package/src/plugins/index.ts +6 -0
  48. package/src/plugins/nodejs-compat.ts +197 -140
  49. package/src/plugins/options.ts +145 -26
  50. package/src/plugins/virtual-modules.ts +135 -0
  51. package/src/plugins/wasm-init.ts +15 -14
@@ -2,174 +2,231 @@ import { getCloudflarePreset, nonPrefixedNodeModules } from "@cloudflare/unenv-p
2
2
  import assert from "node:assert";
3
3
  import { createRequire } from "node:module";
4
4
  import path from "node:path";
5
- import type { Plugin } from "rolldown";
6
5
  import { esmExternalRequirePlugin } from "rolldown/plugins";
7
6
  import { defineEnv } from "unenv";
8
- import type { CloudflarePluginOptions } from "../plugin.js";
7
+ import { createPlugin } from "../factory.js";
9
8
  import { hasNodejsAls, hasNodejsCompat } from "../utils.js";
10
9
 
10
+ const ASYNC_HOOKS_REGEXP = /^(node:)?async_hooks$/;
11
11
  const NODE_BUILTIN_MODULES_REGEXP = new RegExp(`^(${nonPrefixedNodeModules.join("|")}|node:.+)$`);
12
- const VIRTUAL_MODULE_ID_REGEXP = /^virtual:nodejs-global-inject\/.+$/;
13
12
 
14
- export function makeNodejsCompatPlugin(options: CloudflarePluginOptions): Plugin | Array<Plugin> {
15
- if (hasNodejsCompat(options.compatibilityFlags)) {
16
- return makeUnenvPlugin(options);
17
- }
18
- if (hasNodejsAls(options.compatibilityFlags)) {
19
- return makeNodeJsAlsPlugin();
20
- }
21
- return makeNodeJsImportWarningPlugin();
22
- }
23
-
24
- function makeUnenvPlugin(options: CloudflarePluginOptions): Array<Plugin> {
25
- const { alias, inject, external, polyfill } = defineEnv({
26
- presets: [
27
- getCloudflarePreset({
28
- compatibilityDate: options.compatibilityDate,
29
- compatibilityFlags: options.compatibilityFlags,
30
- }),
31
- ],
32
- }).env;
33
-
34
- const injectVirtualModules = makeInjectVirtualModules(inject);
35
- const require = createRequire(import.meta.url);
36
-
37
- return [
38
- esmExternalRequirePlugin({
39
- external: [...external],
40
- skipDuplicateCheck: true,
41
- }),
42
- {
43
- name: "rolldown-plugin-cloudflare:nodejs-compat:injects",
13
+ export const nodejsAlsPlugin = createPlugin("nodejs-als", (options) => {
14
+ if (!hasNodejsAls(options.compatibilityFlags)) return;
15
+ return {
16
+ rolldown: {
44
17
  resolveId: {
45
- filter: { id: VIRTUAL_MODULE_ID_REGEXP },
18
+ filter: { id: ASYNC_HOOKS_REGEXP },
46
19
  handler(id) {
47
- if (injectVirtualModules.has(id)) {
48
- return { id };
49
- }
50
- },
51
- },
52
- load: {
53
- filter: { id: VIRTUAL_MODULE_ID_REGEXP },
54
- handler(id) {
55
- return injectVirtualModules.get(id);
20
+ return { id, external: true };
56
21
  },
57
22
  },
58
23
  },
59
- {
60
- name: "rolldown-plugin-cloudflare:nodejs-compat:unenv-preset-imports",
61
- resolveId: {
62
- filter: { id: /^@cloudflare\/unenv-preset\// },
63
- async handler(id, importer, options) {
64
- const resolved = await this.resolve(id, importer, options);
65
- if (resolved) return resolved;
66
- return { id: require.resolve(id) };
67
- },
24
+ vite: {
25
+ configEnvironment(name) {
26
+ if (name === "client") return;
27
+ return {
28
+ resolve: {
29
+ builtins: ["async_hooks", "node:async_hooks"],
30
+ },
31
+ optimizeDeps: {
32
+ exclude: ["async_hooks", "node:async_hooks"],
33
+ },
34
+ };
68
35
  },
69
36
  },
70
- {
71
- name: "rolldown-plugin-cloudflare:nodejs-compat",
72
- resolveId: {
73
- filter: { id: NODE_BUILTIN_MODULES_REGEXP },
74
- handler(source, importer, options) {
75
- const aliased = alias[source];
76
- if (!aliased) {
77
- return;
78
- }
79
- if (external.includes(aliased)) {
80
- return {
81
- id: aliased,
82
- external: true,
83
- };
84
- }
85
- return this.resolve(aliased, importer, options);
37
+ };
38
+ });
39
+
40
+ export interface UnenvApi {
41
+ polyfill: ReadonlyArray<string>;
42
+ inject: { [injectedName: string]: string };
43
+ }
44
+
45
+ export const nodejsUnenvPlugin = createPlugin<"nodejs-unenv", UnenvApi>(
46
+ "nodejs-unenv",
47
+ (options) => {
48
+ if (!hasNodejsCompat(options.compatibilityFlags)) return;
49
+ const { alias, inject, external, polyfill } = defineEnv({
50
+ presets: [
51
+ getCloudflarePreset({
52
+ compatibilityDate: options.compatibilityDate,
53
+ compatibilityFlags: options.compatibilityFlags,
54
+ }),
55
+ ],
56
+ }).env;
57
+ const entries = new Set(Object.values(alias));
58
+ for (const globalInject of Object.values(inject)) {
59
+ if (typeof globalInject === "string") {
60
+ entries.add(globalInject);
61
+ } else {
62
+ entries.add(globalInject[0]);
63
+ }
64
+ }
65
+ polyfill.forEach((module) => entries.add(module));
66
+ external.forEach((module) => entries.delete(module));
67
+ const require = createRequire(import.meta.url);
68
+ const resolve = (id: string) => {
69
+ if (alias[id] && !external.includes(alias[id])) {
70
+ return require.resolve(alias[id]);
71
+ }
72
+ if (entries.has(id)) {
73
+ return require.resolve(id);
74
+ }
75
+ };
76
+ const RESOLVE_ID_FILTER = {
77
+ id: [NODE_BUILTIN_MODULES_REGEXP, /^unenv\//, /^@cloudflare\/unenv-preset\//],
78
+ };
79
+ return {
80
+ shared: {
81
+ api: {
82
+ polyfill,
83
+ inject: Object.fromEntries(
84
+ Object.entries(inject).map(([injectedName, moduleSpecifier]) => {
85
+ assert(
86
+ typeof moduleSpecifier === "string",
87
+ `expected moduleSpecifier to be a string`,
88
+ );
89
+ return [injectedName, moduleSpecifier];
90
+ }),
91
+ ),
86
92
  },
87
93
  },
88
- transform(code, id) {
89
- const info = this.getModuleInfo(id);
90
- if (!info?.isEntry) {
91
- return;
92
- }
93
- return [
94
- ...polyfill.map((module) => `import "${module}";`),
95
- ...Array.from(injectVirtualModules.keys()).map((module) => `import "${module}";`),
96
- code,
97
- ].join("\n");
94
+ rolldown: {
95
+ async options(options) {
96
+ options.plugins = [
97
+ esmExternalRequirePlugin({
98
+ external: [...external],
99
+ skipDuplicateCheck: true,
100
+ }),
101
+ options.plugins,
102
+ ];
103
+ return options;
104
+ },
105
+ resolveId: {
106
+ filter: RESOLVE_ID_FILTER,
107
+ handler(source, importer, options) {
108
+ const resolved = resolve(source);
109
+ if (!resolved) return;
110
+ return this.resolve(resolved, importer, options);
111
+ },
112
+ },
98
113
  },
99
- } satisfies Plugin,
100
- ];
101
- }
102
-
103
- function makeNodeJsAlsPlugin(): Plugin {
104
- return {
105
- name: "rolldown-plugin-cloudflare:nodejs-als",
106
- resolveId: {
107
- filter: { id: /^(node:)?async_hooks$/ },
108
- handler(id) {
109
- return { id, external: true };
114
+ vite: {
115
+ enforce: "pre",
116
+ async configEnvironment(name) {
117
+ if (name === "client") return;
118
+ return {
119
+ resolve: {
120
+ builtins: [...external],
121
+ },
122
+ ...(this.meta.rolldownVersion
123
+ ? {
124
+ build: {
125
+ rolldownOptions: {
126
+ plugins: [
127
+ esmExternalRequirePlugin({
128
+ external: [...external],
129
+ skipDuplicateCheck: true,
130
+ }),
131
+ ],
132
+ },
133
+ },
134
+ }
135
+ : {}),
136
+ };
137
+ },
138
+ async configureServer(server) {
139
+ await Promise.all(
140
+ Object.values(server.environments).flatMap(async (environment) => {
141
+ const depsOptimizer = environment.depsOptimizer;
142
+ if (!depsOptimizer) return;
143
+ await depsOptimizer.init();
144
+ return Array.from(entries).map((entry) => {
145
+ const resolved = resolve(entry);
146
+ if (!resolved) return;
147
+ const registration = depsOptimizer.registerMissingImport(entry, resolved);
148
+ return registration?.processing;
149
+ });
150
+ }),
151
+ );
152
+ },
153
+ resolveId: {
154
+ filter: RESOLVE_ID_FILTER,
155
+ handler(source, importer, options) {
156
+ const resolved = resolve(source);
157
+ if (!resolved) return;
158
+ if (this.environment.mode === "dev" && this.environment.depsOptimizer) {
159
+ // We are in dev mode (rather than build).
160
+ // So let's pre-bundle this polyfill entry-point using the dependency optimizer.
161
+ const { id } = this.environment.depsOptimizer.registerMissingImport(source, resolved);
162
+ // We use the unresolved path to the polyfill and let the dependency optimizer's
163
+ // resolver find the resolved path to the bundled version.
164
+ return this.resolve(id, importer, options);
165
+ }
166
+ return this.resolve(resolved, importer, options);
167
+ },
168
+ },
110
169
  },
111
- },
112
- };
113
- }
170
+ };
171
+ },
172
+ );
114
173
 
115
- function makeNodeJsImportWarningPlugin(): Plugin {
174
+ export const nodejsImportWarningPlugin = createPlugin("nodejs-import-warning", (options) => {
175
+ if (hasNodejsCompat(options.compatibilityFlags)) return;
116
176
  const imports = new Map<string, Set<string>>();
117
177
  let root = process.cwd();
118
178
  return {
119
- name: "rolldown-plugin-cloudflare:nodejs-import-warnings",
120
- options(options) {
121
- if (options.cwd) {
122
- root = options.cwd;
123
- }
124
- },
125
- resolveId: {
126
- filter: { id: NODE_BUILTIN_MODULES_REGEXP },
127
- async handler(id, importer) {
128
- if (importer) {
129
- if (!imports.has(id)) {
130
- imports.set(id, new Set());
131
- }
132
- imports.get(id)?.add(importer);
179
+ rolldown: {
180
+ options(options) {
181
+ if (options.cwd) {
182
+ root = options.cwd;
133
183
  }
134
- return { id, external: true };
135
184
  },
136
185
  },
137
- buildStart() {
138
- imports.clear();
186
+ vite: {
187
+ enforce: "pre",
188
+ configResolved(config) {
189
+ root = config.root;
190
+ },
139
191
  },
140
- buildEnd() {
141
- if (imports.size > 0) {
142
- let message =
143
- `Unexpected Node.js imports. ` +
144
- `Do you need to enable the "nodejs_compat" compatibility flag? ` +
145
- "Refer to https://developers.cloudflare.com/workers/runtime-apis/nodejs/ for more details.\n";
192
+ shared: {
193
+ buildStart() {
194
+ imports.clear();
195
+ },
196
+ resolveId: {
197
+ filter: { id: NODE_BUILTIN_MODULES_REGEXP },
198
+ async handler(id, importer) {
199
+ if (hasNodejsAls(options.compatibilityFlags) && ASYNC_HOOKS_REGEXP.test(id)) return;
200
+ if (importer) {
201
+ if (!imports.has(id)) {
202
+ imports.set(id, new Set());
203
+ }
204
+ imports.get(id)?.add(importer);
205
+ }
206
+ return { id, external: true };
207
+ },
208
+ },
209
+ buildEnd() {
210
+ const filteredImports: Array<{ id: string; importer: string }> = [];
146
211
  for (const [id, importers] of imports.entries()) {
147
212
  for (const importer of importers) {
148
- message += ` - "${id}" imported from "${path.relative(root, importer)}"\n`;
213
+ if (this.getModuleInfo(importer)) {
214
+ filteredImports.push({ id, importer });
215
+ }
149
216
  }
150
217
  }
151
- this.warn(message);
152
- }
218
+ if (filteredImports.length > 0) {
219
+ const message = [
220
+ "Unexpected Node.js imports. ",
221
+ 'Do you need to enable the "nodejs_compat" compatibility flag? ',
222
+ "Refer to https://developers.cloudflare.com/workers/runtime-apis/nodejs/ for more details.\n",
223
+ ...filteredImports.map(
224
+ ({ id, importer }) => ` - "${id}" imported from "${path.relative(root, importer)}"\n`,
225
+ ),
226
+ ].join("");
227
+ this.warn(message);
228
+ }
229
+ },
153
230
  },
154
231
  };
155
- }
156
-
157
- function makeInjectVirtualModules(
158
- inject: Readonly<Record<string, string | ReadonlyArray<string>>>,
159
- ) {
160
- const virtualModules = new Map<string, string>();
161
- for (const [injectedName, moduleSpecifier] of Object.entries(inject)) {
162
- // The type from unenv is string | string[], but the Cloudflare preset only uses string.
163
- // This indicates a default export that we set on globalThis.
164
- assert(typeof moduleSpecifier === "string", `expected moduleSpecifier to be a string`);
165
- virtualModules.set(
166
- injectVirtualModuleId(injectedName),
167
- `import virtualModule from "${moduleSpecifier}"; globalThis.${injectedName} = virtualModule;`,
168
- );
169
- }
170
- return virtualModules;
171
- }
172
-
173
- function injectVirtualModuleId(module: string) {
174
- return `virtual:nodejs-global-inject/${module}` as const;
175
- }
232
+ });
@@ -1,8 +1,13 @@
1
- import type { Plugin } from "rolldown";
2
- import type { CloudflarePluginOptions } from "../plugin.js";
1
+ import path from "node:path";
2
+ import type * as vite from "vite";
3
+ import { createPlugin } from "../factory.js";
4
+ import type { CloudflarePluginOptions } from "../options.js";
3
5
  import { hasNodejsCompat } from "../utils.js";
6
+ import { WORKER_ENTRY_PREFIX } from "./virtual-modules.js";
4
7
 
5
- const DEFAULT_RESOLVE_CONDITION_NAMES = ["workerd", "worker", "module", "browser", "production"];
8
+ const DEFAULT_CONDITIONS = ["workerd", "worker", "module", "browser"];
9
+
10
+ const DEFAULT_RESOLVE_MAIN_FIELDS = ["browser", "module", "jsnext:main", "jsnext"];
6
11
 
7
12
  const DEFAULT_RESOLVE_EXTENSIONS = [
8
13
  ".mjs",
@@ -17,35 +22,149 @@ const DEFAULT_RESOLVE_EXTENSIONS = [
17
22
  ".ctx",
18
23
  ];
19
24
 
20
- export function makeOptionsPlugin(pluginOptions: CloudflarePluginOptions) {
21
- return {
22
- name: "rolldown-plugin-cloudflare:options",
25
+ const TARGET = "es2024";
26
+
27
+ export const optionsPlugin = createPlugin("options", (pluginOptions) => ({
28
+ rolldown: {
23
29
  options(options) {
30
+ options.input = wrapEntryInput(options.input ?? {});
31
+ options.preserveEntrySignatures ??= "strict";
24
32
  options.platform ??= "neutral";
25
33
  options.resolve ??= {};
26
- options.resolve.conditionNames ??= DEFAULT_RESOLVE_CONDITION_NAMES;
34
+ options.resolve.conditionNames ??= [...DEFAULT_CONDITIONS, "production"];
35
+ options.resolve.mainFields ??= DEFAULT_RESOLVE_MAIN_FIELDS;
27
36
  options.resolve.extensions ??= DEFAULT_RESOLVE_EXTENSIONS;
28
37
  options.transform ??= {};
29
- options.transform.target ??= "es2024";
38
+ options.transform.target ??= TARGET;
30
39
  options.transform.define ??= {};
31
- Object.assign(options.transform.define, {
32
- "process.env.NODE_ENV": '"production"',
33
- "global.process.env.NODE_ENV": '"production"',
34
- "globalThis.process.env.NODE_ENV": '"production"',
35
- ...(hasNodejsCompat(pluginOptions.compatibilityFlags)
36
- ? {}
37
- : {
38
- "process.env": "{}",
39
- "global.process.env": "{}",
40
- "globalThis.process.env": "{}",
41
- }),
42
- ...(pluginOptions.compatibilityDate && pluginOptions.compatibilityDate >= "2022-03-21"
43
- ? {
44
- "navigator.userAgent": '"Cloudflare-Workers"',
45
- }
46
- : {}),
47
- });
40
+ Object.assign(options.transform.define, getDefine(pluginOptions, "production"));
48
41
  return options;
49
42
  },
50
- } satisfies Plugin;
43
+ },
44
+ vite: {
45
+ config(userConfig) {
46
+ const isRolldown = "rolldownVersion" in this.meta;
47
+ const rollupOptions: vite.Rollup.RollupOptions = {
48
+ input: wrapEntryInput(userConfig.environments?.ssr?.build?.rollupOptions?.input ?? {}),
49
+ preserveEntrySignatures: "strict",
50
+ };
51
+ const define = getDefine(
52
+ pluginOptions,
53
+ process.env.NODE_ENV || userConfig.mode || "production",
54
+ );
55
+ return {
56
+ ssr: {
57
+ noExternal: true,
58
+ resolve: {
59
+ conditions: [...DEFAULT_CONDITIONS, "development|production"],
60
+ },
61
+ },
62
+ environments: {
63
+ client: {
64
+ build: {
65
+ outDir: getOutputDirectory(userConfig, "client"),
66
+ },
67
+ },
68
+ ssr: {
69
+ resolve: {
70
+ noExternal: true,
71
+ conditions: [...DEFAULT_CONDITIONS, "development|production"],
72
+ },
73
+ build: {
74
+ ssr: true,
75
+ target: TARGET,
76
+ emitAssets: true,
77
+ copyPublicDir: false,
78
+ outDir: getOutputDirectory(userConfig, "ssr"),
79
+ ...(isRolldown
80
+ ? {
81
+ rolldownOptions: {
82
+ ...rollupOptions,
83
+ platform: "neutral",
84
+ resolve: {
85
+ mainFields: DEFAULT_RESOLVE_MAIN_FIELDS,
86
+ extensions: DEFAULT_RESOLVE_EXTENSIONS,
87
+ },
88
+ },
89
+ }
90
+ : { rollupOptions }),
91
+ },
92
+ optimizeDeps: {
93
+ noDiscovery: false,
94
+ ignoreOutdatedRequests: true,
95
+ ...(isRolldown
96
+ ? {
97
+ rolldownOptions: {
98
+ platform: "neutral",
99
+ resolve: {
100
+ conditionNames: [...DEFAULT_CONDITIONS, "development|production"],
101
+ mainFields: DEFAULT_RESOLVE_MAIN_FIELDS,
102
+ extensions: DEFAULT_RESOLVE_EXTENSIONS,
103
+ },
104
+ transform: {
105
+ target: TARGET,
106
+ define,
107
+ },
108
+ },
109
+ }
110
+ : {
111
+ esbuildOptions: {
112
+ platform: "neutral",
113
+ conditions: [...DEFAULT_CONDITIONS, "development|production"],
114
+ resolveExtensions: DEFAULT_RESOLVE_EXTENSIONS,
115
+ mainFields: DEFAULT_RESOLVE_MAIN_FIELDS,
116
+ target: TARGET,
117
+ define,
118
+ },
119
+ }),
120
+ },
121
+ keepProcessEnv: true,
122
+ },
123
+ },
124
+ };
125
+ },
126
+ },
127
+ }));
128
+
129
+ function wrapEntryInput(input: string | Array<string> | Record<string, string>) {
130
+ const virtualEntryId = (id: string) => `${WORKER_ENTRY_PREFIX}${id}` as const;
131
+
132
+ if (typeof input === "string") {
133
+ return virtualEntryId(input);
134
+ }
135
+ if (Array.isArray(input)) {
136
+ return input.map(virtualEntryId);
137
+ }
138
+ return Object.fromEntries(
139
+ Object.entries(input).map(([key, value]) => [virtualEntryId(key), value]),
140
+ );
141
+ }
142
+
143
+ function getDefine(options: CloudflarePluginOptions, nodeEnv: string): Record<string, string> {
144
+ return {
145
+ "process.env.NODE_ENV": JSON.stringify(nodeEnv),
146
+ "global.process.env.NODE_ENV": JSON.stringify(nodeEnv),
147
+ "globalThis.process.env.NODE_ENV": JSON.stringify(nodeEnv),
148
+ ...(hasNodejsCompat(options.compatibilityFlags)
149
+ ? {}
150
+ : {
151
+ "process.env": "{}",
152
+ "global.process.env": "{}",
153
+ "globalThis.process.env": "{}",
154
+ }),
155
+ ...(options.compatibilityDate && options.compatibilityDate >= "2022-03-21"
156
+ ? {
157
+ "navigator.userAgent": '"Cloudflare-Workers"',
158
+ }
159
+ : {}),
160
+ };
161
+ }
162
+
163
+ function getOutputDirectory(userConfig: vite.UserConfig, environmentName: string) {
164
+ const rootOutputDirectory = userConfig.build?.outDir ?? "dist";
165
+
166
+ return (
167
+ userConfig.environments?.[environmentName]?.build?.outDir ??
168
+ path.join(rootOutputDirectory, environmentName)
169
+ );
51
170
  }