@devdogsuga/devtools 0.1.26 → 0.1.27
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 +72 -11
- package/bin/devtools.mjs +4 -4
- package/dist/cli-CZKkNHK5.js +6619 -0
- package/dist/failure-log-BBPGejQv.js +256 -0
- package/dist/launch.js +267 -281
- package/dist/local-env-D8HC2JK_.js +351 -0
- package/dist/local-env-DrBCXqD8.js +2 -0
- package/dist/menu-CQBsQRgH.js +1684 -0
- package/dist/peer-redirect-hooks.js +15 -0
- package/dist/peers-RBplwovA.js +229 -0
- package/dist/root-C2OmZDwD.js +94 -0
- package/dist/stack-Bx-55oi_.js +2 -0
- package/dist/stack-BynCgRVS.js +181 -0
- package/dist/supabase-project-0knM0mqd.js +426 -0
- package/dist/telemetry-C4foD8MD.js +249 -0
- package/dist/telemetry.js +2 -148
- package/package.json +10 -29
- package/bin/devtools-ci-bare.mjs +0 -48
- package/bin/devtools-ci.mjs +0 -31
- package/dist/args.d.ts +0 -26
- package/dist/args.js +0 -78
- package/dist/bws/bw.d.ts +0 -18
- package/dist/bws/bw.js +0 -70
- package/dist/bws/client.d.ts +0 -43
- package/dist/bws/client.js +0 -363
- package/dist/bws/environments.d.ts +0 -96
- package/dist/bws/environments.js +0 -111
- package/dist/bws/pace.d.ts +0 -25
- package/dist/bws/pace.js +0 -43
- package/dist/bws/retry.d.ts +0 -12
- package/dist/bws/retry.js +0 -39
- package/dist/bws/store.d.ts +0 -1
- package/dist/bws/store.js +0 -32
- package/dist/bws/token.d.ts +0 -31
- package/dist/bws/token.js +0 -88
- package/dist/bws/vault.d.ts +0 -29
- package/dist/bws/vault.js +0 -200
- package/dist/catalog.d.ts +0 -57
- package/dist/catalog.js +0 -57
- package/dist/cf/build.d.ts +0 -13
- package/dist/cf/build.js +0 -65
- package/dist/cf/commands.d.ts +0 -1
- package/dist/cf/commands.js +0 -172
- package/dist/cf/local-env.d.ts +0 -58
- package/dist/cf/local-env.js +0 -177
- package/dist/ci.d.ts +0 -1
- package/dist/ci.js +0 -328
- package/dist/cli.d.ts +0 -11
- package/dist/cli.js +0 -1094
- package/dist/commands.d.ts +0 -261
- package/dist/commands.js +0 -1318
- package/dist/completions.d.ts +0 -4
- package/dist/completions.js +0 -110
- package/dist/cron/commands.d.ts +0 -47
- package/dist/cron/commands.js +0 -372
- package/dist/cron/discovery.d.ts +0 -55
- package/dist/cron/discovery.js +0 -85
- package/dist/cron/schema.d.ts +0 -45
- package/dist/cron/schema.js +0 -35
- package/dist/db/config-push.d.ts +0 -8
- package/dist/db/config-push.js +0 -11
- package/dist/db/connection.d.ts +0 -79
- package/dist/db/connection.js +0 -114
- package/dist/db/exec.d.ts +0 -1
- package/dist/db/exec.js +0 -11
- package/dist/db/generate-types.d.ts +0 -1
- package/dist/db/generate-types.js +0 -4
- package/dist/db/generated-env.d.ts +0 -69
- package/dist/db/generated-env.js +0 -113
- package/dist/db/introspect.d.ts +0 -1
- package/dist/db/introspect.js +0 -122
- package/dist/db/local-env.d.ts +0 -65
- package/dist/db/local-env.js +0 -23
- package/dist/db/new-migration.d.ts +0 -8
- package/dist/db/new-migration.js +0 -69
- package/dist/db/pooler.d.ts +0 -23
- package/dist/db/pooler.js +0 -55
- package/dist/db/run.d.ts +0 -68
- package/dist/db/run.js +0 -156
- package/dist/db/seed-buckets.d.ts +0 -2
- package/dist/db/seed-buckets.js +0 -4
- package/dist/db/seed-production.d.ts +0 -8
- package/dist/db/seed-production.js +0 -79
- package/dist/db/seed-roles.d.ts +0 -7
- package/dist/db/seed-roles.js +0 -33
- package/dist/db/session-refresh.d.ts +0 -43
- package/dist/db/session-refresh.js +0 -40
- package/dist/deploy/migrations.d.ts +0 -20
- package/dist/deploy/migrations.js +0 -68
- package/dist/deploy/orphans.d.ts +0 -20
- package/dist/deploy/orphans.js +0 -177
- package/dist/deploy/preflight.d.ts +0 -53
- package/dist/deploy/preflight.js +0 -189
- package/dist/deploy/report.d.ts +0 -26
- package/dist/deploy/report.js +0 -70
- package/dist/deploy/require-planner.d.ts +0 -2
- package/dist/deploy/require-planner.js +0 -60
- package/dist/deploy/require-token.d.ts +0 -15
- package/dist/deploy/require-token.js +0 -78
- package/dist/deploy/secrets-file.d.ts +0 -17
- package/dist/deploy/secrets-file.js +0 -166
- package/dist/deploy/write-env.d.ts +0 -40
- package/dist/deploy/write-env.js +0 -395
- package/dist/docs/index-pages.d.ts +0 -38
- package/dist/docs/index-pages.js +0 -186
- package/dist/emails/commands.d.ts +0 -15
- package/dist/emails/commands.js +0 -130
- package/dist/emails/email-types.d.ts +0 -13
- package/dist/emails/email-types.js +0 -1
- package/dist/emails/fixtures.d.ts +0 -20
- package/dist/emails/fixtures.js +0 -20
- package/dist/env/audit.d.ts +0 -206
- package/dist/env/audit.js +0 -436
- package/dist/env/cloudflare.d.ts +0 -21
- package/dist/env/cloudflare.js +0 -61
- package/dist/env/commands.d.ts +0 -99
- package/dist/env/commands.js +0 -613
- package/dist/env/discovery.d.ts +0 -15
- package/dist/env/discovery.js +0 -239
- package/dist/env/document.d.ts +0 -119
- package/dist/env/document.js +0 -323
- package/dist/env/example.d.ts +0 -68
- package/dist/env/example.js +0 -605
- package/dist/env/selection.d.ts +0 -105
- package/dist/env/selection.js +0 -228
- package/dist/env-entry.d.ts +0 -49
- package/dist/env-entry.js +0 -204
- package/dist/environment-doctor.d.ts +0 -45
- package/dist/environment-doctor.js +0 -429
- package/dist/environment.d.ts +0 -92
- package/dist/environment.js +0 -185
- package/dist/fingerprint.d.ts +0 -10
- package/dist/fingerprint.js +0 -18
- package/dist/gen/campus-map.d.ts +0 -1
- package/dist/gen/campus-map.js +0 -473
- package/dist/gen/commands.d.ts +0 -1
- package/dist/gen/commands.js +0 -25
- package/dist/gen/hypno.d.ts +0 -1
- package/dist/gen/hypno.js +0 -134
- package/dist/gh/client.d.ts +0 -80
- package/dist/gh/client.js +0 -259
- package/dist/gh/environments.d.ts +0 -83
- package/dist/gh/environments.js +0 -178
- package/dist/gh/rulesets/actors.d.ts +0 -14
- package/dist/gh/rulesets/actors.js +0 -69
- package/dist/gh/rulesets/api.d.ts +0 -15
- package/dist/gh/rulesets/api.js +0 -104
- package/dist/gh/rulesets/commands.d.ts +0 -3
- package/dist/gh/rulesets/commands.js +0 -203
- package/dist/gh/rulesets/desired.d.ts +0 -112
- package/dist/gh/rulesets/desired.js +0 -271
- package/dist/gh/rulesets/diff.d.ts +0 -65
- package/dist/gh/rulesets/diff.js +0 -170
- package/dist/gh/rulesets/types.d.ts +0 -76
- package/dist/gh/rulesets/types.js +0 -13
- package/dist/gh/settings/api.d.ts +0 -40
- package/dist/gh/settings/api.js +0 -180
- package/dist/gh/settings/commands.d.ts +0 -1
- package/dist/gh/settings/commands.js +0 -254
- package/dist/gh/settings/desired.d.ts +0 -53
- package/dist/gh/settings/desired.js +0 -43
- package/dist/gh/settings/diff.d.ts +0 -57
- package/dist/gh/settings/diff.js +0 -161
- package/dist/gh/settings/types.d.ts +0 -92
- package/dist/gh/settings/types.js +0 -11
- package/dist/gh/settings/workflows.d.ts +0 -41
- package/dist/gh/settings/workflows.js +0 -128
- package/dist/grantRoot.d.ts +0 -42
- package/dist/grantRoot.js +0 -89
- package/dist/help.d.ts +0 -18
- package/dist/help.js +0 -169
- package/dist/images/commands.d.ts +0 -32
- package/dist/images/commands.js +0 -231
- package/dist/images/events.d.ts +0 -47
- package/dist/images/events.js +0 -116
- package/dist/images/graphics.d.ts +0 -70
- package/dist/images/graphics.js +0 -234
- package/dist/images/open-graph-types.d.ts +0 -75
- package/dist/images/open-graph-types.js +0 -1
- package/dist/images/prompts.d.ts +0 -34
- package/dist/images/prompts.js +0 -100
- package/dist/images/render.d.ts +0 -21
- package/dist/images/render.js +0 -61
- package/dist/images/select.d.ts +0 -64
- package/dist/images/select.js +0 -96
- package/dist/instance.d.ts +0 -69
- package/dist/instance.js +0 -102
- package/dist/invocation.d.ts +0 -49
- package/dist/invocation.js +0 -78
- package/dist/launch-ci.d.ts +0 -1
- package/dist/launch-ci.js +0 -90
- package/dist/launch.d.ts +0 -18
- package/dist/menu.d.ts +0 -43
- package/dist/menu.js +0 -300
- package/dist/moderation.d.ts +0 -70
- package/dist/moderation.js +0 -128
- package/dist/oauth/browser.d.ts +0 -1
- package/dist/oauth/browser.js +0 -13
- package/dist/oauth/config.d.ts +0 -24
- package/dist/oauth/config.js +0 -24
- package/dist/oauth/connect-url.d.ts +0 -17
- package/dist/oauth/connect-url.js +0 -23
- package/dist/oauth/db.d.ts +0 -37
- package/dist/oauth/db.js +0 -108
- package/dist/oauth/device.d.ts +0 -93
- package/dist/oauth/device.js +0 -198
- package/dist/oauth/discovery.d.ts +0 -28
- package/dist/oauth/discovery.js +0 -54
- package/dist/oauth/env-file.d.ts +0 -6
- package/dist/oauth/env-file.js +0 -35
- package/dist/oauth/exchange.d.ts +0 -43
- package/dist/oauth/exchange.js +0 -93
- package/dist/oauth/label.d.ts +0 -1
- package/dist/oauth/label.js +0 -25
- package/dist/oauth/loopback.d.ts +0 -52
- package/dist/oauth/loopback.js +0 -168
- package/dist/oauth/pkce.d.ts +0 -6
- package/dist/oauth/pkce.js +0 -34
- package/dist/oauth/target.d.ts +0 -54
- package/dist/oauth/target.js +0 -102
- package/dist/oauth/transport.d.ts +0 -41
- package/dist/oauth/transport.js +0 -32
- package/dist/oauth/wizard.d.ts +0 -40
- package/dist/oauth/wizard.js +0 -578
- package/dist/persona.d.ts +0 -18
- package/dist/persona.js +0 -221
- package/dist/pick.d.ts +0 -6
- package/dist/pick.js +0 -64
- package/dist/pipes.d.ts +0 -1
- package/dist/pipes.js +0 -25
- package/dist/planner/checks.d.ts +0 -26
- package/dist/planner/checks.js +0 -104
- package/dist/planner/commands.d.ts +0 -10
- package/dist/planner/commands.js +0 -367
- package/dist/planner/db.d.ts +0 -15
- package/dist/planner/db.js +0 -33
- package/dist/planner/role.d.ts +0 -33
- package/dist/planner/role.js +0 -68
- package/dist/repo/peer-redirect-hooks.d.ts +0 -16
- package/dist/repo/peer-redirect-hooks.js +0 -10
- package/dist/repo/peer-redirect.d.ts +0 -4
- package/dist/repo/peer-redirect.js +0 -26
- package/dist/repo/peers.d.ts +0 -39
- package/dist/repo/peers.js +0 -139
- package/dist/repo/resolve.d.ts +0 -41
- package/dist/repo/resolve.js +0 -152
- package/dist/repo/root.d.ts +0 -23
- package/dist/repo/root.js +0 -91
- package/dist/repo/source.d.ts +0 -14
- package/dist/repo/source.js +0 -54
- package/dist/repo/supabase-project.d.ts +0 -60
- package/dist/repo/supabase-project.js +0 -124
- package/dist/repo/tsx-loader.d.ts +0 -6
- package/dist/repo/tsx-loader.js +0 -41
- package/dist/run/pick.d.ts +0 -90
- package/dist/run/pick.js +0 -664
- package/dist/setup.d.ts +0 -1
- package/dist/setup.js +0 -280
- package/dist/stack.d.ts +0 -48
- package/dist/stack.js +0 -403
- package/dist/telemetry.d.ts +0 -59
- package/dist/tier.d.ts +0 -40
- package/dist/tier.js +0 -96
- package/dist/ui.d.ts +0 -40
- package/dist/ui.js +0 -71
- package/dist/version.d.ts +0 -7
- package/dist/version.js +0 -17
- package/dist/workers.d.ts +0 -5
- package/dist/workers.js +0 -33
- package/dist/workflows/commands.d.ts +0 -54
- package/dist/workflows/commands.js +0 -763
package/dist/environment.js
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What this machine currently is, read once before the menu draws itself.
|
|
3
|
-
*
|
|
4
|
-
* The wizard used to offer the same commands to everyone: someone whose Docker
|
|
5
|
-
* daemon was not running was invited to reset a database that could not
|
|
6
|
-
* answer, and someone whose stack was already up had no way to stop it at all,
|
|
7
|
-
* because "stop" was not in the tree. Both are the same bug. The menu
|
|
8
|
-
* described the CLI rather than the situation.
|
|
9
|
-
*
|
|
10
|
-
* So this reads three facts and `menu.ts` adapts to them. The facts are few
|
|
11
|
-
* and cheap on purpose; see `probeEnvironment` for the budget and why.
|
|
12
|
-
*
|
|
13
|
-
* ## Unknown is not "no"
|
|
14
|
-
*
|
|
15
|
-
* Every probe can come back `"unknown"`, and that is what a failed probe
|
|
16
|
-
* reports rather than guessing. Nothing is hidden or flagged on `"unknown"`: a
|
|
17
|
-
* machine this cannot read gets the full menu, exactly as before. A tool that
|
|
18
|
-
* hides a command because a `docker ps` timed out is worse than one that never
|
|
19
|
-
* adapted at all, because the command it hides is the one you were looking
|
|
20
|
-
* for.
|
|
21
|
-
*/
|
|
22
|
-
import { execFileSync } from "node:child_process";
|
|
23
|
-
import { existsSync } from "node:fs";
|
|
24
|
-
import { join } from "node:path";
|
|
25
|
-
import { findRepoRoot } from "./repo/root.js";
|
|
26
|
-
import { containerPrefix, readProjectId } from "./repo/supabase-project.js";
|
|
27
|
-
/**
|
|
28
|
-
* The repo root, which is where `supabase/config.toml` and `.env` live.
|
|
29
|
-
*
|
|
30
|
-
* Used to be a fixed `join(import.meta.dirname, "..", "..", "..")` — that
|
|
31
|
-
* only worked because devtools lived inside the repo it operated on. Now
|
|
32
|
-
* published as its own package, every former reader of that constant calls
|
|
33
|
-
* `findRepoRoot()` (re-exported here for callers that already import from
|
|
34
|
-
* this module) — see `repo/root.ts` for the marker and why it is a lazy,
|
|
35
|
-
* throwing function rather than a constant.
|
|
36
|
-
*/
|
|
37
|
-
export { findRepoRoot };
|
|
38
|
-
/**
|
|
39
|
-
* The environment that adapts nothing.
|
|
40
|
-
*
|
|
41
|
-
* What the tests use, and what any caller that would rather not probe can
|
|
42
|
-
* pass. Because nothing is hidden or flagged on `"unknown"`, this is exactly
|
|
43
|
-
* the pre-adaptation menu, which makes it a safe fallback rather than a
|
|
44
|
-
* special case to remember.
|
|
45
|
-
*/
|
|
46
|
-
export const UNKNOWN_ENVIRONMENT = {
|
|
47
|
-
docker: "unknown",
|
|
48
|
-
stack: "unknown",
|
|
49
|
-
envFile: "unknown",
|
|
50
|
-
};
|
|
51
|
-
// ── Probes ───────────────────────────────────────────────────────────────────
|
|
52
|
-
/**
|
|
53
|
-
* Every probe is bounded, silent, and total.
|
|
54
|
-
*
|
|
55
|
-
* `timeout` because a Docker daemon that is starting up (or a VM that has just
|
|
56
|
-
* been resumed) accepts the connection and then never answers, and the failure
|
|
57
|
-
* that produces is a menu that hangs before printing anything, with no output
|
|
58
|
-
* to explain what it is waiting for.
|
|
59
|
-
*/
|
|
60
|
-
function run(file, args) {
|
|
61
|
-
try {
|
|
62
|
-
return execFileSync(file, args, {
|
|
63
|
-
cwd: findRepoRoot(),
|
|
64
|
-
encoding: "utf8",
|
|
65
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
66
|
-
timeout: 3000,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Reads the machine.
|
|
75
|
-
*
|
|
76
|
-
* Two subprocesses at worst, one when Docker is down, plus an `existsSync`.
|
|
77
|
-
* The budget is "cheaper than the first keystroke", because this runs before
|
|
78
|
-
* the wizard's first screen and anything slower would be paid on every
|
|
79
|
-
* invocation to save a few of them later.
|
|
80
|
-
*
|
|
81
|
-
* It does NOT call `supabase status`, which is the authoritative answer but
|
|
82
|
-
* takes the better part of a second: it shells out through `pnpm exec` and
|
|
83
|
-
* then talks to every service in the stack. `docker ps` answers the only
|
|
84
|
-
* question the menu has (is it up?) in a fraction of that, and the commands
|
|
85
|
-
* that need real credentials go through `resolveInstance` (`instance.ts`),
|
|
86
|
-
* which reads the session's already-entered env at the moment it matters.
|
|
87
|
-
*/
|
|
88
|
-
export function probeEnvironment(execute = run) {
|
|
89
|
-
const envFile = existsSync(join(findRepoRoot(), ".env"))
|
|
90
|
-
? "yes"
|
|
91
|
-
: "no";
|
|
92
|
-
// `docker info` rather than `docker version`: version answers from the
|
|
93
|
-
// client alone, so it reports success against a daemon that is not running.
|
|
94
|
-
const docker = execute("docker", ["info"]) === null ? "no" : "yes";
|
|
95
|
-
// A stack cannot be up without a daemon to hold it, so this needs no second
|
|
96
|
-
// subprocess to say "no", and asking anyway on a machine with no Docker
|
|
97
|
-
// installed would pay the whole timeout for an answer already in hand.
|
|
98
|
-
if (docker === "no")
|
|
99
|
-
return { docker, stack: "no", envFile };
|
|
100
|
-
const names = execute("docker", ["ps", "--format", "{{.Names}}"]);
|
|
101
|
-
if (names === null)
|
|
102
|
-
return { docker, stack: "unknown", envFile };
|
|
103
|
-
const prefix = containerPrefix(readProjectId(findRepoRoot()));
|
|
104
|
-
const running = names
|
|
105
|
-
.split("\n")
|
|
106
|
-
.some((name) => name.trim().startsWith(prefix));
|
|
107
|
-
return { docker, stack: running ? "yes" : "no", envFile };
|
|
108
|
-
}
|
|
109
|
-
// ── Conditions ───────────────────────────────────────────────────────────────
|
|
110
|
-
/**
|
|
111
|
-
* Whether a `Condition` from the command tree currently holds.
|
|
112
|
-
*
|
|
113
|
-
* The tree declares conditions as strings so that it stays inert data the docs
|
|
114
|
-
* build can render (see `commands.ts`); this is the one place that knows what
|
|
115
|
-
* they mean. `"unknown"` propagates rather than collapsing to false, and both
|
|
116
|
-
* callers below read it as "do nothing".
|
|
117
|
-
*/
|
|
118
|
-
export function holds(condition, env) {
|
|
119
|
-
switch (condition) {
|
|
120
|
-
case "docker":
|
|
121
|
-
return env.docker;
|
|
122
|
-
case "instance-running":
|
|
123
|
-
return env.stack;
|
|
124
|
-
case "instance-stopped":
|
|
125
|
-
// The negation of an unreadable fact is still unreadable.
|
|
126
|
-
if (env.stack === "unknown")
|
|
127
|
-
return "unknown";
|
|
128
|
-
return env.stack === "no" ? "yes" : "no";
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
/** What `needs` renders as when it does not hold. Kept short: it is a hint. */
|
|
132
|
-
const UNMET = {
|
|
133
|
-
docker: "Docker is not running",
|
|
134
|
-
"instance-running": "Supabase is not running on this machine",
|
|
135
|
-
"instance-stopped": "Supabase is already running on this machine",
|
|
136
|
-
};
|
|
137
|
-
/**
|
|
138
|
-
* Whether the wizard should offer this command at all.
|
|
139
|
-
*
|
|
140
|
-
* False only for a command whose `when` is definitively unmet: `stop`
|
|
141
|
-
* against a stack that is already stopped. This is reserved for commands that
|
|
142
|
-
* would be *meaningless*, never merely inconvenient: one that would fail with
|
|
143
|
-
* a good error message stays in the menu and carries `needs` instead, because
|
|
144
|
-
* a contributor who cannot find a command they know exists is worse off than
|
|
145
|
-
* one who runs it and is told why it did not work.
|
|
146
|
-
*
|
|
147
|
-
* Hiding is a wizard-only decision. `--help` still lists these, the dispatcher
|
|
148
|
-
* still accepts them, and the generated reference still documents them. The
|
|
149
|
-
* menu is a guide to right now, and those three are the reference.
|
|
150
|
-
*/
|
|
151
|
-
export function isOffered(node, env) {
|
|
152
|
-
return node.when === undefined || holds(node.when, env) !== "no";
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Why this command will not work right now, or `null` if nothing is in the way.
|
|
156
|
-
*
|
|
157
|
-
* Appended to the entry's hint, so the menu says "needs Docker, which is not
|
|
158
|
-
* running" on the line itself, rather than after the reader has chosen it and
|
|
159
|
-
* waited for a connection attempt to fail.
|
|
160
|
-
*/
|
|
161
|
-
export function blockedBecause(node, env) {
|
|
162
|
-
if (!node.needs)
|
|
163
|
-
return null;
|
|
164
|
-
return holds(node.needs, env) === "no" ? UNMET[node.needs] : null;
|
|
165
|
-
}
|
|
166
|
-
// ── Reporting ────────────────────────────────────────────────────────────────
|
|
167
|
-
const LABELS = {
|
|
168
|
-
yes: "yes",
|
|
169
|
-
no: "no",
|
|
170
|
-
unknown: "could not tell",
|
|
171
|
-
};
|
|
172
|
-
/**
|
|
173
|
-
* The three facts, as the wizard's opening note and as `status --local`.
|
|
174
|
-
*
|
|
175
|
-
* Printed before the first question rather than discovered through failures:
|
|
176
|
-
* "Docker running no" at the top of the screen is the whole explanation for
|
|
177
|
-
* why the database commands below it are flagged, and it costs one glance.
|
|
178
|
-
*/
|
|
179
|
-
export function describeEnvironment(env) {
|
|
180
|
-
return [
|
|
181
|
-
`Docker running ${LABELS[env.docker]}`,
|
|
182
|
-
`Local stack up ${LABELS[env.stack]}`,
|
|
183
|
-
`.env present ${LABELS[env.envFile]}`,
|
|
184
|
-
].join("\n");
|
|
185
|
-
}
|
package/dist/fingerprint.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A fingerprint, for showing that a value changed without showing the value.
|
|
3
|
-
*
|
|
4
|
-
* Length plus first and last character. Enough to tell "I pasted the wrong
|
|
5
|
-
* token" from "the token rotated", and not enough to reconstruct anything. The
|
|
6
|
-
* whole point of these commands is to move secrets without printing them, and a
|
|
7
|
-
* diff that renders values would undo that in the one place people paste output
|
|
8
|
-
* into a chat window.
|
|
9
|
-
*/
|
|
10
|
-
export declare function fingerprint(value: string): string;
|
package/dist/fingerprint.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A fingerprint, for showing that a value changed without showing the value.
|
|
3
|
-
*
|
|
4
|
-
* Length plus first and last character. Enough to tell "I pasted the wrong
|
|
5
|
-
* token" from "the token rotated", and not enough to reconstruct anything. The
|
|
6
|
-
* whole point of these commands is to move secrets without printing them, and a
|
|
7
|
-
* diff that renders values would undo that in the one place people paste output
|
|
8
|
-
* into a chat window.
|
|
9
|
-
*/
|
|
10
|
-
export function fingerprint(value) {
|
|
11
|
-
if (value === "")
|
|
12
|
-
return "empty";
|
|
13
|
-
// Short values would be largely reconstructable from first/last, so they get
|
|
14
|
-
// no description at all.
|
|
15
|
-
if (value.length <= 4)
|
|
16
|
-
return `${value.length} chars`;
|
|
17
|
-
return `${value.length} chars, ${value[0]}…${value[value.length - 1]}`;
|
|
18
|
-
}
|
package/dist/gen/campus-map.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function runGenCampusMap(): Promise<number>;
|
package/dist/gen/campus-map.js
DELETED
|
@@ -1,473 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `devtools gen campus-map`
|
|
3
|
-
*
|
|
4
|
-
* Regenerates `apps/platform/src/components/EventsSection/FindUs/campusMapData.ts`
|
|
5
|
-
* (and the few bytes in `campusMapMeta.ts` beside it) from live OpenStreetMap
|
|
6
|
-
* data.
|
|
7
|
-
*
|
|
8
|
-
* The Directions dialog's campus map is real geometry, not a sketch: every
|
|
9
|
-
* building footprint and road in the frame, pulled via Overpass and projected
|
|
10
|
-
* equirectangular into an SVG viewBox. The DLW's own footprint is exported
|
|
11
|
-
* separately so the component can highlight it, along with its centroid in
|
|
12
|
-
* lat/lon, the pin the Google/Apple Maps links drop, since the building is too
|
|
13
|
-
* new for a name search to resolve reliably. Rerun this when OSM improves the
|
|
14
|
-
* area (the DLW is brand new) or to reframe.
|
|
15
|
-
*
|
|
16
|
-
* Street names come out of here already placed, on the centreline they name and
|
|
17
|
-
* turned to match it. Building labels do not: they live in CampusMap.tsx,
|
|
18
|
-
* positioned by eye, because a building's name belongs beside its footprint and
|
|
19
|
-
* which side is free depends on its neighbours. So re-check those against the
|
|
20
|
-
* rendered map after regenerating. The command prints every labelled building's
|
|
21
|
-
* centroid and every street's placement to help.
|
|
22
|
-
*
|
|
23
|
-
* Two output files because the map is loaded on demand. The path strings are the
|
|
24
|
-
* weight, so they live in campusMapData.ts behind a dynamic import; the viewBox
|
|
25
|
-
* and the DLW's coordinates, needed by the eagerly loaded dialog for the
|
|
26
|
-
* placeholder and the map links, go in campusMapMeta.ts.
|
|
27
|
-
*
|
|
28
|
-
* OSM data is ODbL, so the map must keep its visible "© OpenStreetMap"
|
|
29
|
-
* attribution (FindUs.tsx renders it in the SVG corner).
|
|
30
|
-
*/
|
|
31
|
-
import { writeFileSync } from "node:fs";
|
|
32
|
-
import { join } from "node:path";
|
|
33
|
-
import { findRepoRoot } from "../repo/root.js";
|
|
34
|
-
/** Query bbox (S, W, N, E), generous so roads at the frame's edge arrive whole
|
|
35
|
-
* instead of clipped mid-way. */
|
|
36
|
-
const QUERY_BBOX = [33.9355, -83.39, 33.957, -83.363];
|
|
37
|
-
const S = 33.9372, N = 33.9552, W = -83.3861, E = -83.3668;
|
|
38
|
-
const VW = 480;
|
|
39
|
-
const MAJOR_HIGHWAYS = new Set([
|
|
40
|
-
"primary",
|
|
41
|
-
"secondary",
|
|
42
|
-
"tertiary",
|
|
43
|
-
"residential",
|
|
44
|
-
"unclassified",
|
|
45
|
-
]);
|
|
46
|
-
const MINOR_HIGHWAYS = new Set(["service", "pedestrian"]);
|
|
47
|
-
const HIGHLIGHTS = [
|
|
48
|
-
{ key: "DLW", osm: "Dining, Learning and Well-being Center", via: "way" },
|
|
49
|
-
{ key: "Driftmier", osm: "Driftmier Engineering Center", via: "relation" },
|
|
50
|
-
{ key: "Plant Sciences", osm: "Miller Plant Science", via: "way" },
|
|
51
|
-
{ key: "Boyd", osm: "Boyd Graduate Research Center", via: "way" },
|
|
52
|
-
{ key: "MLC", osm: "Zell B. Miller Student Learning Center", via: "way" },
|
|
53
|
-
{
|
|
54
|
-
key: "Science Learning Center",
|
|
55
|
-
osm: "Science Learning Center",
|
|
56
|
-
via: "way",
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
key: "Science Library",
|
|
60
|
-
osm: "Shirley Mathis McBay Science Library",
|
|
61
|
-
via: "way",
|
|
62
|
-
},
|
|
63
|
-
{ key: "Poultry Science", osm: "Poultry Science Building", via: "way" },
|
|
64
|
-
{ key: "Main Library", osm: "UGA Main Library", via: "way" },
|
|
65
|
-
{ key: "Tate", osm: "Tate Student Center", via: "way" },
|
|
66
|
-
];
|
|
67
|
-
const LANDMARKS = [
|
|
68
|
-
"Brumby Hall",
|
|
69
|
-
"Russell Hall",
|
|
70
|
-
"Creswell Hall",
|
|
71
|
-
"Bolton Dining Commons",
|
|
72
|
-
"Tate Center Parking Deck",
|
|
73
|
-
"Oglethorpe House",
|
|
74
|
-
"Physics Building",
|
|
75
|
-
"Snelling Dining Commons",
|
|
76
|
-
"Journalism Building",
|
|
77
|
-
"Sanford Stadium",
|
|
78
|
-
"Stegeman Coliseum",
|
|
79
|
-
"Aderhold Hall",
|
|
80
|
-
"Food Science Building",
|
|
81
|
-
"Paul D. Coverdell Center",
|
|
82
|
-
"Conner Hall",
|
|
83
|
-
"Memorial Hall",
|
|
84
|
-
];
|
|
85
|
-
const STREETS = [
|
|
86
|
-
{ osm: "Baxter Street", text: "Baxter St", at: 0.35 },
|
|
87
|
-
{ osm: "South Lumpkin Street", text: "S. Lumpkin St", at: 0.65 },
|
|
88
|
-
{ osm: "East Campus Road", text: "East Campus Rd", at: 0.5 },
|
|
89
|
-
{ osm: "East Cloverhurst Avenue", text: "E. Cloverhurst Ave", at: 0.35 },
|
|
90
|
-
{ osm: "Sanford Drive", text: "Sanford Dr", at: 0.5 },
|
|
91
|
-
{ osm: "D. W. Brooks Drive", text: "D.W. Brooks Dr", at: 0.35 },
|
|
92
|
-
];
|
|
93
|
-
function makeProjection() {
|
|
94
|
-
const latMid = (S + N) / 2;
|
|
95
|
-
const kx = Math.cos((latMid * Math.PI) / 180);
|
|
96
|
-
const wDeg = (E - W) * kx;
|
|
97
|
-
const hDeg = N - S;
|
|
98
|
-
const VH = Math.round((VW * hDeg) / wDeg);
|
|
99
|
-
const px = (lon, lat) => [
|
|
100
|
-
(((lon - W) * kx) / wDeg) * VW,
|
|
101
|
-
((N - lat) / hDeg) * VH,
|
|
102
|
-
];
|
|
103
|
-
const fmt = (n) => Math.round(n * 10) / 10;
|
|
104
|
-
return { VH, px, fmt };
|
|
105
|
-
}
|
|
106
|
-
function simplify(pts) {
|
|
107
|
-
const kept = [pts[0]];
|
|
108
|
-
for (const p of pts.slice(1)) {
|
|
109
|
-
const [lx, ly] = kept[kept.length - 1];
|
|
110
|
-
if (Math.hypot(p[0] - lx, p[1] - ly) > 2.5)
|
|
111
|
-
kept.push(p);
|
|
112
|
-
}
|
|
113
|
-
const last = pts[pts.length - 1];
|
|
114
|
-
const tail = kept[kept.length - 1];
|
|
115
|
-
if (tail[0] !== last[0] || tail[1] !== last[1])
|
|
116
|
-
kept.push(last);
|
|
117
|
-
return kept;
|
|
118
|
-
}
|
|
119
|
-
function makeGeomHelpers(px, fmt, VH) {
|
|
120
|
-
const proj = (geom) => geom.map((g) => px(g.lon, g.lat).map(fmt));
|
|
121
|
-
const toPath = (geom, close) => "M" +
|
|
122
|
-
simplify(proj(geom))
|
|
123
|
-
.map((p) => p.join(" "))
|
|
124
|
-
.join("L") +
|
|
125
|
-
(close ? "Z" : "");
|
|
126
|
-
const inFrame = (geom) => geom.some((g) => {
|
|
127
|
-
const [x, y] = px(g.lon, g.lat);
|
|
128
|
-
return x > -8 && x < VW + 8 && y > -8 && y < VH + 8;
|
|
129
|
-
});
|
|
130
|
-
const area = (geom) => {
|
|
131
|
-
const pts = proj(geom);
|
|
132
|
-
let a = 0;
|
|
133
|
-
for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) {
|
|
134
|
-
a += (pts[j][0] + pts[i][0]) * (pts[j][1] - pts[i][1]);
|
|
135
|
-
}
|
|
136
|
-
return Math.abs(a / 2);
|
|
137
|
-
};
|
|
138
|
-
return { proj, toPath, inFrame, area };
|
|
139
|
-
}
|
|
140
|
-
const lineLength = (line) => line
|
|
141
|
-
.slice(1)
|
|
142
|
-
.reduce((a, p, i) => a + Math.hypot(p[0] - line[i][0], p[1] - line[i][1]), 0);
|
|
143
|
-
function chain(lines) {
|
|
144
|
-
const key = (p) => `${p[0]},${p[1]}`;
|
|
145
|
-
const pool = lines.filter((l) => l.length > 1);
|
|
146
|
-
const runs = [];
|
|
147
|
-
while (pool.length > 0) {
|
|
148
|
-
const run = pool.pop();
|
|
149
|
-
for (let grew = true; grew;) {
|
|
150
|
-
grew = false;
|
|
151
|
-
for (let i = 0; i < pool.length; i++) {
|
|
152
|
-
const c = pool[i];
|
|
153
|
-
const head = key(run[0]);
|
|
154
|
-
const tail = key(run[run.length - 1]);
|
|
155
|
-
if (tail === key(c[0]))
|
|
156
|
-
run.push(...c.slice(1));
|
|
157
|
-
else if (tail === key(c[c.length - 1]))
|
|
158
|
-
run.push(...c.slice(0, -1).reverse());
|
|
159
|
-
else if (head === key(c[c.length - 1]))
|
|
160
|
-
run.unshift(...c.slice(0, -1));
|
|
161
|
-
else if (head === key(c[0]))
|
|
162
|
-
run.unshift(...c.slice(1).reverse());
|
|
163
|
-
else
|
|
164
|
-
continue;
|
|
165
|
-
pool.splice(i, 1);
|
|
166
|
-
grew = true;
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
runs.push(run);
|
|
171
|
-
}
|
|
172
|
-
return runs;
|
|
173
|
-
}
|
|
174
|
-
function clipToFrame(line, VH, margin = 8) {
|
|
175
|
-
const [minX, minY] = [margin, margin];
|
|
176
|
-
const [maxX, maxY] = [VW - margin, VH - margin];
|
|
177
|
-
const runs = [];
|
|
178
|
-
let run = [];
|
|
179
|
-
const flush = () => {
|
|
180
|
-
if (run.length > 1)
|
|
181
|
-
runs.push(run);
|
|
182
|
-
run = [];
|
|
183
|
-
};
|
|
184
|
-
for (let i = 1; i < line.length; i++) {
|
|
185
|
-
const a = line[i - 1];
|
|
186
|
-
const b = line[i];
|
|
187
|
-
const [dx, dy] = [b[0] - a[0], b[1] - a[1]];
|
|
188
|
-
let t0 = 0, t1 = 1, hit = true;
|
|
189
|
-
for (const [p, q] of [
|
|
190
|
-
[-dx, a[0] - minX],
|
|
191
|
-
[dx, maxX - a[0]],
|
|
192
|
-
[-dy, a[1] - minY],
|
|
193
|
-
[dy, maxY - a[1]],
|
|
194
|
-
]) {
|
|
195
|
-
if (p === 0) {
|
|
196
|
-
if (q < 0)
|
|
197
|
-
hit = false;
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
const r = q / p;
|
|
201
|
-
if (p < 0 ? r > t1 : r < t0)
|
|
202
|
-
hit = false;
|
|
203
|
-
else if (p < 0)
|
|
204
|
-
t0 = Math.max(t0, r);
|
|
205
|
-
else
|
|
206
|
-
t1 = Math.min(t1, r);
|
|
207
|
-
if (!hit)
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
210
|
-
if (!hit) {
|
|
211
|
-
flush();
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
const at = (t) => [a[0] + dx * t, a[1] + dy * t];
|
|
215
|
-
if (t0 > 0)
|
|
216
|
-
flush();
|
|
217
|
-
if (run.length === 0)
|
|
218
|
-
run.push(at(t0));
|
|
219
|
-
run.push(at(t1));
|
|
220
|
-
if (t1 < 1)
|
|
221
|
-
flush();
|
|
222
|
-
}
|
|
223
|
-
flush();
|
|
224
|
-
return runs;
|
|
225
|
-
}
|
|
226
|
-
function placeAlong(run, frac, span, fmt) {
|
|
227
|
-
const seg = run
|
|
228
|
-
.slice(1)
|
|
229
|
-
.map((p, i) => Math.hypot(p[0] - run[i][0], p[1] - run[i][1]));
|
|
230
|
-
const total = seg.reduce((a, b) => a + b, 0);
|
|
231
|
-
const at = (dist) => {
|
|
232
|
-
let d = Math.max(0, Math.min(total, dist));
|
|
233
|
-
for (let i = 0; i < seg.length; i++) {
|
|
234
|
-
if (d <= seg[i]) {
|
|
235
|
-
const t = seg[i] === 0 ? 0 : d / seg[i];
|
|
236
|
-
return [
|
|
237
|
-
run[i][0] + (run[i + 1][0] - run[i][0]) * t,
|
|
238
|
-
run[i][1] + (run[i + 1][1] - run[i][1]) * t,
|
|
239
|
-
];
|
|
240
|
-
}
|
|
241
|
-
d -= seg[i];
|
|
242
|
-
}
|
|
243
|
-
return run[run.length - 1];
|
|
244
|
-
};
|
|
245
|
-
const mid = total * frac;
|
|
246
|
-
const half = Math.min(span, total) / 2;
|
|
247
|
-
const [x, y] = at(mid);
|
|
248
|
-
const [ax, ay] = at(mid - half);
|
|
249
|
-
const [bx, by] = at(mid + half);
|
|
250
|
-
let angle = (Math.atan2(by - ay, bx - ax) * 180) / Math.PI;
|
|
251
|
-
if (angle > 90)
|
|
252
|
-
angle -= 180;
|
|
253
|
-
if (angle < -90)
|
|
254
|
-
angle += 180;
|
|
255
|
-
return { x: fmt(x), y: fmt(y), angle: fmt(angle) };
|
|
256
|
-
}
|
|
257
|
-
async function overpass(query) {
|
|
258
|
-
for (let attempt = 0; attempt < 4; attempt++) {
|
|
259
|
-
if (attempt > 0)
|
|
260
|
-
await new Promise((r) => setTimeout(r, 8000 * attempt));
|
|
261
|
-
const res = await fetch("https://overpass-api.de/api/interpreter", {
|
|
262
|
-
method: "POST",
|
|
263
|
-
headers: {
|
|
264
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
265
|
-
"User-Agent": "devdogs-platform-campus-map (devdogsuga.org)",
|
|
266
|
-
},
|
|
267
|
-
body: "data=" + encodeURIComponent(query),
|
|
268
|
-
});
|
|
269
|
-
const text = await res.text();
|
|
270
|
-
if (!res.ok || text.startsWith("<")) {
|
|
271
|
-
console.warn(` overpass attempt ${attempt + 1} failed (${res.status})${text.startsWith("<") ? " — XML error body" : ""}`);
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
return JSON.parse(text).elements;
|
|
275
|
-
}
|
|
276
|
-
throw new Error("Overpass did not answer after 4 attempts");
|
|
277
|
-
}
|
|
278
|
-
async function fetchOsm() {
|
|
279
|
-
const bbox = QUERY_BBOX.join(",");
|
|
280
|
-
const ways = await overpass(`[out:json][timeout:90];
|
|
281
|
-
(
|
|
282
|
-
way["highway"~"${[...MAJOR_HIGHWAYS, ...MINOR_HIGHWAYS].join("|")}"](${bbox});
|
|
283
|
-
way["building"](${bbox});
|
|
284
|
-
way["leisure"="stadium"](${bbox});
|
|
285
|
-
);
|
|
286
|
-
out geom tags;`);
|
|
287
|
-
const relWays = await overpass(`[out:json][timeout:90];
|
|
288
|
-
relation["building"](${bbox});
|
|
289
|
-
way(r);
|
|
290
|
-
out geom;`);
|
|
291
|
-
const seen = new Set(ways.map((w) => w.id));
|
|
292
|
-
for (const w of relWays) {
|
|
293
|
-
if (seen.has(w.id))
|
|
294
|
-
continue;
|
|
295
|
-
seen.add(w.id);
|
|
296
|
-
ways.push({ ...w, tags: { ...w.tags, building: "yes" } });
|
|
297
|
-
}
|
|
298
|
-
return ways;
|
|
299
|
-
}
|
|
300
|
-
async function fetchRelationWays(name) {
|
|
301
|
-
return overpass(`[out:json][timeout:90];
|
|
302
|
-
relation["building"]["name"="${name}"](${QUERY_BBOX.join(",")});
|
|
303
|
-
way(r);
|
|
304
|
-
out geom;`);
|
|
305
|
-
}
|
|
306
|
-
function centroid(geom) {
|
|
307
|
-
let a = 0, cx = 0, cy = 0;
|
|
308
|
-
for (let i = 0, j = geom.length - 1; i < geom.length; j = i++) {
|
|
309
|
-
const p = geom[j], q = geom[i];
|
|
310
|
-
const f = p.lon * q.lat - q.lon * p.lat;
|
|
311
|
-
a += f;
|
|
312
|
-
cx += (p.lon + q.lon) * f;
|
|
313
|
-
cy += (p.lat + q.lat) * f;
|
|
314
|
-
}
|
|
315
|
-
const round = (n) => Math.round(n * 1e6) / 1e6;
|
|
316
|
-
return { lat: round(cy / (3 * a)), lon: round(cx / (3 * a)) };
|
|
317
|
-
}
|
|
318
|
-
export async function runGenCampusMap() {
|
|
319
|
-
const { VH, px, fmt } = makeProjection();
|
|
320
|
-
const { proj, toPath, inFrame, area } = makeGeomHelpers(px, fmt, VH);
|
|
321
|
-
console.log("fetching OSM…");
|
|
322
|
-
const elements = await fetchOsm();
|
|
323
|
-
const roadPaths = { major: [], minor: [] };
|
|
324
|
-
const namedRoads = new Map();
|
|
325
|
-
for (const e of elements) {
|
|
326
|
-
const hw = e.tags?.highway;
|
|
327
|
-
if (!hw || !inFrame(e.geometry))
|
|
328
|
-
continue;
|
|
329
|
-
if (e.tags?.area === "yes")
|
|
330
|
-
continue;
|
|
331
|
-
if (MAJOR_HIGHWAYS.has(hw))
|
|
332
|
-
roadPaths.major.push(toPath(e.geometry, false));
|
|
333
|
-
else if (MINOR_HIGHWAYS.has(hw))
|
|
334
|
-
roadPaths.minor.push(toPath(e.geometry, false));
|
|
335
|
-
const name = e.tags?.name;
|
|
336
|
-
if (name === undefined)
|
|
337
|
-
continue;
|
|
338
|
-
const parts = namedRoads.get(name) ?? [];
|
|
339
|
-
parts.push(proj(e.geometry));
|
|
340
|
-
namedRoads.set(name, parts);
|
|
341
|
-
}
|
|
342
|
-
const roadLabels = STREETS.map((street) => {
|
|
343
|
-
const parts = namedRoads.get(street.osm);
|
|
344
|
-
if (parts === undefined)
|
|
345
|
-
throw new Error(`no OSM way named "${street.osm}" reaches the frame`);
|
|
346
|
-
const runs = chain(parts).flatMap((line) => clipToFrame(line, VH));
|
|
347
|
-
if (runs.length === 0)
|
|
348
|
-
throw new Error(`"${street.osm}" has no centreline inside the frame`);
|
|
349
|
-
const longest = runs.reduce((a, r) => lineLength(r) > lineLength(a) ? r : a);
|
|
350
|
-
const width = street.text.length * 3.6;
|
|
351
|
-
if (lineLength(longest) < width)
|
|
352
|
-
console.warn(` ! "${street.text}" is wider than its longest visible run (${fmt(lineLength(longest))}px) — it will overhang both ends`);
|
|
353
|
-
return {
|
|
354
|
-
text: street.text,
|
|
355
|
-
...placeAlong(longest, street.at, width, fmt),
|
|
356
|
-
run: fmt(lineLength(longest)),
|
|
357
|
-
};
|
|
358
|
-
});
|
|
359
|
-
const footprints = [];
|
|
360
|
-
for (const e of elements) {
|
|
361
|
-
if (!(e.tags?.building ?? e.tags?.leisure === "stadium"))
|
|
362
|
-
continue;
|
|
363
|
-
if (!inFrame(e.geometry) || area(e.geometry) < 12)
|
|
364
|
-
continue;
|
|
365
|
-
footprints.push(toPath(e.geometry, true));
|
|
366
|
-
}
|
|
367
|
-
const highlights = {};
|
|
368
|
-
for (const b of HIGHLIGHTS) {
|
|
369
|
-
const geoms = b.via === "relation"
|
|
370
|
-
? (await fetchRelationWays(b.osm)).map((w) => w.geometry)
|
|
371
|
-
: elements
|
|
372
|
-
.filter((e) => (e.tags?.building ?? e.tags?.leisure === "stadium") &&
|
|
373
|
-
e.tags?.name === b.osm)
|
|
374
|
-
.map((e) => e.geometry);
|
|
375
|
-
if (geoms.length === 0)
|
|
376
|
-
throw new Error(`no OSM footprint for "${b.osm}"`);
|
|
377
|
-
const biggest = geoms.reduce((a, g) => (area(g) > area(a) ? g : a));
|
|
378
|
-
const center = centroid(biggest);
|
|
379
|
-
const [pinX] = px(center.lon, center.lat).map(fmt);
|
|
380
|
-
const rings = geoms.map((g) => simplify(proj(g)));
|
|
381
|
-
const ys = rings.flat().map((pt) => pt[1]);
|
|
382
|
-
const crossings = rings.flatMap((ring) => {
|
|
383
|
-
const hits = [];
|
|
384
|
-
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
385
|
-
const [ax, ay] = ring[j];
|
|
386
|
-
const [bx, by] = ring[i];
|
|
387
|
-
if (ax === bx)
|
|
388
|
-
continue;
|
|
389
|
-
if (pinX < Math.min(ax, bx) || pinX >= Math.max(ax, bx))
|
|
390
|
-
continue;
|
|
391
|
-
hits.push(ay + ((pinX - ax) / (bx - ax)) * (by - ay));
|
|
392
|
-
}
|
|
393
|
-
return hits;
|
|
394
|
-
});
|
|
395
|
-
const spans = crossings.length > 0 ? crossings : ys;
|
|
396
|
-
highlights[b.key] = {
|
|
397
|
-
path: geoms.map((g) => toPath(g, true)).join(""),
|
|
398
|
-
center,
|
|
399
|
-
pin: {
|
|
400
|
-
x: pinX,
|
|
401
|
-
top: fmt(Math.min(...ys)),
|
|
402
|
-
bottom: fmt(Math.max(...ys)),
|
|
403
|
-
tipTop: fmt(Math.min(...spans)),
|
|
404
|
-
tipBottom: fmt(Math.max(...spans)),
|
|
405
|
-
},
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
const labeled = [...HIGHLIGHTS.map((b) => b.osm), ...LANDMARKS].map((name) => {
|
|
409
|
-
const b = elements.find((e) => (e.tags?.building ?? e.tags?.leisure === "stadium") &&
|
|
410
|
-
e.tags?.name === name);
|
|
411
|
-
if (!b) {
|
|
412
|
-
const h = HIGHLIGHTS.find((x) => x.osm === name);
|
|
413
|
-
const pin = h ? highlights[h.key]?.pin : undefined;
|
|
414
|
-
if (!pin)
|
|
415
|
-
throw new Error(`no OSM building found for "${name}"`);
|
|
416
|
-
return { name, cx: pin.x, cy: fmt((pin.top + pin.bottom) / 2) };
|
|
417
|
-
}
|
|
418
|
-
const c = b.geometry.reduce((a, g) => [a[0] + g.lon, a[1] + g.lat], [0, 0]);
|
|
419
|
-
const [cx, cy] = px(c[0] / b.geometry.length, c[1] / b.geometry.length).map(fmt);
|
|
420
|
-
return { name, cx, cy };
|
|
421
|
-
});
|
|
422
|
-
const header = `// GENERATED by devtools gen campus-map. Do not edit by hand.\n// Map data © OpenStreetMap contributors, ODbL: openstreetmap.org/copyright\n// Frame: lat ${S}..${N}, lon ${W}..${E}, equirectangular, ${VW}x${VH}.\n`;
|
|
423
|
-
const keys = HIGHLIGHTS.map((b) => b.key);
|
|
424
|
-
const entries = (pick) => JSON.stringify(Object.fromEntries(keys.map((k) => [k, pick(highlights[k])])), null, 2);
|
|
425
|
-
const meta = `${header}
|
|
426
|
-
/** The map's viewBox, also what sizes its placeholder before it loads. */
|
|
427
|
-
export const VIEW = { w: ${VW}, h: ${VH} };
|
|
428
|
-
|
|
429
|
-
/** Every building a meeting can name. The map highlights exactly one of them. */
|
|
430
|
-
export const BUILDING_KEYS = ${JSON.stringify(keys)} as const;
|
|
431
|
-
|
|
432
|
-
export type BuildingKey = (typeof BUILDING_KEYS)[number];
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* Each building's footprint centroid: where the Google/Apple Maps links drop
|
|
436
|
-
* their pin.
|
|
437
|
-
*/
|
|
438
|
-
export const BUILDING_CENTERS: Record<BuildingKey, { lat: number; lon: number }> = ${entries((h) => h.center)};
|
|
439
|
-
`;
|
|
440
|
-
const out = `${header}
|
|
441
|
-
/** Cased streets, one subpath per OSM way. */
|
|
442
|
-
export const MAJOR_ROADS = ${JSON.stringify(roadPaths.major.join(""))};
|
|
443
|
-
|
|
444
|
-
/** Service drives and pedestrian connectors, drawn thin. */
|
|
445
|
-
export const MINOR_ROADS = ${JSON.stringify(roadPaths.minor.join(""))};
|
|
446
|
-
|
|
447
|
-
/** Every building footprint in the frame, highlightable ones included. */
|
|
448
|
-
export const FOOTPRINTS = ${JSON.stringify(footprints.join(""))};
|
|
449
|
-
|
|
450
|
-
/** The footprint the map paints on top, one per building a meeting can name. */
|
|
451
|
-
export const HIGHLIGHT_PATHS: Record<string, string> = ${entries((h) => h.path)};
|
|
452
|
-
|
|
453
|
-
export const HIGHLIGHT_PINS: Record<string, { x: number; top: number; bottom: number; tipTop: number; tipBottom: number }> = ${entries((h) => h.pin)};
|
|
454
|
-
|
|
455
|
-
/** Street names, on the centreline they name and turned to match it. */
|
|
456
|
-
export const ROAD_LABELS: { text: string; x: number; y: number; angle: number }[] = ${JSON.stringify(roadLabels.map(({ text, x, y, angle }) => ({ text, x, y, angle })), null, 2)};
|
|
457
|
-
`;
|
|
458
|
-
const dir = join(findRepoRoot(), "apps", "platform", "src", "components", "EventsSection", "FindUs");
|
|
459
|
-
writeFileSync(join(dir, "campusMapData.ts"), out);
|
|
460
|
-
writeFileSync(join(dir, "campusMapMeta.ts"), meta);
|
|
461
|
-
console.log(`wrote ${dir}/campusMapData.ts (${(out.length / 1024).toFixed(1)}KB) + campusMapMeta.ts`);
|
|
462
|
-
console.log("run prettier --write on both before committing them");
|
|
463
|
-
console.log(`viewBox 0 0 ${VW} ${VH} | ${roadPaths.major.length} major ways, ${roadPaths.minor.length} minor ways, ${footprints.length} footprints`);
|
|
464
|
-
for (const b of labeled)
|
|
465
|
-
console.log(` LABEL ${b.name} → ${b.cx},${b.cy}`);
|
|
466
|
-
for (const k of keys) {
|
|
467
|
-
const { x, top, bottom, tipTop, tipBottom } = highlights[k].pin;
|
|
468
|
-
console.log(` PIN ${k} → x ${x}, y ${top}..${bottom}, tip ${tipTop}..${tipBottom}`);
|
|
469
|
-
}
|
|
470
|
-
for (const r of roadLabels)
|
|
471
|
-
console.log(` ROAD ${r.text} → ${r.x},${r.y} @ ${r.angle}° (${r.run}px visible)`);
|
|
472
|
-
return 0;
|
|
473
|
-
}
|