diff --git a/app/src/routes/projects/helpers.ts b/app/src/routes/projects/helpers.ts
new file mode 100644
index 000000000..20daa4cb7
--- /dev/null
+++ b/app/src/routes/projects/helpers.ts
@@ -0,0 +1,35 @@
+export const PLACEHOLDER_NAMES = [
+ 'The greatest app in the world',
+ 'My epic project',
+ 'The greatest project ever',
+ 'A revolutionary idea',
+ 'Project X',
+ 'Genius React App',
+ 'The next billion dollar idea',
+ 'Mind-blowingly cool app',
+ 'Earth-shatteringly great app',
+ 'Moonshot project',
+];
+
+export const SETTINGS_MESSAGE = [
+ 'Set some dials and knobs and stuff',
+ 'Fine-tune how you want to build',
+ 'Swap out your default code editor if you dare',
+ "You shouldn't be worried about this stuff, yet here you are",
+ 'Mostly a formality',
+ "What's this button do?",
+ 'Customize how you want to build',
+ 'Thanks for stopping by the Settings page',
+ 'This is where the good stuff is',
+ 'Open 24 hours, 7 days a week',
+ '*beep boop*',
+ "Welcome. We've been expecting you.",
+];
+
+export function getRandomPlaceholder() {
+ return PLACEHOLDER_NAMES[Math.floor(Math.random() * PLACEHOLDER_NAMES.length)];
+}
+
+export function getRandomSettingsMessage() {
+ return SETTINGS_MESSAGE[Math.floor(Math.random() * SETTINGS_MESSAGE.length)];
+}
diff --git a/app/vite.config.ts b/app/vite.config.ts
index e04a61c21..ef0fd2051 100644
--- a/app/vite.config.ts
+++ b/app/vite.config.ts
@@ -7,12 +7,13 @@ import pkg from './package.json';
// https://vitejs.dev/config/
export default defineConfig(({ command }) => {
- rmSync('dist-electron', { recursive: true, force: true });
-
+ rmSync('dist-electron', {
+ recursive: true,
+ force: true,
+ });
const isServe = command === 'serve';
const isBuild = command === 'build';
const sourcemap = isServe || !!process.env.VSCODE_DEBUG;
-
return {
resolve: {
alias: {
diff --git a/bun.lockb b/bun.lockb
index 59dd36252..aa33e133e 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/cli/bun.lockb b/cli/bun.lockb
index b2e51cdb3..4c2b5409f 100755
Binary files a/cli/bun.lockb and b/cli/bun.lockb differ
diff --git a/cli/package.json b/cli/package.json
index f4ff291b6..a4638411f 100644
--- a/cli/package.json
+++ b/cli/package.json
@@ -1,52 +1,42 @@
{
- "name": "onlook",
- "description": "The Onlook Command Line Interface",
- "version": "0.0.8",
- "main": "dist/api/index.mjs",
- "module": "dist/api/index.mjs",
- "bin": {
- "onlook": "dist/cli/index.cjs"
- },
- "directories": {
- "test": "tests"
- },
- "scripts": {
- "dev": "npm run esbuild -- --watch",
- "esbuild": "esbuild src/cli/index.ts --bundle --platform=node --format=cjs --outfile=dist/cli/index.cjs",
- "build": "tsc --noEmit --skipLibCheck src/cli/index.ts && esbuild src/cli/index.ts --bundle --platform=node --format=cjs --outfile=dist/cli/index.cjs --define:PACKAGE_VERSION=\\\"$npm_package_version\\\"",
- "build:notype": "npm run esbuild",
- "build:api": "tsc --noEmit --skipLibCheck src/api/index.ts && esbuild src/api/index.ts --bundle --platform=node --format=esm --outfile=dist/api/index.mjs",
- "test": "bun test"
- },
- "keywords": [
- "npx",
- "onlook",
- "setup",
- "plugins"
- ],
- "author": {
- "name": "Onlook",
- "email": "contact@onlook.dev"
- },
- "license": "Apache-2.0",
- "homepage": "https://onlook.dev",
- "devDependencies": {
- "@types/babel__generator": "^7.6.8",
- "@types/babel__traverse": "^7.20.6",
- "@types/bun": "latest",
- "@types/degit": "^2.8.6",
- "esbuild": "^0.23.1",
- "tslib": "^2.6.3",
- "typescript": "^5.0.0"
- },
- "dependencies": {
- "@babel/generator": "^7.14.5",
- "@babel/parser": "^7.14.3",
- "@babel/traverse": "^7.14.5",
- "@babel/types": "^7.14.5",
- "commander": "^12.1.0",
- "degit": "^2.8.4",
- "glob": "^11.0.0",
- "ora": "^8.1.0"
- }
+ "name": "onlook",
+ "description": "The Onlook Command Line Interface",
+ "version": "0.0.8",
+ "main": "dist/index.js",
+ "bin": {
+ "onlook": "dist/index.cjs"
+ },
+ "directories": {
+ "test": "tests"
+ },
+ "scripts": {
+ "esbuild": "esbuild src/index.ts --bundle --platform=node --format=cjs --outfile=dist/index.cjs",
+ "dev": "npm run esbuild -- --watch",
+ "build": "tsc --noEmit --skipLibCheck src/index.ts && esbuild src/index.ts --bundle --platform=node --format=cjs --outfile=dist/index.cjs --define:PACKAGE_VERSION=\\\"$npm_package_version\\\"",
+ "build:notype": "npm run esbuild",
+ "test": "bun test"
+ },
+ "keywords": [
+ "npx",
+ "onlook",
+ "setup",
+ "plugins"
+ ],
+ "author": {
+ "name": "Onlook",
+ "email": "contact@onlook.dev"
+ },
+ "license": "Apache-2.0",
+ "homepage": "https://onlook.dev",
+ "devDependencies": {
+ "@types/bun": "latest",
+ "esbuild": "^0.23.1",
+ "tslib": "^2.6.3",
+ "typescript": "^5.0.0"
+ },
+ "dependencies": {
+ "@onlook/utils": "^0.0.3",
+ "commander": "^12.1.0",
+ "ora": "^8.1.0"
+ }
}
\ No newline at end of file
diff --git a/cli/src/api/index.ts b/cli/src/api/index.ts
deleted file mode 100644
index e21f5d5f6..000000000
--- a/cli/src/api/index.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import type { ApiResponse } from "../models";
-
-export function isOnlookEnabled(folder: string): Promise
> {
- return Promise.resolve({
- status: 'success',
- data: true
- });
-}
-
-export function createProject(folder: string, name: string): Promise {
- return Promise.resolve({
- status: 'success'
- });
-}
\ No newline at end of file
diff --git a/cli/src/create/constant.ts b/cli/src/create/constant.ts
deleted file mode 100644
index ed2c05d61..000000000
--- a/cli/src/create/constant.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const NEXT_TEMPLATE_REPO = 'onlook-dev/starter';
diff --git a/cli/src/create/index.ts b/cli/src/create/index.ts
index d0066f961..cb6c958b2 100644
--- a/cli/src/create/index.ts
+++ b/cli/src/create/index.ts
@@ -1,54 +1,36 @@
-import { exec } from 'child_process';
-import * as fs from 'fs';
+import { createProject, CreateStage, type CreateCallback } from '@onlook/utils';
import ora from 'ora';
-import * as path from 'path';
-import { promisify } from 'util';
-import { NEXT_TEMPLATE_REPO } from './constant';
-
-// @ts-ignore
-import degit from 'degit';
-
-const execAsync = promisify(exec);
-
-export async function create(projectName: string) {
- const targetPath = path.join(process.cwd(), projectName);
+export async function create(projectName: string): Promise {
console.log(`Creating a new Onlook project: ${projectName}`);
-
- // Check if the directory already exists
- if (fs.existsSync(targetPath)) {
- console.error(`Error: Directory ${projectName} already exists.`);
- process.exit(1);
+ const targetPath = process.cwd();
+ const spinner = ora('Initializing project...').start();
+
+ const progressCallback: CreateCallback = (stage: CreateStage, message: string) => {
+ switch (stage) {
+ case CreateStage.CLONING:
+ spinner.text = 'Cloning template...';
+ break;
+ case CreateStage.INSTALLING:
+ spinner.text = 'Installing dependencies...';
+ break;
+ case CreateStage.COMPLETE:
+ spinner.succeed('Project created successfully!');
+ console.log('\nTo get started:');
+ console.log(` cd ${projectName}`);
+ console.log(' npm run dev');
+ break;
+ case CreateStage.ERROR:
+ spinner.fail(message);
+ break;
+ }
}
- const spinner = ora('Initializing project').start();
-
try {
- // Clone the template using degit
- spinner.text = 'Cloning template';
- const emitter = degit(NEXT_TEMPLATE_REPO, {
- cache: false,
- force: true,
- verbose: true,
- });
-
- await emitter.clone(targetPath);
-
- // Change to the project directory
- process.chdir(targetPath);
-
- // Install dependencies
- spinner.text = 'Installing dependencies';
- await execAsync('npm install');
-
- spinner.succeed('Project created successfully');
-
- console.log('\nTo get started:');
- console.log(` cd ${projectName}`);
- console.log(' npm run dev');
+ await createProject(projectName, targetPath, progressCallback);
} catch (error) {
- spinner.fail('Project creation failed');
- console.error('An error occurred:', error);
+ spinner.fail('An error occurred');
+ console.error('Error details:', error);
process.exit(1);
}
}
\ No newline at end of file
diff --git a/cli/src/cli/index.ts b/cli/src/index.ts
similarity index 91%
rename from cli/src/cli/index.ts
rename to cli/src/index.ts
index f8db9eb2d..fb96dbec5 100644
--- a/cli/src/cli/index.ts
+++ b/cli/src/index.ts
@@ -1,7 +1,7 @@
#!/usr/bin/env node
import { Command } from 'commander';
-import { create } from '../create';
-import { setup } from '../setup';
+import { create } from './create';
+import { setup } from './setup';
declare let PACKAGE_VERSION: string;
diff --git a/cli/src/models.ts b/cli/src/models.ts
deleted file mode 100644
index fae847be6..000000000
--- a/cli/src/models.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-export type ApiResponse = SuccessResponse | ErrorResponse;
-
-export type SuccessResponse = {
- status: 'success';
- data?: T;
-};
-
-export type ErrorResponse = {
- status: 'error';
- error: {
- message: string;
- code: string;
- };
-};
\ No newline at end of file
diff --git a/cli/src/setup/constants.ts b/cli/src/setup/constants.ts
deleted file mode 100644
index 4d889a2d7..000000000
--- a/cli/src/setup/constants.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-export enum BUILD_TOOL_NAME {
- NEXT = "next",
- WEBPACK = "webpack",
- CRA = "cra",
- VITE = "vite",
-}
-
-export enum DEPENDENCY_NAME {
- NEXT = "next",
- WEBPACK = "webpack",
- CRA = "react-scripts",
- VITE = "vite"
-}
-
-export const NEXTJS_CONFIG_BASE_NAME = 'next.config';
-export const WEBPACK_CONFIG_BASE_NAME = 'webpack.config';
-export const VITEJS_CONFIG_BASE_NAME = 'vite.config';
-
-export const CONFIG_FILE_PATTERN: Record = {
- [BUILD_TOOL_NAME.NEXT]: `${NEXTJS_CONFIG_BASE_NAME}.*`,
- [BUILD_TOOL_NAME.WEBPACK]: `${WEBPACK_CONFIG_BASE_NAME}.*`,
- [BUILD_TOOL_NAME.VITE]: `${VITEJS_CONFIG_BASE_NAME}.*`,
- [BUILD_TOOL_NAME.CRA]: ''
-}
-
-export const PACKAGE_JSON = 'package.json';
-
-export enum LOCK_FILE_NAME {
- YARN = 'yarn.lock',
- BUN = 'bun.lockb',
- PNPM = 'pnpm-lock.yaml'
-}
-
-export enum PACKAGE_MANAGER {
- YARN = 'yarn',
- NPM = 'npm',
- PNPM = 'pnpm',
- BUN = 'bun'
-}
-
-export const ONLOOK_NEXTJS_PLUGIN = '@onlook/nextjs';
-export const ONLOOK_WEBPACK_PLUGIN = '@onlook/react';
-export const ONLOOK_BABEL_PLUGIN = '@onlook/babel-plugin-react';
-
-export const NEXTJS_COMMON_FILES = ['pages', 'app', 'src/pages', 'src/app'];
-export const CRA_COMMON_FILES = ['public', 'src'];
-
-export const CONFIG_OVERRIDES_FILE = 'config-overrides.js';
-export const BABELRC_FILE = '.babelrc';
-
-export const JS_FILE_EXTENSION = '.js';
-export const MJS_FILE_EXTENSION = '.mjs';
-export const TS_FILE_EXTENSION = '.ts';
-
-export const NEXT_DEPENDENCIES = [ONLOOK_NEXTJS_PLUGIN];
-export const CRA_DEPENDENCIES = [ONLOOK_BABEL_PLUGIN, 'customize-cra', 'react-app-rewired'];
-export const VITE_DEPENDENCIES = [ONLOOK_BABEL_PLUGIN];
-
-export const WEBPACK_DEPENDENCIES = [
- ONLOOK_BABEL_PLUGIN,
- 'babel-loader',
- '@babel/preset-react',
- '@babel/core',
- '@babel/preset-env',
- 'webpack'
-];
diff --git a/cli/src/setup/index.ts b/cli/src/setup/index.ts
index 1440b6c70..f097b6cb9 100755
--- a/cli/src/setup/index.ts
+++ b/cli/src/setup/index.ts
@@ -1,28 +1,37 @@
-import { CRA_DEPENDENCIES } from './constants';
-import { ensureConfigOverrides, isCRAProject, modifyStartScript } from './cra';
-import { Framework } from './frameworks';
-import { installPackages } from './utils';
+import { type SetupCallback, setupProject, SetupStage } from '@onlook/utils';
+import ora from 'ora';
export const setup = async (): Promise => {
- try {
- for (const framework of Framework.getAll()) {
- const updated = await framework.run();
- if (updated) {
- return;
- }
+ const targetPath = process.cwd();
+ const spinner = ora('Initializing project...').start();
+
+ const progressCallback: SetupCallback = (stage: SetupStage, message: string) => {
+ switch (stage) {
+ case SetupStage.INSTALLING:
+ spinner.text = 'Cloning template...';
+ break;
+ case SetupStage.CONFIGURING:
+ spinner.text = 'Installing dependencies...';
+ break;
+ case SetupStage.COMPLETE:
+ spinner.succeed('Project created successfully!');
+ console.log('\nTo get started:');
+ console.log(` cd ${targetPath}`);
+ console.log(' npm run dev');
+ break;
+ case SetupStage.ERROR:
+ spinner.fail(message);
+ break;
+ }
}
- if (await isCRAProject()) {
- console.log('This is a create-react-app project.');
- await installPackages(CRA_DEPENDENCIES);
- ensureConfigOverrides();
- modifyStartScript();
- return;
+ try {
+ await setupProject(targetPath, progressCallback);
+ } catch (error) {
+ spinner.fail('An error occurred');
+ console.error('Error details:', error);
+ process.exit(1);
}
+};
- console.warn('Cannot determine the project framework.', '\nIf this is unexpected, see: https://github.com/onlook-dev/onlook/wiki/How-to-set-up-my-project%3F#do-it-manually');
- } catch (err) {
- console.error(err);
- }
-};
diff --git a/cli/src/setup/next.ts b/cli/src/setup/next.ts
deleted file mode 100644
index 922212219..000000000
--- a/cli/src/setup/next.ts
+++ /dev/null
@@ -1,184 +0,0 @@
-#!/usr/bin/env node
-import * as fs from 'fs';
-import * as path from 'path';
-
-import generate from '@babel/generator';
-import { parse } from '@babel/parser';
-import traverse from '@babel/traverse';
-import * as t from '@babel/types';
-
-import {
- checkVariableDeclarationExist,
- exists,
- genASTParserOptionsByFileExtension,
- genImportDeclaration,
- hasDependency,
- isSupportFileExtension
-} from './utils';
-
-import {
- BUILD_TOOL_NAME,
- CONFIG_FILE_PATTERN,
- DEPENDENCY_NAME,
- NEXTJS_COMMON_FILES,
- NEXTJS_CONFIG_BASE_NAME,
- ONLOOK_NEXTJS_PLUGIN,
-} from './constants';
-
-export const isNextJsProject = async (): Promise => {
- try {
- const configPath = CONFIG_FILE_PATTERN[BUILD_TOOL_NAME.NEXT];
-
- // Check if the configuration file exists
- if (await exists(configPath)) {
- return true;
- }
-
- // Check if the dependency exists
- if (!await hasDependency(DEPENDENCY_NAME.NEXT)) {
- return false;
- }
-
- // Check if one of the directories exists
- const directoryExists = await Promise.all(NEXTJS_COMMON_FILES.map(exists));
-
- return directoryExists.some(Boolean);
- } catch (err) {
- console.error(err);
- return false;
- }
-};
-
-export const modifyNextConfig = (configFileExtension: string): void => {
- if (!isSupportFileExtension(configFileExtension)) {
- console.error('Unsupported file extension');
- return;
- }
-
- const configFileName = `${NEXTJS_CONFIG_BASE_NAME}${configFileExtension}`;
-
- // Define the path to next.config.* file
- const configPath = path.resolve(process.cwd(), configFileName);
-
- if (!fs.existsSync(configPath)) {
- console.error(`${configFileName} not found`);
- return;
- }
-
- console.log(`Adding ${ONLOOK_NEXTJS_PLUGIN} plugin into ${configFileName} file...`);
-
- // Read the existing next.config.* file
- fs.readFile(configPath, 'utf8', (err, data) => {
- if (err) {
- console.error(`Error reading ${configPath}:`, err);
- return;
- }
-
- const astParserOption = genASTParserOptionsByFileExtension(configFileExtension);
-
- // Parse the file content to an AST
- const ast = parse(data, astParserOption);
-
- let hasPathImport = false;
-
- // Traverse the AST to find the experimental.swcPlugins array
- traverse(ast, {
- VariableDeclarator(path) {
- // check if path is imported in .js file
- if (checkVariableDeclarationExist(path, 'path')) {
- hasPathImport = true;
- }
- },
- ImportDeclaration(path) {
- // check if path is imported in .mjs file
- if (path.node.source.value === 'path') {
- hasPathImport = true;
- }
- },
- ObjectExpression(path) {
- const properties = path.node.properties;
- let experimentalProperty: t.ObjectProperty | undefined;
-
- // Find the experimental property
- properties.forEach(prop => {
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key, { name: 'experimental' })) {
- experimentalProperty = prop;
- }
- });
-
- if (!experimentalProperty) {
- // If experimental property is not found, create it
- experimentalProperty = t.objectProperty(
- t.identifier('experimental'),
- t.objectExpression([])
- );
- properties.push(experimentalProperty);
- }
-
- // Ensure experimental is an ObjectExpression
- if (!t.isObjectExpression(experimentalProperty.value)) {
- experimentalProperty.value = t.objectExpression([]);
- }
-
- const experimentalProperties = experimentalProperty.value.properties;
- let swcPluginsProperty: t.ObjectProperty | undefined;
-
- // Find the swcPlugins property
- experimentalProperties.forEach(prop => {
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key, { name: 'swcPlugins' })) {
- swcPluginsProperty = prop;
- }
- });
-
- if (!swcPluginsProperty) {
- // If swcPlugins property is not found, create it
- swcPluginsProperty = t.objectProperty(
- t.identifier('swcPlugins'),
- t.arrayExpression([])
- );
- experimentalProperties.push(swcPluginsProperty);
- }
-
- // Ensure swcPlugins is an ArrayExpression
- if (!t.isArrayExpression(swcPluginsProperty.value)) {
- swcPluginsProperty.value = t.arrayExpression([]);
- }
-
- // Add the new plugin configuration to swcPlugins array
- const pluginConfig = t.arrayExpression([
- t.stringLiteral(ONLOOK_NEXTJS_PLUGIN),
- t.objectExpression([
- t.objectProperty(
- t.identifier('root'),
- t.callExpression(t.memberExpression(t.identifier('path'), t.identifier('resolve')), [t.stringLiteral('.')])
- )
- ])
- ]);
-
- swcPluginsProperty.value.elements.push(pluginConfig);
-
- // Stop traversing after the modification
- path.stop();
- }
- });
-
- // If 'path' is not imported, add the import statement
- if (!hasPathImport) {
- const importDeclaration = genImportDeclaration(configFileExtension, 'path');
- importDeclaration && ast.program.body.unshift(importDeclaration);
- }
-
- // 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);
- return;
- }
-
- console.log(`Successfully updated ${configPath}`);
- });
- });
-};
diff --git a/cli/src/setup/utils.ts b/cli/src/setup/utils.ts
deleted file mode 100644
index 7799af6df..000000000
--- a/cli/src/setup/utils.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-import { NodePath } from '@babel/traverse';
-import * as t from '@babel/types';
-import { execSync } from 'child_process';
-import * as glob from 'glob';
-import * as path from 'path';
-import {
- JS_FILE_EXTENSION,
- LOCK_FILE_NAME,
- MJS_FILE_EXTENSION,
- PACKAGE_JSON,
- PACKAGE_MANAGER,
- TS_FILE_EXTENSION
-} from './constants';
-
-export const exists = async (filePattern: string): Promise => {
- try {
- const pattern = path.resolve(process.cwd(), filePattern);
- const files = getFileNamesByPattern(pattern);
- return files.length > 0;
- } catch (err) {
- console.error(err);
- return false;
- }
-};
-
-export const getFileNamesByPattern = (pattern: string): string[] => glob.globSync(pattern);
-
-export const installPackages = async (packages: string[]): Promise => {
- const packageManager = await getPackageManager();
- const command = packageManager === PACKAGE_MANAGER.YARN ? 'yarn add -D' : `${packageManager} install -D`;
-
- console.log("Package manager found:", packageManager)
- console.log("\n$", `${command} ${packages.join(' ')}`)
-
- execSync(`${command} ${packages.join(' ')}`, { stdio: 'inherit' });
-};
-
-export const getPackageManager = async (): Promise => {
- try {
- if (await exists(LOCK_FILE_NAME.YARN)) {
- return PACKAGE_MANAGER.YARN;
- }
- if (await exists(LOCK_FILE_NAME.PNPM)) {
- return PACKAGE_MANAGER.PNPM;
- }
- if (await exists(LOCK_FILE_NAME.BUN)) {
- return PACKAGE_MANAGER.BUN;
- }
- return PACKAGE_MANAGER.NPM;
- } catch (e) {
- console.error("Error determining package manager, using npm by default", e)
- return PACKAGE_MANAGER.NPM
- }
-
-};
-
-export const hasDependency = async (dependencyName: string): Promise => {
- const packageJsonPath = path.resolve(PACKAGE_JSON);
- if (await exists(packageJsonPath)) {
- const packageJson = require(packageJsonPath);
- return (
- (packageJson.dependencies && packageJson.dependencies[dependencyName]) ||
- (packageJson.devDependencies && packageJson.devDependencies[dependencyName])
- );
- }
- return false;
-};
-
-export const getFileExtensionByPattern = async (dir: string, filePattern: string): Promise => {
- const fullDirPattern = path.resolve(dir, filePattern);
- const files = await getFileNamesByPattern(fullDirPattern);
-
- if (files.length > 0) {
- return path.extname(files[0]);
- }
-
- return null;
-};
-
-export const genASTParserOptionsByFileExtension = (fileExtension: string, sourceType: string = 'module'): object => {
- switch (fileExtension) {
- case JS_FILE_EXTENSION:
- return {
- sourceType: sourceType
- };
- case MJS_FILE_EXTENSION:
- return {
- sourceType: sourceType,
- plugins: ['jsx']
- };
- case TS_FILE_EXTENSION:
- return {
- sourceType: sourceType,
- plugins: ['typescript']
- };
- default:
- return {};
- }
-};
-
-export const genImportDeclaration = (fileExtension: string, dependency: string): t.VariableDeclaration | t.ImportDeclaration | null => {
- switch (fileExtension) {
- case JS_FILE_EXTENSION:
- return t.variableDeclaration('const', [
- t.variableDeclarator(
- t.identifier(dependency),
- t.callExpression(t.identifier('require'), [t.stringLiteral(dependency)])
- )
- ]);
- case MJS_FILE_EXTENSION:
- return t.importDeclaration(
- [t.importDefaultSpecifier(t.identifier(dependency))],
- t.stringLiteral(dependency)
- );
- default:
- return null;
- }
-};
-
-export const checkVariableDeclarationExist = (path: NodePath, dependency: string): boolean => {
- return t.isIdentifier(path.node.id, { name: dependency }) &&
- t.isCallExpression(path.node.init) &&
- (path.node.init.callee as t.V8IntrinsicIdentifier).name === 'require' &&
- (path.node.init.arguments[0] as any).value === dependency;
-};
-
-export const isSupportFileExtension = (fileExtension: string): boolean => {
- return [JS_FILE_EXTENSION, MJS_FILE_EXTENSION].indexOf(fileExtension) !== -1;
-};
-
-export const isViteProjectSupportFileExtension = (fileExtension: string): boolean => {
- return [JS_FILE_EXTENSION, TS_FILE_EXTENSION].indexOf(fileExtension) !== -1;
-};
diff --git a/cli/tests/program.test.ts b/cli/tests/program.test.ts
index 948260a5d..2025d2558 100644
--- a/cli/tests/program.test.ts
+++ b/cli/tests/program.test.ts
@@ -1,5 +1,5 @@
import { afterAll, expect, jest, mock, test } from "bun:test";
-import { createProgram } from "../src/cli";
+import { createProgram } from "../src";
import { setup } from "../src/setup";
const originalConsoleLog = console.log;
diff --git a/docs/bun.lockb b/docs/bun.lockb
index 024575ca7..f6656f303 100755
Binary files a/docs/bun.lockb and b/docs/bun.lockb differ
diff --git a/docs/next.config.mjs b/docs/next.config.mjs
index 40252a559..eb7566655 100644
--- a/docs/next.config.mjs
+++ b/docs/next.config.mjs
@@ -1,10 +1,10 @@
import path from "path";
-
const nextConfig = {
- reactStrictMode: true,
- experimental: {
- swcPlugins: [["@onlook/nextjs", { root: path.resolve(".") }]],
- },
-}
-
-export default nextConfig
+ reactStrictMode: true,
+ experimental: {
+ swcPlugins: [["@onlook/nextjs", {
+ root: path.resolve(".")
+ }]]
+ }
+};
+export default nextConfig;
\ No newline at end of file
diff --git a/docs/package.json b/docs/package.json
index c6b46d54c..2e63dcb63 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -1,44 +1,44 @@
{
- "name": "@onlook/docs",
- "version": "0.0.0",
- "private": true,
- "scripts": {
- "dev": "next dev",
- "build": "next build",
- "start": "next start",
- "lint": "next lint",
- "format": "prettier ./src --write",
- "format:check": "prettier ./src --check"
- },
- "dependencies": {
- "@radix-ui/react-dropdown-menu": "^2.0.6",
- "@radix-ui/react-slot": "^1.0.2",
- "@t3-oss/env-nextjs": "^0.9.2",
- "class-variance-authority": "^0.7.0",
- "clsx": "^2.1.1",
- "lucide-react": "^0.330.0",
- "next": "14.2.10",
- "next-entree": ".",
- "next-themes": "^0.2.1",
- "react": "^18.3.1",
- "react-dom": "^18.3.1",
- "tailwind-merge": "^2.3.0",
- "tailwindcss-animate": "^1.0.7",
- "zod": "^3.23.4"
- },
- "devDependencies": {
- "@ianvs/prettier-plugin-sort-imports": "^4.2.1",
- "@onlook/nextjs": "latest",
- "@types/node": "^20.12.7",
- "@types/react": "^18.3.1",
- "@types/react-dom": "^18.3.0",
- "autoprefixer": "^10.4.19",
- "eslint": "^8.57.0",
- "eslint-config-next": "14.1.0",
- "postcss": "^8.4.38",
- "prettier": "^3.2.5",
- "prettier-plugin-tailwindcss": "^0.5.14",
- "tailwindcss": "^3.4.3",
- "typescript": "^5.4.5"
- }
+ "name": "@onlook/docs",
+ "version": "0.0.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint",
+ "format": "prettier ./src --write",
+ "format:check": "prettier ./src --check"
+ },
+ "dependencies": {
+ "@radix-ui/react-dropdown-menu": "^2.0.6",
+ "@radix-ui/react-slot": "^1.0.2",
+ "@t3-oss/env-nextjs": "^0.9.2",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.330.0",
+ "next": "14.2.10",
+ "next-entree": ".",
+ "next-themes": "^0.2.1",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "tailwind-merge": "^2.3.0",
+ "tailwindcss-animate": "^1.0.7",
+ "zod": "^3.23.4"
+ },
+ "devDependencies": {
+ "@ianvs/prettier-plugin-sort-imports": "^4.2.1",
+ "@onlook/nextjs": "^2.1.1",
+ "@types/node": "^20.12.7",
+ "@types/react": "^18.3.1",
+ "@types/react-dom": "^18.3.0",
+ "autoprefixer": "^10.4.19",
+ "eslint": "^8.57.0",
+ "eslint-config-next": "14.1.0",
+ "postcss": "^8.4.38",
+ "prettier": "^3.2.5",
+ "prettier-plugin-tailwindcss": "^0.5.14",
+ "tailwindcss": "^3.4.3",
+ "typescript": "^5.4.5"
+ }
}
\ No newline at end of file
diff --git a/package.json b/package.json
index c4122bd18..edd0ce543 100644
--- a/package.json
+++ b/package.json
@@ -1,25 +1,25 @@
{
- "name": "@onlook/studio",
- "version": "0.0.0",
- "description": "Onlook Studio",
- "homepage": "https://onlook.dev",
- "main": "dist-electron/main/index.js",
- "license": "Apache-2.0",
- "author": {
- "name": "Onlook",
- "email": "contact@onlook.dev"
- },
- "scripts": {
- "prepare": "husky"
- },
- "repository": {
- "type": "git",
- "url": "git+https://github.com/onlook-dev/onlook.git"
- },
- "bugs": {
- "url": "https://github.com/onlook-dev/onlook/issues"
- },
- "devDependencies": {
- "husky": "^9.0.11"
- }
+ "name": "@onlook/studio",
+ "version": "0.0.0",
+ "description": "Onlook Studio",
+ "homepage": "https://onlook.dev",
+ "main": "dist-electron/main/index.js",
+ "license": "Apache-2.0",
+ "author": {
+ "name": "Onlook",
+ "email": "contact@onlook.dev"
+ },
+ "scripts": {
+ "prepare": "husky"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/onlook-dev/onlook.git"
+ },
+ "bugs": {
+ "url": "https://github.com/onlook-dev/onlook/issues"
+ },
+ "devDependencies": {
+ "husky": "^9.0.11"
+ }
}
\ No newline at end of file
diff --git a/utils/.gitignore b/utils/.gitignore
new file mode 100644
index 000000000..03cf4ff52
--- /dev/null
+++ b/utils/.gitignore
@@ -0,0 +1,178 @@
+# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
+
+# Logs
+
+logs
+_.log
+npm-debug.log_
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Caches
+
+.cache
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
+
+# Runtime data
+
+pids
+_.pid
+_.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+
+lib-cov
+
+# Coverage directory used by tools like istanbul
+
+coverage
+*.lcov
+
+# nyc test coverage
+
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+
+bower_components
+
+# node-waf configuration
+
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+
+build/Release
+
+# Dependency directories
+
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+
+web_modules/
+
+# TypeScript cache
+
+*.tsbuildinfo
+
+# Optional npm cache directory
+
+.npm
+
+# Optional eslint cache
+
+.eslintcache
+
+# Optional stylelint cache
+
+.stylelintcache
+
+# Microbundle cache
+
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+
+.node_repl_history
+
+# Output of 'npm pack'
+
+*.tgz
+
+# Yarn Integrity file
+
+.yarn-integrity
+
+# dotenv environment variable files
+
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+
+.parcel-cache
+
+# Next.js build output
+
+.next
+out
+
+# Nuxt.js build / generate output
+
+.nuxt
+dist
+
+# Gatsby files
+
+# Comment in the public line in if your project uses Gatsby and not Next.js
+
+# https://nextjs.org/blog/next-9-1#public-directory-support
+
+# public
+
+# vuepress build output
+
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+
+.temp
+
+# Docusaurus cache and generated files
+
+.docusaurus
+
+# Serverless directories
+
+.serverless/
+
+# FuseBox cache
+
+.fusebox/
+
+# DynamoDB Local files
+
+.dynamodb/
+
+# TernJS port file
+
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+
+.vscode-test
+
+# yarn v2
+
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+# IntelliJ based IDEs
+.idea
+
+# Finder (MacOS) folder config
+.DS_Store
+
+# Bundle
+build
\ No newline at end of file
diff --git a/utils/README.md b/utils/README.md
new file mode 100644
index 000000000..44cc9d437
--- /dev/null
+++ b/utils/README.md
@@ -0,0 +1,7 @@
+# Onlook Utils
+
+A shared utility package for Onlook. Includes file system functionalities used in both the Onlook app and the CLI
+
+- [X] Create new project
+- [X] Setup existing project
+- [X] Verify Onlook installation
\ No newline at end of file
diff --git a/utils/bun.lockb b/utils/bun.lockb
new file mode 100755
index 000000000..9677c60a2
Binary files /dev/null and b/utils/bun.lockb differ
diff --git a/utils/package.json b/utils/package.json
new file mode 100644
index 000000000..78ff99772
--- /dev/null
+++ b/utils/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "@onlook/utils",
+ "description": "A shared utility library for Onlook",
+ "version": "0.0.3",
+ "type": "commonjs",
+ "main": "dist/index.js",
+ "module": "dist/index.mjs",
+ "types": "dist/index.d.ts",
+ "directories": {
+ "test": "tests"
+ },
+ "scripts": {
+ "dev": "npm run build -- --watch",
+ "build": "tsup src/index.ts --format esm,cjs --outDir dist --dts",
+ "test": "bun test"
+ },
+ "keywords": [
+ "onlook",
+ "utils"
+ ],
+ "author": {
+ "name": "Onlook",
+ "email": "contact@onlook.dev"
+ },
+ "license": "Apache-2.0",
+ "homepage": "https://onlook.dev",
+ "devDependencies": {
+ "@types/babel__generator": "^7.6.8",
+ "@types/babel__traverse": "^7.20.6",
+ "@types/bun": "latest",
+ "@types/degit": "^2.8.6",
+ "tslib": "^2.6.3",
+ "tsup": "^8.3.0",
+ "typescript": "^5.0.0"
+ },
+ "dependencies": {
+ "@babel/generator": "^7.14.5",
+ "@babel/parser": "^7.14.3",
+ "@babel/traverse": "^7.14.5",
+ "@babel/types": "^7.14.5",
+ "degit": "^2.8.4"
+ }
+}
\ No newline at end of file
diff --git a/utils/src/constants.ts b/utils/src/constants.ts
new file mode 100644
index 000000000..694ba0a4f
--- /dev/null
+++ b/utils/src/constants.ts
@@ -0,0 +1,72 @@
+export enum BUILD_TOOL_NAME {
+ NEXT = "next",
+ WEBPACK = "webpack",
+ CRA = "cra",
+ VITE = "vite",
+}
+
+export enum DEPENDENCY_NAME {
+ NEXT = "next",
+ WEBPACK = "webpack",
+ CRA = "react-scripts",
+ VITE = "vite"
+}
+
+export enum CONFIG_BASE_NAME {
+ NEXTJS = 'next.config',
+ WEBPACK = 'webpack.config',
+ VITEJS = 'vite.config'
+}
+
+export const CONFIG_FILE_PATTERN: Record = {
+ [BUILD_TOOL_NAME.NEXT]: `${CONFIG_BASE_NAME.NEXTJS}.*`,
+ [BUILD_TOOL_NAME.WEBPACK]: `${CONFIG_BASE_NAME.WEBPACK}.*`,
+ [BUILD_TOOL_NAME.VITE]: `${CONFIG_BASE_NAME.VITEJS}.*`,
+ [BUILD_TOOL_NAME.CRA]: ''
+}
+
+export const PACKAGE_JSON = 'package.json';
+
+export enum LOCK_FILE_NAME {
+ YARN = 'yarn.lock',
+ BUN = 'bun.lockb',
+ PNPM = 'pnpm-lock.yaml'
+}
+
+export enum PACKAGE_MANAGER {
+ YARN = 'yarn',
+ NPM = 'npm',
+ PNPM = 'pnpm',
+ BUN = 'bun'
+}
+
+export enum ONLOOK_PLUGIN {
+ NEXTJS = '@onlook/nextjs',
+ WEBPACK = '@onlook/react',
+ BABEL = '@onlook/babel-plugin-react'
+}
+
+export const NEXTJS_COMMON_FILES = ['pages', 'app', 'src/pages', 'src/app'];
+export const CRA_COMMON_FILES = ['public', 'src'];
+
+export const CONFIG_OVERRIDES_FILE = 'config-overrides.js';
+export const BABELRC_FILE = '.babelrc';
+
+export enum FILE_EXTENSION {
+ JS = '.js',
+ MJS = '.mjs',
+ TS = '.ts',
+}
+
+export const NEXT_DEPENDENCIES = [ONLOOK_PLUGIN.NEXTJS];
+export const CRA_DEPENDENCIES = [ONLOOK_PLUGIN.BABEL, 'customize-cra', 'react-app-rewired'];
+export const VITE_DEPENDENCIES = [ONLOOK_PLUGIN.BABEL];
+
+export const WEBPACK_DEPENDENCIES = [
+ ONLOOK_PLUGIN.BABEL,
+ 'babel-loader',
+ '@babel/preset-react',
+ '@babel/core',
+ '@babel/preset-env',
+ 'webpack'
+];
diff --git a/utils/src/create/index.ts b/utils/src/create/index.ts
new file mode 100644
index 000000000..99359e7aa
--- /dev/null
+++ b/utils/src/create/index.ts
@@ -0,0 +1,47 @@
+import { exec } from 'child_process';
+import degit from 'degit';
+import * as fs from 'fs';
+import * as path from 'path';
+import { promisify } from 'util';
+import { CreateStage, type CreateCallback } from '..';
+
+const NEXT_TEMPLATE_REPO = 'onlook-dev/starter';
+const execAsync = promisify(exec);
+
+export async function createProject(
+ projectName: string,
+ targetPath: string,
+ onProgress: CreateCallback
+): Promise {
+ const fullPath = path.join(targetPath, projectName);
+
+ // Check if the directory already exists
+ if (fs.existsSync(fullPath)) {
+ throw new Error(`Directory ${fullPath} already exists.`);
+ }
+
+ try {
+ // Clone the template using degit
+ onProgress(CreateStage.CLONING, `Cloning template...`);
+ const emitter = degit(NEXT_TEMPLATE_REPO, {
+ cache: false,
+ force: true,
+ verbose: true,
+ });
+
+ await emitter.clone(fullPath);
+
+ // Change to the project directory
+ process.chdir(fullPath);
+
+ // Install dependencies
+ onProgress(CreateStage.INSTALLING, 'Installing dependencies...');
+ await execAsync('npm install');
+
+ onProgress(CreateStage.COMPLETE, 'Project created successfully!');
+ } catch (error) {
+ onProgress(CreateStage.ERROR, `Project creation failed: ${error}`);
+ throw error;
+ }
+}
+
diff --git a/cli/src/setup/cra.ts b/utils/src/frameworks/cra.ts
similarity index 93%
rename from cli/src/setup/cra.ts
rename to utils/src/frameworks/cra.ts
index 3955f52ad..2fb76b964 100644
--- a/cli/src/setup/cra.ts
+++ b/utils/src/frameworks/cra.ts
@@ -1,5 +1,5 @@
-import { CONFIG_OVERRIDES_FILE, CRA_COMMON_FILES, DEPENDENCY_NAME, JS_FILE_EXTENSION, ONLOOK_WEBPACK_PLUGIN, PACKAGE_JSON } from "./constants";
-import { exists, genASTParserOptionsByFileExtension, hasDependency } from "./utils";
+import { CONFIG_OVERRIDES_FILE, CRA_COMMON_FILES, DEPENDENCY_NAME, FILE_EXTENSION, ONLOOK_PLUGIN, PACKAGE_JSON } from "../constants";
+import { exists, genASTParserOptionsByFileExtension, hasDependency } from "../utils";
import generate from '@babel/generator';
import { parse } from '@babel/parser';
@@ -19,11 +19,16 @@ const defaultContent = `
module.exports = override(
...addBabelPlugins(
- '${ONLOOK_WEBPACK_PLUGIN}'
+ '${ONLOOK_PLUGIN.WEBPACK}'
)
);
`;
+export const modifyCRAConfig = (): void => {
+ ensureConfigOverrides();
+ modifyStartScript();
+}
+
export const ensureConfigOverrides = (): void => {
// Handle the case when the file does not exist
if (!fs.existsSync(configOverridesPath)) {
@@ -39,7 +44,7 @@ export const ensureConfigOverrides = (): void => {
return;
}
// Read the existing file
- const ast = parse(fileContent, genASTParserOptionsByFileExtension(JS_FILE_EXTENSION));
+ const ast = parse(fileContent, genASTParserOptionsByFileExtension(FILE_EXTENSION.JS));
let hasCustomizeCraImport = false;
let hasOnlookReactPlugin = false;
@@ -63,7 +68,7 @@ export const ensureConfigOverrides = (): void => {
path.node.arguments.forEach(arg => {
if (t.isSpreadElement(arg) && t.isCallExpression(arg.argument) &&
t.isIdentifier(arg.argument.callee, { name: 'addBabelPlugins' }) &&
- arg.argument.arguments.some(pluginArg => t.isStringLiteral(pluginArg, { value: ONLOOK_WEBPACK_PLUGIN }))) {
+ arg.argument.arguments.some(pluginArg => t.isStringLiteral(pluginArg, { value: ONLOOK_PLUGIN.WEBPACK }))) {
hasOnlookReactPlugin = true;
}
});
@@ -93,7 +98,7 @@ export const ensureConfigOverrides = (): void => {
// @ts-ignore
path.node.right.arguments.push(
t.spreadElement(t.callExpression(t.identifier('addBabelPlugins'), [
- t.stringLiteral(ONLOOK_WEBPACK_PLUGIN)
+ t.stringLiteral(ONLOOK_PLUGIN.WEBPACK)
]))
);
}
diff --git a/cli/src/setup/frameworks.ts b/utils/src/frameworks/frameworks.ts
similarity index 72%
rename from cli/src/setup/frameworks.ts
rename to utils/src/frameworks/frameworks.ts
index 8885b5993..b9715429a 100644
--- a/cli/src/setup/frameworks.ts
+++ b/utils/src/frameworks/frameworks.ts
@@ -1,14 +1,16 @@
-import { BUILD_TOOL_NAME, CONFIG_FILE_PATTERN, NEXT_DEPENDENCIES, VITE_DEPENDENCIES, WEBPACK_DEPENDENCIES } from "./constants";
+import { BUILD_TOOL_NAME, CONFIG_FILE_PATTERN, CRA_DEPENDENCIES, NEXT_DEPENDENCIES, VITE_DEPENDENCIES, WEBPACK_DEPENDENCIES } from "../constants";
+import { getFileExtensionByPattern, installPackages } from "../utils";
+import { isCRAProject, modifyCRAConfig } from "./cra";
import { isNextJsProject, modifyNextConfig } from "./next";
-import { getFileExtensionByPattern, installPackages } from "./utils";
import { isViteJsProject, modifyViteConfig } from "./vite";
import { isWebpackProject, modifyWebpackConfig } from "./webpack";
+
export class Framework {
static readonly NEXT = new Framework("Next.js", isNextJsProject, modifyNextConfig, NEXT_DEPENDENCIES, BUILD_TOOL_NAME.NEXT);
static readonly VITE = new Framework("Vite", isViteJsProject, modifyViteConfig, VITE_DEPENDENCIES, BUILD_TOOL_NAME.VITE);
static readonly WEBPACK = new Framework("Webpack", isWebpackProject, modifyWebpackConfig, WEBPACK_DEPENDENCIES, BUILD_TOOL_NAME.WEBPACK);
- // static readonly CRA = new Framework("Create React App", isCRAProject, modifyCRAConfig, CRA_DEPENDENCIES, BUILD_TOOL_NAME.CRA);
+ static readonly CRA = new Framework("Create React App", isCRAProject, modifyCRAConfig, CRA_DEPENDENCIES, BUILD_TOOL_NAME.CRA);
private constructor(
public readonly name: string,
@@ -18,7 +20,9 @@ export class Framework {
public readonly buildToolName: BUILD_TOOL_NAME
) { }
- run = async (): Promise => {
+ setup = async (): Promise => {
+ console.log(process.cwd());
+ console.log(`Checking for ${this.name} configuration...`);
if (await this.identify()) {
console.log(`This is a ${this.name} project.`);
@@ -38,7 +42,7 @@ export class Framework {
this.NEXT,
this.VITE,
this.WEBPACK,
- // this.CRA,
+ this.CRA,
];
}
}
diff --git a/utils/src/frameworks/index.ts b/utils/src/frameworks/index.ts
new file mode 100644
index 000000000..9443518c5
--- /dev/null
+++ b/utils/src/frameworks/index.ts
@@ -0,0 +1,48 @@
+import { SetupStage, type SetupCallback } from "..";
+import { BUILD_TOOL_NAME, CONFIG_FILE_PATTERN, CRA_DEPENDENCIES, NEXT_DEPENDENCIES, VITE_DEPENDENCIES, WEBPACK_DEPENDENCIES } from "../constants";
+import { getFileExtensionByPattern, installPackages } from "../utils";
+import { isCRAProject, modifyCRAConfig } from "./cra";
+import { isNextJsProject, modifyNextConfig } from "./next";
+import { isViteJsProject, modifyViteConfig } from "./vite";
+import { isWebpackProject, modifyWebpackConfig } from "./webpack";
+
+export class Framework {
+ static readonly NEXT = new Framework("Next.js", isNextJsProject, modifyNextConfig, NEXT_DEPENDENCIES, BUILD_TOOL_NAME.NEXT);
+ static readonly VITE = new Framework("Vite", isViteJsProject, modifyViteConfig, VITE_DEPENDENCIES, BUILD_TOOL_NAME.VITE);
+ static readonly WEBPACK = new Framework("Webpack", isWebpackProject, modifyWebpackConfig, WEBPACK_DEPENDENCIES, BUILD_TOOL_NAME.WEBPACK);
+ static readonly CRA = new Framework("Create React App", isCRAProject, modifyCRAConfig, CRA_DEPENDENCIES, BUILD_TOOL_NAME.CRA);
+
+ private constructor(
+ public readonly name: string,
+ public readonly identify: () => Promise,
+ public readonly updateConfig: (configFileExtension: string) => void,
+ public readonly dependencies: string[],
+ public readonly buildToolName: BUILD_TOOL_NAME
+ ) { }
+
+ setup = async (callback: SetupCallback): Promise => {
+
+ if (await this.identify()) {
+ callback(SetupStage.INSTALLING, `Installing required packages for ${this.name}...`);
+
+ await installPackages(this.dependencies);
+
+ callback(SetupStage.CONFIGURING, `Applying ${this.name} configuration...`);
+ const configFileExtension = await getFileExtensionByPattern(process.cwd(), CONFIG_FILE_PATTERN[this.buildToolName]);
+ if (configFileExtension) {
+ await this.updateConfig(configFileExtension);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ static getAll(): Framework[] {
+ return [
+ this.NEXT,
+ this.VITE,
+ this.WEBPACK,
+ this.CRA,
+ ];
+ }
+}
diff --git a/utils/src/frameworks/next.ts b/utils/src/frameworks/next.ts
new file mode 100644
index 000000000..d3ba6659d
--- /dev/null
+++ b/utils/src/frameworks/next.ts
@@ -0,0 +1,184 @@
+#!/usr/bin/env node
+import * as fs from 'fs';
+import * as path from 'path';
+
+import generate from '@babel/generator';
+import { parse } from '@babel/parser';
+import traverse from '@babel/traverse';
+import * as t from '@babel/types';
+
+import {
+ checkVariableDeclarationExist,
+ exists,
+ genASTParserOptionsByFileExtension,
+ genImportDeclaration,
+ hasDependency,
+ isSupportFileExtension
+} from '../utils';
+
+import {
+ BUILD_TOOL_NAME,
+ CONFIG_BASE_NAME,
+ CONFIG_FILE_PATTERN,
+ DEPENDENCY_NAME,
+ NEXTJS_COMMON_FILES,
+ ONLOOK_PLUGIN,
+} from '../constants';
+
+export const isNextJsProject = async (): Promise => {
+ try {
+ const configPath = CONFIG_FILE_PATTERN[BUILD_TOOL_NAME.NEXT];
+
+ // Check if the configuration file exists
+ if (await exists(configPath)) {
+ return true;
+ }
+
+ // Check if the dependency exists
+ if (!await hasDependency(DEPENDENCY_NAME.NEXT)) {
+ return false;
+ }
+
+ // Check if one of the directories exists
+ const directoryExists = await Promise.all(NEXTJS_COMMON_FILES.map(exists));
+
+ return directoryExists.some(Boolean);
+ } catch (err) {
+ console.error(err);
+ return false;
+ }
+};
+
+export const modifyNextConfig = (configFileExtension: string): void => {
+ if (!isSupportFileExtension(configFileExtension)) {
+ console.error('Unsupported file extension');
+ return;
+ }
+
+ const configFileName = `${CONFIG_BASE_NAME.NEXTJS}${configFileExtension}`;
+
+ // Define the path to next.config.* file
+ const configPath = path.resolve(process.cwd(), configFileName);
+
+ if (!fs.existsSync(configPath)) {
+ console.error(`${configFileName} not found`);
+ return;
+ }
+
+ console.log(`Adding ${ONLOOK_PLUGIN.NEXTJS} plugin into ${configFileName} file...`);
+
+ // Read the existing next.config.* file
+ fs.readFile(configPath, 'utf8', (err, data) => {
+ if (err) {
+ console.error(`Error reading ${configPath}:`, err);
+ return;
+ }
+
+ const astParserOption = genASTParserOptionsByFileExtension(configFileExtension);
+
+ // Parse the file content to an AST
+ const ast = parse(data, astParserOption);
+
+ let hasPathImport = false;
+
+ // Traverse the AST to find the experimental.swcPlugins array
+ traverse(ast, {
+ VariableDeclarator(path) {
+ // check if path is imported in .js file
+ if (checkVariableDeclarationExist(path, 'path')) {
+ hasPathImport = true;
+ }
+ },
+ ImportDeclaration(path) {
+ // check if path is imported in .mjs file
+ if (path.node.source.value === 'path') {
+ hasPathImport = true;
+ }
+ },
+ ObjectExpression(path) {
+ const properties = path.node.properties;
+ let experimentalProperty: t.ObjectProperty | undefined;
+
+ // Find the experimental property
+ properties.forEach(prop => {
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key, { name: 'experimental' })) {
+ experimentalProperty = prop;
+ }
+ });
+
+ if (!experimentalProperty) {
+ // If experimental property is not found, create it
+ experimentalProperty = t.objectProperty(
+ t.identifier('experimental'),
+ t.objectExpression([])
+ );
+ properties.push(experimentalProperty);
+ }
+
+ // Ensure experimental is an ObjectExpression
+ if (!t.isObjectExpression(experimentalProperty.value)) {
+ experimentalProperty.value = t.objectExpression([]);
+ }
+
+ const experimentalProperties = experimentalProperty.value.properties;
+ let swcPluginsProperty: t.ObjectProperty | undefined;
+
+ // Find the swcPlugins property
+ experimentalProperties.forEach(prop => {
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key, { name: 'swcPlugins' })) {
+ swcPluginsProperty = prop;
+ }
+ });
+
+ if (!swcPluginsProperty) {
+ // If swcPlugins property is not found, create it
+ swcPluginsProperty = t.objectProperty(
+ t.identifier('swcPlugins'),
+ t.arrayExpression([])
+ );
+ experimentalProperties.push(swcPluginsProperty);
+ }
+
+ // Ensure swcPlugins is an ArrayExpression
+ if (!t.isArrayExpression(swcPluginsProperty.value)) {
+ swcPluginsProperty.value = t.arrayExpression([]);
+ }
+
+ // Add the new plugin configuration to swcPlugins array
+ const pluginConfig = t.arrayExpression([
+ t.stringLiteral(ONLOOK_PLUGIN.NEXTJS),
+ t.objectExpression([
+ t.objectProperty(
+ t.identifier('root'),
+ t.callExpression(t.memberExpression(t.identifier('path'), t.identifier('resolve')), [t.stringLiteral('.')])
+ )
+ ])
+ ]);
+
+ swcPluginsProperty.value.elements.push(pluginConfig);
+
+ // Stop traversing after the modification
+ path.stop();
+ }
+ });
+
+ // If 'path' is not imported, add the import statement
+ if (!hasPathImport) {
+ const importDeclaration = genImportDeclaration(configFileExtension, 'path');
+ importDeclaration && ast.program.body.unshift(importDeclaration);
+ }
+
+ // 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);
+ return;
+ }
+
+ console.log(`Successfully updated ${configPath}`);
+ });
+ });
+};
diff --git a/cli/src/setup/vite.ts b/utils/src/frameworks/vite.ts
similarity index 95%
rename from cli/src/setup/vite.ts
rename to utils/src/frameworks/vite.ts
index b6ca41d1b..f102ca8b8 100644
--- a/cli/src/setup/vite.ts
+++ b/utils/src/frameworks/vite.ts
@@ -7,17 +7,17 @@ import * as path from 'path';
import {
BUILD_TOOL_NAME,
+ CONFIG_BASE_NAME,
CONFIG_FILE_PATTERN,
DEPENDENCY_NAME,
- ONLOOK_BABEL_PLUGIN,
- VITEJS_CONFIG_BASE_NAME
-} from "./constants";
+ ONLOOK_PLUGIN,
+} from "../constants";
import {
exists,
genASTParserOptionsByFileExtension,
hasDependency,
isViteProjectSupportFileExtension
-} from "./utils";
+} from "../utils";
// Function to check if a plugin is already in the array
function hasPlugin(pluginsArray: t.Expression[], pluginName: string): boolean {
@@ -56,7 +56,7 @@ export const modifyViteConfig = (configFileExtension: string): void => {
return;
}
- const configFileName = `${VITEJS_CONFIG_BASE_NAME}${configFileExtension}`;
+ const configFileName = `${CONFIG_BASE_NAME.VITEJS}${configFileExtension}`;
const configPath = path.resolve(process.cwd(), configFileName);
if (!fs.existsSync(configPath)) {
@@ -127,7 +127,7 @@ export const modifyViteConfig = (configFileExtension: string): void => {
t.objectExpression([
t.objectProperty(
t.identifier('plugins'),
- t.arrayExpression([t.stringLiteral(ONLOOK_BABEL_PLUGIN)])
+ t.arrayExpression([t.stringLiteral(ONLOOK_PLUGIN.BABEL)])
)
])
)
@@ -148,7 +148,7 @@ export const modifyViteConfig = (configFileExtension: string): void => {
t.objectExpression([
t.objectProperty(
t.identifier('plugins'),
- t.arrayExpression([t.stringLiteral(ONLOOK_BABEL_PLUGIN)])
+ t.arrayExpression([t.stringLiteral(ONLOOK_PLUGIN.BABEL)])
)
])
)
@@ -170,7 +170,7 @@ export const modifyViteConfig = (configFileExtension: string): void => {
t.objectExpression([
t.objectProperty(
t.identifier('plugins'),
- t.arrayExpression([t.stringLiteral(ONLOOK_BABEL_PLUGIN)])
+ t.arrayExpression([t.stringLiteral(ONLOOK_PLUGIN.BABEL)])
)
])
);
@@ -185,14 +185,14 @@ export const modifyViteConfig = (configFileExtension: string): void => {
if (!pluginsProp) {
pluginsProp = t.objectProperty(
t.identifier('plugins'),
- t.arrayExpression([t.stringLiteral(ONLOOK_BABEL_PLUGIN)])
+ t.arrayExpression([t.stringLiteral(ONLOOK_PLUGIN.BABEL)])
);
babelProp.value.properties.push(pluginsProp);
reactPluginAdded = true;
onlookBabelPluginAdded = true;
} else if (t.isArrayExpression(pluginsProp.value)) {
- if (!hasPlugin(pluginsProp.value.elements as t.Expression[], ONLOOK_BABEL_PLUGIN)) {
- pluginsProp.value.elements.push(t.stringLiteral(ONLOOK_BABEL_PLUGIN));
+ if (!hasPlugin(pluginsProp.value.elements as t.Expression[], ONLOOK_PLUGIN.BABEL)) {
+ pluginsProp.value.elements.push(t.stringLiteral(ONLOOK_PLUGIN.BABEL));
reactPluginAdded = true;
onlookBabelPluginAdded = true;
}
@@ -213,7 +213,7 @@ export const modifyViteConfig = (configFileExtension: string): void => {
console.log(`React plugin added to ${configFileName}`);
}
if (onlookBabelPluginAdded) {
- console.log(`${ONLOOK_BABEL_PLUGIN} plugin added to ${configFileName}`);
+ console.log(`${ONLOOK_PLUGIN.BABEL} plugin added to ${configFileName}`);
}
if (reactImportAdded) {
console.log(`React import added to ${configFileName}`);
diff --git a/cli/src/setup/webpack.ts b/utils/src/frameworks/webpack.ts
similarity index 95%
rename from cli/src/setup/webpack.ts
rename to utils/src/frameworks/webpack.ts
index be6af8662..e169f6cec 100644
--- a/cli/src/setup/webpack.ts
+++ b/utils/src/frameworks/webpack.ts
@@ -8,13 +8,13 @@ import * as path from 'path';
import {
BABELRC_FILE,
BUILD_TOOL_NAME,
+ CONFIG_BASE_NAME,
CONFIG_FILE_PATTERN,
DEPENDENCY_NAME,
- ONLOOK_WEBPACK_PLUGIN,
- WEBPACK_CONFIG_BASE_NAME
-} from "./constants";
+ ONLOOK_PLUGIN,
+} from "../constants";
-import { exists, hasDependency, isSupportFileExtension } from "./utils";
+import { exists, hasDependency, isSupportFileExtension } from "../utils";
export const isWebpackProject = async (): Promise => {
try {
@@ -60,7 +60,7 @@ export function modifyWebpackConfig(configFileExtension: string): void {
return;
}
- const configFileName = `${WEBPACK_CONFIG_BASE_NAME}${configFileExtension}`;
+ const configFileName = `${CONFIG_BASE_NAME.WEBPACK}${configFileExtension}`;
// Define the path to webpack.config.* file
const configPath = path.resolve(process.cwd(), configFileName);
@@ -157,9 +157,9 @@ export const modifyBabelrc = (): void => {
}
// Check if "@onlook/react" is already in the plugins array
- if (!babelrcContent.plugins.includes(ONLOOK_WEBPACK_PLUGIN)) {
+ if (!babelrcContent.plugins.includes(ONLOOK_PLUGIN.WEBPACK)) {
// Add "@onlook/react" to the plugins array
- babelrcContent.plugins.push(ONLOOK_WEBPACK_PLUGIN);
+ babelrcContent.plugins.push(ONLOOK_PLUGIN.WEBPACK);
}
// Write the updated content back to the .babelrc file
diff --git a/utils/src/index.ts b/utils/src/index.ts
new file mode 100644
index 000000000..c23aba96f
--- /dev/null
+++ b/utils/src/index.ts
@@ -0,0 +1,28 @@
+export { createProject } from './create';
+export { setupProject } from './setup';
+export { verifyProject } from './verify';
+
+export enum CreateStage {
+ CLONING = 'cloning',
+ INSTALLING = 'installing',
+ COMPLETE = 'complete',
+ ERROR = 'error'
+}
+
+export enum VerifyStage {
+ CHECKING = 'checking',
+ NOT_INSTALLED = 'not_installed',
+ INSTALLED = 'installed',
+ ERROR = 'error'
+}
+
+export enum SetupStage {
+ INSTALLING = 'installing',
+ CONFIGURING = 'configuring',
+ COMPLETE = 'complete',
+ ERROR = 'error'
+}
+
+export type CreateCallback = (stage: CreateStage, message: string) => void;
+export type VerifyCallback = (stage: VerifyStage, message: string) => void;
+export type SetupCallback = (stage: SetupStage, message: string) => void;
diff --git a/utils/src/setup/index.ts b/utils/src/setup/index.ts
new file mode 100755
index 000000000..62e9d565e
--- /dev/null
+++ b/utils/src/setup/index.ts
@@ -0,0 +1,23 @@
+import { SetupStage, type SetupCallback } from '..';
+import { Framework } from '../frameworks';
+
+export const setupProject = async (targetPath: string, onProgress: SetupCallback): Promise => {
+ try {
+ process.chdir(targetPath);
+ onProgress(SetupStage.INSTALLING, 'Installing required packages...');
+
+ for (const framework of Framework.getAll()) {
+ onProgress(SetupStage.INSTALLING, 'Checking for' + framework.name + ' configuration...');
+ const updated = await framework.setup(onProgress);
+ if (updated) {
+ onProgress(SetupStage.COMPLETE, 'Project setup complete.');
+ return;
+ }
+ }
+ console.error('Cannot determine the project framework.', '\nIf this is unexpected, see: https://github.com/onlook-dev/onlook/wiki/How-to-set-up-my-project%3F#do-it-manually');
+ onProgress(SetupStage.ERROR, 'Project setup failed.');
+ } catch (err) {
+ console.error(err);
+ onProgress(SetupStage.ERROR, 'An error occurred.');
+ }
+};
diff --git a/utils/src/utils.ts b/utils/src/utils.ts
new file mode 100644
index 000000000..3c1d1a2f2
--- /dev/null
+++ b/utils/src/utils.ts
@@ -0,0 +1,134 @@
+import { NodePath } from '@babel/traverse';
+import * as t from '@babel/types';
+import { execSync } from 'child_process';
+import * as fs from 'fs';
+import * as glob from 'glob';
+import * as path from 'path';
+import {
+ FILE_EXTENSION,
+ LOCK_FILE_NAME,
+ PACKAGE_JSON,
+ PACKAGE_MANAGER,
+} from './constants';
+
+export const exists = async (filePattern: string): Promise => {
+ try {
+ const pattern = path.resolve(process.cwd(), filePattern);
+ const files = getFileNamesByPattern(pattern);
+ return files.length > 0;
+ } catch (err) {
+ console.error(err);
+ return false;
+ }
+};
+
+export const getFileNamesByPattern = (pattern: string): string[] => glob.globSync(pattern);
+
+export const installPackages = async (packages: string[]): Promise => {
+ const packageManager = await getPackageManager();
+ const command = packageManager === PACKAGE_MANAGER.YARN ? 'yarn add -D' : `${packageManager} install -D`;
+
+ console.log("Package manager found:", packageManager)
+ console.log("\n$", `${command} ${packages.join(' ')}`)
+
+ execSync(`${command} ${packages.join(' ')}`, { stdio: 'inherit' });
+};
+
+export const getPackageManager = async (): Promise => {
+ try {
+ if (await exists(LOCK_FILE_NAME.YARN)) {
+ return PACKAGE_MANAGER.YARN;
+ }
+ if (await exists(LOCK_FILE_NAME.PNPM)) {
+ return PACKAGE_MANAGER.PNPM;
+ }
+ if (await exists(LOCK_FILE_NAME.BUN)) {
+ return PACKAGE_MANAGER.BUN;
+ }
+ return PACKAGE_MANAGER.NPM;
+ } catch (e) {
+ console.error("Error determining package manager, using npm by default", e)
+ return PACKAGE_MANAGER.NPM
+ }
+
+};
+
+export const hasDependency = async (dependencyName: string, targetPath?: string): Promise => {
+ const packageJsonPath = targetPath ? path.resolve(targetPath, PACKAGE_JSON) : path.resolve(PACKAGE_JSON);
+
+ if (await exists(packageJsonPath)) {
+ const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf-8');
+ const packageJson = JSON.parse(packageJsonContent);
+ return (
+ (packageJson.dependencies && dependencyName in packageJson.dependencies) ||
+ (packageJson.devDependencies && dependencyName in packageJson.devDependencies)
+ );
+ }
+ return false;
+};
+
+export const getFileExtensionByPattern = async (dir: string, filePattern: string): Promise => {
+ const fullDirPattern = path.resolve(dir, filePattern);
+ const files = await getFileNamesByPattern(fullDirPattern);
+
+ if (files.length > 0) {
+ return path.extname(files[0]);
+ }
+
+ return null;
+};
+
+export const genASTParserOptionsByFileExtension = (fileExtension: string, sourceType: string = 'module'): object => {
+ switch (fileExtension) {
+ case FILE_EXTENSION.JS:
+ return {
+ sourceType: sourceType
+ };
+ case FILE_EXTENSION.MJS:
+ return {
+ sourceType: sourceType,
+ plugins: ['jsx']
+ };
+ case FILE_EXTENSION.TS:
+ return {
+ sourceType: sourceType,
+ plugins: ['typescript']
+ };
+ default:
+ return {};
+ }
+};
+
+export const genImportDeclaration = (fileExtension: string, dependency: string): t.VariableDeclaration | t.ImportDeclaration | null => {
+ switch (fileExtension) {
+ case FILE_EXTENSION.JS:
+ return t.variableDeclaration('const', [
+ t.variableDeclarator(
+ t.identifier(dependency),
+ t.callExpression(t.identifier('require'), [t.stringLiteral(dependency)])
+ )
+ ]);
+ case FILE_EXTENSION.MJS:
+ return t.importDeclaration(
+ [t.importDefaultSpecifier(t.identifier(dependency))],
+ t.stringLiteral(dependency)
+ );
+ default:
+ return null;
+ }
+};
+
+export const checkVariableDeclarationExist = (path: NodePath, dependency: string): boolean => {
+ return t.isIdentifier(path.node.id, { name: dependency }) &&
+ t.isCallExpression(path.node.init) &&
+ (path.node.init.callee as t.V8IntrinsicIdentifier).name === 'require' &&
+ (path.node.init.arguments[0] as any).value === dependency;
+};
+
+export const isSupportFileExtension = (fileExtension: string): boolean => {
+ return [FILE_EXTENSION.JS, FILE_EXTENSION.MJS].indexOf(fileExtension as FILE_EXTENSION) !== -1;
+};
+
+export const isViteProjectSupportFileExtension = (fileExtension: string): boolean => {
+ return [FILE_EXTENSION.JS, FILE_EXTENSION.TS].indexOf(fileExtension as FILE_EXTENSION) !== -1;
+};
diff --git a/utils/src/verify/index.ts b/utils/src/verify/index.ts
new file mode 100644
index 000000000..a36822ea0
--- /dev/null
+++ b/utils/src/verify/index.ts
@@ -0,0 +1,19 @@
+import { VerifyStage, type VerifyCallback } from '..';
+import { ONLOOK_PLUGIN } from '../constants';
+import { hasDependency } from '../utils';
+
+export const verifyProject = async (targetPath: string, onProgress: VerifyCallback): Promise => {
+ try {
+ for (const dep of [ONLOOK_PLUGIN.BABEL, ONLOOK_PLUGIN.NEXTJS]) {
+ onProgress(VerifyStage.CHECKING, `Checking for ${dep}`);
+ if (await hasDependency(dep, targetPath)) {
+ onProgress(VerifyStage.INSTALLED, `Found ${dep}`);
+ return;
+ }
+ }
+ onProgress(VerifyStage.NOT_INSTALLED, 'No Onlook dependencies found.');
+ } catch (e: any) {
+ console.error(e);
+ onProgress(VerifyStage.ERROR, `Error verifying project. ${e.message}`);
+ }
+};
diff --git a/utils/tsconfig.json b/utils/tsconfig.json
new file mode 100644
index 000000000..dcc6b5ae6
--- /dev/null
+++ b/utils/tsconfig.json
@@ -0,0 +1,31 @@
+{
+ "include": [
+ "src",
+ "tests"
+ ],
+ "compilerOptions": {
+ // Enable latest features
+ "lib": [
+ "ESNext",
+ "DOM"
+ ],
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleDetection": "force",
+ "jsx": "react-jsx",
+ "allowJs": true,
+ // Bundler mode
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+ // Best practices
+ "strict": true,
+ "skipLibCheck": true,
+ "noFallthroughCasesInSwitch": true,
+ // Some stricter flags (disabled by default)
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noPropertyAccessFromIndexSignature": false,
+ }
+}
\ No newline at end of file