azoxjs 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/core/build.js +30 -2
- package/core/compiler/compileToJs.js +39 -6
- package/core/compiler/index.d.ts +41 -0
- package/core/compiler/parser.js +38 -6
- package/core/compiler/resolveComponents.js +67 -7
- package/core/compiler/scopeStyles.js +151 -0
- package/core/index.d.ts +41 -0
- package/core/reactivity/signal.d.ts +88 -0
- package/core/renderer/serverScope.js +102 -4
- package/core/router/navigate.js +25 -2
- package/package.json +17 -7
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ third-party CLI dependencies, no borrowed syntax from React, Vue, or
|
|
|
11
11
|
Next.js. It compiles `.azox` components directly into fine-grained,
|
|
12
12
|
signal-driven DOM updates.
|
|
13
13
|
|
|
14
|
-
> Status: stable (v1.0
|
|
14
|
+
> Status: stable (v1.2.0). The template syntax, the reactivity exports
|
|
15
15
|
> and the shape of the build output will not change without a 2.0. What
|
|
16
16
|
> Azox does **not** do is listed in
|
|
17
17
|
> [Limitations](https://azox.dev/docs/limitations) — those are stopping
|
package/core/build.js
CHANGED
|
@@ -132,7 +132,18 @@ export function buildRoute(projectDir, route, { transformHtml } = {}) {
|
|
|
132
132
|
// A component's <head> block is merged in behind the page's own, so
|
|
133
133
|
// a layout can carry the stylesheet and fonts every page needs while
|
|
134
134
|
// the page keeps the last word on its title and description.
|
|
135
|
-
|
|
135
|
+
// A component's scoped CSS is plain stylesheet text by this point,
|
|
136
|
+
// with every selector already rewritten to require its scope
|
|
137
|
+
// attribute. One <style> carries all of them, so a page makes no
|
|
138
|
+
// extra request for styles that are only a few lines each.
|
|
139
|
+
// A page's own <style> is global: it has no caller to be scoped
|
|
140
|
+
// against, and `body { … }` in a page should mean what it says.
|
|
141
|
+
// Without this it was extracted by the parser and then silently
|
|
142
|
+
// dropped, which is worse than the error it used to raise.
|
|
143
|
+
const scoped = [ast.style, ...(ast.componentStyles ?? [])].filter(Boolean);
|
|
144
|
+
const styleTag = scoped.length ? `<style>\n${scoped.join('\n')}\n</style>` : '';
|
|
145
|
+
|
|
146
|
+
const head = mergeHeads(ast.head, [...(ast.componentHeads ?? []), styleTag].filter(Boolean));
|
|
136
147
|
|
|
137
148
|
let document = wrapDocument(html, projectTitle(projectDir), head, {
|
|
138
149
|
routerSrc: router ? `${route.assetPrefix}${ROUTER_FILENAME}` : null,
|
|
@@ -337,6 +348,17 @@ function removeStaleOutput(projectDir, results, assets = []) {
|
|
|
337
348
|
// Everything in public/ is copied to the build root untouched, so a
|
|
338
349
|
// stylesheet, font or image is referenced by the same path in source
|
|
339
350
|
// and in the built site: public/style.css -> /style.css.
|
|
351
|
+
// Files an editor or OS leaves behind, which should not be published.
|
|
352
|
+
//
|
|
353
|
+
// Every dotfile used to be skipped, which also dropped the ones a static
|
|
354
|
+
// host needs: .nojekyll tells GitHub Pages not to run Jekyll over the
|
|
355
|
+
// output, and .well-known/ is how a domain or certificate is verified.
|
|
356
|
+
const EDITOR_JUNK = new Set(['.DS_Store', 'Thumbs.db', '.git', 'node_modules']);
|
|
357
|
+
|
|
358
|
+
function isEditorJunk(name) {
|
|
359
|
+
return EDITOR_JUNK.has(name);
|
|
360
|
+
}
|
|
361
|
+
|
|
340
362
|
export function copyPublicAssets(projectDir) {
|
|
341
363
|
const publicDir = resolve(projectDir, PUBLIC_DIR);
|
|
342
364
|
if (!existsSync(publicDir)) return [];
|
|
@@ -346,7 +368,7 @@ export function copyPublicAssets(projectDir) {
|
|
|
346
368
|
|
|
347
369
|
const walk = (dir, relativeDir) => {
|
|
348
370
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
349
|
-
if (entry.name
|
|
371
|
+
if (isEditorJunk(entry.name)) continue;
|
|
350
372
|
|
|
351
373
|
const from = join(dir, entry.name);
|
|
352
374
|
const to = join(buildRoot, relativeDir, entry.name);
|
|
@@ -589,6 +611,12 @@ function uniqueTagName(line) {
|
|
|
589
611
|
const meta = line.match(/^<meta\s[^>]*name=["']([^"']+)["']/i);
|
|
590
612
|
if (meta) return `meta:${meta[1].toLowerCase()}`;
|
|
591
613
|
|
|
614
|
+
// Open Graph and Twitter cards key on `property`, not `name`. Without
|
|
615
|
+
// this a shared default and a page's own tag both shipped, and a
|
|
616
|
+
// scraper takes whichever it sees first.
|
|
617
|
+
const property = line.match(/^<meta\s[^>]*property=["']([^"']+)["']/i);
|
|
618
|
+
if (property) return `meta:${property[1].toLowerCase()}`;
|
|
619
|
+
|
|
592
620
|
return null;
|
|
593
621
|
}
|
|
594
622
|
|
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
let uid = 0;
|
|
11
11
|
const nextId = () => `_el${uid++}`;
|
|
12
12
|
|
|
13
|
+
// Tags that open an SVG document fragment. Everything inside one is in
|
|
14
|
+
// the SVG namespace too, which is threaded down as `inSvg` — <circle>
|
|
15
|
+
// and <path> carry no hint of their own.
|
|
16
|
+
//
|
|
17
|
+
// <a> and <script> exist in both languages; they are left as HTML,
|
|
18
|
+
// which is what they almost always are on a page.
|
|
19
|
+
const SVG_TAGS = new Set(['svg']);
|
|
20
|
+
|
|
13
21
|
// Names the build resolved to constants — an inlined JSON import, say.
|
|
14
22
|
// An expression reading only these can never change, so it is emitted
|
|
15
23
|
// as text rather than wrapped in an effect. Set per compile.
|
|
@@ -375,15 +383,31 @@ function hydrateBlock() {
|
|
|
375
383
|
return `
|
|
376
384
|
// Hydrate: the SSR markup is already on the page, so clear it and
|
|
377
385
|
// mount the reactive version in its place.
|
|
386
|
+
//
|
|
387
|
+
// Everything inside the root is replaced, so a listener another script
|
|
388
|
+
// attached to server-rendered markup is discarded with it — a menu
|
|
389
|
+
// button that looked right and did nothing. The event says the DOM has
|
|
390
|
+
// been rebuilt, and is the same one the router dispatches after a
|
|
391
|
+
// navigation, so one listener covers both.
|
|
378
392
|
if (typeof document !== 'undefined') {
|
|
379
393
|
const mount = document.querySelector('[data-azox-root]') ?? document.body;
|
|
380
394
|
mount.innerHTML = '';
|
|
381
395
|
render(mount);
|
|
396
|
+
// Guarded: the emitted module is also run against minimal DOM stubs —
|
|
397
|
+
// in tests, and anywhere rendering happens outside a browser — where
|
|
398
|
+
// CustomEvent and dispatchEvent need not exist.
|
|
399
|
+
if (typeof CustomEvent === 'function' && typeof document.dispatchEvent === 'function') {
|
|
400
|
+
document.dispatchEvent(
|
|
401
|
+
new CustomEvent('azox:navigate', {
|
|
402
|
+
detail: { url: typeof location !== 'undefined' ? location.href : null, from: null, hydrated: true },
|
|
403
|
+
})
|
|
404
|
+
);
|
|
405
|
+
}
|
|
382
406
|
}
|
|
383
407
|
`;
|
|
384
408
|
}
|
|
385
409
|
|
|
386
|
-
function emitNode(node, statements, fallbackVar) {
|
|
410
|
+
function emitNode(node, statements, fallbackVar, inSvg = false) {
|
|
387
411
|
if (!node) return 'null';
|
|
388
412
|
|
|
389
413
|
if (node.type === 'text') {
|
|
@@ -395,7 +419,7 @@ function emitNode(node, statements, fallbackVar) {
|
|
|
395
419
|
if (node.type === 'fragment') {
|
|
396
420
|
const varName = nextId();
|
|
397
421
|
statements.push(`const ${varName} = document.createDocumentFragment();`);
|
|
398
|
-
appendChildren(varName, node.children, statements, fallbackVar);
|
|
422
|
+
appendChildren(varName, node.children, statements, fallbackVar, inSvg);
|
|
399
423
|
return varName;
|
|
400
424
|
}
|
|
401
425
|
|
|
@@ -404,13 +428,22 @@ function emitNode(node, statements, fallbackVar) {
|
|
|
404
428
|
if (node.type === 'scope') return emitScope(node, statements, fallbackVar);
|
|
405
429
|
|
|
406
430
|
const varName = nextId();
|
|
407
|
-
|
|
431
|
+
|
|
432
|
+
// An SVG element needs its namespace. createElement always makes an
|
|
433
|
+
// HTML element, so an inline <svg> compiled to something the browser
|
|
434
|
+
// laid out as an unknown HTML tag: present in the DOM, 0×0 on screen.
|
|
435
|
+
const svg = inSvg || SVG_TAGS.has(node.name);
|
|
436
|
+
statements.push(
|
|
437
|
+
svg
|
|
438
|
+
? `const ${varName} = document.createElementNS("http://www.w3.org/2000/svg", ${JSON.stringify(node.name)});`
|
|
439
|
+
: `const ${varName} = document.createElement(${JSON.stringify(node.name)});`
|
|
440
|
+
);
|
|
408
441
|
|
|
409
442
|
for (const [key, attr] of Object.entries(node.attrs)) {
|
|
410
443
|
emitAttr(varName, key, attr, statements, node.name);
|
|
411
444
|
}
|
|
412
445
|
|
|
413
|
-
appendChildren(varName, node.children, statements, fallbackVar);
|
|
446
|
+
appendChildren(varName, node.children, statements, fallbackVar, svg);
|
|
414
447
|
|
|
415
448
|
return varName;
|
|
416
449
|
}
|
|
@@ -669,9 +702,9 @@ function emitIf(node, statements) {
|
|
|
669
702
|
);
|
|
670
703
|
}
|
|
671
704
|
|
|
672
|
-
function appendChildren(parentVar, children, statements, fallbackVar) {
|
|
705
|
+
function appendChildren(parentVar, children, statements, fallbackVar, inSvg = false) {
|
|
673
706
|
for (const child of children) {
|
|
674
|
-
const childVar = emitNode(child, statements, fallbackVar);
|
|
707
|
+
const childVar = emitNode(child, statements, fallbackVar, inSvg);
|
|
675
708
|
if (childVar !== 'null') statements.push(`${parentVar}.appendChild(${childVar});`);
|
|
676
709
|
}
|
|
677
710
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Types for `azoxjs/compiler`.
|
|
2
|
+
//
|
|
3
|
+
// The compiler is free of Node built-ins on purpose, so these describe
|
|
4
|
+
// an API that runs in a browser as well as in the build.
|
|
5
|
+
|
|
6
|
+
/** A parsed `.azox` file. The node shapes are internal and may change. */
|
|
7
|
+
export interface AzoxAst {
|
|
8
|
+
/** The `<script>` block, with its imports intact. */
|
|
9
|
+
script: string;
|
|
10
|
+
/** The `<head>` block, verbatim. */
|
|
11
|
+
head: string;
|
|
12
|
+
/** The markup, as a tree the compiler and renderer both walk. */
|
|
13
|
+
markup: unknown;
|
|
14
|
+
/** Component name → the specifier it was imported from. */
|
|
15
|
+
components: Record<string, string>;
|
|
16
|
+
/** Prop names the file declared with `props()`. */
|
|
17
|
+
props: string[];
|
|
18
|
+
/** Route parameter names the file declared with `params()`. */
|
|
19
|
+
params: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CompileOptions {
|
|
23
|
+
/** How the emitted module should import the runtime. */
|
|
24
|
+
runtimeSpecifier: string;
|
|
25
|
+
/**
|
|
26
|
+
* Rebases the author's relative imports, since compiled output does
|
|
27
|
+
* not sit beside the source. Called with the file an import was
|
|
28
|
+
* written in, which differs from the page for a hoisted one.
|
|
29
|
+
*/
|
|
30
|
+
rewriteImports?: (script: string, sourcePath?: string) => string;
|
|
31
|
+
/** Values the build resolved, inlined instead of imported. */
|
|
32
|
+
inlineModules?: Record<string, unknown>;
|
|
33
|
+
/** Resolved parameters for one page of a dynamic route. */
|
|
34
|
+
routeParams?: Record<string, string> | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Parses a `.azox` file. Throws on malformed markup, naming the tag. */
|
|
38
|
+
export declare function parseAzox(source: string): AzoxAst;
|
|
39
|
+
|
|
40
|
+
/** Compiles a parsed file into a JavaScript module, as a string. */
|
|
41
|
+
export declare function compileToModule(ast: AzoxAst, options: CompileOptions): string;
|
package/core/compiler/parser.js
CHANGED
|
@@ -16,19 +16,40 @@ export class ParseError extends BuildError {}
|
|
|
16
16
|
const isComponentName = (name) => /^[A-Z]/.test(name);
|
|
17
17
|
|
|
18
18
|
export function parseAzox(source) {
|
|
19
|
-
|
|
19
|
+
// <script>, <head> and <style> are pulled out of the source before
|
|
20
|
+
// anything else looks at it. A literal <text> block may contain those
|
|
21
|
+
// very tags — documenting them requires it — so the spans are blanked
|
|
22
|
+
// first, and only what is left is searched. Without this, a code
|
|
23
|
+
// sample showing a <style> block was eaten as the page's own CSS.
|
|
24
|
+
const searchable = blankTextBlocks(source);
|
|
25
|
+
|
|
26
|
+
const scriptMatch = searchable.match(/<script>([\s\S]*?)<\/script>/);
|
|
20
27
|
const script = scriptMatch ? scriptMatch[1].trim() : '';
|
|
21
28
|
|
|
22
29
|
// An optional <head> block is copied into the document head
|
|
23
30
|
// verbatim: stylesheets, meta tags, fonts. It is markup for the
|
|
24
31
|
// document, not for the page body, so it skips the AST entirely.
|
|
25
|
-
const headMatch =
|
|
32
|
+
const headMatch = searchable.match(/<head>([\s\S]*?)<\/head>/);
|
|
26
33
|
const head = headMatch ? headMatch[1].trim() : '';
|
|
27
34
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
// A <style> block is CSS, not markup. It has to come out before the
|
|
36
|
+
// tokeniser runs: left in, its braces are read as {expressions} and a
|
|
37
|
+
// rule like `.a { color: red }` failed the build with "Unexpected
|
|
38
|
+
// token ':'" — an error about JavaScript, pointing at a stylesheet.
|
|
39
|
+
const styleMatch = searchable.match(/<style>([\s\S]*?)<\/style>/);
|
|
40
|
+
const style = styleMatch ? styleMatch[1].trim() : '';
|
|
41
|
+
|
|
42
|
+
// Cut by position rather than by pattern: replacing on the source
|
|
43
|
+
// would remove the first match anywhere, including one inside a
|
|
44
|
+
// <text> block that was deliberately ignored above.
|
|
45
|
+
const cuts = [scriptMatch, headMatch, styleMatch]
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.map((match) => [match.index, match.index + match[0].length])
|
|
48
|
+
.sort((a, b) => b[0] - a[0]);
|
|
49
|
+
|
|
50
|
+
let template = source;
|
|
51
|
+
for (const [from, to] of cuts) template = template.slice(0, from) + template.slice(to);
|
|
52
|
+
template = template.trim();
|
|
32
53
|
|
|
33
54
|
const tokens = tokenize(template);
|
|
34
55
|
|
|
@@ -55,6 +76,7 @@ export function parseAzox(source) {
|
|
|
55
76
|
return {
|
|
56
77
|
script,
|
|
57
78
|
head,
|
|
79
|
+
style,
|
|
58
80
|
markup,
|
|
59
81
|
components: parseComponentImports(script),
|
|
60
82
|
props: parsePropNames(script),
|
|
@@ -64,6 +86,16 @@ export function parseAzox(source) {
|
|
|
64
86
|
};
|
|
65
87
|
}
|
|
66
88
|
|
|
89
|
+
// Replaces the contents of every <text> block with spaces, keeping the
|
|
90
|
+
// length and therefore every offset in the original. What a <text>
|
|
91
|
+
// block holds is literal by definition, so nothing inside one should be
|
|
92
|
+
// mistaken for the page's script, head or style.
|
|
93
|
+
function blankTextBlocks(source) {
|
|
94
|
+
return source.replace(/<text>[\s\S]*?<\/text>/g, (block) =>
|
|
95
|
+
`<text>${' '.repeat(Math.max(0, block.length - '<text></text>'.length))}</text>`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
67
99
|
// Component imports are written as ordinary import statements, so an
|
|
68
100
|
// editor treats them like any other module reference:
|
|
69
101
|
// import Card from '../components/Card.azox';
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// been written by hand.
|
|
8
8
|
|
|
9
9
|
import { parseAzox } from './parser.js';
|
|
10
|
+
import { scopeId, scopeAttribute, scopeCss } from './scopeStyles.js';
|
|
10
11
|
import { BuildError } from '../buildError.js';
|
|
11
12
|
|
|
12
13
|
// Extends BuildError so the CLI reports it as a user-facing problem
|
|
@@ -20,7 +21,14 @@ export class ComponentError extends BuildError {}
|
|
|
20
21
|
// `resolver` decides how an import specifier becomes source text, so
|
|
21
22
|
// this runs unchanged against disk or against an in-memory map. See
|
|
22
23
|
// sourceResolver.js.
|
|
23
|
-
export function resolveComponents(
|
|
24
|
+
export function resolveComponents(
|
|
25
|
+
ast,
|
|
26
|
+
sourcePath,
|
|
27
|
+
resolver,
|
|
28
|
+
seen = new Set(),
|
|
29
|
+
heads = null,
|
|
30
|
+
styles = null
|
|
31
|
+
) {
|
|
24
32
|
// Imports from stateful components are hoisted here: they cannot
|
|
25
33
|
// live inside the scope function the compiler builds for each one.
|
|
26
34
|
//
|
|
@@ -37,17 +45,31 @@ export function resolveComponents(ast, sourcePath, resolver, seen = new Set(), h
|
|
|
37
45
|
// now only a page could put one there.
|
|
38
46
|
const collected = heads ?? new Map();
|
|
39
47
|
|
|
40
|
-
|
|
48
|
+
// A component's <style> block is collected the same way and keyed by
|
|
49
|
+
// the same path, so a component used twice contributes its CSS once.
|
|
50
|
+
const collectedStyles = styles ?? new Map();
|
|
51
|
+
|
|
52
|
+
const markup = expand(
|
|
53
|
+
ast.markup,
|
|
54
|
+
ast,
|
|
55
|
+
sourcePath,
|
|
56
|
+
resolver,
|
|
57
|
+
seen,
|
|
58
|
+
hoisted,
|
|
59
|
+
collected,
|
|
60
|
+
collectedStyles
|
|
61
|
+
);
|
|
41
62
|
|
|
42
63
|
return {
|
|
43
64
|
...ast,
|
|
44
65
|
markup,
|
|
45
66
|
componentImports: [...hoisted.values()],
|
|
46
67
|
componentHeads: [...collected.values()],
|
|
68
|
+
componentStyles: [...collectedStyles.values()],
|
|
47
69
|
};
|
|
48
70
|
}
|
|
49
71
|
|
|
50
|
-
function expand(node, ast, sourcePath, resolver, seen, hoisted, heads) {
|
|
72
|
+
function expand(node, ast, sourcePath, resolver, seen, hoisted, heads, styles) {
|
|
51
73
|
if (!node || node.type === 'text') return node;
|
|
52
74
|
|
|
53
75
|
// <if> keeps its children in two branches rather than in `children`,
|
|
@@ -57,16 +79,16 @@ function expand(node, ast, sourcePath, resolver, seen, hoisted, heads) {
|
|
|
57
79
|
return {
|
|
58
80
|
...node,
|
|
59
81
|
then: node.then.map((child) =>
|
|
60
|
-
expand(child, ast, sourcePath, resolver, seen, hoisted, heads)
|
|
82
|
+
expand(child, ast, sourcePath, resolver, seen, hoisted, heads, styles)
|
|
61
83
|
),
|
|
62
84
|
otherwise: node.otherwise.map((child) =>
|
|
63
|
-
expand(child, ast, sourcePath, resolver, seen, hoisted, heads)
|
|
85
|
+
expand(child, ast, sourcePath, resolver, seen, hoisted, heads, styles)
|
|
64
86
|
),
|
|
65
87
|
};
|
|
66
88
|
}
|
|
67
89
|
|
|
68
90
|
const children = (node.children ?? []).map((child) =>
|
|
69
|
-
expand(child, ast, sourcePath, resolver, seen, hoisted, heads)
|
|
91
|
+
expand(child, ast, sourcePath, resolver, seen, hoisted, heads, styles)
|
|
70
92
|
);
|
|
71
93
|
|
|
72
94
|
if (node.type !== 'component') {
|
|
@@ -83,13 +105,27 @@ function expand(node, ast, sourcePath, resolver, seen, hoisted, heads) {
|
|
|
83
105
|
component.path,
|
|
84
106
|
resolver,
|
|
85
107
|
new Set([...seen, component.path]),
|
|
86
|
-
heads
|
|
108
|
+
heads,
|
|
109
|
+
styles
|
|
87
110
|
);
|
|
88
111
|
|
|
89
112
|
// Keyed by path: a component used on a page twice must not emit its
|
|
90
113
|
// stylesheet link twice.
|
|
91
114
|
if (component.ast.head) heads.set(component.path, component.ast.head);
|
|
92
115
|
|
|
116
|
+
// A <style> block scopes to this component: its selectors are
|
|
117
|
+
// rewritten to require an attribute, and that attribute is put on the
|
|
118
|
+
// markup below. Both halves key off the same path, so the CSS and the
|
|
119
|
+
// elements it targets always agree.
|
|
120
|
+
let scope = null;
|
|
121
|
+
|
|
122
|
+
if (component.ast.style) {
|
|
123
|
+
scope = scopeId(component.path);
|
|
124
|
+
styles.set(component.path, scopeCss(component.ast.style, scope));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (scope) markScope(inner.markup, scopeAttribute(scope));
|
|
128
|
+
|
|
93
129
|
const values = propValues(node, component.ast.props);
|
|
94
130
|
const { logic, imports } = componentLogic(component.ast.script);
|
|
95
131
|
|
|
@@ -160,6 +196,30 @@ function componentLogic(script) {
|
|
|
160
196
|
return { logic: meaningful.trim() ? body.trim() : null, imports };
|
|
161
197
|
}
|
|
162
198
|
|
|
199
|
+
// Puts the scope attribute on every element the component owns.
|
|
200
|
+
//
|
|
201
|
+
// Slot content is skipped: it was written by whoever used the tag, so
|
|
202
|
+
// it belongs to the caller's scope, and a component must not restyle
|
|
203
|
+
// markup it did not write.
|
|
204
|
+
function markScope(node, attribute) {
|
|
205
|
+
if (!node || node.type === 'text') return;
|
|
206
|
+
|
|
207
|
+
if (node.type === 'fragment' && node.slot) return;
|
|
208
|
+
|
|
209
|
+
if (node.type === 'element' || node.type === 'component') {
|
|
210
|
+
node.attrs = { ...node.attrs, [attribute]: { kind: 'static', value: '' } };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (node.type === 'if') {
|
|
214
|
+
for (const child of [...(node.then ?? []), ...(node.otherwise ?? [])]) {
|
|
215
|
+
markScope(child, attribute);
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const child of node.children ?? []) markScope(child, attribute);
|
|
221
|
+
}
|
|
222
|
+
|
|
163
223
|
function loadComponent(name, ast, sourcePath, resolver, seen) {
|
|
164
224
|
const specifier = ast.components[name];
|
|
165
225
|
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Scoped styles: a component's CSS applies to that component's markup
|
|
2
|
+
// and nothing else.
|
|
3
|
+
//
|
|
4
|
+
// The mechanism is the one most frameworks settled on, because it needs
|
|
5
|
+
// no runtime: every element the component owns gets an attribute, and
|
|
6
|
+
// every selector in its <style> block is rewritten to require that
|
|
7
|
+
// attribute. The result is plain CSS a browser can parse — no shadow
|
|
8
|
+
// DOM, no class-name mangling, nothing to ship.
|
|
9
|
+
//
|
|
10
|
+
// This file has no Node built-ins on purpose: it runs in the browser
|
|
11
|
+
// playground like the rest of the compiler.
|
|
12
|
+
|
|
13
|
+
// A short, stable id from the component's path. Not cryptographic — it
|
|
14
|
+
// only has to be stable across builds and unlikely to collide between
|
|
15
|
+
// the files of one project.
|
|
16
|
+
export function scopeId(path) {
|
|
17
|
+
let hash = 0;
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < path.length; i++) {
|
|
20
|
+
hash = (hash * 31 + path.charCodeAt(i)) | 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return `a${(hash >>> 0).toString(36).slice(0, 6)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const scopeAttribute = (id) => `data-azox-${id}`;
|
|
27
|
+
|
|
28
|
+
// At-rules whose body is more CSS, so the selectors inside them need
|
|
29
|
+
// rewriting too — a rule in a @media block still belongs to the
|
|
30
|
+
// component.
|
|
31
|
+
const NESTED_AT_RULES = /^@(media|supports|container|layer)\b/;
|
|
32
|
+
|
|
33
|
+
// At-rules that contain no selectors. @keyframes names its steps `from`
|
|
34
|
+
// and `50%`, and scoping those would break the animation.
|
|
35
|
+
const OPAQUE_AT_RULES =
|
|
36
|
+
/^@(keyframes|-\w+-keyframes|font-face|import|charset|namespace|property)\b/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Rewrites every selector in `css` to require the scope attribute.
|
|
40
|
+
*
|
|
41
|
+
* .title { … } → .title[data-azox-a1b2c3] { … }
|
|
42
|
+
* .a, .b { … } → .a[data-azox-a1b2c3], .b[data-azox-a1b2c3] { … }
|
|
43
|
+
* :global(.x) { … } → .x { … }
|
|
44
|
+
*/
|
|
45
|
+
export function scopeCss(css, id) {
|
|
46
|
+
return rewriteBlock(css, `[${scopeAttribute(id)}]`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function rewriteBlock(css, attr) {
|
|
50
|
+
let out = '';
|
|
51
|
+
let i = 0;
|
|
52
|
+
|
|
53
|
+
while (i < css.length) {
|
|
54
|
+
const open = css.indexOf('{', i);
|
|
55
|
+
|
|
56
|
+
// Trailing text with no rule after it: a stray comment, or nothing.
|
|
57
|
+
if (open === -1) {
|
|
58
|
+
out += css.slice(i);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const prelude = css.slice(i, open);
|
|
63
|
+
const close = matchingBrace(css, open);
|
|
64
|
+
const body = css.slice(open + 1, close);
|
|
65
|
+
const trimmed = prelude.trim();
|
|
66
|
+
|
|
67
|
+
if (OPAQUE_AT_RULES.test(trimmed)) {
|
|
68
|
+
// Kept whole: its body is not selectors.
|
|
69
|
+
out += `${prelude}{${body}}`;
|
|
70
|
+
} else if (NESTED_AT_RULES.test(trimmed)) {
|
|
71
|
+
// The condition stays; what is inside it is scoped.
|
|
72
|
+
out += `${prelude}{${rewriteBlock(body, attr)}}`;
|
|
73
|
+
} else {
|
|
74
|
+
out += `${scopeSelectorList(prelude, attr)}{${body}}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
i = close + 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Finds the brace closing the one at `open`, so a @media body is taken
|
|
84
|
+
// whole rather than cut at its first inner rule.
|
|
85
|
+
function matchingBrace(css, open) {
|
|
86
|
+
let depth = 0;
|
|
87
|
+
|
|
88
|
+
for (let i = open; i < css.length; i++) {
|
|
89
|
+
if (css[i] === '{') depth++;
|
|
90
|
+
else if (css[i] === '}' && --depth === 0) return i;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return css.length;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// A placeholder for a comment while the selector list is split on
|
|
97
|
+
// commas. Printable on purpose: a control character would be invisible
|
|
98
|
+
// in any output that quotes this source.
|
|
99
|
+
const COMMENT_MARK = (n) => `/*__azox${n}__*/`;
|
|
100
|
+
|
|
101
|
+
function scopeSelectorList(prelude, attr) {
|
|
102
|
+
// Comments carry no selectors and may contain commas.
|
|
103
|
+
const comments = [];
|
|
104
|
+
const stripped = prelude.replace(/\/\*[\s\S]*?\*\//g, (comment) => {
|
|
105
|
+
comments.push(comment);
|
|
106
|
+
return COMMENT_MARK(comments.length - 1);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const scoped = stripped
|
|
110
|
+
.split(',')
|
|
111
|
+
.map((selector) => scopeSelector(selector, attr))
|
|
112
|
+
.join(',');
|
|
113
|
+
|
|
114
|
+
return scoped.replace(/\/\*__azox(\d+)__\*\//g, (_, n) => comments[Number(n)]);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function scopeSelector(selector, attr) {
|
|
118
|
+
const trimmed = selector.trim();
|
|
119
|
+
if (!trimmed) return selector;
|
|
120
|
+
|
|
121
|
+
// An escape hatch for the times a component must reach outside
|
|
122
|
+
// itself — a body class, or markup it renders into a portal.
|
|
123
|
+
const global = trimmed.match(/^:global\(([\s\S]*)\)$/);
|
|
124
|
+
if (global) return selector.replace(trimmed, global[1].trim());
|
|
125
|
+
|
|
126
|
+
const leading = selector.slice(0, selector.length - selector.trimStart().length);
|
|
127
|
+
const trailing = selector.slice(selector.trimEnd().length);
|
|
128
|
+
|
|
129
|
+
// The attribute goes on the last compound of the selector, so
|
|
130
|
+
// `.card .title` scopes the element actually being styled rather than
|
|
131
|
+
// its ancestor. Combinators are left where they are.
|
|
132
|
+
const parts = trimmed.split(/(\s+|\s*[>+~]\s*)/);
|
|
133
|
+
|
|
134
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
135
|
+
if (!parts[i].trim() || /^[>+~\s]+$/.test(parts[i])) continue;
|
|
136
|
+
parts[i] = attachAttribute(parts[i], attr);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return leading + parts.join('') + trailing;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Placed before any pseudo-element or pseudo-class: `.a[attr]:hover` is
|
|
144
|
+
// what was meant, and `.a:hover[attr]` would not match a browser's
|
|
145
|
+
// reading of it.
|
|
146
|
+
function attachAttribute(compound, attr) {
|
|
147
|
+
const pseudo = compound.search(/::?[a-z-]/i);
|
|
148
|
+
if (pseudo === -1) return compound + attr;
|
|
149
|
+
|
|
150
|
+
return compound.slice(0, pseudo) + attr + compound.slice(pseudo);
|
|
151
|
+
}
|
package/core/index.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Types for `azoxjs`.
|
|
2
|
+
//
|
|
3
|
+
// The whole reactivity surface, plus the compiler and renderer, so this
|
|
4
|
+
// entry point and `azoxjs/reactivity` offer the same thing.
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
signal,
|
|
8
|
+
effect,
|
|
9
|
+
computed,
|
|
10
|
+
dispose,
|
|
11
|
+
untracked,
|
|
12
|
+
onMount,
|
|
13
|
+
onCleanup,
|
|
14
|
+
type Signal,
|
|
15
|
+
type ReadonlySignal,
|
|
16
|
+
type EffectHandle,
|
|
17
|
+
} from './reactivity/signal.js';
|
|
18
|
+
|
|
19
|
+
export { parseAzox, compileToModule } from './compiler/index.js';
|
|
20
|
+
|
|
21
|
+
// Imported as well as re-exported: a type-only re-export names the type
|
|
22
|
+
// for consumers but does not put it in scope here, and renderToHtml
|
|
23
|
+
// below refers to it.
|
|
24
|
+
import type { AzoxAst } from './compiler/index.js';
|
|
25
|
+
export type { AzoxAst, CompileOptions } from './compiler/index.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Renders a parsed file to HTML, evaluating its expressions once.
|
|
29
|
+
*
|
|
30
|
+
* `scope` holds the bindings the markup reads. `modules` carries what
|
|
31
|
+
* the file's imports brought in, which the build resolves.
|
|
32
|
+
*/
|
|
33
|
+
export declare function renderToHtml(
|
|
34
|
+
ast: AzoxAst,
|
|
35
|
+
scope: Record<string, unknown>,
|
|
36
|
+
modules?: Record<string, unknown>
|
|
37
|
+
): string;
|
|
38
|
+
|
|
39
|
+
/** The version this copy of Azox reports. */
|
|
40
|
+
export declare const VERSION: string;
|
|
41
|
+
export declare const TAGLINE: string;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Types for `azoxjs/reactivity`.
|
|
2
|
+
//
|
|
3
|
+
// Written by hand rather than generated: the runtime carries no JSDoc,
|
|
4
|
+
// so `tsc --declaration` produces `signal(initialValue: any)`, which
|
|
5
|
+
// type-checks nothing. These describe what the implementation actually
|
|
6
|
+
// guarantees.
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A reactive value. Call it to read — and, inside an `effect`, to
|
|
10
|
+
* subscribe to it.
|
|
11
|
+
*/
|
|
12
|
+
export interface Signal<T> {
|
|
13
|
+
(): T;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Replaces the value, or derives it from the current one.
|
|
17
|
+
*
|
|
18
|
+
* Setting a value equal to the current one (`===`) notifies nobody.
|
|
19
|
+
*
|
|
20
|
+
* A function is treated as an updater, so a signal *holding* a
|
|
21
|
+
* function cannot be replaced by passing it directly — pass
|
|
22
|
+
* `() => theFunction` instead.
|
|
23
|
+
*/
|
|
24
|
+
set(next: T | ((previous: T) => T)): void;
|
|
25
|
+
|
|
26
|
+
/** Reads without subscribing, even inside an effect. */
|
|
27
|
+
peek(): T;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A read-only reactive value. `computed` returns one of these. */
|
|
31
|
+
export interface ReadonlySignal<T> {
|
|
32
|
+
(): T;
|
|
33
|
+
peek(): T;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The handle `effect` returns. Its only public use is `dispose`; its
|
|
38
|
+
* internals are not part of the API and may change.
|
|
39
|
+
*/
|
|
40
|
+
export interface EffectHandle {
|
|
41
|
+
(): void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Creates a reactive value. */
|
|
45
|
+
export declare function signal<T>(initialValue: T): Signal<T>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Runs `fn` now, and again whenever a signal it read changes.
|
|
49
|
+
*
|
|
50
|
+
* Effects created while another is running belong to it, and are
|
|
51
|
+
* disposed with it. Use `untracked` to opt out of that ownership.
|
|
52
|
+
*/
|
|
53
|
+
export declare function effect(fn: () => void): EffectHandle;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A value derived from other signals, recomputed when they change.
|
|
57
|
+
*
|
|
58
|
+
* Read-only by design. The runtime object does carry `set`, but the
|
|
59
|
+
* deriving effect overwrites whatever you assign on its next run, so
|
|
60
|
+
* it is not offered here.
|
|
61
|
+
*/
|
|
62
|
+
export declare function computed<T>(fn: () => T): ReadonlySignal<T>;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Runs `fn` with no effect considered active, so anything it creates
|
|
66
|
+
* is owned by nobody and survives the caller re-running.
|
|
67
|
+
*/
|
|
68
|
+
export declare function untracked<T>(fn: () => T): T;
|
|
69
|
+
|
|
70
|
+
/** Stops an effect and everything it created. Ignores null. */
|
|
71
|
+
export declare function dispose(handle: EffectHandle | null | undefined): void;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Runs once the DOM this scope built is in the document.
|
|
75
|
+
*
|
|
76
|
+
* A function returned from `fn` becomes the scope's cleanup, so setup
|
|
77
|
+
* and teardown can stay together.
|
|
78
|
+
*/
|
|
79
|
+
export declare function onMount(fn: () => void | (() => void)): void;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Registers work to undo when the surrounding scope goes away: a row
|
|
83
|
+
* leaving a keyed list, or a branch of an `<if>` no longer taken.
|
|
84
|
+
*
|
|
85
|
+
* Called outside any scope it does nothing, which is the case at the
|
|
86
|
+
* top level of a page — nothing ever removes it.
|
|
87
|
+
*/
|
|
88
|
+
export declare function onCleanup(fn: () => void): void;
|
|
@@ -44,18 +44,25 @@ export function serverOnCleanup() {}
|
|
|
44
44
|
export function declaredNames(script) {
|
|
45
45
|
const names = new Set();
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
// Only what the script declares at its top level. A name declared
|
|
48
|
+
// inside a callback — `const timer` within onMount, say — is a local
|
|
49
|
+
// of that function, so returning it from the outer scope throws
|
|
50
|
+
// "timer is not defined" and takes the whole page down.
|
|
51
|
+
const topLevel = topLevelSource(script);
|
|
52
|
+
|
|
53
|
+
for (const match of topLevel.matchAll(/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/g)) {
|
|
48
54
|
names.add(match[1]);
|
|
49
55
|
}
|
|
50
|
-
for (const match of
|
|
56
|
+
for (const match of topLevel.matchAll(/function\s+([A-Za-z_$][\w$]*)\s*\(/g)) {
|
|
51
57
|
names.add(match[1]);
|
|
52
58
|
}
|
|
53
|
-
for (const match of
|
|
59
|
+
for (const match of topLevel.matchAll(/class\s+([A-Za-z_$][\w$]*)/g)) {
|
|
54
60
|
names.add(match[1]);
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
// Destructured declarations: const { a, b: c, d = 1 } = …
|
|
58
|
-
|
|
64
|
+
//
|
|
65
|
+
for (const match of topLevel.matchAll(/(?:const|let|var)\s*\{([^}]*)\}\s*=/g)) {
|
|
59
66
|
for (const part of match[1].split(',')) {
|
|
60
67
|
const name = part.split(':').pop().split('=')[0].trim();
|
|
61
68
|
if (/^[A-Za-z_$][\w$]*$/.test(name)) names.add(name);
|
|
@@ -65,6 +72,97 @@ export function declaredNames(script) {
|
|
|
65
72
|
return [...names];
|
|
66
73
|
}
|
|
67
74
|
|
|
75
|
+
// Blanks out everything nested inside braces, brackets or parentheses,
|
|
76
|
+
// leaving the top-level text with its offsets intact — so a declaration
|
|
77
|
+
// inside a function body is no longer visible to the patterns above.
|
|
78
|
+
//
|
|
79
|
+
// Strings, template literals and comments are blanked too: a brace in
|
|
80
|
+
// one of them would otherwise throw the depth count off.
|
|
81
|
+
function topLevelSource(script) {
|
|
82
|
+
let out = '';
|
|
83
|
+
let depth = 0;
|
|
84
|
+
let i = 0;
|
|
85
|
+
|
|
86
|
+
while (i < script.length) {
|
|
87
|
+
const c = script[i];
|
|
88
|
+
const next = script[i + 1];
|
|
89
|
+
|
|
90
|
+
// Comments.
|
|
91
|
+
if (c === '/' && next === '/') {
|
|
92
|
+
const end = script.indexOf('\n', i);
|
|
93
|
+
const stop = end === -1 ? script.length : end;
|
|
94
|
+
out += ' '.repeat(stop - i);
|
|
95
|
+
i = stop;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (c === '/' && next === '*') {
|
|
99
|
+
const end = script.indexOf('*/', i + 2);
|
|
100
|
+
const stop = end === -1 ? script.length : end + 2;
|
|
101
|
+
out += ' '.repeat(stop - i);
|
|
102
|
+
i = stop;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Strings and template literals: skipped whole, so braces inside
|
|
107
|
+
// them do not count. A template's ${...} is skipped with it, which
|
|
108
|
+
// is fine — nothing is declared at the top level in there.
|
|
109
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
110
|
+
const quote = c;
|
|
111
|
+
let j = i + 1;
|
|
112
|
+
while (j < script.length) {
|
|
113
|
+
if (script[j] === '\\') j += 2;
|
|
114
|
+
else if (script[j] === quote) break;
|
|
115
|
+
else j++;
|
|
116
|
+
}
|
|
117
|
+
const stop = Math.min(j + 1, script.length);
|
|
118
|
+
out += ' '.repeat(stop - i);
|
|
119
|
+
i = stop;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// The delimiter itself is kept when it opens at the top level, so
|
|
124
|
+
// `function f(` and `const { a } =` still match; what is nested
|
|
125
|
+
// inside is blanked.
|
|
126
|
+
//
|
|
127
|
+
// The exception is a destructuring pattern — `const { a, b } = …` —
|
|
128
|
+
// whose names are the thing being declared, so its contents are
|
|
129
|
+
// kept even though they sit one level in.
|
|
130
|
+
if (c === '{' || c === '(' || c === '[') {
|
|
131
|
+
const destructuring = c === '{' && depth === 0 && /(?:const|let|var)\s*$/.test(out);
|
|
132
|
+
|
|
133
|
+
out += depth === 0 ? c : ' ';
|
|
134
|
+
depth++;
|
|
135
|
+
|
|
136
|
+
if (destructuring) {
|
|
137
|
+
const close = script.indexOf('}', i + 1);
|
|
138
|
+
if (close !== -1) {
|
|
139
|
+
out += script.slice(i + 1, close + 1);
|
|
140
|
+
depth--;
|
|
141
|
+
i = close + 1;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
i++;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (c === '}' || c === ')' || c === ']') {
|
|
151
|
+
depth = Math.max(0, depth - 1);
|
|
152
|
+
out += depth === 0 ? c : ' ';
|
|
153
|
+
i++;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Newlines are kept at any depth so line-anchored patterns still
|
|
158
|
+
// behave; everything else nested is blanked.
|
|
159
|
+
out += depth === 0 || c === '\n' ? c : ' ';
|
|
160
|
+
i++;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
68
166
|
// Evaluates a script body and returns its declarations. `params` and
|
|
69
167
|
// `args` pass a component's props in as arguments.
|
|
70
168
|
//
|
package/core/router/navigate.js
CHANGED
|
@@ -128,6 +128,7 @@ async function go(href, { restore = false } = {}) {
|
|
|
128
128
|
// already holds wherever the reader last asked to go.
|
|
129
129
|
if (location.href !== href) return;
|
|
130
130
|
|
|
131
|
+
const previous = currentUrl;
|
|
131
132
|
const next = new DOMParser().parseFromString(html, 'text/html');
|
|
132
133
|
const incoming = next.querySelector(ROOT);
|
|
133
134
|
const target = document.querySelector(ROOT);
|
|
@@ -145,6 +146,18 @@ async function go(href, { restore = false } = {}) {
|
|
|
145
146
|
|
|
146
147
|
currentUrl = href;
|
|
147
148
|
restoreScroll(restore);
|
|
149
|
+
|
|
150
|
+
// The page's own module is re-run above, but a classic <script> that
|
|
151
|
+
// enhances the markup — syntax highlighting, heading anchors, a
|
|
152
|
+
// table of contents — is not: it ran once on first load and the
|
|
153
|
+
// nodes it worked on have just been replaced. This event is how such
|
|
154
|
+
// a script knows to run again.
|
|
155
|
+
//
|
|
156
|
+
// Dispatched after the swap and after the module, so a listener sees
|
|
157
|
+
// the finished page.
|
|
158
|
+
document.dispatchEvent(
|
|
159
|
+
new CustomEvent('azox:navigate', { detail: { url: href, from: previous } })
|
|
160
|
+
);
|
|
148
161
|
}
|
|
149
162
|
|
|
150
163
|
// Brings across anything in the new page's head that this one lacks —
|
|
@@ -199,13 +212,23 @@ async function runPageModule(next, pageHref) {
|
|
|
199
212
|
function restoreScroll(restore) {
|
|
200
213
|
if (restore) return; // The browser restores the position itself.
|
|
201
214
|
|
|
202
|
-
const
|
|
203
|
-
const target = hash && document.querySelector(hash);
|
|
215
|
+
const target = elementForHash(location.hash);
|
|
204
216
|
|
|
205
217
|
if (target) target.scrollIntoView();
|
|
206
218
|
else window.scrollTo(0, 0);
|
|
207
219
|
}
|
|
208
220
|
|
|
221
|
+
// A hash is not always a selector. A page may use it to carry state —
|
|
222
|
+
// "#template=landing" — and handing that to querySelector throws
|
|
223
|
+
// "not a valid selector", which stopped the scroll being restored at
|
|
224
|
+
// all and put an error in every visitor's console.
|
|
225
|
+
function elementForHash(hash) {
|
|
226
|
+
if (!hash || hash.length < 2) return null;
|
|
227
|
+
|
|
228
|
+
// getElementById takes an id, not a selector, so nothing can throw.
|
|
229
|
+
return document.getElementById(decodeURIComponent(hash.slice(1)));
|
|
230
|
+
}
|
|
231
|
+
|
|
209
232
|
export function currentHref() {
|
|
210
233
|
return currentUrl;
|
|
211
234
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "azoxjs",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Azox Framework — The Sound of Future Web",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,17 +9,26 @@
|
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "git+https://github.com/darilpratomo/azox.git"
|
|
11
11
|
},
|
|
12
|
-
"homepage": "https://
|
|
12
|
+
"homepage": "https://azox.dev",
|
|
13
13
|
"bugs": {
|
|
14
14
|
"url": "https://github.com/darilpratomo/azox/issues"
|
|
15
15
|
},
|
|
16
16
|
"bin": {
|
|
17
|
-
"azox": "
|
|
17
|
+
"azox": "bin/azox.js"
|
|
18
18
|
},
|
|
19
19
|
"exports": {
|
|
20
|
-
".":
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./core/index.d.ts",
|
|
22
|
+
"default": "./core/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./reactivity": {
|
|
25
|
+
"types": "./core/reactivity/signal.d.ts",
|
|
26
|
+
"default": "./core/reactivity/signal.js"
|
|
27
|
+
},
|
|
28
|
+
"./compiler": {
|
|
29
|
+
"types": "./core/compiler/index.d.ts",
|
|
30
|
+
"default": "./core/compiler/index.js"
|
|
31
|
+
},
|
|
23
32
|
"./package.json": "./package.json"
|
|
24
33
|
},
|
|
25
34
|
"files": [
|
|
@@ -43,5 +52,6 @@
|
|
|
43
52
|
"compiler",
|
|
44
53
|
"ssr",
|
|
45
54
|
"no-virtual-dom"
|
|
46
|
-
]
|
|
55
|
+
],
|
|
56
|
+
"types": "./core/index.d.ts"
|
|
47
57
|
}
|