UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

548 lines (546 loc) 26.9 kB
#!/usr/bin/env node const require_runtime = require('./_virtual/_rolldown/runtime.cjs'); const require_config = require('./config.cjs'); const require_cli_init = require('./cli/init.cjs'); const require_timing = require('./cli/timing.cjs'); const require_plugin = require('./plugin.cjs'); const require_catalog = require('./cli/catalog.cjs'); const require_help = require('./cli/help.cjs'); const require_parse = require('./cli/parse.cjs'); const require_prompts = require('./cli/prompts.cjs'); const require_theme = require('./utils/theme.cjs'); const require_banner = require('./utils/banner.cjs'); let _clack_prompts = require("@clack/prompts"); _clack_prompts = require_runtime.__toESM(_clack_prompts, 1); let every_plugin = require("every-plugin"); //#region src/cli.ts function printConfigView(result) { console.log(); console.log(require_theme.colors.cyan(require_theme.frames.top(52))); console.log(` ${require_theme.icons.app} ${require_theme.gradients.cyber("CONFIG")}`); console.log(require_theme.colors.cyan(require_theme.frames.bottom(52))); console.log(); console.log(` ${require_theme.colors.dim("Account")} ${require_theme.colors.cyan(result.account)}`); console.log(` ${require_theme.colors.dim("Domain")} ${require_theme.colors.white(result.domain ?? "not configured")}`); if (result.staging) console.log(` ${require_theme.colors.dim("Staging")} ${require_theme.colors.magenta(result.staging.domain)}`); console.log(); } function formatTimeAgo(isoTimestamp) { const diffMs = Date.now() - new Date(isoTimestamp).getTime(); const diffMins = Math.floor(diffMs / 6e4); if (diffMins < 1) return "just now"; if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? "s" : ""} ago`; const diffHours = Math.floor(diffMins / 60); if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? "s" : ""} ago`; const diffDays = Math.floor(diffHours / 24); if (diffDays < 30) return `${diffDays} day${diffDays > 1 ? "s" : ""} ago`; return isoTimestamp.split("T")[0] ?? isoTimestamp; } function normalizeVersion(v) { return v.replace(/^[\^~>=v]+/, "").trim(); } function printTimingSummary(timings) { if (!timings || timings.length === 0) return; console.log(` ${require_theme.colors.dim("Timings:")}`); for (const timing of timings) console.log(` ${require_theme.colors.dim(timing.name.padEnd(22))} ${require_timing.formatDuration(timing.durationMs)}`); console.log(` ${require_theme.colors.dim("total".padEnd(22))} ${require_timing.formatDuration(require_timing.sumPhaseDurations(timings))}`); } function printStartSummary(summary) { console.log(); console.log(` ${require_theme.colors.dim("Config Source:")} ${summary.configSource}`); if (summary.configSourceHttp) console.log(` ${require_theme.colors.dim(summary.configSourceHttp)}`); console.log(` ${require_theme.colors.dim("Account:")} ${summary.account}`); console.log(` ${require_theme.colors.dim("Domain:")} ${summary.domain ?? "not configured"}`); console.log(); console.log(` ${require_theme.colors.dim("Modules:")}`); console.log(` ${require_theme.colors.dim("HOST")}${summary.modules.host ?? "local"}`); console.log(` ${require_theme.colors.dim("UI")}${summary.modules.ui ?? "local"}`); console.log(` ${require_theme.colors.dim("API")}${summary.modules.api ?? "local"}`); if (summary.modules.auth) console.log(` ${require_theme.colors.dim("AUTH")}${summary.modules.auth}`); if (summary.warnings.length > 0) { console.log(); for (const w of summary.warnings) console.log(` ${require_theme.colors.yellow(w)}`); } console.log(); } function clearSpinnerStopLine() { if (!process.stdout.isTTY) return; process.stdout.write("\x1B[1A\x1B[2K\x1B[1G"); } async function warnIfOutdated(client, command) { if (![ "dev", "build", "start" ].includes(command)) return; try { const status = await client.status(); if (status.status === "error" || !status.packages) return; const frameworkPackages = ["everything-dev", "every-plugin"]; const outdated = status.packages.filter((p) => p.installed && p.latest && normalizeVersion(p.installed) !== normalizeVersion(p.latest) && frameworkPackages.includes(p.name)); if (outdated.length === 0) return; console.log(); console.log(require_theme.colors.yellow(` ! Outdated packages detected:`)); for (const pkg of outdated) console.log(require_theme.colors.dim(` ${pkg.name} ${pkg.installed}${pkg.latest}`)); console.log(require_theme.colors.dim(` Run ${require_theme.colors.cyan("bos upgrade")} to update packages and sync template files.`)); console.log(); } catch {} } async function main() { const args = process.argv.slice(2); if (args.includes("--help") || args.includes("-h")) { require_help.printHelp(); return; } const invocationArgs = args.length > 0 ? args : ["dev"]; const command = invocationArgs[0] ?? "dev"; const configPath = require_config.findConfigPath(); const commandMatch = require_catalog.findCommandDescriptor(invocationArgs); if (!commandMatch) { console.error(`Unknown command: ${command}`); process.exit(1); } const { descriptor, consumed } = commandMatch; const commandArgs = invocationArgs.slice(consumed); require_banner.printBanner(); const pluginRuntime = (0, every_plugin.createPluginRuntime)({ registry: { bos: { module: require_plugin.default } }, secrets: {} }); const client = (await pluginRuntime.usePlugin.bind(pluginRuntime)("bos", { variables: { configPath: configPath ?? void 0 }, secrets: {} })).createClient(); await warnIfOutdated(client, command); try { const input = require_parse.parseCommandInput(descriptor, commandArgs); if (descriptor.key === "dev") { const devSpinner = _clack_prompts.spinner(); devSpinner.start("Starting dev environment"); const devPhaseLabels = { config: "Preparing config...", install: "Installing dependencies...", "build plugin": "Building plugin...", build: "Building everything-dev..." }; const onDevProgress = (event) => { const label = devPhaseLabels[event.phase] ?? event.phase; if (event.status === "running") devSpinner.message(label); }; require_plugin.pluginEvents.on("progress", onDevProgress); let result; try { result = await client.dev(input); } finally { require_plugin.pluginEvents.off("progress", onDevProgress); } if (result.status === "error") { devSpinner.stop("Failed"); console.error(`[CLI] ${result.description}`); process.exit(1); } devSpinner.stop(); clearSpinnerStopLine(); const session = require_plugin.consumeDevSession(); if (session) { const { devApp } = await Promise.resolve().then(() => require("./dev-session.cjs")); devApp(session.orchestrator, session.services, session.runtimeConfig); } return; } if (descriptor.key === "start") { const startSpinner = _clack_prompts.spinner(); startSpinner.start("Starting production environment"); const startPhaseLabels = { config: "Preparing config...", "generate artifacts": "Generating code artifacts..." }; const onStartProgress = (event) => { const label = startPhaseLabels[event.phase] ?? event.phase; if (event.status === "running") startSpinner.message(label); }; require_plugin.pluginEvents.on("progress", onStartProgress); let result; try { result = await client.start(input); } finally { require_plugin.pluginEvents.off("progress", onStartProgress); } if (result.status === "error") { startSpinner.stop("Failed"); console.error(`[CLI] ${result.error || "Unknown error"}`); process.exit(1); } startSpinner.stop("Ready"); const session = require_plugin.consumeDevSession(); if (session) { const summary = session.summary; if (summary) printStartSummary(summary); const { startApp } = await Promise.resolve().then(() => require("./dev-session.cjs")); startApp(session.orchestrator, session.services, session.runtimeConfig); } return; } if (descriptor.key === "init") { let initInput = { ...input }; if (!initInput.noInteractive) { const basic = await require_prompts.promptInitBasic({ extends: initInput.extends, account: initInput.account, domain: initInput.domain }); let parentPluginKeys = []; let parentConfig = null; const fetchSpinner = _clack_prompts.spinner(); fetchSpinner.start("Fetching parent config"); try { parentConfig = await require_cli_init.fetchParentConfig(basic.extendsAccount, basic.extendsGateway); if (parentConfig?.plugins && typeof parentConfig.plugins === "object") parentPluginKeys = Object.keys(parentConfig.plugins); } catch { fetchSpinner.stop("Config not found"); console.error(`[CLI] No config found at bos://${basic.extendsAccount}/${basic.extendsGateway}`); process.exit(1); } fetchSpinner.stop("Config fetched"); if (typeof parentConfig?.title === "string" && parentConfig.title.trim() && typeof parentConfig?.description === "string" && parentConfig.description.trim()) { const shouldContinue = await _clack_prompts.confirm({ message: `You will be extending ${parentConfig.title} - ${parentConfig.description}. Continue?`, initialValue: true }); if (_clack_prompts.isCancel(shouldContinue) || !shouldContinue) process.exit(0); } const overrides = await require_prompts.promptInitOverrides({ parentPluginKeys, plugins: initInput.plugins, overrides: initInput.overrides }); const directory = initInput.directory || basic.domain || basic.extendsGateway; initInput = { ...initInput, extends: `bos://${basic.extendsAccount}/${basic.extendsGateway}`, directory, account: basic.account, domain: basic.domain || void 0, plugins: overrides.plugins, overrides: overrides.overrides, noInteractive: true }; } const initSpinner = _clack_prompts.spinner(); initSpinner.start("Initializing project"); const phaseLabels = { "parent config": "Fetching parent config...", "template source": "Resolving template source...", "scaffold project": "Creating project scaffold...", "copy files": "Copying template files...", "personalize config": "Personalizing config...", "write snapshot": "Writing snapshot...", "resolve config": "Resolving config...", "generate env/docker": "Generating environment config...", "create env file": "Creating .env file...", "install dependencies": "Installing dependencies...", "generate types": "Generating types...", "generate migrations": "Generating database migrations...", "generate code artifacts": "Generating code artifacts..." }; const onProgress = (event) => { const label = phaseLabels[event.phase] ?? event.phase; if (event.status === "running") initSpinner.message(label); }; require_plugin.pluginEvents.on("progress", onProgress); let result; try { result = await client.init(initInput); } finally { require_plugin.pluginEvents.off("progress", onProgress); } if (result.status === "error") { initSpinner.stop("Failed"); console.error(`[CLI] ${result.error || "Unknown error"}`); process.exit(1); } initSpinner.stop("Project initialized"); console.log(` ${require_theme.colors.dim("Extends:")} ${result.extends}`); console.log(` ${require_theme.colors.dim("Directory:")} ${result.directory}`); if (result.account) console.log(` ${require_theme.colors.dim("Account:")} ${result.account}`); if (result.domain) console.log(` ${require_theme.colors.dim("Domain:")} ${result.domain}`); if (result.overrides && result.overrides.length > 0) console.log(` ${require_theme.colors.dim("Overrides:")} ${result.overrides.join(", ")}`); if (result.plugins && result.plugins.length > 0) console.log(` ${require_theme.colors.dim("Plugins:")} ${result.plugins.join(", ")}`); console.log(` ${require_theme.colors.dim("Files copied:")} ${result.filesCopied}`); printTimingSummary(result.timings); console.log(); console.log(require_theme.colors.dim(" Next steps:")); console.log(require_theme.colors.dim(` cd ${result.directory}`)); if (!initInput.noInstall) { console.log(require_theme.colors.dim(" docker compose up -d --wait")); console.log(require_theme.colors.dim(" bun run dev")); } else { console.log(require_theme.colors.dim(" bun install")); console.log(require_theme.colors.dim(" docker compose up -d --wait")); console.log(require_theme.colors.dim(" bun run dev")); } console.log(); if (initInput.noInteractive !== true && !initInput.noInstall && result.targetDir) { if (await _clack_prompts.confirm({ message: "Run docker compose up -d --wait?", initialValue: true }) === true) { const dockerSpinner = _clack_prompts.spinner(); dockerSpinner.start("Starting Docker services"); try { await require_cli_init.runDockerComposeUp(result.targetDir); dockerSpinner.stop("Docker services ready"); } catch (error) { dockerSpinner.stop("Docker services not started"); _clack_prompts.log.warn(`docker compose up -d --wait failed: ${error instanceof Error ? error.message : error}`); } } } return; } const result = await client[descriptor.key](input); if (descriptor.key === "config") { if (!result.config) { console.error("No bos.config.json found"); process.exit(1); } printConfigView(result.config); process.stdout.write(`${JSON.stringify(result.config, null, 2)}\n`); return; } if (descriptor.key === "sync") { console.log(); if (result.status === "error") { console.error(`[CLI] ${result.error || "Unknown error"}`); process.exit(1); } if (result.status === "dry-run") console.log(require_theme.colors.cyan(`${require_theme.icons.ok} Dry run — no files written`)); else console.log(require_theme.colors.green(`${require_theme.icons.ok} Template synced`)); if (result.updated.length > 0) { console.log(` ${require_theme.colors.dim("Updated:")} ${result.updated.length} file(s)`); for (const f of result.updated) console.log(` ${require_theme.colors.dim(f)}`); } if (result.added.length > 0) { console.log(` ${require_theme.colors.dim("Added:")} ${result.added.length} file(s)`); for (const f of result.added) console.log(` ${require_theme.colors.dim(f)}`); } if (result.skipped.length > 0) { console.log(` ${require_theme.colors.yellow("Skipped:")} ${result.skipped.length} file(s) (locally modified, use --force to overwrite)`); for (const f of result.skipped) console.log(` ${require_theme.colors.dim(f)}`); } if (result.updated.length === 0 && result.added.length === 0 && result.skipped.length === 0) console.log(` ${require_theme.colors.dim("Already up to date")}`); if (result.status !== "dry-run" && result.updated.length > 0) { console.log(); console.log(require_theme.colors.dim(" Review changes — your customizations take priority:")); console.log(require_theme.colors.dim(" • api/src/contract.ts, api/src/index.ts, api/src/db/schema.ts — never overwritten")); console.log(require_theme.colors.dim(" • ui/src/components/**, ui/src/styles.css — never overwritten")); console.log(require_theme.colors.dim(" • Other updated files — accept framework improvements, then restore your changes")); console.log(require_theme.colors.dim(" • Skipped files — yours already, only update with --force")); } console.log(); return; } if (descriptor.key === "upgrade") { console.log(); if (result.status === "error") { console.error(`[CLI] ${result.error || "Unknown error"}`); process.exit(1); } if (result.status === "dry-run") console.log(require_theme.colors.cyan(`${require_theme.icons.ok} Dry run — no changes applied`)); else console.log(require_theme.colors.green(`${require_theme.icons.ok} Upgrade successful`)); for (const pkg of result.packages) if (pkg.from && pkg.from !== pkg.to) console.log(` ${require_theme.colors.dim(`${pkg.name}:`)} ${pkg.from}${pkg.to}`); else if (!pkg.from) console.log(` ${require_theme.colors.dim(`${pkg.name}:`)} ${pkg.to} (new)`); else console.log(` ${require_theme.colors.dim(`${pkg.name}:`)} ${pkg.to} (up to date)`); if (result.changelogUrl) console.log(` ${require_theme.colors.dim("Changelog:")} ${result.changelogUrl}`); if (result.availablePlugins && result.availablePlugins.length > 0) console.log(` ${require_theme.colors.dim("New parent plugins:")} ${result.availablePlugins.join(", ")}`); if (result.selectedPlugins && result.selectedPlugins.length > 0) console.log(` ${require_theme.colors.dim("Added plugins:")} ${result.selectedPlugins.join(", ")}`); printTimingSummary(result.timings); if (result.sync) { const sync = result.sync; if (sync.updated.length > 0) { console.log(` ${require_theme.colors.dim("Updated:")} ${sync.updated.length} file(s)`); for (const f of sync.updated) console.log(` ${require_theme.colors.dim(f)}`); } if (sync.added.length > 0) { console.log(` ${require_theme.colors.dim("Added:")} ${sync.added.length} file(s)`); for (const f of sync.added) console.log(` ${require_theme.colors.dim(f)}`); } if (sync.skipped.length > 0) { console.log(` ${require_theme.colors.yellow("Skipped:")} ${sync.skipped.length} file(s) (locally modified, use --force to overwrite)`); for (const f of sync.skipped) console.log(` ${require_theme.colors.dim(f)}`); } if (result.status !== "dry-run" && (sync.updated.length > 0 || sync.added.length > 0 || sync.skipped.length > 0)) { console.log(); console.log(require_theme.colors.dim(" Resolve differences — your code takes priority:")); console.log(); console.log(require_theme.colors.dim(" Never overwritten (safe):")); console.log(require_theme.colors.dim(" • api/src/contract.ts, api/src/index.ts, api/src/db/schema.ts")); console.log(require_theme.colors.dim(" • ui/src/components/**, ui/src/styles.css")); console.log(); console.log(require_theme.colors.dim(" Replaced — review and keep your changes:")); console.log(require_theme.colors.dim(" • api/drizzle.config.ts, api/tsconfig.json, api/tsconfig.contract.json")); console.log(require_theme.colors.dim(" • api/plugin.dev.ts, api/rspack.config.js")); console.log(require_theme.colors.dim(" • ui/src/routes/* (core routes only)")); console.log(); console.log(require_theme.colors.dim(" Merged — your deps preserved:")); console.log(require_theme.colors.dim(" • package.json, api/package.json, ui/package.json")); console.log(); console.log(require_theme.colors.dim(" Skipped — already yours:")); console.log(require_theme.colors.dim(" • Use --force only if you want framework updates")); } } if (result.migrated && result.migrated.length > 0) { console.log(` ${require_theme.colors.yellow("Removed:")} ${result.migrated.length} obsolete file(s)`); for (const f of result.migrated) console.log(` ${require_theme.colors.dim(f)}`); } console.log(); return; } if (descriptor.key === "status") { console.log(); if (result.status === "error") { console.error(`[CLI] ${result.error || "Unknown error"}`); process.exit(1); } console.log(require_theme.colors.cyan(require_theme.frames.top(52))); console.log(` ${require_theme.icons.app} ${require_theme.gradients.cyber("STATUS")}`); console.log(require_theme.colors.cyan(require_theme.frames.bottom(52))); console.log(); if (result.extends) console.log(` ${require_theme.colors.dim("Extends:")} ${result.extends}`); if (result.account) console.log(` ${require_theme.colors.dim("Account:")} ${result.account}`); if (result.domain) console.log(` ${require_theme.colors.dim("Domain:")} ${result.domain}`); console.log(); console.log(` ${require_theme.colors.dim("Packages:")}`); for (const pkg of result.packages) { const hasUpdate = pkg.installed && pkg.latest && normalizeVersion(pkg.installed) !== normalizeVersion(pkg.latest); const versionStr = hasUpdate ? `${pkg.installed}${pkg.latest}` : pkg.installed || "not installed"; const label = hasUpdate ? require_theme.colors.yellow(versionStr) : require_theme.colors.dim(versionStr); console.log(` ${require_theme.colors.dim(`${pkg.name}`)} ${label}`); } console.log(); if (result.lastSync) { const ago = formatTimeAgo(result.lastSync); console.log(` ${require_theme.colors.dim("Last sync:")} ${ago}`); } else console.log(` ${require_theme.colors.dim("Last sync:")} never`); const envLabel = result.envFile === "found" ? require_theme.colors.green("found") : result.envFile === "example-only" ? require_theme.colors.yellow("missing (only .env.example found)") : require_theme.colors.error("missing"); console.log(` ${require_theme.colors.dim(".env:")} ${envLabel}`); if (result.parentReachable !== void 0) { const parentLabel = result.parentReachable ? require_theme.colors.green("reachable") : require_theme.colors.error("unreachable"); console.log(` ${require_theme.colors.dim("Parent:")} ${parentLabel}`); } if (result.packages.some((p) => p.installed && p.latest && normalizeVersion(p.installed) !== normalizeVersion(p.latest))) { console.log(); console.log(require_theme.colors.dim(` Run ${require_theme.colors.cyan("bos upgrade")} to update packages and sync template files.`)); } console.log(); return; } if (descriptor.key === "typesGen") { console.log(); if (result.status === "error") { console.error(`[CLI] ${result.error || "Unknown error"}`); process.exit(1); } console.log(require_theme.colors.green(`${require_theme.icons.ok} Types generated`)); if (result.source) console.log(` ${require_theme.colors.dim("Mode:")} ${result.source === "remote" ? require_theme.colors.cyan("remote") : require_theme.colors.dim("local")}`); if (result.generated.length > 0) { console.log(` ${require_theme.colors.dim("Generated:")}`); for (const f of result.generated) console.log(` ${require_theme.colors.dim(f)}`); } if (result.fetched.length > 0) { console.log(` ${require_theme.colors.dim("Fetched from remote:")}`); for (const url of result.fetched) console.log(` ${require_theme.colors.dim(url)}`); } if (result.skipped.length > 0) { console.log(` ${require_theme.colors.dim("Skipped (local):")}`); for (const s of result.skipped) console.log(` ${require_theme.colors.dim(s)}`); } if (result.failed.length > 0) { console.log(` ${require_theme.colors.yellow("Failed:")}`); for (const f of result.failed) console.log(` ${require_theme.colors.error(f)}`); } console.log(); return; } if (result?.status === "error") { console.error(`[CLI] ${result.error || "Unknown error"}`); process.exit(1); } if (descriptor.key === "keyPublish") { process.stdout.write(`Generated publish key for ${result.account}\n`); process.stdout.write(` Network: ${result.network}\n`); process.stdout.write(` Contract: ${result.contract}\n`); process.stdout.write(` Allowance: ${result.allowance}\n`); process.stdout.write(` Functions: ${result.functionNames.join(", ")}\n`); process.stdout.write(` Public key: ${result.publicKey}\n`); process.stdout.write(` Private key: ${result.privateKey}\n`); process.stdout.write(` Copy: NEAR_PRIVATE_KEY=${result.privateKey}\n`); } if (descriptor.key === "pluginAdd") { console.log(); console.log(require_theme.colors.green(`${require_theme.icons.ok} Added plugin ${result.key}`)); if (result.development) console.log(` ${require_theme.colors.dim("Development:")} ${result.development}`); if (result.production) console.log(` ${require_theme.colors.dim("Production:")} ${result.production}`); console.log(); return; } if (descriptor.key === "pluginRemove") { console.log(); console.log(require_theme.colors.green(`${require_theme.icons.ok} Removed plugin ${result.key}`)); console.log(); return; } if (descriptor.key === "pluginList") { console.log(); console.log(require_theme.colors.cyan(require_theme.frames.top(52))); console.log(` ${require_theme.icons.config} ${require_theme.gradients.cyber("PLUGINS")}`); console.log(require_theme.colors.cyan(require_theme.frames.bottom(52))); console.log(); if (result.plugins.length === 0) console.log(require_theme.colors.dim(" No plugins configured")); else for (const pluginItem of result.plugins) { console.log(` ${require_theme.colors.cyan(pluginItem.key)}`); if (pluginItem.development) console.log(` ${require_theme.colors.dim("Development:")} ${pluginItem.development}`); if (pluginItem.production) console.log(` ${require_theme.colors.dim("Production:")} ${pluginItem.production}`); } console.log(); return; } if (descriptor.key === "pluginPublish") { console.log(); console.log(require_theme.colors.green(`${require_theme.icons.ok} Published plugin ${result.key}`)); if (result.path) console.log(` ${require_theme.colors.dim("Path:")} ${result.path}`); if (result.script) console.log(` ${require_theme.colors.dim("Script:")} bun run ${result.script}`); if (result.production) console.log(` ${require_theme.colors.dim("Production:")} ${result.production}`); console.log(); return; } if (descriptor.key === "publish") { if (result.status === "dry-run") { console.log(); console.log(require_theme.colors.cyan(`${require_theme.icons.ok} Dry run complete`)); console.log(` ${require_theme.colors.dim("Registry URL:")} ${result.registryUrl}`); console.log(); return; } if (result.status === "published") { console.log(); console.log(require_theme.colors.green(`${require_theme.icons.ok} Published successfully`)); console.log(` ${require_theme.colors.dim("Registry URL:")} ${result.registryUrl}`); if (result.txHash) console.log(` ${require_theme.colors.dim("Transaction:")} ${result.txHash}`); if (result.built && result.built.length > 0) console.log(` ${require_theme.colors.dim("Built:")} ${result.built.join(", ")}`); if (result.skipped && result.skipped.length > 0) console.log(` ${require_theme.colors.dim("Skipped:")} ${result.skipped.join(", ")}`); console.log(); return; } } } catch (error) { console.error(`[CLI] ${error instanceof Error ? error.message : String(error)}`); process.exit(1); } } main().catch((error) => { console.error("[CLI] Fatal error:", error); process.exit(1); }); //#endregion //# sourceMappingURL=cli.cjs.map