alchemy 0.10.0 → 0.10.2

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 (52) hide show
  1. package/lib/alchemy.js +4 -1
  2. package/lib/cloudflare/api.js +1 -1
  3. package/lib/cloudflare/assets.js +2 -2
  4. package/lib/cloudflare/d1-database.d.ts +28 -1
  5. package/lib/cloudflare/d1-database.js +39 -1
  6. package/lib/cloudflare/d1-migrations.d.ts +46 -0
  7. package/lib/cloudflare/d1-migrations.js +82 -0
  8. package/lib/cloudflare/worker.js +2 -2
  9. package/lib/cloudflare/wrangler.json.js +1 -1
  10. package/lib/esbuild/bundle.d.ts +2 -2
  11. package/lib/esbuild/bundle.js +28 -15
  12. package/lib/internal/docs/providers.d.ts +2 -2
  13. package/lib/internal/docs/providers.js +5 -4
  14. package/lib/test/bun.js +1 -1
  15. package/lib/test/prune.d.ts +13 -0
  16. package/lib/test/prune.js +130 -0
  17. package/lib/util/content-type.js +1 -1
  18. package/lib/util/rm.js +1 -1
  19. package/lib/web/astro.js +6 -3
  20. package/lib/web/shadcn-component.js +2 -2
  21. package/lib/web/shadcn.d.ts +1 -1
  22. package/lib/web/shadcn.js +3 -2
  23. package/lib/web/tailwind.js +3 -2
  24. package/lib/web/vite.js +5 -3
  25. package/lib/web/vitepress/config.js +1 -1
  26. package/lib/web/vitepress/custom-theme.js +2 -2
  27. package/lib/web/vitepress/home-page.d.ts +1 -1
  28. package/lib/web/vitepress/home-page.js +1 -1
  29. package/lib/web/vitepress/vitepress.js +7 -4
  30. package/package.json +1 -1
  31. package/src/alchemy.ts +4 -1
  32. package/src/cloudflare/api.ts +1 -1
  33. package/src/cloudflare/assets.ts +2 -2
  34. package/src/cloudflare/d1-database.ts +65 -1
  35. package/src/cloudflare/d1-migrations.ts +137 -0
  36. package/src/cloudflare/worker.ts +2 -2
  37. package/src/cloudflare/wrangler.json.ts +1 -1
  38. package/src/esbuild/bundle.ts +44 -22
  39. package/src/internal/docs/providers.ts +5 -4
  40. package/src/test/bun.ts +1 -1
  41. package/src/test/prune.ts +165 -0
  42. package/src/util/content-type.ts +1 -1
  43. package/src/util/rm.ts +1 -1
  44. package/src/web/astro.ts +15 -17
  45. package/src/web/shadcn-component.ts +6 -6
  46. package/src/web/shadcn.ts +9 -8
  47. package/src/web/tailwind.ts +5 -4
  48. package/src/web/vite.ts +14 -12
  49. package/src/web/vitepress/config.ts +1 -1
  50. package/src/web/vitepress/custom-theme.ts +9 -9
  51. package/src/web/vitepress/home-page.ts +21 -17
  52. package/src/web/vitepress/vitepress.ts +7 -9
@@ -1,11 +1,11 @@
1
1
  import { type } from "arktype";
2
- import path from "path";
2
+ import path from "node:path";
3
3
  import { CSSFile } from "../../ai/css-file";
4
4
  import { Data } from "../../ai/data";
5
5
  import { TypeScriptFile } from "../../ai/typescript-file";
6
6
  import { VueFile } from "../../ai/vue-file";
7
7
  import { alchemy } from "../../alchemy";
8
- import { Folder } from "../../fs";
8
+ import { Folder } from "../../fs/folder";
9
9
  import { Resource } from "../../resource";
10
10
  /**
11
11
  * Resource for generating a custom VitePress theme using AI.
@@ -1,5 +1,5 @@
1
1
  import { Document } from "../../ai/document";
2
- import { StaticTextFile } from "../../fs";
2
+ import { StaticTextFile } from "../../fs/static-text-file";
3
3
  import type { Secret } from "../../secret";
4
4
  /**
5
5
  * Image that can be themed for light/dark mode
@@ -1,7 +1,7 @@
1
1
  import yaml from "yaml";
2
2
  import { Document } from "../../ai/document";
3
3
  import { alchemy } from "../../alchemy";
4
- import { StaticTextFile } from "../../fs";
4
+ import { StaticTextFile } from "../../fs/static-text-file";
5
5
  /**
6
6
  * Parse YAML frontmatter to home page config
7
7
  */
@@ -1,8 +1,11 @@
1
1
  import { exec } from "child_process";
2
- import fs from "fs/promises";
3
- import path from "path";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
4
  import { promisify } from "util";
5
- import { Folder, StaticJsonFile, StaticTextFile, StaticTypeScriptFile, } from "../../fs";
5
+ import { Folder } from "../../fs/folder";
6
+ import { StaticJsonFile } from "../../fs/static-json-file";
7
+ import { StaticTextFile } from "../../fs/static-text-file";
8
+ import { StaticTypeScriptFile } from "../../fs/static-typescript-file";
6
9
  import { Resource } from "../../resource";
7
10
  import { InstallDependencies, fixedDependencies } from "./dependencies";
8
11
  const execAsync = promisify(exec);
@@ -75,7 +78,7 @@ export const VitepressProject = Resource("project::VitepressProject", {
75
78
  // `import alchemy from "alchemy";
76
79
  // import { Folder } from "alchemy/fs";
77
80
  // import { Document } from "alchemy/docs";
78
- // import path from "path";
81
+ // import path from "node:path";
79
82
  // const app = await alchemy("alchemy.run", {
80
83
  // stage: "prod",
81
84
  // phase: process.argv.includes("--destroy") ? "destroy" : "up",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alchemy",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "type": "module",
5
5
  "module": "./lib/index.js",
6
6
  "scripts": {
package/src/alchemy.ts CHANGED
@@ -111,7 +111,10 @@ async function _alchemy(
111
111
  : 0;
112
112
  const indent = " ".repeat(leadingSpaces);
113
113
 
114
- const { isFileCollection, isFileRef } = await import("./fs");
114
+ const [{ isFileRef }, { isFileCollection }] = await Promise.all([
115
+ import("./fs/file-ref"),
116
+ import("./fs/file-collection"),
117
+ ]);
115
118
 
116
119
  const appendices: Record<string, string> = {};
117
120
 
@@ -131,7 +131,7 @@ export class CloudflareApi {
131
131
  errorMsg.includes("ETIMEDOUT") ||
132
132
  errorMsg.includes("ECONNREFUSED");
133
133
 
134
- return isNetworkError;
134
+ return isNetworkError || error.status?.toString().startsWith("5");
135
135
  },
136
136
  5, // Maximum 5 attempts (1 initial + 4 retries)
137
137
  1000 // Start with 1s delay, will exponentially increase
@@ -1,5 +1,5 @@
1
- import * as fs from "fs/promises";
2
- import * as path from "path";
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
3
  import type { Context } from "../context";
4
4
  import { Resource } from "../resource";
5
5
  import { getContentType } from "../util/content-type";
@@ -6,6 +6,7 @@ import {
6
6
  type CloudflareApiOptions,
7
7
  } from "./api";
8
8
  import { CloudflareApiError, handleApiError } from "./api-error";
9
+ import { applyMigrations, listMigrationsFiles } from "./d1-migrations";
9
10
 
10
11
  /**
11
12
  * Properties for creating or updating a D1 Database
@@ -57,6 +58,25 @@ export interface D1DatabaseProps extends CloudflareApiOptions {
57
58
  * @default false
58
59
  */
59
60
  adopt?: boolean;
61
+
62
+ /**
63
+ * These files will be generated internally with the D1Database wrapper function when migrationsDir is specified
64
+ *
65
+ * @private
66
+ */
67
+ migrationsFiles?: Array<{ id: string; sql: string }>;
68
+
69
+ /**
70
+ * Name of the table used to track migrations. Only used if migrationsDir is specified. Defaults to 'd1_migrations'
71
+ * This is analogous to wrangler's `migrations_table`.
72
+ */
73
+ migrationsTable?: string;
74
+
75
+ /**
76
+ * Directory containing migration SQL files. If not set, no migrations will be applied.
77
+ * This is analogous to wrangler's `migrations_dir`.
78
+ */
79
+ migrationsDir?: string;
60
80
  }
61
81
 
62
82
  /**
@@ -97,6 +117,20 @@ export interface D1Database
97
117
  };
98
118
  }
99
119
 
120
+ export async function D1Database(
121
+ id: string,
122
+ props: Omit<D1DatabaseProps, "migrationsFiles">
123
+ ) {
124
+ const migrationsFiles = props.migrationsDir
125
+ ? await listMigrationsFiles(props.migrationsDir)
126
+ : [];
127
+
128
+ return D1DatabaseResource(id, {
129
+ ...props,
130
+ migrationsFiles,
131
+ });
132
+ }
133
+
100
134
  /**
101
135
  * Creates and manages Cloudflare D1 Databases.
102
136
  *
@@ -126,9 +160,16 @@ export interface D1Database
126
160
  * }
127
161
  * });
128
162
  *
163
+ * @example
164
+ * // Create a database with migrations
165
+ * const dbWithMigrations = await D1Database("mydb", {
166
+ * name: "mydb",
167
+ * migrationsDir: "./migrations",
168
+ * });
169
+ *
129
170
  * @see https://developers.cloudflare.com/d1/
130
171
  */
131
- export const D1Database = Resource(
172
+ export const D1DatabaseResource = Resource(
132
173
  "cloudflare::D1Database",
133
174
  async function (
134
175
  this: Context<D1Database>,
@@ -204,6 +245,29 @@ export const D1Database = Resource(
204
245
  }
205
246
  }
206
247
 
248
+ // Run migrations if provided
249
+ if (props.migrationsFiles && props.migrationsFiles.length > 0) {
250
+ try {
251
+ const migrationsTable = props.migrationsTable || "d1_migrations";
252
+ const databaseId = dbData.result.uuid || this.output?.id;
253
+
254
+ if (!databaseId) {
255
+ throw new Error("Database ID not found for migrations");
256
+ }
257
+
258
+ await applyMigrations({
259
+ migrationsFiles: props.migrationsFiles,
260
+ migrationsTable,
261
+ accountId: api.accountId,
262
+ databaseId,
263
+ api,
264
+ });
265
+ } catch (migrationErr) {
266
+ console.error("Failed to apply D1 migrations:", migrationErr);
267
+ throw migrationErr;
268
+ }
269
+ }
270
+
207
271
  return this({
208
272
  type: "d1",
209
273
  id: dbData.result.uuid || "",
@@ -0,0 +1,137 @@
1
+ import * as fs from "fs/promises";
2
+ import * as path from "node:path";
3
+ import { CloudflareApi } from "./api";
4
+ import { handleApiError } from "./api-error";
5
+
6
+ export interface D1MigrationOptions {
7
+ migrationsFiles: Array<{ id: string; sql: string }>;
8
+ migrationsTable: string;
9
+ accountId: string;
10
+ databaseId: string;
11
+ api: CloudflareApi;
12
+ }
13
+
14
+ const getPrefix = (name: string) => {
15
+ const prefix = name.split("_")[0];
16
+ const num = parseInt(prefix, 10);
17
+ return isNaN(num) ? null : num;
18
+ };
19
+
20
+ async function readMigrationFile(filePath: string): Promise<string> {
21
+ return fs.readFile(filePath, "utf-8");
22
+ }
23
+
24
+ /**
25
+ * Reads migration SQL files from the migrationsDir, sorted by filename.
26
+ * @param migrationsDir Directory containing .sql migration files
27
+ */
28
+ export async function listMigrationsFiles(
29
+ migrationsDir: string
30
+ ): Promise<Array<{ id: string; sql: string }>> {
31
+ const entries = await fs.readdir(migrationsDir);
32
+
33
+ const sqlFiles = entries
34
+ .filter((f: string) => f.endsWith(".sql"))
35
+ .sort((a: string, b: string) => {
36
+ const aNum = getPrefix(a);
37
+ const bNum = getPrefix(b);
38
+
39
+ if (aNum !== null && bNum !== null) return aNum - bNum;
40
+ if (aNum !== null) return -1;
41
+ if (bNum !== null) return 1;
42
+
43
+ return a.localeCompare(b);
44
+ });
45
+
46
+ const files: Array<{ id: string; sql: string }> = [];
47
+ for (const file of sqlFiles) {
48
+ const sql = await readMigrationFile(path.join(migrationsDir, file));
49
+ files.push({ id: file, sql });
50
+ }
51
+
52
+ return files;
53
+ }
54
+
55
+ /**
56
+ * Ensures the migrations table exists in the D1 database.
57
+ */
58
+ export async function ensureMigrationsTable(
59
+ options: D1MigrationOptions
60
+ ): Promise<void> {
61
+ const createTableSQL = `CREATE TABLE IF NOT EXISTS ${options.migrationsTable} (id TEXT PRIMARY KEY, applied_at TEXT);`;
62
+
63
+ await executeD1SQL(options, createTableSQL);
64
+ }
65
+
66
+ /**
67
+ * Gets the list of applied migration IDs from the migrations table.
68
+ */
69
+ export async function getAppliedMigrations(
70
+ options: D1MigrationOptions
71
+ ): Promise<Set<string>> {
72
+ const sql = `SELECT id FROM ${options.migrationsTable};`;
73
+
74
+ const result = await executeD1SQL(options, sql);
75
+
76
+ const ids = (result?.result[0]?.results || []).map((row: any) => row.id);
77
+ return new Set(ids);
78
+ }
79
+
80
+ /**
81
+ * Executes a SQL statement against the D1 database using the HTTP API.
82
+ */
83
+ export async function executeD1SQL(
84
+ options: D1MigrationOptions,
85
+ sql: string
86
+ ): Promise<{
87
+ result: [
88
+ {
89
+ results: Array<unknown>;
90
+ success: boolean;
91
+ meta: any;
92
+ },
93
+ ];
94
+ errors: Array<any>;
95
+ messages: Array<any>;
96
+ success: boolean;
97
+ }> {
98
+ const response = await options.api.post(
99
+ `/accounts/${options.accountId}/d1/database/${options.databaseId}/query`,
100
+ { sql }
101
+ );
102
+
103
+ if (!response.ok) {
104
+ await handleApiError(
105
+ response,
106
+ "executing migration SQL",
107
+ "D1 database",
108
+ options.databaseId
109
+ );
110
+ }
111
+
112
+ return response.json();
113
+ }
114
+
115
+ /**
116
+ * Applies all pending migrations from the provided files to the D1 database.
117
+ */
118
+ export async function applyMigrations(
119
+ options: D1MigrationOptions
120
+ ): Promise<void> {
121
+ await ensureMigrationsTable(options);
122
+ const applied = await getAppliedMigrations(options);
123
+
124
+ for (const migration of options.migrationsFiles) {
125
+ const migrationId = migration.id;
126
+
127
+ if (applied.has(migrationId)) continue;
128
+
129
+ // Run the migration
130
+ await executeD1SQL(options, migration.sql);
131
+ // Record as applied
132
+ const insertSQL = `INSERT INTO ${options.migrationsTable} (id, applied_at) VALUES ('${migrationId.replace("'", "''")}', datetime('now'));`;
133
+ await executeD1SQL(options, insertSQL);
134
+
135
+ console.log(`Applied migration: ${migrationId}`);
136
+ }
137
+ }
@@ -1,5 +1,5 @@
1
- import * as crypto from "crypto";
2
- import * as fs from "fs/promises";
1
+ import * as crypto from "node:crypto";
2
+ import * as fs from "node:fs/promises";
3
3
  import type { Context } from "../context";
4
4
  import { Bundle, type BundleProps } from "../esbuild/bundle";
5
5
  import { Resource } from "../resource";
@@ -1,5 +1,5 @@
1
1
  import type { Context } from "../context";
2
- import { StaticJsonFile } from "../fs";
2
+ import { StaticJsonFile } from "../fs/static-json-file";
3
3
  import { Resource } from "../resource";
4
4
  import type { Bindings } from "./bindings";
5
5
  import type { DurableObjectNamespace } from "./durable-object-namespace";
@@ -119,22 +119,54 @@ export const Bundle = Resource(
119
119
  props: BundleProps
120
120
  ): Promise<Bundle> {
121
121
  // Determine output path
122
- const outputPath = getOutputPath(props);
122
+ const outDirPath =
123
+ props.outfile != null ? path.dirname(props.outfile) : props.outdir;
123
124
 
124
- // Ensure output directory exists
125
- await fs.promises.mkdir(path.dirname(outputPath), { recursive: true });
125
+ if (outDirPath == null) {
126
+ throw new Error(
127
+ `You need to specify either outfile or outdir in your bundle configuration ${JSON.stringify(props)}`
128
+ );
129
+ }
126
130
 
131
+ // Ensure output directory exists
132
+ await fs.promises.mkdir(path.dirname(outDirPath), { recursive: true });
127
133
  if (this.phase === "delete") {
128
- await fs.promises.unlink(outputPath).catch(() => {});
129
- // Also clean up sourcemap if it exists
130
- await fs.promises.unlink(outputPath + ".map").catch(() => {});
131
- return this.destroy();
134
+ await fs.promises.rm(outDirPath, { recursive: true });
132
135
  }
133
136
 
134
137
  const result = await bundle(props);
138
+ // Check that bundle created output an file for the given entrypoint
139
+ // Use bundle metada data to retrieve its content
140
+ const bundleOutputPath = Object.entries(result.metafile.outputs).find(
141
+ ([file, output]) => {
142
+ if (output.entryPoint === undefined) {
143
+ return false;
144
+ }
145
+ // resolve to absolute and then relative to ensure consistent result (e.g. ./src/handler.ts instead of src/handler.ts)
146
+ const relativeOutput = path.relative(
147
+ process.cwd(),
148
+ path.resolve(output.entryPoint)
149
+ );
150
+ return (
151
+ relativeOutput ===
152
+ path.relative(
153
+ process.cwd(),
154
+ path.resolve(process.cwd(), props.entryPoint)
155
+ )
156
+ );
157
+ }
158
+ )?.[0];
159
+
160
+ if (!bundleOutputPath) {
161
+ throw new Error(`Unable to find a compiled file`);
162
+ }
163
+
135
164
  // Calculate hash of the output
136
- const contents = await fs.promises.readFile(outputPath);
137
- const hash = crypto.createHash("sha256").update(contents).digest("hex");
165
+ const bundleOutputContent = await fs.promises.readFile(bundleOutputPath);
166
+ const hash = crypto
167
+ .createHash("sha256")
168
+ .update(bundleOutputContent)
169
+ .digest("hex");
138
170
 
139
171
  // Store metadata in context
140
172
  await this.set("metafile", result.metafile);
@@ -142,18 +174,18 @@ export const Bundle = Resource(
142
174
 
143
175
  // Return output info
144
176
  return this({
145
- path: outputPath,
177
+ path: bundleOutputPath,
146
178
  hash,
147
179
  });
148
180
  }
149
181
  );
150
182
 
151
183
  export async function bundle(props: BundleProps) {
152
- const outputPath = getOutputPath(props);
153
184
  return await esbuild.build({
154
185
  ...props.options,
155
186
  entryPoints: [props.entryPoint],
156
- outfile: outputPath,
187
+ outdir: props.outdir,
188
+ outfile: props.outfile,
157
189
  bundle: true,
158
190
  format: props.format,
159
191
  target: props.target,
@@ -169,13 +201,3 @@ export async function bundle(props: BundleProps) {
169
201
  write: true,
170
202
  });
171
203
  }
172
-
173
- function getOutputPath(props: BundleProps) {
174
- return (
175
- props.outfile ||
176
- path.join(
177
- props.outdir || "dist",
178
- path.basename(props.entryPoint, path.extname(props.entryPoint)) + ".js"
179
- )
180
- );
181
- }
@@ -1,9 +1,10 @@
1
1
  import { type } from "arktype";
2
- import fs from "fs/promises";
3
- import path from "path";
4
- import { Data, Document } from "../../ai";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { Data } from "../../ai/data";
5
+ import { Document } from "../../ai/document";
5
6
  import { alchemy } from "../../alchemy";
6
- import { Folder } from "../../fs";
7
+ import { Folder } from "../../fs/folder";
7
8
 
8
9
  export interface DocsProps {
9
10
  /**
package/src/test/bun.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  import { afterAll, beforeAll, it } from "bun:test";
4
4
  import path from "node:path";
5
5
  import { alchemy } from "../alchemy";
6
- import { R2RestStateStore } from "../cloudflare";
6
+ import { R2RestStateStore } from "../cloudflare/r2-rest-state-store";
7
7
  import { Scope } from "../scope";
8
8
  import type { StateStoreType } from "../state";
9
9
 
@@ -0,0 +1,165 @@
1
+ import { exec } from "child_process";
2
+ import * as esbuild from "esbuild";
3
+ import { glob } from "glob";
4
+ import { spawn } from "node:child_process";
5
+ import path from "node:path";
6
+ import { promisify } from "util";
7
+
8
+ const execAsync = promisify(exec);
9
+
10
+ /**
11
+ * Runs only tests that have changed dependencies
12
+ * @param directory Directory to scan for test files
13
+ * @param baseCommit Optional base commit to compare against
14
+ */
15
+ export async function runChangedTests(
16
+ directory: string,
17
+ baseCommit?: string
18
+ ): Promise<void> {
19
+ const changedTests = await findChangedTestFiles(directory, baseCommit);
20
+
21
+ if (changedTests.length === 0) {
22
+ console.log("No tests affected by recent changes.");
23
+ return;
24
+ }
25
+
26
+ // Run the tests with bun using spawn for stdio inheritance
27
+ return new Promise<void>((resolve, reject) => {
28
+ console.log(`bun test ${changedTests.join(" ")}`);
29
+ // resolve();
30
+ const child = spawn("bun", ["test", ...changedTests], { stdio: "inherit" });
31
+
32
+ child.on("close", (code) => {
33
+ if (code === 0) resolve();
34
+ else reject(new Error(`Tests exited with code ${code}`));
35
+ });
36
+
37
+ child.on("error", reject);
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Identifies test files that have changed or depend on changed files
43
+ * @param directory Directory to scan for test files
44
+ * @param baseCommit Optional base commit to compare against (defaults to HEAD~1)
45
+ * @returns Array of changed test files
46
+ */
47
+ export async function findChangedTestFiles(
48
+ directory: string,
49
+ baseCommit: string = "HEAD~1"
50
+ ): Promise<string[]> {
51
+ // 1. Find all test files
52
+ const testFiles = await findTestFiles(directory);
53
+
54
+ // 2. Get git changed files
55
+ const changedFiles = await getChangedFiles(baseCommit);
56
+
57
+ // 3. Process each test file
58
+ const changedTestFiles: string[] = [];
59
+
60
+ for (const testFile of testFiles) {
61
+ // Get dependency tree for the test file
62
+ const dependencies = await getDependencies(testFile);
63
+
64
+ // Check if any dependencies have changed
65
+ const changedDependencies = [...dependencies].filter((dep) =>
66
+ changedFiles.has(dep)
67
+ );
68
+
69
+ if (changedDependencies.length > 0) {
70
+ changedTestFiles.push(testFile);
71
+ }
72
+ }
73
+
74
+ return changedTestFiles.map((p) => path.relative(process.cwd(), p));
75
+ }
76
+
77
+ /**
78
+ * Finds all test files in a directory
79
+ * @param directory Directory to scan
80
+ * @returns Array of test file paths
81
+ */
82
+ async function findTestFiles(directory: string): Promise<string[]> {
83
+ return await glob(path.join(directory, "**/*.test.ts"));
84
+ }
85
+
86
+ /**
87
+ * Gets list of files changed since a base commit
88
+ * @param baseCommit Base commit to compare against
89
+ * @returns Set of changed file paths
90
+ */
91
+ async function getChangedFiles(baseCommit: string): Promise<Set<string>> {
92
+ // Get changed files from git
93
+ const { stdout: gitOutput } = await execAsync(
94
+ `git diff --name-only ${baseCommit}`
95
+ );
96
+
97
+ // Convert to set of absolute paths
98
+ const changedFiles = new Set<string>();
99
+ const { stdout: workDir } = await execAsync("git rev-parse --show-toplevel");
100
+ const rootDir = workDir.trim();
101
+
102
+ gitOutput
103
+ .split("\n")
104
+ .filter((line) => line.trim().length > 0)
105
+ .forEach((file) => {
106
+ changedFiles.add(path.resolve(rootDir, file));
107
+ });
108
+
109
+ return changedFiles;
110
+ }
111
+
112
+ /**
113
+ * Gets dependency tree for a test file
114
+ * @param testFile Path to test file
115
+ * @returns Set of dependency file paths
116
+ */
117
+ async function getDependencies(testFile: string): Promise<Set<string>> {
118
+ const dependencies = new Set<string>();
119
+
120
+ // Add the test file itself to dependencies
121
+ const absTestPath = path.resolve(testFile);
122
+ dependencies.add(absTestPath);
123
+
124
+ try {
125
+ // Use esbuild with metafile to get dependencies
126
+ const result = await esbuild.build({
127
+ entryPoints: [testFile],
128
+ write: false,
129
+ platform: "node",
130
+ format: "esm",
131
+ bundle: true,
132
+ metafile: true,
133
+ external: ["@cloudflare/workers-types", "bun:test"],
134
+ });
135
+
136
+ if (!result.metafile) {
137
+ return dependencies;
138
+ }
139
+
140
+ // Function to normalize paths for comparison
141
+ const normalizePath = (p: string) => path.resolve(p).replace(/\\/g, "/");
142
+
143
+ // Process imports from the metafile
144
+ const { inputs } = result.metafile;
145
+ for (const filePath in inputs) {
146
+ // Skip node_modules and non-local imports
147
+ if (
148
+ filePath.includes("node_modules") ||
149
+ filePath.includes("bun:") ||
150
+ !filePath.endsWith(".ts")
151
+ ) {
152
+ continue;
153
+ }
154
+
155
+ const absolutePath = normalizePath(filePath);
156
+ dependencies.add(absolutePath);
157
+ }
158
+
159
+ return dependencies;
160
+ } catch (error) {
161
+ console.error(`Error analyzing dependencies for ${testFile}:`, error);
162
+ // Return at least the test file itself
163
+ return dependencies;
164
+ }
165
+ }
@@ -1,4 +1,4 @@
1
- import path from "path";
1
+ import path from "node:path";
2
2
 
3
3
  // Common MIME types
4
4
  const mimeTypes: Record<string, string> = {
package/src/util/rm.ts CHANGED
@@ -1,4 +1,4 @@
1
- import fs from "fs/promises";
1
+ import fs from "node:fs/promises";
2
2
 
3
3
  export async function rm(path: string) {
4
4
  try {