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

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,11 +1,21 @@
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";
5
+
6
+ export default function vitePlugin(
7
+ opts: {
8
+ legacySDKImports?: boolean;
9
+ } = {}
10
+ ) {
11
+ const { legacySDKImports = false } = opts;
3
12
 
4
- export default function vitePlugin() {
5
13
  return {
6
14
  name: "base44",
15
+ enforce: "pre",
7
16
  config: ({ mode }) => {
8
17
  const env = loadEnv(mode ?? "development", process.cwd(), "");
18
+ const isRunningInSandbox = env.BASE44_SANDBOX_MODE === "true";
9
19
 
10
20
  return {
11
21
  resolve: {
@@ -13,67 +23,114 @@ export default function vitePlugin() {
13
23
  "@/": "/src/",
14
24
  },
15
25
  },
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,
30
- },
31
- },
26
+ ...(isRunningInSandbox
27
+ ? ({
28
+ plugins: [
29
+ {
30
+ name: "iframe-hmr",
31
+ configureServer(server) {
32
+ server.middlewares.use((req, res, next) => {
33
+ // Allow iframe embedding
34
+ res.setHeader("X-Frame-Options", "ALLOWALL");
35
+ res.setHeader(
36
+ "Content-Security-Policy",
37
+ "frame-ancestors *;"
38
+ );
39
+ next();
40
+ });
41
+ },
42
+ },
43
+ ...(mode === "development"
44
+ ? [errorOverlayPlugin(), visualEditPlugin()]
45
+ : []),
46
+ ],
47
+ server: {
48
+ host: "0.0.0.0", // Bind to all interfaces for container access
49
+ port: 5173,
50
+ strictPort: true,
51
+ // Allow all hosts - essential for Modal tunnel URLs
52
+ allowedHosts: true,
53
+ watch: {
54
+ // Enable polling for better file change detection in containers
55
+ usePolling: true,
56
+ interval: 100, // Check every 100ms for responsive HMR
57
+ },
58
+ hmr: {
59
+ protocol: "wss",
60
+ clientPort: 443,
61
+ },
62
+ },
63
+ build: {
64
+ rollupOptions: {
65
+ onwarn(warning, warn) {
66
+ // Treat import errors as fatal errors
67
+ if (
68
+ warning.code === "UNRESOLVED_IMPORT" ||
69
+ warning.code === "MISSING_EXPORT"
70
+ ) {
71
+ throw new Error(`Build failed: ${warning.message}`);
72
+ }
73
+ // Use default for other warnings
74
+ warn(warning);
75
+ },
76
+ },
77
+ },
78
+ } as Partial<UserConfig>)
79
+ : {}),
32
80
  optimizeDeps: {
33
81
  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
- ),
82
+ loader: {
83
+ ".js": "jsx",
41
84
  },
85
+ ...(legacySDKImports
86
+ ? {
87
+ define: {
88
+ "process.env.VITE_BASE44_APP_ID": JSON.stringify(
89
+ env.VITE_BASE44_APP_ID
90
+ ),
91
+ "process.env.VITE_BASE44_BACKEND_URL": JSON.stringify(
92
+ env.VITE_BASE44_BACKEND_URL
93
+ ),
94
+ },
95
+ }
96
+ : {}),
42
97
  },
43
98
  },
44
99
  };
45
100
  },
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
101
  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
- }
102
+ if (legacySDKImports) {
103
+ if (source.includes("/entities")) {
104
+ return this.resolve(
105
+ "@base44/vite-plugin/compat/entities.cjs",
106
+ importer,
107
+ options
108
+ );
109
+ }
62
110
 
63
- if (source.includes("/functions")) {
64
- return this.resolve(
65
- "@base44/vite-plugin/compat/functions.cjs",
66
- importer,
67
- options
68
- );
69
- }
111
+ if (source.includes("/functions")) {
112
+ return this.resolve(
113
+ "@base44/vite-plugin/compat/functions.cjs",
114
+ importer,
115
+ options
116
+ );
117
+ }
118
+
119
+ if (source.includes("/integrations")) {
120
+ return this.resolve(
121
+ "@base44/vite-plugin/compat/integrations.cjs",
122
+ importer,
123
+ options
124
+ );
125
+ }
70
126
 
71
- if (source.includes("/integrations")) {
72
- return this.resolve(
73
- "@base44/vite-plugin/compat/integrations.cjs",
74
- importer,
75
- options
76
- );
127
+ if (source.includes("@/agents")) {
128
+ return this.resolve(
129
+ "@base44/vite-plugin/compat/agents.cjs",
130
+ importer,
131
+ options
132
+ );
133
+ }
77
134
  }
78
135
 
79
136
  return null;
@@ -0,0 +1,266 @@
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
+
6
+ // Helper function to check if JSX element contains dynamic content
7
+ function checkIfElementHasDynamicContent(jsxElement: any) {
8
+ let hasDynamicContent = false;
9
+
10
+ // Helper function to check if any node contains dynamic patterns
11
+ function checkNodeForDynamicContent(node: any) {
12
+ // JSX expressions like {variable}, {func()}, {obj.prop}
13
+ if (t.isJSXExpressionContainer(node)) {
14
+ const expression = node.expression;
15
+
16
+ // Skip empty expressions {}
17
+ if (t.isJSXEmptyExpression(expression)) {
18
+ return false;
19
+ }
20
+
21
+ // Any non-literal expression is considered dynamic
22
+ if (!t.isLiteral(expression)) {
23
+ return true;
24
+ }
25
+ }
26
+
27
+ // Template literals with expressions `Hello ${name}`
28
+ if (t.isTemplateLiteral(node) && node.expressions.length > 0) {
29
+ return true;
30
+ }
31
+
32
+ // Member expressions like props.title, state.value
33
+ if (t.isMemberExpression(node)) {
34
+ return true;
35
+ }
36
+
37
+ // Function calls like getData(), format()
38
+ if (t.isCallExpression(node)) {
39
+ return true;
40
+ }
41
+
42
+ // Conditional expressions like condition ? "yes" : "no"
43
+ if (t.isConditionalExpression(node)) {
44
+ return true;
45
+ }
46
+
47
+ // Identifier references (could be props, state, variables)
48
+ if (t.isIdentifier(node)) {
49
+ // Common dynamic identifiers
50
+ const dynamicNames = [
51
+ "props",
52
+ "state",
53
+ "data",
54
+ "item",
55
+ "value",
56
+ "text",
57
+ "content",
58
+ ];
59
+ if (dynamicNames.some((name) => node.name.includes(name))) {
60
+ return true;
61
+ }
62
+ }
63
+
64
+ return false;
65
+ }
66
+
67
+ // Recursively traverse all child nodes
68
+ function traverseNode(node: any) {
69
+ if (checkNodeForDynamicContent(node)) {
70
+ hasDynamicContent = true;
71
+ return;
72
+ }
73
+
74
+ // Recursively check child nodes
75
+ Object.keys(node).forEach((key) => {
76
+ const value = node[key];
77
+
78
+ if (Array.isArray(value)) {
79
+ value.forEach((child) => {
80
+ if (child && typeof child === "object" && child.type) {
81
+ traverseNode(child);
82
+ }
83
+ });
84
+ } else if (value && typeof value === "object" && value.type) {
85
+ traverseNode(value);
86
+ }
87
+ });
88
+ }
89
+
90
+ // Check all children of the JSX element
91
+ jsxElement.children.forEach((child: any) => {
92
+ if (hasDynamicContent) return; // Early exit if already found dynamic content
93
+ traverseNode(child);
94
+ });
95
+
96
+ return hasDynamicContent;
97
+ }
98
+
99
+ export function visualEditPlugin() {
100
+ return {
101
+ name: "visual-edit-transform",
102
+ enforce: "pre",
103
+ order: "pre",
104
+ // Inject Tailwind CDN for visual editing capabilities
105
+ transformIndexHtml(html: any) {
106
+ // Inject the Tailwind CSS CDN script right before the closing </head> tag
107
+ const tailwindScript = ` <!-- Tailwind CSS CDN for visual editing -->\n <script src="https://cdn.tailwindcss.com"></script>\n `;
108
+ return html.replace("</head>", tailwindScript + "</head>");
109
+ },
110
+ transform(code: any, id: any) {
111
+ // Skip node_modules and visual-edit-agent itself
112
+ if (id.includes("node_modules") || id.includes("visual-edit-agent")) {
113
+ return null;
114
+ }
115
+
116
+ // Process JS/JSX/TS/TSX files
117
+ if (!id.match(/\.(jsx?|tsx?)$/)) {
118
+ return null;
119
+ }
120
+
121
+ // Extract filename from path, preserving pages/ or components/ structure
122
+ const pathParts = id.split("/");
123
+ let filename;
124
+
125
+ // Check if this is a pages or components file
126
+ if (id.includes("/pages/")) {
127
+ const pagesIndex = pathParts.findIndex((part: any) => part === "pages");
128
+ if (pagesIndex >= 0 && pagesIndex < pathParts.length - 1) {
129
+ // Get all parts from 'pages' to the file, preserving nested structure
130
+ const relevantParts = pathParts.slice(pagesIndex, pathParts.length);
131
+ const lastPart = relevantParts[relevantParts.length - 1];
132
+ // Remove file extension from the last part
133
+ relevantParts[relevantParts.length - 1] = lastPart.includes(".")
134
+ ? lastPart.split(".")[0]
135
+ : lastPart;
136
+ filename = relevantParts.join("/");
137
+ } else {
138
+ filename = pathParts[pathParts.length - 1];
139
+ if (filename.includes(".")) {
140
+ filename = filename.split(".")[0];
141
+ }
142
+ }
143
+ } else if (id.includes("/components/")) {
144
+ const componentsIndex = pathParts.findIndex(
145
+ (part: any) => part === "components"
146
+ );
147
+ if (componentsIndex >= 0 && componentsIndex < pathParts.length - 1) {
148
+ // Get all parts from 'components' to the file, preserving nested structure
149
+ const relevantParts = pathParts.slice(
150
+ componentsIndex,
151
+ pathParts.length
152
+ );
153
+ const lastPart = relevantParts[relevantParts.length - 1];
154
+ // Remove file extension from the last part
155
+ relevantParts[relevantParts.length - 1] = lastPart.includes(".")
156
+ ? lastPart.split(".")[0]
157
+ : lastPart;
158
+ filename = relevantParts.join("/");
159
+ } else {
160
+ filename = pathParts[pathParts.length - 1];
161
+ if (filename.includes(".")) {
162
+ filename = filename.split(".")[0];
163
+ }
164
+ }
165
+ } else {
166
+ // For other files (like layout), just use the filename
167
+ filename = pathParts[pathParts.length - 1];
168
+ if (filename.includes(".")) {
169
+ filename = filename.split(".")[0];
170
+ }
171
+ }
172
+
173
+ try {
174
+ // Parse the code into an AST
175
+ const ast = parse(code, {
176
+ sourceType: "module",
177
+ plugins: [
178
+ "jsx",
179
+ "typescript",
180
+ "decorators-legacy",
181
+ "classProperties",
182
+ "objectRestSpread",
183
+ "functionBind",
184
+ "exportDefaultFrom",
185
+ "exportNamespaceFrom",
186
+ "dynamicImport",
187
+ "nullishCoalescingOperator",
188
+ "optionalChaining",
189
+ "asyncGenerators",
190
+ "bigInt",
191
+ "optionalCatchBinding",
192
+ "throwExpressions",
193
+ ],
194
+ });
195
+
196
+ // Traverse the AST and add source location and dynamic content attributes to JSX elements
197
+ let elementsProcessed = 0;
198
+ traverse.default(ast, {
199
+ JSXElement(path) {
200
+ const jsxElement = path.node;
201
+ const openingElement = jsxElement.openingElement;
202
+
203
+ // Skip fragments
204
+ if (t.isJSXFragment(jsxElement)) return;
205
+
206
+ // Skip if already has source location attribute
207
+ const hasSourceLocation = openingElement.attributes.some(
208
+ (attr) =>
209
+ t.isJSXAttribute(attr) &&
210
+ t.isJSXIdentifier(attr.name) &&
211
+ attr.name.name === "data-source-location"
212
+ );
213
+
214
+ if (hasSourceLocation) return;
215
+
216
+ // Get line and column from AST node location
217
+ const { line, column } = openingElement.loc?.start || {
218
+ line: 1,
219
+ column: 0,
220
+ };
221
+
222
+ // Create the source location attribute
223
+ const sourceLocationAttr = t.jsxAttribute(
224
+ t.jsxIdentifier("data-source-location"),
225
+ t.stringLiteral(`${filename}:${line}:${column}`)
226
+ );
227
+
228
+ // Check if element has dynamic content
229
+ const isDynamic = checkIfElementHasDynamicContent(jsxElement);
230
+
231
+ // Create the dynamic content attribute
232
+ const dynamicContentAttr = t.jsxAttribute(
233
+ t.jsxIdentifier("data-dynamic-content"),
234
+ t.stringLiteral(isDynamic ? "true" : "false")
235
+ );
236
+
237
+ // Add both attributes to the beginning of the attributes array
238
+ openingElement.attributes.unshift(
239
+ sourceLocationAttr,
240
+ dynamicContentAttr
241
+ );
242
+ elementsProcessed++;
243
+ },
244
+ });
245
+
246
+ // Generate the code back from the AST
247
+ const result = generate.default(ast, {
248
+ compact: false,
249
+ concise: false,
250
+ retainLines: true,
251
+ });
252
+
253
+ return {
254
+ code: result.code,
255
+ map: null,
256
+ };
257
+ } catch (error) {
258
+ console.error("Failed to add source location to JSX:", error);
259
+ return {
260
+ code: code, // Return original code on failure
261
+ map: null,
262
+ };
263
+ }
264
+ },
265
+ };
266
+ }
@@ -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