exportc 0.0.3 → 0.0.5

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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # exportc
2
2
 
3
- Add [export](https://github.com/ihasq/export) to existing Vite projects.
3
+ Add [export](https://github.com/ihasq/export) to existing Vite projects. One command sets up server-side functions with full TypeScript support.
4
4
 
5
5
  ## Quick Start
6
6
 
@@ -18,13 +18,20 @@ npm run dev
18
18
  npm run export
19
19
  ```
20
20
 
21
+ ## What You Get
22
+
23
+ - **Single command dev** -- `npm run dev` starts both Vite and Wrangler
24
+ - **Auto-generated types** -- TypeScript definitions from your actual code
25
+ - **Workers Sites deploy** -- Static assets + server exports in one deployment
26
+ - **Zero config** -- Production URL auto-detected from package name
27
+
21
28
  ## Usage
22
29
 
23
- After initialization, import your server exports using the `export:/` prefix:
30
+ After initialization, import your server exports using the `export/` prefix:
24
31
 
25
32
  ```typescript
26
33
  // In your Vite app
27
- import { hello, Counter } from "export:/";
34
+ import { hello, Counter } from "export/";
28
35
 
29
36
  const message = await hello("World"); // "Hello, World!"
30
37
 
@@ -34,24 +41,17 @@ await counter.increment(); // 1
34
41
 
35
42
  ## Commands
36
43
 
37
- ### `exportc init`
38
-
39
- Initialize export in your Vite project:
40
- - Creates `export/` directory with example server code
41
- - Updates `vite.config.ts` with the export plugin
42
- - Adds npm scripts for development and deployment
43
-
44
- ### `exportc dev`
45
-
46
- Start the Wrangler development server for your exports.
47
-
48
- ### `exportc deploy`
49
-
50
- Deploy your exports to Cloudflare Workers.
44
+ | Command | Description |
45
+ |---------|-------------|
46
+ | `npm run dev` | Start Vite + Wrangler together, auto-generate types |
47
+ | `npm run export` | Build Vite app and deploy to Workers Sites |
48
+ | `exportc init` | Initialize export in your project |
49
+ | `exportc dev` | Start Wrangler dev server standalone |
50
+ | `exportc deploy` | Deploy exports only |
51
51
 
52
52
  ## Vite Plugin
53
53
 
54
- The `exportPlugin` automatically starts Wrangler and transforms `export:/` imports:
54
+ The `exportPlugin` handles everything automatically:
55
55
 
56
56
  ```typescript
57
57
  // vite.config.ts
@@ -67,15 +67,20 @@ export default defineConfig({
67
67
  });
68
68
  ```
69
69
 
70
- **Development** (`npm run dev`):
71
- 1. Automatically starts Wrangler dev server
72
- 2. Waits for it to be ready
73
- 3. Transforms `export:/` imports to `http://localhost:8787`
70
+ ### Development (`npm run dev`)
71
+
72
+ 1. Automatically starts Wrangler dev server in the background
73
+ 2. Waits for it to be ready before serving your app
74
+ 3. Generates `export-env.d.ts` with TypeScript declarations
75
+ 4. Watches for changes and regenerates types automatically
76
+ 5. Transforms `export/` imports to `http://localhost:8787`
74
77
 
75
- **Production** (`npm run export`):
76
- 1. Builds Vite app
78
+ ### Production (`npm run export`)
79
+
80
+ 1. Builds your Vite app
77
81
  2. Deploys to Workers Sites (static assets + server exports)
78
- 3. `export:/` imports resolve to `https://{worker-name}.workers.dev`
82
+ 3. `export/` imports resolve to `https://{worker-name}.workers.dev`
83
+ 4. Everything runs on Cloudflare's edge network
79
84
 
80
85
  ## Project Structure
81
86
 
@@ -83,30 +88,37 @@ After running `exportc init`:
83
88
 
84
89
  ```
85
90
  my-vite-app/
86
- ├── src/ # Your Vite app
91
+ ├── src/ # Your Vite app (unchanged)
87
92
  ├── export/ # Server exports (Cloudflare Worker)
88
93
  │ ├── index.ts # Your server code
89
- └── package.json # Worker configuration
90
- ├── export-env.d.ts # TypeScript declarations
94
+ ├── package.json # Worker configuration
95
+ │ └── .gitignore # Generated files excluded
96
+ ├── export-env.d.ts # TypeScript declarations (auto-generated)
91
97
  └── vite.config.ts # Updated with exportPlugin
92
98
  ```
93
99
 
94
100
  ## TypeScript Support
95
101
 
96
- The `export-env.d.ts` file provides type declarations for `export:/` imports. Update it when you add new exports:
102
+ The `export-env.d.ts` file is **automatically generated** when you run `npm run dev`. The Vite plugin watches for changes to your export files and regenerates type declarations automatically.
97
103
 
98
104
  ```typescript
99
- // export-env.d.ts
100
- declare module "export:/" {
105
+ // export-env.d.ts (auto-generated)
106
+ declare module "export/" {
101
107
  export function hello(name: string): Promise<string>;
102
- export function myNewFunction(): Promise<void>;
108
+ export class Counter {
109
+ constructor(initial?: number);
110
+ increment(): Promise<number>;
111
+ [Symbol.dispose](): Promise<void>;
112
+ }
103
113
  }
104
114
 
105
- declare module "export:/utils" {
115
+ declare module "export/utils" {
106
116
  export function formatDate(date: Date): Promise<string>;
107
117
  }
108
118
  ```
109
119
 
120
+ Types are inferred from your actual export code, so you get accurate type information with zero manual maintenance.
121
+
110
122
  ## License
111
123
 
112
124
  MIT
package/commands/dev.js CHANGED
@@ -45,7 +45,7 @@ export async function dev(argv) {
45
45
  `Wrangler dev server is running at ${pc.cyan("http://localhost:8787")}
46
46
 
47
47
  ${pc.bold("In your Vite app:")}
48
- ${pc.cyan(`import { hello } from "export:/";`)}
48
+ ${pc.cyan(`import { hello } from "export/";`)}
49
49
  ${pc.cyan(`const message = await hello("World");`)}
50
50
 
51
51
  ${pc.dim("Press Ctrl+C to stop")}`,
package/commands/init.js CHANGED
@@ -8,14 +8,21 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
8
 
9
9
  export async function init(argv) {
10
10
  const cwd = process.cwd();
11
+ const isNonInteractive = argv.yes || argv.y || process.env.CI === "true";
11
12
 
12
- p.intro(pc.bgCyan(pc.black(" exportc init ")));
13
+ if (!isNonInteractive) {
14
+ p.intro(pc.bgCyan(pc.black(" exportc init ")));
15
+ }
13
16
 
14
17
  // Check for Vite project
15
18
  const viteConfigFiles = ["vite.config.ts", "vite.config.js", "vite.config.mts", "vite.config.mjs"];
16
19
  const viteConfig = viteConfigFiles.find((f) => fs.existsSync(path.join(cwd, f)));
17
20
 
18
21
  if (!viteConfig) {
22
+ if (isNonInteractive) {
23
+ console.error("Error: No Vite config found. exportc currently only supports Vite projects.");
24
+ process.exit(1);
25
+ }
19
26
  p.cancel("No Vite config found. exportc currently only supports Vite projects.");
20
27
  process.exit(1);
21
28
  }
@@ -23,6 +30,10 @@ export async function init(argv) {
23
30
  // Check for package.json
24
31
  const pkgPath = path.join(cwd, "package.json");
25
32
  if (!fs.existsSync(pkgPath)) {
33
+ if (isNonInteractive) {
34
+ console.error("Error: No package.json found.");
35
+ process.exit(1);
36
+ }
26
37
  p.cancel("No package.json found.");
27
38
  process.exit(1);
28
39
  }
@@ -31,7 +42,7 @@ export async function init(argv) {
31
42
 
32
43
  // Check if already initialized
33
44
  const exportDir = path.join(cwd, "export");
34
- if (fs.existsSync(exportDir)) {
45
+ if (fs.existsSync(exportDir) && !isNonInteractive) {
35
46
  const overwrite = await p.confirm({
36
47
  message: "export/ directory already exists. Continue and overwrite?",
37
48
  initialValue: false,
@@ -42,28 +53,40 @@ export async function init(argv) {
42
53
  }
43
54
  }
44
55
 
45
- // Get worker name
46
- const workerName = await p.text({
47
- message: "Worker name:",
48
- placeholder: pkg.name ? `${pkg.name}-api` : "my-api",
49
- defaultValue: pkg.name ? `${pkg.name}-api` : "my-api",
50
- validate: (v) => {
51
- if (!v) return "Worker name is required";
52
- if (!/^[a-z0-9-]+$/.test(v)) return "Use lowercase letters, numbers, and hyphens only";
53
- },
54
- });
56
+ // Get worker name (use default in non-interactive mode)
57
+ const defaultWorkerName = pkg.name ? `${pkg.name}-api` : "my-api";
58
+ let workerName;
59
+
60
+ if (isNonInteractive) {
61
+ workerName = argv.name || defaultWorkerName;
62
+ } else {
63
+ workerName = await p.text({
64
+ message: "Worker name:",
65
+ placeholder: defaultWorkerName,
66
+ defaultValue: defaultWorkerName,
67
+ validate: (v) => {
68
+ if (!v) return "Worker name is required";
69
+ if (!/^[a-z0-9-]+$/.test(v)) return "Use lowercase letters, numbers, and hyphens only";
70
+ },
71
+ });
55
72
 
56
- if (p.isCancel(workerName)) {
57
- p.cancel("Operation cancelled.");
58
- process.exit(0);
73
+ if (p.isCancel(workerName)) {
74
+ p.cancel("Operation cancelled.");
75
+ process.exit(0);
76
+ }
59
77
  }
60
78
 
61
79
  // Check for TypeScript
62
80
  const isTypeScript = viteConfig.endsWith(".ts") || viteConfig.endsWith(".mts") ||
63
81
  fs.existsSync(path.join(cwd, "tsconfig.json"));
64
82
 
65
- const s = p.spinner();
66
- s.start("Initializing export...");
83
+ let s;
84
+ if (!isNonInteractive) {
85
+ s = p.spinner();
86
+ s.start("Initializing export...");
87
+ } else {
88
+ console.log("Initializing export...");
89
+ }
67
90
 
68
91
  // Create export directory
69
92
  if (!fs.existsSync(exportDir)) {
@@ -76,7 +99,7 @@ export async function init(argv) {
76
99
  if (!fs.existsSync(indexPath)) {
77
100
  const template = isTypeScript
78
101
  ? `// Server-side exports - these will be available to your client code
79
- // Import from "export:/" in your client code
102
+ // Import from "export/" in your client code
80
103
 
81
104
  export async function hello(name: string): Promise<string> {
82
105
  return \`Hello, \${name}!\`;
@@ -103,7 +126,7 @@ export class Counter {
103
126
  }
104
127
  `
105
128
  : `// Server-side exports - these will be available to your client code
106
- // Import from "export:/" in your client code
129
+ // Import from "export/" in your client code
107
130
 
108
131
  export async function hello(name) {
109
132
  return \`Hello, \${name}!\`;
@@ -237,13 +260,13 @@ wrangler.toml
237
260
  if (isTypeScript) {
238
261
  const envDtsPath = path.join(cwd, "export-env.d.ts");
239
262
  if (!fs.existsSync(envDtsPath)) {
240
- const envDtsContent = `// Type declarations for export:/ imports
263
+ const envDtsContent = `// Type declarations for export/ imports
241
264
  // This file is auto-generated by exportc. You can modify it to add custom types.
242
265
 
243
266
  // Re-export types from your export directory
244
267
  // Update this when you add new exports
245
268
 
246
- declare module "export:/" {
269
+ declare module "export/" {
247
270
  export function hello(name: string): Promise<string>;
248
271
  export function add(a: number, b: number): Promise<number>;
249
272
  export class Counter {
@@ -255,25 +278,29 @@ declare module "export:/" {
255
278
  }
256
279
 
257
280
  // Add more module declarations for subpaths:
258
- // declare module "export:/utils" {
281
+ // declare module "export/utils" {
259
282
  // export function myUtil(): Promise<void>;
260
283
  // }
261
284
  `;
262
285
  fs.writeFileSync(envDtsPath, envDtsContent);
263
286
  }
264
287
 
265
- // Update tsconfig.json to include the type declarations
288
+ // Update tsconfig to include the type declarations
289
+ // Modern Vite uses tsconfig.app.json for app code, older uses tsconfig.json directly
290
+ const tsconfigAppPath = path.join(cwd, "tsconfig.app.json");
266
291
  const tsconfigPath = path.join(cwd, "tsconfig.json");
267
- if (fs.existsSync(tsconfigPath)) {
292
+ const targetTsconfig = fs.existsSync(tsconfigAppPath) ? tsconfigAppPath : tsconfigPath;
293
+
294
+ if (fs.existsSync(targetTsconfig)) {
268
295
  try {
269
- const tsconfigContent = fs.readFileSync(tsconfigPath, "utf8");
296
+ const tsconfigContent = fs.readFileSync(targetTsconfig, "utf8");
270
297
  const tsconfig = JSON.parse(tsconfigContent);
271
298
 
272
299
  // Add export-env.d.ts to include if not already present
273
300
  tsconfig.include = tsconfig.include || [];
274
301
  if (!tsconfig.include.includes("export-env.d.ts")) {
275
302
  tsconfig.include.push("export-env.d.ts");
276
- fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
303
+ fs.writeFileSync(targetTsconfig, JSON.stringify(tsconfig, null, 2) + "\n");
277
304
  }
278
305
  } catch {
279
306
  // Ignore JSON parse errors (might have comments)
@@ -281,22 +308,23 @@ declare module "export:/" {
281
308
  }
282
309
  }
283
310
 
284
- s.stop("Export initialized!");
311
+ if (!isNonInteractive) {
312
+ s.stop("Export initialized!");
285
313
 
286
- const filesCreated = isTypeScript
287
- ? `${pc.cyan("export/")}
314
+ const filesCreated = isTypeScript
315
+ ? `${pc.cyan("export/")}
288
316
  ├── index.${ext} ${pc.dim("# Your server exports")}
289
317
  ├── package.json ${pc.dim("# Worker configuration")}
290
318
  └── .gitignore
291
319
 
292
- ${pc.cyan("export-env.d.ts")} ${pc.dim("# Type declarations for export:/ imports")}`
293
- : `${pc.cyan("export/")}
320
+ ${pc.cyan("export-env.d.ts")} ${pc.dim("# Type declarations for export/ imports")}`
321
+ : `${pc.cyan("export/")}
294
322
  ├── index.${ext} ${pc.dim("# Your server exports")}
295
323
  ├── package.json ${pc.dim("# Worker configuration")}
296
324
  └── .gitignore`;
297
325
 
298
- p.note(
299
- `${filesCreated}
326
+ p.note(
327
+ `${filesCreated}
300
328
 
301
329
  ${pc.bold("Next steps:")}
302
330
 
@@ -307,14 +335,20 @@ ${pc.bold("Next steps:")}
307
335
  ${pc.cyan("npm run dev")}
308
336
 
309
337
  3. Import in your client code:
310
- ${pc.cyan(`import { hello } from "export:/";`)}
338
+ ${pc.cyan(`import { hello } from "export/";`)}
311
339
  ${pc.cyan(`const message = await hello("World");`)}
312
340
 
313
341
  4. Deploy to Cloudflare Workers Sites:
314
342
  ${pc.cyan("npm run export")}
315
343
  ${pc.dim("# Builds Vite + deploys everything to Workers")}`,
316
- "Created"
317
- );
318
-
319
- p.outro(`Run ${pc.cyan("cd export && npm install")} to get started!`);
344
+ "Created"
345
+ );
346
+
347
+ p.outro(`Run ${pc.cyan("cd export && npm install")} to get started!`);
348
+ } else {
349
+ console.log("Export initialized successfully!");
350
+ console.log("\nNext steps:");
351
+ console.log(" cd export && npm install && cd ..");
352
+ console.log(" npm run dev");
353
+ }
320
354
  }
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "exportc",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "CLI to add export to existing projects",
5
5
  "scripts": {
6
- "test": "node --test test/*.test.mjs"
6
+ "test": "node --test test/init.test.mjs",
7
+ "test:e2e": "node --test test/e2e.test.mjs"
7
8
  },
8
9
  "keywords": [
9
10
  "cloudflare",
package/vite-plugin.d.ts CHANGED
@@ -37,11 +37,11 @@ export interface ExportPluginOptions {
37
37
  * Vite plugin for export integration
38
38
  *
39
39
  * Automatically starts Wrangler when you run `npm run dev` and allows
40
- * importing server exports using the "export:/" prefix:
40
+ * importing server exports using the "export/" prefix:
41
41
  *
42
42
  * ```ts
43
- * import { hello } from "export:/";
44
- * import { utils } from "export:/utils";
43
+ * import { hello } from "export/";
44
+ * import { utils } from "export/utils";
45
45
  * ```
46
46
  *
47
47
  * @example
package/vite-plugin.js CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Vite plugin for export integration
3
3
  *
4
- * Allows importing server exports using the "export:/" prefix:
5
- * import { hello } from "export:/";
6
- * import { utils } from "export:/utils";
4
+ * Allows importing server exports using the "export/" prefix:
5
+ * import { hello } from "export/";
6
+ * import { utils } from "export/utils";
7
7
  *
8
8
  * In development:
9
9
  * - Automatically starts Wrangler dev server
@@ -14,10 +14,10 @@
14
14
  */
15
15
 
16
16
  import { spawn } from "node:child_process";
17
- import { existsSync, readFileSync } from "node:fs";
17
+ import { existsSync, readFileSync, writeFileSync, watchFile, unwatchFile } from "node:fs";
18
18
  import { resolve } from "node:path";
19
19
 
20
- const EXPORT_PREFIX = "export:";
20
+ const EXPORT_PREFIX = "export/";
21
21
  const DEFAULT_DEV_PORT = 8787;
22
22
 
23
23
  /**
@@ -55,6 +55,44 @@ export function exportPlugin(options = {}) {
55
55
  return null;
56
56
  };
57
57
 
58
+ // Generate export-env.d.ts from .export-types.js
59
+ const generateTypeDeclarations = (root) => {
60
+ try {
61
+ const typesFilePath = resolve(root, exportDir, ".export-types.js");
62
+ if (!existsSync(typesFilePath)) return;
63
+
64
+ const typesContent = readFileSync(typesFilePath, "utf8");
65
+ // Extract the first line which contains the default export
66
+ const firstLine = typesContent.split('\n')[0];
67
+ if (!firstLine.startsWith('export default ')) return;
68
+
69
+ // Remove "export default " prefix and trailing ";"
70
+ const jsonStr = firstLine.slice('export default '.length).replace(/;$/, '');
71
+
72
+ // Parse the types map
73
+ const typesMap = JSON.parse(jsonStr);
74
+
75
+ // Generate declare module statements
76
+ const declarations = ['// Auto-generated by exportc. Do not edit manually.', '// Re-run "npm run dev" to regenerate after changing export/ files.', ''];
77
+
78
+ for (const [route, types] of Object.entries(typesMap)) {
79
+ const modulePath = route === "" ? "export/" : `export/${route}`;
80
+ declarations.push(`declare module "${modulePath}" {`);
81
+ // Indent the type declarations
82
+ const indented = types.split('\n').map(line => line ? ` ${line}` : '').join('\n');
83
+ declarations.push(indented);
84
+ declarations.push('}');
85
+ declarations.push('');
86
+ }
87
+
88
+ const envDtsPath = resolve(root, "export-env.d.ts");
89
+ writeFileSync(envDtsPath, declarations.join('\n'));
90
+ console.log(`[exportc] Generated type declarations: export-env.d.ts`);
91
+ } catch (err) {
92
+ console.warn(`[exportc] Failed to generate type declarations:`, err.message);
93
+ }
94
+ };
95
+
58
96
  const startWrangler = (root) => {
59
97
  const exportPath = resolve(root, exportDir);
60
98
 
@@ -164,8 +202,27 @@ export function exportPlugin(options = {}) {
164
202
  const root = server.config.root || process.cwd();
165
203
  wranglerReadyPromise = startWrangler(root);
166
204
 
205
+ // Generate types after Wrangler is ready
206
+ wranglerReadyPromise.then(() => {
207
+ // Initial generation
208
+ setTimeout(() => generateTypeDeclarations(root), 1000);
209
+
210
+ // Watch for changes to .export-types.js
211
+ const typesFilePath = resolve(root, exportDir, ".export-types.js");
212
+ if (existsSync(typesFilePath)) {
213
+ watchFile(typesFilePath, { interval: 1000 }, () => {
214
+ generateTypeDeclarations(root);
215
+ });
216
+ }
217
+ });
218
+
167
219
  // Cleanup on server close
168
- server.httpServer?.on("close", stopWrangler);
220
+ server.httpServer?.on("close", () => {
221
+ stopWrangler();
222
+ // Stop watching
223
+ const typesFilePath = resolve(root, exportDir, ".export-types.js");
224
+ unwatchFile(typesFilePath);
225
+ });
169
226
  }
170
227
  },
171
228
 
@@ -233,7 +290,7 @@ export { default } from "${fullUrl}";
233
290
  return null;
234
291
  }
235
292
 
236
- if (code.includes('import("export:') || code.includes("import('export:")) {
293
+ if (code.includes('import("export/') || code.includes("import('export/")) {
237
294
  const baseUrl = isDev ? devUrl : prodUrl;
238
295
  if (!baseUrl && !isDev) {
239
296
  this.warn(`[exportc] Production URL not configured for dynamic imports in ${id}`);
@@ -241,7 +298,7 @@ export { default } from "${fullUrl}";
241
298
  }
242
299
 
243
300
  const transformed = code.replace(
244
- /import\((['"])export:([^'"]*)\1\)/g,
301
+ /import\((['"])export\/([^'"]*)\1\)/g,
245
302
  (match, quote, path) => {
246
303
  const fullUrl = new URL(path || "/", baseUrl).href;
247
304
  return `import(${quote}${fullUrl}${quote})`;