Skip to content

Commit

Permalink
fix: live reloading (QwikDev#511)
Browse files Browse the repository at this point in the history
  • Loading branch information
manucorporat authored May 23, 2022
1 parent 35a4a9e commit afd72cb
Show file tree
Hide file tree
Showing 24 changed files with 554 additions and 189 deletions.
2 changes: 1 addition & 1 deletion packages/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
},
"devDependencies": {
"@builder.io/partytown": "^0.5.4",
"@builder.io/qwik": "0.0.20-5",
"@builder.io/qwik": "0.0.20-7",
"@builder.io/qwik-city": "0.0.5",
"@cloudflare/kv-asset-handler": "0.2.0",
"@cloudflare/workers-types": "^3.10.0",
Expand Down
32 changes: 32 additions & 0 deletions packages/docs/pages/docs/file.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
title: Overview
fetch: https://hackmd.io/@mhevery/Sy52N2Ax9
---

# File

Qwik is a new kind of web framework that can deliver instant loading web applications at any size or complexity. Your sites and apps can boot with less than 1kb of JS (_including_ your code, regardless of complexity), and achieve unheard of performance at scale.

## Qwik is:

- **General-purpose**: Qwik can be used to build any type of web site or application
- **Instant-on**: Unlike other frameworks, Qwik is [resumable](./concepts/resumable.mdx) which means Qwik applications require **0 hydration**. This allows Qwik apps to have instant-on interactivity, regardless of size or complexity
- **Optimized for speed**: Qwik has unprecedented performance, offering sub-second full page loads even on mobile devices. Qwik achieves this by delivering pure HTML, and incrementally loading JS only as-needed.

<img
alt="Qwik Diagram"
src="https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2Fd33c7a95e98144f682ab67dd27d1f957?format=webp&width=2000"
/>

## Does page speed really matter?

Put simply: slow sites deter visitors, costing businesses millions. Fast sites have better SEO, better UX, and are more profitable.

Some examples from [web.dev](https://web.dev):

| | |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Every 100ms faster → 1% more conversions** <br /> For Mobify, every 100ms decrease in homepage load speed worked out to a 1.11% increase in session-based conversion, yielding an average annual revenue increase of nearly $380,000. | **50% faster → 12% more sales** <br /> When AutoAnything reduced page load time by half, they saw a boost of 12% to 13% in sales. |
| **20% faster → 10% more conversions** <br /> Retailer Furniture Village audited their site speed and developed a plan to address the problems they found, leading to a 20% reduction in page load time and a 10% increase in conversion rate. | **40% faster → 15% more sign-ups** <br /> Pinterest reduced perceived wait times by 40% and this increased search engine traffic and sign-ups by 15%. |
| **850ms faster → 7% more conversions** <br /> COOK reduced average page load time by 850 milliseconds which increased conversions by 7%, decreased bounce rates by 7%, and increased pages per session by 10%. | **1 seconds slowness → 10% less users** <br /> The BBC found they lost an additional 10% of users for every additional second their site took to load. |
| | |
66 changes: 49 additions & 17 deletions packages/docs/src/components/repl/worker/update.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable no-console */
import type { InputOptions, OutputAsset, OutputChunk } from 'rollup';
import type { QwikRollupPluginOptions } from '@builder.io/qwik/optimizer';
import type { Diagnostic, QwikRollupPluginOptions } from '@builder.io/qwik/optimizer';
import type { ReplInputOptions, ReplModuleOutput, ReplResult } from '../types';
import { getCtx, QwikReplContext } from './context';
import { loadDependencies } from './dependencies';
Expand Down Expand Up @@ -40,11 +40,13 @@ export const update = async (options: ReplInputOptions) => {
ctx.clientModules = result.clientModules;
} catch (e: any) {
result.diagnostics.push({
scope: 'runtime',
message: String(e.stack || e),
severity: 'Error',
origin: String(e.stack || 'repl error'),
code_highlights: [],
show_environment: false,
category: 'error',
file: '',
highlights: [],
suggestions: null,
code: 'runtime error',
});
console.error(e);
}
Expand Down Expand Up @@ -87,13 +89,28 @@ const bundleClient = async (
replMinify(options),
],
onwarn(warning) {
result.diagnostics.push({
const diagnostic: Diagnostic = {
scope: 'rollup-ssr',
code: warning.code ?? null,
message: warning.message,
severity: 'Error',
show_environment: false,
code_highlights: [],
origin: 'client rollup',
});
category: 'warning',
highlights: [],
file: warning.id || '',
suggestions: null,
};
const loc = warning.loc;
if (loc && loc.file) {
diagnostic.file = loc.file;
diagnostic.highlights.push({
startCol: loc.column,
endCol: loc.column + 1,
startLine: loc.line,
endLine: loc.line + 1,
lo: 0,
hi: 0,
});
}
result.diagnostics.push(diagnostic);
},
};

Expand Down Expand Up @@ -137,13 +154,28 @@ const bundleSSR = async (options: ReplInputOptions, ctx: QwikReplContext, result
cache: ctx.ssrCache,
plugins: [self.qwikOptimizer?.qwikRollup(qwikRollupSsrOpts), replResolver(options, 'ssr')],
onwarn(warning) {
result.diagnostics.push({
const diagnostic: Diagnostic = {
scope: 'rollup-ssr',
code: warning.code ?? null,
message: warning.message,
severity: 'Error',
show_environment: false,
code_highlights: [],
origin: 'ssr rollup',
});
category: 'warning',
highlights: [],
file: warning.id || '',
suggestions: null,
};
const loc = warning.loc;
if (loc && loc.file) {
diagnostic.file = loc.file;
diagnostic.highlights.push({
startCol: loc.column,
endCol: loc.column + 1,
startLine: loc.line,
endLine: loc.line + 1,
lo: 0,
hi: 0,
});
}
result.diagnostics.push(diagnostic);
},
};

Expand Down
2 changes: 1 addition & 1 deletion packages/qwik/src/core/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ export interface QRL<TYPE = any> {
// (undocumented)
__brand__QRL__: TYPE;
// (undocumented)
invoke(...args: TYPE extends (...args: infer ARGS) => any ? ARGS : never): TYPE extends (...args: any[]) => infer RETURN ? ValueOrPromise<RETURN> : never;
invoke(...args: TYPE extends (...args: infer ARGS) => any ? ARGS : never): Promise<TYPE extends (...args: any[]) => infer RETURN ? RETURN : never>;
// (undocumented)
invokeFn(el?: Element, context?: InvokeContext, beforeFn?: () => void): TYPE extends (...args: infer ARGS) => infer RETURN ? (...args: ARGS) => ValueOrPromise<RETURN> : never;
// (undocumented)
Expand Down
88 changes: 49 additions & 39 deletions packages/qwik/src/core/component/component-ctx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { isStyleTask, newInvokeContext } from '../use/use-core';
import { getProps, QContext } from '../props/props';
import { processNode } from '../render/jsx/jsx-runtime';
import { wrapSubscriber } from '../use/use-subscriber';
import { logDebug } from '../util/log';
import { logDebug, logError } from '../util/log';
import type { ValueOrPromise } from '../util/types';
import { removeSub } from '../object/q-object';

Expand Down Expand Up @@ -42,46 +42,56 @@ export const renderComponent = (rctx: RenderContext, ctx: QContext): ValueOrProm
ctx.refMap.array.forEach((obj) => {
removeSub(obj, hostElement);
});
const onRenderFn = onRenderQRL.invokeFn(rctx.containerEl, invocatinContext);

// Execution of the render function
const renderPromise = onRenderFn(wrapSubscriber(getProps(ctx), hostElement));
const onRenderFn = onRenderQRL.invokeFn(rctx.containerEl, invocatinContext);

// Wait for results
return then(renderPromise, (jsxNode) => {
rctx.hostElements.add(hostElement);
try {
// Execution of the render function
const renderPromise = onRenderFn(wrapSubscriber(getProps(ctx), hostElement));

const waitOnPromise = promiseAll(waitOn);
return then(waitOnPromise, (waitOnResolved) => {
waitOnResolved.forEach((task) => {
if (isStyleTask(task)) {
appendStyle(rctx, hostElement, task);
}
});
if (ctx.dirty) {
logDebug('Dropping render. State changed during render.');
return renderComponent(rctx, ctx);
// Wait for results
return then(
renderPromise,
(jsxNode) => {
rctx.hostElements.add(hostElement);
const waitOnPromise = promiseAll(waitOn);
return then(waitOnPromise, (waitOnResolved) => {
waitOnResolved.forEach((task) => {
if (isStyleTask(task)) {
appendStyle(rctx, hostElement, task);
}
});
if (ctx.dirty) {
logDebug('Dropping render. State changed during render.');
return renderComponent(rctx, ctx);
}
let componentCtx = ctx.component;
if (!componentCtx) {
componentCtx = ctx.component = {
hostElement,
slots: [],
styleHostClass: undefined,
styleClass: undefined,
styleId: undefined,
};
const scopedStyleId = hostElement.getAttribute(ComponentScopedStyles) ?? undefined;
if (scopedStyleId) {
componentCtx.styleId = scopedStyleId;
componentCtx.styleHostClass = styleHost(scopedStyleId);
componentCtx.styleClass = styleContent(scopedStyleId);
hostElement.classList.add(componentCtx.styleHostClass);
}
}
componentCtx.slots = [];
newCtx.components.push(componentCtx);
return visitJsxNode(newCtx, hostElement, processNode(jsxNode), false);
});
},
(err) => {
logError(err);
}
let componentCtx = ctx.component;
if (!componentCtx) {
componentCtx = ctx.component = {
hostElement,
slots: [],
styleHostClass: undefined,
styleClass: undefined,
styleId: undefined,
};
const scopedStyleId = hostElement.getAttribute(ComponentScopedStyles) ?? undefined;
if (scopedStyleId) {
componentCtx.styleId = scopedStyleId;
componentCtx.styleHostClass = styleHost(scopedStyleId);
componentCtx.styleClass = styleContent(scopedStyleId);
hostElement.classList.add(componentCtx.styleHostClass);
}
}
componentCtx.slots = [];
newCtx.components.push(componentCtx);
return visitJsxNode(newCtx, hostElement, processNode(jsxNode), false);
});
});
);
} catch (err) {
logError(err);
}
};
5 changes: 3 additions & 2 deletions packages/qwik/src/core/import/qrl-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,10 @@ class QRL<TYPE = any> implements IQRL<TYPE> {
return copy;
}

invoke(...args: TYPE extends (...args: infer ARGS) => any ? ARGS : never) {
async invoke(...args: TYPE extends (...args: infer ARGS) => any ? ARGS : never) {
const fn = this.invokeFn();
return fn(...args) as any;
const result = await fn(...args);
return result;
}

serialize(options?: QRLSerializeOptions) {
Expand Down
2 changes: 1 addition & 1 deletion packages/qwik/src/core/import/qrl.public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export interface QRL<TYPE = any> {
resolve(container?: Element): Promise<TYPE>;
invoke(
...args: TYPE extends (...args: infer ARGS) => any ? ARGS : never
): TYPE extends (...args: any[]) => infer RETURN ? ValueOrPromise<RETURN> : never;
): Promise<TYPE extends (...args: any[]) => infer RETURN ? RETURN : never>;

invokeFn(
el?: Element,
Expand Down
9 changes: 9 additions & 0 deletions packages/qwik/src/core/object/q-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { debugStringify } from '../util/stringify';
import { WatchDescriptor, WatchFlags } from '../watch/watch.public';
import type { Subscriber } from '../use/use-subscriber';
import { tryGetContext } from '../props/props';
import { RenderEvent } from '../util/markers';

export type ObjToProxyMap = WeakMap<any, any>;
export type QObject<T extends {}> = T & { __brand__: 'QObject' };
Expand Down Expand Up @@ -187,6 +188,14 @@ class ReadWriteProxyHandler implements ProxyHandler<TargetType> {
const unwrappedNewValue = unwrapProxy(newValue);
if (qDev) {
verifySerializable(unwrappedNewValue);
const invokeCtx = tryGetInvokeContext();
if (invokeCtx && invokeCtx.event === RenderEvent) {
logWarn(
'State mutation inside render function. Move mutation to useWatch(), useClientEffect() or useServerMount()',
invokeCtx.hostElement,
prop
);
}
}
const isArray = Array.isArray(target);
if (isArray) {
Expand Down
5 changes: 3 additions & 2 deletions packages/qwik/src/core/util/promises.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ export function isPromise(value: any): value is Promise<any> {

export const then = <T, B>(
promise: ValueOrPromise<T>,
thenFn: (arg: Awaited<T>) => ValueOrPromise<B>
thenFn: (arg: Awaited<T>) => ValueOrPromise<B>,
rejectFn?: (err: any) => any
): ValueOrPromise<B> => {
return isPromise(promise) ? promise.then(thenFn as any) : thenFn(promise as any);
return isPromise(promise) ? promise.then(thenFn as any, rejectFn) : thenFn(promise as any);
};

export const promiseAll = <T extends any[]>(promises: T): ValueOrPromise<T> => {
Expand Down
33 changes: 19 additions & 14 deletions packages/qwik/src/optimizer/core/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::code_move::{new_module, NewModuleCtx};
use crate::collector::global_collect;
use crate::entry_strategy::EntryPolicy;
use crate::transform::{HookKind, QwikTransform, QwikTransformOptions};
use crate::utils::{CodeHighlight, Diagnostic, DiagnosticSeverity, SourceLocation};
use crate::utils::{Diagnostic, DiagnosticCategory, DiagnosticScope, SourceLocation};
use path_slash::PathExt;
use serde::{Deserialize, Serialize};

Expand All @@ -20,7 +20,7 @@ use anyhow::{Context, Error};

use swc_atoms::JsWord;
use swc_common::comments::SingleThreadedComments;
use swc_common::errors::{DiagnosticBuilder, Emitter, Handler};
use swc_common::errors::{DiagnosticBuilder, DiagnosticId, Emitter, Handler};
use swc_common::{sync::Lrc, FileName, Globals, Mark, SourceMap};
use swc_ecmascript::ast;
use swc_ecmascript::codegen::text_writer::JsWriter;
Expand Down Expand Up @@ -486,25 +486,30 @@ fn handle_error(
.iter()
.map(|diagnostic| {
let message = diagnostic.message();
let code = diagnostic.get_code().and_then(|m| {
if let DiagnosticId::Error(s) = m {
Some(s)
} else {
None
}
});

let span = diagnostic.span.clone();
let suggestions = diagnostic.suggestions.clone();

let span_labels = span.span_labels();
let code_highlights = if span_labels.is_empty() {
let highlights = if span_labels.is_empty() {
None
} else {
Some(
span_labels
.into_iter()
.map(|span_label| CodeHighlight {
message: span_label.label,
loc: SourceLocation::from(source_map, span_label.span),
})
.map(|span_label| SourceLocation::from(source_map, span_label.span))
.collect(),
)
};

let hints = if suggestions.is_empty() {
let suggestions = if suggestions.is_empty() {
None
} else {
Some(
Expand All @@ -516,13 +521,13 @@ fn handle_error(
};

Diagnostic {
origin: origin.clone(),
file: origin.clone(),
code,
message,
code_highlights,
hints,
show_environment: false,
severity: DiagnosticSeverity::Error,
documentation_url: None,
highlights,
suggestions,
category: DiagnosticCategory::Error,
scope: DiagnosticScope::Optimizer,
}
})
.collect()
Expand Down
Loading

0 comments on commit afd72cb

Please sign in to comment.