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
package/lib/alchemy.js CHANGED
@@ -30,7 +30,10 @@ async function _alchemy(...args) {
30
30
  ? secondLine.match(/^(\s*)/)?.[1]?.length || 0
31
31
  : 0;
32
32
  const indent = " ".repeat(leadingSpaces);
33
- const { isFileCollection, isFileRef } = await import("./fs");
33
+ const [{ isFileRef }, { isFileCollection }] = await Promise.all([
34
+ import("./fs/file-ref"),
35
+ import("./fs/file-collection"),
36
+ ]);
34
37
  const appendices = {};
35
38
  const stringValues = await Promise.all(values.map(async function resolve(value) {
36
39
  if (typeof value === "string") {
@@ -79,7 +79,7 @@ export class CloudflareApi {
79
79
  errorMsg.includes("ECONNRESET") ||
80
80
  errorMsg.includes("ETIMEDOUT") ||
81
81
  errorMsg.includes("ECONNREFUSED");
82
- return isNetworkError;
82
+ return isNetworkError || error.status?.toString().startsWith("5");
83
83
  }, 5, // Maximum 5 attempts (1 initial + 4 retries)
84
84
  1000 // Start with 1s delay, will exponentially increase
85
85
  );
@@ -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 { Resource } from "../resource";
4
4
  import { getContentType } from "../util/content-type";
5
5
  /**
@@ -40,6 +40,25 @@ export interface D1DatabaseProps extends CloudflareApiOptions {
40
40
  * @default false
41
41
  */
42
42
  adopt?: boolean;
43
+ /**
44
+ * These files will be generated internally with the D1Database wrapper function when migrationsDir is specified
45
+ *
46
+ * @private
47
+ */
48
+ migrationsFiles?: Array<{
49
+ id: string;
50
+ sql: string;
51
+ }>;
52
+ /**
53
+ * Name of the table used to track migrations. Only used if migrationsDir is specified. Defaults to 'd1_migrations'
54
+ * This is analogous to wrangler's `migrations_table`.
55
+ */
56
+ migrationsTable?: string;
57
+ /**
58
+ * Directory containing migration SQL files. If not set, no migrations will be applied.
59
+ * This is analogous to wrangler's `migrations_dir`.
60
+ */
61
+ migrationsDir?: string;
43
62
  }
44
63
  /**
45
64
  * Output returned after D1 Database creation/update
@@ -72,6 +91,7 @@ export interface D1Database extends Resource<"cloudflare::D1Database">, D1Databa
72
91
  mode: "auto" | "disabled";
73
92
  };
74
93
  }
94
+ export declare function D1Database(id: string, props: Omit<D1DatabaseProps, "migrationsFiles">): Promise<D1Database>;
75
95
  /**
76
96
  * Creates and manages Cloudflare D1 Databases.
77
97
  *
@@ -101,9 +121,16 @@ export interface D1Database extends Resource<"cloudflare::D1Database">, D1Databa
101
121
  * }
102
122
  * });
103
123
  *
124
+ * @example
125
+ * // Create a database with migrations
126
+ * const dbWithMigrations = await D1Database("mydb", {
127
+ * name: "mydb",
128
+ * migrationsDir: "./migrations",
129
+ * });
130
+ *
104
131
  * @see https://developers.cloudflare.com/d1/
105
132
  */
106
- export declare const D1Database: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<D1Database>, id: string, props: D1DatabaseProps) => Promise<D1Database>);
133
+ export declare const D1DatabaseResource: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<D1Database>, id: string, props: D1DatabaseProps) => Promise<D1Database>);
107
134
  interface CloudflareD1Response {
108
135
  result: {
109
136
  uuid?: string;
@@ -1,6 +1,16 @@
1
1
  import { Resource } from "../resource";
2
2
  import { CloudflareApi, createCloudflareApi, } from "./api";
3
3
  import { CloudflareApiError, handleApiError } from "./api-error";
4
+ import { applyMigrations, listMigrationsFiles } from "./d1-migrations";
5
+ export async function D1Database(id, props) {
6
+ const migrationsFiles = props.migrationsDir
7
+ ? await listMigrationsFiles(props.migrationsDir)
8
+ : [];
9
+ return D1DatabaseResource(id, {
10
+ ...props,
11
+ migrationsFiles,
12
+ });
13
+ }
4
14
  /**
5
15
  * Creates and manages Cloudflare D1 Databases.
6
16
  *
@@ -30,9 +40,16 @@ import { CloudflareApiError, handleApiError } from "./api-error";
30
40
  * }
31
41
  * });
32
42
  *
43
+ * @example
44
+ * // Create a database with migrations
45
+ * const dbWithMigrations = await D1Database("mydb", {
46
+ * name: "mydb",
47
+ * migrationsDir: "./migrations",
48
+ * });
49
+ *
33
50
  * @see https://developers.cloudflare.com/d1/
34
51
  */
35
- export const D1Database = Resource("cloudflare::D1Database", async function (id, props) {
52
+ export const D1DatabaseResource = Resource("cloudflare::D1Database", async function (id, props) {
36
53
  const api = await createCloudflareApi(props);
37
54
  const databaseName = props.name || id;
38
55
  if (this.phase === "delete") {
@@ -91,6 +108,27 @@ export const D1Database = Resource("cloudflare::D1Database", async function (id,
91
108
  dbData = await createDatabase(api, databaseName, props);
92
109
  }
93
110
  }
111
+ // Run migrations if provided
112
+ if (props.migrationsFiles && props.migrationsFiles.length > 0) {
113
+ try {
114
+ const migrationsTable = props.migrationsTable || "d1_migrations";
115
+ const databaseId = dbData.result.uuid || this.output?.id;
116
+ if (!databaseId) {
117
+ throw new Error("Database ID not found for migrations");
118
+ }
119
+ await applyMigrations({
120
+ migrationsFiles: props.migrationsFiles,
121
+ migrationsTable,
122
+ accountId: api.accountId,
123
+ databaseId,
124
+ api,
125
+ });
126
+ }
127
+ catch (migrationErr) {
128
+ console.error("Failed to apply D1 migrations:", migrationErr);
129
+ throw migrationErr;
130
+ }
131
+ }
94
132
  return this({
95
133
  type: "d1",
96
134
  id: dbData.result.uuid || "",
@@ -0,0 +1,46 @@
1
+ import { CloudflareApi } from "./api";
2
+ export interface D1MigrationOptions {
3
+ migrationsFiles: Array<{
4
+ id: string;
5
+ sql: string;
6
+ }>;
7
+ migrationsTable: string;
8
+ accountId: string;
9
+ databaseId: string;
10
+ api: CloudflareApi;
11
+ }
12
+ /**
13
+ * Reads migration SQL files from the migrationsDir, sorted by filename.
14
+ * @param migrationsDir Directory containing .sql migration files
15
+ */
16
+ export declare function listMigrationsFiles(migrationsDir: string): Promise<Array<{
17
+ id: string;
18
+ sql: string;
19
+ }>>;
20
+ /**
21
+ * Ensures the migrations table exists in the D1 database.
22
+ */
23
+ export declare function ensureMigrationsTable(options: D1MigrationOptions): Promise<void>;
24
+ /**
25
+ * Gets the list of applied migration IDs from the migrations table.
26
+ */
27
+ export declare function getAppliedMigrations(options: D1MigrationOptions): Promise<Set<string>>;
28
+ /**
29
+ * Executes a SQL statement against the D1 database using the HTTP API.
30
+ */
31
+ export declare function executeD1SQL(options: D1MigrationOptions, sql: string): Promise<{
32
+ result: [
33
+ {
34
+ results: Array<unknown>;
35
+ success: boolean;
36
+ meta: any;
37
+ }
38
+ ];
39
+ errors: Array<any>;
40
+ messages: Array<any>;
41
+ success: boolean;
42
+ }>;
43
+ /**
44
+ * Applies all pending migrations from the provided files to the D1 database.
45
+ */
46
+ export declare function applyMigrations(options: D1MigrationOptions): Promise<void>;
@@ -0,0 +1,82 @@
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
+ const getPrefix = (name) => {
6
+ const prefix = name.split("_")[0];
7
+ const num = parseInt(prefix, 10);
8
+ return isNaN(num) ? null : num;
9
+ };
10
+ async function readMigrationFile(filePath) {
11
+ return fs.readFile(filePath, "utf-8");
12
+ }
13
+ /**
14
+ * Reads migration SQL files from the migrationsDir, sorted by filename.
15
+ * @param migrationsDir Directory containing .sql migration files
16
+ */
17
+ export async function listMigrationsFiles(migrationsDir) {
18
+ const entries = await fs.readdir(migrationsDir);
19
+ const sqlFiles = entries
20
+ .filter((f) => f.endsWith(".sql"))
21
+ .sort((a, b) => {
22
+ const aNum = getPrefix(a);
23
+ const bNum = getPrefix(b);
24
+ if (aNum !== null && bNum !== null)
25
+ return aNum - bNum;
26
+ if (aNum !== null)
27
+ return -1;
28
+ if (bNum !== null)
29
+ return 1;
30
+ return a.localeCompare(b);
31
+ });
32
+ const files = [];
33
+ for (const file of sqlFiles) {
34
+ const sql = await readMigrationFile(path.join(migrationsDir, file));
35
+ files.push({ id: file, sql });
36
+ }
37
+ return files;
38
+ }
39
+ /**
40
+ * Ensures the migrations table exists in the D1 database.
41
+ */
42
+ export async function ensureMigrationsTable(options) {
43
+ const createTableSQL = `CREATE TABLE IF NOT EXISTS ${options.migrationsTable} (id TEXT PRIMARY KEY, applied_at TEXT);`;
44
+ await executeD1SQL(options, createTableSQL);
45
+ }
46
+ /**
47
+ * Gets the list of applied migration IDs from the migrations table.
48
+ */
49
+ export async function getAppliedMigrations(options) {
50
+ const sql = `SELECT id FROM ${options.migrationsTable};`;
51
+ const result = await executeD1SQL(options, sql);
52
+ const ids = (result?.result[0]?.results || []).map((row) => row.id);
53
+ return new Set(ids);
54
+ }
55
+ /**
56
+ * Executes a SQL statement against the D1 database using the HTTP API.
57
+ */
58
+ export async function executeD1SQL(options, sql) {
59
+ const response = await options.api.post(`/accounts/${options.accountId}/d1/database/${options.databaseId}/query`, { sql });
60
+ if (!response.ok) {
61
+ await handleApiError(response, "executing migration SQL", "D1 database", options.databaseId);
62
+ }
63
+ return response.json();
64
+ }
65
+ /**
66
+ * Applies all pending migrations from the provided files to the D1 database.
67
+ */
68
+ export async function applyMigrations(options) {
69
+ await ensureMigrationsTable(options);
70
+ const applied = await getAppliedMigrations(options);
71
+ for (const migration of options.migrationsFiles) {
72
+ const migrationId = migration.id;
73
+ if (applied.has(migrationId))
74
+ continue;
75
+ // Run the migration
76
+ await executeD1SQL(options, migration.sql);
77
+ // Record as applied
78
+ const insertSQL = `INSERT INTO ${options.migrationsTable} (id, applied_at) VALUES ('${migrationId.replace("'", "''")}', datetime('now'));`;
79
+ await executeD1SQL(options, insertSQL);
80
+ console.log(`Applied migration: ${migrationId}`);
81
+ }
82
+ }
@@ -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 { Bundle } from "../esbuild/bundle";
4
4
  import { Resource } from "../resource";
5
5
  import { getContentType } from "../util/content-type";
@@ -1,4 +1,4 @@
1
- import { StaticJsonFile } from "../fs";
1
+ import { StaticJsonFile } from "../fs/static-json-file";
2
2
  import { Resource } from "../resource";
3
3
  /**
4
4
  * Resource for managing wrangler.json configuration files
@@ -95,7 +95,8 @@ export interface Bundle extends Resource<"esbuild::Bundle"> {
95
95
  export declare const Bundle: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<Bundle>, id: string, props: BundleProps) => Promise<Bundle>);
96
96
  export declare function bundle(props: BundleProps): Promise<esbuild.BuildResult<{
97
97
  entryPoints: string[];
98
- outfile: string;
98
+ outdir: string | undefined;
99
+ outfile: string | undefined;
99
100
  bundle: true;
100
101
  format: "iife" | "cjs" | "esm" | undefined;
101
102
  target: string | string[] | undefined;
@@ -107,7 +108,6 @@ export declare function bundle(props: BundleProps): Promise<esbuild.BuildResult<
107
108
  write: true;
108
109
  splitting?: boolean;
109
110
  preserveSymlinks?: boolean;
110
- outdir?: string;
111
111
  outbase?: string;
112
112
  packages?: "bundle" | "external";
113
113
  alias?: Record<string, string>;
@@ -23,34 +23,51 @@ export const Bundle = Resource("esbuild::Bundle", {
23
23
  alwaysUpdate: true,
24
24
  }, async function (id, props) {
25
25
  // Determine output path
26
- const outputPath = getOutputPath(props);
26
+ const outDirPath = props.outfile != null ? path.dirname(props.outfile) : props.outdir;
27
+ if (outDirPath == null) {
28
+ throw new Error(`You need to specify either outfile or outdir in your bundle configuration ${JSON.stringify(props)}`);
29
+ }
27
30
  // Ensure output directory exists
28
- await fs.promises.mkdir(path.dirname(outputPath), { recursive: true });
31
+ await fs.promises.mkdir(path.dirname(outDirPath), { recursive: true });
29
32
  if (this.phase === "delete") {
30
- await fs.promises.unlink(outputPath).catch(() => { });
31
- // Also clean up sourcemap if it exists
32
- await fs.promises.unlink(outputPath + ".map").catch(() => { });
33
- return this.destroy();
33
+ await fs.promises.rm(outDirPath, { recursive: true });
34
34
  }
35
35
  const result = await bundle(props);
36
+ // Check that bundle created output an file for the given entrypoint
37
+ // Use bundle metada data to retrieve its content
38
+ const bundleOutputPath = Object.entries(result.metafile.outputs).find(([file, output]) => {
39
+ if (output.entryPoint === undefined) {
40
+ return false;
41
+ }
42
+ // resolve to absolute and then relative to ensure consistent result (e.g. ./src/handler.ts instead of src/handler.ts)
43
+ const relativeOutput = path.relative(process.cwd(), path.resolve(output.entryPoint));
44
+ return (relativeOutput ===
45
+ path.relative(process.cwd(), path.resolve(process.cwd(), props.entryPoint)));
46
+ })?.[0];
47
+ if (!bundleOutputPath) {
48
+ throw new Error(`Unable to find a compiled file`);
49
+ }
36
50
  // Calculate hash of the output
37
- const contents = await fs.promises.readFile(outputPath);
38
- const hash = crypto.createHash("sha256").update(contents).digest("hex");
51
+ const bundleOutputContent = await fs.promises.readFile(bundleOutputPath);
52
+ const hash = crypto
53
+ .createHash("sha256")
54
+ .update(bundleOutputContent)
55
+ .digest("hex");
39
56
  // Store metadata in context
40
57
  await this.set("metafile", result.metafile);
41
58
  await this.set("hash", hash);
42
59
  // Return output info
43
60
  return this({
44
- path: outputPath,
61
+ path: bundleOutputPath,
45
62
  hash,
46
63
  });
47
64
  });
48
65
  export async function bundle(props) {
49
- const outputPath = getOutputPath(props);
50
66
  return await esbuild.build({
51
67
  ...props.options,
52
68
  entryPoints: [props.entryPoint],
53
- outfile: outputPath,
69
+ outdir: props.outdir,
70
+ outfile: props.outfile,
54
71
  bundle: true,
55
72
  format: props.format,
56
73
  target: props.target,
@@ -66,7 +83,3 @@ export async function bundle(props) {
66
83
  write: true,
67
84
  });
68
85
  }
69
- function getOutputPath(props) {
70
- return (props.outfile ||
71
- path.join(props.outdir || "dist", path.basename(props.entryPoint, path.extname(props.entryPoint)) + ".js"));
72
- }
@@ -1,5 +1,5 @@
1
- import { Document } from "../../ai";
2
- import { Folder } from "../../fs";
1
+ import { Document } from "../../ai/document";
2
+ import { Folder } from "../../fs/folder";
3
3
  export interface DocsProps {
4
4
  /**
5
5
  * The output directory for the docs.
@@ -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
  export async function Providers({ srcDir, outDir, filter, parallel = true, }) {
8
9
  outDir = typeof outDir === "string" ? outDir : outDir.path;
9
10
  const exclude = [
package/lib/test/bun.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { afterAll, beforeAll, it } from "bun:test";
3
3
  import path from "node:path";
4
4
  import { alchemy } from "../alchemy";
5
- import { R2RestStateStore } from "../cloudflare";
5
+ import { R2RestStateStore } from "../cloudflare/r2-rest-state-store";
6
6
  import { Scope } from "../scope";
7
7
  /**
8
8
  * Add test functionality to alchemy instance
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Runs only tests that have changed dependencies
3
+ * @param directory Directory to scan for test files
4
+ * @param baseCommit Optional base commit to compare against
5
+ */
6
+ export declare function runChangedTests(directory: string, baseCommit?: string): Promise<void>;
7
+ /**
8
+ * Identifies test files that have changed or depend on changed files
9
+ * @param directory Directory to scan for test files
10
+ * @param baseCommit Optional base commit to compare against (defaults to HEAD~1)
11
+ * @returns Array of changed test files
12
+ */
13
+ export declare function findChangedTestFiles(directory: string, baseCommit?: string): Promise<string[]>;
@@ -0,0 +1,130 @@
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
+ const execAsync = promisify(exec);
8
+ /**
9
+ * Runs only tests that have changed dependencies
10
+ * @param directory Directory to scan for test files
11
+ * @param baseCommit Optional base commit to compare against
12
+ */
13
+ export async function runChangedTests(directory, baseCommit) {
14
+ const changedTests = await findChangedTestFiles(directory, baseCommit);
15
+ if (changedTests.length === 0) {
16
+ console.log("No tests affected by recent changes.");
17
+ return;
18
+ }
19
+ // Run the tests with bun using spawn for stdio inheritance
20
+ return new Promise((resolve, reject) => {
21
+ console.log(`bun test ${changedTests.join(" ")}`);
22
+ // resolve();
23
+ const child = spawn("bun", ["test", ...changedTests], { stdio: "inherit" });
24
+ child.on("close", (code) => {
25
+ if (code === 0)
26
+ resolve();
27
+ else
28
+ reject(new Error(`Tests exited with code ${code}`));
29
+ });
30
+ child.on("error", reject);
31
+ });
32
+ }
33
+ /**
34
+ * Identifies test files that have changed or depend on changed files
35
+ * @param directory Directory to scan for test files
36
+ * @param baseCommit Optional base commit to compare against (defaults to HEAD~1)
37
+ * @returns Array of changed test files
38
+ */
39
+ export async function findChangedTestFiles(directory, baseCommit = "HEAD~1") {
40
+ // 1. Find all test files
41
+ const testFiles = await findTestFiles(directory);
42
+ // 2. Get git changed files
43
+ const changedFiles = await getChangedFiles(baseCommit);
44
+ // 3. Process each test file
45
+ const changedTestFiles = [];
46
+ for (const testFile of testFiles) {
47
+ // Get dependency tree for the test file
48
+ const dependencies = await getDependencies(testFile);
49
+ // Check if any dependencies have changed
50
+ const changedDependencies = [...dependencies].filter((dep) => changedFiles.has(dep));
51
+ if (changedDependencies.length > 0) {
52
+ changedTestFiles.push(testFile);
53
+ }
54
+ }
55
+ return changedTestFiles.map((p) => path.relative(process.cwd(), p));
56
+ }
57
+ /**
58
+ * Finds all test files in a directory
59
+ * @param directory Directory to scan
60
+ * @returns Array of test file paths
61
+ */
62
+ async function findTestFiles(directory) {
63
+ return await glob(path.join(directory, "**/*.test.ts"));
64
+ }
65
+ /**
66
+ * Gets list of files changed since a base commit
67
+ * @param baseCommit Base commit to compare against
68
+ * @returns Set of changed file paths
69
+ */
70
+ async function getChangedFiles(baseCommit) {
71
+ // Get changed files from git
72
+ const { stdout: gitOutput } = await execAsync(`git diff --name-only ${baseCommit}`);
73
+ // Convert to set of absolute paths
74
+ const changedFiles = new Set();
75
+ const { stdout: workDir } = await execAsync("git rev-parse --show-toplevel");
76
+ const rootDir = workDir.trim();
77
+ gitOutput
78
+ .split("\n")
79
+ .filter((line) => line.trim().length > 0)
80
+ .forEach((file) => {
81
+ changedFiles.add(path.resolve(rootDir, file));
82
+ });
83
+ return changedFiles;
84
+ }
85
+ /**
86
+ * Gets dependency tree for a test file
87
+ * @param testFile Path to test file
88
+ * @returns Set of dependency file paths
89
+ */
90
+ async function getDependencies(testFile) {
91
+ const dependencies = new Set();
92
+ // Add the test file itself to dependencies
93
+ const absTestPath = path.resolve(testFile);
94
+ dependencies.add(absTestPath);
95
+ try {
96
+ // Use esbuild with metafile to get dependencies
97
+ const result = await esbuild.build({
98
+ entryPoints: [testFile],
99
+ write: false,
100
+ platform: "node",
101
+ format: "esm",
102
+ bundle: true,
103
+ metafile: true,
104
+ external: ["@cloudflare/workers-types", "bun:test"],
105
+ });
106
+ if (!result.metafile) {
107
+ return dependencies;
108
+ }
109
+ // Function to normalize paths for comparison
110
+ const normalizePath = (p) => path.resolve(p).replace(/\\/g, "/");
111
+ // Process imports from the metafile
112
+ const { inputs } = result.metafile;
113
+ for (const filePath in inputs) {
114
+ // Skip node_modules and non-local imports
115
+ if (filePath.includes("node_modules") ||
116
+ filePath.includes("bun:") ||
117
+ !filePath.endsWith(".ts")) {
118
+ continue;
119
+ }
120
+ const absolutePath = normalizePath(filePath);
121
+ dependencies.add(absolutePath);
122
+ }
123
+ return dependencies;
124
+ }
125
+ catch (error) {
126
+ console.error(`Error analyzing dependencies for ${testFile}:`, error);
127
+ // Return at least the test file itself
128
+ return dependencies;
129
+ }
130
+ }
@@ -1,4 +1,4 @@
1
- import path from "path";
1
+ import path from "node:path";
2
2
  // Common MIME types
3
3
  const mimeTypes = {
4
4
  ".html": "text/html",
package/lib/util/rm.js CHANGED
@@ -1,4 +1,4 @@
1
- import fs from "fs/promises";
1
+ import fs from "node:fs/promises";
2
2
  export async function rm(path) {
3
3
  try {
4
4
  await fs.rm(path, { recursive: true, force: true });
package/lib/web/astro.js CHANGED
@@ -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 { ShadcnUI } from "./shadcn";
8
11
  const execAsync = promisify(exec);
@@ -1,6 +1,6 @@
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
5
  import { Resource } from "../resource";
6
6
  import { ignore } from "../util/ignore";
@@ -1,5 +1,5 @@
1
1
  import type { Context } from "../context";
2
- import { Folder } from "../fs";
2
+ import { Folder } from "../fs/folder";
3
3
  import { Resource } from "../resource";
4
4
  /**
5
5
  * Properties for initializing Shadcn UI
package/lib/web/shadcn.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { exec } from "child_process";
2
- import path from "path";
2
+ import path from "node:path";
3
3
  import { promisify } from "util";
4
- import { Folder, StaticJsonFile } from "../fs";
4
+ import { Folder } from "../fs/folder";
5
+ import { StaticJsonFile } from "../fs/static-json-file";
5
6
  import { StaticTypeScriptFile } from "../fs/static-typescript-file";
6
7
  import { Resource } from "../resource";
7
8
  import { ShadcnComponent } from "./shadcn-component";
@@ -1,7 +1,8 @@
1
1
  import { exec } from "child_process";
2
- import path from "path";
2
+ import path from "node:path";
3
3
  import { promisify } from "util";
4
- import { Folder, StaticTextFile } from "../fs";
4
+ import { Folder } from "../fs/folder";
5
+ import { StaticTextFile } from "../fs/static-text-file";
5
6
  import { Resource } from "../resource";
6
7
  const execAsync = promisify(exec);
7
8
  /**
package/lib/web/vite.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { exec } from "child_process";
2
- import path from "path";
2
+ import path from "node:path";
3
3
  import { promisify } from "util";
4
- import { Folder, StaticJsonFile, StaticTypeScriptFile } from "../fs";
4
+ import { Folder } from "../fs/folder";
5
+ import { StaticJsonFile } from "../fs/static-json-file";
6
+ import { StaticTypeScriptFile } from "../fs/static-typescript-file";
5
7
  import { Resource } from "../resource";
6
8
  import { rm } from "../util/rm";
7
9
  import { ShadcnComponent } from "./shadcn-component";
@@ -74,7 +76,7 @@ export const ViteProject = Resource("project::ViteProject", {
74
76
  cwd: props.name,
75
77
  framework: "vite",
76
78
  });
77
- await StaticTypeScriptFile(path.join(props.name, "vite.config.ts"), `import path from "path";
79
+ await StaticTypeScriptFile(path.join(props.name, "vite.config.ts"), `import path from "node:path";
78
80
  import react from "@vitejs/plugin-react";
79
81
  import { defineConfig } from "vite";
80
82
  ${tailwind ? "import tailwindcss from '@tailwindcss/vite';" : ""}
@@ -1,4 +1,4 @@
1
- import path from "path";
1
+ import path from "node:path";
2
2
  import { StaticTypeScriptFile } from "../../fs/static-typescript-file";
3
3
  export function VitePressConfig(props) {
4
4
  return StaticTypeScriptFile(path.join(props.cwd, ".vitepress", "config.mts"), `import { transformerTwoslash } from "@shikijs/vitepress-twoslash";