@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,35 @@
|
|
|
1
|
+
export const REQUIRED_CONFIG_KEYS = [
|
|
2
|
+
'apiKey',
|
|
3
|
+
'authDomain',
|
|
4
|
+
'projectId',
|
|
5
|
+
'storageBucket',
|
|
6
|
+
'messagingSenderId',
|
|
7
|
+
'appId',
|
|
8
|
+
];
|
|
9
|
+
export const OPTIONAL_CONFIG_KEYS = ['measurementId'];
|
|
10
|
+
/**
|
|
11
|
+
* Monta a configuração a partir do que o `firebase apps:sdkconfig` devolveu, recusando-se a seguir
|
|
12
|
+
* com campo faltando: uma instalação carimbada pela metade só falha quando alguém abre a aplicação.
|
|
13
|
+
*/
|
|
14
|
+
export const runtimeConfigFrom = (sdkConfig, functionsRegion) => {
|
|
15
|
+
const missing = REQUIRED_CONFIG_KEYS.filter((key) => (sdkConfig[key] ?? '') === '');
|
|
16
|
+
if (missing.length > 0) {
|
|
17
|
+
throw new Error(`The Firebase web app configuration is missing: ${missing.join(', ')}.\n`
|
|
18
|
+
+ 'Check the web app in the Firebase console, then run "microteia connect" again.');
|
|
19
|
+
}
|
|
20
|
+
const config = {
|
|
21
|
+
apiKey: sdkConfig.apiKey,
|
|
22
|
+
authDomain: sdkConfig.authDomain,
|
|
23
|
+
projectId: sdkConfig.projectId,
|
|
24
|
+
storageBucket: sdkConfig.storageBucket,
|
|
25
|
+
messagingSenderId: sdkConfig.messagingSenderId,
|
|
26
|
+
appId: sdkConfig.appId,
|
|
27
|
+
functionsRegion,
|
|
28
|
+
};
|
|
29
|
+
for (const key of OPTIONAL_CONFIG_KEYS) {
|
|
30
|
+
const value = sdkConfig[key];
|
|
31
|
+
if (value !== undefined && value !== '')
|
|
32
|
+
config[key] = value;
|
|
33
|
+
}
|
|
34
|
+
return config;
|
|
35
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ParsedArgs } from '../shared/args.js';
|
|
2
|
+
import type { KernelManifest } from '../kernel/manifest.js';
|
|
3
|
+
export declare const installPath: (root: string) => string;
|
|
4
|
+
/**
|
|
5
|
+
* Lê o manifesto do pacote já desempacotado, recusando com o comando que falta em vez do `ENOENT` cru.
|
|
6
|
+
* É a porta pela qual o `deploy` descobre que não há o que publicar.
|
|
7
|
+
*/
|
|
8
|
+
export declare const readInstalledManifest: (root: string) => Promise<KernelManifest>;
|
|
9
|
+
export declare const unpack: (args: ParsedArgs) => Promise<void>;
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { access, cp, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { flagValue, hasFlag, requiredFlag } from '../shared/args.js';
|
|
4
|
+
import { CLI_VERSION } from '../shared/paths.js';
|
|
5
|
+
import { run } from '../shared/run.js';
|
|
6
|
+
import { blank, bullets, heading, info, success, warn } from '../shared/ui.js';
|
|
7
|
+
import { compareVersions } from '../kernel/manifest.js';
|
|
8
|
+
import { ENVIRONMENT_FILE, readEnvironment, writeEnvironment } from './environment.js';
|
|
9
|
+
import { describeIndex, parseIndexState, restoreDeclaredIndexes } from './backendIndexes.js';
|
|
10
|
+
import { refreshAuthoringWorkspace } from './pluginWorkspace.js';
|
|
11
|
+
import { workspacePath } from './workspace.js';
|
|
12
|
+
import { ensureOfficeEnv, ensureRootGitignore, officeEnvName } from './backendEnv.js';
|
|
13
|
+
import { readOfficeFunctions } from './customFunctions.js';
|
|
14
|
+
import { INSTALLATION_GUIDE, INSTALLATION_README, installationGuide, installationReadme, } from '../vendor/installationDocs.js';
|
|
15
|
+
import { refreshChangelogs } from '../vendor/changelogs.js';
|
|
16
|
+
import { fetchKernel } from './channel.js';
|
|
17
|
+
/**
|
|
18
|
+
* `microteia unpack` — põe o pacote em `.microteia/install`, e nada mais.
|
|
19
|
+
*
|
|
20
|
+
* Era um passo dentro do `deploy`, e juntar os dois custava duas coisas. A primeira é que trocar de
|
|
21
|
+
* kernel obrigava a publicar tudo: quem quisesse só o hosting — porque functions, rules e índices não
|
|
22
|
+
* mudaram naquela versão — não tinha como pedir isso, já que o `--only` vinha depois da troca do
|
|
23
|
+
* pacote no mesmo comando. A segunda é que não havia momento entre "o pacote chegou" e "o pacote foi
|
|
24
|
+
* publicado" para PREENCHER as configurações que o pacote declara — e é justamente esse o momento em
|
|
25
|
+
* que se descobre que existe um parâmetro novo.
|
|
26
|
+
*
|
|
27
|
+
* Separado, o fluxo é: `unpack` traz o kernel e escreve o que faltar no `.env.<projectId>` da raiz;
|
|
28
|
+
* quem instala preenche o que precisa; `deploy --only …` publica o que interessa.
|
|
29
|
+
*
|
|
30
|
+
* Não toca no Firebase. É seguro rodar a qualquer momento.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* O que sobrevive à troca do kernel dentro de `.microteia/`.
|
|
34
|
+
*
|
|
35
|
+
* `functions/node_modules` é o que evita reinstalar ~250 pacotes a cada upgrade — é cache, não conteúdo.
|
|
36
|
+
* `functions/custom` NÃO está mais aqui: ele passou a ser DERIVADO, montado no deploy a partir de
|
|
37
|
+
* `functions/` e `extensions/<plugin>/functions/`, que ficam fora de `.microteia/`. Uma exceção a menos
|
|
38
|
+
* nesta lista é uma coisa a menos que depende de alguém não mexer nela.
|
|
39
|
+
*/
|
|
40
|
+
const PRESERVED = ['functions/node_modules'];
|
|
41
|
+
const exists = async (path) => {
|
|
42
|
+
try {
|
|
43
|
+
await access(path);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Leva o que sobrevive ao upgrade para o staging — MOVENDO, não copiando.
|
|
52
|
+
*
|
|
53
|
+
* Copiar estourava o limite de descritores do processo no macOS: o único item preservado é
|
|
54
|
+
* `functions/node_modules`, que são ~250 pacotes e dezenas de milhares de arquivos, e o `cp` recursivo
|
|
55
|
+
* abre muitos ao mesmo tempo. O sintoma era intermitente e ilegível — `ENFILE: file table overflow`
|
|
56
|
+
* apontando um diretório qualquer de dentro do `@google-cloud/firestore` —, e o mesmo comando passava
|
|
57
|
+
* na tentativa seguinte.
|
|
58
|
+
*
|
|
59
|
+
* Mover é estritamente melhor, e não só por causa disso: a origem é apagada três linhas abaixo, então
|
|
60
|
+
* a cópia nunca teve para que existir. O que era proporcional ao tamanho do `node_modules` passou a ser
|
|
61
|
+
* uma operação só.
|
|
62
|
+
*
|
|
63
|
+
* O `cp` fica como recurso para o único caso em que `rename` não pode funcionar: origem e destino em
|
|
64
|
+
* sistemas de arquivos diferentes (`EXDEV`). Aqui os dois estão sob `.microteia/`, então é caminho
|
|
65
|
+
* morto — e é justamente por isso que ele não pode ser a via normal.
|
|
66
|
+
*/
|
|
67
|
+
const preserve = async (from, to) => {
|
|
68
|
+
try {
|
|
69
|
+
await rename(from, to);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error.code !== 'EXDEV')
|
|
73
|
+
throw error;
|
|
74
|
+
await cp(from, to, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
export const installPath = (root) => join(workspacePath(root), 'install');
|
|
78
|
+
const stagingPath = (root) => join(workspacePath(root), 'staging');
|
|
79
|
+
/**
|
|
80
|
+
* O Console que o pacote exige. Vale para o pacote que ACABOU de ser extraído e para o que já está no
|
|
81
|
+
* lugar, e por isso não mora em nenhum dos dois.
|
|
82
|
+
*/
|
|
83
|
+
const assertConsoleCanInstall = (manifest) => {
|
|
84
|
+
if (compareVersions(CLI_VERSION, manifest.minimumCliVersion) < 0) {
|
|
85
|
+
throw new Error(`This package needs Microteia Console ${manifest.minimumCliVersion} or newer; this is ${CLI_VERSION}.\n`
|
|
86
|
+
+ 'Update it with "npm i -g @microteia/console".');
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Lê o manifesto do pacote já desempacotado, recusando com o comando que falta em vez do `ENOENT` cru.
|
|
91
|
+
* É a porta pela qual o `deploy` descobre que não há o que publicar.
|
|
92
|
+
*/
|
|
93
|
+
export const readInstalledManifest = async (root) => {
|
|
94
|
+
const path = join(installPath(root), 'manifest.json');
|
|
95
|
+
if (!await exists(path)) {
|
|
96
|
+
throw new Error('No unpacked Microteia package found in this installation.\n'
|
|
97
|
+
+ 'Run "microteia unpack --version latest" first.');
|
|
98
|
+
}
|
|
99
|
+
const manifest = JSON.parse(await readFile(path, 'utf8'));
|
|
100
|
+
assertConsoleCanInstall(manifest);
|
|
101
|
+
return manifest;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Extrai para o `staging`, e só isso. A instalação anterior fica INTACTA.
|
|
105
|
+
*
|
|
106
|
+
* Extrair e trocar era um passo só, e a troca vinha antes de o manifesto ser lido — então um pacote que
|
|
107
|
+
* exige um Console mais novo apagava o `install/` da versão que funcionava e só depois recusava,
|
|
108
|
+
* deixando a instalação sem kernel nenhum. Enquanto o tarball chegava à mão isso era raro; com o
|
|
109
|
+
* `--version` lendo o canal, um Console desatualizado encontra um pacote novo na primeira tentativa.
|
|
110
|
+
*/
|
|
111
|
+
const stage = async (tarball, root) => {
|
|
112
|
+
const staging = stagingPath(root);
|
|
113
|
+
await rm(staging, { recursive: true, force: true });
|
|
114
|
+
await mkdir(staging, { recursive: true });
|
|
115
|
+
await run('tar', ['-xzf', tarball, '-C', staging]);
|
|
116
|
+
return staging;
|
|
117
|
+
};
|
|
118
|
+
/** O manifesto do que foi extraído, conferido ANTES de a instalação anterior ser tocada. */
|
|
119
|
+
const readStagedManifest = async (staging) => {
|
|
120
|
+
const path = join(staging, 'manifest.json');
|
|
121
|
+
if (!await exists(path)) {
|
|
122
|
+
throw new Error('This file does not look like a Microteia package: it carries no manifest.json.');
|
|
123
|
+
}
|
|
124
|
+
const manifest = JSON.parse(await readFile(path, 'utf8'));
|
|
125
|
+
assertConsoleCanInstall(manifest);
|
|
126
|
+
return manifest;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Troca o pacote, preservando o que é do escritório. O pacote é substituído por inteiro — é isso que faz
|
|
130
|
+
* o upgrade ser confiável —, então o que não pode ser perdido é movido de volta explicitamente, em vez
|
|
131
|
+
* de o extrator "mesclar" por cima e deixar restos da versão anterior.
|
|
132
|
+
*
|
|
133
|
+
* A partir daqui não há como voltar atrás, e é por isso que tudo o que pode recusar já recusou.
|
|
134
|
+
*/
|
|
135
|
+
const commit = async (root, staging) => {
|
|
136
|
+
const install = installPath(root);
|
|
137
|
+
for (const relative of PRESERVED) {
|
|
138
|
+
const previous = join(install, relative);
|
|
139
|
+
if (!await exists(previous))
|
|
140
|
+
continue;
|
|
141
|
+
await rm(join(staging, relative), { recursive: true, force: true });
|
|
142
|
+
await preserve(previous, join(staging, relative));
|
|
143
|
+
info(` kept ${relative} from the previous install`);
|
|
144
|
+
}
|
|
145
|
+
await rm(install, { recursive: true, force: true });
|
|
146
|
+
await rename(staging, install);
|
|
147
|
+
return install;
|
|
148
|
+
};
|
|
149
|
+
/** As extensões de exemplo que este pacote carrega, para o guia listá-las. */
|
|
150
|
+
const exampleIds = async (install) => {
|
|
151
|
+
try {
|
|
152
|
+
const entries = await readdir(join(install, 'sdk', 'examples'), { withFileTypes: true });
|
|
153
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const kernelSource = (args) => {
|
|
160
|
+
const path = flagValue(args, 'kernel');
|
|
161
|
+
const version = flagValue(args, 'version');
|
|
162
|
+
if (path !== undefined && version !== undefined) {
|
|
163
|
+
throw new Error('Pass either --version or --kernel, not both: they are two ways to name one package.');
|
|
164
|
+
}
|
|
165
|
+
if (path !== undefined)
|
|
166
|
+
return { kind: 'file', path: resolve(path) };
|
|
167
|
+
if (version !== undefined)
|
|
168
|
+
return { kind: 'channel', version };
|
|
169
|
+
// Uma bandeira sem valor é erro de digitação, e dizer "falta --version" a quem acabou de escrever
|
|
170
|
+
// `--version` manda a pessoa procurar o problema onde ele não está.
|
|
171
|
+
if (hasFlag(args, 'version')) {
|
|
172
|
+
throw new Error('--version needs a number, or the word "latest".');
|
|
173
|
+
}
|
|
174
|
+
if (hasFlag(args, 'kernel')) {
|
|
175
|
+
throw new Error('--kernel needs the path to a package file.');
|
|
176
|
+
}
|
|
177
|
+
throw new Error('Missing --version or --kernel.\n'
|
|
178
|
+
+ ' microteia unpack --version latest takes the newest published version\n'
|
|
179
|
+
+ ' microteia unpack --version 9.8.2 takes one exact version\n'
|
|
180
|
+
+ ' microteia unpack --kernel <package>.tgz takes a file you already have');
|
|
181
|
+
};
|
|
182
|
+
export const unpack = async (args) => {
|
|
183
|
+
const file = resolve(hasFlag(args, 'file') ? requiredFlag(args, 'file') : join(process.cwd(), ENVIRONMENT_FILE));
|
|
184
|
+
const root = dirname(file);
|
|
185
|
+
const source = kernelSource(args);
|
|
186
|
+
const environment = await readEnvironment(file);
|
|
187
|
+
const projectId = environment.firebase.projectId;
|
|
188
|
+
heading(`Unpacking Microteia into ${environment.project.name}`);
|
|
189
|
+
const tarball = source.kind === 'file'
|
|
190
|
+
? source.path
|
|
191
|
+
: (await fetchKernel(source.version)).path;
|
|
192
|
+
/**
|
|
193
|
+
* Extrair, conferir, e só então trocar. A recusa limpa o `staging` que abriu: 7 MB extraídos que
|
|
194
|
+
* ninguém vai usar são lixo, e deixá-los sugere que algo foi instalado.
|
|
195
|
+
*/
|
|
196
|
+
const staging = await stage(tarball, root);
|
|
197
|
+
const manifest = await readStagedManifest(staging).catch(async (error) => {
|
|
198
|
+
await rm(staging, { recursive: true, force: true });
|
|
199
|
+
throw error;
|
|
200
|
+
});
|
|
201
|
+
const install = await commit(root, staging);
|
|
202
|
+
const baselineCount = (manifest.baselineFunctions ?? manifest.functions).length;
|
|
203
|
+
success(`kernel ${manifest.version} (contract ${manifest.kernelVersion}), ${baselineCount} baseline function(s)`
|
|
204
|
+
+ `, ${manifest.functions.length - baselineCount} claimed by extensions`);
|
|
205
|
+
if (manifest.sdk !== undefined) {
|
|
206
|
+
info(` authoring SDK ${manifest.sdk.version} included — see "microteia plugin-init"`);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* O `microteia.yaml` passa a dizer o que está no lugar, e é AQUI que ele é gravado. Ele só era
|
|
210
|
+
* escrito no fim do `deploy`, então entre desempacotar e publicar o arquivo anunciava a versão
|
|
211
|
+
* anterior — e o `MICROTEIA.md`, reescrito três linhas abaixo, já anunciava a nova. Duas fontes
|
|
212
|
+
* no mesmo diretório discordando é o tipo de coisa que se descobre pela dúvida de alguém.
|
|
213
|
+
*
|
|
214
|
+
* `deployed` fica para o `deploy`. Numa instalação anterior a este campo ele nasce do `version` que
|
|
215
|
+
* estava lá: aquele número foi gravado por um `deploy`, então é o que já foi publicado — sem isso, o
|
|
216
|
+
* `provision` diria "nenhum kernel publicado" a uma instalação que está no ar.
|
|
217
|
+
*/
|
|
218
|
+
const deployed = environment.kernel.deployed ?? environment.kernel.version;
|
|
219
|
+
await writeEnvironment(file, {
|
|
220
|
+
...environment,
|
|
221
|
+
kernel: { version: manifest.version, deployed },
|
|
222
|
+
});
|
|
223
|
+
success(`recorded kernel ${manifest.version} in ${ENVIRONMENT_FILE}`);
|
|
224
|
+
if (deployed !== null && deployed !== manifest.version) {
|
|
225
|
+
info(` ${deployed} is still what the project runs — "microteia deploy" is what changes that`);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* E os índices das extensões voltam ao arquivo que acabou de ser substituído.
|
|
229
|
+
*
|
|
230
|
+
* O pacote traz só a baseline: o que uma extensão reivindica sai dela no `pack`, para não viajar até
|
|
231
|
+
* quem não instalou a extensão. Como este comando apaga `install/` inteiro, sem este passo o arquivo
|
|
232
|
+
* volta à baseline e a publicação de índices seguinte acha no projeto índices que o arquivo não
|
|
233
|
+
* declara — e PERGUNTA se pode apagá-los, no meio de um upgrade.
|
|
234
|
+
*
|
|
235
|
+
* Quem apaga é quem repara, e antes de qualquer comando poder publicar o arquivo incompleto.
|
|
236
|
+
*/
|
|
237
|
+
const recorded = parseIndexState(environment.backend.indexes);
|
|
238
|
+
if (recorded.length > 0) {
|
|
239
|
+
const restored = await restoreDeclaredIndexes(install, manifest.baselineIndexes ?? [], recorded);
|
|
240
|
+
success(`kept ${recorded.length} composite index(es) of installed extensions in firestore.indexes.json`);
|
|
241
|
+
if (restored.length > 0) {
|
|
242
|
+
info(' this package does not declare them, and publishing indexes would have offered to');
|
|
243
|
+
info(' delete them from the project:');
|
|
244
|
+
bullets(restored.map(describeIndex));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// A região é do PACOTE, não do arquivo: as Cloud Functions são compiladas com ela fixa.
|
|
248
|
+
if (projectId !== null && environment.firebase.region !== manifest.requires.functionsRegion) {
|
|
249
|
+
warn(`this package is compiled for region ${manifest.requires.functionsRegion}; `
|
|
250
|
+
+ `the installation recorded ${environment.firebase.region}.`);
|
|
251
|
+
}
|
|
252
|
+
// Os dois documentos da raiz. O `MICROTEIA.md` é reescrito sempre — é o que permite a ele carregar os
|
|
253
|
+
// números DESTE pacote em vez de uma cópia deles. O `README.md` nasce e nunca é tocado: é do escritório.
|
|
254
|
+
const examples = await exampleIds(install);
|
|
255
|
+
await writeFile(join(root, INSTALLATION_GUIDE), installationGuide(environment.project.name, manifest, examples));
|
|
256
|
+
success(`wrote ${INSTALLATION_GUIDE}`);
|
|
257
|
+
if (!await exists(join(root, INSTALLATION_README))) {
|
|
258
|
+
await writeFile(join(root, INSTALLATION_README), installationReadme(environment.project.name));
|
|
259
|
+
success(`created ${INSTALLATION_README} — it is yours, and nothing rewrites it`);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* E os dois históricos técnicos, sobrepostos pela mesma razão que o `MICROTEIA.md`: eles prometem
|
|
263
|
+
* estar atuais.
|
|
264
|
+
*
|
|
265
|
+
* Cada um vem do artefato que ele descreve — o do kernel de dentro do pacote, o do Console do binário
|
|
266
|
+
* que está rodando. É o que impede qualquer um dos dois de falar de uma versão que não está aqui, e
|
|
267
|
+
* era exatamente o que acontecia com os guias de `extensions/` enquanto o Console os escrevia.
|
|
268
|
+
*
|
|
269
|
+
* A ausência avisa e não interrompe: num pacote anterior a esta capacidade o do kernel não existe, e
|
|
270
|
+
* trocar de kernel não pode falhar por causa de um documento.
|
|
271
|
+
*/
|
|
272
|
+
const changelogs = await refreshChangelogs(root, install);
|
|
273
|
+
if (changelogs.written.length > 0)
|
|
274
|
+
success(`wrote ${changelogs.written.join(' and ')}`);
|
|
275
|
+
for (const name of changelogs.missing) {
|
|
276
|
+
warn(`this package carries no ${name}, so the previous one was left in place`);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* E o diretório de autoria, se ele existir — pela MESMA razão que o `MICROTEIA.md` é reescrito aqui.
|
|
280
|
+
*
|
|
281
|
+
* O que há em `extensions/` além das extensões do escritório é documentação do que o kernel sabe
|
|
282
|
+
* fazer, e ela era escrita uma única vez, pelo `plugin-init`. Trocava-se de kernel e ela continuava
|
|
283
|
+
* descrevendo o contrato anterior — para o autor e para a IA que a lê como instrução —, sem nada
|
|
284
|
+
* reprovar, porque nada comparava. Vem antes do retorno de instalação não conectada de propósito:
|
|
285
|
+
* escrever extensão não depende de haver projeto Firebase.
|
|
286
|
+
*/
|
|
287
|
+
await refreshAuthoringWorkspace(root, environment.project.name);
|
|
288
|
+
blank();
|
|
289
|
+
if (projectId === null) {
|
|
290
|
+
info('This installation is not connected to a Firebase project yet.');
|
|
291
|
+
bullets(['microteia connect --project <firebase-project-id>', 'microteia deploy']);
|
|
292
|
+
blank();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const gitignore = await ensureRootGitignore(root);
|
|
296
|
+
if (gitignore === 'created')
|
|
297
|
+
success('created .gitignore');
|
|
298
|
+
else if (gitignore === 'appended')
|
|
299
|
+
success('added ".env.*" to .gitignore — the settings file is a credential');
|
|
300
|
+
/**
|
|
301
|
+
* Os parâmetros do PACOTE mais os que as functions do escritório declaram. A união importa porque o
|
|
302
|
+
* `firebase-functions` valida os `defineString` globalmente: um parâmetro sem linha no arquivo recusa
|
|
303
|
+
* o deploy de todas as functions, e não só das que o leem.
|
|
304
|
+
*
|
|
305
|
+
* A leitura é tolerante de propósito. Este comando é o que troca de kernel, e um diretório de function
|
|
306
|
+
* pela metade — ninguém escreve o `index.ts` primeiro — não pode impedir um upgrade que nada tem com
|
|
307
|
+
* ele. Quem RECUSA por isso é o `deploy`, no momento em que a montagem de fato importa.
|
|
308
|
+
*/
|
|
309
|
+
const office = await readOfficeFunctions(root).catch((error) => {
|
|
310
|
+
warn(`could not read your own Cloud Functions: ${error instanceof Error ? error.message.split('\n')[0] : String(error)}`);
|
|
311
|
+
info(' their settings are not listed below. "microteia deploy" refuses until this is fixed.');
|
|
312
|
+
return { parameters: [] };
|
|
313
|
+
});
|
|
314
|
+
const settings = await ensureOfficeEnv(root, projectId, [...manifest.backendParameters, ...office.parameters]);
|
|
315
|
+
if (settings.created) {
|
|
316
|
+
success(`created ${officeEnvName(projectId)} with ${settings.added.length} setting(s), all empty`);
|
|
317
|
+
}
|
|
318
|
+
else if (settings.added.length > 0) {
|
|
319
|
+
success(`added ${settings.added.length} new setting(s) to ${officeEnvName(projectId)}: ${settings.added.join(', ')}`);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
info(`${officeEnvName(projectId)} already declares every setting this package needs`);
|
|
323
|
+
}
|
|
324
|
+
blank();
|
|
325
|
+
info('Next:');
|
|
326
|
+
bullets([
|
|
327
|
+
`fill in what you need in ${officeEnvName(projectId)} (optional features stay empty)`,
|
|
328
|
+
'microteia deploy (everything)',
|
|
329
|
+
'microteia deploy --only hosting (just the application)',
|
|
330
|
+
'microteia deploy --only rules,indexes,functions (just the backend)',
|
|
331
|
+
]);
|
|
332
|
+
blank();
|
|
333
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Como o relatório descobre se há versão nova. É INJETADO, e não chamado daqui: `--version` é o primeiro
|
|
3
|
+
* comando que se roda quando algo está estranho, e a resposta que se cola num pedido de ajuda. Ele
|
|
4
|
+
* responde sobre a instalação, não sobre a rede — quem quiser a checagem passa o `newerThan`, e quem
|
|
5
|
+
* está testando ou offline não paga por ela.
|
|
6
|
+
*/
|
|
7
|
+
export type NewerLookup = (version: string | null) => Promise<{
|
|
8
|
+
version: string;
|
|
9
|
+
} | null>;
|
|
10
|
+
export declare const versionReport: (directory: string, newer?: NewerLookup) => Promise<string[]>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { CLI_VERSION } from '../shared/paths.js';
|
|
4
|
+
import { ENVIRONMENT_FILE, parseEnvironment } from './environment.js';
|
|
5
|
+
import { installPath } from './unpack.js';
|
|
6
|
+
/**
|
|
7
|
+
* O que `microteia --version` responde.
|
|
8
|
+
*
|
|
9
|
+
* O Console tem versão PRÓPRIA e deliberadamente independente do kernel — é ela que o
|
|
10
|
+
* `minimumCliVersion` do pacote confere. Só que quem pergunta a versão quase nunca quer saber da
|
|
11
|
+
* ferramenta: quer saber que Microteia está rodando ali. Responder um número só, e justamente o que
|
|
12
|
+
* não é o do produto, manda procurar no `MICROTEIA.md` o que a pergunta já era.
|
|
13
|
+
*
|
|
14
|
+
* As duas do kernel são lidas de fontes DIFERENTES de propósito. A que está no lugar vem do manifesto
|
|
15
|
+
* do pacote desempacotado, que é o artefato; a publicada vem do `microteia.yaml`, que é registro do que
|
|
16
|
+
* um comando fez. Ler as duas do mesmo lugar seria não poder mostrar que elas divergem — e divergir é o
|
|
17
|
+
* estado normal entre `unpack` e `deploy`.
|
|
18
|
+
*/
|
|
19
|
+
const readJson = async (path) => {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
export const versionReport = async (directory, newer) => {
|
|
28
|
+
const lines = [`Microteia Console ${CLI_VERSION}`];
|
|
29
|
+
const source = await readFile(join(directory, ENVIRONMENT_FILE), 'utf8').catch(() => null);
|
|
30
|
+
if (source === null)
|
|
31
|
+
return lines;
|
|
32
|
+
/**
|
|
33
|
+
* Um `microteia.yaml` ilegível não pode fazer `--version` falhar: ele é o primeiro comando que se
|
|
34
|
+
* roda quando algo está estranho, e é a resposta que se cola num pedido de ajuda.
|
|
35
|
+
*/
|
|
36
|
+
const environment = (() => {
|
|
37
|
+
try {
|
|
38
|
+
return parseEnvironment(source);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
})();
|
|
44
|
+
if (environment === null) {
|
|
45
|
+
lines.push(`This installation's ${ENVIRONMENT_FILE} could not be read.`);
|
|
46
|
+
return lines;
|
|
47
|
+
}
|
|
48
|
+
const manifest = await readJson(join(installPath(directory), 'manifest.json'));
|
|
49
|
+
const inPlace = manifest?.version ?? environment.kernel.version;
|
|
50
|
+
const deployed = environment.kernel.deployed;
|
|
51
|
+
lines.push(`Installation ${environment.project.name}`);
|
|
52
|
+
lines.push(`Firebase project ${environment.firebase.projectId ?? 'not connected'}`);
|
|
53
|
+
lines.push(`Kernel in place ${inPlace ?? 'none — run "microteia unpack --version latest"'}`);
|
|
54
|
+
lines.push(`Kernel deployed ${deployed ?? 'none — run "microteia deploy"'}`);
|
|
55
|
+
if (manifest?.kernelVersion !== undefined) {
|
|
56
|
+
lines.push(`Plugin contract ${manifest.kernelVersion}`);
|
|
57
|
+
}
|
|
58
|
+
if (inPlace !== null && deployed !== null && inPlace !== deployed) {
|
|
59
|
+
lines.push('');
|
|
60
|
+
lines.push(`${inPlace} is in place and ${deployed} is what the project runs. "microteia deploy" publishes it.`);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* E o que o canal oferece, quando há canal alcançável. Vem por último porque é a única linha que
|
|
64
|
+
* depende de rede: se ela faltar, tudo acima continua sendo a resposta inteira.
|
|
65
|
+
*/
|
|
66
|
+
const available = newer === undefined ? null : await newer(inPlace);
|
|
67
|
+
if (available !== null) {
|
|
68
|
+
lines.push('');
|
|
69
|
+
lines.push(`Kernel ${available.version} is published. "microteia unpack --version latest" takes it.`);
|
|
70
|
+
}
|
|
71
|
+
return lines;
|
|
72
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { MicroteiaRuntimeConfig } from './runtimeConfigShape.js';
|
|
2
|
+
/**
|
|
3
|
+
* Diretório local da instalação: o pacote desempacotado e a configuração resolvida por `connect`.
|
|
4
|
+
* Fica ao lado do `microteia.yaml` e é descartável — tudo aqui pode ser refeito a partir do tarball
|
|
5
|
+
* mais um `connect`. A única coisa que NÃO é descartável é `functions/custom/`, e por isso o deploy
|
|
6
|
+
* a preserva explicitamente.
|
|
7
|
+
*/
|
|
8
|
+
export declare const WORKSPACE_DIR = ".microteia";
|
|
9
|
+
export declare const RESOLVED_CONFIG_FILE = "firebase-config.json";
|
|
10
|
+
export declare const workspacePath: (root: string) => string;
|
|
11
|
+
export declare const resolvedConfigPath: (root: string) => string;
|
|
12
|
+
export declare const kernelPath: (root: string, version: string) => string;
|
|
13
|
+
export declare const readResolvedConfig: (root: string) => Promise<MicroteiaRuntimeConfig>;
|
|
14
|
+
export declare const writeResolvedConfig: (root: string, config: MicroteiaRuntimeConfig) => Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* O arquivo que o navegador carrega ANTES do bundle. É o único ponto em que o projeto Firebase entra
|
|
17
|
+
* numa instalação — ver `index.html` e `infrastructure/firebase/runtimeConfig.ts` no kernel.
|
|
18
|
+
*/
|
|
19
|
+
export declare const stampedConfigJs: (config: MicroteiaRuntimeConfig) => string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Diretório local da instalação: o pacote desempacotado e a configuração resolvida por `connect`.
|
|
5
|
+
* Fica ao lado do `microteia.yaml` e é descartável — tudo aqui pode ser refeito a partir do tarball
|
|
6
|
+
* mais um `connect`. A única coisa que NÃO é descartável é `functions/custom/`, e por isso o deploy
|
|
7
|
+
* a preserva explicitamente.
|
|
8
|
+
*/
|
|
9
|
+
export const WORKSPACE_DIR = '.microteia';
|
|
10
|
+
export const RESOLVED_CONFIG_FILE = 'firebase-config.json';
|
|
11
|
+
export const workspacePath = (root) => join(root, WORKSPACE_DIR);
|
|
12
|
+
export const resolvedConfigPath = (root) => join(workspacePath(root), RESOLVED_CONFIG_FILE);
|
|
13
|
+
export const kernelPath = (root, version) => join(workspacePath(root), 'kernel', version);
|
|
14
|
+
export const readResolvedConfig = async (root) => {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(await readFile(resolvedConfigPath(root), 'utf8'));
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error.code === 'ENOENT') {
|
|
20
|
+
throw new Error('This installation is not connected to a Firebase project yet. Run "microteia connect --project <id>".', { cause: error });
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
export const writeResolvedConfig = async (root, config) => {
|
|
26
|
+
await writeFile(resolvedConfigPath(root), `${JSON.stringify(config, null, 2)}\n`);
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* O arquivo que o navegador carrega ANTES do bundle. É o único ponto em que o projeto Firebase entra
|
|
30
|
+
* numa instalação — ver `index.html` e `infrastructure/firebase/runtimeConfig.ts` no kernel.
|
|
31
|
+
*/
|
|
32
|
+
export const stampedConfigJs = (config) => `// Generated by "microteia deploy". Do not edit by hand.\n`
|
|
33
|
+
+ `window.__MICROTEIA_CONFIG__ = ${JSON.stringify(config, null, 2)};\n`;
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from './shared/args.js';
|
|
3
|
+
import { CLI_VERSION } from './shared/paths.js';
|
|
4
|
+
import { blank, failure, heading, info } from './shared/ui.js';
|
|
5
|
+
import { pack } from './vendor/pack.js';
|
|
6
|
+
import { packConsole } from './vendor/packConsole.js';
|
|
7
|
+
import { publish } from './vendor/publish.js';
|
|
8
|
+
import { init } from './console/init.js';
|
|
9
|
+
import { connect } from './console/connect.js';
|
|
10
|
+
import { unpack } from './console/unpack.js';
|
|
11
|
+
import { deploy } from './console/deploy.js';
|
|
12
|
+
import { provision } from './console/provision.js';
|
|
13
|
+
import { pluginInit } from './console/pluginWorkspace.js';
|
|
14
|
+
import { versionReport } from './console/versions.js';
|
|
15
|
+
import { newerThan } from './console/channel.js';
|
|
16
|
+
const COMMANDS = [
|
|
17
|
+
{
|
|
18
|
+
name: 'init',
|
|
19
|
+
usage: 'microteia init [--name <office>] [--id <environment-id>] [--force]',
|
|
20
|
+
summary: 'Create a Microteia installation in this directory.',
|
|
21
|
+
audience: 'office',
|
|
22
|
+
execute: init,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'connect',
|
|
26
|
+
usage: 'microteia connect --project <firebase-project-id> [--no-create-web-app]',
|
|
27
|
+
summary: 'Bind this installation to a Firebase project you own.',
|
|
28
|
+
audience: 'office',
|
|
29
|
+
execute: connect,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'unpack',
|
|
33
|
+
usage: 'microteia unpack --version <latest|x.y.z> | --kernel <package.tgz>',
|
|
34
|
+
summary: 'Put a Microteia version in place, without publishing anything.',
|
|
35
|
+
audience: 'office',
|
|
36
|
+
execute: unpack,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'deploy',
|
|
40
|
+
usage: 'microteia deploy [--only rules,indexes,functions,hosting] [--delay <seconds>] [--prune]',
|
|
41
|
+
summary: 'Publish the package that is in place to the connected Firebase project.',
|
|
42
|
+
audience: 'office',
|
|
43
|
+
execute: deploy,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'provision',
|
|
47
|
+
usage: 'microteia provision [--prune] [--delay <seconds>]',
|
|
48
|
+
summary: 'Give the installed extensions the Cloud Functions and indexes they ask for.',
|
|
49
|
+
audience: 'office',
|
|
50
|
+
execute: provision,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'plugin-init',
|
|
54
|
+
usage: 'microteia plugin-init [--dir <path>] [--force]',
|
|
55
|
+
summary: 'Prepare "extensions/", to author extensions in TypeScript in your editor.',
|
|
56
|
+
audience: 'office',
|
|
57
|
+
execute: pluginInit,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'pack',
|
|
61
|
+
usage: 'microteia pack [--output <dir>] [--skip-verify]',
|
|
62
|
+
summary: 'Build an installable Microteia package from this repository.',
|
|
63
|
+
audience: 'publisher',
|
|
64
|
+
execute: pack,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'pack-console',
|
|
68
|
+
usage: 'microteia pack-console [--output <dir>]',
|
|
69
|
+
summary: 'Build a standalone copy of this Console, to run outside the repository.',
|
|
70
|
+
audience: 'publisher',
|
|
71
|
+
execute: packConsole,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'publish',
|
|
75
|
+
usage: 'microteia publish [--kernel-only|--console-only] [--repo <owner/name>] [--otp <code>] [--dry-run]',
|
|
76
|
+
summary: 'Release what pack and pack-console built, so an office can download it.',
|
|
77
|
+
audience: 'publisher',
|
|
78
|
+
execute: publish,
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
const printHelp = () => {
|
|
82
|
+
heading(`Microteia Console ${CLI_VERSION}`);
|
|
83
|
+
info('Install and update Microteia in a Firebase project you own.\n');
|
|
84
|
+
const office = COMMANDS.filter((command) => command.audience === 'office');
|
|
85
|
+
const publisher = COMMANDS.filter((command) => command.audience === 'publisher');
|
|
86
|
+
// A largura sai do NOME mais longo, não de um número escrito à mão: `pack-console` e
|
|
87
|
+
// `plugin-init` já estouravam o 10 fixo, e o help saía com nome e resumo colados.
|
|
88
|
+
const width = Math.max(...COMMANDS.map((command) => command.name.length)) + 2;
|
|
89
|
+
if (office.length > 0) {
|
|
90
|
+
info('Installing:');
|
|
91
|
+
for (const command of office)
|
|
92
|
+
info(` ${command.name.padEnd(width)}${command.summary}`);
|
|
93
|
+
blank();
|
|
94
|
+
}
|
|
95
|
+
if (publisher.length > 0) {
|
|
96
|
+
info('Publishing:');
|
|
97
|
+
for (const command of publisher)
|
|
98
|
+
info(` ${command.name.padEnd(width)}${command.summary}`);
|
|
99
|
+
blank();
|
|
100
|
+
}
|
|
101
|
+
info('Run "microteia <command> --help" for usage.\n');
|
|
102
|
+
};
|
|
103
|
+
const main = async () => {
|
|
104
|
+
const argv = process.argv.slice(2);
|
|
105
|
+
const [name, ...rest] = argv;
|
|
106
|
+
if (name === undefined || name === '--help' || name === '-h' || name === 'help') {
|
|
107
|
+
printHelp();
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
if (name === '--version' || name === '-v') {
|
|
111
|
+
for (const line of await versionReport(process.cwd(), newerThan))
|
|
112
|
+
info(line);
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
const command = COMMANDS.find((candidate) => candidate.name === name);
|
|
116
|
+
if (command === undefined) {
|
|
117
|
+
failure(`Unknown command "${name}".`);
|
|
118
|
+
printHelp();
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
const args = parseArgs(rest);
|
|
122
|
+
if (args.flags.has('help')) {
|
|
123
|
+
info(`\n${command.usage}\n\n${command.summary}\n`);
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
await command.execute(args);
|
|
127
|
+
return 0;
|
|
128
|
+
};
|
|
129
|
+
main()
|
|
130
|
+
.then((code) => { process.exitCode = code; })
|
|
131
|
+
.catch((error) => {
|
|
132
|
+
blank();
|
|
133
|
+
failure(error instanceof Error ? error.message : String(error));
|
|
134
|
+
blank();
|
|
135
|
+
process.exitCode = 1;
|
|
136
|
+
});
|