zero-query 1.0.9 → 1.1.1
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/cli/commands/build-api.js +442 -0
- package/cli/commands/build.js +33 -2
- package/cli/commands/bundle.js +41 -0
- package/cli/commands/dev/server.js +56 -3
- package/cli/scaffold/default/app/components/contacts/contacts.css +9 -9
- package/cli/scaffold/default/app/components/playground/playground.css +1 -1
- package/cli/scaffold/default/app/components/playground/playground.html +5 -5
- package/cli/scaffold/default/app/components/playground/playground.js +1 -1
- package/cli/scaffold/default/app/components/toolkit/toolkit.css +1 -1
- package/cli/scaffold/default/app/components/toolkit/toolkit.html +3 -3
- package/cli/scaffold/default/app/components/toolkit/toolkit.js +4 -4
- package/cli/utils.js +6 -6
- package/dist/API.md +6603 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +387 -25
- package/dist/zquery.min.js +47 -17
- package/index.d.ts +9 -3
- package/index.js +10 -2
- package/package.json +2 -1
- package/src/component.js +243 -6
- package/src/reactive.js +4 -3
- package/src/router.js +79 -9
- package/src/store.js +49 -3
- package/tests/cli.test.js +80 -0
- package/tests/compare.test.js +486 -0
- package/tests/dev-server.test.js +489 -0
- package/tests/docs.test.js +1650 -0
- package/tests/electron-features.test.js +864 -0
- package/types/misc.d.ts +7 -7
- package/types/reactive.d.ts +1 -1
- package/types/store.d.ts +2 -1
package/src/router.js
CHANGED
|
@@ -47,6 +47,9 @@ class Router {
|
|
|
47
47
|
const isFile = typeof location !== 'undefined' && location.protocol === 'file:';
|
|
48
48
|
this._mode = isFile ? 'hash' : (config.mode || 'history');
|
|
49
49
|
|
|
50
|
+
// Keep-alive cache: component name → { container, instance }
|
|
51
|
+
this._keepAliveCache = new Map();
|
|
52
|
+
|
|
50
53
|
// Base path for sub-path deployments
|
|
51
54
|
// Priority: explicit config.base → window.__ZQ_BASE → <base href> tag
|
|
52
55
|
let rawBase = config.base;
|
|
@@ -604,34 +607,96 @@ class Router {
|
|
|
604
607
|
await prefetch(matched.component);
|
|
605
608
|
}
|
|
606
609
|
|
|
607
|
-
|
|
608
|
-
|
|
610
|
+
const isKeepAlive = !!matched.keepAlive;
|
|
611
|
+
const componentName = typeof matched.component === 'string' ? matched.component : null;
|
|
612
|
+
|
|
613
|
+
// Deactivate previous keep-alive instance (hide instead of destroy)
|
|
614
|
+
if (this._instance && this._currentKeepAlive && this._currentComponentName) {
|
|
615
|
+
const cached = this._keepAliveCache.get(this._currentComponentName);
|
|
616
|
+
if (cached) {
|
|
617
|
+
cached.container.style.display = 'none';
|
|
618
|
+
// Call deactivated() lifecycle hook
|
|
619
|
+
if (cached.instance._def.deactivated) {
|
|
620
|
+
try { cached.instance._def.deactivated.call(cached.instance); }
|
|
621
|
+
catch (err) { reportError(ErrorCode.COMP_LIFECYCLE, `Component "${this._currentComponentName}" deactivated() threw`, { component: this._currentComponentName }, err); }
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
this._instance = null;
|
|
625
|
+
} else if (this._instance) {
|
|
626
|
+
// Destroy previous non-keepAlive instance
|
|
609
627
|
this._instance.destroy();
|
|
610
628
|
this._instance = null;
|
|
611
629
|
}
|
|
612
630
|
|
|
613
|
-
// Create container
|
|
614
631
|
const _routeStart = typeof window !== 'undefined' && window.__zqRenderHook ? performance.now() : 0;
|
|
615
|
-
this._el.innerHTML = '';
|
|
616
632
|
|
|
617
633
|
// Pass route params and query as props
|
|
618
634
|
const props = { ...params, $route: to, $query: query, $params: params };
|
|
619
635
|
|
|
636
|
+
// Keep-alive: reuse cached instance
|
|
637
|
+
if (isKeepAlive && componentName && this._keepAliveCache.has(componentName)) {
|
|
638
|
+
const cached = this._keepAliveCache.get(componentName);
|
|
639
|
+
// Hide all children, show the cached one
|
|
640
|
+
[...this._el.children].forEach(c => { c.style.display = 'none'; });
|
|
641
|
+
cached.container.style.display = '';
|
|
642
|
+
this._instance = cached.instance;
|
|
643
|
+
this._currentKeepAlive = true;
|
|
644
|
+
this._currentComponentName = componentName;
|
|
645
|
+
// Call activated() lifecycle hook
|
|
646
|
+
if (cached.instance._def.activated) {
|
|
647
|
+
try { cached.instance._def.activated.call(cached.instance); }
|
|
648
|
+
catch (err) { reportError(ErrorCode.COMP_LIFECYCLE, `Component "${componentName}" activated() threw`, { component: componentName }, err); }
|
|
649
|
+
}
|
|
650
|
+
if (_routeStart) window.__zqRenderHook(this._el, performance.now() - _routeStart, 'route', componentName);
|
|
651
|
+
}
|
|
620
652
|
// If component is a string (registered name), mount it
|
|
621
|
-
if (
|
|
622
|
-
|
|
653
|
+
else if (componentName) {
|
|
654
|
+
// Hide all keep-alive cached children (don't destroy)
|
|
655
|
+
[...this._el.children].forEach(c => {
|
|
656
|
+
if (c.dataset.zqKeepAlive) {
|
|
657
|
+
c.style.display = 'none';
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
// Remove non-keep-alive children
|
|
661
|
+
[...this._el.children].forEach(c => {
|
|
662
|
+
if (!c.dataset.zqKeepAlive) c.remove();
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
const container = document.createElement(componentName);
|
|
666
|
+
if (isKeepAlive) container.dataset.zqKeepAlive = componentName;
|
|
623
667
|
this._el.appendChild(container);
|
|
624
668
|
try {
|
|
625
|
-
this._instance = mount(container,
|
|
669
|
+
this._instance = mount(container, componentName, props);
|
|
626
670
|
} catch (err) {
|
|
627
671
|
reportError(ErrorCode.COMP_NOT_FOUND, `Failed to mount component for route "${matched.path}"`, { component: matched.component, path: matched.path }, err);
|
|
628
672
|
return;
|
|
629
673
|
}
|
|
630
|
-
|
|
674
|
+
|
|
675
|
+
if (isKeepAlive) {
|
|
676
|
+
this._keepAliveCache.set(componentName, { container, instance: this._instance });
|
|
677
|
+
// Call activated() on first mount
|
|
678
|
+
if (this._instance._def.activated) {
|
|
679
|
+
try { this._instance._def.activated.call(this._instance); }
|
|
680
|
+
catch (err) { reportError(ErrorCode.COMP_LIFECYCLE, `Component "${componentName}" activated() threw`, { component: componentName }, err); }
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
this._currentKeepAlive = isKeepAlive;
|
|
685
|
+
this._currentComponentName = componentName;
|
|
686
|
+
if (_routeStart) window.__zqRenderHook(this._el, performance.now() - _routeStart, 'route', componentName);
|
|
631
687
|
}
|
|
632
688
|
// If component is a render function
|
|
633
689
|
else if (typeof matched.component === 'function') {
|
|
634
|
-
|
|
690
|
+
// Clear non-keepAlive content
|
|
691
|
+
[...this._el.children].forEach(c => {
|
|
692
|
+
if (c.dataset.zqKeepAlive) c.style.display = 'none';
|
|
693
|
+
else c.remove();
|
|
694
|
+
});
|
|
695
|
+
const wrapper = document.createElement('div');
|
|
696
|
+
wrapper.innerHTML = matched.component(to);
|
|
697
|
+
while (wrapper.firstChild) this._el.appendChild(wrapper.firstChild);
|
|
698
|
+
this._currentKeepAlive = false;
|
|
699
|
+
this._currentComponentName = null;
|
|
635
700
|
if (_routeStart) window.__zqRenderHook(this._el, performance.now() - _routeStart, 'route', to);
|
|
636
701
|
}
|
|
637
702
|
}
|
|
@@ -690,6 +755,11 @@ class Router {
|
|
|
690
755
|
document.removeEventListener('click', this._onLinkClick);
|
|
691
756
|
this._onLinkClick = null;
|
|
692
757
|
}
|
|
758
|
+
// Destroy all keep-alive cached instances
|
|
759
|
+
for (const [, cached] of this._keepAliveCache) {
|
|
760
|
+
cached.instance.destroy();
|
|
761
|
+
}
|
|
762
|
+
this._keepAliveCache.clear();
|
|
693
763
|
if (this._instance) this._instance.destroy();
|
|
694
764
|
this._listeners.clear();
|
|
695
765
|
this._substateListeners = [];
|
package/src/store.js
CHANGED
|
@@ -86,8 +86,9 @@ class Store {
|
|
|
86
86
|
batch(fn) {
|
|
87
87
|
this._batching = true;
|
|
88
88
|
this._batchQueue = [];
|
|
89
|
+
let result;
|
|
89
90
|
try {
|
|
90
|
-
fn(this.state);
|
|
91
|
+
result = fn(this.state);
|
|
91
92
|
} finally {
|
|
92
93
|
this._batching = false;
|
|
93
94
|
// Deduplicate: keep only the last change per key
|
|
@@ -100,6 +101,7 @@ class Store {
|
|
|
100
101
|
this._notifySubscribers(key, value, old);
|
|
101
102
|
}
|
|
102
103
|
}
|
|
104
|
+
return result;
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
/**
|
|
@@ -187,8 +189,14 @@ class Store {
|
|
|
187
189
|
}
|
|
188
190
|
|
|
189
191
|
/**
|
|
190
|
-
* Subscribe to changes on a specific state key
|
|
191
|
-
*
|
|
192
|
+
* Subscribe to changes on a specific state key, multiple keys, or all changes.
|
|
193
|
+
*
|
|
194
|
+
* Signatures:
|
|
195
|
+
* subscribe(callback) → wildcard, fires on every change
|
|
196
|
+
* subscribe('key', callback) → fires when 'key' changes
|
|
197
|
+
* subscribe(['a','b'], callback) → fires when any listed key changes
|
|
198
|
+
*
|
|
199
|
+
* @param {string|string[]|Function} keyOrFn - state key, array of keys, or function for all changes
|
|
192
200
|
* @param {Function} [fn] - callback (key, value, oldValue)
|
|
193
201
|
* @returns {Function} - unsubscribe
|
|
194
202
|
*/
|
|
@@ -199,6 +207,16 @@ class Store {
|
|
|
199
207
|
return () => this._wildcards.delete(keyOrFn);
|
|
200
208
|
}
|
|
201
209
|
|
|
210
|
+
// Multi-key subscription: subscribe(['files', 'isProcessing'], callback)
|
|
211
|
+
if (Array.isArray(keyOrFn)) {
|
|
212
|
+
const keys = keyOrFn;
|
|
213
|
+
const handler = (key, value, old) => {
|
|
214
|
+
if (keys.includes(key)) fn(key, value, old);
|
|
215
|
+
};
|
|
216
|
+
this._wildcards.add(handler);
|
|
217
|
+
return () => this._wildcards.delete(handler);
|
|
218
|
+
}
|
|
219
|
+
|
|
202
220
|
if (!this._subscribers.has(keyOrFn)) {
|
|
203
221
|
this._subscribers.set(keyOrFn, new Set());
|
|
204
222
|
}
|
|
@@ -270,3 +288,31 @@ export function createStore(name, config) {
|
|
|
270
288
|
export function getStore(name = 'default') {
|
|
271
289
|
return _stores.get(name) || null;
|
|
272
290
|
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
// Store-Component Connector
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Create a store connector descriptor for use in component definitions.
|
|
299
|
+
* When used in a component's `stores` config, auto-subscribes to the
|
|
300
|
+
* listed keys on mount and cleans up on destroy.
|
|
301
|
+
*
|
|
302
|
+
* Usage:
|
|
303
|
+
* $.component('my-comp', {
|
|
304
|
+
* stores: {
|
|
305
|
+
* app: connectStore(appStore, ['files', 'isProcessing']),
|
|
306
|
+
* },
|
|
307
|
+
* render() {
|
|
308
|
+
* return `<div>${this.stores.app.files.length} files</div>`;
|
|
309
|
+
* }
|
|
310
|
+
* });
|
|
311
|
+
*
|
|
312
|
+
* @param {Store} store - the store instance to connect
|
|
313
|
+
* @param {string[]} keys - state keys to sync
|
|
314
|
+
* @returns {{ _zqConnector: true, store: Store, keys: string[] }}
|
|
315
|
+
*/
|
|
316
|
+
export function connectStore(store, keys) {
|
|
317
|
+
return { _zqConnector: true, store, keys };
|
|
318
|
+
}
|
package/tests/cli.test.js
CHANGED
|
@@ -1021,3 +1021,83 @@ describe('CLI - createProject', () => {
|
|
|
1021
1021
|
spy.mockRestore();
|
|
1022
1022
|
});
|
|
1023
1023
|
});
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
// ===========================================================================
|
|
1027
|
+
// minifyTemplateLiterals - regex literal awareness
|
|
1028
|
+
// ===========================================================================
|
|
1029
|
+
|
|
1030
|
+
describe('CLI - minifyTemplateLiterals regex handling', () => {
|
|
1031
|
+
let minifyTemplateLiterals;
|
|
1032
|
+
|
|
1033
|
+
beforeEach(async () => {
|
|
1034
|
+
const mod = await import('../cli/commands/bundle.js');
|
|
1035
|
+
minifyTemplateLiterals = mod.minifyTemplateLiterals;
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
it('preserves code after regex containing backtick characters', () => {
|
|
1039
|
+
// Simulates: h = h.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
1040
|
+
// The backtick inside the regex must not be mistaken for a template literal.
|
|
1041
|
+
const input = "h = h.replace(/\x60([^\x60]+)\x60/g, '<code>$1</code>');\nreturn h;";
|
|
1042
|
+
const result = minifyTemplateLiterals(input);
|
|
1043
|
+
expect(result).toContain('return h');
|
|
1044
|
+
expect(result).toContain('<code>$1</code>');
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
it('does not eat closing braces after backtick-in-regex', () => {
|
|
1048
|
+
const input = [
|
|
1049
|
+
"var fmt = {",
|
|
1050
|
+
" md(raw) {",
|
|
1051
|
+
" let h = raw;",
|
|
1052
|
+
" h = h.replace(/\x60([^\x60]+)\x60/g, '<code>$1</code>');",
|
|
1053
|
+
" h = h.replace(/^[\\-\\*] (.+)$/gm, '<li>$1</li>');",
|
|
1054
|
+
" return h;",
|
|
1055
|
+
" }",
|
|
1056
|
+
"};",
|
|
1057
|
+
"var canvas = document.getElementById('bg-canvas');",
|
|
1058
|
+
].join('\n');
|
|
1059
|
+
const result = minifyTemplateLiterals(input);
|
|
1060
|
+
expect(result).toContain('return h;');
|
|
1061
|
+
expect(result).toContain('};');
|
|
1062
|
+
expect(result).toContain("getElementById('bg-canvas')");
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
it('handles regex with single backtick', () => {
|
|
1066
|
+
const input = "x.replace(/\x60/g, \"'\");\nvar y = 1;";
|
|
1067
|
+
const result = minifyTemplateLiterals(input);
|
|
1068
|
+
expect(result).toContain('var y = 1');
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
it('handles regex backtick in character class', () => {
|
|
1072
|
+
const input = "x.match(/[\x60'\"]/g);\nvar z = 2;";
|
|
1073
|
+
const result = minifyTemplateLiterals(input);
|
|
1074
|
+
expect(result).toContain('var z = 2');
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
it('does not confuse division operator with regex', () => {
|
|
1078
|
+
// After a number or identifier, / is division, not regex start
|
|
1079
|
+
const input = "var x = a / b;\nvar t = \x60hello\x60;";
|
|
1080
|
+
const result = minifyTemplateLiterals(input);
|
|
1081
|
+
expect(result).toContain('a / b');
|
|
1082
|
+
expect(result).toContain('\x60hello\x60');
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
it('still minifies real template literals correctly', () => {
|
|
1086
|
+
const input = "var html = \x60<div> <span> text </span> </div>\x60;";
|
|
1087
|
+
const result = minifyTemplateLiterals(input);
|
|
1088
|
+
// Template whitespace should be collapsed
|
|
1089
|
+
expect(result).not.toContain(' <span> ');
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
it('handles regex after return keyword', () => {
|
|
1093
|
+
const input = "return /\x60test\x60/g;\nvar after = 1;";
|
|
1094
|
+
const result = minifyTemplateLiterals(input);
|
|
1095
|
+
expect(result).toContain('var after = 1');
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
it('handles regex after assignment operator', () => {
|
|
1099
|
+
const input = "var re = /\x60([^\x60]+)\x60/gi;\nvar next = true;";
|
|
1100
|
+
const result = minifyTemplateLiterals(input);
|
|
1101
|
+
expect(result).toContain('var next = true');
|
|
1102
|
+
});
|
|
1103
|
+
});
|