Skip to content

Commit 3c27f0c

Browse files
author
克隆(宗可龙)
committed
Merge branch 'main' of https://github.com/onlook-dev/onlook into translation_zh
* 'main' of https://github.com/onlook-dev/onlook: Publish version v0.2.23 (onlook-dev#1694) Fix edit panel not fully hidden (onlook-dev#1695) Update file watcher and remove elide lines (onlook-dev#1693) Add sticky position (onlook-dev#1692) Publish version v0.2.22 (onlook-dev#1679) added text transforms (onlook-dev#1689) Update URL when changing project settings URL (onlook-dev#1690) fix: error when setting text color (onlook-dev#1688) Revert freestyle source (onlook-dev#1686)
2 parents 67acb6d + 48b1d9f commit 3c27f0c

File tree

21 files changed

+317
-992
lines changed

21 files changed

+317
-992
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ jobs:
5555
5656
- uses: oven-sh/setup-bun@v1
5757
with:
58-
bun-version: latest
58+
bun-version: 1.2.7
5959

6060
- name: Install dependencies
6161
run: bun install --frozen-lockfile

.husky/pre-commit

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
#!/usr/bin/env sh
2-
bun run lint:precommit && bun run format:precommit && git add .
2+
bun run lint && bun run format && git add .

apps/studio/common/helpers/twTranslator/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -979,12 +979,13 @@ export const propertyMap: Map<
979979
['clip-path', (val) => `[clip-path:${getCustomVal(val)}]`],
980980
[
981981
'color',
982-
(val) =>
982+
(val, isCustom = false) =>
983983
({
984984
transparent: 'text-transparent',
985985
currentColor: 'text-current',
986986
currentcolor: 'text-current',
987-
})[val] ?? (isColor(val, true) ? `text-[${getCustomVal(val)}]` : ''),
987+
})[val] ??
988+
(isCustom ? `text-${val}` : isColor(val, true) ? `text-[${getCustomVal(val)}]` : ''),
988989
],
989990
['color-scheme', (val) => `[color-scheme:${getCustomVal(val)}]`],
990991
['column-count', (val) => `[column-count:${getCustomVal(val)}]`],

apps/studio/electron/main/assets/images.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { DefaultSettings } from '@onlook/models/constants';
33
import { promises as fs, readFileSync } from 'fs';
44
import mime from 'mime-lite';
55
import path from 'path';
6-
import writeFileAtomic from 'write-file-atomic';
76

87
async function scanImagesDirectory(projectRoot: string): Promise<ImageContentData[]> {
98
const imagesPath = path.join(projectRoot, DefaultSettings.IMAGE_FOLDER);
@@ -167,7 +166,7 @@ async function updateImageReferences(
167166
return;
168167
}
169168
const updatedContent = content.replace(pattern, newImageUrl);
170-
await writeFileAtomic(file, updatedContent, { encoding: 'utf8' });
169+
await fs.writeFile(file, updatedContent, 'utf8');
171170
}),
172171
);
173172
}

apps/studio/electron/main/code/files.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { existsSync, promises as fs } from 'fs';
22
import * as path from 'path';
33
import prettier from 'prettier';
4-
import writeFileAtomic from 'write-file-atomic';
54

65
export async function readFile(filePath: string): Promise<string | null> {
76
try {
@@ -23,36 +22,52 @@ export async function writeFile(
2322
content: string,
2423
encoding: 'utf8' | 'base64' = 'utf8',
2524
): Promise<void> {
25+
if (!filePath) {
26+
throw new Error('File path is required');
27+
}
28+
2629
try {
27-
if (!content || content.trim() === '') {
28-
throw new Error(`New content is empty: ${filePath}`);
29-
}
3030
const fullPath = path.resolve(filePath);
3131
const isNewFile = !existsSync(fullPath);
3232

33-
// Ensure parent directory exists
34-
const parentDir = path.dirname(fullPath);
35-
await fs.mkdir(parentDir, { recursive: true });
36-
37-
// Handle base64 encoded content
38-
let fileContent = content;
33+
let fileContent: string | Buffer = content;
3934
if (encoding === 'base64') {
40-
// Strip data URL prefix if present
41-
const base64Data = content.replace(/^data:[^,]+,/, '');
42-
fileContent = Buffer.from(base64Data, 'base64').toString('base64');
35+
try {
36+
// Strip data URL prefix if present and validate base64
37+
const base64Data = content.replace(/^data:[^,]+,/, '');
38+
if (!isValidBase64(base64Data)) {
39+
throw new Error('Invalid base64 content');
40+
}
41+
fileContent = Buffer.from(base64Data, 'base64');
42+
} catch (e: any) {
43+
throw new Error(`Invalid base64 content: ${e.message}`);
44+
}
4345
}
4446

45-
writeFileAtomic(fullPath, fileContent, encoding);
47+
// Ensure parent directory exists
48+
const parentDir = path.dirname(fullPath);
49+
await fs.mkdir(parentDir, { recursive: true });
50+
await fs.writeFile(fullPath, fileContent, { encoding });
4651

4752
if (isNewFile) {
4853
console.log('New file created:', fullPath);
4954
}
5055
} catch (error: any) {
51-
console.error('Error writing to file:', error);
56+
const errorMessage = `Failed to write to ${filePath}: ${error.message}`;
57+
console.error(errorMessage);
5258
throw error;
5359
}
5460
}
5561

62+
// Helper function to validate base64 strings
63+
function isValidBase64(str: string): boolean {
64+
try {
65+
return Buffer.from(str, 'base64').toString('base64') === str;
66+
} catch {
67+
return false;
68+
}
69+
}
70+
5671
export async function formatContent(filePath: string, content: string): Promise<string> {
5772
try {
5873
const config = (await prettier.resolveConfig(filePath)) || {};

apps/studio/electron/main/hosting/helpers.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { addNextBuildConfig } from '@onlook/foundation';
22
import { CUSTOM_OUTPUT_DIR } from '@onlook/models/constants';
3+
import type { FreestyleFile } from 'freestyle-sandboxes';
34
import {
45
appendFileSync,
56
copyFileSync,
@@ -14,13 +15,7 @@ import { join } from 'node:path';
1415

1516
const SUPPORTED_LOCK_FILES = ['bun.lock', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
1617

17-
export type FileRecord = Record<
18-
string,
19-
{
20-
content: string;
21-
encoding: string;
22-
}
23-
>;
18+
export type FileRecord = Record<string, FreestyleFile>;
2419

2520
export function serializeFiles(currentDir: string, basePath: string = ''): FileRecord {
2621
const files: FileRecord = {};

apps/studio/electron/main/hosting/index.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,18 @@ import {
2424
type DeploymentSource,
2525
type FreestyleDeployWebConfiguration,
2626
type FreestyleDeployWebSuccessResponse,
27+
type FreestyleFile,
2728
} from 'freestyle-sandboxes';
2829
import { prepareDirForDeployment } from 'freestyle-sandboxes/utils';
2930
import { mainWindow } from '..';
3031
import analytics from '../analytics';
3132
import { getRefreshedAuthTokens } from '../auth';
32-
import { postprocessNextBuild, preprocessNextBuild, updateGitignore } from './helpers';
33+
import {
34+
postprocessNextBuild,
35+
preprocessNextBuild,
36+
updateGitignore,
37+
type FileRecord,
38+
} from './helpers';
3339
import { runBuildScript } from './run';
3440
import { LogTimer } from '/common/helpers/timer';
3541

@@ -81,12 +87,20 @@ class HostingManager {
8187

8288
// Serialize the files for deployment
8389
const NEXT_BUILD_OUTPUT_PATH = `${folderPath}/${CUSTOM_OUTPUT_DIR}/standalone`;
84-
const source: DeploymentSource = await prepareDirForDeployment(NEXT_BUILD_OUTPUT_PATH);
90+
const source: DeploymentSource = (await prepareDirForDeployment(
91+
NEXT_BUILD_OUTPUT_PATH,
92+
)) as {
93+
files: {
94+
[key: string]: FreestyleFile;
95+
};
96+
kind: 'files';
97+
};
98+
const files: FileRecord = source.files;
8599

86100
this.emitState(PublishStatus.LOADING, 'Deploying project...');
87101
timer.log('Files serialized, sending to Freestyle...');
88102

89-
const id = await this.sendHostingPostRequest(source, urls);
103+
const id = await this.sendHostingPostRequest(files, urls);
90104
timer.log('Deployment completed');
91105

92106
this.emitState(PublishStatus.PUBLISHED, 'Deployment successful, deployment ID: ' + id);
@@ -182,7 +196,7 @@ class HostingManager {
182196

183197
async unpublish(urls: string[]): Promise<PublishResponse> {
184198
try {
185-
const id = await this.sendHostingPostRequest({ files: {}, kind: 'files' }, urls);
199+
const id = await this.sendHostingPostRequest({}, urls);
186200
this.emitState(PublishStatus.UNPUBLISHED, 'Deployment deleted with ID: ' + id);
187201

188202
analytics.track('hosting unpublish', {
@@ -206,23 +220,23 @@ class HostingManager {
206220
}
207221
}
208222

209-
async sendHostingPostRequest(source: DeploymentSource, urls: string[]): Promise<string> {
223+
async sendHostingPostRequest(files: FileRecord, urls: string[]): Promise<string> {
210224
const authTokens = await getRefreshedAuthTokens();
211225
const config: FreestyleDeployWebConfiguration = {
212226
domains: urls,
213227
entrypoint: 'server.js',
214228
};
215229

216230
const res: Response = await fetch(
217-
`${import.meta.env.VITE_SUPABASE_API_URL}${FUNCTIONS_ROUTE}${BASE_API_ROUTE}${ApiRoutes.HOSTING_V2}${HostingRoutes.DEPLOY_WEB_V2}`,
231+
`${import.meta.env.VITE_SUPABASE_API_URL}${FUNCTIONS_ROUTE}${BASE_API_ROUTE}${ApiRoutes.HOSTING_V2}${HostingRoutes.DEPLOY_WEB}`,
218232
{
219233
method: 'POST',
220234
headers: {
221235
'Content-Type': 'application/json',
222236
Authorization: `Bearer ${authTokens.accessToken}`,
223237
},
224238
body: JSON.stringify({
225-
source,
239+
files,
226240
config,
227241
}),
228242
},

apps/studio/electron/main/run/index.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class RunManager {
1414
private static instance: RunManager;
1515
private mapping = new Map<string, TemplateNode>();
1616
private subscription: AsyncSubscription | null = null;
17+
private selfModified = new Set<string>();
1718
state: RunState = RunState.STOPPED;
1819
runningDirs = new Set<string>();
1920
createdFiles = new Set<string>();
@@ -129,23 +130,21 @@ class RunManager {
129130

130131
this.subscription = await subscribe(
131132
folderPath,
132-
(err, events) => {
133+
async (err, events) => {
133134
if (err) {
134135
console.error(`Watcher error: ${err}`);
135136
return;
136137
}
137138

138139
for (const event of events) {
139-
if (event.type === 'update') {
140+
if (this.selfModified.has(event.path)) {
141+
this.selfModified.delete(event.path);
142+
continue;
143+
}
144+
if (event.type === 'update' || event.type === 'create') {
140145
if (this.isAllowedExtension(event.path)) {
141-
this.processFileForMapping(event.path);
142-
}
143-
} else if (event.type === 'create') {
144-
if (!this.createdFiles.has(event.path)) {
145-
if (this.isAllowedExtension(event.path)) {
146-
this.processFileForMapping(event.path);
147-
}
148-
this.createdFiles.add(event.path);
146+
this.selfModified.add(event.path);
147+
await this.processFileForMapping(event.path);
149148
}
150149
}
151150
}
@@ -182,6 +181,7 @@ class RunManager {
182181
}
183182

184183
await writeFile(filePath, content);
184+
185185
for (const [key, value] of Object.entries(newMapping)) {
186186
this.mapping.set(key, value);
187187
}
@@ -195,6 +195,7 @@ class RunManager {
195195
await this.clearSubscription();
196196
this.runningDirs.clear();
197197
this.mapping.clear();
198+
this.selfModified.clear();
198199
}
199200

200201
async cleanProjectDir(folderPath: string): Promise<void> {

apps/studio/package.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"productName": "Onlook",
33
"name": "@onlook/studio",
4-
"version": "0.2.21",
4+
"version": "0.2.23",
55
"homepage": "https://onlook.com",
66
"main": "dist-electron/main/index.js",
77
"description": "The first-ever devtool for designers",
@@ -57,7 +57,7 @@
5757
"embla-carousel-wheel-gestures": "^8.0.1",
5858
"fflate": "^0.8.2",
5959
"fix-path": "^4.0.0",
60-
"freestyle-sandboxes": "^0.0.35",
60+
"freestyle-sandboxes": "^0.0.36",
6161
"i18next": "^24.2.2",
6262
"istextorbinary": "^9.5.0",
6363
"js-string-escape": "^1.0.1",
@@ -85,7 +85,6 @@
8585
"ts-morph": "^25.0.0",
8686
"type-fest": "^4.26.1",
8787
"use-resize-observer": "^9.1.0",
88-
"write-file-atomic": "^6.0.0",
8988
"zod": "^3.23.8"
9089
},
9190
"devDependencies": {
@@ -105,7 +104,6 @@
105104
"@types/react": "^18.2.64",
106105
"@types/react-dom": "^18.2.21",
107106
"@types/shell-quote": "^1.7.5",
108-
"@types/write-file-atomic": "^4.0.3",
109107
"@vitejs/plugin-react": "^4.2.1",
110108
"autoprefixer": "^10.4.20",
111109
"css-tree": "^3.1.0",

apps/studio/src/components/Modals/Settings/Project/index.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ const ProjectTab = observer(() => {
3434
});
3535
};
3636

37+
const handleUpdateUrl = (url: string) => {
38+
projectsManager.updatePartialProject({
39+
url,
40+
});
41+
projectsManager.editorEngine?.canvas.saveFrames(
42+
projectsManager.editorEngine?.canvas.frames.map((frame) => ({
43+
...frame,
44+
url,
45+
})),
46+
);
47+
};
48+
3749
return (
3850
<div className="text-sm">
3951
<div className="flex flex-col gap-4 p-6">
@@ -57,11 +69,7 @@ const ProjectTab = observer(() => {
5769
<Input
5870
id="url"
5971
value={url}
60-
onChange={(e) =>
61-
projectsManager.updatePartialProject({
62-
url: e.target.value,
63-
})
64-
}
72+
onChange={(e) => handleUpdateUrl(e.target.value)}
6573
className="w-2/3"
6674
/>
6775
</div>

apps/studio/src/lib/editor/engine/canvas/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ export class CanvasManager {
9999
this.notifySettingsObservers(id);
100100
}
101101

102+
saveFrames(frames: FrameSettings[]) {
103+
this.webFrames = frames;
104+
this.saveSettings();
105+
}
106+
102107
async applySettings(project: Project) {
103108
this.zoomScale = project.settings?.scale || DefaultSettings.SCALE;
104109
this.panPosition = project.settings?.position || this.getDefaultPanPosition();

apps/studio/src/lib/editor/styles/group.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const PositionGroup = [
4040
new CompoundStyleImpl(
4141
CompoundStyleKey.Position,
4242
new SingleStyleImpl('position', 'relative', 'Position', StyleType.Select, {
43-
options: ['relative', 'absolute', 'fixed', 'static'],
43+
options: ['relative', 'absolute', 'fixed', 'static', 'sticky'],
4444
}),
4545
[
4646
new SingleStyleImpl('top', '', 'Top', StyleType.Number, {
@@ -242,6 +242,10 @@ export const StyleGroup = [
242242
];
243243

244244
export const TextGroup = [
245+
new SingleStyleImpl('textTransform', 'none', 'Transform', StyleType.Select, {
246+
options: ['none', 'capitalize', 'uppercase', 'lowercase'],
247+
}),
248+
245249
new SingleStyleImpl('color', '#000000', 'Color', StyleType.Color),
246250

247251
new SingleStyleImpl('fontSize', '16px', 'Size', StyleType.Number, {

apps/studio/src/routes/editor/EditPanel/StylesTab/single/ColorInput/ColorBrandPicker.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ const ColorGroup = ({
7272
className="w-5 h-5 rounded-sm"
7373
style={{ backgroundColor: color.lightColor }}
7474
/>
75-
<span className="text-xs font-normal truncate max-w-32">{color.name}</span>
75+
<span className="text-xs font-normal truncate max-w-32">
76+
{toNormalCase(color.name)}
77+
</span>
7678
</div>
7779
))}
7880
</div>

0 commit comments

Comments
 (0)