@mondaydotcomorg/z2h-cli 0.1.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/README.md +16 -0
- package/bin/z2h-cli.js +2 -0
- package/dist/commands/build.d.ts +5 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +37 -0
- package/dist/commands/clean.d.ts +5 -0
- package/dist/commands/clean.d.ts.map +1 -0
- package/dist/commands/clean.js +24 -0
- package/dist/commands/deploy.d.ts +6 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +92 -0
- package/dist/commands/dev.d.ts +5 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +58 -0
- package/dist/constants.d.ts +25 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +67 -0
- package/dist/esm/commands/build.d.ts +5 -0
- package/dist/esm/commands/build.d.ts.map +1 -0
- package/dist/esm/commands/build.mjs +35 -0
- package/dist/esm/commands/clean.d.ts +5 -0
- package/dist/esm/commands/clean.d.ts.map +1 -0
- package/dist/esm/commands/clean.mjs +22 -0
- package/dist/esm/commands/deploy.d.ts +6 -0
- package/dist/esm/commands/deploy.d.ts.map +1 -0
- package/dist/esm/commands/deploy.mjs +86 -0
- package/dist/esm/commands/dev.d.ts +5 -0
- package/dist/esm/commands/dev.d.ts.map +1 -0
- package/dist/esm/commands/dev.mjs +56 -0
- package/dist/esm/constants.d.ts +25 -0
- package/dist/esm/constants.d.ts.map +1 -0
- package/dist/esm/constants.mjs +57 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.mjs +86 -0
- package/dist/esm/shadow/consumer-pkg.d.ts +24 -0
- package/dist/esm/shadow/consumer-pkg.d.ts.map +1 -0
- package/dist/esm/shadow/consumer-pkg.mjs +81 -0
- package/dist/esm/shadow/gitignore.d.ts +7 -0
- package/dist/esm/shadow/gitignore.d.ts.map +1 -0
- package/dist/esm/shadow/gitignore.mjs +29 -0
- package/dist/esm/shadow/merge-package-json.d.ts +16 -0
- package/dist/esm/shadow/merge-package-json.d.ts.map +1 -0
- package/dist/esm/shadow/merge-package-json.mjs +58 -0
- package/dist/esm/shadow/metadata.d.ts +4 -0
- package/dist/esm/shadow/metadata.d.ts.map +1 -0
- package/dist/esm/shadow/metadata.mjs +18 -0
- package/dist/esm/shadow/paths.d.ts +3 -0
- package/dist/esm/shadow/paths.d.ts.map +1 -0
- package/dist/esm/shadow/paths.mjs +25 -0
- package/dist/esm/shadow/port.d.ts +7 -0
- package/dist/esm/shadow/port.d.ts.map +1 -0
- package/dist/esm/shadow/port.mjs +34 -0
- package/dist/esm/shadow/prepare.d.ts +3 -0
- package/dist/esm/shadow/prepare.d.ts.map +1 -0
- package/dist/esm/shadow/prepare.mjs +60 -0
- package/dist/esm/shadow/scaffold.d.ts +7 -0
- package/dist/esm/shadow/scaffold.d.ts.map +1 -0
- package/dist/esm/shadow/scaffold.mjs +112 -0
- package/dist/esm/shadow/sync.d.ts +9 -0
- package/dist/esm/shadow/sync.d.ts.map +1 -0
- package/dist/esm/shadow/sync.mjs +29 -0
- package/dist/esm/shadow/yarnrc.d.ts +10 -0
- package/dist/esm/shadow/yarnrc.d.ts.map +1 -0
- package/dist/esm/shadow/yarnrc.mjs +27 -0
- package/dist/esm/types.d.ts +53 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.mjs +1 -0
- package/dist/esm/util/app-name.d.ts +12 -0
- package/dist/esm/util/app-name.d.ts.map +1 -0
- package/dist/esm/util/app-name.mjs +17 -0
- package/dist/esm/util/logger.d.ts +22 -0
- package/dist/esm/util/logger.d.ts.map +1 -0
- package/dist/esm/util/logger.mjs +59 -0
- package/dist/esm/util/manifest.d.ts +31 -0
- package/dist/esm/util/manifest.d.ts.map +1 -0
- package/dist/esm/util/manifest.mjs +31 -0
- package/dist/esm/util/s3/client.d.ts +11 -0
- package/dist/esm/util/s3/client.d.ts.map +1 -0
- package/dist/esm/util/s3/client.mjs +38 -0
- package/dist/esm/util/s3/upload.d.ts +2 -0
- package/dist/esm/util/s3/upload.d.ts.map +1 -0
- package/dist/esm/util/s3/upload.mjs +32 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/shadow/consumer-pkg.d.ts +24 -0
- package/dist/shadow/consumer-pkg.d.ts.map +1 -0
- package/dist/shadow/consumer-pkg.js +84 -0
- package/dist/shadow/gitignore.d.ts +7 -0
- package/dist/shadow/gitignore.d.ts.map +1 -0
- package/dist/shadow/gitignore.js +35 -0
- package/dist/shadow/merge-package-json.d.ts +16 -0
- package/dist/shadow/merge-package-json.d.ts.map +1 -0
- package/dist/shadow/merge-package-json.js +66 -0
- package/dist/shadow/metadata.d.ts +4 -0
- package/dist/shadow/metadata.d.ts.map +1 -0
- package/dist/shadow/metadata.js +21 -0
- package/dist/shadow/paths.d.ts +3 -0
- package/dist/shadow/paths.d.ts.map +1 -0
- package/dist/shadow/paths.js +31 -0
- package/dist/shadow/port.d.ts +7 -0
- package/dist/shadow/port.d.ts.map +1 -0
- package/dist/shadow/port.js +41 -0
- package/dist/shadow/prepare.d.ts +3 -0
- package/dist/shadow/prepare.d.ts.map +1 -0
- package/dist/shadow/prepare.js +62 -0
- package/dist/shadow/scaffold.d.ts +7 -0
- package/dist/shadow/scaffold.d.ts.map +1 -0
- package/dist/shadow/scaffold.js +118 -0
- package/dist/shadow/sync.d.ts +9 -0
- package/dist/shadow/sync.d.ts.map +1 -0
- package/dist/shadow/sync.js +31 -0
- package/dist/shadow/yarnrc.d.ts +10 -0
- package/dist/shadow/yarnrc.d.ts.map +1 -0
- package/dist/shadow/yarnrc.js +33 -0
- package/dist/types.d.ts +53 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/util/app-name.d.ts +12 -0
- package/dist/util/app-name.d.ts.map +1 -0
- package/dist/util/app-name.js +20 -0
- package/dist/util/logger.d.ts +22 -0
- package/dist/util/logger.d.ts.map +1 -0
- package/dist/util/logger.js +71 -0
- package/dist/util/manifest.d.ts +31 -0
- package/dist/util/manifest.d.ts.map +1 -0
- package/dist/util/manifest.js +36 -0
- package/dist/util/s3/client.d.ts +11 -0
- package/dist/util/s3/client.d.ts.map +1 -0
- package/dist/util/s3/client.js +48 -0
- package/dist/util/s3/upload.d.ts +2 -0
- package/dist/util/s3/upload.d.ts.map +1 -0
- package/dist/util/s3/upload.js +39 -0
- package/package.json +56 -0
- package/src/commands/build.ts +42 -0
- package/src/commands/clean.ts +26 -0
- package/src/commands/deploy.ts +114 -0
- package/src/commands/dev.ts +64 -0
- package/src/constants.ts +63 -0
- package/src/index.ts +90 -0
- package/src/shadow/consumer-pkg.ts +100 -0
- package/src/shadow/gitignore.ts +30 -0
- package/src/shadow/merge-package-json.ts +62 -0
- package/src/shadow/metadata.ts +17 -0
- package/src/shadow/paths.ts +24 -0
- package/src/shadow/port.ts +34 -0
- package/src/shadow/prepare.ts +68 -0
- package/src/shadow/scaffold.ts +120 -0
- package/src/shadow/sync.ts +30 -0
- package/src/shadow/yarnrc.ts +29 -0
- package/src/types.ts +60 -0
- package/src/util/app-name.ts +16 -0
- package/src/util/logger.ts +64 -0
- package/src/util/manifest.ts +65 -0
- package/src/util/s3/client.ts +40 -0
- package/src/util/s3/upload.ts +41 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { pathExists, readFile, writeFile } from 'fs-extra';
|
|
2
|
+
import { execa } from 'execa';
|
|
3
|
+
import { info } from '../util/logger';
|
|
4
|
+
import { buildShadowPkg, hashConsumerDeps, writeShadowPkg } from './merge-package-json';
|
|
5
|
+
import type { ConsumerPaths, ConsumerPkg } from '../types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Regenerate the shadow package.json from the current CLI template and
|
|
9
|
+
* run `yarn install` at the consumer dir iff the consumer's declared deps
|
|
10
|
+
* have changed since the last install. Idempotent — safe to call on every
|
|
11
|
+
* build, dev start, or consumer package.json edit.
|
|
12
|
+
*/
|
|
13
|
+
export async function syncShadow(paths: ConsumerPaths, consumerPkg: ConsumerPkg): Promise<void> {
|
|
14
|
+
const shadow = buildShadowPkg(consumerPkg);
|
|
15
|
+
await writeShadowPkg(paths.shadowPkg, shadow);
|
|
16
|
+
|
|
17
|
+
const newHash = hashConsumerDeps(consumerPkg);
|
|
18
|
+
const oldHash = (await pathExists(paths.shadowDepsHash)) ? await readFile(paths.shadowDepsHash, 'utf8') : null;
|
|
19
|
+
|
|
20
|
+
if (oldHash !== newHash) {
|
|
21
|
+
info(oldHash ? 'consumer deps changed — yarn install' : 'first run — yarn install');
|
|
22
|
+
// Yarn 4 is cache-first and non-interactive by default
|
|
23
|
+
// in non-TTY contexts, so no flags are needed
|
|
24
|
+
await execa('yarn', ['install'], {
|
|
25
|
+
cwd: paths.consumerDir,
|
|
26
|
+
stdio: 'inherit',
|
|
27
|
+
});
|
|
28
|
+
await writeFile(paths.shadowDepsHash, newHash);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { pathExists, readFile, writeFile } from 'fs-extra';
|
|
3
|
+
import { YARNRC_LINES } from '../constants';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Ensure the consumer has a `.yarnrc.yml` that pins `nodeLinker: node-modules`.
|
|
7
|
+
*
|
|
8
|
+
* Yarn 4 defaults to PnP, which trident-toolkit's webpack pipeline doesn't
|
|
9
|
+
* support cleanly (webpack expects a flat node_modules). We force the
|
|
10
|
+
* classic linker so installs produce a real `node_modules/`. Idempotent —
|
|
11
|
+
* leaves any other settings the user added alone.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export async function ensureYarnRc(consumerDir: string): Promise<void> {
|
|
15
|
+
const yarnrcPath = path.join(consumerDir, '.yarnrc.yml');
|
|
16
|
+
|
|
17
|
+
if (!(await pathExists(yarnrcPath))) {
|
|
18
|
+
await writeFile(yarnrcPath, YARNRC_LINES.join('\n\n') + '\n');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const body = await readFile(yarnrcPath, 'utf8');
|
|
23
|
+
if (/^\s*nodeLinker\s*:/m.test(body)) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const prefix = body.length && !body.endsWith('\n') ? '\n' : '';
|
|
28
|
+
await writeFile(yarnrcPath, body + prefix + 'nodeLinker: node-modules\n');
|
|
29
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared domain types for the Z2H CLI. Command-scoped option types live
|
|
3
|
+
* with their command; narrow single-use types stay local to their file.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ─────────── Consumer pkg management ───────────
|
|
7
|
+
export interface ConsumerPkg {
|
|
8
|
+
name: string;
|
|
9
|
+
version?: string;
|
|
10
|
+
private?: boolean;
|
|
11
|
+
workspaces?: string[] | { packages?: string[] };
|
|
12
|
+
dependencies?: Record<string, string>;
|
|
13
|
+
devDependencies?: Record<string, string>;
|
|
14
|
+
peerDependencies?: Record<string, string>;
|
|
15
|
+
scripts?: Record<string, string>;
|
|
16
|
+
[k: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ─────────── Shadow pkg (generated by CLI, lives at .zth/package.json) ───
|
|
20
|
+
export interface ShadowPkg {
|
|
21
|
+
name: string;
|
|
22
|
+
version: string;
|
|
23
|
+
private: true;
|
|
24
|
+
scripts: Record<string, string>;
|
|
25
|
+
peerDependencies: Record<string, string>;
|
|
26
|
+
devDependencies: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─────────── Persisted shadow metadata (.zth/metadata.json) ───────
|
|
30
|
+
export interface ShadowMetadata {
|
|
31
|
+
createdAt: string;
|
|
32
|
+
appName: string;
|
|
33
|
+
port: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ─────────── Filesystem paths, resolved once per invocation ─────
|
|
37
|
+
export interface ConsumerPaths {
|
|
38
|
+
consumerDir: string;
|
|
39
|
+
consumerPkg: string;
|
|
40
|
+
consumerSrc: string;
|
|
41
|
+
consumerSrcEntry: string;
|
|
42
|
+
shadowDir: string;
|
|
43
|
+
shadowPkg: string;
|
|
44
|
+
shadowSrc: string;
|
|
45
|
+
shadowWrapper: string;
|
|
46
|
+
shadowTridentrc: string;
|
|
47
|
+
shadowNvmrc: string;
|
|
48
|
+
shadowDts: string;
|
|
49
|
+
shadowTsconfig: string;
|
|
50
|
+
shadowBuild: string;
|
|
51
|
+
shadowDepsHash: string;
|
|
52
|
+
shadowMetadata: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─────────── prepareShadow result ───────────
|
|
56
|
+
export interface PreparedShadow {
|
|
57
|
+
appName: string;
|
|
58
|
+
port: number;
|
|
59
|
+
paths: ConsumerPaths;
|
|
60
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip any npm scope (`@scope/`) from a package name.
|
|
3
|
+
* e.g. `@trident/customer-viewer` → `customer-viewer`, `foo` → `foo`.
|
|
4
|
+
*/
|
|
5
|
+
export function cleanAppName(name: string): string {
|
|
6
|
+
return name.replace(/^@[^/]+\//, '');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Canonical MF bundle name: the de-scoped app name with an `mf-` prefix.
|
|
11
|
+
* This matches the trident convention (shadow pkg is `@trident/mf-<app>`,
|
|
12
|
+
* emitted bundle is `MF-mf-<app>`).
|
|
13
|
+
*/
|
|
14
|
+
export function mfAppName(name: string): string {
|
|
15
|
+
return `mf-${cleanAppName(name)}`;
|
|
16
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
|
|
3
|
+
export type OutputMode = 'human' | 'json';
|
|
4
|
+
|
|
5
|
+
let mode: OutputMode = 'human';
|
|
6
|
+
|
|
7
|
+
export function setOutputMode(next: OutputMode): void {
|
|
8
|
+
mode = next;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getOutputMode(): OutputMode {
|
|
12
|
+
return mode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Progress, info, and warnings go to stderr so stdout stays clean
|
|
17
|
+
* for machine-readable output. Always suppressed in JSON mode except
|
|
18
|
+
* when the consumer explicitly wants chatty progress (e.g. setup).
|
|
19
|
+
*/
|
|
20
|
+
export function info(msg: string): void {
|
|
21
|
+
if (mode === 'human') {
|
|
22
|
+
process.stderr.write(pc.cyan(`[z2h-cli] ${msg}\n`));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function warn(msg: string): void {
|
|
27
|
+
if (mode === 'human') {
|
|
28
|
+
process.stderr.write(pc.yellow(`[z2h-cli] ${msg}\n`));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function errorLog(msg: string): void {
|
|
33
|
+
if (mode === 'human') {
|
|
34
|
+
process.stderr.write(pc.red(`[z2h-cli] ${msg}\n`));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Data output. In human mode writes `human` to stdout; in JSON mode writes
|
|
40
|
+
* JSON-stringified `data` to stdout. Exactly one line of stdout per call.
|
|
41
|
+
*/
|
|
42
|
+
export function output(human: string, data: unknown): void {
|
|
43
|
+
if (mode === 'json') {
|
|
44
|
+
process.stdout.write(`${JSON.stringify(data)}\n`);
|
|
45
|
+
} else {
|
|
46
|
+
process.stdout.write(`${human}\n`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Emit a structured failure payload (JSON mode) or a red error line (human mode),
|
|
52
|
+
* then exit with the given code.
|
|
53
|
+
*/
|
|
54
|
+
export function fail(message: string, payload: Record<string, unknown>, exitCode = 1): never {
|
|
55
|
+
if (mode === 'json') {
|
|
56
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: message, ...payload })}\n`);
|
|
57
|
+
} else {
|
|
58
|
+
process.stderr.write(pc.red(`[z2h-cli] ${message}\n`));
|
|
59
|
+
if (typeof payload.suggestedCommand === 'string') {
|
|
60
|
+
process.stderr.write(pc.dim(` → ${payload.suggestedCommand}\n`));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
process.exit(exitCode);
|
|
64
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { PutObjectCommand } from '@aws-sdk/client-s3';
|
|
2
|
+
import { BUCKET_WRITE_NAME, MANIFEST_KEY, MANIFEST_READ_URL, getS3Client } from './s3/client';
|
|
3
|
+
|
|
4
|
+
export interface SharedDep {
|
|
5
|
+
name?: string;
|
|
6
|
+
globalName: string;
|
|
7
|
+
url?: string;
|
|
8
|
+
minVersion?: string;
|
|
9
|
+
dependencies: Record<string, number>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Shape of the `asset-manifest.json` trident-toolkit emits alongside the
|
|
14
|
+
* bundle. We consume this file directly as the base for a Z2H registry
|
|
15
|
+
* entry — deploy.ts only overlays `version` and `url`.
|
|
16
|
+
*/
|
|
17
|
+
export interface AssetManifestFile {
|
|
18
|
+
sameAsHost: boolean;
|
|
19
|
+
skipTranslationFetch: boolean;
|
|
20
|
+
shared: SharedDep[];
|
|
21
|
+
entrypoints: string[];
|
|
22
|
+
nonExternalsEntrypoints?: string[];
|
|
23
|
+
scriptType?: 'module' | 'classic';
|
|
24
|
+
mfs?: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ManifestEntry extends AssetManifestFile {
|
|
28
|
+
version: number;
|
|
29
|
+
url: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type Manifest = Record<string, ManifestEntry>;
|
|
33
|
+
|
|
34
|
+
export async function readManifest(): Promise<Manifest> {
|
|
35
|
+
// Read via the public CDN endpoint, no AWS auth required.
|
|
36
|
+
const res = await fetch(MANIFEST_READ_URL);
|
|
37
|
+
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
throw new Error(`failed to read manifest from ${MANIFEST_READ_URL}: HTTP ${res.status}`);
|
|
40
|
+
}
|
|
41
|
+
return (await res.json()) as Manifest;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function nextVersion(manifest: Manifest, app: string) {
|
|
45
|
+
const current = manifest[app]?.version;
|
|
46
|
+
const n = current ? Number(current) : 0;
|
|
47
|
+
return (Number.isNaN(n) ? 0 : n) + 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function upsertEntry(manifest: Manifest, app: string, entry: ManifestEntry): Manifest {
|
|
51
|
+
return { ...manifest, [app]: entry };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function writeManifest(manifest: Manifest): Promise<void> {
|
|
55
|
+
const s3 = getS3Client();
|
|
56
|
+
await s3.send(
|
|
57
|
+
new PutObjectCommand({
|
|
58
|
+
Bucket: BUCKET_WRITE_NAME,
|
|
59
|
+
Key: MANIFEST_KEY,
|
|
60
|
+
Body: JSON.stringify(manifest, null, 2),
|
|
61
|
+
ContentType: 'application/json',
|
|
62
|
+
CacheControl: 'no-cache',
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { HeadBucketCommand, S3Client, S3ServiceException } from '@aws-sdk/client-s3';
|
|
2
|
+
|
|
3
|
+
export const BUCKET_WRITE_NAME = 'prod-use1-bigbrain-zth-mf-assets';
|
|
4
|
+
export const REGION = 'us-east-1';
|
|
5
|
+
export const BUCKET_WRITE_URL = `https://${BUCKET_WRITE_NAME}.s3.${REGION}.amazonaws.com`;
|
|
6
|
+
|
|
7
|
+
export const BUCKET_READ_DNS_NAME = 'bigbrain-zth-mf-assets';
|
|
8
|
+
export const BUCKET_READ_DNS_URL = `https://${BUCKET_READ_DNS_NAME}.bigbrain.me`;
|
|
9
|
+
|
|
10
|
+
// No `.json` extension — TridentSkeleton's `mfUrl` API appends `/mf-assets`
|
|
11
|
+
// to the bucket base when fetching, so the manifest must live at this exact
|
|
12
|
+
// key. See microfrontends/mf-bigbrain-zth/.../ZeroToHeroHost.tsx.
|
|
13
|
+
export const MANIFEST_KEY = 'mf-assets';
|
|
14
|
+
export const MANIFEST_READ_URL = `${BUCKET_READ_DNS_URL}/${MANIFEST_KEY}`;
|
|
15
|
+
|
|
16
|
+
let client: S3Client | null = null;
|
|
17
|
+
|
|
18
|
+
export function getS3Client(): S3Client {
|
|
19
|
+
if (!client) {
|
|
20
|
+
client = new S3Client({ region: REGION });
|
|
21
|
+
}
|
|
22
|
+
return client;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function assertBucketExists(): Promise<void> {
|
|
26
|
+
try {
|
|
27
|
+
await getS3Client().send(new HeadBucketCommand({ Bucket: BUCKET_WRITE_NAME }));
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (err instanceof S3ServiceException) {
|
|
30
|
+
const status = err.$metadata.httpStatusCode;
|
|
31
|
+
if (status === 404 || err.name === 'NotFound' || err.name === 'NoSuchBucket') {
|
|
32
|
+
throw new Error(`S3 bucket "${BUCKET_WRITE_NAME}" does not exist (region: ${REGION}).`);
|
|
33
|
+
}
|
|
34
|
+
if (status === 403) {
|
|
35
|
+
throw new Error(`No access to S3 bucket "${BUCKET_WRITE_NAME}" (region: ${REGION}). Check AWS credentials.`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readFile } from 'fs-extra';
|
|
3
|
+
import { readdir } from 'node:fs/promises';
|
|
4
|
+
import { lookup as lookupMime } from 'mime-types';
|
|
5
|
+
import pMap from 'p-map';
|
|
6
|
+
import { PutObjectCommand } from '@aws-sdk/client-s3';
|
|
7
|
+
import { BUCKET_WRITE_NAME, getS3Client } from './client';
|
|
8
|
+
|
|
9
|
+
// Each deploy uploads to a version-prefixed path (<app>/<version>/...), so
|
|
10
|
+
// the URL itself is the cache key. Assets never need to be revalidated.
|
|
11
|
+
const FOREVER = 'public, immutable';
|
|
12
|
+
|
|
13
|
+
const UPLOAD_CONCURRENCY = 5;
|
|
14
|
+
|
|
15
|
+
export async function uploadDir(localDir: string, keyPrefix: string): Promise<string[]> {
|
|
16
|
+
const entries = await readdir(localDir, { recursive: true, withFileTypes: true });
|
|
17
|
+
const files = entries.filter(e => e.isFile()).map(e => path.relative(localDir, path.join(e.parentPath, e.name)));
|
|
18
|
+
|
|
19
|
+
const s3 = getS3Client();
|
|
20
|
+
|
|
21
|
+
await pMap(
|
|
22
|
+
files,
|
|
23
|
+
async rel => {
|
|
24
|
+
const body = await readFile(path.join(localDir, rel));
|
|
25
|
+
const contentType = lookupMime(rel) || 'application/octet-stream';
|
|
26
|
+
const key = `${keyPrefix}/${rel.split(path.sep).join('/')}`;
|
|
27
|
+
await s3.send(
|
|
28
|
+
new PutObjectCommand({
|
|
29
|
+
Bucket: BUCKET_WRITE_NAME,
|
|
30
|
+
Key: key,
|
|
31
|
+
Body: body,
|
|
32
|
+
ContentType: contentType,
|
|
33
|
+
CacheControl: FOREVER,
|
|
34
|
+
})
|
|
35
|
+
);
|
|
36
|
+
},
|
|
37
|
+
{ concurrency: UPLOAD_CONCURRENCY }
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return files.map(f => f.split(path.sep).join('/'));
|
|
41
|
+
}
|