@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.
- package/CONSOLE-VERSIONS.md +85 -0
- package/README.md +64 -0
- package/lib/console/backendEnv.d.ts +52 -0
- package/lib/console/backendEnv.js +118 -0
- package/lib/console/backendIndexes.d.ts +40 -0
- package/lib/console/backendIndexes.js +178 -0
- package/lib/console/backendParameters.d.ts +48 -0
- package/lib/console/backendParameters.js +100 -0
- package/lib/console/callableAccess.d.ts +39 -0
- package/lib/console/callableAccess.js +61 -0
- package/lib/console/channel.d.ts +49 -0
- package/lib/console/channel.js +145 -0
- package/lib/console/connect.d.ts +2 -0
- package/lib/console/connect.js +74 -0
- package/lib/console/customFunctions.d.ts +120 -0
- package/lib/console/customFunctions.js +298 -0
- package/lib/console/deploy.d.ts +2 -0
- package/lib/console/deploy.js +261 -0
- package/lib/console/deployFunctions.d.ts +45 -0
- package/lib/console/deployFunctions.js +204 -0
- package/lib/console/environment.d.ts +62 -0
- package/lib/console/environment.js +151 -0
- package/lib/console/firebaseCli.d.ts +15 -0
- package/lib/console/firebaseCli.js +90 -0
- package/lib/console/firestoreRest.d.ts +39 -0
- package/lib/console/firestoreRest.js +131 -0
- package/lib/console/hostingCheck.d.ts +29 -0
- package/lib/console/hostingCheck.js +44 -0
- package/lib/console/init.d.ts +2 -0
- package/lib/console/init.js +60 -0
- package/lib/console/pluginWorkspace.d.ts +48 -0
- package/lib/console/pluginWorkspace.js +148 -0
- package/lib/console/provision.d.ts +70 -0
- package/lib/console/provision.js +478 -0
- package/lib/console/regionOrphans.d.ts +43 -0
- package/lib/console/regionOrphans.js +99 -0
- package/lib/console/runtimeConfigShape.d.ts +25 -0
- package/lib/console/runtimeConfigShape.js +35 -0
- package/lib/console/unpack.d.ts +9 -0
- package/lib/console/unpack.js +333 -0
- package/lib/console/versions.d.ts +10 -0
- package/lib/console/versions.js +72 -0
- package/lib/console/workspace.d.ts +19 -0
- package/lib/console/workspace.js +33 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +136 -0
- package/lib/kernel/backendShape.d.ts +74 -0
- package/lib/kernel/backendShape.js +91 -0
- package/lib/kernel/channel.d.ts +79 -0
- package/lib/kernel/channel.js +157 -0
- package/lib/kernel/manifest.d.ts +138 -0
- package/lib/kernel/manifest.js +145 -0
- package/lib/shared/args.d.ts +14 -0
- package/lib/shared/args.js +44 -0
- package/lib/shared/digest.d.ts +30 -0
- package/lib/shared/digest.js +34 -0
- package/lib/shared/paths.d.ts +90 -0
- package/lib/shared/paths.js +97 -0
- package/lib/shared/run.d.ts +28 -0
- package/lib/shared/run.js +49 -0
- package/lib/shared/ui.d.ts +23 -0
- package/lib/shared/ui.js +43 -0
- package/lib/vendor/authoringGuide.d.ts +19 -0
- package/lib/vendor/authoringGuide.js +284 -0
- package/lib/vendor/bundleAudit.d.ts +19 -0
- package/lib/vendor/bundleAudit.js +95 -0
- package/lib/vendor/changelogs.d.ts +62 -0
- package/lib/vendor/changelogs.js +131 -0
- package/lib/vendor/exampleSources.d.ts +29 -0
- package/lib/vendor/exampleSources.js +126 -0
- package/lib/vendor/installationDocs.d.ts +35 -0
- package/lib/vendor/installationDocs.js +243 -0
- package/lib/vendor/pack.d.ts +2 -0
- package/lib/vendor/pack.js +276 -0
- package/lib/vendor/packConsole.d.ts +13 -0
- package/lib/vendor/packConsole.js +63 -0
- package/lib/vendor/packageTemplates.d.ts +66 -0
- package/lib/vendor/packageTemplates.js +200 -0
- package/lib/vendor/pluginWorkspaceTemplates.d.ts +19 -0
- package/lib/vendor/pluginWorkspaceTemplates.js +29 -0
- package/lib/vendor/publish.d.ts +2 -0
- package/lib/vendor/publish.js +277 -0
- package/lib/vendor/repository.d.ts +9 -0
- package/lib/vendor/repository.js +26 -0
- 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[]>;
|