Skip to content

Commit

Permalink
Merge pull request #65 from socketsupply/advanced-project
Browse files Browse the repository at this point in the history
Advanced project
  • Loading branch information
heapwolf authored Mar 12, 2024
2 parents 2da6a8d + 2b932d5 commit 6751a89
Show file tree
Hide file tree
Showing 56 changed files with 332,825 additions and 998 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ node_modules/
# Default output directory
dist/
build/
.DS_Store

package-lock.json

Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ A javascript scratchpad similar to Electron Fiddle or CodePen.
- Evaluate the code or the selection in the editor.
- Live-reload (currently for desktop only, simulator/emulator next).

> [!WARNING]
> This project is PRE-RELEASE! It requires the `dev` branch of the socket runtime.
# DESCRIPTION

Socket App Studio provides a sandbox environment where developers can write, build, and run experiments or snippets of code, making it easier to test and share code. <a href="https://github.com/socketsupply/socket-app-studio">Download it from GitHub</a> for Windows, MacOS, and Linux.

![screenshot](docs/screenshot3.png)
![screenshot](docs/screenshot2.png)
![screenshot](docs/screenshot4.png)
![screenshot](docs/screenshot5.png)
16 changes: 7 additions & 9 deletions build.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ const cp = async (a, b) => fs.cp(
async function copy (target) {
await cp('src/index.html', target)
await cp('src/vm.js', target)
await cp('src/preview.js', target)
await cp('src/worker.js', target)
await cp('icons/icon.png', target)
await cp('src/templates', target)
await cp('src/examples', target)
await cp('src/template', target)
await cp('src/fonts', target)
await cp('src/vs', target)
await cp('src/lib', target)
await cp('src/css', target)
}

Expand All @@ -35,6 +36,7 @@ async function main (argv) {
await esbuild.build({
entryPoints: workerEntryPoints.map((entry) => `node_modules/monaco-editor/esm/${entry}`),
bundle: true,
minify: false,
format: 'iife',
outbase: 'node_modules/monaco-editor/esm/',
outdir: 'src'
Expand All @@ -45,9 +47,8 @@ async function main (argv) {
format: 'esm',
bundle: true,
minify: false,
sourcemap: true,
sourcemap: false,
external: ['socket:*', 'node:*'],
keepNames: true,
loader: {
'.ttf': 'file'
}
Expand All @@ -57,10 +58,7 @@ async function main (argv) {

const opts = {
...params,
outdir: target,
minifyWhitespace: false,
minifyIdentifiers: true,
minifySyntax: true
outdir: target
}
await esbuild.build(opts)
await copy(target)
Expand Down
Binary file removed docs/screenshot2.png
Binary file not shown.
Binary file removed docs/screenshot3.png
Binary file not shown.
Binary file added docs/screenshot4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshot5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"@socketsupply/tonic": "^15.1.2",
"esbuild": "^0.20.0",
"monaco-editor": "^0.46.0",
"standard": "^16.0.4",
"standard": "^17.1.0",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",
"xterm-addon-search": "^0.13.0"
Expand Down
14 changes: 9 additions & 5 deletions socket.ini
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,13 @@ watch = true
; default value: true
reload = true

[webview.service-workers]

/ = /worker.js

; Mount file system paths in webview navigator
[webview.navigator.mounts]
$HOST_HOME = /user/home
; $HOST_HOME/directory-in-home-folder/ = /mount/path/in/navigator
; $HOST_CONTAINER/directory-app-container/ = /mount/path/in/navigator
; $HOST_PROCESS_WORKING_DIRECTORY/directory-in-app-process-working-directory/ = /mount/path/in/navigator
Expand Down Expand Up @@ -267,6 +271,7 @@ codesign_paths = ""
; default value: "13.0.0"
; minimum_supported_version = "13.0.0"

trafficLightPosition = "10x24"

; The icon to use for identifying your app on MacOS.
icon = "icons/icon.png"
Expand Down Expand Up @@ -323,11 +328,11 @@ width = 80%

; Minimum height of the window in pixels or as a percentage of the screen.
; default value: 0
; min_height = 0
min_height = 500

; Minimum width of the window in pixels or as a percentage of the screen.
; default value: 0
; min_width = 0
min_width = 700

; If the window is resizable or not.
; default value: true
Expand All @@ -337,6 +342,8 @@ width = 80%
; default value: false
; frameless = false

titleBarStyle = "hiddenInset"

; If the window is utility window or not.
; default value: false
; utility = false
Expand Down Expand Up @@ -385,6 +392,3 @@ runner_mac_flags = ""
runner_win32 = ""
; The headless runner command flags for Windows
runner_win32_flags = ""

[permissions]
allow_service_worker = false
158 changes: 108 additions & 50 deletions src/editor.js → src/components/editor.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,48 @@
import fs from 'socket:fs'
import path from 'socket:path'
import { lookup } from 'socket:mime'

import * as monaco from 'monaco-editor'
import Tonic from '@socketsupply/tonic'
import { resizePNG } from './icon/index.js'

import * as monaco from 'monaco-editor'
import { resizePNG } from '../lib/icon.js'

function rgbaToHex (rgbaString) {
const rgbaValues = rgbaString.match(/\d+/g)

const r = parseInt(rgbaValues[0])
const g = parseInt(rgbaValues[1])
const b = parseInt(rgbaValues[2])

const a = Math.round(parseFloat(rgbaValues[3]) * 255)

function rgbaToHex(rgbaString) {
const rgbaValues = rgbaString.match(/\d+/g);

const r = parseInt(rgbaValues[0]);
const g = parseInt(rgbaValues[1]);
const b = parseInt(rgbaValues[2]);

const a = Math.round(parseFloat(rgbaValues[3]) * 255);

const rHex = r.toString(16).padStart(2, '0');
const gHex = g.toString(16).padStart(2, '0');
const bHex = b.toString(16).padStart(2, '0');
const aHex = a.toString(16).padStart(2, '0');

return `#${rHex}${gHex}${bHex}${aHex}`;
const rHex = r.toString(16).padStart(2, '0')
const gHex = g.toString(16).padStart(2, '0')
const bHex = b.toString(16).padStart(2, '0')
const aHex = a.toString(16).padStart(2, '0')

return `#${rHex}${gHex}${bHex}${aHex}`
}

globalThis.MonacoEnvironment = {
getWorkerUrl: function (moduleId, label) {
if (label === 'json') {
return 'vs/language/json/json.worker.js';
return 'lib/vs/language/json/json.worker.js'
}

if (label === 'css' || label === 'scss' || label === 'less') {
return 'vs/language/css/css.worker.js';
return 'lib/vs/language/css/css.worker.js'
}

if (label === 'html' || label === 'handlebars' || label === 'razor') {
return 'vs/language/html/html.worker.js';
return 'lib/vs/language/html/html.worker.js'
}

if (label === 'typescript' || label === 'javascript') {
return 'vs/language/typescript/ts.worker.js';
return 'lib/vs/language/typescript/ts.worker.js'
}

return 'vs/editor/editor.worker.js';
return 'lib/vs/editor/editor.worker.js'
}
}

Expand All @@ -50,6 +51,10 @@ class AppEditor extends Tonic {
return this.editor.getValue()
}

set value (s) {
this.editor.setValue(s)
}

async click (e) {
const el = Tonic.match(e.target, '[data-event]')
if (!el) return
Expand All @@ -59,14 +64,14 @@ class AppEditor extends Tonic {
const pickerOpts = {
types: [
{
description: "Images",
description: 'Images',
accept: {
"image/*": ['.png'],
},
},
'image/*': ['.png']
}
}
],
excludeAcceptAllOption: true,
multiple: false,
multiple: false
}

if (event === 'size') {
Expand Down Expand Up @@ -99,22 +104,42 @@ class AppEditor extends Tonic {
this.editor.getModel().getValueInRange(this.editor.getSelection())
}

async writeToDisk (projectNode) {
async writeToDisk (projectNode, data) {
const app = document.querySelector('app-view')
const dest = path.join(app.state.cwd, projectNode.id)
await fs.promises.writeFile(dest, projectNode.data)
const preview = document.querySelector('app-preview')

try {
await fs.promises.writeFile(projectNode.id, data)
} catch (err) {
console.error(`Unable to write to ${dest}`, err)
}

this.props.parent.reloadPreviewWindows()
}

async loadProjectNode (projectNode) {
if (!projectNode) return

const parent = this.props.parent
const ext = path.extname(projectNode.id)
const type = await lookup(ext.slice(1))

if (type.length) {
if (/image/.test(type[0].mime)) {
// Display a preview for this type.
return
}
}

this.projectNode = projectNode

const fileName = projectNode.label
const imagePreview = this.querySelector('.image-preview')

if (fileName.endsWith('.assets')) {
const blob = new Blob([projectNode.data], { type: 'image/png' })
if (projectNode.isDirectory && fileName === 'icons') {
const iconPath = path.join(projectNode.id, 'icon.png')
const data = await fs.promises.readFile(iconPath)
const blob = new Blob([data], { type: 'image/png' })
const url = URL.createObjectURL(blob)
;[...imagePreview.querySelectorAll('img')].forEach(img => (img.src = url))
imagePreview.classList.add('show')
Expand All @@ -123,9 +148,20 @@ class AppEditor extends Tonic {

imagePreview.classList.remove('show')

if (this.editor) {
monaco.editor.setModelLanguage(this.editor.getModel(), projectNode.language)
this.editor.setValue(projectNode.data)
if (!projectNode.isDirectory && this.editor) {
const ext = path.extname(projectNode.id)

const mappings = parent.state.settings.extensionLanguageMappings
const lang = mappings[ext] || ext.slice(1)
monaco.editor.setModelLanguage(this.editor.getModel(), lang)
let data = await fs.promises.readFile(projectNode.id, 'utf8')

if (path.extname(projectNode.id) === '.json') {
try {
data = JSON.stringify(JSON.parse(data), null, 2)
} catch {}
}
this.editor.setValue(data)
}
}

Expand Down Expand Up @@ -180,37 +216,43 @@ class AppEditor extends Tonic {
monaco.editor.setTheme(theme)
}

async loadAPIs(directoryPath = './socket') {
async loadAPIs (directoryPath = './socket') {
const readDir = async (dirPath) => {
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true })

entries.forEach(async (entry) => {
const fullPath = path.join(dirPath, entry.name);
const fullPath = path.join(dirPath, entry.name)

if (entry.isDirectory()) {
readDir(fullPath).catch(err => console.error(`Error reading directory ${fullPath}:`, err));
readDir(fullPath).catch(err => console.error(`Error reading directory ${fullPath}:`, err))
} else {
if (path.extname(fullPath) === '.ts') {
fs.promises.readFile(fullPath, 'utf8')
.then(sourceText => {
monaco.languages.typescript.javascriptDefaults.addExtraLib(sourceText, `socket/${fullPath}`);
monaco.languages.typescript.typescriptDefaults.addExtraLib(sourceText, `socket/${fullPath}`);
monaco.languages.typescript.javascriptDefaults.addExtraLib(sourceText, `socket/${fullPath}`)
monaco.languages.typescript.typescriptDefaults.addExtraLib(sourceText, `socket/${fullPath}`)
})
.catch(err => console.error(`Error reading file ${fullPath}:`, err));
.catch(err => console.error(`Error reading file ${fullPath}:`, err))
}
}
});
};
})
}

try {
await readDir(directoryPath);
await readDir(directoryPath)
} catch (err) {
console.error('Error initiating read directory operation:', err);
console.error('Error initiating read directory operation:', err)
}
}

async refreshSettings () {
const parent = this.props.parent
this.editor.updateOptions(parent.state.settings?.editorOptions || {})
}

connected () {
let theme
const parent = this.props.parent

this.editor = monaco.editor.create(this.querySelector('.editor'), {
value: '',
Expand All @@ -223,15 +265,31 @@ class AppEditor extends Tonic {
renderLineHighlight: 'none'
})

this.editor.getModel().onDidChangeContent(() => {
this.refreshSettings()

const model = this.editor.getModel()

model.onDidChangeContent(async () => {
clearTimeout(this.writeDebounce)

if (!this.projectNode) return
this.projectNode.data = this.editor.getValue()
const value = this.editor.getValue()
const terminal = document.querySelector('app-terminal')

this.writeDebounce = setTimeout(() => {
this.writeToDisk(this.projectNode)
}, 256)
if (this.projectNode.label === 'settings.json' && this.projectNode.parent.id === 'root') {

try {
this.props.parent.state.settings = JSON.parse(value)
} catch (err) {
terminal.error(`Unable to parse settings file (${err.message})`)
return
}
terminal.info(`Settings file updated.`)
}

this.writeToDisk(this.projectNode, value)
}, 620)
})

window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
Expand Down
Loading

0 comments on commit 6751a89

Please sign in to comment.