This commit is contained in:
Dmitry Vasilev
2023-07-02 20:22:41 +03:00
parent cc7339268b
commit f2dba93c7a
8 changed files with 197 additions and 87 deletions

View File

@@ -200,16 +200,10 @@
} }
.allow_file_access { .allow_file_access {
height: 100%;
padding: 10px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
} }
.allow_file_access .subtitle { .allow_file_access .subtitle {
padding: 10px;
font-size: 0.8em; font-size: 0.8em;
} }

View File

@@ -808,7 +808,7 @@ const clear_io_trace = state => {
return run_code({...state, io_trace: null}) return run_code({...state, io_trace: null})
} }
const do_load_dir = (state, dir) => { const load_files = (state, dir) => {
const collect_files = dir => dir.kind == 'file' const collect_files = dir => dir.kind == 'file'
? [dir] ? [dir]
: dir.children.map(collect_files).flat() : dir.children.map(collect_files).flat()
@@ -820,7 +820,7 @@ const do_load_dir = (state, dir) => {
return { return {
...state, ...state,
project_dir: dir, project_dir: dir,
files: {...files, ...state.files}, files: {...files, '': state.files['']},
} }
} }
@@ -842,24 +842,29 @@ const apply_entrypoint_settings = (state, entrypoint_settings) => {
} }
} }
const load_dir = (state, dir, entrypoint_settings) => { const load_dir = (state, dir, has_file_system_access, entrypoint_settings) => {
// Clear parse cache and rerun code // Clear parse cache and rerun code
const with_dir = do_load_dir(state, dir) const with_dir = load_files(state, dir)
return run_code({ return run_code({
...( ...(
entrypoint_settings == null entrypoint_settings == null
? with_dir ? with_dir
: apply_entrypoint_settings(with_dir, entrypoint_settings) : apply_entrypoint_settings(with_dir, entrypoint_settings)
), ),
has_file_system_access,
// remove cache. We have to clear cache because imports of modules that are // remove cache. We have to clear cache because imports of modules that are
// not available because project_dir is not available have errors and the // not available because project_dir is not available have errors and the
// errors are cached // errors are cached
parse_result: null, parse_result: null,
external_imports_cache: null,
}) })
} }
const create_file = (state, dir, current_module) => { const create_file = (state, dir, current_module) => {
return {...load_dir(state, dir), current_module} return {...load_dir(state, dir, true), current_module}
} }
const open_run_window = (state, globals) => { const open_run_window = (state, globals) => {
@@ -877,7 +882,7 @@ const open_run_window = (state, globals) => {
const get_initial_state = (state, entrypoint_settings) => { const get_initial_state = (state, entrypoint_settings) => {
const with_files = state.project_dir == null const with_files = state.project_dir == null
? state ? state
: do_load_dir(state, state.project_dir) : load_files(state, state.project_dir)
const with_settings = apply_entrypoint_settings(with_files, entrypoint_settings) const with_settings = apply_entrypoint_settings(with_files, entrypoint_settings)

View File

@@ -21,7 +21,7 @@ export function el(tag, className, ...children){
const append = child => { const append = child => {
if(typeof(child) == 'undefined') { if(typeof(child) == 'undefined') {
throw new Error('illegal state') throw new Error('illegal state')
} else if(child !== null) { } else if(child !== null && child !== false) {
result.appendChild( result.appendChild(
typeof(child) == 'string' typeof(child) == 'string'
? document.createTextNode(child) ? document.createTextNode(child)

View File

@@ -1,7 +1,13 @@
import {el} from './domutils.js' import {el} from './domutils.js'
import {map_find} from '../utils.js' import {map_find} from '../utils.js'
import {load_dir, create_file} from '../filesystem.js' import {open_dir, create_file} from '../filesystem.js'
import {exec, get_state, open_directory, reload_run_window} from '../index.js' import {
exec,
get_state,
open_directory,
reload_run_window,
close_directory,
} from '../index.js'
const is_html = path => path.endsWith('.htm') || path.endsWith('.html') const is_html = path => path.endsWith('.htm') || path.endsWith('.html')
const is_js = path => path == '' || path.endsWith('.js') || path.endsWith('.mjs') const is_js = path => path == '' || path.endsWith('.js') || path.endsWith('.mjs')
@@ -25,27 +31,56 @@ export class Files {
reload_run_window(get_state()) reload_run_window(get_state())
} }
render(state) {
if(state.project_dir == null) {
this.el.innerHTML = ''
this.el.appendChild(
el('div', 'allow_file_access',
el('a', {
href: 'javascript:void(0)',
click: open_directory,
},
`Allow access to local project folder`,
),
el('div', 'subtitle', `Your files will never leave your device`)
)
)
} else {
this.render_files(state)
}
}
render_files(state) { render(state) {
const children = [ const file_actions = state.has_file_system_access
? el('div', 'file_actions',
el('a', {
'class': 'file_action',
href: 'javascript: void(0)',
click: this.create_file.bind(this, false),
},
'New file'
),
el('a', {
'class': 'file_action',
href: 'javascript: void(0)',
click: this.create_file.bind(this, true),
},
'New dir'
),
el('a', {
'class': 'file_action',
href: 'javascript: void(0)',
click: close_directory,
},
'Revoke access'
),
el('a', {
href: 'https://github.com/leporello-js/leporello-js#selecting-entrypoint-module',
target: '__blank',
"class": 'select_entrypoint_title',
title: 'Select entrypoint',
},
'Entry point'
),
)
: el('div', 'file_actions',
el('div', 'file_action allow_file_access',
el('a', {
href: 'javascript: void(0)',
click: open_directory,
}, 'Allow access to local project folder'),
el('span', 'subtitle', `Your files will never leave your device`)
),
)
const file_elements = [
this.render_file({name: '*scratch*', path: ''}, state), this.render_file({name: '*scratch*', path: ''}, state),
this.render_file(state.project_dir, state), this.render_file(state.project_dir, state),
] ]
@@ -54,41 +89,19 @@ export class Files {
if(files == null) { if(files == null) {
this.el.innerHTML = '' this.el.innerHTML = ''
this.el.appendChild(file_actions)
this.el.appendChild( this.el.appendChild(
el('div', 'file_actions', el('div', 'files', file_elements)
el('a', {
'class': 'file_action',
href: 'javascript: void(0)',
click: this.create_file.bind(this, false),
},
'Create file'
),
el('a', {
'class': 'file_action',
href: 'javascript: void(0)',
click: this.create_file.bind(this, true),
}, 'Create dir'),
el('a', {
href: 'https://github.com/leporello-js/leporello-js#selecting-entrypoint-module',
target: '__blank',
"class": 'select_entrypoint_title',
title: 'Select entrypoint',
}, 'Entry point'),
)
)
this.el.appendChild(
el('div', 'files',
children
)
) )
} else { } else {
// Replace to preserve scroll position // Replace to preserve scroll position
files.replaceChildren(...children) this.el.replaceChild(file_actions, this.el.children[0])
files.replaceChildren(...file_elements)
} }
} }
render_select_entrypoint(file, state) { render_select_entrypoint(file, state) {
if(file.kind == 'directory') { if(!state.has_file_system_access || file.kind == 'directory') {
return null return null
} else if(is_js(file.path)) { } else if(is_js(file.path)) {
return el('span', 'select_entrypoint', return el('span', 'select_entrypoint',
@@ -189,9 +202,9 @@ export class Files {
await create_file(path, is_dir) await create_file(path, is_dir)
// Reload all files for simplicity // Reload all files for simplicity
load_dir(false).then(dir => { open_dir(false).then(dir => {
if(is_dir) { if(is_dir) {
exec('load_dir', dir) exec('load_dir', dir, true)
} else { } else {
exec('create_file', dir, path) exec('create_file', dir, path)
} }
@@ -205,8 +218,15 @@ export class Files {
this.active_el = e.currentTarget.parentElement this.active_el = e.currentTarget.parentElement
e.currentTarget.classList.add('active') e.currentTarget.classList.add('active')
this.active_file = file this.active_file = file
if(file.kind != 'directory') { if(file.kind != 'directory') {
if(get_state().has_file_system_access) {
exec('change_current_module', file.path) exec('change_current_module', file.path)
} else {
// in examples mode, on click file we also change entrypoint for
// simplicity
exec('change_entrypoint', file.path)
}
} }
} }
} }

9
src/effects.js vendored
View File

@@ -1,4 +1,5 @@
import {write_file} from './filesystem.js' import {write_file} from './filesystem.js'
import {write_example} from './examples.js'
import {color_file} from './color.js' import {color_file} from './color.js'
import { import {
root_calltree_node, root_calltree_node,
@@ -293,6 +294,12 @@ export const EFFECTS = {
localStorage[key] = value localStorage[key] = value
}, },
write: (state, [name, contents], ui) => write_file(name, contents), write: (state, [name, contents], ui) => {
if(state.has_file_system_access) {
write_file(name, contents)
} else {
write_example(name, contents)
}
}
} }

57
src/examples.js Normal file
View File

@@ -0,0 +1,57 @@
export const write_example = (name, contents) => {
localStorage['examples_' + name] = contents
}
const read_example = name => {
return localStorage['examples_' + name]
}
const list = [
'github_api/index.js',
'ethers/block_by_timestamp.js',
'ethers/index.js',
// TODO for html5 example, open run window or hint that it should be opened
]
.map(l => l.split('/'))
const get_children = path => {
const children = list.filter(l => path.every((elem, i) => elem == l[i] ))
const files = children.filter(c => c.length == path.length + 1)
const dirs = [...new Set(children
.filter(c => c.length != path.length + 1)
.map(c => c[path.length])
)]
return Promise.all(files.map(async f => {
const name = f[path.length]
const filepath = f.slice(0, path.length + 1).join('/')
return {
name,
path: filepath,
kind: 'file',
contents:
read_example(filepath) ??
await fetch(globalThis.location.origin + '/docs/examples/'+ filepath)
.then(r => r.text()),
}
})
.concat(dirs.map(async d => {
const p = [...path, d]
return {
name: d,
path: p.join('/'),
kind: 'directory',
children: await get_children(p),
}
})))
}
export const examples_promise = get_children([]).then(children => {
return {
kind: 'directory',
name: 'examples',
path: null,
children,
}
})

View File

@@ -15,11 +15,10 @@ const send_message = (message) => {
}); });
} }
globalThis.clear_directory_handle = () => { export const close_dir = () => {
send_message({type: 'SET_DIR_HANDLE', data: null}) send_message({type: 'SET_DIR_HANDLE', data: null})
clearInterval(keepalive_interval_id) clearInterval(keepalive_interval_id)
keepalive_interval_id = null keepalive_interval_id = null
window.location.reload()
} }
let dir_handle let dir_handle
@@ -120,7 +119,7 @@ const read_file = async handle => {
return await file_data.text() return await file_data.text()
} }
const do_load_dir = async (handle, path) => { const do_open_dir = async (handle, path) => {
if(handle.kind == 'directory') { if(handle.kind == 'directory') {
const children = [] const children = []
for await (let [name, h] of handle) { for await (let [name, h] of handle) {
@@ -134,7 +133,7 @@ const do_load_dir = async (handle, path) => {
kind: 'directory', kind: 'directory',
children: (await Promise.all( children: (await Promise.all(
children.map(c => children.map(c =>
do_load_dir(c, path == null ? c.name : path + '/' + c.name) do_open_dir(c, path == null ? c.name : path + '/' + c.name)
) )
)).sort((a, b) => a.name.localeCompare(b.name)) )).sort((a, b) => a.name.localeCompare(b.name))
} }
@@ -159,7 +158,7 @@ export const create_file = (path, is_dir) => {
) )
} }
export const load_dir = async (should_request_access) => { export const open_dir = async (should_request_access) => {
let handle let handle
if(should_request_access) { if(should_request_access) {
handle = await request_directory_handle() handle = await request_directory_handle()
@@ -171,5 +170,5 @@ export const load_dir = async (should_request_access) => {
} else { } else {
keep_service_worker_alive() keep_service_worker_alive()
} }
return do_load_dir(handle, null) return do_open_dir(handle, null)
} }

View File

@@ -1,12 +1,22 @@
import {UI} from './editor/ui.js' import {UI} from './editor/ui.js'
import {EFFECTS, render_initial_state, apply_side_effects} from './effects.js' import {EFFECTS, render_initial_state, apply_side_effects} from './effects.js'
import {load_dir, init_window_service_worker} from './filesystem.js' import {
open_dir,
close_dir,
init_window_service_worker
} from './filesystem.js'
import {examples_promise} from './examples.js'
const EXAMPLE = `const fib = n => const EXAMPLE = `function fib(n) {
n == 0 || n == 1 if(n == 0 || n == 1) {
? n return n
: fib(n - 1) + fib(n - 2) } else {
fib(6)` return fib(n - 1) + fib(n - 2)
}
}
fib(6)
`
const set_error_handler = (w, with_unhandled_rejection = true) => { const set_error_handler = (w, with_unhandled_rejection = true) => {
@@ -131,38 +141,56 @@ export const reload_run_window = state => {
globalThis.run_window.location = get_html_url(state) globalThis.run_window.location = get_html_url(state)
} }
const read_modules = async () => { const read_modules = async () => {
const default_module = {'': localStorage.code || EXAMPLE} const default_module = {'': localStorage.code || EXAMPLE}
const project_dir = await load_dir(false) const project_dir = await open_dir(false)
if(project_dir == null) { if(project_dir == null) {
// Single anonymous module
return { return {
project_dir: await examples_promise,
files: default_module, files: default_module,
has_file_system_access: false,
} }
} else { } else {
return { return {
project_dir, project_dir,
files: default_module, files: default_module,
has_file_system_access: true,
} }
} }
} }
const get_entrypoint_settings = () => ({ const get_entrypoint_settings = () => {
current_module: localStorage.current_module ?? '',
entrypoint: localStorage.entrypoint ?? '', const params = new URLSearchParams(window.location.search)
const entrypoint = null
?? params.get('entrypoint')
?? localStorage.entrypoint
?? ''
return {
current_module: params.get('entrypoint') ?? localStorage.current_module ?? '',
entrypoint,
html_file: localStorage.html_file ?? '', html_file: localStorage.html_file ?? '',
}) }
}
export const open_directory = () => { export const open_directory = () => {
if(globalThis.showDirectoryPicker == null) { if(globalThis.showDirectoryPicker == null) {
throw new Error('Your browser is not supporting File System Access API') throw new Error('Your browser is not supporting File System Access API')
} }
load_dir(true).then(dir => { open_dir(true).then(dir => {
exec('load_dir', dir, get_entrypoint_settings()) exec('load_dir', dir, true, get_entrypoint_settings())
}) })
} }
export const close_directory = async () => {
close_dir()
exec('load_dir', await examples_promise, false, get_entrypoint_settings())
}
let COMMANDS let COMMANDS
let ui let ui