mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
webapp/platform publishes six packages and the channels app carries a seventh in-tree, and every one of them went out under @mattermost — types, client, components, shared, eslint-plugin, compass-icons and mattermost-redux — while the plugin that consumes them already asks for @hanzoteam/types and @hanzoteam/client by name. It could not build: it was asking a registry for a package nobody publishes. They are @hanzoteam/* now, and mattermost-redux is @hanzoteam/redux, moved to src/packages/@hanzoteam/redux so the directory matches the alias that always pointed there. @mattermost/calls-common and @mattermost/desktop-api stay. Those are published by someone else and resolved from npm; renaming a package we merely consume would name a thing that does not exist. The jest moduleNameMapper entries needed doing by hand — they spell the scope inside a regex, so a sweep over the plain name walks straight past them, and a missed one fails at require time rather than at build.
132 lines
6.4 KiB
JavaScript
132 lines
6.4 KiB
JavaScript
// Publish this fork's platform packages under the @hanzoteam scope.
|
|
//
|
|
// Run from webapp/platform: REGISTRY_TOKEN=<forge write:package token> \
|
|
// node publish.mjs [client types shared ...]
|
|
//
|
|
// A package is published from a STAGING copy, never from the source tree, so a
|
|
// failed run cannot leave a rewritten package.json behind for the next build to
|
|
// pick up. Build first (`npm run build` in the package) — the manifest's `files`
|
|
// is what gets packed, and for these that is `lib`.
|
|
//
|
|
// THE MAP IS THE WHOLE DESIGN. `OURS` says which packages this fork publishes
|
|
// and what each is called once published, and it is read TWICE: once to rename
|
|
// the package, once to rewrite the dependencies. That is what makes the rewrite
|
|
// correct rather than a careful search-and-replace — a dependency is renamed if
|
|
// and only if we are the ones publishing it. The webapp also depends on four
|
|
// @mattermost packages we do NOT fork (`desktop-api`, `calls-common`,
|
|
// `use-external-link`, `no-dispatch-getstate`); they resolve from public npm and
|
|
// are left exactly as written, which a blanket `@mattermost/`→`@hanzoteam/`
|
|
// substitution would silently break by pointing them at packages nobody has
|
|
// published.
|
|
//
|
|
// This exists because the first publish was done by hand and got that wrong in
|
|
// one place: @hanzoteam/client shipped a peerDependency on `@hanzoteam/types`
|
|
// at THIS fork's version, 11.11.0 — a coordinate that exists in no registry,
|
|
// upstream or ours. npm 7+ installs peers automatically, so every `npm i
|
|
// @hanzoteam/client` died with ETARGET and the package was unusable from the
|
|
// moment it was published. Nothing in the source was wrong; the rename was
|
|
// applied to the name and not to what the name points at.
|
|
import {execFileSync} from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
const OURS = {
|
|
'@hanzoteam/client': '@hanzoteam/client',
|
|
'@hanzoteam/components': '@hanzoteam/components',
|
|
'@hanzoteam/eslint-plugin': '@hanzoteam/eslint-plugin',
|
|
'@hanzoteam/shared': '@hanzoteam/shared',
|
|
'@hanzoteam/types': '@hanzoteam/types',
|
|
'@hanzoteam/redux': '@hanzoteam/redux',
|
|
};
|
|
|
|
const REGISTRY = 'https://git.hanzo.ai/v1/packages/hanzoteam/npm/';
|
|
const HOME = 'https://git.hanzo.ai/hanzoteam/server';
|
|
|
|
// The directory each package lives in, derived from the published name so the
|
|
// two cannot drift: platform/<dir> is the tail of the SOURCE name.
|
|
const dirOf = (src) => (src.startsWith('@') ? src.split('/')[1] : src);
|
|
|
|
const token = process.env.REGISTRY_TOKEN;
|
|
if (!token) {
|
|
throw new Error('REGISTRY_TOKEN is required (a forge token scoped write:package)');
|
|
}
|
|
|
|
// Default to every package we publish; a caller may name a subset.
|
|
const wanted = process.argv.slice(2);
|
|
const targets = Object.keys(OURS).filter((src) => !wanted.length || wanted.includes(dirOf(src)));
|
|
if (!targets.length) {
|
|
throw new Error(`nothing to publish; known: ${Object.keys(OURS).map(dirOf).join(' ')}`);
|
|
}
|
|
|
|
for (const src of targets) {
|
|
const dir = path.resolve(dirOf(src));
|
|
const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
if (manifest.name !== src) {
|
|
throw new Error(`${dir}: expected ${src}, found ${manifest.name}`);
|
|
}
|
|
|
|
manifest.name = OURS[src];
|
|
for (const field of ['dependencies', 'devDependencies', 'peerDependencies', 'peerDependenciesMeta']) {
|
|
const block = manifest[field];
|
|
if (!block) {
|
|
continue;
|
|
}
|
|
manifest[field] = Object.fromEntries(
|
|
Object.entries(block).map(([dep, spec]) => [OURS[dep] ?? dep, spec]),
|
|
);
|
|
}
|
|
manifest.homepage = `${HOME}/tree/master/webapp/platform/${dirOf(src)}`;
|
|
manifest.repository = {type: 'git', url: `git+${HOME}.git`, directory: `webapp/platform/${dirOf(src)}`};
|
|
manifest.keywords = ['hanzo', 'team'];
|
|
|
|
// Stage: the manifest plus exactly what `files` declares. Publishing from a
|
|
// copy is what keeps a failure from mutating the source tree.
|
|
const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'hanzoteam-'));
|
|
fs.writeFileSync(path.join(stage, 'package.json'), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
for (const entry of manifest.files ?? []) {
|
|
fs.cpSync(path.join(dir, entry), path.join(stage, entry), {recursive: true});
|
|
}
|
|
// The same map, read a third time — over the BUILT output. TypeScript writes a
|
|
// module specifier exactly as authored, so `lib/*.d.ts` names the sibling by the
|
|
// name it has in this monorepo, and a declaration naming a package the consumer
|
|
// never installed makes every type it carries resolve to `any` on the far side.
|
|
//
|
|
// It is done HERE, to the staged copy, and not in the source: the name a package
|
|
// is published under does not exist inside this repo, so putting it in source
|
|
// makes the emitted JS require a module no workspace provides — which type
|
|
// checking does not catch (paths satisfy tsc and change nothing it emits) and
|
|
// which fails at bundle time in whatever consumes the workspace. One name in the
|
|
// tree, one rename at the boundary.
|
|
for (const [from, to] of Object.entries(OURS)) {
|
|
const specifier = new RegExp(`(['"])${from.replace('/', '\\/')}(/[^'"]*)?\\1`, 'g');
|
|
const walk = (p) => {
|
|
for (const e of fs.readdirSync(p, {withFileTypes: true})) {
|
|
const full = path.join(p, e.name);
|
|
if (e.isDirectory()) {
|
|
walk(full);
|
|
} else if (/\.(js|mjs|cjs|d\.ts|ts|map)$/.test(e.name)) {
|
|
const before = fs.readFileSync(full, 'utf8');
|
|
const after = before.replace(specifier, (_, q, sub) => `${q}${to}${sub ?? ''}${q}`);
|
|
if (after !== before) {
|
|
fs.writeFileSync(full, after);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
walk(stage);
|
|
}
|
|
|
|
// The credential lives in the staging directory and dies with it, so it is
|
|
// never written into the repository or into a shared npm config.
|
|
fs.writeFileSync(
|
|
path.join(stage, '.npmrc'),
|
|
`@hanzoteam:registry=${REGISTRY}\n${REGISTRY.replace(/^https:/, '')}:_authToken=${token}\n`,
|
|
{mode: 0o600},
|
|
);
|
|
|
|
execFileSync('npm', ['publish', '--access', 'public'], {cwd: stage, stdio: 'inherit'});
|
|
fs.rmSync(stage, {recursive: true, force: true});
|
|
process.stdout.write(`published ${manifest.name}@${manifest.version}\n`);
|
|
}
|