Skip to content

Commit

Permalink
Merge pull request #55 from atellmer/bugfix/resolve-routes
Browse files Browse the repository at this point in the history
Bugfix/resolve routes
  • Loading branch information
atellmer authored Apr 13, 2024
2 parents d5b66dc + 98aff1d commit 6d9591b
Show file tree
Hide file tree
Showing 19 changed files with 452 additions and 236 deletions.
11 changes: 7 additions & 4 deletions packages/core/src/fiber/fiber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { detectIsTagVirtualNode, detectIsPlainVirtualNode, detectAreSameComponen
import { type Instance, type Callback, type TimerId } from '../shared';
import { type Context, type ContextProviderValue } from '../context';
import { detectIsComponent } from '../component';
import { detectIsFunction } from '../utils';
import { detectIsFunction, error } from '../utils';
import { type Atom } from '../atom';
import { $$scope } from '../scope';

Expand Down Expand Up @@ -72,11 +72,14 @@ class Fiber<N = NativeElement> {
}
}

setError(error: Error) {
setError(err: Error) {
if (detectIsFunction(this.catch)) {
this.catch(error);
this.catch(err);
error(err);
} else if (this.parent) {
this.parent.setError(error);
this.parent.setError(err);
} else {
throw err;
}
}

Expand Down
40 changes: 24 additions & 16 deletions packages/core/src/scheduler/scheduler.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { ROOT, HOOK_DELIMETER, YIELD_INTERVAL, TaskPriority } from '../constants';
import { getTime, detectIsFunction, detectIsPromise, nextTick } from '../utils';
import { type WorkLoop, workLoop, detectIsBusy } from '../workloop';
import { type SetPendingStatus } from '../start-transition';
import { type Callback } from '../shared';
import { type Fiber } from '../fiber';
import { ROOT, HOOK_DELIMETER, YIELD_INTERVAL, TaskPriority } from '../constants';
import { getTime, detectIsFunction, nextTick } from '../utils';
import { EventEmitter } from '../emitter';
import { platform } from '../platform';
import { type Fiber } from '../fiber';

class MessageChannel extends EventEmitter<PortEvent> {
port1: MessagePort = null;
Expand Down Expand Up @@ -170,9 +170,10 @@ class Scheduler {

private execute() {
const isBusy = detectIsBusy();
const { high, normal, low } = this.getQueues();

if (!isBusy && !this.isMessageLoopRunning) {
const { high, normal, low } = this.getQueues();

this.pick(high) || this.pick(normal) || this.pick(low);
}
}
Expand All @@ -187,26 +188,33 @@ class Scheduler {
}

private requestCallback(callback: WorkLoop) {
callback(false);
this.task = null;
this.execute();
const result = callback(false);

if (detectIsPromise(result)) {
result.finally(() => {
this.requestCallback(callback);
});
} else {
this.task = null;
this.execute();
}
}

private performWorkUntilDeadline() {
if (this.scheduledCallback) {
this.deadline = getTime() + YIELD_INTERVAL;
const hasMoreWork = this.scheduledCallback(true);
const result = this.scheduledCallback(true);

if (hasMoreWork) {
if (detectIsPromise(result)) {
result.finally(() => {
this.port.postMessage(null);
});
} else if (result) {
this.port.postMessage(null);
} else {
if (hasMoreWork === null) {
setTimeout(() => this.port.postMessage(null)); // has promise
} else {
this.complete(this.task);
this.reset();
this.execute();
}
this.complete(this.task);
this.reset();
this.execute();
}
} else {
this.isMessageLoopRunning = false;
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const detectIsEmpty = (o: any) => detectIsNull(o) || detectIsUndefined(o);

const detectIsFalsy = (o: any) => detectIsEmpty(o) || o === false;

const detectIsPromise = <T = unknown>(o: any): o is Promise<T> => o instanceof Promise;

const getTime = () => Date.now();

const dummyFn = () => {};
Expand Down Expand Up @@ -102,6 +104,7 @@ export {
detectIsNull,
detectIsEmpty,
detectIsFalsy,
detectIsPromise,
getTime,
dummyFn,
trueFn,
Expand Down
19 changes: 6 additions & 13 deletions packages/core/src/workloop/workloop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
detectIsArray,
detectIsFunction,
detectIsTextBased,
detectIsPromise,
createIndexKey,
trueFn,
} from '../utils';
Expand Down Expand Up @@ -53,12 +54,9 @@ import { type RestoreOptions, scheduler } from '../scheduler';
import { Fragment, detectIsFragment } from '../fragment';
import { unmountFiber } from '../unmount';

let hasPendingPromise = false;
export type WorkLoop = (isAsync: boolean) => boolean | Promise<unknown> | null;

export type WorkLoop = (isAsync: boolean) => boolean;

function workLoop(isAsync: boolean): boolean | null {
if (hasPendingPromise) return null;
function workLoop(isAsync: boolean): boolean | Promise<unknown> | null {
const $scope = $$scope();
const wipFiber = $scope.getWorkInProgress();
let unit = $scope.getNextUnitOfWork();
Expand All @@ -78,12 +76,8 @@ function workLoop(isAsync: boolean): boolean | null {
commit($scope);
}
} catch (err) {
if (err instanceof Promise) {
hasPendingPromise = true;
err.finally(() => {
hasPendingPromise = false;
!isAsync && workLoop(false);
});
if (detectIsPromise(err)) {
return err;
} else {
const emitter = $scope.getEmitter();

Expand Down Expand Up @@ -378,10 +372,9 @@ function mount(fiber: Fiber, prev: Fiber, $scope: Scope) {
component.children = result as Array<Instance>;
platform.detectIsPortal(inst) && fiber.markHost(PORTAL_HOST_MASK);
} catch (err) {
if (err instanceof Promise) throw err;
if (detectIsPromise(err)) throw err;
component.children = [];
fiber.setError(err);
error(err);
}
} else if (detectIsVirtualNodeFactory(inst)) {
inst = inst();
Expand Down
6 changes: 3 additions & 3 deletions packages/styled/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ const GlobalStyle = createGlobalStyle<{ $light?: boolean }>`
```
## Theming

The styled offers complete theming support by exporting a `<ThemeProvider>` wrapper component. This component supplies a theme to all its child components through the context API. Consequently, all styled components in the render tree, regardless of their depth, can access the provided theme.
The styled offers complete theming support by exporting a `<ThemeProvider>` wrapper component. This component supplies a theme to all its child components through the `Context API`. Consequently, all styled components in the render tree, regardless of their depth, can access the provided theme.

```tsx
type Theme = {
Expand Down Expand Up @@ -522,7 +522,7 @@ const style = useStyle(styled => ({

## Server Side Rendering

The styled supports server-side rendering, complemented by stylesheet rehydration. Essentially, each time your application is rendered on the server, a `ServerStyleSheet` can be created and a provider can be added to your component tree, which accepts styles through a context API.
The styled supports server-side rendering, complemented by stylesheet rehydration. Essentially, each time your application is rendered on the server, a `ServerStyleSheet` can be created and a provider can be added to your component tree, which accepts styles through a `Context API`. Please note that `sheet.collectStyles()` already contains the provider and you do not need to do anything additional.

### Rendering to string

Expand All @@ -535,7 +535,7 @@ const sheet = new ServerStyleSheet();
try {
const app = await renderToString(sheet.collectStyles(<App />));
const tags = sheet.getStyleTags();
const mark = '{{%styles%}}' // somewhere in your <head></head>
const mark = '__styled__' // somewhere in your <head></head>
const page = `<!DOCTYPE html>${app}`.replace(mark, tags.join(''));

res.statusCode = 200;
Expand Down
2 changes: 1 addition & 1 deletion packages/web-router/src/context/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { type Route } from '../create-routes';
export type ActiveRouteContextValue = {
location: RouterLocation;
params: Map<string, string>;
activeRoute: Route;
route: Route;
};

const ActiveRouteContext = createContext<ActiveRouteContextValue>(null, { displayName: 'ActiveRoute' });
Expand Down
136 changes: 115 additions & 21 deletions packages/web-router/src/create-routes/create-routes.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import { component } from '@dark-engine/core';
import { resetBrowserHistory } from '@test-utils';
import { component, Fragment } from '@dark-engine/core';

import { Routes } from './types';
import { createRoutes, resolve } from './create-routes';
import { ROOT_MARK } from '../constants';

afterEach(() => {
resetBrowserHistory();
});

describe('@web-router/create-routes', () => {
test('can match simple routes correctly', () => {
Expand Down Expand Up @@ -52,13 +46,14 @@ describe('@web-router/create-routes', () => {
];
const $routes = createRoutes(routes);

expect(resolve('/', $routes)).toBe(null);
expect(resolve('', $routes)).toBe(null);
expect(resolve('/xxx', $routes)).toBe(null);
expect(() => resolve('/', $routes)).toThrowError();
expect(() => resolve('', $routes)).toThrowError();
expect(() => resolve('/xxx', $routes)).toThrowError();
expect(resolve('/second', $routes).path).toBe('second');
expect(resolve('/second/1', $routes)).toBe(null);
expect(resolve('/first/1/xxx', $routes)).toBe(null);
expect(resolve('/some/broken/url', $routes)).toBe(null);
expect(() => resolve('/second/1', $routes)).toThrowError();
expect(() => resolve('/first/1/xxx', $routes)).toThrowError();
expect(() => resolve('/some/broken/url', $routes)).toThrowError();
expect(resolve('/first', $routes).path).toBe('first');
});

test('can match nested routes correctly', () => {
Expand Down Expand Up @@ -92,7 +87,7 @@ describe('@web-router/create-routes', () => {
expect(resolve('/second', $routes).path).toBe('second');
expect(resolve('/second/a', $routes).path).toBe('second/a');
expect(resolve('/second/b', $routes).path).toBe('second/b');
expect(resolve('/second/b/some/broken/route', $routes)).toBe(null);
expect(() => resolve('/second/b/some/broken/route', $routes)).toThrowError();
expect(resolve('/third', $routes).path).toBe('third');
});

Expand Down Expand Up @@ -280,9 +275,9 @@ describe('@web-router/create-routes', () => {
];
const $routes = createRoutes(routes);

expect(resolve('/', $routes).path).toBe(`${ROOT_MARK}`);
expect(resolve('', $routes).path).toBe(`${ROOT_MARK}`);
expect(resolve('/broken', $routes).path).toBe(`${ROOT_MARK}`);
expect(resolve('/', $routes).path).toBe('');
expect(resolve('', $routes).path).toBe('');
expect(resolve('/broken', $routes).path).toBe('');
expect(resolve('/second', $routes).path).toBe(`second`);
expect(resolve('/third', $routes).path).toBe(`third`);
});
Expand Down Expand Up @@ -724,13 +719,112 @@ describe('@web-router/create-routes', () => {
];
const $routes = createRoutes(routes);

expect(resolve('/', $routes).path).toBe(`first/${ROOT_MARK}`);
expect(resolve('/first', $routes).path).toBe(`first/${ROOT_MARK}`);
expect(resolve('/', $routes).path).toBe(`first`);
expect(resolve('/first', $routes).path).toBe(`first`);
expect(resolve('/first/nested', $routes).path).toBe('first/nested');
expect(resolve('/first/666', $routes).path).toBe(`first/:id`);
expect(resolve('/first/666/broken', $routes).path).toBe(`first/${ROOT_MARK}`);
expect(resolve('/first/666/broken', $routes).path).toBe(`first`);
expect(resolve('/second', $routes).path).toBe('second');
expect(resolve('/third/', $routes).path).toBe('third');
expect(resolve('/broken/url', $routes).path).toBe(`first/${ROOT_MARK}`);
expect(resolve('/broken/url', $routes).path).toBe(`first`);
});

test('can resolve nested indexed routes', () => {
// https://github.com/atellmer/dark/issues/53
const routes: Routes = [
{
path: '/',
component: Fragment,
children: [
{
path: '/',
component: Fragment,
},
{
path: 'contact',
component: Fragment,
},
{
path: 'de',
component: Fragment,
children: [
{
path: '/',
component: Fragment,
},
{
path: 'contact',
component: Fragment,
},
],
},
],
},
{
path: '**',
redirectTo: '/',
},
];
const $routes = createRoutes(routes);

expect(resolve('/', $routes).path).toBe(``);
expect(resolve('/contact', $routes).path).toBe(`contact`);
expect(resolve('/de', $routes).path).toBe('de');
expect(resolve('/de/contact', $routes).path).toBe(`de/contact`);
expect(resolve('/broken', $routes).path).toBe(``);
expect(resolve('/de/broken', $routes).path).toBe(``);
expect(resolve('/de/contact/broken', $routes).path).toBe(``);
});

test('can resolve i18n static routes', () => {
// https://github.com/atellmer/dark/issues/53
const routes: Routes = [
...['en', 'it', 'fr'].map(lang => ({
path: lang,
component: Fragment,
children: [
{
path: 'contact',
component: Fragment,
},
{
path: '**',
pathMatch: 'full',
redirectTo: '/not-found',
},
],
})),
{
path: '',
component: Fragment,
children: [
{
path: 'contact',
component: Fragment,
},
{
path: 'not-found',
component: Fragment,
},
],
},
{
path: '**',
redirectTo: 'not-found',
},
] as Routes;
const $routes = createRoutes(routes);

expect(resolve('/', $routes).path).toBe(``);
expect(resolve('/contact', $routes).path).toBe(`contact`);
expect(resolve('/en', $routes).path).toBe('en');
expect(resolve('/en/contact', $routes).path).toBe(`en/contact`);
expect(resolve('/it', $routes).path).toBe('it');
expect(resolve('/it/contact', $routes).path).toBe(`it/contact`);
expect(resolve('/fr', $routes).path).toBe('fr');
expect(resolve('/fr/contact', $routes).path).toBe(`fr/contact`);
expect(resolve('/broken', $routes).path).toBe(`not-found`);
expect(resolve('/en/broken', $routes).path).toBe(`not-found`);
expect(resolve('/en/contact/broken', $routes).path).toBe(`not-found`);
});
});
Loading

0 comments on commit 6d9591b

Please sign in to comment.