refactor stuff to support differentiating between setting and updating components in REPL

pull/2179/head
Richard Harris 7 years ago
parent 5d2d35b3b8
commit fda8d1947d

@ -13,12 +13,11 @@
<script>
import { onMount, beforeUpdate, createEventDispatcher, getContext } from 'svelte';
import Message from './Message.svelte';
const dispatch = createEventDispatcher();
const { navigate } = getContext('REPL');
export let mode;
export let code;
export let readonly = false;
export let errorLoc = null;
export let flex = false;
@ -27,17 +26,46 @@
let w;
let h;
let code = '';
let mode;
// We have to expose set and update methods, rather
// than making this state-driven through props,
// because it's difficult to update an editor
// without resetting scroll otherwise
export function set(new_code, new_mode) {
if (new_mode !== mode) {
createEditor(mode = new_mode);
}
code = new_code;
if (editor) editor.setValue(code);
}
export function update(new_code) {
code = new_code;
if (editor) {
const { left, top } = editor.getScrollInfo();
editor.setValue(code = new_code);
editor.scrollTo(left, top);
}
}
export function resize() {
editor.refresh();
}
const modes = {
js: {
name: 'javascript',
json: false
},
json: {
name: 'javascript',
json: true
},
handlebars: {
svelte: {
name: 'handlebars',
base: 'text/html'
}
@ -51,15 +79,6 @@
let destroyed = false;
let CodeMirror;
$: if (CodeMirror) {
createEditor(mode);
}
$: if (editor && !updating && code != null) {
updating = true;
editor.setValue(code);
}
$: if (editor && w && h) {
editor.refresh();
}
@ -96,9 +115,13 @@
onMount(() => {
if (_CodeMirror) {
CodeMirror = _CodeMirror;
createEditor(mode || 'svelte');
editor.setValue(code || '');
} else {
codemirror_promise.then(mod => {
CodeMirror = mod.default;
createEditor(mode || 'svelte');
editor.setValue(code || '');
});
}
@ -113,7 +136,7 @@
});
function createEditor(mode) {
if (destroyed) return;
if (destroyed || !CodeMirror) return;
if (editor) {
editor.toTextArea();
@ -125,7 +148,7 @@
indentWithTabs: true,
indentUnit: 2,
tabSize: 2,
value: code,
value: '',
mode: modes[mode] || {
name: mode
},
@ -142,8 +165,8 @@
editor.on('change', instance => {
if (!updating) {
updating = true;
code = instance.getValue();
dispatch('change', { value: code });
// code = instance.getValue();
dispatch('change', { value: instance.getValue() });
}
});
@ -254,6 +277,8 @@
<pre style="position: absolute; left: 0; top: 0"
>{code}</pre>
<p class='loading message'>loading editor...</p>
<div style="position: absolute; width: 100%; bottom: 0">
<Message kind='info'>loading editor...</Message>
</div>
{/if}
</div>

@ -3,16 +3,17 @@
import Icon from '../../Icon.svelte';
import { enter } from '../../../utils/events.js';
export let handle_select;
const { components, selected } = getContext('REPL');
let editing = null;
function selectComponent(component) {
if ($selected != component) {
if ($selected !== component) {
editing = null;
handle_select(component);
}
selected.set(component);
}
function editTab(component) {

@ -1,27 +1,16 @@
<script>
import { getContext } from 'svelte';
import { getContext, onMount } from 'svelte';
import CodeMirror from '../CodeMirror.svelte';
import Message from '../Message.svelte';
const { selected, handle_change, navigate } = getContext('REPL');
const { bundle, selected, handle_change, navigate, register_module_editor } = getContext('REPL');
export let error;
export let errorLoc;
export let warnings;
$: message = warning => {
let str = warning.message;
let loc = [];
if (warning.filename && warning.filename !== `${$selected.name}.${$selected.type}`) {
loc.push(warning.filename);
}
if (warning.start) loc.push(warning.start.line, warning.start.column);
return str + (loc.length ? ` (${loc.join(':')})` : ``);
};
let editor;
onMount(() => {
register_module_editor(editor);
});
</script>
<style>
@ -49,23 +38,22 @@
<div class="editor-wrapper">
<div class="editor">
{#if $selected}
<CodeMirror
mode="{$selected.type === 'js' ? 'javascript' : 'handlebars'}"
code={$selected.source}
bind:this={editor}
{errorLoc}
on:change={handle_change}
/>
{/if}
</div>
<div class="info">
{#if error}
<Message kind="error" details={error} filename="{$selected.name}.{$selected.type}"/>
{:else if warnings.length > 0}
{#each warnings as warning}
{#if $bundle}
{#if $bundle.error}
<Message kind="error" details={$bundle.error} filename="{$selected.name}.{$selected.type}"/>
{:else if $bundle.warnings.length > 0}
{#each $bundle.warnings as warning}
<Message kind="warning" details={warning} filename="{$selected.name}.{$selected.type}"/>
{/each}
{/if}
{/if}
</div>
</div>

@ -4,8 +4,8 @@
const { navigate } = getContext('REPL');
export let kind;
export let details;
export let filename;
export let details = null;
export let filename = null;
function message(details) {
let str = details.message || '[missing message]';

@ -0,0 +1,38 @@
export default class Compiler {
constructor(version) {
this.worker = new Worker('/workers/compiler.js');
this.worker.postMessage({ type: 'init', version });
this.uid = 1;
this.handlers = new Map();
this.worker.onmessage = event => {
const handler = this.handlers.get(event.data.id);
handler(event.data.result);
this.handlers.delete(event.data.id);
};
}
compile(component, options) {
return new Promise(fulfil => {
const id = this.uid++;
this.handlers.set(id, fulfil);
this.worker.postMessage({
id,
type: 'compile',
source: component.source,
options: Object.assign({
name: component.name,
filename: `${component.name}.svelte`
}, options),
entry: component.name === 'App'
});
});
}
destroy() {
this.worker.terminate();
}
}

@ -44,7 +44,6 @@
});
return () => {
proxy.eval(`if (window.component) window.component.\$destroy();`)
proxy.destroy();
}
});

@ -1,24 +1,69 @@
<script>
import { getContext } from 'svelte';
import { getContext, onMount } from 'svelte';
import SplitPane from '../SplitPane.svelte';
import Viewer from './Viewer.svelte';
import CompilerOptions from './CompilerOptions.svelte';
import Compiler from './Compiler.js';
import PropEditor from './PropEditor.svelte';
import CodeMirror from '../CodeMirror.svelte';
const { bundle, values } = getContext('REPL');
const { values, register_output } = getContext('REPL');
export let js;
export let css;
export let props;
export let version;
export let sourceErrorLoc;
export let runtimeError;
export let compile_options;
export let embedded;
export let show_props;
let props;
let compile_options = {
generate: 'dom',
dev: false,
css: false,
hydratable: false,
customElement: false,
immutable: false,
legacy: false
};
register_output({
set: async selected => {
if (selected.type === 'js') {
js_editor.set(`/* Select a component to see its compiled code */`);
css_editor.set(`/* Select a component to see its compiled code */`);
return;
}
const compiled = await compiler.compile(selected, compile_options);
js_editor.set(compiled.js, 'js');
css_editor.set(compiled.css, 'css');
if (compiled.props) props = compiled.props;
},
update: async selected => {
if (selected.type === 'js') return;
const compiled = await compiler.compile(selected, compile_options);
js_editor.update(compiled.js);
css_editor.update(compiled.css);
if (compiled.props) props = compiled.props;
}
});
let compiler;
onMount(() => {
compiler = new Compiler(version);
return () => compiler.destroy();
});
// refs
let viewer;
let js_editor;
let css_editor;
const setters = {};
let view = 'result';
@ -94,6 +139,20 @@
.props code {
top: .1rem;
}
.tab-content {
position: absolute;
width: 100%;
height: 100%;
opacity: 0;
pointer-events: none;
}
.tab-content.visible {
/* can't use visibility due to a weird painting bug in Chrome */
opacity: 1;
pointer-events: all;
}
</style>
<div class="view-toggle">
@ -113,7 +172,8 @@
>CSS output</button>
</div>
{#if view === 'result'}
<!-- component viewer -->
<div class="tab-content" class:visible="{view === 'result'}">
<SplitPane type="vertical" pos={67} fixed={!show_props} fixed_pos={100}>
<div slot="a">
<Viewer
@ -152,19 +212,23 @@
{/if}
</section>
</SplitPane>
{:else if embedded}
</div>
<!-- js output -->
<div class="tab-content" class:visible="{view === 'js'}">
{#if embedded}
<CodeMirror
mode="javascript"
code="{view === 'js' ? js : css}"
bind:this={js_editor}
mode="js"
errorLoc={sourceErrorLoc}
readonly
/>
{:else}
{:else}
<SplitPane type="vertical" pos={67}>
<div slot="a">
<CodeMirror
mode="javascript"
code="{view === 'js' ? js : css}"
bind:this={js_editor}
mode="js"
errorLoc={sourceErrorLoc}
readonly
/>
@ -176,4 +240,15 @@
<CompilerOptions bind:options={compile_options}/>
</section>
</SplitPane>
{/if}
{/if}
</div>
<!-- css output -->
<div class="tab-content" class:visible="{view === 'css'}">
<CodeMirror
bind:this={css_editor}
mode="css"
errorLoc={sourceErrorLoc}
readonly
/>
</div>

@ -1,7 +1,6 @@
<script>
import { onMount, setContext } from 'svelte';
import { writable } from 'svelte/store';
import * as fleece from 'golden-fleece';
import SplitPane from './SplitPane.svelte';
import CodeMirror from './CodeMirror.svelte';
import ComponentSelector from './Input/ComponentSelector.svelte';
@ -10,7 +9,6 @@
import InputOutputToggle from './InputOutputToggle.svelte';
export let version = 'beta'; // TODO change this to latest when the time comes
export let app;
export let embedded = false;
export let orientation = 'columns';
export let show_props = true;
@ -23,14 +21,37 @@
version; // workaround
return {
imports,
imports: $bundle.imports,
components: $components,
values: $values
};
}
export function update(app) {
// TODO
export function set(data) {
components.set(data.components);
values.set(data.values);
selected.set(data.components[0]);
module_editor.set($selected.source, $selected.type);
output.set($selected);
}
export function update(data) {
const { name, type } = $selected || {};
components.set(data.components);
values.set(data.values);
const matched_component = data.components.find(file => file.name === name && file.type === type);
selected.set(matched_component || data.components[0]);
if (matched_component) {
module_editor.update(matched_component.source);
output.update(matched_component);
} else {
module_editor.set(matched_component.source, matched_component.type);
output.set(matched_component);
}
}
const components = writable([]);
@ -38,6 +59,9 @@
const selected = writable(null);
const bundle = writable(null);
let module_editor;
let output;
setContext('REPL', {
components,
values,
@ -52,6 +76,8 @@
const component = $components.find(c => c.name === name && c.type === type);
selected.set(component);
output.set($selected);
// TODO select the line/column in question
},
@ -69,109 +95,59 @@
components.update(c => c);
// recompile selected component
compile($selected, compile_options);
output.update($selected);
// regenerate bundle (TODO do this in a separate worker?)
workers.bundler.postMessage({ type: 'bundle', components: $components });
},
register_module_editor(editor) {
module_editor = editor;
},
register_output(handlers) {
output = handlers;
}
});
$: {
components.set(app.components);
values.set(app.values);
selected.set(app.components[0]);
function handle_select(component) {
selected.set(component);
module_editor.set(component.source, component.type);
output.set($selected);
}
let workers;
let imports = null;
let import_map = null;
let dom;
let ssr;
let sourceError = null;
let runtimeError = null;
let warnings = [];
let js = '';
let css = '';
let uid = 0;
let props = [];
let input;
let sourceErrorLoc;
let runtimeErrorLoc;
let compile_options = {
generate: 'dom',
dev: false,
css: false,
hydratable: false,
customElement: false,
immutable: false,
legacy: false
};
let runtimeErrorLoc; // TODO refactor this stuff — runtimeErrorLoc is unused
let width = typeof window !== 'undefined' ? window.innerWidth : 300;
let show_output = false;
onMount(async () => {
workers = {
compiler: new Worker('/workers/compiler.js'),
bundler: new Worker('/workers/bundler.js')
};
workers.compiler.postMessage({ type: 'init', version });
workers.compiler.onmessage = event => {
js = event.data.js;
css = event.data.css || `/* Add a <sty` + `le> tag to see compiled CSS */`;
if (event.data.props) props = event.data.props;
};
workers.bundler.postMessage({ type: 'init', version });
workers.bundler.onmessage = event => {
bundle.set(event.data);
({ imports, import_map, dom, ssr, warnings, error: sourceError } = event.data);
if (sourceError) console.error(sourceError);
runtimeError = null;
};
return () => {
workers.compiler.terminate();
workers.bundler.terminate();
};
});
function compile(component, options) {
if (component.type === 'svelte') {
workers.compiler.postMessage({
type: 'compile',
source: component.source,
options: Object.assign({
name: component.name,
filename: `${component.name}.svelte`
}, options),
entry: component === $components[0]
});
} else {
js = css = `/* Select a component to see its compiled code */`;
}
}
$: if (sourceError && $selected) {
sourceErrorLoc = sourceError.filename === `${$selected.name}.${$selected.type}`
? sourceError.start
: null;
}
$: if (runtimeError && $selected) {
runtimeErrorLoc = runtimeError.filename === `${$selected.name}.${$selected.type}`
? runtimeError.start
$: if ($bundle && $bundle.error && $selected) {
sourceErrorLoc = $bundle.error.filename === `${$selected.name}.${$selected.type}`
? $bundle.error.start
: null;
}
$: if (workers && app.components) {
workers.bundler.postMessage({ type: 'bundle', components: app.components });
}
$: if (workers && $selected) {
compile($selected, compile_options);
$: if (workers && $components) {
workers.bundler.postMessage({ type: 'bundle', components: $components });
}
</script>
@ -240,21 +216,12 @@
fixed_pos={50}
>
<section slot=a>
<ComponentSelector/>
<ModuleEditor error={sourceError} errorLoc="{sourceErrorLoc || runtimeErrorLoc}" {warnings}/>
<ComponentSelector {handle_select}/>
<ModuleEditor bind:this={input} errorLoc="{sourceErrorLoc || runtimeErrorLoc}"/>
</section>
<section slot=b style='height: 100%;'>
<Output
bind:compile_options
{version}
{js}
{css}
{props}
{runtimeError}
{embedded}
{show_props}
/>
<Output {version} {embedded} {show_props}/>
</section>
</SplitPane>
</div>

@ -15,11 +15,7 @@
export let version, gist, demo;
let app = {
components: [],
values: {}
};
let repl;
let name = 'loading...';
onMount(() => {
@ -67,7 +63,7 @@
return a.name < b.name ? -1 : 1;
});
app = { components, values };
repl.set({ components, values });
});
} else if (demo) {
const url = `api/examples/${demo}`;
@ -76,10 +72,10 @@
if (response.ok) {
const data = await response.json();
app = {
repl.set({
values: tryParseData(data.json5) || {}, // TODO make this more error-resistant
components: data.components
};
});
}
});
}
@ -114,6 +110,6 @@
<div class="repl-outer">
{#if process.browser}
<Repl {version} {app} embedded={true}/>
<Repl bind:this={repl} {version} embedded={true}/>
{/if}
</div>

@ -18,16 +18,10 @@
export let version, demo, gist_id;
let repl;
let gist;
let app = {
components: [],
values: {}
};
let name = 'loading...';
let zen_mode = false;
let repl;
$: if (typeof history !== 'undefined') {
const params = [];
@ -99,7 +93,7 @@
return a.name < b.name ? -1 : 1;
});
app = { components, values };
repl.set({ components, values });
});
}
});
@ -117,10 +111,10 @@
console.log(data.components);
app = {
repl.set({
values: tryParseData(data.json5) || {}, // TODO make this more error-resistant
components: data.components
};
});
gist = null;
}
@ -198,7 +192,6 @@
<div class="repl-outer {zen_mode ? 'zen-mode' : ''}">
<AppControls
{examples}
{app}
{name}
{gist}
{repl}
@ -208,6 +201,6 @@
/>
{#if process.browser}
<Repl bind:this={repl} {version} {app}/>
<Repl bind:this={repl} {version}/>
{/if}
</div>

@ -24,8 +24,9 @@
const { sections } = getContext('tutorial');
const lookup = new Map();
let repl;
let prev;
const lookup = new Map();
sections.forEach(section => {
section.chapters.forEach(chapter => {
@ -52,27 +53,24 @@
$: selected = lookup.get(slug);
$: improve_link = `${tutorial_repo_link}/${selected.chapter.section_dir}/${selected.chapter.chapter_dir}`;
// TODO once reactive values are fixed
// $: app = {
// components: chapter.app_a,
// values: {}
// };
let app;
$: start(chapter);
const clone = file => ({
name: file.name,
type: file.type,
source: file.source
});
function start(chapter) {
app = {
components: chapter.app_a,
$: if (repl) {
repl.set({
components: chapter.app_a.map(clone),
values: {}
};
});
}
function complete() {
app = {
components: chapter.app_b,
repl.update({
components: chapter.app_b.map(clone),
values: {}
};
});
}
</script>
@ -232,6 +230,6 @@
</div>
<div class="tutorial-repl">
<Repl {app} orientation="rows" show_props={false}/>
<Repl bind:this={repl} orientation="rows" show_props={false}/>
</div>
</div>

@ -29,7 +29,7 @@ const commonCompilerOptions = {
css: false
};
function compile({ source, options, entry }) {
function compile({ id, source, options, entry }) {
try {
const { js, css, stats, vars } = svelte.compile(
source,
@ -40,11 +40,26 @@ function compile({ source, options, entry }) {
? (vars || stats.vars).map(v => v.writable && v.export_name).filter(Boolean) // TODO remove stats post-launch
: null;
return { js: js.code, css: css.code, props };
return {
id,
result: {
js: js.code,
css: css.code || `/* Add a <sty` + `le> tag to see compiled CSS */`,
props
}
};
} catch (err) {
let result = `/* Error compiling component\n\n${err.message}`;
if (err.frame) result += `\n${err.frame}`;
result += `\n\n*/`;
return { code: result, props: null };
let message = `/* Error compiling component\n\n${err.message}`;
if (err.frame) message += `\n${err.frame}`;
message += `\n\n*/`;
return {
id,
result: {
js: message,
css: message,
props: null
}
};
}
}

Loading…
Cancel
Save