onelaraveljs 1.0.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.
Files changed (67) hide show
  1. package/README.md +87 -0
  2. package/docs/integration_analysis.md +116 -0
  3. package/docs/onejs_analysis.md +108 -0
  4. package/docs/optimization_implementation_group2.md +458 -0
  5. package/docs/optimization_plan.md +130 -0
  6. package/index.js +16 -0
  7. package/package.json +13 -0
  8. package/src/app.js +61 -0
  9. package/src/core/API.js +72 -0
  10. package/src/core/ChildrenRegistry.js +410 -0
  11. package/src/core/DOMBatcher.js +207 -0
  12. package/src/core/ErrorBoundary.js +226 -0
  13. package/src/core/EventDelegator.js +416 -0
  14. package/src/core/Helper.js +817 -0
  15. package/src/core/LoopContext.js +97 -0
  16. package/src/core/OneDOM.js +246 -0
  17. package/src/core/OneMarkup.js +444 -0
  18. package/src/core/Router.js +996 -0
  19. package/src/core/SEOConfig.js +321 -0
  20. package/src/core/SectionEngine.js +75 -0
  21. package/src/core/TemplateEngine.js +83 -0
  22. package/src/core/View.js +273 -0
  23. package/src/core/ViewConfig.js +229 -0
  24. package/src/core/ViewController.js +1410 -0
  25. package/src/core/ViewControllerOptimized.js +164 -0
  26. package/src/core/ViewIdentifier.js +361 -0
  27. package/src/core/ViewLoader.js +272 -0
  28. package/src/core/ViewManager.js +1962 -0
  29. package/src/core/ViewState.js +761 -0
  30. package/src/core/ViewSystem.js +301 -0
  31. package/src/core/ViewTemplate.js +4 -0
  32. package/src/core/helpers/BindingHelper.js +239 -0
  33. package/src/core/helpers/ConfigHelper.js +37 -0
  34. package/src/core/helpers/EventHelper.js +172 -0
  35. package/src/core/helpers/LifecycleHelper.js +17 -0
  36. package/src/core/helpers/ReactiveHelper.js +169 -0
  37. package/src/core/helpers/RenderHelper.js +15 -0
  38. package/src/core/helpers/ResourceHelper.js +89 -0
  39. package/src/core/helpers/TemplateHelper.js +11 -0
  40. package/src/core/managers/BindingManager.js +671 -0
  41. package/src/core/managers/ConfigurationManager.js +136 -0
  42. package/src/core/managers/EventManager.js +309 -0
  43. package/src/core/managers/LifecycleManager.js +356 -0
  44. package/src/core/managers/ReactiveManager.js +334 -0
  45. package/src/core/managers/RenderEngine.js +292 -0
  46. package/src/core/managers/ResourceManager.js +441 -0
  47. package/src/core/managers/ViewHierarchyManager.js +258 -0
  48. package/src/core/managers/ViewTemplateManager.js +127 -0
  49. package/src/core/reactive/ReactiveComponent.js +592 -0
  50. package/src/core/services/EventService.js +418 -0
  51. package/src/core/services/HttpService.js +106 -0
  52. package/src/core/services/LoggerService.js +57 -0
  53. package/src/core/services/StateService.js +512 -0
  54. package/src/core/services/StorageService.js +856 -0
  55. package/src/core/services/StoreService.js +258 -0
  56. package/src/core/services/TemplateDetectorService.js +361 -0
  57. package/src/core/services/Test.js +18 -0
  58. package/src/helpers/devWarnings.js +205 -0
  59. package/src/helpers/performance.js +226 -0
  60. package/src/helpers/utils.js +287 -0
  61. package/src/init.js +343 -0
  62. package/src/plugins/auto-plugin.js +34 -0
  63. package/src/services/Test.js +18 -0
  64. package/src/types/index.js +193 -0
  65. package/src/utils/date-helper.js +51 -0
  66. package/src/utils/helpers.js +39 -0
  67. package/src/utils/validation.js +32 -0
@@ -0,0 +1,258 @@
1
+ import { __defineProp } from "../../helpers/utils.js";
2
+
3
+ class StoreDep{
4
+ constructor(servive) {
5
+ /**
6
+ * @type {StoreService}
7
+ */
8
+ this.service = servive;
9
+ this.ownProps = ['__dep__', 'set', 'get', 'has', 'subscribe', 'unasubscribe'];
10
+ this.existsKeys = new Set();
11
+ this.data = {};
12
+ this.changedKeys = new Set();
13
+ this.listeners = new Map();
14
+ this.multiKeyListeners = []; // Array of {keys: Set, callback: Function, called: Boolean}
15
+ this.hasPendingFlush = false;
16
+ }
17
+ set(key, value) {
18
+ if(this.ownProps.includes(key)){
19
+ return;
20
+ }
21
+ this.data[key] = value;
22
+ this.emitsChange(key, value);
23
+ if(!this.existsKeys.has(key)){
24
+ this.existsKeys.add(key);
25
+ __defineProp(this.service, key, {
26
+ get: () => {
27
+ return this.get(key);
28
+ },
29
+ set: (newValue) => {
30
+ this.set(key, newValue);
31
+ },
32
+ enumerable: true,
33
+ configurable: true
34
+
35
+ });
36
+ }
37
+ }
38
+ get(key) {
39
+ return typeof this.data[key] !== 'undefined' ? this.data[key] : null;
40
+ }
41
+ has(key) {
42
+ return this.existsKeys.has(key);
43
+ }
44
+ emitsChange(key, value) {
45
+ if(!this.changedKeys.has(key)){
46
+ this.changedKeys.add(key);
47
+ }
48
+
49
+ // Chỉ schedule 1 flush duy nhất cho batch changes
50
+ if(!this.hasPendingFlush){
51
+ this.hasPendingFlush = true;
52
+ Promise.resolve().then(() => {
53
+ this.flushChanges();
54
+ });
55
+ }
56
+ }
57
+
58
+ flushChanges() {
59
+ // Reset multi-key listeners called flag
60
+ this.multiKeyListeners.forEach(listener => {
61
+ listener.called = false;
62
+ });
63
+
64
+ // Gọi single-key listeners
65
+ this.changedKeys.forEach(changedKey => {
66
+ if(this.listeners.has(changedKey)){
67
+ const listeners = this.listeners.get(changedKey);
68
+ listeners.forEach(callback => {
69
+ callback(this.get(changedKey));
70
+ });
71
+ }
72
+
73
+ // Check multi-key listeners
74
+ this.multiKeyListeners.forEach(listener => {
75
+ if(!listener.called && listener.keys.has(changedKey)){
76
+ listener.called = true;
77
+ // Collect all changed values for subscribed keys
78
+ const values = {};
79
+ listener.keys.forEach(k => {
80
+ if(this.changedKeys.has(k)){
81
+ values[k] = this.get(k);
82
+ }
83
+ });
84
+ listener.callback(values);
85
+ }
86
+ });
87
+ });
88
+
89
+ this.changedKeys.clear();
90
+ this.hasPendingFlush = false;
91
+ }
92
+ /**
93
+ * Đăng ký lắng nghe thay đổi của khoá lưu trữ
94
+ *
95
+ * @param {string|array} key - Khoá lưu trữ hoặc array của các khoá
96
+ * @param {function} callback - Hàm callback khi khoá thay đổi
97
+ * @return {function} Hàm hủy đăng ký lắng nghe
98
+ */
99
+ subscribe(key, callback) {
100
+ // Hỗ trợ subscribe với array keys: subscribe(['key1', 'key2'], callback)
101
+ // Callback sẽ được gọi 1 lần duy nhất khi bất kỳ key nào thay đổi
102
+ if(Array.isArray(key)){
103
+ const keys = new Set(key.filter(k => this.existsKeys.has(k)));
104
+ if(keys.size === 0){
105
+ return () => {};
106
+ }
107
+
108
+ const listener = { keys, callback, called: false };
109
+ this.multiKeyListeners.push(listener);
110
+
111
+ // Return unsubscribe function
112
+ return () => {
113
+ const index = this.multiKeyListeners.indexOf(listener);
114
+ if(index !== -1){
115
+ this.multiKeyListeners.splice(index, 1);
116
+ }
117
+ };
118
+ }
119
+
120
+ if(typeof key === "object" && key !== null){
121
+
122
+ const unsubscribes = {};
123
+ for(const k of Object.keys(key)){
124
+ unsubscribes[k] = this.subscribe(k, key[k]);
125
+ }
126
+ return () => {
127
+ for(const k of Object.keys(unsubscribes)){
128
+ unsubscribes[k]();
129
+ }
130
+ };
131
+ }
132
+ if(typeof key !== 'string' || !this.existsKeys.has(key) || typeof callback !== 'function'){
133
+ return;
134
+ }
135
+ if(!this.listeners.has(key)){
136
+ this.listeners.set(key, []);
137
+ }
138
+ const listeners = this.listeners.get(key);
139
+ listeners.push(callback);
140
+
141
+ return () => this.unasubscribe(key, callback);
142
+ }
143
+
144
+ /**
145
+ * Hủy đăng ký lắng nghe thay đổi của khoá lưu trữ
146
+ *
147
+ * @param {string|array} key - Khoá lưu trữ hoặc array của các khoá
148
+ * @param {function|null} callback - Hàm callback đã đăng ký (nếu null sẽ hủy tất cả callback của khoá)
149
+ */
150
+ unasubscribe(key, callback = null) {
151
+ // Hỗ trợ unsubscribe với array keys (không phân biệt thứ tự)
152
+ if(Array.isArray(key)){
153
+ const keySet = new Set(key);
154
+
155
+ // Helper function để check 2 Sets có giống nhau không
156
+ const areSetsEqual = (set1, set2) => {
157
+ if(set1.size !== set2.size) return false;
158
+ for(const k of set1){
159
+ if(!set2.has(k)) return false;
160
+ }
161
+ return true;
162
+ };
163
+
164
+ if(!callback){
165
+ // Xóa TẤT CẢ multi-key listeners có cùng set keys
166
+ for(let i = this.multiKeyListeners.length - 1; i >= 0; i--){
167
+ if(areSetsEqual(this.multiKeyListeners[i].keys, keySet)){
168
+ this.multiKeyListeners.splice(i, 1);
169
+ }
170
+ }
171
+ return;
172
+ }
173
+
174
+ // Xóa listener cụ thể với callback
175
+ const index = this.multiKeyListeners.findIndex(listener => {
176
+ return listener.callback === callback && areSetsEqual(listener.keys, keySet);
177
+ });
178
+
179
+ if(index !== -1){
180
+ this.multiKeyListeners.splice(index, 1);
181
+ }
182
+ return;
183
+ }
184
+
185
+ if(typeof key === "object" && key !== null){
186
+ for(const k of Object.keys(key)){
187
+ this.unasubscribe(k, key[k]);
188
+ }
189
+ return;
190
+ }
191
+ if(typeof key !== 'string' || !this.existsKeys.has(key)){
192
+ return;
193
+ }
194
+ if(callback && typeof callback !== 'function'){
195
+ return;
196
+ }
197
+ if(callback){
198
+ const listeners = this.listeners.get(key);
199
+ const index = listeners.indexOf(callback);
200
+ if(index !== -1){
201
+ listeners.splice(index, 1);
202
+ }
203
+ if(listeners.length === 0){
204
+ this.listeners.delete(key);
205
+ }
206
+ } else {
207
+ this.listeners.delete(key);
208
+ }
209
+ }
210
+ }
211
+
212
+ export class StoreService {
213
+ static intances = new Map();
214
+ static getInstance(name = null) {
215
+ if (!name || name === '' || name === 'default' || name === undefined) {
216
+ name = 'default';
217
+ }
218
+ if (!StoreService.intances.has(name)) {
219
+ StoreService.intances.set(name, new StoreService());
220
+ }
221
+ return StoreService.intances.get(name);
222
+ }
223
+ constructor() {
224
+ /**
225
+ * @type {Map<string, StoreDep>}
226
+ * @private
227
+ */
228
+ const dep = new StoreDep(this);
229
+ Object.defineProperty(this, '__dep__', {
230
+ value: dep,
231
+ writable: false,
232
+ enumerable: false,
233
+ configurable: false
234
+ });
235
+ }
236
+ /**
237
+ * Lấy hoặc tạo một kho lưu trữ theo tên
238
+ * @param {string} name - Tên kho lưu trữ
239
+ * @returns {StoreDep}
240
+ */
241
+ set(name, value = null) {
242
+ return this.__dep__.set(name, value);
243
+ }
244
+ get(name) {
245
+ return this.__dep__.get(name);
246
+ }
247
+ has(name) {
248
+ return this.__dep__.has(name);
249
+ }
250
+ subscribe(name, callback) {
251
+ return this.__dep__.subscribe(name, callback);
252
+ }
253
+ unsubscribe(name, callback) {
254
+ return this.__dep__.unasubscribe(name, callback);
255
+ }
256
+ }
257
+
258
+ export default StoreService.getInstance();
@@ -0,0 +1,361 @@
1
+ /**
2
+ * Class quản lý việc parse và tìm kiếm các cặp comment tags
3
+ * Hỗ trợ tìm kiếm theo pattern với wildcard (*)
4
+ *
5
+ * @example
6
+ * const parser = new TemplateDetectorService(document.body);
7
+ * const pairs = parser.find('one-template:*');
8
+ * parser.display(pairs);
9
+ */
10
+ export class TemplateDetectorService {
11
+ /**
12
+ * Khởi tạo parser với element gốc
13
+ * @param {Element} rootElement - Element gốc để tìm kiếm (mặc định: document.body)
14
+ */
15
+ constructor(rootElement = document.body) {
16
+ this.rootElement = rootElement || document.body;
17
+ this.cachedComments = null;
18
+ }
19
+
20
+ /**
21
+ * Đặt element gốc mới
22
+ * @param {Element} element - Element gốc mới
23
+ */
24
+ setRootElement(element) {
25
+ this.rootElement = element;
26
+ this.cachedComments = null;
27
+ return this;
28
+ }
29
+
30
+ /**
31
+ * Lấy tất cả comment nodes từ rootElement
32
+ * @param {boolean} useCache - Sử dụng cache hay không
33
+ * @returns {Array<Comment>}
34
+ */
35
+ getAllComments(useCache = true) {
36
+ if (useCache && this.cachedComments) {
37
+ return this.cachedComments;
38
+ }
39
+
40
+ const comments = [];
41
+ const walker = document.createTreeWalker(
42
+ this.rootElement,
43
+ NodeFilter.SHOW_COMMENT,
44
+ null,
45
+ false
46
+ );
47
+
48
+ let node;
49
+ while (node = walker.nextNode()) {
50
+ comments.push(node);
51
+ }
52
+
53
+ if (useCache) {
54
+ this.cachedComments = comments;
55
+ }
56
+
57
+ return comments;
58
+ }
59
+
60
+ /**
61
+ * Parse một comment node để lấy thông tin
62
+ * @param {Comment} commentNode - Comment node cần parse
63
+ * @returns {Object|null} - Thông tin tag hoặc null nếu không phải tag hợp lệ
64
+ */
65
+ parseComment(commentNode) {
66
+ const text = commentNode.nodeValue.trim();
67
+
68
+ // Pattern tổng quát: [prefix:name] hoặc [prefix]
69
+ // Kiểm tra tag mở: <!-- [prefix:name attribute="..."] -->
70
+ const openMatch = text.match(/^\[([^\/\]]+?)(?:\s+(.+))?\]$/);
71
+ if (openMatch) {
72
+ const fullName = openMatch[1];
73
+ const attributes = {};
74
+
75
+ if (openMatch[2]) {
76
+ // Parse các attributes như subscribe="userState,items"
77
+ const attrMatch = openMatch[2].match(/(\w+)="([^"]+)"/g);
78
+ if (attrMatch) {
79
+ attrMatch.forEach(attr => {
80
+ const [key, value] = attr.split('=');
81
+ attributes[key] = value.replace(/"/g, '');
82
+ });
83
+ }
84
+ }
85
+
86
+ return {
87
+ type: 'open',
88
+ fullName: fullName,
89
+ attributes: attributes,
90
+ node: commentNode
91
+ };
92
+ }
93
+
94
+ // Kiểm tra tag đóng: <!-- [/prefix:name] -->
95
+ const closeMatch = text.match(/^\[\/([^\/\]]+?)\]$/);
96
+ if (closeMatch) {
97
+ return {
98
+ type: 'close',
99
+ fullName: closeMatch[1],
100
+ node: commentNode
101
+ };
102
+ }
103
+
104
+ return null;
105
+ }
106
+
107
+ /**
108
+ * Lấy tất cả các siblings giữa hai node (cùng parent)
109
+ * @param {Node} startNode - Node bắt đầu
110
+ * @param {Node} endNode - Node kết thúc
111
+ * @returns {Array<Node>|null} - Mảng nodes hoặc null nếu không cùng parent
112
+ */
113
+ getNodesBetween(startNode, endNode) {
114
+ // Kiểm tra xem có cùng parent không
115
+ if (startNode.parentNode !== endNode.parentNode) {
116
+ return null;
117
+ }
118
+
119
+ const nodes = [];
120
+ let current = startNode.nextSibling;
121
+
122
+ while (current && current !== endNode) {
123
+ nodes.push(current);
124
+ current = current.nextSibling;
125
+ }
126
+
127
+ return nodes;
128
+ }
129
+
130
+ /**
131
+ * Chuyển đổi pattern thành regex
132
+ * @param {string} pattern - Pattern với wildcard (*)
133
+ * @returns {RegExp}
134
+ *
135
+ * @example
136
+ * patternToRegex('*:*') // Match tất cả
137
+ * patternToRegex('one-*') // Match "one-template", "one-component"
138
+ * patternToRegex('one-template:*') // Match "one-template:profile", etc.
139
+ */
140
+ patternToRegex(pattern) {
141
+ if (!pattern || pattern === '*' || pattern === '*:*') {
142
+ return /.*/;
143
+ }
144
+
145
+ // Escape các ký tự đặc biệt trong regex (trừ *)
146
+ let regexStr = pattern
147
+ .replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars
148
+ .replace(/\*/g, '.*'); // Convert * thành .*
149
+
150
+ // Thêm ^ và $ để match chính xác toàn bộ string
151
+ regexStr = '^' + regexStr + '$';
152
+
153
+ return new RegExp(regexStr);
154
+ }
155
+
156
+ /**
157
+ * Kiểm tra xem một tên có khớp với pattern không
158
+ * @param {string} fullName - Tên đầy đủ của tag
159
+ * @param {string} pattern - Pattern để so khớp
160
+ * @returns {boolean}
161
+ */
162
+ matchPattern(fullName, pattern) {
163
+ const regex = this.patternToRegex(pattern);
164
+ return regex.test(fullName);
165
+ }
166
+
167
+ /**
168
+ * Tìm tất cả các cặp tags theo pattern
169
+ * @param {string} pattern - Pattern để lọc (mặc định: '*:*')
170
+ * @param {Object} options - Tùy chọn bổ sung
171
+ * @param {boolean} options.useCache - Sử dụng cache comments
172
+ * @returns {Array<Object>} - Mảng các cặp tags tìm được
173
+ *
174
+ * @example
175
+ * parser.find('*:*') // Tìm tất cả
176
+ * parser.find('one-template:*') // Chỉ tìm one-template
177
+ * parser.find('one-template:profile') // Tìm chính xác profile
178
+ */
179
+ find(pattern = '*:*', options = {}, total = null) {
180
+ const { useCache = true } = options;
181
+ const isTotal = total && typeof total === 'number' && total > 0;
182
+ const isLast = total && total === -1;
183
+ const isFirst = total && total === 1;
184
+ const comments = this.getAllComments(useCache);
185
+ const parsed = comments.map(c => this.parseComment(c)).filter(p => p !== null);
186
+ // console.error({ parsed });
187
+ const pairs = [];
188
+ const stack = [];
189
+
190
+ for (let i = 0; i < parsed.length; i++) {
191
+ const current = parsed[i];
192
+
193
+ // Kiểm tra xem có khớp với pattern không
194
+ if (!this.matchPattern(current.fullName, pattern)) {
195
+ continue;
196
+ }
197
+
198
+ if (current.type === 'open') {
199
+ // Đẩy tag mở vào stack
200
+ stack.push({
201
+ ...current,
202
+ index: i
203
+ });
204
+ } else if (current.type === 'close') {
205
+ // Tìm tag mở tương ứng từ stack
206
+ let foundIndex = -1;
207
+ for (let j = stack.length - 1; j >= 0; j--) {
208
+ if (stack[j].fullName === current.fullName) {
209
+ foundIndex = j;
210
+ break;
211
+ }
212
+ }
213
+
214
+ if (foundIndex !== -1) {
215
+ const openTag = stack[foundIndex];
216
+
217
+ // Lấy các nodes nằm giữa
218
+ const nodesBetween = this.getNodesBetween(openTag.node, current.node);
219
+
220
+ // Chỉ thêm vào nếu cùng parent
221
+ if (nodesBetween !== null) {
222
+ pairs.push({
223
+ fullName: current.fullName,
224
+ openTag: openTag.node,
225
+ closeTag: current.node,
226
+ attributes: openTag.attributes,
227
+ nodes: nodesBetween,
228
+ parent: openTag.node.parentNode
229
+ });
230
+ }
231
+
232
+ // Xóa khỏi stack
233
+ stack.splice(foundIndex, 1);
234
+ }
235
+ }
236
+ }
237
+
238
+ return pairs;
239
+ }
240
+
241
+ /**
242
+ * Tìm một cặp tag duy nhất theo pattern
243
+ * @param {string} pattern - Pattern để tìm
244
+ * @returns {Object|null} - Cặp tag tìm được hoặc null
245
+ */
246
+ findOne(pattern) {
247
+ const pairs = this.find(pattern);
248
+ return pairs.length > 0 ? pairs[0] : null;
249
+ }
250
+
251
+ /**
252
+ * Lọc các cặp tags theo điều kiện tùy chỉnh
253
+ * @param {Function} filterFn - Hàm filter (pair) => boolean
254
+ * @param {string} pattern - Pattern ban đầu
255
+ * @returns {Array<Object>}
256
+ */
257
+ filter(filterFn, pattern = '*:*') {
258
+ const pairs = this.find(pattern);
259
+ return pairs.filter(filterFn);
260
+ }
261
+
262
+ /**
263
+ * Đếm số lượng cặp tags theo pattern
264
+ * @param {string} pattern - Pattern để đếm
265
+ * @returns {number}
266
+ */
267
+ count(pattern = '*:*') {
268
+ return this.find(pattern).length;
269
+ }
270
+
271
+ /**
272
+ * Lấy danh sách tất cả các tên tags duy nhất
273
+ * @param {string} pattern - Pattern để lọc
274
+ * @returns {Array<string>}
275
+ */
276
+ getUniqueNames(pattern = '*:*') {
277
+ const pairs = this.find(pattern);
278
+ return [...new Set(pairs.map(p => p.fullName))];
279
+ }
280
+
281
+ /**
282
+ * Hiển thị kết quả tìm kiếm trong console
283
+ * @param {Array<Object>|string} pairsOrPattern - Mảng pairs hoặc pattern string
284
+ * @param {Object} options - Tùy chọn hiển thị
285
+ */
286
+ display(pairsOrPattern, options = {}) {
287
+ const {
288
+ showAttributes = true,
289
+ showParent = true,
290
+ showNodes = true,
291
+ maxNodePreview = 3
292
+ } = options;
293
+
294
+ let pairs, patternInfo = '';
295
+
296
+ // Nếu truyền vào là string, tìm kiếm trước
297
+ if (typeof pairsOrPattern === 'string') {
298
+ patternInfo = ` với pattern "${pairsOrPattern}"`;
299
+ pairs = this.find(pairsOrPattern);
300
+ } else {
301
+ pairs = pairsOrPattern;
302
+ }
303
+
304
+ console.log(`\n${'='.repeat(80)}`);
305
+ console.log(`Tìm thấy ${pairs.length} cặp tags${patternInfo}`);
306
+ console.log(`${'='.repeat(80)}\n`);
307
+
308
+ if (pairs.length === 0) {
309
+ console.log('❌ Không tìm thấy cặp tags nào!');
310
+ return;
311
+ }
312
+
313
+ pairs.forEach((pair, index) => {
314
+ console.log(`\n📦 Cặp ${index + 1}: [${pair.fullName}]`);
315
+ console.log('-'.repeat(60));
316
+
317
+ if (showAttributes) {
318
+ console.log('📋 Attributes:',
319
+ Object.keys(pair.attributes).length > 0
320
+ ? pair.attributes
321
+ : '(không có)'
322
+ );
323
+ }
324
+
325
+ if (showParent) {
326
+ console.log('🔼 Parent:', pair.parent.tagName.toLowerCase(),
327
+ pair.parent.className ? `class="${pair.parent.className}"` : ''
328
+ );
329
+ }
330
+
331
+ if (showNodes) {
332
+ console.log(`📄 Nội dung (${pair.nodes.length} nodes):`);
333
+
334
+ const previewNodes = pair.nodes.slice(0, maxNodePreview);
335
+ previewNodes.forEach(node => {
336
+ if (node.nodeType === Node.ELEMENT_NODE) {
337
+ const className = node.className ? ` class="${node.className}"` : '';
338
+ console.log(` ├─ <${node.tagName.toLowerCase()}${className}>`);
339
+ } else if (node.nodeType === Node.TEXT_NODE && node.nodeValue.trim()) {
340
+ const text = node.nodeValue.trim().substring(0, 50);
341
+ console.log(` ├─ Text: "${text}${text.length >= 50 ? '...' : ''}"`);
342
+ }
343
+ });
344
+
345
+ if (pair.nodes.length > maxNodePreview) {
346
+ console.log(` └─ ... và ${pair.nodes.length - maxNodePreview} nodes khác`);
347
+ }
348
+ }
349
+ });
350
+
351
+ console.log(`\n${'='.repeat(80)}\n`);
352
+ }
353
+
354
+ /**
355
+ * Xóa cache để buộc parse lại
356
+ */
357
+ clearCache() {
358
+ this.cachedComments = null;
359
+ return this;
360
+ }
361
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Test service để demo alias @app
3
+ */
4
+ export class TestService {
5
+ constructor() {
6
+ this.name = 'TestService';
7
+ }
8
+
9
+ getMessage() {
10
+ return `Hello from ${this.name}!`;
11
+ }
12
+
13
+ static create() {
14
+ return new TestService();
15
+ }
16
+ }
17
+
18
+ export default TestService;