From f3532e146b64defd29262843c5e342f5788211c6 Mon Sep 17 00:00:00 2001 From: Kiet <31864905+Kitenite@users.noreply.github.com> Date: Sun, 12 Jan 2025 16:27:49 -0800 Subject: [PATCH] Move domain and add better error handling (#1035) * Add next.js error checks * Update typescript config for build * Preview button * Handling create link failed --- apps/studio/electron/main/events/hosting.ts | 11 +- apps/studio/electron/main/hosting/helpers.ts | 40 ++++++- apps/studio/electron/main/hosting/index.ts | 87 +++++++++----- apps/studio/package.json | 2 +- apps/studio/src/lib/projects/hosting.ts | 57 +++++---- .../src/routes/editor/EditPanel/index.tsx | 2 +- .../editor/TopBar/ShareProject/index.tsx | 105 ++++++++++++----- .../studio/src/routes/editor/TopBar/index.tsx | 2 +- bun.lockb | Bin 579344 -> 579344 bytes packages/foundation/src/frameworks/next.ts | 108 ++++++++++++------ packages/foundation/src/index.ts | 2 +- packages/foundation/tests/config.test.ts | 70 +++++++++--- packages/models/src/constants/index.ts | 1 + packages/models/src/hosting/index.ts | 2 +- 14 files changed, 350 insertions(+), 139 deletions(-) diff --git a/apps/studio/electron/main/events/hosting.ts b/apps/studio/electron/main/events/hosting.ts index 90c321642..d00ac63a3 100644 --- a/apps/studio/electron/main/events/hosting.ts +++ b/apps/studio/electron/main/events/hosting.ts @@ -8,8 +8,11 @@ export function listenForHostingMessages() { return await hostingManager.deploy(folderPath, buildScript, url); }); - ipcMain.handle(MainChannels.UNPUBLISH_HOSTING_ENV, (e: Electron.IpcMainInvokeEvent, args) => { - const { url } = args; - return hostingManager.unpublish(url); - }); + ipcMain.handle( + MainChannels.UNPUBLISH_HOSTING_ENV, + async (e: Electron.IpcMainInvokeEvent, args) => { + const { url } = args; + return await hostingManager.unpublish(url); + }, + ); } diff --git a/apps/studio/electron/main/hosting/helpers.ts b/apps/studio/electron/main/hosting/helpers.ts index 726f9e5aa..6b9225703 100644 --- a/apps/studio/electron/main/hosting/helpers.ts +++ b/apps/studio/electron/main/hosting/helpers.ts @@ -1,4 +1,4 @@ -import { addStandaloneConfig } from '@onlook/foundation'; +import { addNextBuildConfig } from '@onlook/foundation'; import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from 'fs'; import { isBinary } from 'istextorbinary'; import { exec } from 'node:child_process'; @@ -46,10 +46,31 @@ export function serializeFiles(currentDir: string, basePath: string = ''): FileR return files; } -export async function prepareNextProject(projectDir: string) { - const res = await addStandaloneConfig(projectDir); +export async function preprocessNextBuild(projectDir: string): Promise<{ + success: boolean; + error?: string; +}> { + const res = await addNextBuildConfig(projectDir); if (!res) { - return false; + return { + success: false, + error: 'Failed to add standalone config to Next.js project. Make sure project is Next.js and next.config.{js|ts|mjs|cjs} is present', + }; + } + + return { success: true }; +} + +export async function postprocessNextBuild(projectDir: string): Promise<{ + success: boolean; + error?: string; +}> { + const entrypointExists = await checkEntrypointExists(projectDir); + if (!entrypointExists) { + return { + success: false, + error: 'Failed to find entrypoint server.js in .next/standalone', + }; } copyDir(projectDir + '/public', projectDir + '/.next/standalone/public'); @@ -58,11 +79,18 @@ export async function prepareNextProject(projectDir: string) { for (const lockFile of SUPPORTED_LOCK_FILES) { if (existsSync(projectDir + '/' + lockFile)) { copyFileSync(projectDir + '/' + lockFile, projectDir + '/.next/standalone/' + lockFile); - return true; + return { success: true }; } } - return false; + return { + success: false, + error: 'Failed to find lock file. Supported lock files: ' + SUPPORTED_LOCK_FILES.join(', '), + }; +} + +async function checkEntrypointExists(projectDir: string) { + return existsSync(join(projectDir, '/.next/standalone/server.js')); } export function copyDir(src: string, dest: string) { diff --git a/apps/studio/electron/main/hosting/index.ts b/apps/studio/electron/main/hosting/index.ts index 83379dac5..33d7a8cad 100644 --- a/apps/studio/electron/main/hosting/index.ts +++ b/apps/studio/electron/main/hosting/index.ts @@ -3,17 +3,19 @@ import { HostingStatus } from '@onlook/models/hosting'; import { FreestyleSandboxes, type FreestyleDeployWebSuccessResponse } from 'freestyle-sandboxes'; import { mainWindow } from '..'; import analytics from '../analytics'; -import { PersistentStorage } from '../storage'; -import { prepareNextProject, runBuildScript, serializeFiles } from './helpers'; +import { + postprocessNextBuild, + preprocessNextBuild, + runBuildScript, + serializeFiles, +} from './helpers'; import { LogTimer } from '/common/helpers/timer'; class HostingManager { private static instance: HostingManager; private freestyle: FreestyleSandboxes | null = null; - private userId: string | null = null; private constructor() { - this.restoreSettings(); this.freestyle = this.initFreestyleClient(); } @@ -34,11 +36,6 @@ class HostingManager { return HostingManager.instance; } - private restoreSettings() { - const settings = PersistentStorage.USER_SETTINGS.read() || {}; - this.userId = settings.id || null; - } - async deploy( folderPath: string, buildScript: string, @@ -55,44 +52,61 @@ class HostingManager { return { state: HostingStatus.ERROR, message: 'Hosting client not initialized' }; } - // TODO: Check if project is a Next.js project + try { + this.emitState(HostingStatus.DEPLOYING, 'Preparing project...'); - const BUILD_OUTPUT_PATH = folderPath + '/.next'; - const BUILD_SCRIPT_NO_LINT = buildScript + ' -- --no-lint'; + const { success: preprocessSuccess, error: preprocessError } = + await preprocessNextBuild(folderPath); + + if (!preprocessSuccess) { + this.emitState( + HostingStatus.ERROR, + 'Failed to prepare project for deployment, error: ' + preprocessError, + ); + return { + state: HostingStatus.ERROR, + message: 'Failed to prepare project for deployment, error: ' + preprocessError, + }; + } - try { this.emitState(HostingStatus.DEPLOYING, 'Creating optimized build...'); timer.log('Starting build'); - const STANDALONE_PATH = BUILD_OUTPUT_PATH + '/standalone'; - const { success, error } = await runBuildScript(folderPath, BUILD_SCRIPT_NO_LINT); + const BUILD_SCRIPT_NO_LINT = buildScript + ' -- --no-lint'; + const { success: buildSuccess, error: buildError } = await runBuildScript( + folderPath, + BUILD_SCRIPT_NO_LINT, + ); timer.log('Build completed'); - if (!success) { - this.emitState(HostingStatus.ERROR, `Build failed with error: ${error}`); + if (!buildSuccess) { + this.emitState(HostingStatus.ERROR, `Build failed with error: ${buildError}`); return { state: HostingStatus.ERROR, - message: `Build failed with error: ${error}`, + message: `Build failed with error: ${buildError}`, }; } - this.emitState(HostingStatus.DEPLOYING, 'Preparing project...'); + this.emitState(HostingStatus.DEPLOYING, 'Preparing project for deployment...'); - const preparedResult = await prepareNextProject(folderPath); + const { success: postprocessSuccess, error: postprocessError } = + await postprocessNextBuild(folderPath); timer.log('Project preparation completed'); - if (!preparedResult) { + if (!postprocessSuccess) { this.emitState( HostingStatus.ERROR, - 'Failed to prepare project for deployment, no lock file found', + 'Failed to postprocess project for deployment, error: ' + postprocessError, ); return { state: HostingStatus.ERROR, - message: 'Failed to prepare project for deployment, no lock file found', + message: + 'Failed to postprocess project for deployment, error: ' + postprocessError, }; } - const files = serializeFiles(STANDALONE_PATH); + const NEXT_BUILD_OUTPUT_PATH = folderPath + '/.next/standalone'; + const files = serializeFiles(NEXT_BUILD_OUTPUT_PATH); timer.log('Files serialized'); const config = { @@ -151,10 +165,16 @@ class HostingManager { }); } - async unpublish(url: string) { + async unpublish(url: string): Promise<{ + success: boolean; + message?: string; + }> { if (!this.freestyle) { console.error('Freestyle client not initialized'); - return; + return { + success: false, + message: 'Freestyle client not initialized', + }; } const config = { @@ -169,7 +189,10 @@ class HostingManager { if (!res.projectId) { console.error('Failed to delete deployment', res); - return false; + return { + success: false, + message: 'Failed to delete deployment. ' + res, + }; } this.emitState(HostingStatus.NO_ENV, 'Deployment deleted'); @@ -178,14 +201,20 @@ class HostingManager { state: HostingStatus.NO_ENV, message: 'Deployment deleted', }); - return true; + return { + success: true, + message: 'Deployment deleted', + }; } catch (error) { console.error('Failed to delete deployment', error); this.emitState(HostingStatus.ERROR, 'Failed to delete deployment'); analytics.trackError('Failed to delete deployment', { error, }); - return false; + return { + success: false, + message: 'Failed to delete deployment. ' + error, + }; } } } diff --git a/apps/studio/package.json b/apps/studio/package.json index 97e5683fd..c39952382 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -62,7 +62,7 @@ "embla-carousel-wheel-gestures": "^8.0.1", "fflate": "^0.8.2", "fix-path": "^4.0.0", - "freestyle-sandboxes": "^0.0.7", + "freestyle-sandboxes": "^0.0.11", "istextorbinary": "^9.5.0", "js-string-escape": "^1.0.1", "langfuse-vercel": "^3.29.1", diff --git a/apps/studio/src/lib/projects/hosting.ts b/apps/studio/src/lib/projects/hosting.ts index de1022da6..c642d91f2 100644 --- a/apps/studio/src/lib/projects/hosting.ts +++ b/apps/studio/src/lib/projects/hosting.ts @@ -1,4 +1,4 @@ -import { MainChannels } from '@onlook/models/constants'; +import { HOSTING_DOMAIN, MainChannels } from '@onlook/models/constants'; import { HostingStatus } from '@onlook/models/hosting'; import type { Project } from '@onlook/models/projects'; import { makeAutoObservable } from 'mobx'; @@ -72,21 +72,30 @@ export class HostingManager { .replace(/^-|-$/g, ''); } - createLink() { - const newUrl = `${this.createProjectSubdomain(this.project.id)}.onlook.live`; + async createLink(): Promise { + const newUrl = `${this.createProjectSubdomain(this.project.id)}.${HOSTING_DOMAIN}`; + sendAnalytics('hosting create link', { + url: newUrl, + }); this.updateProject({ hosting: { url: newUrl, }, }); - this.updateState({ url: newUrl, status: HostingStatus.READY }); - sendAnalytics('hosting create link', { - url: newUrl, - }); - this.publish(); + const success = await this.publish(); + if (!success) { + this.updateProject({ + hosting: { + url: null, + }, + }); + this.updateState({ url: null, status: HostingStatus.NO_ENV }); + return false; + } + return true; } - async publish() { + async publish(): Promise { sendAnalytics('hosting publish'); const folderPath = this.project.folderPath; if (!folderPath) { @@ -94,7 +103,7 @@ export class HostingManager { sendAnalyticsError('Failed to publish', { message: 'Failed to publish hosting environment, missing folder path', }); - return; + return false; } const buildScript: string = this.project.commands?.build || 'npm run build'; @@ -103,7 +112,7 @@ export class HostingManager { sendAnalyticsError('Failed to publish', { message: 'Failed to publish hosting environment, missing build script', }); - return; + return false; } const url = this.project.hosting?.url; @@ -112,7 +121,7 @@ export class HostingManager { sendAnalyticsError('Failed to publish', { message: 'Failed to publish hosting environment, missing url', }); - return; + return false; } this.updateState({ status: HostingStatus.DEPLOYING, message: 'Creating deployment...' }); @@ -126,7 +135,7 @@ export class HostingManager { url, }); - if (!res) { + if (!res || res.state === HostingStatus.ERROR) { console.error('Failed to publish hosting environment'); this.updateState({ status: HostingStatus.ERROR, @@ -135,7 +144,7 @@ export class HostingManager { sendAnalyticsError('Failed to publish', { message: 'Failed to publish hosting environment, no response from client', }); - return; + return false; } sendAnalytics('hosting publish success', { @@ -144,23 +153,27 @@ export class HostingManager { }); this.updateState({ status: res.state, message: res.message }); + return true; } async unpublish() { this.updateState({ status: HostingStatus.DELETING, message: 'Deleting deployment...' }); sendAnalytics('hosting unpublish'); - const res: boolean = await invokeMainChannel(MainChannels.UNPUBLISH_HOSTING_ENV, { + const res: { + success: boolean; + message?: string; + } = await invokeMainChannel(MainChannels.UNPUBLISH_HOSTING_ENV, { url: this.state.url, }); - if (!res) { - console.error('Failed to unpublish hosting environment'); + if (!res.success) { + console.error('Failed to unpublish hosting environment', res); this.updateState({ status: HostingStatus.ERROR, - message: 'Failed to unpublish hosting environment', + message: res.message || 'Failed to unpublish hosting environment', }); sendAnalyticsError('Failed to unpublish', { - message: 'Failed to unpublish hosting environment', + message: res.message || 'Failed to unpublish hosting environment', }); return; } @@ -181,6 +194,10 @@ export class HostingManager { } refresh() { - this.updateState({ status: HostingStatus.READY, message: null }); + if (this.state.url) { + this.updateState({ status: HostingStatus.READY, message: null }); + } else { + this.updateState({ status: HostingStatus.NO_ENV, message: null, url: null }); + } } } diff --git a/apps/studio/src/routes/editor/EditPanel/index.tsx b/apps/studio/src/routes/editor/EditPanel/index.tsx index 0c35427f5..fdc1c8479 100644 --- a/apps/studio/src/routes/editor/EditPanel/index.tsx +++ b/apps/studio/src/routes/editor/EditPanel/index.tsx @@ -71,7 +71,7 @@ const EditPanel = observer(() => { value={EditorTabValue.CHAT} > - {'Chat (beta)'} + {'Chat'} {selectedTab === EditorTabValue.CHAT && } diff --git a/apps/studio/src/routes/editor/TopBar/ShareProject/index.tsx b/apps/studio/src/routes/editor/TopBar/ShareProject/index.tsx index 5313bfb32..9ebe0a4dc 100644 --- a/apps/studio/src/routes/editor/TopBar/ShareProject/index.tsx +++ b/apps/studio/src/routes/editor/TopBar/ShareProject/index.tsx @@ -79,11 +79,7 @@ const ShareProject = observer(() => { }; const renderHeader = () => { - if (!projectsManager.hosting?.state.url) { - return 'Share public link'; - } - - return HostingStateMessages[projectsManager.hosting?.state.status]; + return HostingStateMessages[projectsManager.hosting?.state.status || HostingStatus.NO_ENV]; }; const renderNoEnv = () => { @@ -96,7 +92,8 @@ const ShareProject = observer(() => { className="space-y-4" >

- Share your app with the world and update it at any time in Onlook. + Share your app with the world and update it at any time in Onlook. We currently + only support Next.js projects.

- ); + const buttonClasses = + 'px-3 flex items-center border-[0.5px] text-xs justify-center shadow-sm h-8 rounded-md transition-all duration-300 ease-in-out'; + let colorClasses = 'border-input bg-background hover:bg-background-onlook text-foreground'; + + switch (projectsManager.hosting?.state.status) { + case HostingStatus.READY: + colorClasses = 'border-teal-300 bg-teal-700 hover:bg-teal-500/20 text-teal-100'; + return ( + + ); + case HostingStatus.ERROR: + colorClasses = 'border-red-500/30 bg-red-500/10 hover:bg-red-500/20 text-red-500'; + return ( + + ); + case HostingStatus.DEPLOYING: + case HostingStatus.DELETING: + return ( + + ); + case HostingStatus.NO_ENV: + default: + return ( + + ); + } }; const renderReady = () => { @@ -238,7 +273,19 @@ const ShareProject = observer(() => {
{renderLink()} - {renderUnpublish()} + {renderPublishControls()} +

+ Want to host on your own domain? + +

); diff --git a/apps/studio/src/routes/editor/TopBar/index.tsx b/apps/studio/src/routes/editor/TopBar/index.tsx index 81385478d..23f78ff6a 100644 --- a/apps/studio/src/routes/editor/TopBar/index.tsx +++ b/apps/studio/src/routes/editor/TopBar/index.tsx @@ -77,7 +77,7 @@ const EditorTopBar = observer(() => { )} -
+
{/* */} diff --git a/bun.lockb b/bun.lockb index f72110c720ccbe8fa22e0aef8696257e833aa774..2221e1b5a245d3aab9ad0288c5d795380edc7d57 100755 GIT binary patch delta 27041 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 diff --git a/packages/foundation/src/frameworks/next.ts b/packages/foundation/src/frameworks/next.ts index 2b99edeae..a4414522e 100644 --- a/packages/foundation/src/frameworks/next.ts +++ b/packages/foundation/src/frameworks/next.ts @@ -203,7 +203,76 @@ export const removeNextCache = (): void => { } }; -export const addStandaloneConfig = (projectDir: string): Promise => { +const addConfigProperty = ( + ast: t.File, + propertyName: string, + propertyValue: t.Expression, +): boolean => { + let propertyExists = false; + + traverse(ast, { + ObjectExpression(path) { + const properties = path.node.properties; + let hasProperty = false; + + // Check if property already exists + properties.forEach((prop) => { + if (t.isObjectProperty(prop) && t.isIdentifier(prop.key, { name: propertyName })) { + hasProperty = true; + propertyExists = true; + + // If the property value is an object expression, merge properties + if (t.isObjectExpression(prop.value) && t.isObjectExpression(propertyValue)) { + const existingProps = new Map( + prop.value.properties + .filter( + (p): p is t.ObjectProperty => + t.isObjectProperty(p) && t.isIdentifier(p.key), + ) + .map((p) => [(p.key as t.Identifier).name, p]), + ); + + // Add or update properties from propertyValue + propertyValue.properties.forEach((newProp) => { + if (t.isObjectProperty(newProp) && t.isIdentifier(newProp.key)) { + existingProps.set(newProp.key.name, newProp); + } + }); + + // Update the property value with merged properties + prop.value.properties = Array.from(existingProps.values()); + } else { + // For non-object properties, just replace the value + prop.value = propertyValue; + } + } + }); + + if (!hasProperty) { + // Add the new property if it doesn't exist + properties.push(t.objectProperty(t.identifier(propertyName), propertyValue)); + propertyExists = true; + } + + // Stop traversing after the modification + path.stop(); + }, + }); + + return propertyExists; +}; + +const addTypescriptConfig = (ast: t.File): boolean => { + return addConfigProperty( + ast, + 'typescript', + t.objectExpression([ + t.objectProperty(t.identifier('ignoreBuildErrors'), t.booleanLiteral(true)), + ]), + ); +}; + +export const addNextBuildConfig = (projectDir: string): Promise => { return new Promise((resolve) => { // Find any config file const possibleExtensions = ['.js', '.ts', '.mjs', '.cjs']; @@ -238,41 +307,14 @@ export const addStandaloneConfig = (projectDir: string): Promise => { const astParserOption = genASTParserOptionsByFileExtension(configFileExtension); const ast = parse(data, astParserOption); - let outputExists = false; - - traverse(ast, { - ObjectExpression(path) { - const properties = path.node.properties; - let hasOutputProperty = false; - - // Check if output property already exists - properties.forEach((prop) => { - if ( - t.isObjectProperty(prop) && - t.isIdentifier(prop.key, { name: 'output' }) - ) { - hasOutputProperty = true; - outputExists = true; - } - }); - - if (!hasOutputProperty) { - // Add output: 'standalone' property - properties.push( - t.objectProperty(t.identifier('output'), t.stringLiteral('standalone')), - ); - outputExists = true; - } - // Stop traversing after the modification - path.stop(); - }, - }); + // Add both configurations + const outputExists = addConfigProperty(ast, 'output', t.stringLiteral('standalone')); + const typescriptExists = addTypescriptConfig(ast); // Generate the modified code from the AST const updatedCode = generate(ast, {}, data).code; - // Write the updated content back to next.config.* file fs.writeFile(configPath, updatedCode, 'utf8', (err) => { if (err) { console.error(`Error writing ${configPath}:`, err); @@ -281,9 +323,9 @@ export const addStandaloneConfig = (projectDir: string): Promise => { } console.log( - `Successfully updated ${configPath} with standalone output configuration`, + `Successfully updated ${configPath} with standalone output and typescript configuration`, ); - resolve(outputExists); + resolve(outputExists && typescriptExists); }); }); }); diff --git a/packages/foundation/src/index.ts b/packages/foundation/src/index.ts index c3bdc5254..ebb57796f 100644 --- a/packages/foundation/src/index.ts +++ b/packages/foundation/src/index.ts @@ -1,5 +1,5 @@ export { createProject } from './create'; -export { addStandaloneConfig } from './frameworks/next'; +export { addNextBuildConfig } from './frameworks/next'; export { revertLegacyOnlook } from './revert'; export { setupProject } from './setup'; export { verifyProject } from './verify'; diff --git a/packages/foundation/tests/config.test.ts b/packages/foundation/tests/config.test.ts index 138a45284..1091c504b 100644 --- a/packages/foundation/tests/config.test.ts +++ b/packages/foundation/tests/config.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, test } from 'bun:test'; import fs from 'fs'; import path from 'path'; -import { addStandaloneConfig } from '../src/frameworks/next'; +import { addNextBuildConfig } from '../src/frameworks/next'; describe('Next.js Config Modifications', () => { const configFiles = ['next.config.js', 'next.config.ts', 'next.config.mjs']; @@ -33,8 +33,8 @@ module.exports = nextConfig; fs.writeFileSync(configPath, initialConfig, 'utf8'); - // Apply the standalone config modification - addStandaloneConfig(process.cwd()); + // Apply the config modifications + addNextBuildConfig(process.cwd()); // Wait a bit for the file operation to complete await new Promise((resolve) => setTimeout(resolve, 100)); @@ -42,8 +42,10 @@ module.exports = nextConfig; // Read the modified config const modifiedConfig = fs.readFileSync(configPath, 'utf8'); - // Verify the output configuration was added + // Verify both configurations were added expect(modifiedConfig).toContain('output: "standalone"'); + expect(modifiedConfig).toContain('typescript: {'); + expect(modifiedConfig).toContain('ignoreBuildErrors: true'); expect(modifiedConfig).toContain('reactStrictMode: true'); // Clean up this config file @@ -51,24 +53,27 @@ module.exports = nextConfig; } }); - test('addStandaloneConfig does not duplicate output property', async () => { + test('addStandaloneConfig does not duplicate properties', async () => { const configPath = path.resolve(process.cwd(), 'next.config.js'); - // Create config with existing output property using CommonJS syntax - const configWithOutput = ` + // Create config with existing properties using CommonJS syntax + const configWithExisting = ` /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, - output: "standalone" + output: "standalone", + typescript: { + ignoreBuildErrors: true + } }; module.exports = nextConfig; `.trim(); - fs.writeFileSync(configPath, configWithOutput, 'utf8'); + fs.writeFileSync(configPath, configWithExisting, 'utf8'); - // Apply the standalone config modification - addStandaloneConfig(process.cwd()); + // Apply the config modifications + addNextBuildConfig(process.cwd()); // Wait a bit for the file operation to complete await new Promise((resolve) => setTimeout(resolve, 100)); @@ -76,11 +81,50 @@ module.exports = nextConfig; // Read the modified config const modifiedConfig = fs.readFileSync(configPath, 'utf8'); - // Count occurrences of 'output' + // Count occurrences of properties const outputCount = (modifiedConfig.match(/output:/g) || []).length; + const typescriptCount = (modifiedConfig.match(/typescript:/g) || []).length; - // Verify there's only one output property + // Verify there's only one instance of each property expect(outputCount).toBe(1); + expect(typescriptCount).toBe(1); expect(modifiedConfig).toContain('output: "standalone"'); + expect(modifiedConfig).toContain('typescript: {'); + expect(modifiedConfig).toContain('ignoreBuildErrors: true'); + }); + + test('addStandaloneConfig preserves existing typescript attributes', async () => { + const configPath = path.resolve(process.cwd(), 'next.config.js'); + + // Create config with existing typescript properties + const configWithExisting = ` +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + typescript: { + tsconfigPath: "./custom-tsconfig.json", + ignoreBuildErrors: false + } +}; + +module.exports = nextConfig; + `.trim(); + + fs.writeFileSync(configPath, configWithExisting, 'utf8'); + + // Apply the config modifications + addNextBuildConfig(process.cwd()); + + // Wait a bit for the file operation to complete + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Read the modified config + const modifiedConfig = fs.readFileSync(configPath, 'utf8'); + + // Verify typescript configuration + expect(modifiedConfig).toContain('typescript: {'); + expect(modifiedConfig).toContain('ignoreBuildErrors: true'); // Should be updated to true + expect(modifiedConfig).toContain('tsconfigPath: "./custom-tsconfig.json"'); // Should be preserved + expect((modifiedConfig.match(/typescript:/g) || []).length).toBe(1); // Should still only have one typescript block }); }); diff --git a/packages/models/src/constants/index.ts b/packages/models/src/constants/index.ts index 54cb36de6..5cc800b93 100644 --- a/packages/models/src/constants/index.ts +++ b/packages/models/src/constants/index.ts @@ -166,6 +166,7 @@ export enum Links { export const APP_NAME = 'Onlook'; export const APP_SCHEMA = 'onlook'; +export const HOSTING_DOMAIN = 'onlook.live'; export const MAX_NAME_LENGTH = 50; export const DefaultSettings = { SCALE: 0.6, diff --git a/packages/models/src/hosting/index.ts b/packages/models/src/hosting/index.ts index f82915a49..fed8f18f9 100644 --- a/packages/models/src/hosting/index.ts +++ b/packages/models/src/hosting/index.ts @@ -7,7 +7,7 @@ export enum HostingStatus { } export const HostingStateMessages = { - [HostingStatus.NO_ENV]: 'Share public link', + [HostingStatus.NO_ENV]: 'Share public link (beta)', [HostingStatus.READY]: 'Public link', [HostingStatus.DEPLOYING]: 'Deploying', [HostingStatus.ERROR]: 'Error',