@dimina-kit/devtools 0.3.1-dev.20260518114324 → 0.3.2-dev.20260522085907

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 (89) hide show
  1. package/README.md +33 -71
  2. package/dist/main/app/app.js +5 -0
  3. package/dist/main/app/bootstrap.js +2 -1
  4. package/dist/main/app/launch.d.ts +1 -1
  5. package/dist/main/app/launch.js +6 -7
  6. package/dist/main/index.bundle.js +306 -317
  7. package/dist/main/ipc/panels.js +3 -5
  8. package/dist/main/ipc/projects.d.ts +1 -1
  9. package/dist/main/ipc/projects.js +5 -5
  10. package/dist/main/ipc/settings.d.ts +1 -1
  11. package/dist/main/ipc/settings.js +14 -1
  12. package/dist/main/ipc/simulator.d.ts +1 -1
  13. package/dist/main/ipc/simulator.js +1 -10
  14. package/dist/main/services/automation/exec.d.ts +2 -0
  15. package/dist/main/services/automation/exec.js +7 -9
  16. package/dist/main/services/automation/handlers/app.js +37 -51
  17. package/dist/main/services/automation/handlers/element.js +27 -41
  18. package/dist/main/services/default-adapter.js +3 -3
  19. package/dist/main/services/mcp/index.d.ts +1 -0
  20. package/dist/main/services/mcp/index.js +1 -0
  21. package/dist/main/services/mcp/server.js +5 -0
  22. package/dist/main/services/mcp/status.d.ts +10 -0
  23. package/dist/main/services/mcp/status.js +14 -0
  24. package/dist/main/services/mcp/target-manager.js +2 -1
  25. package/dist/main/services/notifications/renderer-notifier.d.ts +3 -3
  26. package/dist/main/services/notifications/renderer-notifier.js +3 -6
  27. package/dist/main/services/projects/local-provider.js +3 -3
  28. package/dist/main/services/projects/project-repository.d.ts +0 -1
  29. package/dist/main/services/settings/index.js +2 -1
  30. package/dist/main/services/simulator-storage/index.js +47 -77
  31. package/dist/main/services/views/view-manager.d.ts +3 -7
  32. package/dist/main/services/views/view-manager.js +20 -27
  33. package/dist/main/services/window-service.d.ts +3 -3
  34. package/dist/main/services/workbench-context.d.ts +3 -6
  35. package/dist/main/services/workbench-context.js +1 -3
  36. package/dist/main/services/workspace/workspace-service.js +8 -24
  37. package/dist/main/utils/theme.d.ts +24 -0
  38. package/dist/main/utils/theme.js +38 -1
  39. package/dist/preload/index.d.ts +5 -2
  40. package/dist/preload/index.js +3 -2
  41. package/dist/preload/instrumentation/app-data.d.ts +32 -2
  42. package/dist/preload/instrumentation/app-data.js +187 -172
  43. package/dist/preload/instrumentation/index.d.ts +2 -2
  44. package/dist/preload/instrumentation/index.js +2 -2
  45. package/dist/preload/instrumentation/wxml.d.ts +16 -4
  46. package/dist/preload/instrumentation/wxml.js +188 -178
  47. package/dist/preload/miniapp-snapshot/host.d.ts +12 -0
  48. package/dist/preload/miniapp-snapshot/host.js +117 -0
  49. package/dist/preload/miniapp-snapshot/types.d.ts +62 -0
  50. package/dist/preload/miniapp-snapshot/types.js +15 -0
  51. package/dist/preload/runtime/bridge.js +5 -24
  52. package/dist/preload/runtime/custom-apis.d.ts +1 -1
  53. package/dist/preload/runtime/custom-apis.js +3 -7
  54. package/dist/preload/shared/expose.d.ts +15 -0
  55. package/dist/preload/shared/expose.js +33 -0
  56. package/dist/preload/shared/page-iframe.d.ts +3 -0
  57. package/dist/preload/shared/page-iframe.js +6 -0
  58. package/dist/preload/shared/types.d.ts +2 -0
  59. package/dist/preload/windows/simulator.js +400 -348
  60. package/dist/renderer/assets/constants-DI382mTP.js +2 -0
  61. package/dist/renderer/assets/index-D8VFYBh-.js +46 -0
  62. package/dist/renderer/assets/{input-DqVQNCx_.js → input-CoWcfa8u.js} +2 -2
  63. package/dist/renderer/assets/ipc-transport-Cd2xPrsa.js +9 -0
  64. package/dist/renderer/assets/popover-Nq5gT9DO.js +2 -0
  65. package/dist/renderer/assets/select-By5KH-UG.js +2 -0
  66. package/dist/renderer/assets/{settings-BFNfnYvU.js → settings-Dj_N70qR.js} +2 -2
  67. package/dist/renderer/assets/settings-api-Do0_Smq1.js +2 -0
  68. package/dist/renderer/assets/workbenchSettings-CazSLGYt.js +8 -0
  69. package/dist/renderer/entries/main/index.html +5 -4
  70. package/dist/renderer/entries/popover/index.html +5 -4
  71. package/dist/renderer/entries/settings/index.html +4 -4
  72. package/dist/renderer/entries/workbench-settings/index.html +4 -3
  73. package/dist/shared/ipc-channels.d.ts +4 -12
  74. package/dist/shared/ipc-channels.js +10 -14
  75. package/dist/shared/simulator-route.d.ts +72 -0
  76. package/dist/shared/simulator-route.js +169 -0
  77. package/dist/shared/types.d.ts +2 -10
  78. package/dist/simulator/assets/simulator-DJCozrU5.js +9 -0
  79. package/dist/simulator/simulator.html +6 -43
  80. package/package.json +4 -4
  81. package/dist/preload/instrumentation/disposable.d.ts +0 -11
  82. package/dist/preload/instrumentation/disposable.js +0 -15
  83. package/dist/renderer/assets/index-Czz_bp6b.js +0 -46
  84. package/dist/renderer/assets/ipc-transport-CCHrCtDr.js +0 -9
  85. package/dist/renderer/assets/popover-BRg4XI_w.js +0 -2
  86. package/dist/renderer/assets/select-CvILHHZU.js +0 -2
  87. package/dist/renderer/assets/settings-api-DOAgmBst.js +0 -2
  88. package/dist/renderer/assets/workbenchSettings-DjVMgMMH.js +0 -8
  89. package/dist/simulator/assets/simulator-DL6kOCq-.js +0 -9
@@ -1,59 +1,4 @@
1
- import { SimulatorChannel, BridgeChannel } from '../../shared/ipc-channels.js';
2
- import { onHostMessage, sendToHost } from '../runtime/host.js';
3
1
  import { clearAppDataSnapshot, setAppDataSnapshot, } from '../runtime/bridge.js';
4
- import { createDisposableSet } from './disposable.js';
5
- const appDataCache = new Map();
6
- // Bridges in insertion order — drives the `bridges` array in snapshots so
7
- // renderer tabs render in a stable, chronological sequence.
8
- const bridgeOrder = [];
9
- // Page path per bridge: set from `page_*` init's body.path. Component init
10
- // also carries a path but it's the component's own path, not the page route.
11
- const bridgePagePath = new Map();
12
- function recordBridge(bridgeId) {
13
- if (!bridgeOrder.includes(bridgeId))
14
- bridgeOrder.push(bridgeId);
15
- }
16
- function buildSnapshot() {
17
- const bridges = [];
18
- for (const id of bridgeOrder) {
19
- bridges.push({ id, pagePath: bridgePagePath.get(id) ?? null });
20
- }
21
- const entries = {};
22
- for (const [key, entry] of appDataCache) {
23
- const slash = key.indexOf('/');
24
- if (slash < 0)
25
- continue;
26
- const bridgeId = key.slice(0, slash);
27
- const moduleId = key.slice(slash + 1);
28
- if (!entries[bridgeId])
29
- entries[bridgeId] = {};
30
- const displayKey = entry.componentPath ?? moduleId;
31
- entries[bridgeId][displayKey] = entry.data;
32
- }
33
- return { bridges, entries };
34
- }
35
- function publishSnapshot() {
36
- const data = {};
37
- for (const [key, entry] of appDataCache) {
38
- data[key] = entry.data;
39
- }
40
- setAppDataSnapshot(data);
41
- }
42
- function clearBridge(bridgeId) {
43
- const prefix = `${bridgeId}/`;
44
- for (const key of [...appDataCache.keys()]) {
45
- if (key.startsWith(prefix))
46
- appDataCache.delete(key);
47
- }
48
- const idx = bridgeOrder.indexOf(bridgeId);
49
- if (idx >= 0)
50
- bridgeOrder.splice(idx, 1);
51
- bridgePagePath.delete(bridgeId);
52
- publishSnapshot();
53
- // Push a refreshed snapshot so the renderer can drop the evicted bridge
54
- // immediately, without waiting for the user to hit the refresh button.
55
- sendToHost(SimulatorChannel.AppDataAll, buildSnapshot());
56
- }
57
2
  // dimina runtime emits two kinds of service→main messages relevant to the
58
3
  // AppData panel:
59
4
  // • update batches `{type:'ub', body:{bridgeId, updates:[{moduleId,data}]}}`
@@ -132,131 +77,201 @@ function decodeOutgoingMessage(message) {
132
77
  bridgeId: typeof r.body?.bridgeId === 'string' ? r.body.bridgeId : undefined,
133
78
  };
134
79
  }
135
- function instrumentPostMessage(worker) {
136
- const orig = worker.postMessage.bind(worker);
137
- worker.postMessage = function (msg, ...rest) {
138
- const decoded = decodeOutgoingMessage(msg);
139
- if (decoded?.type === 'pageUnload' && decoded.bridgeId) {
140
- clearBridge(decoded.bridgeId);
80
+ /**
81
+ * The AppData snapshot data source.
82
+ *
83
+ * Instruments `window.Worker` (incoming `message` decoding + outgoing
84
+ * `postMessage` interception) and installs `window.__simulatorHook.appData`,
85
+ * accumulating per-(bridgeId, moduleId) setData state in an internal cache.
86
+ * Every cache mutation — an accepted `ub`/`page_*` worker message, or a
87
+ * `pageUnload` evicting a bridge — calls `emit()` so the `miniappSnapshot`
88
+ * host republishes the full snapshot. The source itself never touches IPC:
89
+ * the host owns push, pull and the install-time publish.
90
+ *
91
+ * It keeps the `__simulatorData.getAppdata()` automation surface working by
92
+ * mirroring the flat cache into the simulator bridge via `setAppDataSnapshot`
93
+ * / `clearAppDataSnapshot`.
94
+ */
95
+ export function createAppDataSource() {
96
+ const appDataCache = new Map();
97
+ // Bridges in insertion order — drives the `bridges` array in snapshots so
98
+ // renderer tabs render in a stable, chronological sequence.
99
+ const bridgeOrder = [];
100
+ // Page path per bridge: set from `page_*` init's body.path. Component init
101
+ // also carries a path but it's the component's own path, not the page route.
102
+ const bridgePagePath = new Map();
103
+ let emit = null;
104
+ let installed = false;
105
+ let originalHook;
106
+ let originalDescriptor;
107
+ let OriginalWorker;
108
+ function recordBridge(bridgeId) {
109
+ if (!bridgeOrder.includes(bridgeId))
110
+ bridgeOrder.push(bridgeId);
111
+ }
112
+ function buildSnapshot() {
113
+ const bridges = [];
114
+ for (const id of bridgeOrder) {
115
+ bridges.push({ id, pagePath: bridgePagePath.get(id) ?? null });
141
116
  }
142
- return orig(msg, ...rest);
143
- };
144
- }
145
- function createInstrumentedWorker(OriginalWorker) {
146
- function InstrumentedWorker(scriptURL, options) {
147
- const resolvedScriptURL = scriptURL instanceof URL
148
- ? scriptURL
149
- : new URL(scriptURL, window.location.href);
150
- const worker = Reflect.construct(OriginalWorker, [resolvedScriptURL, options], new.target ?? InstrumentedWorker);
151
- instrumentPostMessage(worker);
152
- worker.addEventListener('message', (event) => {
153
- const entries = decodeWorkerMessage(event.data);
154
- if (!entries)
155
- return;
156
- for (const entry of entries) {
157
- const hookBody = {
158
- bridgeId: entry.bridgeId,
159
- moduleId: entry.moduleId,
160
- data: entry.data,
161
- mode: entry.mode,
162
- };
163
- if (entry.mode === 'init')
164
- hookBody.componentPath = entry.componentPath;
165
- window.__simulatorHook?.appData(hookBody);
117
+ const entries = {};
118
+ for (const [key, entry] of appDataCache) {
119
+ const slash = key.indexOf('/');
120
+ if (slash < 0)
121
+ continue;
122
+ const bridgeId = key.slice(0, slash);
123
+ const moduleId = key.slice(slash + 1);
124
+ if (!entries[bridgeId])
125
+ entries[bridgeId] = {};
126
+ const displayKey = entry.componentPath ?? moduleId;
127
+ entries[bridgeId][displayKey] = entry.data;
128
+ }
129
+ return { bridges, entries };
130
+ }
131
+ // Mirror the flat cache into the `__simulatorData.getAppdata()` automation
132
+ // surface so MCP / e2e reads keep working after the snapshot migration.
133
+ function publishSnapshot() {
134
+ const data = {};
135
+ for (const [key, entry] of appDataCache) {
136
+ data[key] = entry.data;
137
+ }
138
+ setAppDataSnapshot(data);
139
+ }
140
+ function clearBridge(bridgeId) {
141
+ const prefix = `${bridgeId}/`;
142
+ for (const key of [...appDataCache.keys()]) {
143
+ if (key.startsWith(prefix))
144
+ appDataCache.delete(key);
145
+ }
146
+ const idx = bridgeOrder.indexOf(bridgeId);
147
+ if (idx >= 0)
148
+ bridgeOrder.splice(idx, 1);
149
+ bridgePagePath.delete(bridgeId);
150
+ publishSnapshot();
151
+ // The eviction is a cache mutation — notify the host so the renderer can
152
+ // drop the evicted bridge's tab immediately.
153
+ emit?.();
154
+ }
155
+ function applyAppData(body) {
156
+ const record = body;
157
+ if (!record?.bridgeId || !record?.moduleId)
158
+ return;
159
+ // R6: drop ub patches whose bridge has never been initialised. dimina
160
+ // dispatches pageUnload to the service worker, which fires onUnload —
161
+ // and onUnload's setData (e.g. console-test's stopTimer) produces a
162
+ // late `ub` that arrives AFTER our clearBridge. Without this gate the
163
+ // late patch would resurrect the unloaded bridge as a ghost tab.
164
+ if (record.mode !== 'init' && !bridgePagePath.has(record.bridgeId))
165
+ return;
166
+ const key = `${record.bridgeId}/${record.moduleId}`;
167
+ const prev = appDataCache.get(key);
168
+ const incoming = (record.data && typeof record.data === 'object'
169
+ ? record.data
170
+ : {});
171
+ // init = full-state replace; patch = merge into previous (setData semantics)
172
+ const merged = record.mode === 'init'
173
+ ? { ...incoming }
174
+ : { ...(prev?.data ?? {}), ...incoming };
175
+ const componentPath = record.componentPath ?? prev?.componentPath;
176
+ const next = componentPath !== undefined
177
+ ? { componentPath, data: merged }
178
+ : { data: merged };
179
+ appDataCache.set(key, next);
180
+ recordBridge(record.bridgeId);
181
+ if (record.mode === 'init'
182
+ && record.moduleId.startsWith('page_')
183
+ && record.componentPath) {
184
+ bridgePagePath.set(record.bridgeId, record.componentPath);
185
+ }
186
+ publishSnapshot();
187
+ emit?.();
188
+ }
189
+ function instrumentPostMessage(worker) {
190
+ const orig = worker.postMessage.bind(worker);
191
+ worker.postMessage = function (msg, ...rest) {
192
+ const decoded = decodeOutgoingMessage(msg);
193
+ if (decoded?.type === 'pageUnload' && decoded.bridgeId) {
194
+ clearBridge(decoded.bridgeId);
166
195
  }
196
+ return orig(msg, ...rest);
197
+ };
198
+ }
199
+ function createInstrumentedWorker(Original) {
200
+ function InstrumentedWorker(scriptURL, options) {
201
+ const resolvedScriptURL = scriptURL instanceof URL
202
+ ? scriptURL
203
+ : new URL(scriptURL, window.location.href);
204
+ const worker = Reflect.construct(Original, [resolvedScriptURL, options], new.target ?? InstrumentedWorker);
205
+ instrumentPostMessage(worker);
206
+ worker.addEventListener('message', (event) => {
207
+ const entries = decodeWorkerMessage(event.data);
208
+ if (!entries)
209
+ return;
210
+ for (const entry of entries) {
211
+ const hookBody = {
212
+ bridgeId: entry.bridgeId,
213
+ moduleId: entry.moduleId,
214
+ data: entry.data,
215
+ mode: entry.mode,
216
+ };
217
+ if (entry.mode === 'init')
218
+ hookBody.componentPath = entry.componentPath;
219
+ window.__simulatorHook?.appData(hookBody);
220
+ }
221
+ });
222
+ return worker;
223
+ }
224
+ Object.setPrototypeOf(InstrumentedWorker, Original);
225
+ Object.defineProperty(InstrumentedWorker, 'prototype', {
226
+ value: Original.prototype,
167
227
  });
168
- return worker;
228
+ return InstrumentedWorker;
169
229
  }
170
- Object.setPrototypeOf(InstrumentedWorker, OriginalWorker);
171
- Object.defineProperty(InstrumentedWorker, 'prototype', {
172
- value: OriginalWorker.prototype,
173
- });
174
- return InstrumentedWorker;
175
- }
176
- export function installAppDataInstrumentation() {
177
- const disposables = createDisposableSet();
178
- const originalHook = window.__simulatorHook;
179
- const originalDescriptor = Object.getOwnPropertyDescriptor(window, 'Worker');
180
- const OriginalWorker = window.Worker;
181
- window.__simulatorHook = {
182
- appData: (body) => {
183
- const record = body;
184
- if (!record?.bridgeId || !record?.moduleId)
230
+ return {
231
+ id: 'appdata',
232
+ snapshot: () => buildSnapshot(),
233
+ start(onChange) {
234
+ if (installed)
185
235
  return;
186
- // R6: drop ub patches whose bridge has never been initialised. dimina
187
- // dispatches pageUnload to the service worker, which fires onUnload —
188
- // and onUnload's setData (e.g. console-test's stopTimer) produces a
189
- // late `ub` that arrives AFTER our clearBridge. Without this gate the
190
- // late patch would resurrect the unloaded bridge as a ghost tab.
191
- if (record.mode !== 'init' && !bridgePagePath.has(record.bridgeId))
236
+ installed = true;
237
+ emit = onChange;
238
+ originalHook = window.__simulatorHook;
239
+ originalDescriptor = Object.getOwnPropertyDescriptor(window, 'Worker');
240
+ OriginalWorker = window.Worker;
241
+ window.__simulatorHook = {
242
+ appData: (body) => applyAppData(body),
243
+ };
244
+ Object.defineProperty(window, 'Worker', {
245
+ configurable: true,
246
+ writable: true,
247
+ value: createInstrumentedWorker(OriginalWorker),
248
+ });
249
+ },
250
+ dispose() {
251
+ if (!installed)
192
252
  return;
193
- const key = `${record.bridgeId}/${record.moduleId}`;
194
- const prev = appDataCache.get(key);
195
- const incoming = (record.data && typeof record.data === 'object'
196
- ? record.data
197
- : {});
198
- // init = full-state replace; patch = merge into previous (setData semantics)
199
- const merged = record.mode === 'init'
200
- ? { ...incoming }
201
- : { ...(prev?.data ?? {}), ...incoming };
202
- const componentPath = record.componentPath ?? prev?.componentPath;
203
- const next = componentPath !== undefined
204
- ? { componentPath, data: merged }
205
- : { data: merged };
206
- appDataCache.set(key, next);
207
- recordBridge(record.bridgeId);
208
- if (record.mode === 'init'
209
- && record.moduleId.startsWith('page_')
210
- && record.componentPath) {
211
- bridgePagePath.set(record.bridgeId, record.componentPath);
253
+ installed = false;
254
+ if (originalDescriptor) {
255
+ Object.defineProperty(window, 'Worker', originalDescriptor);
212
256
  }
213
- publishSnapshot();
214
- const payload = {
215
- bridgeId: record.bridgeId,
216
- moduleId: record.moduleId,
217
- data: merged,
218
- };
219
- if (componentPath !== undefined)
220
- payload.componentPath = componentPath;
221
- sendToHost(SimulatorChannel.AppData, payload);
257
+ else if (OriginalWorker) {
258
+ window.Worker = OriginalWorker;
259
+ }
260
+ if (originalHook) {
261
+ window.__simulatorHook = originalHook;
262
+ }
263
+ else {
264
+ delete window.__simulatorHook;
265
+ }
266
+ originalDescriptor = undefined;
267
+ OriginalWorker = undefined;
268
+ originalHook = undefined;
269
+ appDataCache.clear();
270
+ bridgeOrder.length = 0;
271
+ bridgePagePath.clear();
272
+ clearAppDataSnapshot();
273
+ emit = null;
222
274
  },
223
275
  };
224
- Object.defineProperty(window, 'Worker', {
225
- configurable: true,
226
- writable: true,
227
- value: createInstrumentedWorker(OriginalWorker),
228
- });
229
- disposables.add(onHostMessage(BridgeChannel.AppDataGetAllRequest, () => sendAllAppData()));
230
- publishSnapshot();
231
- disposables.add(() => {
232
- if (originalDescriptor) {
233
- Object.defineProperty(window, 'Worker', originalDescriptor);
234
- }
235
- else {
236
- window.Worker = OriginalWorker;
237
- }
238
- });
239
- disposables.add(() => {
240
- if (originalHook) {
241
- window.__simulatorHook = originalHook;
242
- }
243
- else {
244
- delete window.__simulatorHook;
245
- }
246
- });
247
- disposables.add(() => {
248
- appDataCache.clear();
249
- bridgeOrder.length = 0;
250
- bridgePagePath.clear();
251
- clearAppDataSnapshot();
252
- publishSnapshot();
253
- });
254
- return () => {
255
- disposables.disposeAll();
256
- };
257
- }
258
- export function sendAllAppData() {
259
- publishSnapshot();
260
- sendToHost(SimulatorChannel.AppDataAll, buildSnapshot());
261
276
  }
262
277
  //# sourceMappingURL=app-data.js.map
@@ -1,4 +1,4 @@
1
1
  export { installConsoleInstrumentation } from './console.js';
2
- export { installAppDataInstrumentation, sendAllAppData } from './app-data.js';
3
- export { installWxmlInstrumentation, sendWxmlTree, setupWxmlObserver } from './wxml.js';
2
+ export { createAppDataSource, type AppDataSnapshot } from './app-data.js';
3
+ export { createWxmlSource } from './wxml.js';
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1,4 +1,4 @@
1
1
  export { installConsoleInstrumentation } from './console.js';
2
- export { installAppDataInstrumentation, sendAllAppData } from './app-data.js';
3
- export { installWxmlInstrumentation, sendWxmlTree, setupWxmlObserver } from './wxml.js';
2
+ export { createAppDataSource } from './app-data.js';
3
+ export { createWxmlSource } from './wxml.js';
4
4
  //# sourceMappingURL=index.js.map
@@ -1,5 +1,17 @@
1
- declare function sendWxmlTree(): void;
2
- export declare function installWxmlInstrumentation(): () => void;
3
- export declare const setupWxmlObserver: typeof installWxmlInstrumentation;
4
- export { sendWxmlTree };
1
+ import { type WxmlNode } from '../runtime/bridge.js';
2
+ import type { MiniappSnapshotSource } from '../miniapp-snapshot/types.js';
3
+ /**
4
+ * The WXML snapshot data source.
5
+ *
6
+ * Observes the simulator page's Vue tree (retry-attach + MutationObserver +
7
+ * top-level navigation observer), recomputes a normalized {@link WxmlNode}
8
+ * tree on every (debounced) change, stores it internally, and calls `emit()`
9
+ * so the `miniappSnapshot` host publishes it. The source itself never touches
10
+ * IPC — the host owns the push/pull transport and the install-time publish.
11
+ *
12
+ * It keeps the `__simulatorData.getWxml()` automation surface working by
13
+ * mirroring each computed tree into the simulator bridge via
14
+ * `setWxmlSnapshot` / `clearWxmlSnapshot`.
15
+ */
16
+ export declare function createWxmlSource(): MiniappSnapshotSource<WxmlNode | null>;
5
17
  //# sourceMappingURL=wxml.d.ts.map