@skuba-lib/api 2.3.0-rolldown-cdk-bundle-plugin-20260804050549 → 2.3.0-rolldown-cdk-bundle-plugin-20260804234000

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.
@@ -40,7 +40,8 @@ interface LambdaAssetOptions {
40
40
  * This is the directory holding the `package.json` that depends on them; in a
41
41
  * workspace, that is the individual package rather than the workspace root.
42
42
  *
43
- * Defaults to `process.cwd()`.
43
+ * Relative paths resolve against rolldown's `cwd`, which defaults to
44
+ * `process.cwd()`.
44
45
  */
45
46
  projectRoot?: string;
46
47
  /**
@@ -49,7 +50,8 @@ interface LambdaAssetOptions {
49
50
  * pnpm keeps this at the workspace root, so its directory is also where the
50
51
  * workspace config and patches are staged from.
51
52
  *
52
- * Defaults to the nearest lock file, walking up from `process.cwd()`.
53
+ * Defaults to the nearest lock file, walking up from rolldown's `cwd`, which
54
+ * defaults to `process.cwd()`. A relative path resolves against that `cwd`.
53
55
  */
54
56
  depsLockFilePath?: string;
55
57
  }
@@ -59,12 +61,13 @@ interface LambdaAssetOptions {
59
61
  * The plugin supports ESM output and pnpm only. It makes no assumptions about
60
62
  * how you bundle; it only augments what rolldown has already written:
61
63
  *
62
- * 1. Writes a `package.json` of `type: 'module'`.
63
- * 2. Installs `nodeModules` into the output directory with pnpm, staging the
64
+ * 1. Emits any extra `assets` into the build output alongside the bundle, via
65
+ * rolldown's `emitFile`.
66
+ * 2. Writes a `package.json` of `type: 'module'`.
67
+ * 3. Installs `nodeModules` into the output directory with pnpm, staging the
64
68
  * workspace config, `.npmrc`, lock file and patches to do so.
65
- * 3. Strips those install-only files back out, leaving the bundle, the
69
+ * 4. Strips those install-only files back out, leaving the bundle, the
66
70
  * generated `package.json` and `node_modules`.
67
- * 4. Copies any extra `assets` into the output directory alongside the bundle.
68
71
  *
69
72
  * The result can be handed to CDK as `aws_lambda.Code.fromAsset(outputDir)`.
70
73
  *
@@ -40,7 +40,8 @@ interface LambdaAssetOptions {
40
40
  * This is the directory holding the `package.json` that depends on them; in a
41
41
  * workspace, that is the individual package rather than the workspace root.
42
42
  *
43
- * Defaults to `process.cwd()`.
43
+ * Relative paths resolve against rolldown's `cwd`, which defaults to
44
+ * `process.cwd()`.
44
45
  */
45
46
  projectRoot?: string;
46
47
  /**
@@ -49,7 +50,8 @@ interface LambdaAssetOptions {
49
50
  * pnpm keeps this at the workspace root, so its directory is also where the
50
51
  * workspace config and patches are staged from.
51
52
  *
52
- * Defaults to the nearest lock file, walking up from `process.cwd()`.
53
+ * Defaults to the nearest lock file, walking up from rolldown's `cwd`, which
54
+ * defaults to `process.cwd()`. A relative path resolves against that `cwd`.
53
55
  */
54
56
  depsLockFilePath?: string;
55
57
  }
@@ -59,12 +61,13 @@ interface LambdaAssetOptions {
59
61
  * The plugin supports ESM output and pnpm only. It makes no assumptions about
60
62
  * how you bundle; it only augments what rolldown has already written:
61
63
  *
62
- * 1. Writes a `package.json` of `type: 'module'`.
63
- * 2. Installs `nodeModules` into the output directory with pnpm, staging the
64
+ * 1. Emits any extra `assets` into the build output alongside the bundle, via
65
+ * rolldown's `emitFile`.
66
+ * 2. Writes a `package.json` of `type: 'module'`.
67
+ * 3. Installs `nodeModules` into the output directory with pnpm, staging the
64
68
  * workspace config, `.npmrc`, lock file and patches to do so.
65
- * 3. Strips those install-only files back out, leaving the bundle, the
69
+ * 4. Strips those install-only files back out, leaving the bundle, the
66
70
  * generated `package.json` and `node_modules`.
67
- * 4. Copies any extra `assets` into the output directory alongside the bundle.
68
71
  *
69
72
  * The result can be handed to CDK as `aws_lambda.Code.fromAsset(outputDir)`.
70
73
  *
package/lib/index.cjs CHANGED
@@ -4,7 +4,7 @@ const require_cdk_index = require("./cdk/index.cjs");
4
4
  const require_git = require("./git-BGcWTYZJ.cjs");
5
5
  const require_github = require("./github-B2fQutQz.cjs");
6
6
  const require_net = require("./net-zrteIHld.cjs");
7
- const require_rolldown = require("./rolldown-CbJyS-Uy.cjs");
7
+ const require_rolldown = require("./rolldown-CN3DH_Tw.cjs");
8
8
  Object.defineProperty(exports, "Buildkite", {
9
9
  enumerable: true,
10
10
  get: function() {
package/lib/index.d.cts CHANGED
@@ -3,5 +3,5 @@ import { t as index_d_exports$1 } from "./cdk/index.cjs";
3
3
  import { t as index_d_exports$2 } from "./index-CLEYcnJY.cjs";
4
4
  import { t as index_d_exports$3 } from "./index-CPm5rAMy.cjs";
5
5
  import { t as index_d_exports$4 } from "./index-C9lVo3vU.cjs";
6
- import { t as index_d_exports$5 } from "./index-J1Qqgryi.cjs";
6
+ import { t as index_d_exports$5 } from "./index-BEmN8nSJ.cjs";
7
7
  export { index_d_exports as Buildkite, index_d_exports$1 as Cdk, index_d_exports$2 as Git, index_d_exports$3 as GitHub, index_d_exports$4 as Net, index_d_exports$5 as Rolldown };
package/lib/index.d.mts CHANGED
@@ -3,5 +3,5 @@ import { t as index_d_exports$1 } from "./cdk/index.mjs";
3
3
  import { t as index_d_exports$2 } from "./index-CLEYcnJY.mjs";
4
4
  import { t as index_d_exports$3 } from "./index-CwdiFn_m.mjs";
5
5
  import { t as index_d_exports$4 } from "./index-C9lVo3vU.mjs";
6
- import { t as index_d_exports$5 } from "./index-J1Qqgryi.mjs";
6
+ import { t as index_d_exports$5 } from "./index-BEmN8nSJ.mjs";
7
7
  export { index_d_exports as Buildkite, index_d_exports$1 as Cdk, index_d_exports$2 as Git, index_d_exports$3 as GitHub, index_d_exports$4 as Net, index_d_exports$5 as Rolldown };
package/lib/index.mjs CHANGED
@@ -3,5 +3,5 @@ import { t as cdk_exports } from "./cdk/index.mjs";
3
3
  import { t as git_exports } from "./git-DKqd7R1V.mjs";
4
4
  import { t as github_exports } from "./github-EDhl9ON9.mjs";
5
5
  import { t as net_exports } from "./net-97EvMPiP.mjs";
6
- import { t as rolldown_exports } from "./rolldown-08iefo1V.mjs";
6
+ import { t as rolldown_exports } from "./rolldown--bMMtGC0.mjs";
7
7
  export { buildkite_exports as Buildkite, cdk_exports as Cdk, git_exports as Git, github_exports as GitHub, net_exports as Net, rolldown_exports as Rolldown };
@@ -1,3 +1,3 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_rolldown = require("../rolldown-CbJyS-Uy.cjs");
2
+ const require_rolldown = require("../rolldown-CN3DH_Tw.cjs");
3
3
  exports.lambdaAsset = require_rolldown.lambdaAsset;
@@ -1,2 +1,2 @@
1
- import { i as lambdaAsset, n as LambdaAssetFile, r as LambdaAssetOptions } from "../index-J1Qqgryi.cjs";
1
+ import { i as lambdaAsset, n as LambdaAssetFile, r as LambdaAssetOptions } from "../index-BEmN8nSJ.cjs";
2
2
  export { type LambdaAssetFile, type LambdaAssetOptions, lambdaAsset };
@@ -1,2 +1,2 @@
1
- import { i as lambdaAsset, n as LambdaAssetFile, r as LambdaAssetOptions } from "../index-J1Qqgryi.mjs";
1
+ import { i as lambdaAsset, n as LambdaAssetFile, r as LambdaAssetOptions } from "../index-BEmN8nSJ.mjs";
2
2
  export { type LambdaAssetFile, type LambdaAssetOptions, lambdaAsset };
@@ -1,2 +1,2 @@
1
- import { n as lambdaAsset } from "../rolldown-08iefo1V.mjs";
1
+ import { n as lambdaAsset } from "../rolldown--bMMtGC0.mjs";
2
2
  export { lambdaAsset };
@@ -7,7 +7,7 @@ import fs from "fs-extra";
7
7
  import * as path from "node:path";
8
8
  import { findUp } from "find-up";
9
9
  import { pathToFileURL } from "node:url";
10
- import { parse, stringify } from "yaml";
10
+ import { isMap, parseDocument } from "yaml";
11
11
  //#region src/rolldown/pnpm.ts
12
12
  const PNPM_LOCK = "pnpm-lock.yaml";
13
13
  const PNPM_WORKSPACE_YAML = "pnpm-workspace.yaml";
@@ -31,24 +31,38 @@ const PNPM_INSTALL_COMMAND = [
31
31
  "--no-frozen-lockfile"
32
32
  ];
33
33
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
34
- const parseJsonFile = async (filePath) => {
34
+ const readJsonFile = async (filePath) => {
35
35
  try {
36
- return JSON.parse(await fs.promises.readFile(filePath, "utf-8"));
36
+ return await fs.readJson(filePath);
37
37
  } catch (err) {
38
38
  throw new Error(`Failed to parse ${filePath} as JSON: ${err.message}`);
39
39
  }
40
40
  };
41
41
  /**
42
- * Reads the `packageManager` pin from the workspace root manifest.
42
+ * Reads a `name@version` package manager pin from a manifest, preferring the
43
+ * `packageManager` field and falling back to `devEngines.packageManager`.
44
+ *
45
+ * `devEngines.packageManager` is either an object `{ name, version }` or an
46
+ * array of them; a pin is only returned when both `name` and `version` are
47
+ * present, since corepack needs the version to activate.
48
+ */
49
+ const readPin = (parsed) => {
50
+ if (typeof parsed.packageManager === "string") return parsed.packageManager;
51
+ const raw = (isRecord(parsed.devEngines) ? parsed.devEngines : void 0)?.packageManager;
52
+ const packageManager = Array.isArray(raw) ? raw[0] : raw;
53
+ if (isRecord(packageManager) && typeof packageManager.name === "string" && typeof packageManager.version === "string") return `${packageManager.name}@${packageManager.version}`;
54
+ };
55
+ /**
56
+ * Reads the package manager pin from the workspace root manifest.
43
57
  *
44
58
  * Forwarding it to the output directory keeps the staged install on the same
45
59
  * pnpm version as the project.
46
60
  */
47
- const readPackageManagerField = async (workspaceRoot) => {
61
+ const readPackageManagerPin = async (workspaceRoot) => {
48
62
  const pkgPath = path.join(workspaceRoot, "package.json");
49
63
  if (!await pathExists(pkgPath)) return;
50
- const parsed = await parseJsonFile(pkgPath);
51
- return isRecord(parsed) && typeof parsed.packageManager === "string" ? parsed.packageManager : void 0;
64
+ const parsed = await readJsonFile(pkgPath);
65
+ return isRecord(parsed) ? readPin(parsed) : void 0;
52
66
  };
53
67
  /**
54
68
  * The output directory installs a subset of the workspace's dependencies, so
@@ -56,15 +70,12 @@ const readPackageManagerField = async (workspaceRoot) => {
56
70
  * install on an unused patch unless this is set.
57
71
  */
58
72
  const allowUnusedPatches = (content) => {
59
- let doc;
60
- try {
61
- doc = parse(content);
62
- } catch (err) {
63
- throw new Error(`Failed to parse ${PNPM_WORKSPACE_YAML}: ${err.message}`);
64
- }
65
- if (!isRecord(doc)) return content;
66
- doc.allowUnusedPatches = true;
67
- return stringify(doc);
73
+ const doc = parseDocument(content);
74
+ const [error] = doc.errors;
75
+ if (error) throw new Error(`Failed to parse ${PNPM_WORKSPACE_YAML}: ${error.message}`);
76
+ if (!isMap(doc.contents)) return content;
77
+ doc.set("allowUnusedPatches", true);
78
+ return doc.toString();
68
79
  };
69
80
  /**
70
81
  * Stages the pnpm config that an install in `outputDir` needs.
@@ -76,19 +87,24 @@ const allowUnusedPatches = (content) => {
76
87
  * credentials that must not ship in the asset.
77
88
  */
78
89
  const stageWorkspaceFiles = async (workspaceRoot, outputDir, stagedFiles = []) => {
79
- for (const file of PNPM_WORKSPACE_FILES) {
90
+ const present = (await Promise.all(PNPM_WORKSPACE_FILES.map(async (file) => {
80
91
  const src = path.join(workspaceRoot, file);
81
- if (!await pathExists(src)) continue;
92
+ return await pathExists(src) ? {
93
+ file,
94
+ src
95
+ } : void 0;
96
+ }))).filter((entry) => entry !== void 0);
97
+ for (const { file } of present) stagedFiles.push(path.join(outputDir, file));
98
+ await Promise.all(present.map(async ({ file, src }) => {
82
99
  const dest = path.join(outputDir, file);
83
100
  if (file === PNPM_WORKSPACE_YAML) await fs.promises.writeFile(dest, allowUnusedPatches(await fs.promises.readFile(src, "utf8")));
84
101
  else await fs.promises.copyFile(src, dest);
85
- stagedFiles.push(dest);
86
- }
102
+ }));
87
103
  const patchesSrc = path.join(workspaceRoot, PNPM_PATCHES_DIR);
88
104
  if (await pathExists(patchesSrc)) {
89
105
  const patchesDest = path.join(outputDir, PNPM_PATCHES_DIR);
90
- await fs.copy(patchesSrc, patchesDest);
91
106
  stagedFiles.push(patchesDest);
107
+ await fs.copy(patchesSrc, patchesDest);
92
108
  }
93
109
  return stagedFiles;
94
110
  };
@@ -105,7 +121,7 @@ const extractDependencies = async (resolveFrom, modules) => {
105
121
  } catch {
106
122
  modPkgPath = void 0;
107
123
  }
108
- const parsed = modPkgPath ? await parseJsonFile(modPkgPath) : void 0;
124
+ const parsed = modPkgPath ? await readJsonFile(modPkgPath) : void 0;
109
125
  const version = isRecord(parsed) && typeof parsed.version === "string" ? parsed.version.trim() : void 0;
110
126
  if (!version) throw new Error(`Cannot extract version for module '${mod}'. Check that it's referenced in your package.json or installed.`);
111
127
  result[mod] = version;
@@ -115,30 +131,46 @@ const extractDependencies = async (resolveFrom, modules) => {
115
131
  //#endregion
116
132
  //#region src/rolldown/lambdaAsset.ts
117
133
  const PLUGIN_NAME = "skuba:lambda-asset";
118
- const resolveOutputDir = ({ dir, file }) => {
134
+ const resolveOutputDir = ({ dir, file }, cwd) => {
119
135
  if (!dir) throw new Error(`${PLUGIN_NAME} requires \`output.dir\`; it prepares a deployable directory, so a single \`output.file\`${file ? ` (${file})` : ""} has nowhere to install into.`);
120
- return path.resolve(dir);
136
+ return path.resolve(cwd, dir);
121
137
  };
122
- const resolveLockFile = async (depsLockFilePath) => {
123
- if (!depsLockFilePath) return findUp(PNPM_LOCK);
124
- const resolved = path.resolve(depsLockFilePath);
138
+ const resolveLockFile = async (depsLockFilePath, cwd) => {
139
+ if (!depsLockFilePath) return findUp(PNPM_LOCK, { cwd });
140
+ const resolved = path.resolve(cwd, depsLockFilePath);
125
141
  if (!await pathExists(resolved)) throw new Error(`${PLUGIN_NAME} cannot find a ${PNPM_LOCK} at '${depsLockFilePath}'.`);
126
142
  return resolved;
127
143
  };
144
+ const toPosix = (p) => p.split(path.sep).join("/");
128
145
  /**
129
- * Copies extra `assets` into the output directory alongside the bundle.
146
+ * Emits extra `assets` into the build output alongside the bundle.
130
147
  *
131
- * `from` is resolved relative to `projectRoot`, and `to` relative to the
132
- * output directory, creating parent directories as needed. Directories are
133
- * copied recursively.
148
+ * `from` is resolved relative to `projectRoot`; `to` becomes the emitted
149
+ * `fileName`, which rolldown writes relative to the output directory, creating
150
+ * parent directories as needed. Directories are emitted recursively, one asset
151
+ * per file. `originalFileName` points rolldown at each source file so watch mode
152
+ * rebuilds when it changes.
134
153
  */
135
- const copyAssets = async (outputDir, projectRoot, assets) => {
136
- for (const asset of assets) {
154
+ const emitAssets = async (emit, projectRoot, assets) => {
155
+ await Promise.all(assets.map(async (asset) => {
137
156
  const from = path.resolve(projectRoot, asset.from);
138
- const to = path.join(outputDir, asset.to ?? path.basename(asset.from));
139
- await fs.ensureDir(path.dirname(to));
140
- await fs.copy(from, to);
141
- }
157
+ const to = asset.to ?? path.basename(asset.from);
158
+ const normalized = path.normalize(to);
159
+ if (path.isAbsolute(normalized) || normalized === "." || normalized === ".." || normalized.startsWith(`..${path.sep}`)) throw new Error(`${PLUGIN_NAME} refuses to copy asset '${asset.from}' to '${asset.to}': it escapes the output directory.`);
160
+ const files = (await fs.promises.stat(from)).isDirectory() ? (await fs.promises.readdir(from, {
161
+ recursive: true,
162
+ withFileTypes: true
163
+ })).filter((entry) => entry.isFile()).map((entry) => path.join(entry.parentPath, entry.name)) : [from];
164
+ await Promise.all(files.map(async (absFile) => {
165
+ const rel = path.relative(from, absFile);
166
+ emit({
167
+ type: "asset",
168
+ fileName: toPosix(rel ? path.join(to, rel) : to),
169
+ originalFileName: absFile,
170
+ source: await fs.promises.readFile(absFile)
171
+ });
172
+ }));
173
+ }));
142
174
  };
143
175
  const writeOutputPackageJson = (outputDir, fields = {}) => fs.promises.writeFile(path.join(outputDir, "package.json"), JSON.stringify({
144
176
  type: "module",
@@ -160,12 +192,13 @@ const stripInstallFiles = async (outputDir, stagedFiles, cause) => {
160
192
  * The plugin supports ESM output and pnpm only. It makes no assumptions about
161
193
  * how you bundle; it only augments what rolldown has already written:
162
194
  *
163
- * 1. Writes a `package.json` of `type: 'module'`.
164
- * 2. Installs `nodeModules` into the output directory with pnpm, staging the
195
+ * 1. Emits any extra `assets` into the build output alongside the bundle, via
196
+ * rolldown's `emitFile`.
197
+ * 2. Writes a `package.json` of `type: 'module'`.
198
+ * 3. Installs `nodeModules` into the output directory with pnpm, staging the
165
199
  * workspace config, `.npmrc`, lock file and patches to do so.
166
- * 3. Strips those install-only files back out, leaving the bundle, the
200
+ * 4. Strips those install-only files back out, leaving the bundle, the
167
201
  * generated `package.json` and `node_modules`.
168
- * 4. Copies any extra `assets` into the output directory alongside the bundle.
169
202
  *
170
203
  * The result can be handed to CDK as `aws_lambda.Code.fromAsset(outputDir)`.
171
204
  *
@@ -189,41 +222,50 @@ const stripInstallFiles = async (outputDir, stagedFiles, cause) => {
189
222
  */
190
223
  const lambdaAsset = ({ nodeModules = [], assets = [], projectRoot: projectRootOption, depsLockFilePath: depsLockFilePathOption } = {}) => {
191
224
  const prepared = /* @__PURE__ */ new Set();
225
+ let cwd = process.cwd();
192
226
  return {
193
227
  name: PLUGIN_NAME,
194
- buildStart() {
228
+ async buildStart(options) {
229
+ cwd = options.cwd;
195
230
  prepared.clear();
231
+ if (assets.length) {
232
+ const projectRoot = path.resolve(cwd, projectRootOption ?? ".");
233
+ await emitAssets((file) => this.emitFile(file), projectRoot, assets);
234
+ }
196
235
  },
197
236
  async writeBundle(outputOptions) {
198
- const outputDir = resolveOutputDir(outputOptions);
237
+ const outputDir = resolveOutputDir(outputOptions, cwd);
199
238
  if (prepared.has(outputDir)) return;
200
239
  prepared.add(outputDir);
201
- const projectRoot = path.resolve(projectRootOption ?? process.cwd());
202
- await copyAssets(outputDir, projectRoot, assets);
240
+ const projectRoot = path.resolve(cwd, projectRootOption ?? ".");
203
241
  if (!nodeModules.length) {
204
242
  await writeOutputPackageJson(outputDir);
205
243
  return;
206
244
  }
207
- const depsLockFilePath = await resolveLockFile(depsLockFilePathOption);
245
+ const depsLockFilePath = await resolveLockFile(depsLockFilePathOption, cwd);
208
246
  if (!depsLockFilePath) throw new Error(`${PLUGIN_NAME} cannot find a ${PNPM_LOCK}, which \`nodeModules\` requires. Specify one with \`depsLockFilePath\`.`);
209
247
  const workspaceRoot = path.dirname(depsLockFilePath);
210
248
  const projectPackageJson = path.join(projectRoot, "package.json");
211
249
  if (!await pathExists(projectPackageJson)) throw new Error(`${PLUGIN_NAME} cannot find a package.json in '${projectRoot}', which \`nodeModules\` requires. Specify the directory with \`projectRoot\`.`);
212
250
  await writeOutputPackageJson(outputDir, {
213
251
  dependencies: await extractDependencies(projectPackageJson, nodeModules),
214
- packageManager: await readPackageManagerField(workspaceRoot)
252
+ packageManager: await readPackageManagerPin(workspaceRoot)
215
253
  });
216
254
  const stagedFiles = [];
217
255
  try {
218
256
  await stageWorkspaceFiles(workspaceRoot, outputDir, stagedFiles);
219
257
  const lockDest = path.join(outputDir, PNPM_LOCK);
220
- await fs.promises.copyFile(depsLockFilePath, lockDest);
221
258
  stagedFiles.push(lockDest);
259
+ await fs.promises.copyFile(depsLockFilePath, lockDest);
222
260
  log.plain(log.bold(PLUGIN_NAME), "installing", log.bold(nodeModules.join(", ")));
223
261
  const [bin, ...args] = PNPM_INSTALL_COMMAND;
224
262
  await createExec({ cwd: outputDir })(bin, ...args);
225
263
  } catch (err) {
226
- await stripInstallFiles(outputDir, stagedFiles, err);
264
+ await stripInstallFiles(outputDir, [
265
+ ...stagedFiles,
266
+ path.join(outputDir, "package.json"),
267
+ path.join(outputDir, "node_modules")
268
+ ], err);
227
269
  throw err;
228
270
  }
229
271
  await stripInstallFiles(outputDir, stagedFiles);
@@ -33,24 +33,38 @@ const PNPM_INSTALL_COMMAND = [
33
33
  "--no-frozen-lockfile"
34
34
  ];
35
35
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
36
- const parseJsonFile = async (filePath) => {
36
+ const readJsonFile = async (filePath) => {
37
37
  try {
38
- return JSON.parse(await fs_extra.default.promises.readFile(filePath, "utf-8"));
38
+ return await fs_extra.default.readJson(filePath);
39
39
  } catch (err) {
40
40
  throw new Error(`Failed to parse ${filePath} as JSON: ${err.message}`);
41
41
  }
42
42
  };
43
43
  /**
44
- * Reads the `packageManager` pin from the workspace root manifest.
44
+ * Reads a `name@version` package manager pin from a manifest, preferring the
45
+ * `packageManager` field and falling back to `devEngines.packageManager`.
46
+ *
47
+ * `devEngines.packageManager` is either an object `{ name, version }` or an
48
+ * array of them; a pin is only returned when both `name` and `version` are
49
+ * present, since corepack needs the version to activate.
50
+ */
51
+ const readPin = (parsed) => {
52
+ if (typeof parsed.packageManager === "string") return parsed.packageManager;
53
+ const raw = (isRecord(parsed.devEngines) ? parsed.devEngines : void 0)?.packageManager;
54
+ const packageManager = Array.isArray(raw) ? raw[0] : raw;
55
+ if (isRecord(packageManager) && typeof packageManager.name === "string" && typeof packageManager.version === "string") return `${packageManager.name}@${packageManager.version}`;
56
+ };
57
+ /**
58
+ * Reads the package manager pin from the workspace root manifest.
45
59
  *
46
60
  * Forwarding it to the output directory keeps the staged install on the same
47
61
  * pnpm version as the project.
48
62
  */
49
- const readPackageManagerField = async (workspaceRoot) => {
63
+ const readPackageManagerPin = async (workspaceRoot) => {
50
64
  const pkgPath = node_path.join(workspaceRoot, "package.json");
51
65
  if (!await require_fs.pathExists(pkgPath)) return;
52
- const parsed = await parseJsonFile(pkgPath);
53
- return isRecord(parsed) && typeof parsed.packageManager === "string" ? parsed.packageManager : void 0;
66
+ const parsed = await readJsonFile(pkgPath);
67
+ return isRecord(parsed) ? readPin(parsed) : void 0;
54
68
  };
55
69
  /**
56
70
  * The output directory installs a subset of the workspace's dependencies, so
@@ -58,15 +72,12 @@ const readPackageManagerField = async (workspaceRoot) => {
58
72
  * install on an unused patch unless this is set.
59
73
  */
60
74
  const allowUnusedPatches = (content) => {
61
- let doc;
62
- try {
63
- doc = (0, yaml.parse)(content);
64
- } catch (err) {
65
- throw new Error(`Failed to parse ${PNPM_WORKSPACE_YAML}: ${err.message}`);
66
- }
67
- if (!isRecord(doc)) return content;
68
- doc.allowUnusedPatches = true;
69
- return (0, yaml.stringify)(doc);
75
+ const doc = (0, yaml.parseDocument)(content);
76
+ const [error] = doc.errors;
77
+ if (error) throw new Error(`Failed to parse ${PNPM_WORKSPACE_YAML}: ${error.message}`);
78
+ if (!(0, yaml.isMap)(doc.contents)) return content;
79
+ doc.set("allowUnusedPatches", true);
80
+ return doc.toString();
70
81
  };
71
82
  /**
72
83
  * Stages the pnpm config that an install in `outputDir` needs.
@@ -78,19 +89,24 @@ const allowUnusedPatches = (content) => {
78
89
  * credentials that must not ship in the asset.
79
90
  */
80
91
  const stageWorkspaceFiles = async (workspaceRoot, outputDir, stagedFiles = []) => {
81
- for (const file of PNPM_WORKSPACE_FILES) {
92
+ const present = (await Promise.all(PNPM_WORKSPACE_FILES.map(async (file) => {
82
93
  const src = node_path.join(workspaceRoot, file);
83
- if (!await require_fs.pathExists(src)) continue;
94
+ return await require_fs.pathExists(src) ? {
95
+ file,
96
+ src
97
+ } : void 0;
98
+ }))).filter((entry) => entry !== void 0);
99
+ for (const { file } of present) stagedFiles.push(node_path.join(outputDir, file));
100
+ await Promise.all(present.map(async ({ file, src }) => {
84
101
  const dest = node_path.join(outputDir, file);
85
102
  if (file === PNPM_WORKSPACE_YAML) await fs_extra.default.promises.writeFile(dest, allowUnusedPatches(await fs_extra.default.promises.readFile(src, "utf8")));
86
103
  else await fs_extra.default.promises.copyFile(src, dest);
87
- stagedFiles.push(dest);
88
- }
104
+ }));
89
105
  const patchesSrc = node_path.join(workspaceRoot, PNPM_PATCHES_DIR);
90
106
  if (await require_fs.pathExists(patchesSrc)) {
91
107
  const patchesDest = node_path.join(outputDir, PNPM_PATCHES_DIR);
92
- await fs_extra.default.copy(patchesSrc, patchesDest);
93
108
  stagedFiles.push(patchesDest);
109
+ await fs_extra.default.copy(patchesSrc, patchesDest);
94
110
  }
95
111
  return stagedFiles;
96
112
  };
@@ -107,7 +123,7 @@ const extractDependencies = async (resolveFrom, modules) => {
107
123
  } catch {
108
124
  modPkgPath = void 0;
109
125
  }
110
- const parsed = modPkgPath ? await parseJsonFile(modPkgPath) : void 0;
126
+ const parsed = modPkgPath ? await readJsonFile(modPkgPath) : void 0;
111
127
  const version = isRecord(parsed) && typeof parsed.version === "string" ? parsed.version.trim() : void 0;
112
128
  if (!version) throw new Error(`Cannot extract version for module '${mod}'. Check that it's referenced in your package.json or installed.`);
113
129
  result[mod] = version;
@@ -117,30 +133,46 @@ const extractDependencies = async (resolveFrom, modules) => {
117
133
  //#endregion
118
134
  //#region src/rolldown/lambdaAsset.ts
119
135
  const PLUGIN_NAME = "skuba:lambda-asset";
120
- const resolveOutputDir = ({ dir, file }) => {
136
+ const resolveOutputDir = ({ dir, file }, cwd) => {
121
137
  if (!dir) throw new Error(`${PLUGIN_NAME} requires \`output.dir\`; it prepares a deployable directory, so a single \`output.file\`${file ? ` (${file})` : ""} has nowhere to install into.`);
122
- return node_path.resolve(dir);
138
+ return node_path.resolve(cwd, dir);
123
139
  };
124
- const resolveLockFile = async (depsLockFilePath) => {
125
- if (!depsLockFilePath) return (0, find_up.findUp)(PNPM_LOCK);
126
- const resolved = node_path.resolve(depsLockFilePath);
140
+ const resolveLockFile = async (depsLockFilePath, cwd) => {
141
+ if (!depsLockFilePath) return (0, find_up.findUp)(PNPM_LOCK, { cwd });
142
+ const resolved = node_path.resolve(cwd, depsLockFilePath);
127
143
  if (!await require_fs.pathExists(resolved)) throw new Error(`${PLUGIN_NAME} cannot find a ${PNPM_LOCK} at '${depsLockFilePath}'.`);
128
144
  return resolved;
129
145
  };
146
+ const toPosix = (p) => p.split(node_path.sep).join("/");
130
147
  /**
131
- * Copies extra `assets` into the output directory alongside the bundle.
148
+ * Emits extra `assets` into the build output alongside the bundle.
132
149
  *
133
- * `from` is resolved relative to `projectRoot`, and `to` relative to the
134
- * output directory, creating parent directories as needed. Directories are
135
- * copied recursively.
150
+ * `from` is resolved relative to `projectRoot`; `to` becomes the emitted
151
+ * `fileName`, which rolldown writes relative to the output directory, creating
152
+ * parent directories as needed. Directories are emitted recursively, one asset
153
+ * per file. `originalFileName` points rolldown at each source file so watch mode
154
+ * rebuilds when it changes.
136
155
  */
137
- const copyAssets = async (outputDir, projectRoot, assets) => {
138
- for (const asset of assets) {
156
+ const emitAssets = async (emit, projectRoot, assets) => {
157
+ await Promise.all(assets.map(async (asset) => {
139
158
  const from = node_path.resolve(projectRoot, asset.from);
140
- const to = node_path.join(outputDir, asset.to ?? node_path.basename(asset.from));
141
- await fs_extra.default.ensureDir(node_path.dirname(to));
142
- await fs_extra.default.copy(from, to);
143
- }
159
+ const to = asset.to ?? node_path.basename(asset.from);
160
+ const normalized = node_path.normalize(to);
161
+ if (node_path.isAbsolute(normalized) || normalized === "." || normalized === ".." || normalized.startsWith(`..${node_path.sep}`)) throw new Error(`${PLUGIN_NAME} refuses to copy asset '${asset.from}' to '${asset.to}': it escapes the output directory.`);
162
+ const files = (await fs_extra.default.promises.stat(from)).isDirectory() ? (await fs_extra.default.promises.readdir(from, {
163
+ recursive: true,
164
+ withFileTypes: true
165
+ })).filter((entry) => entry.isFile()).map((entry) => node_path.join(entry.parentPath, entry.name)) : [from];
166
+ await Promise.all(files.map(async (absFile) => {
167
+ const rel = node_path.relative(from, absFile);
168
+ emit({
169
+ type: "asset",
170
+ fileName: toPosix(rel ? node_path.join(to, rel) : to),
171
+ originalFileName: absFile,
172
+ source: await fs_extra.default.promises.readFile(absFile)
173
+ });
174
+ }));
175
+ }));
144
176
  };
145
177
  const writeOutputPackageJson = (outputDir, fields = {}) => fs_extra.default.promises.writeFile(node_path.join(outputDir, "package.json"), JSON.stringify({
146
178
  type: "module",
@@ -162,12 +194,13 @@ const stripInstallFiles = async (outputDir, stagedFiles, cause) => {
162
194
  * The plugin supports ESM output and pnpm only. It makes no assumptions about
163
195
  * how you bundle; it only augments what rolldown has already written:
164
196
  *
165
- * 1. Writes a `package.json` of `type: 'module'`.
166
- * 2. Installs `nodeModules` into the output directory with pnpm, staging the
197
+ * 1. Emits any extra `assets` into the build output alongside the bundle, via
198
+ * rolldown's `emitFile`.
199
+ * 2. Writes a `package.json` of `type: 'module'`.
200
+ * 3. Installs `nodeModules` into the output directory with pnpm, staging the
167
201
  * workspace config, `.npmrc`, lock file and patches to do so.
168
- * 3. Strips those install-only files back out, leaving the bundle, the
202
+ * 4. Strips those install-only files back out, leaving the bundle, the
169
203
  * generated `package.json` and `node_modules`.
170
- * 4. Copies any extra `assets` into the output directory alongside the bundle.
171
204
  *
172
205
  * The result can be handed to CDK as `aws_lambda.Code.fromAsset(outputDir)`.
173
206
  *
@@ -191,41 +224,50 @@ const stripInstallFiles = async (outputDir, stagedFiles, cause) => {
191
224
  */
192
225
  const lambdaAsset = ({ nodeModules = [], assets = [], projectRoot: projectRootOption, depsLockFilePath: depsLockFilePathOption } = {}) => {
193
226
  const prepared = /* @__PURE__ */ new Set();
227
+ let cwd = process.cwd();
194
228
  return {
195
229
  name: PLUGIN_NAME,
196
- buildStart() {
230
+ async buildStart(options) {
231
+ cwd = options.cwd;
197
232
  prepared.clear();
233
+ if (assets.length) {
234
+ const projectRoot = node_path.resolve(cwd, projectRootOption ?? ".");
235
+ await emitAssets((file) => this.emitFile(file), projectRoot, assets);
236
+ }
198
237
  },
199
238
  async writeBundle(outputOptions) {
200
- const outputDir = resolveOutputDir(outputOptions);
239
+ const outputDir = resolveOutputDir(outputOptions, cwd);
201
240
  if (prepared.has(outputDir)) return;
202
241
  prepared.add(outputDir);
203
- const projectRoot = node_path.resolve(projectRootOption ?? process.cwd());
204
- await copyAssets(outputDir, projectRoot, assets);
242
+ const projectRoot = node_path.resolve(cwd, projectRootOption ?? ".");
205
243
  if (!nodeModules.length) {
206
244
  await writeOutputPackageJson(outputDir);
207
245
  return;
208
246
  }
209
- const depsLockFilePath = await resolveLockFile(depsLockFilePathOption);
247
+ const depsLockFilePath = await resolveLockFile(depsLockFilePathOption, cwd);
210
248
  if (!depsLockFilePath) throw new Error(`${PLUGIN_NAME} cannot find a ${PNPM_LOCK}, which \`nodeModules\` requires. Specify one with \`depsLockFilePath\`.`);
211
249
  const workspaceRoot = node_path.dirname(depsLockFilePath);
212
250
  const projectPackageJson = node_path.join(projectRoot, "package.json");
213
251
  if (!await require_fs.pathExists(projectPackageJson)) throw new Error(`${PLUGIN_NAME} cannot find a package.json in '${projectRoot}', which \`nodeModules\` requires. Specify the directory with \`projectRoot\`.`);
214
252
  await writeOutputPackageJson(outputDir, {
215
253
  dependencies: await extractDependencies(projectPackageJson, nodeModules),
216
- packageManager: await readPackageManagerField(workspaceRoot)
254
+ packageManager: await readPackageManagerPin(workspaceRoot)
217
255
  });
218
256
  const stagedFiles = [];
219
257
  try {
220
258
  await stageWorkspaceFiles(workspaceRoot, outputDir, stagedFiles);
221
259
  const lockDest = node_path.join(outputDir, PNPM_LOCK);
222
- await fs_extra.default.promises.copyFile(depsLockFilePath, lockDest);
223
260
  stagedFiles.push(lockDest);
261
+ await fs_extra.default.promises.copyFile(depsLockFilePath, lockDest);
224
262
  require_logging.log.plain(require_logging.log.bold(PLUGIN_NAME), "installing", require_logging.log.bold(nodeModules.join(", ")));
225
263
  const [bin, ...args] = PNPM_INSTALL_COMMAND;
226
264
  await require_exec.createExec({ cwd: outputDir })(bin, ...args);
227
265
  } catch (err) {
228
- await stripInstallFiles(outputDir, stagedFiles, err);
266
+ await stripInstallFiles(outputDir, [
267
+ ...stagedFiles,
268
+ node_path.join(outputDir, "package.json"),
269
+ node_path.join(outputDir, "node_modules")
270
+ ], err);
229
271
  throw err;
230
272
  }
231
273
  await stripInstallFiles(outputDir, stagedFiles);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skuba-lib/api",
3
- "version": "2.3.0-rolldown-cdk-bundle-plugin-20260804050549",
3
+ "version": "2.3.0-rolldown-cdk-bundle-plugin-20260804234000",
4
4
  "description": "Node.js development API for skuba",
5
5
  "homepage": "https://github.com/seek-oss/skuba#readme",
6
6
  "bugs": {