-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Expand file tree
/
Copy pathresolve-content-root.mjs
More file actions
253 lines (237 loc) · 9.61 KB
/
Copy pathresolve-content-root.mjs
File metadata and controls
253 lines (237 loc) · 9.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
#!/usr/bin/env node
// Copyright (c) Meta Platforms, Inc. and affiliates.
/**
* @file resolve-content-root.mjs
*
* Resolves the CONTENT ROOT the docsite data pipeline reads package
* documentation from (component .doc.mjs prop tables, package.json versions,
* READMEs, CHANGELOGs).
*
* The docsite CODE is always the current checkout. What varies per deploy is
* *which version of the @astryxdesign/* packages* supplies that documentation:
*
* canary → the live monorepo workspace (REPO_ROOT). Documents main / WIP.
* Used for the canary deploy AND every PR preview.
* latest → the last PUBLISHED release on npm. Documents exactly what
* `npm install` gives a consumer today. Used for production.
*
* Target selection (no manual config in the common case):
* - Explicit override: DOCSITE_TARGET=latest|canary always wins.
* - Otherwise derived from Vercel's VERCEL_ENV: `production` → latest,
* everything else (preview, development, unset) → canary.
*
* How `latest` sources are materialized: the version is read live from npm
* (`npm view @astryxdesign/core version`) so it always tracks the current
* published release with nothing to hand-maintain. Each docsite @astryxdesign/*
* dependency's published tarball is downloaded and unpacked into a cache dir
* laid out like the monorepo, so generate-data.mjs's workspace discovery works
* unchanged. The published tarballs ship `src/` (see each package's
* package.json "files"), so the .doc.mjs docs the pipeline reads are present.
*
* This module is the SINGLE place that maps a target to a filesystem root.
* generate-data.mjs consumes CONTENT_ROOT from here; every generated registry
* keeps its exact shape, so no consuming page code changes.
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import {execFileSync} from 'node:child_process';
import * as os from 'node:os';
import {fileURLToPath} from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DOCSITE_ROOT = path.resolve(__dirname, '..');
const REPO_ROOT = path.resolve(DOCSITE_ROOT, '..', '..');
/** The npm dist-tag whose version `latest` documents. */
const PUBLISHED_DIST_TAG = 'latest';
/** Package whose published version defines the release the docsite pins to. */
const VERSION_SOURCE_PKG = '@astryxdesign/core';
function run(cmd, args, opts = {}) {
return execFileSync(cmd, args, {encoding: 'utf-8', ...opts});
}
/**
* Resolve the build target. Explicit DOCSITE_TARGET wins; otherwise derive from
* VERCEL_ENV (production → latest, else canary).
*/
export function getTarget() {
const explicit = (process.env.DOCSITE_TARGET || '').trim();
if (explicit) {
if (explicit !== 'canary' && explicit !== 'latest') {
throw new Error(
`Invalid DOCSITE_TARGET="${explicit}". Expected "canary" or "latest".`,
);
}
return explicit;
}
return process.env.VERCEL_ENV === 'production' ? 'latest' : 'canary';
}
/**
* The docsite's @astryxdesign/* dependencies that are documented on the
* `latest` (production) site: the ones actually published to the stable npm
* `latest` dist-tag. Mapped to the monorepo-relative dir each occupies (so the
* materialized cache mirrors REPO_ROOT's layout — themes under
* packages/themes/<name>, everything else under packages/<name>).
*
* Packages that never reach the stable tag are EXCLUDED from `latest`:
* - `private` packages, and
* - `astryx.canaryOnly` packages (e.g. @astryxdesign/charts, @astryxdesign/lab)
* — these publish only as canaries, so no stable version exists to document.
* This mirrors the publishable predicate in .github/workflows/release.yml's
* stable-publish step, so production documents exactly the stable release set.
* (On canary these still appear, sourced from the workspace like everything else.)
*/
function latestPublishablePackages() {
const pkg = JSON.parse(
fs.readFileSync(path.join(DOCSITE_ROOT, 'package.json'), 'utf-8'),
);
const deps = {...pkg.dependencies, ...pkg.devDependencies};
return Object.keys(deps)
.filter(name => name.startsWith('@astryxdesign/'))
.map(name => {
const short = name.slice('@astryxdesign/'.length);
const dir = short.startsWith('theme-')
? path.join('packages', 'themes', short.slice('theme-'.length))
: path.join('packages', short);
return {name, dir};
})
.filter(({dir}) => {
// Read the package's own workspace manifest for its publish flags. Same
// rule release.yml uses for the stable `latest` tag.
const manifestPath = path.join(REPO_ROOT, dir, 'package.json');
if (!fs.existsSync(manifestPath)) {
return false;
}
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
return manifest.private !== true && manifest.astryx?.canaryOnly !== true;
});
}
/** Latest published version of the version-source package, read live from npm. */
function latestPublishedVersion() {
const out = run(
'npm',
[
'view',
`${VERSION_SOURCE_PKG}@${PUBLISHED_DIST_TAG}`,
'version',
'--json',
],
{maxBuffer: 8 * 1024 * 1024},
).trim();
// `npm view … version` prints a bare quoted string for a single match.
const version = JSON.parse(out);
if (typeof version !== 'string' || !version) {
throw new Error(
`Could not resolve a published version for ${VERSION_SOURCE_PKG}@${PUBLISHED_DIST_TAG}.`,
);
}
return version;
}
/**
* Materialize the published package sources for `latest` into a cache dir by
* downloading each package's PUBLISHED TARBALL from npm.
*
* Layout produced (mirrors a real monorepo release so generate-data.mjs's
* workspace discovery works unchanged):
* <cache>/package.json — synthetic root (workspaces globs)
* <cache>/packages/core/… — @astryxdesign/core tarball
* <cache>/packages/themes/<name>/… — @astryxdesign/theme-<name> tarballs
*/
function materializeFromNpm(version, packages) {
const cacheRoot = path.join(DOCSITE_ROOT, '.content-cache', `npm-${version}`);
const stamp = path.join(cacheRoot, '.stamp');
const stampValue = `npm-${version}:${packages
.map(p => p.name)
.sort()
.join(',')}`;
if (
fs.existsSync(stamp) &&
fs.readFileSync(stamp, 'utf-8').trim() === stampValue
) {
return cacheRoot;
}
fs.rmSync(cacheRoot, {recursive: true, force: true});
fs.mkdirSync(cacheRoot, {recursive: true});
// Synthetic root package.json so discoverPackageDirs() expands the same
// workspace globs it would in the real monorepo.
fs.writeFileSync(
path.join(cacheRoot, 'package.json'),
JSON.stringify(
{
name: 'astryx-pinned-content',
private: true,
version,
workspaces: ['packages/*', 'packages/themes/*'],
},
null,
2,
),
);
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-pin-'));
try {
for (const {name, dir} of packages) {
const spec = `${name}@${version}`;
// `npm pack` downloads the exact published tarball and prints its
// filename; --pack-destination keeps it out of the cwd.
//
// Run it from the tmp dir (OUTSIDE the repo), NOT from the workspace: the
// root package.json declares `devEngines.packageManager: pnpm`, which
// makes npm hard-error EBADDEVENGINES if it discovers that manifest by
// walking up from the cwd. The tmp dir has no package.json, so npm skips
// the engine guard. (COREPACK_ENABLE_STRICT=0 as belt-and-suspenders.)
const out = run(
'npm',
['pack', spec, '--pack-destination', tmp, '--json'],
{
cwd: tmp,
maxBuffer: 256 * 1024 * 1024,
env: {...process.env, COREPACK_ENABLE_STRICT: '0'},
},
);
const tgz = path.join(tmp, JSON.parse(out)[0].filename);
const dest = path.join(cacheRoot, dir);
fs.mkdirSync(dest, {recursive: true});
// npm tarballs wrap everything under a top-level "package/" dir.
run('tar', ['-x', '-z', '-f', tgz, '--strip-components=1', '-C', dest]);
}
} finally {
fs.rmSync(tmp, {recursive: true, force: true});
}
fs.writeFileSync(stamp, stampValue);
return cacheRoot;
}
/**
* Returns {target, contentRoot, cliRoot, version} for the current target.
* contentRoot mirrors REPO_ROOT layout (has packages/* and package.json).
*/
export function resolveContentRoot() {
const target = getTarget();
if (target === 'canary') {
return {
target,
contentRoot: REPO_ROOT,
cliRoot: path.join(REPO_ROOT, 'packages', 'cli'),
version: null,
};
}
// latest
const version = latestPublishedVersion();
const packages = latestPublishablePackages();
const contentRoot = materializeFromNpm(version, packages);
return {
target,
// DATA (component .doc.mjs prop tables, package.json versions, READMEs) is
// read from the published release — that's the point of `latest`.
contentRoot,
// EXECUTABLE content (CLI templates: showcases, examples, blocks, pages,
// long-form docs) is LIVE-RENDERED as React and always resolves
// @astryxdesign/core from node_modules — i.e. the version the docsite
// bundles. Pinning it would make a stale release's demo use API the bundled
// core no longer exposes, breaking render AND typecheck. So it always comes
// from the real workspace; only the documented DATA is pinned.
cliRoot: path.join(REPO_ROOT, 'packages', 'cli'),
version,
};
}
// CLI usage: `node resolve-content-root.mjs` prints the resolution as JSON.
if (import.meta.url === `file://${process.argv[1]}`) {
const r = resolveContentRoot();
process.stdout.write(JSON.stringify(r, null, 2) + '\n');
}