@oconde/radar 0.1.0 → 0.2.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ ### Novo
6
+
7
+ - `@oconde/radar/sveltekit`: `radarHandle` (contexto, `x-request-id`, log `http.request`; opção `requestId` para usar o id do app), `radarHandleError`, `radarClientErrors` (rota que recebe erros do navegador), `moveClientSourceMaps` e `composeServerSourceMaps`.
8
+ - `@oconde/radar/browser`: `reportClientError`, `handleErrorWithRadar` e `listenForClientErrors`, sem dependência de Node.
9
+ - Erros do navegador trazem o que a pessoa fez logo antes: `listenForClientErrors` guarda o último clique ou envio de formulário (elemento e texto visível, nunca o valor digitado; links com texto longo, números ou `@` vão sem o texto, e e-mails e números viram `<email>` e `<número>`) e a rota grava em `ui.action`, `ui.element`, `ui.label` e `ui.msBefore`. No modo `requestDetail: 'route'` o texto do elemento não é enviado. `captureActions: false` desliga.
10
+ - Frames do navegador ganham o nome da função do código original (inferido do source map), em vez de ficarem sem nome.
11
+ - Comando `radar-sourcemaps [buildDir]`: depois do `vite build`, tira os source maps de `build/client` e reescreve os caminhos para continuarem apontando para o código. Também junta os dois níveis de mapa do servidor (o do Vite e o do `adapter-node`), para a stack do servidor apontar direto para o arquivo em `src/`, e não para `.svelte-kit/output/server`.
12
+ - `radar.withContext`, `radar.checkIn` e `radar.cron` para workers e tarefas agendadas.
13
+ - `radar.captureException` para quem já tem a exceção montada.
14
+
15
+ ### Mudanças de comportamento
16
+
17
+ - **Agrupamento de erros:** frames que, pelo source map, caem em `node_modules` deixam de contar como código do app, e arquivos gerados com `sourcemap: 'hidden'` passam a usar o `.map` ao lado. As duas coisas mudam a impressão digital de alguns erros: no deploy que atualizar o SDK, erros já conhecidos podem reaparecer como novos uma vez.
18
+ - `radarHandleError()` e `handleErrorWithRadar()` sem handler interno imprimem o erro no console e deixam o SvelteKit usar a mensagem padrão (antes devolviam "Internal Error", inclusive no 404).
19
+ - Um `x-request-id` com caracteres fora do ASCII imprimível é trocado por um id gerado.
20
+ - Check-ins cortam `environment` em 64 caracteres, `release` em 128 e `durationMs` em 7 dias, como o servidor exige.
21
+
22
+ ### Correções
23
+
24
+ - Express: segredos em parâmetros do caminho (como `/reset/:token`) eram enviados sem máscara quando a requisição terminava no middleware de erro do app.
25
+
26
+ ## 0.1.0
27
+
28
+ Primeira versão: NestJS, Express, logs, erros com trecho de código, requisições e mascaramento de dados sensíveis.
package/PROTOCOL.md CHANGED
@@ -11,7 +11,7 @@ Content-Type: application/json
11
11
  Content-Encoding: gzip (opcional)
12
12
  ```
13
13
 
14
- Limites: corpo de até 1 MB depois de descompactado, até 500 eventos por lote.
14
+ Limites: até 500 eventos por lote e 1 MB de corpo, tanto nos bytes recebidos quanto no JSON depois de descompactado. `Content-Encoding` aceita só `gzip` ou nenhum.
15
15
 
16
16
  ## Tipos
17
17
 
@@ -117,14 +117,48 @@ export type IngestError = { error: { code: IngestErrorCode; message: string } };
117
117
 
118
118
  | Status | Corpo | Quando | O SDK faz |
119
119
  |---|---|---|---|
120
- | 202 | `IngestResponse` | lote aceito, mesmo que parcialmente: eventos inválidos ou de `type` desconhecido entram em `dropped` | segue |
121
- | 400 | `invalid_batch` | o envelope não é um `EventBatch` (sem `v: 1`, sem `events`) | descarta o lote, avisa uma vez |
120
+ | 202 | `IngestResponse` | lote aceito, mesmo que parcialmente: eventos inválidos, de `type` desconhecido ou acima da cota do dia entram em `dropped` | segue |
121
+ | 400 | `invalid_batch` | JSON inválido, `Content-Encoding` diferente de `gzip`, ou o envelope não é um `EventBatch` (sem `v: 1`, sem `sdk`, sem `events`) | descarta o lote, avisa uma vez |
122
122
  | 401 | `invalid_key` | chave ausente, inexistente ou revogada | pausa os envios por 10 min, descarta a fila, avisa uma vez |
123
- | 403 | `account_disabled` | conta desativada | igual ao 401 |
124
- | 413 | `payload_too_large` | acima de 1 MB ou 500 eventos | descarta o lote |
125
- | 429 | `quota_exceeded` / `rate_limited`, com `Retry-After` em segundos | cota do dia ou excesso de requisições | devolve o lote à fila e espera o `Retry-After` |
123
+ | 403 | `account_disabled` | conta desativada ou projeto apagado | igual ao 401 |
124
+ | 413 | `payload_too_large` | acima de 1 MB ou de 500 eventos | descarta o lote |
125
+ | 429 | `rate_limited`, com `Retry-After: 1` | excesso de requisições da mesma chave | devolve o lote à fila e espera o `Retry-After` |
126
+ | 429 | `quota_exceeded`, com `Retry-After` até a virada do dia | a cota do dia acabou e nenhum evento do lote coube | devolve o lote à fila e espera o `Retry-After` |
126
127
  | 5xx / rede | — | servidor fora | devolve o lote à fila e tenta com espera crescente |
127
128
 
129
+ Quando a cota do dia acaba, o Radar avisa uma vez por dia em todos os canais ativos da conta.
130
+
131
+ ## Check-in de tarefa agendada
132
+
133
+ ```
134
+ POST https://radar-ingest.oconde.dev/api/v1/checkins/<slug>
135
+ Authorization: Bearer rk_<40 caracteres>
136
+ Content-Type: application/json
137
+
138
+ { "status": "in_progress" | "ok" | "error", "checkInId"?: string, "durationMs"?: number, "environment"?: string, "release"?: string }
139
+ ```
140
+
141
+ - `slug`: `^[a-z0-9][a-z0-9-]{0,63}$`, o mesmo do monitor criado no Radar, no projeto da chave.
142
+ - `checkInId` (até 64 caracteres) liga o `in_progress` ao `ok` ou `error` da mesma execução. Sem ele, `ok` e `error` fecham a execução aberta mais recente.
143
+ - `202 { "accepted": true }`. `404 unknown_monitor` quando o slug não existe no projeto da chave, `400 invalid_checkin` para corpo fora do formato, e `401`, `403` e `429` como no envio de eventos.
144
+ - O SDK manda na hora, sem fila, e nunca lança: `radar.checkIn(slug, status, { checkInId, durationMs })` ou `radar.cron(slug, fn)`.
145
+
146
+ Exemplo para um script sem o SDK:
147
+
148
+ ```bash
149
+ curl -fsS -X POST -H "Authorization: Bearer $RADAR_KEY" -H 'content-type: application/json' \
150
+ -d '{"status":"ok"}' https://radar-ingest.oconde.dev/api/v1/checkins/walg-archive-check
151
+ ```
152
+
153
+ ## Resumo de release
154
+
155
+ ```
156
+ GET https://radar-ingest.oconde.dev/api/v1/releases/<release>
157
+ Authorization: Bearer rk_<40 caracteres>
158
+ ```
159
+
160
+ `200 { "release", "firstSeen": iso | null, "newIssues": n, "events": n, "issues": [{ "title", "culprit", "count", "url" }] }`, com até 5 erros (mais ocorrências primeiro), só do projeto da chave. Serve para o pipeline de deploy olhar a release logo depois de publicar.
161
+
128
162
  ## Compatibilidade
129
163
 
130
164
  O v1 só cresce: campo opcional novo pode; mudar o significado, tornar obrigatório ou remover não pode. O servidor ignora campos desconhecidos. Mudança que quebra vira `/api/v2/events`, com o v1 mantido.
@@ -132,7 +166,9 @@ O v1 só cresce: campo opcional novo pode; mudar o significado, tornar obrigató
132
166
 
133
167
  ## Limites recomendados no SDK
134
168
 
135
- O servidor corta o que passar destes limites; o SDK deve cortar antes de enviar.
169
+ O servidor também corta, com regras um pouco diferentes: a `message` do log em 2000 bytes (UTF-8), a mensagem da exceção em 2000 caracteres, `attrs`, `query` e headers em 16 KB, `params` em 4 KB e `body` em 16 KB. Um campo JSON acima do limite vira `{ "_truncated": "<começo do JSON>" }`. Abaixo de 64 níveis de profundidade o servidor troca o valor por `null`; o SDK corta em 6.
170
+
171
+ O SDK deve cortar antes de enviar:
136
172
 
137
173
  | Item | Limite |
138
174
  |---|---|
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @oconde/radar
2
2
 
3
- SDK do [Radar](https://radar.oconde.dev) para Node.js e NestJS: logs, erros com trecho de código e contexto de requisição. Sem dependências em runtime.
3
+ SDK do [Radar](https://radar.oconde.dev) para Node.js, NestJS e SvelteKit: logs, erros com trecho de código, contexto de requisição, tarefas agendadas e erros do navegador. Sem dependências em runtime.
4
+
5
+ **Documentação completa: https://radar.oconde.dev/docs/**
4
6
 
5
7
  ## Instalação
6
8
 
@@ -8,7 +10,7 @@ SDK do [Radar](https://radar.oconde.dev) para Node.js e NestJS: logs, erros com
8
10
  npm install @oconde/radar
9
11
  ```
10
12
 
11
- Node 20 ou mais novo. Funciona em ESM e CommonJS, do NestJS 10 em diante.
13
+ Node 20 ou mais novo. Funciona em ESM e CommonJS, do NestJS 10 em diante. Gere a chave do projeto no painel do Radar e ponha em `RADAR_KEY`.
12
14
 
13
15
  ## NestJS
14
16
 
@@ -22,103 +24,97 @@ import { RadarModule } from '@oconde/radar/nest';
22
24
  export class AppModule {}
23
25
  ```
24
26
 
25
- Com `ConfigService`:
27
+ O módulo registra o contexto por requisição, um log `http.request` por requisição e a captura dos erros 5xx. Nos services:
26
28
 
27
29
  ```ts
28
- RadarModule.forRootAsync({
29
- inject: [ConfigService],
30
- useFactory: (config: ConfigService) => ({ key: config.get('RADAR_KEY') }),
31
- });
32
- ```
30
+ import { radar } from '@oconde/radar';
33
31
 
34
- O módulo registra sozinho o contexto por requisição, um log `http.request` por requisição e a captura dos erros 5xx lançados nos handlers. Chame `app.enableShutdownHooks()` para o Radar enviar a fila ao desligar.
32
+ radar.info('webhook.olx.lead', { leadId });
33
+ ```
35
34
 
36
- Nos services, nada precisa ser injetado:
35
+ ## Express
37
36
 
38
37
  ```ts
39
38
  import { radar } from '@oconde/radar';
40
39
 
41
- radar.info('webhook.olx.lead', { leadId });
40
+ radar.init({ key: process.env.RADAR_KEY });
41
+ app.use(radar.middleware());
42
42
  ```
43
43
 
44
- Quem prefere injeção usa `RadarService`, que tem os mesmos métodos.
45
-
46
- Erros lançados em guards e pipes não passam pelo interceptor. Se o app não tem filtro de exceção próprio, registre o do Radar:
44
+ Depois das rotas, para os erros dos handlers chegarem ao Radar:
47
45
 
48
46
  ```ts
49
- import { APP_FILTER } from '@nestjs/core';
50
- import { RadarExceptionFilter } from '@oconde/radar/nest';
47
+ import type { ErrorRequestHandler } from 'express';
48
+
49
+ const reportErrors: ErrorRequestHandler = (error, req, res, next) => {
50
+ radar.captureError(error);
51
+ next(error);
52
+ };
51
53
 
52
- providers: [{ provide: APP_FILTER, useClass: RadarExceptionFilter }];
54
+ app.use(reportErrors);
53
55
  ```
54
56
 
55
- Se já tem, acrescente no `catch` dele: `if (status >= 500) radar.captureError(exception)`. O mesmo erro nunca é enviado duas vezes.
57
+ Exemplo completo em [Express e Node](https://radar.oconde.dev/docs/sdk/express/).
56
58
 
57
- ## Express
59
+ ## SvelteKit
58
60
 
59
61
  ```ts
62
+ import { sequence } from '@sveltejs/kit/hooks';
60
63
  import { radar } from '@oconde/radar';
64
+ import { radarHandle, radarHandleError } from '@oconde/radar/sveltekit';
61
65
 
62
- radar.init({ key: process.env.RADAR_KEY });
63
- app.use(radar.middleware());
66
+ radar.init({ key: process.env.RADAR_KEY, release: process.env.GIT_SHA });
67
+
68
+ export const handle = sequence(radarHandle(), yourHandle);
69
+ export const handleError = radarHandleError();
64
70
  ```
65
71
 
66
- ## API
72
+ Se o app já gera o próprio id de requisição, passe-o e coloque o `radarHandle` depois do handle que o cria: `radarHandle({ requestId: (event) => event.locals.requestId })`.
73
+
74
+ Erros do navegador vão por uma rota do próprio app, sem expor a chave. Ela limita tamanho e frequência por visitante e no total; atrás de proxy, configure `ADDRESS_HEADER`/`XFF_DEPTH` do adapter-node para o IP do visitante ser o real:
67
75
 
68
- | Função | O que faz |
69
- |---|---|
70
- | `radar.init(options)` | configura uma vez; chamadas seguintes são ignoradas |
71
- | `radar.debug/info/warn/error(message, data?)` | log com `data` como atributos; dentro de uma requisição sai com o `requestId` e o usuário |
72
- | `radar.logRequest(title, data?, { level?, redact? }?)` | log com a requisição atual inteira: método, URL, rota, params, query, headers, body, IP |
73
- | `radar.captureError(error, data?)` | erro com stack, trecho de código, requisição, usuário e runtime |
74
- | `radar.setUser({ id, email, name })` | usuário da requisição atual |
75
- | `radar.track(name, fn, data?)` | mede `fn`, registra sucesso ou falha com `durationMs`, captura e relança o erro |
76
- | `radar.middleware()` | middleware Express de contexto (`x-request-id`) |
77
- | `radar.flush(timeoutMs?)` / `radar.close(timeoutMs?)` | envia a fila; `close` também desliga |
78
- | `radar.settings` | opções em uso (só leitura); `radar.settings.console` diz se o Radar já imprime no stdout, útil para um logger próprio não duplicar linhas |
76
+ ```ts title="src/routes/api/radar/client-errors/+server.ts"
77
+ import { radarClientErrors } from '@oconde/radar/sveltekit';
79
78
 
80
- Mensagens de log são chaves estáveis (`webhook.olx.lead`); o que varia vai em `data`.
79
+ export const POST = radarClientErrors();
80
+ ```
81
81
 
82
- ## Opções
82
+ ```ts
83
+ import { handleErrorWithRadar, listenForClientErrors } from '@oconde/radar/browser';
83
84
 
84
- | Opção | Padrão | |
85
- |---|---|---|
86
- | `key` | — | sem chave nada é enviado |
87
- | `endpoint` | `https://radar-ingest.oconde.dev` | |
88
- | `environment` | `NODE_ENV` ou `production` | |
89
- | `release` | — | ex.: o SHA do commit |
90
- | `console` | `false` | imprime cada log como JSON no stdout, mesmo sem chave |
91
- | `minLevel` | `info` | |
92
- | `redact` | `mask` | `none` guarda tokens e senhas inteiros |
93
- | `requestDetail` | `full` | `route` manda da requisição só o método e a rota (`/students/:studentId`), com status e duração: sem URL concreta, query, params, headers, corpo, IP nem user agent. Para apps com dado sensível (saúde, LGPD) |
94
- | `logRequests` | `true` | log `http.request` automático |
95
- | `ignorePaths` | `['/healthz', '/health']` | |
96
- | `captureUnhandled` | `true` | `uncaughtException` e `unhandledRejection`; o processo cai como cairia sem o Radar |
97
- | `debug` | `false` | mostra problemas internos do SDK |
85
+ listenForClientErrors();
86
+ export const handleError = handleErrorWithRadar();
87
+ ```
98
88
 
99
- ## Dados sensíveis
89
+ O erro do navegador chega com o que a pessoa fez logo antes (por exemplo, "clicou no botão Aplicar cupom"): o último clique ou envio de formulário até 10 s antes, com o texto visível do elemento e nunca o que foi digitado. `listenForClientErrors({ captureActions: false })` desliga.
100
90
 
101
- Com `redact: 'mask'` (padrão), o SDK mascara antes de enviar:
91
+ Com `build: { sourcemap: 'hidden' }` no Vite e `radar-sourcemaps` depois do build, os mapas saem de `build/client`, os do servidor passam a apontar direto para `src/`, e os erros mostram o código original. Detalhes em [SvelteKit](https://radar.oconde.dev/docs/sdk/sveltekit/) e [Erros do navegador](https://radar.oconde.dev/docs/sdk/browser/).
102
92
 
103
- - headers e chaves (no corpo, na query, nos parâmetros da rota, no caminho da URL e nos atributos) cujo nome tenha uma destas palavras: `password`, `senha`, `secret`, `token`, `auth`, `authorization`, `cookie`, `apikey`, `api key`, `private key`, `access key`, `signature`, `jwt`, `session`, `credential`, `cpf`, `card`, `cvv`, `cvc`, `pin`, `otp`, além da chave `key` sozinha e dos headers `x-…-key`, `x-…-secret` e `x-…-token`. A comparação é por palavra: `cardio` e `passos` não são mascarados;
104
- - qualquer valor com cara de credencial (`Bearer …`, `Basic …`, JWT), seja qual for a chave.
93
+ ## Tarefas agendadas
105
94
 
106
- Tokens e chaves saem como `Bearer eyJh…5x9Q #a1b2c3d4`: dá para ver se veio, qual era e se dois valores são iguais. Senhas, CPF, cartão, CVV, PIN e OTP saem só como `••• #a1b2c3d4`, sem nenhum pedaço do valor. A impressão digital é um HMAC com chave derivada da chave do projeto, então não dá para descobrir o valor por força bruta sem ela.
95
+ ```ts
96
+ await radar.cron('nightly-report', () => buildReport());
97
+ ```
107
98
 
108
- `redact: 'none'` (no `init` ou por chamada de `logRequest`) guarda os valores inteiros.
99
+ `radar.cron` avisa o Radar no início, no fim e na falha, e o Radar alerta se a tarefa atrasar ou falhar. Em scripts sem o SDK, o check-in é um `POST` (veja o [PROTOCOL.md](PROTOCOL.md)). Para jobs e workers, `radar.withContext(fn)` dá a cada execução o próprio `requestId`. Detalhes em [Tarefas agendadas](https://radar.oconde.dev/docs/sdk/cron/).
109
100
 
110
- ## Trecho de código nos erros
101
+ ## Erros com o seu código
111
102
 
112
- O SDK lê os source maps sozinho. Em projetos TypeScript, ligue no `tsconfig`:
103
+ Ligue no `tsconfig` para os erros mostrarem as linhas do TypeScript:
113
104
 
114
105
  ```json
115
106
  { "compilerOptions": { "sourceMap": true, "inlineSources": true } }
116
107
  ```
117
108
 
118
- Com `inlineSources`, o código vai dentro do `.map` e a imagem de produção não precisa conter `src/`.
109
+ ## Na documentação
119
110
 
120
- ## Garantias
111
+ - [Começar em 5 minutos](https://radar.oconde.dev/docs/start/getting-started/)
112
+ - [Todas as opções](https://radar.oconde.dev/docs/sdk/options/)
113
+ - [Dados sensíveis e `requestDetail: 'route'`](https://radar.oconde.dev/docs/sdk/sensitive-data/)
114
+ - [Garantias: fila, lotes e o que acontece quando o Radar cai](https://radar.oconde.dev/docs/sdk/guarantees/)
115
+ - [Referência da API](https://radar.oconde.dev/docs/reference/sdk-api/)
116
+ - [Protocolo de envio](https://radar.oconde.dev/docs/reference/protocol/) (também em [PROTOCOL.md](PROTOCOL.md), para quem escreve um SDK em outra linguagem)
121
117
 
122
- Nenhuma função lança erro para o app (exceto `track`, que relança o erro de `fn`). A fila fica em memória (até 1000 eventos), é enviada a cada 2 s ou 100 eventos e tenta de novo com espera crescente se o Radar estiver fora. O SDK não registra `SIGTERM`.
118
+ ## Licença
123
119
 
124
- Formato dos dados enviados: [PROTOCOL.md](PROTOCOL.md).
120
+ MIT
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/sveltekit/sourcemaps.ts
4
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "fs";
5
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
6
+
7
+ // src/sveltekit/vlq.ts
8
+ var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
9
+ var DIGITS = new Map([...ALPHABET].map((char, index) => [char, index]));
10
+ var FIELDS = 5;
11
+ function decodeGroup(group) {
12
+ const values = [];
13
+ let value = 0;
14
+ let factor = 1;
15
+ for (const char of group) {
16
+ const digit = DIGITS.get(char);
17
+ if (digit === void 0) throw new Error(`invalid source map mapping: ${group}`);
18
+ value += digit % 32 * factor;
19
+ if (digit >= 32) {
20
+ factor *= 32;
21
+ continue;
22
+ }
23
+ const magnitude = Math.floor(value / 2);
24
+ values.push(value % 2 === 1 ? -magnitude : magnitude);
25
+ value = 0;
26
+ factor = 1;
27
+ }
28
+ return values;
29
+ }
30
+ function encodeValue(delta) {
31
+ let value = delta < 0 ? -delta * 2 + 1 : delta * 2;
32
+ let encoded = "";
33
+ do {
34
+ const digit = value % 32;
35
+ value = Math.floor(value / 32);
36
+ encoded += ALPHABET[value > 0 ? digit + 32 : digit];
37
+ } while (value > 0);
38
+ return encoded;
39
+ }
40
+ function decodeMappings(mappings) {
41
+ const state = new Array(FIELDS).fill(0);
42
+ return mappings.split(";").map((line) => {
43
+ state[0] = 0;
44
+ const segments = [];
45
+ for (const group of line.split(",")) {
46
+ if (!group) continue;
47
+ const values = decodeGroup(group).slice(0, FIELDS);
48
+ segments.push(values.map((delta, field) => state[field] = (state[field] ?? 0) + delta));
49
+ }
50
+ return segments;
51
+ });
52
+ }
53
+ function encodeMappings(lines) {
54
+ const state = new Array(FIELDS).fill(0);
55
+ return lines.map((segments) => {
56
+ state[0] = 0;
57
+ return segments.map(
58
+ (segment) => segment.map((value, field) => {
59
+ const delta = value - (state[field] ?? 0);
60
+ state[field] = value;
61
+ return encodeValue(delta);
62
+ }).join("")
63
+ ).join(",");
64
+ }).join(";");
65
+ }
66
+
67
+ // src/sveltekit/sourcemaps.ts
68
+ function filesUnder(directory) {
69
+ return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
70
+ const full = join(directory, entry.name);
71
+ if (entry.isDirectory()) return filesUnder(full);
72
+ return entry.isFile() ? [full] : [];
73
+ });
74
+ }
75
+ function rebase(source, fromDir, root, toDir) {
76
+ if (typeof source !== "string" || isAbsolute(source) || source.includes("://")) return source;
77
+ return relative(toDir, resolve(fromDir, root, source)).split(sep).join("/");
78
+ }
79
+ function moveMap(file, target, originalDir) {
80
+ try {
81
+ const map = JSON.parse(readFileSync(file, "utf8"));
82
+ if (Array.isArray(map.sources)) {
83
+ const root = typeof map.sourceRoot === "string" ? map.sourceRoot : "";
84
+ map.sources = map.sources.map((source) => rebase(source, originalDir, root, dirname(target)));
85
+ delete map.sourceRoot;
86
+ }
87
+ writeFileSync(target, JSON.stringify(map));
88
+ unlinkSync(file);
89
+ } catch {
90
+ renameSync(file, target);
91
+ }
92
+ }
93
+ function moveClientSourceMaps(buildDir2 = "build") {
94
+ const clientDir = resolve(buildDir2, "client");
95
+ const mapsDir = resolve(buildDir2, "client-maps");
96
+ const generatedDir = resolve(buildDir2, "..", ".svelte-kit/output/client");
97
+ if (!existsSync(clientDir)) return -1;
98
+ let moved2 = 0;
99
+ for (const file of filesUnder(clientDir)) {
100
+ if (file.endsWith(".map.gz") || file.endsWith(".map.br")) {
101
+ unlinkSync(file);
102
+ continue;
103
+ }
104
+ if (!file.endsWith(".map")) continue;
105
+ const path = relative(clientDir, file);
106
+ const target = join(mapsDir, path);
107
+ mkdirSync(dirname(target), { recursive: true });
108
+ const generated = join(generatedDir, path);
109
+ moveMap(file, target, dirname(existsSync(generated) ? generated : file));
110
+ moved2 += 1;
111
+ }
112
+ return moved2;
113
+ }
114
+ function parseSourceMap(file) {
115
+ try {
116
+ const raw = JSON.parse(readFileSync(file, "utf8"));
117
+ if (typeof raw.mappings !== "string" || !Array.isArray(raw.sources)) return null;
118
+ const root = typeof raw.sourceRoot === "string" ? raw.sourceRoot : "";
119
+ return {
120
+ raw,
121
+ lines: decodeMappings(raw.mappings),
122
+ sources: raw.sources.map((source) => typeof source === "string" ? source : ""),
123
+ contents: Array.isArray(raw.sourcesContent) ? raw.sourcesContent.map((content) => typeof content === "string" ? content : null) : [],
124
+ names: Array.isArray(raw.names) ? raw.names.map(String) : [],
125
+ baseDir: resolve(dirname(file), root)
126
+ };
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+ function isUrl(source) {
132
+ return source.includes("://");
133
+ }
134
+ function isInside(directory, path) {
135
+ const rel = relative(directory, path);
136
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
137
+ }
138
+ function traceSegment(map, line, column) {
139
+ const segments = map.lines[line];
140
+ if (!segments || segments.length === 0) return null;
141
+ let low = 0;
142
+ let high = segments.length - 1;
143
+ let found = -1;
144
+ while (low <= high) {
145
+ const middle = low + high >> 1;
146
+ if ((segments[middle]?.[0] ?? 0) <= column) {
147
+ found = middle;
148
+ low = middle + 1;
149
+ } else {
150
+ high = middle - 1;
151
+ }
152
+ }
153
+ const segment = segments[Math.max(found, 0)];
154
+ return segment && segment.length >= 4 ? segment : null;
155
+ }
156
+ var ComposedTable = class {
157
+ constructor(outputDir) {
158
+ this.outputDir = outputDir;
159
+ }
160
+ outputDir;
161
+ sources = [];
162
+ contents = [];
163
+ names = [];
164
+ sourceIndex = /* @__PURE__ */ new Map();
165
+ nameIndex = /* @__PURE__ */ new Map();
166
+ source(baseDir, source, content) {
167
+ const key = isUrl(source) || isAbsolute(source) ? source : relative(this.outputDir, resolve(baseDir, source)).split(sep).join("/");
168
+ const existing = this.sourceIndex.get(key);
169
+ if (existing !== void 0) return existing;
170
+ this.sourceIndex.set(key, this.sources.length);
171
+ this.sources.push(key);
172
+ this.contents.push(content);
173
+ return this.sources.length - 1;
174
+ }
175
+ name(value) {
176
+ const existing = this.nameIndex.get(value);
177
+ if (existing !== void 0) return existing;
178
+ this.nameIndex.set(value, this.names.length);
179
+ this.names.push(value);
180
+ return this.names.length - 1;
181
+ }
182
+ };
183
+ function composeMap(file, generatedDir, generatedMaps) {
184
+ const outer = parseSourceMap(file);
185
+ if (!outer) return false;
186
+ const inners = outer.sources.map((source) => {
187
+ if (isUrl(source)) return null;
188
+ const path = resolve(outer.baseDir, source);
189
+ if (!isInside(generatedDir, path)) return null;
190
+ if (!generatedMaps.has(path)) generatedMaps.set(path, parseSourceMap(`${path}.map`));
191
+ return generatedMaps.get(path) ?? null;
192
+ });
193
+ if (inners.every((inner) => inner === null)) return false;
194
+ const table = new ComposedTable(dirname(file));
195
+ let traced = 0;
196
+ const lines = outer.lines.map(
197
+ (segments) => segments.map((segment) => {
198
+ const [column = 0, sourceIndex = 0, line = 0, originalColumn = 0, nameIndex] = segment;
199
+ if (segment.length < 4) return [column];
200
+ const inner = inners[sourceIndex];
201
+ const original = inner ? traceSegment(inner, line, originalColumn) : null;
202
+ if (inner && original) {
203
+ traced += 1;
204
+ const [, innerSource = 0, innerLine = 0, innerColumn = 0, innerName] = original;
205
+ const source2 = table.source(inner.baseDir, inner.sources[innerSource] ?? "", inner.contents[innerSource] ?? null);
206
+ const name2 = innerName !== void 0 ? inner.names[innerName] : nameIndex !== void 0 ? outer.names[nameIndex] : void 0;
207
+ return name2 === void 0 ? [column, source2, innerLine, innerColumn] : [column, source2, innerLine, innerColumn, table.name(name2)];
208
+ }
209
+ const source = table.source(outer.baseDir, outer.sources[sourceIndex] ?? "", outer.contents[sourceIndex] ?? null);
210
+ const name = nameIndex !== void 0 ? outer.names[nameIndex] : void 0;
211
+ return name === void 0 ? [column, source, line, originalColumn] : [column, source, line, originalColumn, table.name(name)];
212
+ })
213
+ );
214
+ if (traced === 0) return false;
215
+ const { sourceRoot: _root, ignoreList: _ignore, x_google_ignoreList: _googleIgnore, ...rest } = outer.raw;
216
+ const composed2 = { ...rest, sources: table.sources, sourcesContent: table.contents, names: table.names, mappings: encodeMappings(lines) };
217
+ writeFileSync(file, JSON.stringify(composed2));
218
+ return true;
219
+ }
220
+ function composeServerSourceMaps(buildDir2 = "build") {
221
+ const serverDir = resolve(buildDir2, "server");
222
+ const generatedDir = resolve(buildDir2, "..", ".svelte-kit/output/server");
223
+ if (!existsSync(serverDir) || !existsSync(generatedDir)) return 0;
224
+ const generatedMaps = /* @__PURE__ */ new Map();
225
+ let composed2 = 0;
226
+ for (const file of filesUnder(serverDir)) {
227
+ if (file.endsWith(".map") && composeMap(file, generatedDir, generatedMaps)) composed2 += 1;
228
+ }
229
+ return composed2;
230
+ }
231
+
232
+ // src/bin/radar-sourcemaps.ts
233
+ var buildDir = process.argv[2] ?? "build";
234
+ var moved = moveClientSourceMaps(buildDir);
235
+ if (moved < 0) {
236
+ process.stderr.write(`radar: ${buildDir}/client not found; run it after vite build
237
+ `);
238
+ process.exit(1);
239
+ }
240
+ process.stdout.write(`radar: ${moved} source maps moved from ${buildDir}/client to ${buildDir}/client-maps
241
+ `);
242
+ var composed = composeServerSourceMaps(buildDir);
243
+ process.stdout.write(`radar: ${composed} server source maps in ${buildDir}/server now point to the original sources
244
+ `);