@microteia/console 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CONSOLE-VERSIONS.md +85 -0
  2. package/README.md +64 -0
  3. package/lib/console/backendEnv.d.ts +52 -0
  4. package/lib/console/backendEnv.js +118 -0
  5. package/lib/console/backendIndexes.d.ts +40 -0
  6. package/lib/console/backendIndexes.js +178 -0
  7. package/lib/console/backendParameters.d.ts +48 -0
  8. package/lib/console/backendParameters.js +100 -0
  9. package/lib/console/callableAccess.d.ts +39 -0
  10. package/lib/console/callableAccess.js +61 -0
  11. package/lib/console/channel.d.ts +49 -0
  12. package/lib/console/channel.js +145 -0
  13. package/lib/console/connect.d.ts +2 -0
  14. package/lib/console/connect.js +74 -0
  15. package/lib/console/customFunctions.d.ts +120 -0
  16. package/lib/console/customFunctions.js +298 -0
  17. package/lib/console/deploy.d.ts +2 -0
  18. package/lib/console/deploy.js +261 -0
  19. package/lib/console/deployFunctions.d.ts +45 -0
  20. package/lib/console/deployFunctions.js +204 -0
  21. package/lib/console/environment.d.ts +62 -0
  22. package/lib/console/environment.js +151 -0
  23. package/lib/console/firebaseCli.d.ts +15 -0
  24. package/lib/console/firebaseCli.js +90 -0
  25. package/lib/console/firestoreRest.d.ts +39 -0
  26. package/lib/console/firestoreRest.js +131 -0
  27. package/lib/console/hostingCheck.d.ts +29 -0
  28. package/lib/console/hostingCheck.js +44 -0
  29. package/lib/console/init.d.ts +2 -0
  30. package/lib/console/init.js +60 -0
  31. package/lib/console/pluginWorkspace.d.ts +48 -0
  32. package/lib/console/pluginWorkspace.js +148 -0
  33. package/lib/console/provision.d.ts +70 -0
  34. package/lib/console/provision.js +478 -0
  35. package/lib/console/regionOrphans.d.ts +43 -0
  36. package/lib/console/regionOrphans.js +99 -0
  37. package/lib/console/runtimeConfigShape.d.ts +25 -0
  38. package/lib/console/runtimeConfigShape.js +35 -0
  39. package/lib/console/unpack.d.ts +9 -0
  40. package/lib/console/unpack.js +333 -0
  41. package/lib/console/versions.d.ts +10 -0
  42. package/lib/console/versions.js +72 -0
  43. package/lib/console/workspace.d.ts +19 -0
  44. package/lib/console/workspace.js +33 -0
  45. package/lib/index.d.ts +2 -0
  46. package/lib/index.js +136 -0
  47. package/lib/kernel/backendShape.d.ts +74 -0
  48. package/lib/kernel/backendShape.js +91 -0
  49. package/lib/kernel/channel.d.ts +79 -0
  50. package/lib/kernel/channel.js +157 -0
  51. package/lib/kernel/manifest.d.ts +138 -0
  52. package/lib/kernel/manifest.js +145 -0
  53. package/lib/shared/args.d.ts +14 -0
  54. package/lib/shared/args.js +44 -0
  55. package/lib/shared/digest.d.ts +30 -0
  56. package/lib/shared/digest.js +34 -0
  57. package/lib/shared/paths.d.ts +90 -0
  58. package/lib/shared/paths.js +97 -0
  59. package/lib/shared/run.d.ts +28 -0
  60. package/lib/shared/run.js +49 -0
  61. package/lib/shared/ui.d.ts +23 -0
  62. package/lib/shared/ui.js +43 -0
  63. package/lib/vendor/authoringGuide.d.ts +19 -0
  64. package/lib/vendor/authoringGuide.js +284 -0
  65. package/lib/vendor/bundleAudit.d.ts +19 -0
  66. package/lib/vendor/bundleAudit.js +95 -0
  67. package/lib/vendor/changelogs.d.ts +62 -0
  68. package/lib/vendor/changelogs.js +131 -0
  69. package/lib/vendor/exampleSources.d.ts +29 -0
  70. package/lib/vendor/exampleSources.js +126 -0
  71. package/lib/vendor/installationDocs.d.ts +35 -0
  72. package/lib/vendor/installationDocs.js +243 -0
  73. package/lib/vendor/pack.d.ts +2 -0
  74. package/lib/vendor/pack.js +276 -0
  75. package/lib/vendor/packConsole.d.ts +13 -0
  76. package/lib/vendor/packConsole.js +63 -0
  77. package/lib/vendor/packageTemplates.d.ts +66 -0
  78. package/lib/vendor/packageTemplates.js +200 -0
  79. package/lib/vendor/pluginWorkspaceTemplates.d.ts +19 -0
  80. package/lib/vendor/pluginWorkspaceTemplates.js +29 -0
  81. package/lib/vendor/publish.d.ts +2 -0
  82. package/lib/vendor/publish.js +277 -0
  83. package/lib/vendor/repository.d.ts +9 -0
  84. package/lib/vendor/repository.js +26 -0
  85. package/package.json +21 -0
@@ -0,0 +1,284 @@
1
+ /**
2
+ * `AUTHORING.md` — o manual de quem escreve extensão, e o contraponto do `ARCHITECTURE.md`: aquele
3
+ * explica o kernel a quem o altera, este explica o CONTRATO a quem nunca vai vê-lo.
4
+ *
5
+ * Em inglês, e não por preferência: é superfície de produto, entregue a escritórios em qualquer lugar.
6
+ * A regra de idioma do repositório é exatamente essa — o produto fala inglês porque tem de poder falar
7
+ * qualquer idioma; o português é a nossa comunicação interna.
8
+ *
9
+ * Mora no Console (e não como arquivo `.md` solto) para que a versão do kernel e a lista de pontos de
10
+ * extensão sejam CARIMBADAS no texto: um manual que fala de um contrato diferente do instalado é pior
11
+ * que manual nenhum.
12
+ */
13
+ const pointsTable = (points) => [
14
+ '| Point | API | Accepts | Many? |',
15
+ '|---|---|---|---|',
16
+ ...points.map((point) => `| \`${point.id}\` | ${point.apiVersion} | ${point.allowedImplKinds.length === 0 ? '_declaration only_' : point.allowedImplKinds.map((kind) => `\`${kind}\``).join(', ')} | ${point.multiple ? 'yes' : 'no'} |`),
17
+ ].join('\n');
18
+ export const authoringGuide = (kernelVersion, points, hostModules) => `# Authoring Microteia extensions
19
+
20
+ Kernel contract **${kernelVersion}**. Everything here describes the platform installed next to this
21
+ directory, not a general idea of it.
22
+
23
+ ## 1. What an extension is
24
+
25
+ A declaration. A single JSON document that says *what should exist* — screens, automations, actions,
26
+ scripts, pages, translations, a theme — which the platform reads and installs. The platform has no
27
+ knowledge of your extension: it only knows the points you contribute to.
28
+
29
+ Two consequences you should take seriously:
30
+
31
+ - **You never modify the platform.** There is no patch, no hook into internals, no override. If
32
+ something seems to require changing the kernel, you are looking at the wrong extension point.
33
+ - **Uninstalling has to be clean.** Anything your extension declares can be withdrawn. That is why
34
+ option lists travel as *declarations* and not as data: the options belong to the organization, and
35
+ removing your extension must not delete them.
36
+
37
+ ## 2. The loop
38
+
39
+ npx microteia-plugin new com.example.hello # scaffold
40
+ npx tsc --noEmit # types
41
+ npx microteia-plugin check hello/plugin.ts # the gate
42
+ npx microteia-plugin build hello/plugin.ts # writes the .json
43
+
44
+ \`check\` is not an approximation of the platform's validation — it *is* the platform's validation, the
45
+ same function, the same messages. If \`check\` passes, the package installs.
46
+
47
+ Then import the \`.json\` on the **Extensions** screen.
48
+
49
+ ## 3. The package
50
+
51
+ \`\`\`ts
52
+ import type { AuthoredPlugin } from '@microteia/sdk';
53
+
54
+ export default {
55
+ manifest: { id, version, kernel, name, publisher, contributes, /* dependencies, backend, ... */ },
56
+ summary: 'One line, shown in the Extensions list.',
57
+ workflows: [], // required, may be empty
58
+ screens: [], // entities, with their fields
59
+ screenPatches: [], // fields added to a screen that already exists
60
+ lookups: [], tagLists: [],
61
+ actions: [], scripts: [],
62
+ views: [], // pages delivered as code
63
+ startup: '', // code that runs once when the extension loads
64
+ migrations: [], // converting data an earlier version of YOURS wrote
65
+ overview: {}, // a rich description, in Widget DSL
66
+ } satisfies AuthoredPlugin;
67
+ \`\`\`
68
+
69
+ Every field except \`manifest\`, \`summary\` and \`workflows\` is optional, and a package may be any
70
+ subset — a single automation is a legitimate extension.
71
+
72
+ **Two types, one letter apart, and it matters.** \`AuthoredPlugin\` is what you write — it lets a view
73
+ point at a file with \`sourceFile\`. \`MarketplacePlugin\` is what the platform installs, where every
74
+ view carries its \`source\` inline. The compiler turns the first into the second. Write against
75
+ \`AuthoredPlugin\`; reach for \`MarketplacePlugin\` only when you are reading a built \`.json\` back.
76
+
77
+ ### Identity and versions
78
+
79
+ \`manifest.id\` is reverse-DNS and permanent: it is how an update recognises the thing it updates. Use
80
+ a domain you control. \`manifest.version\` is semver. \`manifest.kernel\` is the range of platform
81
+ contracts you work against — \`^${kernelVersion}\` for what is installed here.
82
+
83
+ Every change to an extension raises \`manifest.version\`, and records itself as one entry at the top of
84
+ that extension's \`VERSIONS.md\` — the file \`create\` generates next to \`plugin.ts\`. The entry is
85
+ written for whoever *uses* the extension: what they will notice, in one plain sentence, never the file
86
+ or the field you touched. An extension changed without a new version is an extension that never reaches
87
+ the organizations that already have it: the version is the whole of how an update is recognised.
88
+
89
+ ### References between your own artifacts
90
+
91
+ Inside the package, artifacts point at each other by \`templateId\` (automations, actions, scripts) or
92
+ \`key\` (screens), never by real id — the real ids are generated at install time and differ per
93
+ organization. \`check\` refuses a reference that leads nowhere.
94
+
95
+ To point at something *another* extension installs, declare it:
96
+
97
+ \`\`\`ts
98
+ dependencies: { 'com.example.projects': { version: '^2.0.0', collections: ['projects'] } }
99
+ \`\`\`
100
+
101
+ ## 4. Extension points
102
+
103
+ ${pointsTable(points)}
104
+
105
+ The \`Accepts\` column is the important one. Four kinds of implementation exist:
106
+
107
+ | Kind | What it is | Who may use it |
108
+ |---|---|---|
109
+ | \`widget\` | a declarative tree the platform renders (Widget DSL) | anyone |
110
+ | \`workflow\` | one of the automations in your package | anyone |
111
+ | \`script\` | code in the server-side sandbox | anyone |
112
+ | \`code\` | a React component, delivered as source | anyone — **but see §6** |
113
+
114
+ Prefer \`widget\` when it is enough. A declarative page needs no review, cannot break, and survives
115
+ platform upgrades without you touching it.
116
+
117
+ ## 5. Text — the rule that has no exception
118
+
119
+ Every label a person reads is a **reference**, resolved at render:
120
+
121
+ \`\`\`ts
122
+ const label = (key: string) => \\\`@com.example.hello/\\\${key}\\\`;
123
+
124
+ fields: [{ key: 'title', label: label('field.title'), type: 'text', order: 0 }]
125
+ \`\`\`
126
+
127
+ and the words themselves are a contribution:
128
+
129
+ \`\`\`ts
130
+ import { messageBundleContribution } from '@microteia/sdk';
131
+
132
+ contributes: [
133
+ messageBundleContribution({ locale: 'en', namespace: 'com.example.hello', messages: EN }),
134
+ messageBundleContribution({ locale: 'pt-BR', namespace: 'com.example.hello', messages: PT_BR }),
135
+ ]
136
+ \`\`\`
137
+
138
+ Ship \`en\` always — it is the source language, and what someone sees when no translation for their
139
+ locale is installed. **The platform does not translate your vocabulary.** "Document type" is the
140
+ platform's concept; *Power of attorney* and *Petition* are yours, and only you can answer for them.
141
+
142
+ A key with no translation renders as the key itself. Ugly, diagnosable, and never a blank screen.
143
+
144
+ What the organization *typed* — a screen they named, a list option they added — is their data. It
145
+ passes through untranslated, always.
146
+
147
+ ## 6. Code trust - why a human has to say yes
148
+
149
+ A \`view\` travels as **source**. The browser compiles it and evaluates it against a fixed allowlist of
150
+ modules:
151
+
152
+ ${hostModules.map((name) => `- \`${name}\``).join('\n')}
153
+
154
+ Any other import throws. This is an import allowlist, not a sandbox: the code runs in the page, so it
155
+ can reach \`fetch\` and the DOM like any script. The real containment is **provenance**.
156
+
157
+ So: a package carrying \`views\` or \`startup\` installs **inert**. An administrator of the organization
158
+ opens the source, reads it, and trusts it. The trust is bound to that exact code by content hash —
159
+ change one character and it must be trusted again. Nothing you can put in the package changes this,
160
+ and you should not design as though it might.
161
+
162
+ Author views as real files:
163
+
164
+ \`\`\`ts
165
+ views: [{ key: 'summary', title: label('page.summary'), icon: 'chart', sourceFile: './views/Summary.view.tsx' }]
166
+ \`\`\`
167
+
168
+ \`sourceFile\` is an authoring field: the compiler replaces it with the file's contents. Keeping the
169
+ code in a real \`.tsx\` is what lets \`tsc\` and your editor check the part of the extension most likely
170
+ to be wrong.
171
+
172
+ A view does not have to be one file. \`sourceFile\` also takes a list, merged in the order given:
173
+
174
+ \`\`\`ts
175
+ views: [{
176
+ key: 'summary',
177
+ title: label('page.summary'),
178
+ icon: 'chart',
179
+ sourceFile: ['./views/Summary.view.tsx', './views/Summary.viewmodel.tsx', './views/Summary.styles.tsx'],
180
+ }]
181
+ \`\`\`
182
+
183
+ The package still carries one source per view — the runtime evaluates one module per view, and its
184
+ \`require\` resolves nothing but the curated API. The list is an authoring convenience, and it changes
185
+ three things you have to know:
186
+
187
+ - **Import across the listed files normally.** The relative import is what makes \`tsc\` and your editor
188
+ connect them; the compiler erases it, because after the merge the names are already in scope. A
189
+ relative import pointing outside the list is refused at build time — the runtime could never resolve it.
190
+ - **The merged module has one scope.** A top-level name declared in two of the files collides, and the
191
+ build says which name and which files. Importing the same thing from the same module in several files
192
+ is not a collision: those imports fold into one.
193
+ - **Exactly one of the files exports \`default\`** — it is the view's component.
194
+
195
+ \`startupFile\` takes a list for the same reason, and so do \`source\` and \`startup\` when the code
196
+ reaches the package as text rather than as a path (a \`?raw\` import). There the merge cannot check a
197
+ path, so it checks names: every name a relative import brings has to be declared by one of the merged
198
+ sources, which is what catches merging the view and forgetting its view model.
199
+
200
+ \`startup\` receives a \`host\` and may return a teardown function:
201
+
202
+ \`\`\`ts
203
+ export default function startup(host: ExtensionHost) {
204
+ host.commands.register({ /* ... */ });
205
+ return () => { /* undone when the extension is disabled */ };
206
+ }
207
+ \`\`\`
208
+
209
+ ## 7. Data your extension owns
210
+
211
+ A \`screen\` declares an entity: a collection, its fields, its title. The platform draws the list, the
212
+ detail, the form, the filters and the search. You write no UI for any of it.
213
+
214
+ - **Fields the organization adds** to your screen are theirs. An update of your extension preserves them.
215
+ - **Option lists** (\`lookups\`) travel as a declaration plus optional *seed* options. Seeds are written
216
+ only when the collection is empty; after that the options are the organization's, and neither an
217
+ update nor uninstalling touches them.
218
+ - **\`screenPatches\`** add fields to a screen that already exists — someone else's, or the platform's.
219
+ They are added, never substituted, and uninstalling removes only yours.
220
+
221
+ ### Changing the shape of data you already wrote
222
+
223
+ That needs a \`migration\`: code that runs once per organization, in the server sandbox, converting
224
+ what an earlier version of *your* extension wrote. Never hard-code a collection name — read it from
225
+ \`input.collections\`, because the name can differ. Write it to be safely re-runnable: it may resume.
226
+
227
+ ## 8. When your extension needs the server
228
+
229
+ \`manifest.backend\` declares what the organization's Firebase project must provide — Cloud Functions,
230
+ composite indexes, configuration parameters, manual prerequisites. The extension stays **pending**,
231
+ and does not load, until an administrator runs \`microteia provision\`. Failing closed is deliberate: a
232
+ menu whose backend is missing produces the most expensive kind of defect, where the app opens and
233
+ every call fails looking like something else entirely.
234
+
235
+ ### You can write those functions yourself
236
+
237
+ Put them in \`<folder>/functions/\`, read through its \`index.ts\`, and name each one in
238
+ \`manifest.backend.functions\`. Then they behave exactly like the platform's own: deployed only where
239
+ the extension is installed, removed by \`microteia provision --prune\` when nothing needs them.
240
+
241
+ npx microteia-plugin create function --plugin <folder>
242
+
243
+ | What it needs | Where you declare it |
244
+ |---|---|
245
+ | an npm package the platform does not carry | \`dependencies\` in \`<folder>/functions/package.json\` |
246
+ | a credential or setting | \`defineString('NAME')\`, and \`'NAME'\` in \`manifest.backend.parameters\` |
247
+ | something a person must do by hand first | \`manifest.backend.prerequisites\` |
248
+
249
+ Two rules follow from how a Firebase project works, and both are refusals rather than warnings:
250
+
251
+ - **A \`defineString\` with no value refuses the deploy of *every* function in the project**, not just
252
+ yours — \`firebase-functions\` validates parameters globally. The Console lists every one it finds in
253
+ \`.env.<projectId>\`, empty, so filling it in is the only step left.
254
+ - **One installation, one \`node_modules\`.** Two extensions asking for different ranges of the same
255
+ package is refused, because nothing decides between them. A range the platform already pins is kept:
256
+ the runtime is the platform's choice, and one extension may not move it for the others.
257
+
258
+ A function shared by several extensions goes in \`../functions/\` instead, next to \`microteia.yaml\`.
259
+ Nobody owns it, so nothing can claim it, and it is deployed with every \`microteia deploy\`.
260
+
261
+ ## 9. Size
262
+
263
+ | | |
264
+ |---|---|
265
+ | Warning | 400 KiB |
266
+ | Refused | 800 KiB |
267
+
268
+ A package is stored as a single document. If you are near the limit, the usual cause is code that
269
+ should be several extensions, or a seed list that should be data.
270
+
271
+ ## 10. Testing
272
+
273
+ \`validatePluginPackage\` is exported from \`@microteia/sdk\` — the same function \`check\` calls. Keeping
274
+ it in a test means a broken package fails in a second, on your machine:
275
+
276
+ \`\`\`ts
277
+ import { validatePluginPackage } from '@microteia/sdk';
278
+ import plugin from './plugin';
279
+
280
+ it('installs cleanly', () => { expect(validatePluginPackage(plugin)).toEqual([]); });
281
+ \`\`\`
282
+
283
+ Read \`examples/\` for first-party extensions that exercise every mechanism described above.
284
+ `;
@@ -0,0 +1,19 @@
1
+ export interface BundleLeak {
2
+ file: string;
3
+ what: string;
4
+ }
5
+ /**
6
+ * Os valores dos `.env.*` da raiz. Comparar contra eles é melhor do que procurar um padrão: cobre
7
+ * qualquer ambiente que exista na máquina de quem empacota, inclusive um criado depois disto.
8
+ */
9
+ export declare const environmentSecrets: (root: string) => Promise<string[]>;
10
+ export declare const findBundleLeaks: (bundleDir: string, secrets: string[]) => Promise<BundleLeak[]>;
11
+ /**
12
+ * Os `.d.ts` de um diretório que ainda carregam comentário.
13
+ *
14
+ * Existe pelo mesmo motivo do resto deste arquivo: confere o ARTEFATO, não o procedimento. O
15
+ * `tsconfig.sdk.json` liga `removeComments`, mas uma flag num arquivo de configuração pode ser
16
+ * desligada sem que ninguém perceba — e o que vazaria é o JSDoc deste repositório, que é em português
17
+ * e narra decisões internas, para dentro do pacote de todo escritório que instalar o SDK.
18
+ */
19
+ export declare const declarationsWithComments: (directory: string) => Promise<string[]>;
@@ -0,0 +1,95 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ /**
4
+ * O bundle distribuído não pode carregar projeto Firebase nenhum.
5
+ *
6
+ * O risco não é teórico: até a versão 7.46 o Vite inlinava `VITE_*` em tempo de build, e um pacote
7
+ * montado com `TARGET=dev` levaria as credenciais de desenvolvimento embutidas. Como `config.ts` cai
8
+ * no valor inlinado quando ninguém carimba `firebase-config.js`, uma instalação mal carimbada
9
+ * conectaria em SILÊNCIO no Firebase de quem empacotou — escrevendo dados de cliente lá dentro.
10
+ *
11
+ * Por isso a verificação não confia no procedimento (`pack` compila com `TARGET=dist`, e `.env.dist`
12
+ * não existe): ela confere o artefato pronto.
13
+ */
14
+ const TEXT_EXTENSIONS = ['.js', '.mjs', '.cjs', '.html', '.css', '.json', '.map', '.txt'];
15
+ /** Prefixo de toda chave de API web do Firebase — pega o caso em que não há `.env` local para comparar. */
16
+ const FIREBASE_API_KEY_MARKER = 'AIzaSy';
17
+ const isTextFile = (name) => TEXT_EXTENSIONS.some((extension) => name.endsWith(extension));
18
+ const walk = async (directory, prefix = '') => {
19
+ const entries = await readdir(directory, { withFileTypes: true });
20
+ const files = [];
21
+ for (const entry of entries) {
22
+ const relative = prefix === '' ? entry.name : `${prefix}/${entry.name}`;
23
+ if (entry.isDirectory())
24
+ files.push(...await walk(join(directory, entry.name), relative));
25
+ else if (isTextFile(entry.name))
26
+ files.push(relative);
27
+ }
28
+ return files;
29
+ };
30
+ /**
31
+ * Os valores dos `.env.*` da raiz. Comparar contra eles é melhor do que procurar um padrão: cobre
32
+ * qualquer ambiente que exista na máquina de quem empacota, inclusive um criado depois disto.
33
+ */
34
+ export const environmentSecrets = async (root) => {
35
+ const entries = await readdir(root, { withFileTypes: true });
36
+ const values = new Set();
37
+ for (const entry of entries) {
38
+ if (!entry.isFile())
39
+ continue;
40
+ if (!entry.name.startsWith('.env.') || entry.name === '.env.example')
41
+ continue;
42
+ const content = await readFile(join(root, entry.name), 'utf8');
43
+ for (const line of content.split('\n')) {
44
+ const trimmed = line.trim();
45
+ if (trimmed === '' || trimmed.startsWith('#'))
46
+ continue;
47
+ const equals = trimmed.indexOf('=');
48
+ if (equals === -1)
49
+ continue;
50
+ const value = trimmed.slice(equals + 1).trim().replace(/^["']|["']$/g, '');
51
+ // Valores curtos demais dão falso positivo contra código minificado.
52
+ if (value.length >= 8)
53
+ values.add(value);
54
+ }
55
+ }
56
+ return [...values];
57
+ };
58
+ export const findBundleLeaks = async (bundleDir, secrets) => {
59
+ const files = await walk(bundleDir);
60
+ const leaks = [];
61
+ for (const file of files) {
62
+ const content = await readFile(join(bundleDir, file), 'utf8');
63
+ for (const secret of secrets) {
64
+ if (content.includes(secret))
65
+ leaks.push({ file, what: `environment value ${secret.slice(0, 8)}…` });
66
+ }
67
+ if (content.includes(FIREBASE_API_KEY_MARKER)) {
68
+ leaks.push({ file, what: `a Firebase web API key (${FIREBASE_API_KEY_MARKER}…)` });
69
+ }
70
+ }
71
+ return leaks;
72
+ };
73
+ /**
74
+ * Os `.d.ts` de um diretório que ainda carregam comentário.
75
+ *
76
+ * Existe pelo mesmo motivo do resto deste arquivo: confere o ARTEFATO, não o procedimento. O
77
+ * `tsconfig.sdk.json` liga `removeComments`, mas uma flag num arquivo de configuração pode ser
78
+ * desligada sem que ninguém perceba — e o que vazaria é o JSDoc deste repositório, que é em português
79
+ * e narra decisões internas, para dentro do pacote de todo escritório que instalar o SDK.
80
+ */
81
+ export const declarationsWithComments = async (directory) => {
82
+ const found = [];
83
+ const walk = async (current) => {
84
+ for (const entry of await readdir(current, { withFileTypes: true })) {
85
+ const path = join(current, entry.name);
86
+ if (entry.isDirectory())
87
+ await walk(path);
88
+ else if (entry.name.endsWith('.d.ts') && (await readFile(path, 'utf8')).includes('/*')) {
89
+ found.push(path);
90
+ }
91
+ }
92
+ };
93
+ await walk(directory);
94
+ return found.sort();
95
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * O histórico técnico dos DOIS artefatos que o escritório recebe — a plataforma e o Console.
3
+ *
4
+ * Cada um carrega o seu, e essa é a decisão inteira. O `KERNEL-VERSIONS.md` viaja dentro do pacote; o
5
+ * `CONSOLE-VERSIONS.md` viaja com o binário do Console. Enquanto os dois textos saem do artefato que
6
+ * eles descrevem, nenhum dos dois pode falar de uma versão que não está ali — que é exatamente o que
7
+ * acontecia com os guias de `extensions/` enquanto eles moravam no Console.
8
+ *
9
+ * Os dois são ESCRITOS EM INGLÊS, ao contrário do `VERSIONS.md` da raiz do repositório. A regra é a de
10
+ * sempre: o que é entregue a um escritório que pode estar em qualquer lugar é superfície de produto e
11
+ * fala inglês; o que é comunicação nossa continua em português. O `VERSIONS.md` da raiz é para o
12
+ * usuário do escritório e não viaja no pacote; estes dois são o registro do que mudou no código, para
13
+ * quem mantém ou integra uma instalação.
14
+ *
15
+ * **NÃO ficam em `extensions/`.** Aquele diretório é opcional (pode nem existir, e pode ter sido criado
16
+ * com `--dir` em qualquer lugar), é escrito pelo SDK, e já tem um `VERSIONS.md` por extensão dentro
17
+ * dele — um segundo arquivo com o mesmo nome, um nível acima, significando outra coisa. Os dois moram
18
+ * na raiz da instalação, ao lado do `microteia.yaml`, que é onde o `--version` aponta.
19
+ */
20
+ export declare const KERNEL_CHANGELOG = "KERNEL-VERSIONS.md";
21
+ export declare const CONSOLE_CHANGELOG = "CONSOLE-VERSIONS.md";
22
+ export declare const changelogVersions: (contents: string) => string[];
23
+ /**
24
+ * O que impede o arquivo de ser confiável, na ordem em que atrapalha.
25
+ *
26
+ * A primeira é a que existe para doer: a versão que está sendo empacotada tem de ser a do TOPO. Sem
27
+ * isso o histórico é opcional na prática, e um histórico opcional para na primeira release apressada
28
+ * — que é justamente a que alguém vai querer ler depois.
29
+ */
30
+ export declare const changelogProblems: (file: string, contents: string, version: string) => string[];
31
+ /**
32
+ * Lê o histórico e RECUSA se ele não cobre a versão que está saindo. Usado pelo `pack` e pelo
33
+ * `pack-console`, que são os dois momentos em que ainda dá para escrever a entrada.
34
+ *
35
+ * A recusa é do artefato, não do procedimento — mesma postura do `bundleAudit` e da conferência de
36
+ * reivindicações de backend: o que sai daqui vai para escritórios, e um pacote cujo histórico não
37
+ * menciona a própria versão é um pacote que ninguém consegue datar depois.
38
+ */
39
+ export declare const readChangelog: (directory: string, file: string, version: string) => Promise<string>;
40
+ /**
41
+ * A entrada de UMA versão, para ser o corpo do release publicado.
42
+ *
43
+ * O texto do release não é escrito à mão em lugar nenhum: ele é o mesmo parágrafo que já foi conferido
44
+ * pelo `pack`, recortado daqui. Dois textos sobre a mesma versão divergem — e o que divergiria é
45
+ * justamente o que fica visível para quem procura o que mudou.
46
+ *
47
+ * Recorta do cabeçalho da versão até o próximo cabeçalho, e não até o fim: o que vem depois é o
48
+ * histórico anterior, e ele já está publicado nos releases dele.
49
+ */
50
+ export declare const changelogEntry: (contents: string, version: string) => string;
51
+ export interface ChangelogRefresh {
52
+ written: string[];
53
+ missing: string[];
54
+ }
55
+ /**
56
+ * Põe os dois históricos na raiz da instalação, SOBREPONDO — pela mesma razão que o `MICROTEIA.md` é
57
+ * sobreposto: eles prometem estar atuais, e um documento que promete isso não pode ser editável.
58
+ *
59
+ * A falta de um dos dois avisa e não derruba nada. O do kernel falta num pacote anterior a esta
60
+ * capacidade, e trocar de kernel não pode falhar por causa de um documento.
61
+ */
62
+ export declare const refreshChangelogs: (root: string, install: string) => Promise<ChangelogRefresh>;
@@ -0,0 +1,131 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { consoleRoot } from '../shared/paths.js';
4
+ import { compareVersions } from '../kernel/manifest.js';
5
+ /**
6
+ * O histórico técnico dos DOIS artefatos que o escritório recebe — a plataforma e o Console.
7
+ *
8
+ * Cada um carrega o seu, e essa é a decisão inteira. O `KERNEL-VERSIONS.md` viaja dentro do pacote; o
9
+ * `CONSOLE-VERSIONS.md` viaja com o binário do Console. Enquanto os dois textos saem do artefato que
10
+ * eles descrevem, nenhum dos dois pode falar de uma versão que não está ali — que é exatamente o que
11
+ * acontecia com os guias de `extensions/` enquanto eles moravam no Console.
12
+ *
13
+ * Os dois são ESCRITOS EM INGLÊS, ao contrário do `VERSIONS.md` da raiz do repositório. A regra é a de
14
+ * sempre: o que é entregue a um escritório que pode estar em qualquer lugar é superfície de produto e
15
+ * fala inglês; o que é comunicação nossa continua em português. O `VERSIONS.md` da raiz é para o
16
+ * usuário do escritório e não viaja no pacote; estes dois são o registro do que mudou no código, para
17
+ * quem mantém ou integra uma instalação.
18
+ *
19
+ * **NÃO ficam em `extensions/`.** Aquele diretório é opcional (pode nem existir, e pode ter sido criado
20
+ * com `--dir` em qualquer lugar), é escrito pelo SDK, e já tem um `VERSIONS.md` por extensão dentro
21
+ * dele — um segundo arquivo com o mesmo nome, um nível acima, significando outra coisa. Os dois moram
22
+ * na raiz da instalação, ao lado do `microteia.yaml`, que é onde o `--version` aponta.
23
+ */
24
+ export const KERNEL_CHANGELOG = 'KERNEL-VERSIONS.md';
25
+ export const CONSOLE_CHANGELOG = 'CONSOLE-VERSIONS.md';
26
+ /**
27
+ * O cabeçalho de uma entrada. É o MESMO formato dos outros históricos do produto — o `VERSIONS.md` da
28
+ * raiz e o de cada extensão que o `create` gera —, para que não existam duas convenções para a mesma
29
+ * coisa.
30
+ */
31
+ const ENTRY = /^#### \[(\d+\.\d+\.\d+)\] — /gm;
32
+ /**
33
+ * O comentário HTML do topo é o MOLDE da entrada nova, e ele mostra um cabeçalho de verdade — é o que faz
34
+ * alguém acertar o formato sem procurar. Por isso ele sai antes da contagem: senão o molde é lido como a
35
+ * entrada mais recente do arquivo, e o histórico inteiro parece fora de ordem.
36
+ */
37
+ const withoutTemplates = (contents) => contents.replace(/<!--[\s\S]*?-->/g, '');
38
+ export const changelogVersions = (contents) => [...withoutTemplates(contents).matchAll(ENTRY)].map((match) => match[1]);
39
+ /**
40
+ * O que impede o arquivo de ser confiável, na ordem em que atrapalha.
41
+ *
42
+ * A primeira é a que existe para doer: a versão que está sendo empacotada tem de ser a do TOPO. Sem
43
+ * isso o histórico é opcional na prática, e um histórico opcional para na primeira release apressada
44
+ * — que é justamente a que alguém vai querer ler depois.
45
+ */
46
+ export const changelogProblems = (file, contents, version) => {
47
+ const versions = changelogVersions(contents);
48
+ const problems = [];
49
+ if (versions.length === 0) {
50
+ problems.push(`${file} carries no entry at all. Expected one headed "#### [${version}] — <date>".`);
51
+ return problems;
52
+ }
53
+ if (versions[0] !== version) {
54
+ problems.push(versions.includes(version)
55
+ ? `${file} has an entry for ${version}, but ${versions[0]} is on top. The newest goes first.`
56
+ : `${file} has no entry for ${version}, which is the version being packaged. Add one on top.`);
57
+ }
58
+ for (let index = 1; index < versions.length; index += 1) {
59
+ const order = compareVersions(versions[index - 1], versions[index]);
60
+ if (order === 0)
61
+ problems.push(`${file} lists ${versions[index]} twice.`);
62
+ else if (order < 0) {
63
+ problems.push(`${file} lists ${versions[index - 1]} above ${versions[index]}, and the newest goes first.`);
64
+ }
65
+ }
66
+ return problems;
67
+ };
68
+ /**
69
+ * Lê o histórico e RECUSA se ele não cobre a versão que está saindo. Usado pelo `pack` e pelo
70
+ * `pack-console`, que são os dois momentos em que ainda dá para escrever a entrada.
71
+ *
72
+ * A recusa é do artefato, não do procedimento — mesma postura do `bundleAudit` e da conferência de
73
+ * reivindicações de backend: o que sai daqui vai para escritórios, e um pacote cujo histórico não
74
+ * menciona a própria versão é um pacote que ninguém consegue datar depois.
75
+ */
76
+ export const readChangelog = async (directory, file, version) => {
77
+ const contents = await readFile(join(directory, file), 'utf8').catch(() => {
78
+ throw new Error(`${join(directory, file)} does not exist, and every packaged version needs its entry there.`);
79
+ });
80
+ const problems = changelogProblems(file, contents, version);
81
+ if (problems.length > 0) {
82
+ throw new Error(`The technical changelog does not cover this version:\n${problems.map((problem) => ` ${problem}`).join('\n')}`);
83
+ }
84
+ return contents;
85
+ };
86
+ /**
87
+ * A entrada de UMA versão, para ser o corpo do release publicado.
88
+ *
89
+ * O texto do release não é escrito à mão em lugar nenhum: ele é o mesmo parágrafo que já foi conferido
90
+ * pelo `pack`, recortado daqui. Dois textos sobre a mesma versão divergem — e o que divergiria é
91
+ * justamente o que fica visível para quem procura o que mudou.
92
+ *
93
+ * Recorta do cabeçalho da versão até o próximo cabeçalho, e não até o fim: o que vem depois é o
94
+ * histórico anterior, e ele já está publicado nos releases dele.
95
+ */
96
+ export const changelogEntry = (contents, version) => {
97
+ const body = withoutTemplates(contents);
98
+ const headings = [...body.matchAll(ENTRY)];
99
+ const index = headings.findIndex((match) => match[1] === version);
100
+ if (index === -1) {
101
+ throw new Error(`The changelog has no entry for ${version}.`);
102
+ }
103
+ const start = headings[index].index;
104
+ const end = index + 1 < headings.length ? headings[index + 1].index : body.length;
105
+ return `${body.slice(start, end).trim()}\n`;
106
+ };
107
+ /**
108
+ * Põe os dois históricos na raiz da instalação, SOBREPONDO — pela mesma razão que o `MICROTEIA.md` é
109
+ * sobreposto: eles prometem estar atuais, e um documento que promete isso não pode ser editável.
110
+ *
111
+ * A falta de um dos dois avisa e não derruba nada. O do kernel falta num pacote anterior a esta
112
+ * capacidade, e trocar de kernel não pode falhar por causa de um documento.
113
+ */
114
+ export const refreshChangelogs = async (root, install) => {
115
+ const sources = [
116
+ [join(install, KERNEL_CHANGELOG), KERNEL_CHANGELOG],
117
+ [join(consoleRoot, CONSOLE_CHANGELOG), CONSOLE_CHANGELOG],
118
+ ];
119
+ const written = [];
120
+ const missing = [];
121
+ for (const [from, name] of sources) {
122
+ const contents = await readFile(from, 'utf8').catch(() => null);
123
+ if (contents === null) {
124
+ missing.push(name);
125
+ continue;
126
+ }
127
+ await writeFile(join(root, name), contents);
128
+ written.push(name);
129
+ }
130
+ return { written, missing };
131
+ };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * As extensões de primeira parte, empacotadas como FONTE — `plugin.ts` e as views em `.tsx` —, uma pasta
3
+ * por extensão.
4
+ *
5
+ * Viajavam como `.json`. Um `.json` de 90 KB não ensina nada: é o artefato, e quem abre não vê como se
6
+ * chega nele. O que ensina é a fonte, e ela já viaja de qualquer forma — o código das views está dentro
7
+ * do `.json`, em string. Passar a mandar o `.ts` não expõe nada novo; expõe de forma legível o que já ia.
8
+ *
9
+ * A única transformação é de IMPORT: neste repositório um plugin importa `../../domain/entities/...` e
10
+ * `../contributionBuilders`; do lado do escritório, tudo isso é `@microteia/sdk`. Todo símbolo usado
11
+ * pelos doze está no barril do SDK — e é por isso que a conferência é possível: `exampleSources.test.ts`
12
+ * COMPILA cada exemplo reescrito e exige que o `.json` saia byte a byte igual ao versionado. Um símbolo
13
+ * que faltasse no barril reprovaria ali, e não no escritório de alguém.
14
+ */
15
+ /** Arquivo distribuído → módulo de origem. O nome da pasta do exemplo é o do `.json`, sem extensão. */
16
+ export declare const EXAMPLE_SOURCES: Record<string, string>;
17
+ /**
18
+ * Reescreve os imports de um plugin para a forma que o escritório usa.
19
+ *
20
+ * Um especificador relativo que alcance `domain/` ou os construtores de contribuição é, do lado de fora,
21
+ * `@microteia/sdk`. As views passam a ser vizinhas do `plugin.ts` (`./views/X`), porque no exemplo cada
22
+ * extensão tem a sua pasta — aqui elas moram num diretório comum, com um nível a mais.
23
+ */
24
+ export declare const rewriteImports: (source: string) => string;
25
+ /**
26
+ * Escreve `<target>/<plugin>/plugin.ts` e `<target>/<plugin>/views/*` para cada exemplo. Devolve os
27
+ * nomes escritos, que é o que o `pack` reporta.
28
+ */
29
+ export declare const stageExamples: (repo: string, target: string) => Promise<string[]>;