@base44-preview/vite-plugin 0.1.0-dev.c16b316 → 0.1.0-dev.e434ee3

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/src/index.ts CHANGED
@@ -1,82 +1,139 @@
1
- import type { Plugin } from "vite";
1
+ import type { Plugin, UserConfig } from "vite";
2
2
  import { loadEnv } from "vite";
3
+ import { errorOverlayPlugin } from "./error-overlay-plugin.js";
4
+ import { visualEditPlugin } from "./visual-edit-plugin.js";
3
5
 
4
- export default function vitePlugin() {
5
- return {
6
- name: "base44",
7
- config: ({ mode }) => {
8
- const env = loadEnv(mode ?? "development", process.cwd(), "");
6
+ const isRunningInSandbox = !!process.env.MODAL_SANDBOX_ID;
9
7
 
10
- return {
11
- resolve: {
12
- alias: {
13
- "@/": "/src/",
14
- },
15
- },
16
- server: {
17
- host: "0.0.0.0", // Bind to all interfaces for container access
18
- port: 5173,
19
- strictPort: true,
20
- // Allow all hosts - essential for Modal tunnel URLs
21
- allowedHosts: true,
22
- watch: {
23
- // Enable polling for better file change detection in containers
24
- usePolling: true,
25
- interval: 100, // Check every 100ms for responsive HMR
26
- },
27
- hmr: {
28
- protocol: "wss",
29
- clientPort: 443,
8
+ export default function vitePlugin(
9
+ opts: {
10
+ legacySDKImports?: boolean;
11
+ } = {}
12
+ ) {
13
+ const { legacySDKImports = false } = opts;
14
+
15
+ return [
16
+ {
17
+ name: "base44",
18
+ config: ({ mode }) => {
19
+ const env = loadEnv(mode ?? "development", process.cwd(), "");
20
+
21
+ return {
22
+ resolve: {
23
+ alias: {
24
+ "@/": "/src/",
25
+ },
30
26
  },
31
- },
32
- optimizeDeps: {
33
- esbuildOptions: {
34
- define: {
35
- "process.env.VITE_BASE44_APP_ID": JSON.stringify(
36
- env.VITE_BASE44_APP_ID
37
- ),
38
- "process.env.VITE_BASE44_BACKEND_URL": JSON.stringify(
39
- env.VITE_BASE44_SERVER_URL
40
- ),
27
+ ...(isRunningInSandbox
28
+ ? ({
29
+ server: {
30
+ host: "0.0.0.0", // Bind to all interfaces for container access
31
+ port: 5173,
32
+ strictPort: true,
33
+ // Allow all hosts - essential for Modal tunnel URLs
34
+ allowedHosts: true,
35
+ watch: {
36
+ // Enable polling for better file change detection in containers
37
+ usePolling: true,
38
+ interval: 100, // Check every 100ms for responsive HMR
39
+ },
40
+ hmr: {
41
+ protocol: "wss",
42
+ clientPort: 443,
43
+ },
44
+ },
45
+ build: {
46
+ rollupOptions: {
47
+ onwarn(warning, warn) {
48
+ // Treat import errors as fatal errors
49
+ if (
50
+ warning.code === "UNRESOLVED_IMPORT" ||
51
+ warning.code === "MISSING_EXPORT"
52
+ ) {
53
+ throw new Error(`Build failed: ${warning.message}`);
54
+ }
55
+ // Use default for other warnings
56
+ warn(warning);
57
+ },
58
+ },
59
+ },
60
+ } as Partial<UserConfig>)
61
+ : {}),
62
+ optimizeDeps: {
63
+ esbuildOptions: {
64
+ loader: {
65
+ ".js": "jsx",
66
+ },
67
+ ...(legacySDKImports
68
+ ? {
69
+ define: {
70
+ "process.env.VITE_BASE44_APP_ID": JSON.stringify(
71
+ env.VITE_BASE44_APP_ID
72
+ ),
73
+ "process.env.VITE_BASE44_BACKEND_URL": JSON.stringify(
74
+ env.VITE_BASE44_BACKEND_URL
75
+ ),
76
+ },
77
+ }
78
+ : {}),
41
79
  },
42
80
  },
43
- },
44
- };
45
- },
46
- configureServer(server) {
47
- server.middlewares.use((req, res, next) => {
48
- // Allow iframe embedding
49
- res.setHeader("X-Frame-Options", "ALLOWALL");
50
- res.setHeader("Content-Security-Policy", "frame-ancestors *;");
51
- next();
52
- });
53
- },
54
- resolveId(source, importer, options) {
55
- if (source.includes("/entities")) {
56
- return this.resolve(
57
- "@base44/vite-plugin/compat/entities.cjs",
58
- importer,
59
- options
60
- );
61
- }
81
+ };
82
+ },
83
+ resolveId(source, importer, options) {
84
+ if (legacySDKImports) {
85
+ if (source.includes("/entities")) {
86
+ return this.resolve(
87
+ "@base44/vite-plugin/compat/entities.cjs",
88
+ importer,
89
+ options
90
+ );
91
+ }
92
+
93
+ if (source.includes("/functions")) {
94
+ return this.resolve(
95
+ "@base44/vite-plugin/compat/functions.cjs",
96
+ importer,
97
+ options
98
+ );
99
+ }
62
100
 
63
- if (source.includes("/functions")) {
64
- return this.resolve(
65
- "@base44/vite-plugin/compat/functions.cjs",
66
- importer,
67
- options
68
- );
69
- }
101
+ if (source.includes("/integrations")) {
102
+ return this.resolve(
103
+ "@base44/vite-plugin/compat/integrations.cjs",
104
+ importer,
105
+ options
106
+ );
107
+ }
70
108
 
71
- if (source.includes("/integrations")) {
72
- return this.resolve(
73
- "@base44/vite-plugin/compat/integrations.cjs",
74
- importer,
75
- options
76
- );
77
- }
109
+ if (source.includes("@/agents")) {
110
+ return this.resolve(
111
+ "@base44/vite-plugin/compat/agents.cjs",
112
+ importer,
113
+ options
114
+ );
115
+ }
116
+ }
78
117
 
79
- return null;
80
- },
81
- } as Plugin;
118
+ return null;
119
+ },
120
+ } as Plugin,
121
+ ...(isRunningInSandbox
122
+ ? [
123
+ {
124
+ name: "iframe-hmr",
125
+ configureServer(server) {
126
+ server.middlewares.use((req, res, next) => {
127
+ // Allow iframe embedding
128
+ res.setHeader("X-Frame-Options", "ALLOWALL");
129
+ res.setHeader("Content-Security-Policy", "frame-ancestors *;");
130
+ next();
131
+ });
132
+ },
133
+ } as Plugin,
134
+ errorOverlayPlugin(),
135
+ visualEditPlugin(),
136
+ ]
137
+ : []),
138
+ ];
82
139
  }
@@ -0,0 +1,268 @@
1
+ import { parse } from "@babel/parser";
2
+ import { default as traverse } from "@babel/traverse";
3
+ import { default as generate } from "@babel/generator";
4
+ import * as t from "@babel/types";
5
+ import type { Plugin } from "vite";
6
+
7
+ // Helper function to check if JSX element contains dynamic content
8
+ function checkIfElementHasDynamicContent(jsxElement: any) {
9
+ let hasDynamicContent = false;
10
+
11
+ // Helper function to check if any node contains dynamic patterns
12
+ function checkNodeForDynamicContent(node: any) {
13
+ // JSX expressions like {variable}, {func()}, {obj.prop}
14
+ if (t.isJSXExpressionContainer(node)) {
15
+ const expression = node.expression;
16
+
17
+ // Skip empty expressions {}
18
+ if (t.isJSXEmptyExpression(expression)) {
19
+ return false;
20
+ }
21
+
22
+ // Any non-literal expression is considered dynamic
23
+ if (!t.isLiteral(expression)) {
24
+ return true;
25
+ }
26
+ }
27
+
28
+ // Template literals with expressions `Hello ${name}`
29
+ if (t.isTemplateLiteral(node) && node.expressions.length > 0) {
30
+ return true;
31
+ }
32
+
33
+ // Member expressions like props.title, state.value
34
+ if (t.isMemberExpression(node)) {
35
+ return true;
36
+ }
37
+
38
+ // Function calls like getData(), format()
39
+ if (t.isCallExpression(node)) {
40
+ return true;
41
+ }
42
+
43
+ // Conditional expressions like condition ? "yes" : "no"
44
+ if (t.isConditionalExpression(node)) {
45
+ return true;
46
+ }
47
+
48
+ // Identifier references (could be props, state, variables)
49
+ if (t.isIdentifier(node)) {
50
+ // Common dynamic identifiers
51
+ const dynamicNames = [
52
+ "props",
53
+ "state",
54
+ "data",
55
+ "item",
56
+ "value",
57
+ "text",
58
+ "content",
59
+ ];
60
+ if (dynamicNames.some((name) => node.name.includes(name))) {
61
+ return true;
62
+ }
63
+ }
64
+
65
+ return false;
66
+ }
67
+
68
+ // Recursively traverse all child nodes
69
+ function traverseNode(node: any) {
70
+ if (checkNodeForDynamicContent(node)) {
71
+ hasDynamicContent = true;
72
+ return;
73
+ }
74
+
75
+ // Recursively check child nodes
76
+ Object.keys(node).forEach((key) => {
77
+ const value = node[key];
78
+
79
+ if (Array.isArray(value)) {
80
+ value.forEach((child) => {
81
+ if (child && typeof child === "object" && child.type) {
82
+ traverseNode(child);
83
+ }
84
+ });
85
+ } else if (value && typeof value === "object" && value.type) {
86
+ traverseNode(value);
87
+ }
88
+ });
89
+ }
90
+
91
+ // Check all children of the JSX element
92
+ jsxElement.children.forEach((child: any) => {
93
+ if (hasDynamicContent) return; // Early exit if already found dynamic content
94
+ traverseNode(child);
95
+ });
96
+
97
+ return hasDynamicContent;
98
+ }
99
+
100
+ export function visualEditPlugin() {
101
+ return {
102
+ name: "visual-edit-transform",
103
+ apply: (config) => config.mode === "development",
104
+ enforce: "pre",
105
+ order: "pre",
106
+ // Inject Tailwind CDN for visual editing capabilities
107
+ transformIndexHtml(html: any) {
108
+ // Inject the Tailwind CSS CDN script right before the closing </head> tag
109
+ const tailwindScript = ` <!-- Tailwind CSS CDN for visual editing -->\n <script src="https://cdn.tailwindcss.com"></script>\n `;
110
+ return html.replace("</head>", tailwindScript + "</head>");
111
+ },
112
+ transform(code: any, id: any) {
113
+ // Skip node_modules and visual-edit-agent itself
114
+ if (id.includes("node_modules") || id.includes("visual-edit-agent")) {
115
+ return null;
116
+ }
117
+
118
+ // Process JS/JSX/TS/TSX files
119
+ if (!id.match(/\.(jsx?|tsx?)$/)) {
120
+ return null;
121
+ }
122
+
123
+ // Extract filename from path, preserving pages/ or components/ structure
124
+ const pathParts = id.split("/");
125
+ let filename;
126
+
127
+ // Check if this is a pages or components file
128
+ if (id.includes("/pages/")) {
129
+ const pagesIndex = pathParts.findIndex((part: any) => part === "pages");
130
+ if (pagesIndex >= 0 && pagesIndex < pathParts.length - 1) {
131
+ // Get all parts from 'pages' to the file, preserving nested structure
132
+ const relevantParts = pathParts.slice(pagesIndex, pathParts.length);
133
+ const lastPart = relevantParts[relevantParts.length - 1];
134
+ // Remove file extension from the last part
135
+ relevantParts[relevantParts.length - 1] = lastPart.includes(".")
136
+ ? lastPart.split(".")[0]
137
+ : lastPart;
138
+ filename = relevantParts.join("/");
139
+ } else {
140
+ filename = pathParts[pathParts.length - 1];
141
+ if (filename.includes(".")) {
142
+ filename = filename.split(".")[0];
143
+ }
144
+ }
145
+ } else if (id.includes("/components/")) {
146
+ const componentsIndex = pathParts.findIndex(
147
+ (part: any) => part === "components"
148
+ );
149
+ if (componentsIndex >= 0 && componentsIndex < pathParts.length - 1) {
150
+ // Get all parts from 'components' to the file, preserving nested structure
151
+ const relevantParts = pathParts.slice(
152
+ componentsIndex,
153
+ pathParts.length
154
+ );
155
+ const lastPart = relevantParts[relevantParts.length - 1];
156
+ // Remove file extension from the last part
157
+ relevantParts[relevantParts.length - 1] = lastPart.includes(".")
158
+ ? lastPart.split(".")[0]
159
+ : lastPart;
160
+ filename = relevantParts.join("/");
161
+ } else {
162
+ filename = pathParts[pathParts.length - 1];
163
+ if (filename.includes(".")) {
164
+ filename = filename.split(".")[0];
165
+ }
166
+ }
167
+ } else {
168
+ // For other files (like layout), just use the filename
169
+ filename = pathParts[pathParts.length - 1];
170
+ if (filename.includes(".")) {
171
+ filename = filename.split(".")[0];
172
+ }
173
+ }
174
+
175
+ try {
176
+ // Parse the code into an AST
177
+ const ast = parse(code, {
178
+ sourceType: "module",
179
+ plugins: [
180
+ "jsx",
181
+ "typescript",
182
+ "decorators-legacy",
183
+ "classProperties",
184
+ "objectRestSpread",
185
+ "functionBind",
186
+ "exportDefaultFrom",
187
+ "exportNamespaceFrom",
188
+ "dynamicImport",
189
+ "nullishCoalescingOperator",
190
+ "optionalChaining",
191
+ "asyncGenerators",
192
+ "bigInt",
193
+ "optionalCatchBinding",
194
+ "throwExpressions",
195
+ ],
196
+ });
197
+
198
+ // Traverse the AST and add source location and dynamic content attributes to JSX elements
199
+ let elementsProcessed = 0;
200
+ traverse.default(ast, {
201
+ JSXElement(path) {
202
+ const jsxElement = path.node;
203
+ const openingElement = jsxElement.openingElement;
204
+
205
+ // Skip fragments
206
+ if (t.isJSXFragment(jsxElement)) return;
207
+
208
+ // Skip if already has source location attribute
209
+ const hasSourceLocation = openingElement.attributes.some(
210
+ (attr) =>
211
+ t.isJSXAttribute(attr) &&
212
+ t.isJSXIdentifier(attr.name) &&
213
+ attr.name.name === "data-source-location"
214
+ );
215
+
216
+ if (hasSourceLocation) return;
217
+
218
+ // Get line and column from AST node location
219
+ const { line, column } = openingElement.loc?.start || {
220
+ line: 1,
221
+ column: 0,
222
+ };
223
+
224
+ // Create the source location attribute
225
+ const sourceLocationAttr = t.jsxAttribute(
226
+ t.jsxIdentifier("data-source-location"),
227
+ t.stringLiteral(`${filename}:${line}:${column}`)
228
+ );
229
+
230
+ // Check if element has dynamic content
231
+ const isDynamic = checkIfElementHasDynamicContent(jsxElement);
232
+
233
+ // Create the dynamic content attribute
234
+ const dynamicContentAttr = t.jsxAttribute(
235
+ t.jsxIdentifier("data-dynamic-content"),
236
+ t.stringLiteral(isDynamic ? "true" : "false")
237
+ );
238
+
239
+ // Add both attributes to the beginning of the attributes array
240
+ openingElement.attributes.unshift(
241
+ sourceLocationAttr,
242
+ dynamicContentAttr
243
+ );
244
+ elementsProcessed++;
245
+ },
246
+ });
247
+
248
+ // Generate the code back from the AST
249
+ const result = generate.default(ast, {
250
+ compact: false,
251
+ concise: false,
252
+ retainLines: true,
253
+ });
254
+
255
+ return {
256
+ code: result.code,
257
+ map: null,
258
+ };
259
+ } catch (error) {
260
+ console.error("Failed to add source location to JSX:", error);
261
+ return {
262
+ code: code, // Return original code on failure
263
+ map: null,
264
+ };
265
+ }
266
+ },
267
+ } as Plugin;
268
+ }
@@ -1,78 +0,0 @@
1
- name: Claude Code Review
2
-
3
- on:
4
- pull_request:
5
- types: [opened, synchronize]
6
- # Optional: Only run on specific file changes
7
- # paths:
8
- # - "src/**/*.ts"
9
- # - "src/**/*.tsx"
10
- # - "src/**/*.js"
11
- # - "src/**/*.jsx"
12
-
13
- jobs:
14
- claude-review:
15
- if: |
16
- github.actor != 'github-actions[bot]' &&
17
- github.actor != 'claude-code[bot]' &&
18
- github.actor != 'claude[bot]' &&
19
- github.actor != 'claude' &&
20
- github.event.pull_request.head.commit.parents[1] == null
21
-
22
- runs-on: ubuntu-latest
23
- permissions:
24
- contents: read
25
- pull-requests: read
26
- issues: read
27
- id-token: write
28
-
29
- steps:
30
- - name: Checkout repository
31
- uses: actions/checkout@v4
32
- with:
33
- fetch-depth: 1
34
-
35
- - name: Run Claude Code Review
36
- id: claude-review
37
- uses: anthropics/claude-code-action@beta
38
- with:
39
- anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
40
-
41
- # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
42
- model: "claude-opus-4-20250514"
43
-
44
- # Direct prompt for automated review (no @claude mention needed)
45
- direct_prompt: |
46
- Please review this pull request and provide feedback on:
47
- - Code quality and best practices
48
- - Potential bugs or issues
49
- - Performance considerations
50
- - Security concerns
51
- - Test coverage
52
-
53
- Be constructive and helpful in your feedback.
54
-
55
- # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR
56
- # use_sticky_comment: true
57
-
58
- # Optional: Customize review based on file types
59
- # direct_prompt: |
60
- # Review this PR focusing on:
61
- # - For TypeScript files: Type safety and proper interface usage
62
- # - For API endpoints: Security, input validation, and error handling
63
- # - For React components: Performance, accessibility, and best practices
64
- # - For tests: Coverage, edge cases, and test quality
65
-
66
- # Optional: Different prompts for different authors
67
- # direct_prompt: |
68
- # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' &&
69
- # 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' ||
70
- # 'Please provide a thorough code review focusing on our coding standards and best practices.' }}
71
-
72
- # Optional: Add specific tools for running tests or linting
73
- # allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)"
74
-
75
- # Optional: Skip review for certain conditions
76
- # if: |
77
- # !contains(github.event.pull_request.title, '[skip-review]') &&
78
- # !contains(github.event.pull_request.title, '[WIP]')
@@ -1,63 +0,0 @@
1
- name: Claude Code
2
-
3
- on:
4
- issue_comment:
5
- types: [created]
6
- pull_request_review_comment:
7
- types: [created]
8
- issues:
9
- types: [opened, assigned]
10
- pull_request_review:
11
- types: [submitted]
12
-
13
- jobs:
14
- claude:
15
- if: |
16
- (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
17
- (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
18
- (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
19
- (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
20
- runs-on: ubuntu-latest
21
- permissions:
22
- contents: read
23
- pull-requests: read
24
- issues: read
25
- id-token: write
26
- actions: read # Required for Claude to read CI results on PRs
27
- steps:
28
- - name: Checkout repository
29
- uses: actions/checkout@v4
30
- with:
31
- fetch-depth: 1
32
-
33
- - name: Run Claude Code
34
- id: claude
35
- uses: anthropics/claude-code-action@beta
36
- with:
37
- anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
38
-
39
- # This is an optional setting that allows Claude to read CI results on PRs
40
- additional_permissions: |
41
- actions: read
42
-
43
- # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
44
- model: "claude-opus-4-20250514"
45
-
46
- # Optional: Customize the trigger phrase (default: @claude)
47
- # trigger_phrase: "/claude"
48
-
49
- # Optional: Trigger when specific user is assigned to an issue
50
- # assignee_trigger: "claude-bot"
51
-
52
- # Optional: Allow Claude to run specific commands
53
- allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
54
-
55
- # Optional: Add custom instructions for Claude to customize its behavior for your project
56
- # custom_instructions: |
57
- # Follow our coding standards
58
- # Ensure all new code has tests
59
- # Use TypeScript for new files
60
-
61
- # Optional: Custom environment variables for Claude
62
- # claude_env: |
63
- # NODE_ENV: test