Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for q:base #93

Merged
merged 2 commits into from
Nov 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ check_rules_nodejs_version(minimum_version_string = "2.2.0")

# Setup the Node.js toolchain
node_repositories(
node_version = "15.0.1",
package_json = ["//:package.json"],
node_version = "16.6.2",
)

yarn_install(
Expand Down
11 changes: 4 additions & 7 deletions integration/specs/qwikloader_spec.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
<html>
<head>
<script src="/qwikloader.js" type="module"></script>
<link rel="q.protocol.base" href="./" />
</head>
<body>
<h1>it should register all events</h1>
Expand All @@ -13,9 +12,7 @@ <h1>it should register all events</h1>
- it can listen on all events an
- and it can process protocol urls.
</pre>
<button
on:click="base:qwikloader_spec#updateElement?selector=%23click_test>pre&content=PASSED"
>
<button on:click="./qwikloader_spec#updateElement?selector=%23click_test>pre&content=PASSED">
click
</button>
</test>
Expand All @@ -26,7 +23,7 @@ <h1>it should listen on non-bubbling event</h1>
Verify that non-bubbling events are correctly captured.
</pre>
<button
on:mouseenter="base:qwikloader_spec#updateElement?selector=%23non_bubbling_event>pre&content=PASSED"
on:mouseenter="./qwikloader_spec#updateElement?selector=%23non_bubbling_event>pre&content=PASSED"
>
mouseenter
</button>
Expand All @@ -35,7 +32,7 @@ <h1>it should listen on non-bubbling event</h1>
<h1>it should set up `$init` event</h1>
<test id="autofire_$init">
<pre
on:q-init="base:qwikloader_spec#updateElement?selector=%23autofire_\$init>pre&content=PASSED"
on:q-init="./qwikloader_spec#updateElement?selector=%23autofire_\$init>pre&content=PASSED"
>
Verify qwikloader correctly fires `$init` event on start.
</pre>
Expand All @@ -44,7 +41,7 @@ <h1>it should set up `$init` event</h1>
<h1>it should broadcast document events</h1>
<test id="broadcast_scroll">
<pre
on:document:scroll="base:qwikloader_spec#updateElement?selector=%23broadcast_scroll>pre&content=PASSED"
on:document:scroll="./qwikloader_spec#updateElement?selector=%23broadcast_scroll>pre&content=PASSED"
>
Verify qwikloader correctly fires `scroll` event on scroll.
</pre>
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@
"link.dist": "cd dist-dev/@builder.io-qwik && npm link"
},
"devDependencies": {
"@bazel/typescript": "^3.5.0",
"@bazel/ibazel": "^0.15.10",
"@bazel/bazelisk": "^1.7.5",
"@microsoft/api-extractor": "^7.18.19",
"@napi-rs/cli": "^1.3.5",
"@napi-rs/triples": "^1.0.3",
"@node-rs/helper": "^1.2.1",
"@octokit/action": "^3.18.0",
"@types/cross-spawn": "^6.0.2",
Expand Down
3 changes: 3 additions & 0 deletions scripts/package_build.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def package_build(
"@npm//rollup",
"@npm//source-map-support",
"@npm//terser",
"@npm//@octokit/action",
"@npm//typescript",
"@npm//@types/cross-spawn",
"@npm//cross-spawn",
Expand All @@ -34,6 +35,8 @@ def package_build(
"@npm//semver",
"@npm//path-browserify",
"@npm//@types/path-browserify",
"@npm//@types/prompts",
"@npm//prompts",
"//scripts:all_build_source",
],
outs = [
Expand Down
41 changes: 22 additions & 19 deletions src/bootloader-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,26 @@
*/
export const qrlResolver = (
doc: Document,
eventUrl: string | null | undefined,
linkElm?: HTMLLinkElement,
href?: string,
url?: URL
element: Element | null,
eventUrl?: string | null,
_url?: string,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not important, but why the _ here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because it is cut and paste from a bootloader where they are parameters to save bytes and I wanted to make it clear that they are not to be used.

_base?: string | URL
): URL | undefined => {
if (eventUrl) {
url = new URL(
eventUrl.replace(/^(\w+):(\/)?/, (str, protocol, slash) => {
linkElm = doc.querySelector(`[rel="q.protocol.${protocol}"]`) as HTMLLinkElement;
href = linkElm && linkElm.href;
if (!href) error(protocol + ' not defined');
return href + (href!.endsWith('/') ? '' : slash || '');
}),
doc.baseURI
);
url.pathname += '.js';
if (eventUrl === undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (eventUrl) won't do, can we use if (eventUrl == null)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NO, it specifically is looking for undefined

// recursive call
if (element) {
_url = element.getAttribute('q:base')!;
_base = qrlResolver(
doc,
element.parentNode && (element.parentNode as HTMLElement).closest('[q\\:base]')
);
} else {
_url = doc.baseURI;
}
} else if (eventUrl) {
(_url = eventUrl + '.js'), (_base = qrlResolver(doc, element!.closest('[q\\:base]')));
}
return url;
return _url ? new URL(_url, _base) : undefined;
};

const error = (msg: string) => {
Expand All @@ -67,7 +69,7 @@ export const qwikLoader = (doc: Document, hasInitialized?: boolean | number) =>
};

const dispatch = async (element: Element, eventName: string, ev: Event, url?: URL) => {
url = qrlResolver(doc, element.getAttribute('on:' + eventName));
url = qrlResolver(doc, element, element.getAttribute('on:' + eventName));
if (url) {
const handler = getModuleExport(
url,
Expand Down Expand Up @@ -172,13 +174,14 @@ export const setupPrefetching = (
const intersectionObserverCallback = (items: IntersectionObserverEntry[]) => {
items.forEach((item) => {
if (item.intersectionRatio > 0) {
const attrs = item.target.attributes;
const element = item.target;
const attrs = element.attributes;
for (let i = 0; i < attrs.length; i++) {
const attr = attrs[i];
const name = attr.name;
const value = attr.value;
if (name.startsWith('on:') && value) {
const url = qrlResolver(doc, value)!;
const url = qrlResolver(doc, element, value)!;
url.hash = url.search = '';
const key = url.toString();
if (!qrlCache[key]) {
Expand Down
42 changes: 25 additions & 17 deletions src/bootloader-shared.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,30 +98,38 @@ describe('qwikloader', () => {
doc = createDocument();
});

it('get protocol, trim ending slash', () => {
const link = doc.createElement('link');
link.setAttribute('rel', 'q.protocol.test');
link.href = 'http://qwik.dev/test/';
doc.head.appendChild(link);
it('should resolve full URL', () => {
const div = doc.createElement('div');
expect(String(qrlResolver(doc, div, 'http://foo.bar/baz'))).toEqual('http://foo.bar/baz.js');
});

const url = qrlResolver(doc, 'test:/hi')!;
expect(url.pathname).toBe('/test/hi.js');
it('should resolve relative URL against base', () => {
const div = doc.createElement('div');
expect(String(qrlResolver(doc, div, './bar'))).toEqual('http://document.qwik.dev/bar.js');
});

it('get protocol', () => {
const link = doc.createElement('link');
link.setAttribute('rel', 'q.protocol.test');
link.href = 'http://qwik.dev/test';
doc.head.appendChild(link);
it('should resolve relative URL against q:base', () => {
const div = doc.createElement('div');
div.setAttribute('q:base', '../baz/');
expect(String(qrlResolver(doc, div, './bar'))).toEqual('http://document.qwik.dev/baz/bar.js');
});

const url = qrlResolver(doc, 'test:/hi')!;
expect(url.pathname).toBe('/test/hi.js');
it('should resolve relative URL against nested q:base', () => {
const div = doc.createElement('div');
const parent = doc.createElement('parent');
doc.body.appendChild(parent);
parent.appendChild(div);
parent.setAttribute('q:base', './parent/');
div.setAttribute('q:base', './child/');
expect(String(qrlResolver(doc, div, './bar'))).toEqual(
'http://document.qwik.dev/parent/child/bar.js'
);
});

it('do nothing for null/undefined/empty string', () => {
expect(qrlResolver(doc, null)).toBeFalsy();
expect(qrlResolver(doc, undefined)).toBeFalsy();
expect(qrlResolver(doc, '')).toBeFalsy();
const div = doc.createElement('div');
expect(qrlResolver(doc, null, null)).toBeFalsy();
expect(qrlResolver(doc, div, '')).toBeFalsy();
});
});

Expand Down
2 changes: 1 addition & 1 deletion src/core/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ export function qHook<PROPS extends {}, STATE extends {} | undefined | unknown,
export function qHook<COMP extends QComponent, ARGS extends {} | unknown = unknown, RET = unknown>(hook: (props: PropsOf<COMP>, state: StateOf<COMP>, args: ARGS) => ValueOrPromise<RET>): QHook<PropsOf<COMP>, StateOf<COMP>, any, RET>;

// @public
export function qImport<T>(node: Node | Document, url: string | QRL<T> | URL): T | Promise<T>;
export function qImport<T>(element: Element, url: string | QRL<T> | URL): T | Promise<T>;

// @public (undocumented)
export type QObject<T extends {}> = T & {
Expand Down
5 changes: 3 additions & 2 deletions src/core/component/q-component-ctx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,11 @@ export function getHostElement(element: Element): HTMLElement | null {
function insertStyleIfNeeded(ctx: QComponentCtx, style: string | null) {
if (style) {
const styleId = styleKey(style as any)!;
const document = ctx.hostElement.ownerDocument;
const host = ctx.hostElement;
const document = host.ownerDocument;
const head = document.querySelector('head')!;
if (!head.querySelector(`style[q\\:style="${styleId}"]`)) {
const styleImport = Promise.resolve(qImport<string>(document, style));
const styleImport = Promise.resolve(qImport<string>(host, style));
styleImport.then((styles: string) => {
const style = document.createElement('style');
style.setAttribute('q:style', styleId);
Expand Down
7 changes: 3 additions & 4 deletions src/core/component/q-component.unit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ import { Fragment, h, qHook, qObject } from '@builder.io/qwik';
import { ElementFixture, trigger } from '../../testing/element_fixture';
import { expectDOM } from '../../testing/expect-dom.unit';
import { qRender } from '../render/q-render.public';
import { TEST_CONFIG } from '../util/test_config';
import { qComponent } from './q-component.public';
import { qStyles } from './qrl-styles';

describe('q-component', () => {
it('should declare and render basic component', async () => {
const fixture = new ElementFixture(TEST_CONFIG);
const fixture = new ElementFixture();
await qRender(fixture.host, <HelloWorld></HelloWorld>);
expectDOM(
fixture.host,
Expand All @@ -21,7 +20,7 @@ describe('q-component', () => {
});

it('should render Counter and accept events', async () => {
const fixture = new ElementFixture(TEST_CONFIG);
const fixture = new ElementFixture();
await qRender(fixture.host, <MyCounter step={5} value={15} />);
expectDOM(
fixture.host,
Expand Down Expand Up @@ -51,7 +50,7 @@ describe('q-component', () => {
});

it('should render a collection of todo items', async () => {
const host = new ElementFixture(TEST_CONFIG).host;
const host = new ElementFixture().host;
const items = qObject({
items: [
qObject({
Expand Down
64 changes: 23 additions & 41 deletions src/core/import/qImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,12 @@ import { assertDefined } from '../assert/assert';
/**
* Lazy load a `QRL` symbol and returns the resulting value.
*
* @param base -`QRL`s are relative, and therefore they need a base for resolution.
* - `Element` use `base.ownerDocument.baseURI`
* - `Document` use `base.baseURI`
* @param element - Location of the URL to resolve against.
* @param url - A relative URL (as `string` or `QRL`) or fully qualified `URL`
* @returns A cached value synchronously or promise of imported value.
* @public
*/
export function qImport<T>(node: Node | Document, url: string | QRL<T> | URL): T | Promise<T> {
export function qImport<T>(element: Element, url: string | QRL<T> | URL): T | Promise<T> {
if (isParsedQRL(url)) {
assertDefined(url._serialized);
url = Array.isArray(url._serialized) ? url._serialized[0] : url._serialized!;
Expand All @@ -35,9 +33,9 @@ export function qImport<T>(node: Node | Document, url: string | QRL<T> | URL): T
return Promise.resolve<T>(testSymbol);
}
}
const doc: QDocument = node.ownerDocument || (node as Document);
const doc: QDocument = element.ownerDocument!;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not possible for document to be an element?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not any more.

const corePlatform = getPlatform(doc);
const normalizedUrl = toUrl(doc, url);
const normalizedUrl = toUrl(doc, element, url);
const importPath = corePlatform.toPath(normalizedUrl);
const exportName = qExport(normalizedUrl);
const cacheKey = importPath + '#' + exportName;
Expand Down Expand Up @@ -82,43 +80,27 @@ export function qImportSet(doc: QDocument, cacheKey: string, value: any): void {
* @param url - relative URL
* @returns fully qualified URL.
*/
export function toUrl(doc: Document, url: string | QRL | URL): URL {
if (typeof url === 'string') {
const baseURI = getConfig(doc, `baseURI`) || doc.baseURI;
return new URL(adjustProtocol(doc, url), baseURI);
} else {
return url as URL;
}
}
export function toUrl(doc: Document, element: Element | null, url?: string | QRL | URL): URL {
let _url: string | QRL | URL;
let _base: string | URL | undefined = undefined;

/**
* Convert custom protocol to path by looking it up in `QConfig`
*
* Paths such as
* ```
* QRL`foo:/bar`
* ```
*
* The `QRL` looks up `foo` in the document's `<link ref="q.protocol.foo" href="somePath">`
* resulting in `somePath/bar`
*
* @param doc
* @param qrl
* @returns URL where the custom protocol has been resolved.
*/
function adjustProtocol(doc: Document, qrl: string | QRL): string {
return String(qrl).replace(/(^\w+):\/?/, (all, protocol) => {
let value = getConfig(doc, `protocol.` + protocol);
if (value && !value.endsWith('/')) {
value = value + '/';
if (url === undefined) {
// recursive call
if (element) {
_url = element.getAttribute('q:base')!;
_base = toUrl(
doc,
element.parentNode && (element.parentNode as HTMLElement).closest('[q\\:base]')
);
} else {
_url = doc.baseURI;
}
return value || all;
});
}

function getConfig(doc: Document, configKey: string) {
const linkElm = doc.querySelector(`link[rel="q.${configKey}"]`) as HTMLLinkElement;
return linkElm && linkElm.getAttribute('href');
} else if (url) {
(_url = url), (_base = toUrl(doc, element!.closest('[q\\:base]')));
} else {
throw new Error('INTERNAL ERROR');
}
return new URL(String(_url), _base);
}

/**
Expand Down
12 changes: 7 additions & 5 deletions src/core/import/qImport.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,24 @@
* found in the LICENSE file at https://github.com/BuilderIO/qwik/blob/main/LICENSE
*/

import { TEST_CONFIG } from '../util/test_config';
import { qExport, qImport, qParams } from '../import/qImport';
import { ElementFixture, isPromise } from '@builder.io/qwik/testing';
import { getBaseUri } from '../util/base_uri';

describe('qImport', () => {
it('should import default symbol', async () => {
const fixture = new ElementFixture(TEST_CONFIG);
const valuePromise = qImport(fixture.host, 'import:qImport_default_unit');
const fixture = new ElementFixture();
fixture.host.setAttribute('q:base', 'file://' + getBaseUri());
const valuePromise = qImport(fixture.host, './qImport_default_unit');
expect(isPromise(valuePromise)).toBe(true);
expect(await valuePromise).toEqual('DEFAULT_VALUE');
// second read is direct.
expect(qImport(fixture.host, 'import:qImport_default_unit')).toEqual('DEFAULT_VALUE');
expect(qImport(fixture.host, './qImport_default_unit')).toEqual('DEFAULT_VALUE');
});

it('should import symbol from extension', async () => {
const fixture = new ElementFixture(TEST_CONFIG);
const fixture = new ElementFixture();
fixture.host.setAttribute('q:base', 'file://' + getBaseUri());
const valuePromise = qImport(fixture.host, '../import/qImport_symbol_unit#mySymbol');
expect(isPromise(valuePromise)).toBe(true);
expect(await valuePromise).toEqual('MY_SYMBOL_VALUE');
Expand Down
Loading