@microteia/console 2.19.0 → 2.21.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.
@@ -22,6 +22,33 @@
22
22
  - fix(command): what was broken, and what it did to whoever hit it.
23
23
  -->
24
24
 
25
+ #### [2.21.0] — 2026-09-09
26
+
27
+ - fix(cli/pack): reading-only examples are left out of the first-party backend accounting.
28
+ `extension-templates/` holds two kinds of package with the same shape and different owners — the
29
+ first-party extensions, whose Cloud Functions this kernel compiles and deploys, and the examples,
30
+ whose backend belongs to whoever installs them. The pack derives its baseline as
31
+ `all functions − claimed`, so the first example to teach Cloud Functions would have aborted every
32
+ publish by claiming a name that is not here, and a name that WAS here would have been taken out of
33
+ the kernel baseline by a mere template. `firstPartyTemplates` is now the single door onto that
34
+ directory, and `READING_ONLY_EXAMPLES` is derived from where an example's source lives rather than
35
+ kept as a second list.
36
+ - feat(cli/vendor): `stageExamples` carries `functions/` and `scripts/` beside `views/`, and an
37
+ example whose source is a DIRECTORY takes `plugin.ts` as its entry. Without both, an example could
38
+ not teach a Cloud Function or a sandbox script — the two files simply would not travel.
39
+ - fix(cli/vendor): `rewriteImports` sends the node-only modules to `@microteia/sdk/node` instead of to
40
+ the isomorphic barrel, which is where `transpileScriptSource` deliberately is not. The symptom
41
+ would have been in the office of whoever installed, not here.
42
+ - fix(cli/vendor): the `MICROTEIA.md` it writes derives its build command from the examples actually
43
+ present. It named `examples/pessoa`, which left the repository with the office extensions — a
44
+ generated document telling the reader to compile something that is not there.
45
+
46
+ #### [2.20.0] — 2026-09-01
47
+
48
+ - feat(cli/export-catalog): the snapshot carries what each installed extension declares in `manifest.exports` — the artifacts another extension may reference as `<extension-id>/<source-id>`. Whoever authors outside the office knew an extension was installed and had no way to learn the name of anything inside it, so the reference could be written by hand and never discovered. Each entry carries the id, the artifact's name, and its `kind`, which is the part that decides where the reference fits: a script goes in `scriptId`, an automation in `targetWorkflowId`. What the artifact DOES still never travels — the steps and the code are behaviour, and the snapshot is shape.
49
+ - feat(cli/export-catalog): a declared export that matches no artifact in the package travels as `unknown` rather than being dropped. The install gate checks the LIST, so a reference to it would be accepted there and fail only on the step's first run; saying it is better than hiding it.
50
+ - feat(cli/export-catalog): `exports` is written for every extension, empty list included. A present-and-empty list is the answer "this extension exports nothing"; an absent key is the silence of a Console older than the field, and the kernel tells the two apart instead of reading absence as a claim.
51
+
25
52
  #### [2.19.0] — 2026-08-28
26
53
 
27
54
  - fix(cli/deploy): the installation gate is charged against the functions that are GOING UP, not against every source found on disk. It lived in `functionProblems` — the door that answers "can this set be assembled?" — and that door reads every `extensions/<x>/functions/` directory that exists, including one left behind by an extension that was uninstalled and removed. The result was `microteia deploy` refusing in full — hosting, rules and indexes with it — over a callable no command would have deployed. `gateProblems` now carries the function's name, and `gateProblemsFor` filters by what is being deployed.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Microteia Console 2.19.0
1
+ # Microteia Console 2.21.0
2
2
 
3
3
  Installs and updates Microteia in a Firebase project you own. It has no npm dependencies of its own —
4
4
  what it needs are command-line tools that are already on the machine, listed at the bottom.
@@ -76,11 +76,33 @@ export interface CatalogListExport {
76
76
  label: string;
77
77
  ownerPluginId?: string;
78
78
  }
79
+ /**
80
+ * Um artefato que a extensão declara em `manifest.exports` — a superfície que outra extensão pode
81
+ * referenciar como `<pluginId>/<sourceId>`.
82
+ *
83
+ * O `kind` viaja porque é ele que decide ONDE a referência cabe: script em `scriptId`, automação em
84
+ * `targetWorkflowId`. `unknown` é o export declarado que não casa com artefato nenhum do pacote —
85
+ * dizê-lo é melhor que omiti-lo, porque uma referência a ele passaria no portão da instalação, que
86
+ * confere a LISTA, e falharia só na primeira execução.
87
+ *
88
+ * O que NÃO viaja é o que o artefato faz: os passos e o código são comportamento, e o retrato é
89
+ * esquema. O nome e o tipo são contrato.
90
+ */
91
+ export interface CatalogExtensionExportedArtifact {
92
+ id: string;
93
+ kind: 'workflow' | 'action' | 'script' | 'unknown';
94
+ name: string;
95
+ }
79
96
  export interface CatalogExtensionExport {
80
97
  id: string;
81
98
  name: string;
82
99
  version: string;
83
100
  active: boolean;
101
+ /**
102
+ * Sempre escrita, mesmo vazia. É a lista PRESENTE e vazia que diz "esta extensão não exporta nada";
103
+ * a chave ausente é o silêncio de um Console anterior a este campo, e o kernel distingue as duas.
104
+ */
105
+ exports: CatalogExtensionExportedArtifact[];
84
106
  }
85
107
  export interface CatalogExport {
86
108
  version: number;
@@ -46,6 +46,38 @@ const asRecord = (value) => (typeof value === 'object' && value !== null && !Arr
46
46
  ? value
47
47
  : undefined);
48
48
  const FIELD_ORIGINS = ['builtin', 'plugin', 'design'];
49
+ /**
50
+ * A superfície exportada de uma cópia instalada: os ids de `manifest.exports`, cada um casado com o
51
+ * artefato do pacote que o atende, para ganhar tipo e nome.
52
+ *
53
+ * O Console não alcança `src/` — logo não há `MarketplacePlugin` tipado aqui, e a leitura é do
54
+ * documento cru, como todo o resto deste arquivo. Um export que não casa com nada continua na lista
55
+ * como `unknown`: a extensão está declarando algo que não entrega, e esconder isso do autor apenas
56
+ * adia a descoberta para a primeira execução.
57
+ */
58
+ const exportedArtifacts = (extension) => {
59
+ const declared = extension.manifest?.exports;
60
+ if (!Array.isArray(declared))
61
+ return [];
62
+ const pkg = asRecord(extension.data.package) ?? {};
63
+ const named = new Map();
64
+ const collect = (raw, kind) => {
65
+ for (const one of Array.isArray(raw) ? raw : []) {
66
+ const record = asRecord(one);
67
+ const id = text(record?.templateId);
68
+ if (id === undefined || named.has(id))
69
+ continue;
70
+ named.set(id, { id, kind, name: text(record?.name) ?? text(record?.title) ?? id });
71
+ }
72
+ };
73
+ collect(pkg.workflows, 'workflow');
74
+ collect(pkg.actions, 'action');
75
+ collect(pkg.scripts, 'script');
76
+ return declared
77
+ .filter((id) => typeof id === 'string' && id !== '')
78
+ .map((id) => named.get(id) ?? { id, kind: 'unknown', name: id })
79
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
80
+ };
49
81
  /**
50
82
  * A mesma dedução que a aplicação faz ao ler a tela (`screenDefinitionMapper.ts`): o `origin` gravado
51
83
  * quando ele existe, senão `plugin` para quem tem dono e `design` para o que o escritório criou. Ela é
@@ -135,7 +167,13 @@ export const buildCatalogExport = (input) => ({
135
167
  kernelVersion: input.kernelVersion,
136
168
  consoleVersion: CLI_VERSION,
137
169
  extensions: input.extensions
138
- .map((one) => ({ id: one.id, name: one.name, version: one.version, active: one.active }))
170
+ .map((one) => ({
171
+ id: one.id,
172
+ name: one.name,
173
+ version: one.version,
174
+ active: one.active,
175
+ exports: exportedArtifacts(one),
176
+ }))
139
177
  .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)),
140
178
  screens: input.screens
141
179
  .filter((document) => document.data.enabled !== false)
@@ -252,13 +290,16 @@ export const exportCatalog = async (args) => {
252
290
  + `${document.lookups.length} list(s), ${document.tagLists.length} tag list(s)`);
253
291
  info(` ${fromExtensions} screen(s) come from the ${document.extensions.length} installed extension(s); `
254
292
  + `the rest the office built itself.`);
293
+ const exported = document.extensions.reduce((total, one) => total + one.exports.length, 0);
294
+ info(` ${exported} artifact(s) are declared for other extensions to reference as <extension-id>/<id>.`);
255
295
  blank();
256
296
  await checkIgnored(directory);
257
297
  info('It is a SNAPSHOT, and it says so: the authoring tools report when it was taken and warn when it');
258
298
  info('is old. Nothing keeps it in step with the office — run this again after screens change there.');
259
299
  blank();
260
300
  info('What it deliberately leaves out: records, the values inside a list of options or a tag list,');
261
- info('members and permissions, workflows, scripts, actions, secrets and settings. It describes what');
262
- info('the entities ARE, and nothing an office typed into them.');
301
+ info('members and permissions, what a workflow, script or action DOES, secrets and settings. It');
302
+ info('describes what the entities ARE, and nothing an office typed into them. What each extension');
303
+ info('declares in "exports" travels: that is the surface another extension may reference.');
263
304
  blank();
264
305
  };
@@ -87,4 +87,4 @@ export declare const consoleRoot: string;
87
87
  * de código nunca chegou a quem instala. Este bloco fica sendo o que ele já era antes de crescer: a
88
88
  * política de semver da ferramenta, mais o registro do que motivou as versões até a `2.5.0`.
89
89
  */
90
- export declare const CLI_VERSION = "2.19.0";
90
+ export declare const CLI_VERSION = "2.21.0";
@@ -94,4 +94,4 @@ export const consoleRoot = resolve(here, '..', '..');
94
94
  * de código nunca chegou a quem instala. Este bloco fica sendo o que ele já era antes de crescer: a
95
95
  * política de semver da ferramenta, mais o registro do que motivou as versões até a `2.5.0`.
96
96
  */
97
- export const CLI_VERSION = '2.19.0';
97
+ export const CLI_VERSION = '2.21.0';
@@ -1,19 +1,33 @@
1
1
  /**
2
- * As extensões de primeira parte, empacotadas como FONTE — `plugin.ts` e as views em `.tsx` —, uma pasta
3
- * por extensão.
2
+ * Arquivo distribuído → módulo de origem, relativo a `src/plugins/marketplace/`. O nome da pasta do
3
+ * exemplo é o do `.json`, sem extensão.
4
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.
5
+ * Há DUAS formas de origem, e a diferença não é de estilo: é de quem o módulo é.
8
6
  *
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.
7
+ * - **Um arquivo** (`automation.ts`) — a extensão mora na raiz do marketplace porque ela é, ou já foi,
8
+ * uma extensão de PRIMEIRA PARTE do catálogo embutido. Os vizinhos que ela importa são descobertos
9
+ * por `siblingModules`, e as views dela ficam em `views/<módulo>/`.
10
+ * - **Um diretório** (`examples/report-example`) — a extensão existe SÓ para ser lida. Nada dela entra
11
+ * no `MARKETPLACE_CATALOG`, então ela não tem por que ocupar a raiz do marketplace junto das que
12
+ * entram: ali as cinco de primeira parte se perderiam no meio de doze moldes. Tudo o que é dela mora
13
+ * na pasta dela — o `plugin.ts`, os catálogos de mensagem e as `views/`.
14
14
  */
15
- /** Arquivo distribuído → módulo de origem. O nome da pasta do exemplo é o do `.json`, sem extensão. */
16
15
  export declare const EXAMPLE_SOURCES: Record<string, string>;
16
+ /**
17
+ * Os exemplos que existem SÓ para ser lidos — os que não estão no catálogo embutido.
18
+ *
19
+ * A pergunta que eles respondem é do `pack`: de quem é o backend que este repositório sobe. Uma
20
+ * extensão de primeira parte que declare `manifest.backend` reivindica uma function que o kernel
21
+ * compila e deploya, e o `pack` confere isso — reivindicar um nome que não existe aborta o
22
+ * empacotamento, e reivindicar um que existe o TIRA da baseline. Um exemplo não é nada disso: o
23
+ * `functions/` dele viaja como fonte para ser lido, e quem o instalar de verdade constrói o próprio.
24
+ * Sem esta distinção, o primeiro exemplo a ensinar Cloud Functions reprovaria toda publicação.
25
+ *
26
+ * É DERIVADA da forma da origem, e não uma segunda lista a manter: morar em `examples/` é
27
+ * exatamente o que significa não estar no catálogo embutido. As cinco de primeira parte moram na raiz
28
+ * do marketplace porque o bundle as carrega, e é por isso que elas continuam sendo cobradas.
29
+ */
30
+ export declare const READING_ONLY_EXAMPLES: ReadonlySet<string>;
17
31
  /**
18
32
  * Reescreve os imports de um plugin para a forma que o escritório usa.
19
33
  *
@@ -10,19 +10,70 @@ import { join } from 'node:path';
10
10
  *
11
11
  * A única transformação é de IMPORT: neste repositório um plugin importa `../../domain/entities/...` e
12
12
  * `../contributionBuilders`; do lado do escritório, tudo isso é `@microteia/sdk`. Todo símbolo usado
13
- * pelos doze está no barril do SDK — e é por isso que a conferência é possível: `exampleSources.test.ts`
13
+ * por eles está no barril do SDK — e é por isso que a conferência é possível: `exampleSources.test.ts`
14
14
  * COMPILA cada exemplo reescrito e exige que o `.json` saia byte a byte igual ao versionado. Um símbolo
15
15
  * que faltasse no barril reprovaria ali, e não no escritório de alguém.
16
16
  */
17
- /** Arquivo distribuído → módulo de origem. O nome da pasta do exemplo é o do `.json`, sem extensão. */
17
+ /** A pasta, dentro do marketplace, em que mora um exemplo que existe só para ser lido. */
18
+ const EXAMPLES_DIR = 'examples';
19
+ /**
20
+ * Arquivo distribuído → módulo de origem, relativo a `src/plugins/marketplace/`. O nome da pasta do
21
+ * exemplo é o do `.json`, sem extensão.
22
+ *
23
+ * Há DUAS formas de origem, e a diferença não é de estilo: é de quem o módulo é.
24
+ *
25
+ * - **Um arquivo** (`automation.ts`) — a extensão mora na raiz do marketplace porque ela é, ou já foi,
26
+ * uma extensão de PRIMEIRA PARTE do catálogo embutido. Os vizinhos que ela importa são descobertos
27
+ * por `siblingModules`, e as views dela ficam em `views/<módulo>/`.
28
+ * - **Um diretório** (`examples/report-example`) — a extensão existe SÓ para ser lida. Nada dela entra
29
+ * no `MARKETPLACE_CATALOG`, então ela não tem por que ocupar a raiz do marketplace junto das que
30
+ * entram: ali as cinco de primeira parte se perderiam no meio de doze moldes. Tudo o que é dela mora
31
+ * na pasta dela — o `plugin.ts`, os catálogos de mensagem e as `views/`.
32
+ */
18
33
  export const EXAMPLE_SOURCES = {
19
- 'automation-example': 'automation.ts',
34
+ 'automation-example': 'examples/automation-example',
20
35
  brasil: 'brasil.ts',
21
36
  calendar: 'calendar.ts',
22
37
  dashboard: 'dashboard.ts',
23
38
  'locale-pt-br': 'localePtBr',
24
39
  news: 'news.ts',
40
+ 'report-example': 'examples/report-example',
41
+ 'widget-example': 'examples/widget-example',
42
+ 'access-example': 'examples/access-example',
43
+ 'backend-example': 'examples/backend-example',
44
+ 'library-example': 'examples/library-example',
45
+ 'consumer-example': 'examples/consumer-example',
46
+ 'patch-example': 'examples/patch-example',
47
+ 'theme-example': 'examples/theme-example',
48
+ 'auth-example': 'examples/auth-example',
49
+ 'command-example': 'examples/command-example',
50
+ 'settings-example': 'examples/settings-example',
25
51
  };
52
+ /**
53
+ * Os exemplos que existem SÓ para ser lidos — os que não estão no catálogo embutido.
54
+ *
55
+ * A pergunta que eles respondem é do `pack`: de quem é o backend que este repositório sobe. Uma
56
+ * extensão de primeira parte que declare `manifest.backend` reivindica uma function que o kernel
57
+ * compila e deploya, e o `pack` confere isso — reivindicar um nome que não existe aborta o
58
+ * empacotamento, e reivindicar um que existe o TIRA da baseline. Um exemplo não é nada disso: o
59
+ * `functions/` dele viaja como fonte para ser lido, e quem o instalar de verdade constrói o próprio.
60
+ * Sem esta distinção, o primeiro exemplo a ensinar Cloud Functions reprovaria toda publicação.
61
+ *
62
+ * É DERIVADA da forma da origem, e não uma segunda lista a manter: morar em `examples/` é
63
+ * exatamente o que significa não estar no catálogo embutido. As cinco de primeira parte moram na raiz
64
+ * do marketplace porque o bundle as carrega, e é por isso que elas continuam sendo cobradas.
65
+ */
66
+ export const READING_ONLY_EXAMPLES = new Set(Object.entries(EXAMPLE_SOURCES)
67
+ .filter(([, origin]) => origin.startsWith(`${EXAMPLES_DIR}/`))
68
+ .map(([id]) => id));
69
+ /**
70
+ * Os módulos do kernel cuja casa, para quem escreve de fora, é `@microteia/sdk/node` — a entrada de
71
+ * FERRAMENTA do pacote, e não o barril isomórfico.
72
+ *
73
+ * `scriptSource` está aqui porque um `Script` viaja com `sourceTs` E `compiledJs`, e o sandbox nunca
74
+ * transpila: quem empacota precisa produzir o segundo, com o MESMO transpilador que o runtime usa.
75
+ */
76
+ const NODE_ONLY_MODULES = ['sdk/node/', 'domain/services/scriptSource'];
26
77
  /**
27
78
  * Reescreve os imports de um plugin para a forma que o escritório usa.
28
79
  *
@@ -31,6 +82,14 @@ export const EXAMPLE_SOURCES = {
31
82
  * extensão tem a sua pasta — aqui elas moram num diretório comum, com um nível a mais.
32
83
  */
33
84
  export const rewriteImports = (source) => source.replace(/from '(\.[^']*)'/g, (match, specifier) => {
85
+ // A FERRAMENTA vem antes da superfície, e a ordem é o ponto: estes caminhos também casariam com as
86
+ // regras genéricas abaixo, que os mandariam para o barril isomórfico — onde `transpileScriptSource`
87
+ // NÃO está, de propósito, porque ele arrasta o `sucrase` e `@microteia/sdk` é a superfície que não
88
+ // arrasta nada. Do lado do escritório ele vem de `@microteia/sdk/node`, que é outra entrada do
89
+ // mesmo pacote. O sintoma seria no escritório de quem instalou, não aqui, se o round-trip byte a
90
+ // byte não compilasse cada exemplo reescrito.
91
+ if (NODE_ONLY_MODULES.some((one) => specifier.includes(one)))
92
+ return "from '@microteia/sdk/node'";
34
93
  if (/(^|\/)contributionBuilders$/.test(specifier)
35
94
  || specifier.includes('domain/')
36
95
  || specifier.includes('sdk/')) {
@@ -81,9 +140,18 @@ const siblingModules = async (marketplace, entry) => {
81
140
  }
82
141
  return collected;
83
142
  };
84
- const viewFiles = async (viewsDir) => {
143
+ /**
144
+ * Os arquivos de uma pasta do exemplo — `views/` ou `functions/` —, e o teste deles COM eles, de
145
+ * propósito. Pasta ausente é lista vazia, nunca erro: nem todo exemplo tem as duas.
146
+ *
147
+ * O `.rules.test.ts` ao lado de um `.rules.ts` é material didático: é onde se vê que a regra de uma
148
+ * view pode ser extraída e exercitada sem montar React nenhum, que é exatamente o que o `AGENTS.md`
149
+ * pede de quem escreve aqui. O `.test.ts` da RAIZ do módulo é outra coisa — aquele é nosso, sobre a
150
+ * nossa extensão — e é por isso que só ele é filtrado, no laço de baixo.
151
+ */
152
+ const sourceFilesIn = async (folder) => {
85
153
  try {
86
- return (await readdir(viewsDir)).filter((name) => name.endsWith('.tsx') || name.endsWith('.ts'));
154
+ return (await readdir(folder)).filter((name) => name.endsWith('.tsx') || name.endsWith('.ts'));
87
155
  }
88
156
  catch {
89
157
  return [];
@@ -107,7 +175,7 @@ export const stageExamples = async (repo, target) => {
107
175
  }
108
176
  const plugin = origin.replace(/\.ts$/, '');
109
177
  const views = join(marketplace, 'views', plugin);
110
- const files = await viewFiles(views);
178
+ const files = await sourceFilesIn(views);
111
179
  if (files.length > 0)
112
180
  await mkdir(join(directory, 'views'), { recursive: true });
113
181
  for (const name of files) {
@@ -115,14 +183,34 @@ export const stageExamples = async (repo, target) => {
115
183
  }
116
184
  }
117
185
  else {
118
- // Extensão que é um DIRETÓRIO (o pacote de idioma, com um arquivo por namespace). O `index.ts`
119
- // vira `plugin.ts`; os demais são vizinhos dele e os imports relativos seguem valendo.
120
- for (const name of await readdir(join(marketplace, origin))) {
186
+ // Extensão que é um DIRETÓRIO: o exemplo de leitura, cuja entrada é `plugin.ts`, e o pacote de
187
+ // idioma, cuja entrada é `index.ts` porque ele é um módulo de primeira parte antes de ser um
188
+ // exemplo. A entrada vira `plugin.ts` no destino nos dois casos — é o nome que o autor compila —
189
+ // e os demais `.ts` são vizinhos dela, com os imports relativos seguindo valendo.
190
+ const source = join(marketplace, origin);
191
+ const names = await readdir(source);
192
+ const entry = names.includes('plugin.ts') ? 'plugin.ts' : 'index.ts';
193
+ for (const name of names) {
121
194
  if (!name.endsWith('.ts') || name.endsWith('.test.ts'))
122
195
  continue;
123
- const source = rewriteImports(await readFile(join(marketplace, origin, name), 'utf8'));
124
- const isEntry = name === 'index.ts';
125
- await writeFile(join(directory, isEntry ? 'plugin.ts' : name), isEntry ? HEADER(id) + source : source);
196
+ const text = rewriteImports(await readFile(join(source, name), 'utf8'));
197
+ const isEntry = name === entry;
198
+ await writeFile(join(directory, isEntry ? 'plugin.ts' : name), isEntry ? HEADER(id) + text : text);
199
+ }
200
+ // As views, as Cloud Functions e os scripts de um exemplo moram DENTRO dele, e não em
201
+ // `views/<módulo>/`: a pasta é o pacote, exatamente como do lado do escritório. `functions/` e
202
+ // `scripts/` viajam VERBATIM — a primeira já é escrita contra `microteia-functions/office` e
203
+ // `firebase-functions`, que são os nomes que valem lá, e a segunda não importa nada: o script
204
+ // roda no sandbox, contra globais que só existem lá dentro.
205
+ for (const folder of ['views', 'functions', 'scripts']) {
206
+ const from = join(source, folder);
207
+ const files = await sourceFilesIn(from);
208
+ if (files.length === 0)
209
+ continue;
210
+ await mkdir(join(directory, folder), { recursive: true });
211
+ for (const name of files) {
212
+ await writeFile(join(directory, folder, name), await readFile(join(from, name), 'utf8'));
213
+ }
126
214
  }
127
215
  }
128
216
  written.push(id);
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Os `extension-templates/*.json` cujo backend é DESTE repositório — a única porta para essa pergunta.
3
+ *
4
+ * `extension-templates/` guarda duas coisas com a mesma forma e donos diferentes: as extensões de
5
+ * primeira parte, que viajam no bundle e cujas Cloud Functions o kernel compila e deploya, e os
6
+ * exemplos, que viajam como fonte para serem lidos e cujo backend pertence a quem os instalar. A conta
7
+ * do `pack` — baseline = todas as functions menos as reivindicadas — só faz sentido sobre as primeiras.
8
+ *
9
+ * A porta é única porque a pergunta já estava em dois lugares: o `pack` e o teste que guarda o `pack`.
10
+ * Dois leitores do mesmo diretório com filtros diferentes é como um portão passa a aprovar o que ele
11
+ * existe para reprovar — aqui, o exemplo que ensina Cloud Functions abortando toda publicação, ou uma
12
+ * function saindo da baseline do kernel porque um MOLDE a reivindicou.
13
+ */
14
+ export declare const firstPartyTemplates: <T>(root: string) => T[];
@@ -0,0 +1,23 @@
1
+ import { readdirSync, readFileSync } from 'node:fs';
2
+ import { basename, join } from 'node:path';
3
+ import { READING_ONLY_EXAMPLES } from './exampleSources.js';
4
+ /**
5
+ * Os `extension-templates/*.json` cujo backend é DESTE repositório — a única porta para essa pergunta.
6
+ *
7
+ * `extension-templates/` guarda duas coisas com a mesma forma e donos diferentes: as extensões de
8
+ * primeira parte, que viajam no bundle e cujas Cloud Functions o kernel compila e deploya, e os
9
+ * exemplos, que viajam como fonte para serem lidos e cujo backend pertence a quem os instalar. A conta
10
+ * do `pack` — baseline = todas as functions menos as reivindicadas — só faz sentido sobre as primeiras.
11
+ *
12
+ * A porta é única porque a pergunta já estava em dois lugares: o `pack` e o teste que guarda o `pack`.
13
+ * Dois leitores do mesmo diretório com filtros diferentes é como um portão passa a aprovar o que ele
14
+ * existe para reprovar — aqui, o exemplo que ensina Cloud Functions abortando toda publicação, ou uma
15
+ * function saindo da baseline do kernel porque um MOLDE a reivindicou.
16
+ */
17
+ export const firstPartyTemplates = (root) => {
18
+ const dir = join(root, 'extension-templates');
19
+ return readdirSync(dir)
20
+ .filter((file) => file.endsWith('.json'))
21
+ .filter((file) => !READING_ONLY_EXAMPLES.has(basename(file, '.json')))
22
+ .map((file) => JSON.parse(readFileSync(join(dir, file), 'utf8')));
23
+ };
@@ -254,7 +254,7 @@ ${examples.length === 0 ? '_None in this package._' : examples.map((id) => `- \`
254
254
  Each is the real source of an extension that ships with Microteia. Build any of them and you get the
255
255
  same \`.json\` the platform installs:
256
256
 
257
- npx microteia-plugin build examples/pessoa/plugin.ts
257
+ npx microteia-plugin build examples/${examples[0] ?? '<example>'}/plugin.ts
258
258
 
259
259
  ## 6. An AI assistant in this directory
260
260
 
@@ -13,6 +13,7 @@ import { KERNEL_CHANGELOG, readChangelog } from './changelogs.js';
13
13
  import { assertInsideRepository } from './repository.js';
14
14
  import { SDK_TARBALL } from './pluginWorkspaceTemplates.js';
15
15
  import { stageExamples } from './exampleSources.js';
16
+ import { firstPartyTemplates } from './firstPartyTemplates.js';
16
17
  import { packageCustomIndexTs, packageCustomTsconfig, packageFirebaseJson, packageFunctionsIndexJs, packageFunctionsPackageJson, } from './packageTemplates.js';
17
18
  /**
18
19
  * `dist` é um modo do Vite para o qual NÃO existe `.env.dist`. É isso que garante um bundle sem
@@ -23,13 +24,13 @@ const DISTRIBUTION_TARGET = 'dist';
23
24
  * As exigências de backend das extensões de PRIMEIRA PARTE saem dos `extension-templates/*.json`, que
24
25
  * são gerados dos `.ts` e conferidos pelo `verify-extensions` que o gate de qualidade acima já roda.
25
26
  * Ler o `.json` gerado em vez do `.ts` evita carregar o Vite aqui só para avaliar um módulo.
27
+ *
28
+ * Os exemplos que só viajam para ser lidos ficam de FORA (`READING_ONLY_EXAMPLES`), e o motivo está
29
+ * escrito lá: o backend de um exemplo não é deste repositório. Cobrá-lo aqui faria o exemplo que
30
+ * ensina Cloud Functions abortar toda publicação — e, se a function existisse, a reivindicação a
31
+ * tiraria da baseline do kernel, que é o oposto exato do que um exemplo deve provocar.
26
32
  */
27
- const firstPartyClaims = async (root) => {
28
- const dir = join(root, 'extension-templates');
29
- const files = (await readdir(dir)).filter((file) => file.endsWith('.json'));
30
- const templates = await Promise.all(files.map((file) => readJson(join(dir, file))));
31
- return claimsFromTemplates(templates);
32
- };
33
+ const firstPartyClaims = (root) => claimsFromTemplates(firstPartyTemplates(root));
33
34
  const readJson = async (path) => JSON.parse(await readFile(path, 'utf8'));
34
35
  /**
35
36
  * Os `defineString` e as callables estão espalhados pelos handlers, não num arquivo só — então a
@@ -131,7 +132,7 @@ export const pack = async (args) => {
131
132
  // ausência que é só do parser.
132
133
  const callables = scan.callables.filter((name) => functionNames.includes(name));
133
134
  const rootIndexes = await readJson(join(root, 'firestore.indexes.json'));
134
- const claims = await firstPartyClaims(root);
135
+ const claims = firstPartyClaims(root);
135
136
  const problems = claimProblems(claims, functionNames, rootIndexes.indexes);
136
137
  if (problems.length > 0) {
137
138
  throw new Error('An extension declares backend artifacts that do not add up, so the package would be wrong:\n'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microteia/console",
3
- "version": "2.19.0",
3
+ "version": "2.21.0",
4
4
  "description": "Install and update Microteia in a Firebase project you own.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",