Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 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 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 | 3x 3x 3x 2x 2x 6x 2x 4x 8x 8x 4x 4x 2x 2x 2x 2x 2x 2x 2x 6x 6x 1x 5x 5x 5x 4x 8x 4x 4x 1x 2x 1x 1x 1x 1x 1x 1x 3x | /**
* Builds targets/<target>/emit/, the only tree the Pebble bundler reads for a build target.
*
* The steps run in this order, which is why they live in one tool rather than an
* && chain in package.json:
*
* clean tsc never prunes its outDir, so a deleted or renamed .ts would leave its old
* .js behind. waf globs emit/**\/*.js with no filter, so that zombie ships: it
* spends bytes against the 65535 cap, and a stale require still resolves
* against it, which makes a half-finished rename look fine locally while CI
* (always building a fresh emit/) disagrees.
* compile the face's src/pkjs + the framework's ts/ -> emit/, as CommonJS for the SDK's bundler.
* copy the *.g.js Clay components are committed, not tsc output, so tsc never puts
* them in emit/. The emitted index.js requires them by relative path, so they
* have to land beside it.
* vendor ical.js ships from node_modules rather than the source tree, so it lands the
* same way for the same reason, and only for a face whose code requires the calendar reader.
*
* emit/ is written straight into the target's waf staging sandbox (targets/<target>/) so the
* native build never has to stage it. tsc roots at the repo root (the framework sits outside any
* one face), so the tree keeps its source shape: emit/<face>/src/pkjs/index.js for a face in a family,
* or emit/src/pkjs/index.js for a face on its own, beside the framework's emit/<framework folder>/ts/**.
* The wscript tells paf_build.build_face which of those the entry is.
*
* A face that ships several targets (Gridlock's watchface and watchapp) compiles once. The emit tree
* does not depend on the target, so the first target's emit/ is copied whole into each of the others.
*
* build.ts runs it before every Pebble build, which is what paf build runs.
*/
import fs from 'node:fs';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { createRequire } from 'node:module';
import { faceRelative } from '../shared/faces.ts';
import { faceTargetNames } from '../manifest/build-manifests.ts';
import { ENGINE, ENGINE_REL, WORKSPACE, icaljsBundle } from '../shared/paths.ts';
import { ToolError, exitCodeOf, reportFailure } from '../shared/tool-error.ts';
import { isMainScript } from '../shared/entry.ts';
const requireHost = createRequire(import.meta.url);
const ROOT = WORKSPACE;
/** The framework's pkjs compiler options, which every face's generated tsconfig extends. */
export const PKJS_BASE_TSCONFIG = path.join(ENGINE, 'tsconfig.pkjs.json');
/**
* The paths this tool reads and writes. The sandbox is named after the build target. Its
* sources live under the source face. For a face with one target the two names match, but a
* face that ships several targets (a watchface and a watchapp, say) compiles the same source
* face into each target's sandbox.
*/
export interface FacePaths {
faceSrc: string; // <sourceFace>/src/pkjs
sandbox: string; // targets/<target>
emit: string; // targets/<target>/emit
emitPkjs: string; // targets/<target>/emit/<sourceFace>/src/pkjs
icaljsTo: string; // targets/<target>/emit/<framework folder>/ts/calendar/icaljs.js
tsconfig: string; // targets/<target>/tsconfig.pkjs.json (generated)
skipDir: string; // <sourceFace>/src/pkjs/clay/builder
}
/**
* Works out every path this tool reads and writes for one target.
*
* @param target The build target, and the name its sandbox is written under.
* @param sourceFace The face the target's sources live under. Defaults to the target's own name.
* @return The paths this tool reads and writes for the target.
*/
export function facePaths(target: string, sourceFace: string = target): FacePaths {
// the sandbox is named after the target, but the sources are the source face's, and that face
// may sit one level deeper inside a family folder, so its real path is looked up
const rel = faceRelative(sourceFace);
const faceSrc = path.join(ROOT, rel, 'src', 'pkjs');
const sandbox = path.join(ROOT, 'targets', target);
const emit = path.join(sandbox, 'emit');
return {
faceSrc,
sandbox,
emit,
emitPkjs: path.join(emit, ...rel.split('/'), 'src', 'pkjs'),
icaljsTo: path.join(emit, ...ENGINE_REL.split('/'), 'ts', 'calendar', 'icaljs.js'),
tsconfig: path.join(sandbox, 'tsconfig.pkjs.json'),
skipDir: path.join(faceSrc, 'clay', 'builder'),
};
}
/**
* Empties the face's emit tree. It is entirely built output and gitignored, so this is always safe.
*
* @param p The paths for the target being cleaned.
*/
export function cleanEmit(p: FacePaths): void {
fs.rmSync(p.emit, { recursive: true, force: true });
}
/**
* Writes the per-face pkjs tsconfig into the staging sandbox.
*
* It roots at the repo root (so the framework's ts/, shared across faces, stays inside rootDir) and
* emits into the sandbox's emit/. All paths are relative to the sandbox where the file is written.
*
* @param sourceFace The face whose src/pkjs to include.
* @param p The paths for the target being built.
*/
export function writeTsconfig(sourceFace: string, p: FacePaths): void {
// a face may sit one level deeper, inside a family folder, so the globs follow its real path
// rather than assuming the name is the folder
const rel = faceRelative(sourceFace);
const tsconfig = {
extends: path.relative(p.sandbox, PKJS_BASE_TSCONFIG).split(path.sep).join('/'),
compilerOptions: { rootDir: '../..', outDir: 'emit' },
include: [path.posix.join('../..', rel, 'src/pkjs/**/*.ts'), path.posix.join('../..', ENGINE_REL, 'ts/**/*.ts')],
// builder pieces are bundled into the committed *.g.js so compiling them here
// would ship them a second time as loose modules against the 65535 byte cap
// the framework's are excluded for every face even the ones carrying no Clay builder
// ts/testing only holds helpers the specs share, so it never ships either
exclude: [
path.posix.join('../..', rel, 'src/pkjs/clay/builder/**'),
path.posix.join('../..', ENGINE_REL, 'ts/clay/builder/**'),
path.posix.join('../..', ENGINE_REL, 'ts/testing/**'),
'../../**/*.spec.ts',
],
};
fs.mkdirSync(p.sandbox, { recursive: true });
fs.writeFileSync(p.tsconfig, JSON.stringify(tsconfig, null, 2) + '\n');
}
/**
* Compiles the pkjs runtime into the face's emit/.
*
* Runs tsc's own entry under this node rather than the node_modules/.bin shim, which on
* Windows is a .cmd that would need a shell and bring its quoting rules along.
*
* @param p The paths for the target being compiled, including its generated tsconfig.
*/
export function compile(p: FacePaths): void {
const result = spawnSync(process.execPath, [requireHost.resolve('typescript/bin/tsc'), '-p', p.tsconfig], {
stdio: 'inherit',
});
// status is null when a signal killed it, so anything but a clean 0 has to fail here.
// letting a compile error through would copy over a half-built emit/ and still exit 0
if (result.status !== 0) {
throw new ToolError(`tsc exited ${result.status === null ? `on signal ${result.signal}` : result.status}`, exitCodeOf(result));
}
}
/**
* Every committed *.g.js under the face's src/pkjs/, as paths relative to that dir.
*
* @param p The paths for the target being built, including the face's src/pkjs.
* @return Each generated file's path, relative to the face's src/pkjs.
*/
export function findGenerated(p: FacePaths): string[] {
const found: string[] = [];
const walk = (current: string): void => {
if (current === p.skipDir) {
return;
}
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
const full = path.join(current, entry.name);
if (entry.isDirectory()) {
walk(full);
} else if (entry.name.endsWith('.g.js')) {
found.push(path.relative(p.faceSrc, full));
}
}
};
walk(p.faceSrc);
return found.sort();
}
/**
* Mirrors each generated component into emit/, keeping its path under the face's src/pkjs/.
*
* @param p The paths for the target being built.
* @return The generated files that were copied, relative to the face's src/pkjs.
*/
export function copyGenerated(p: FacePaths): string[] {
const names = findGenerated(p);
for (const name of names) {
const to = path.join(p.emitPkjs, name);
fs.mkdirSync(path.dirname(to), { recursive: true });
fs.copyFileSync(path.join(p.faceSrc, name), to);
}
return names;
}
/**
* Whether a chain of relative requires in the emitted CommonJS leads from one file to another.
*
* tsc drops an import that only brings in types, so the requires left in emit/ are the real runtime
* graph, the same one the Pebble bundler follows from the face's entry. The emitted files alone
* cannot say, since tsc still emits a file a face only reaches for its types.
*
* @param from The emitted file to start from, normally the face's index.js.
* @param to The emitted file to look for.
* @return True when the requires lead from one to the other.
*/
export function requires(from: string, to: string): boolean {
const seen = new Set<string>();
const pending = [path.resolve(from)];
const target = path.resolve(to);
while (pending.length) {
const file = pending.pop() as string;
if (file === target) {
return true;
}
Iif (seen.has(file) || !fs.existsSync(file)) {
continue;
}
seen.add(file);
for (const match of fs.readFileSync(file, 'utf8').matchAll(/require\(["'](\.{1,2}\/[^"']+)["']\)/g)) {
const base = path.resolve(path.dirname(file), match[1]);
const found = [base, base + '.js', path.join(base, 'index.js')].find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
Eif (found) {
pending.push(found);
}
}
}
return false;
}
/**
* Copies ical.js's prebuilt ES5 CommonJS file in beside the compiled calendar code, for a face whose
* code requires the calendar reader.
*
* A face that never requires it gets no copy, even though tsc may still have emitted the calendar
* code for the types another module borrows from it. A face that does need the library and finds it
* missing throws rather than letting the build carry on, because the require that reaches for it
* would otherwise fail on the phone, where nobody is watching a build log.
*
* @param p The paths for the target being built, including where ical.js should land.
* @return True when the face needed ical.js and it was copied, false when the face does not use it.
*/
export function copyIcalJs(p: FacePaths): boolean {
if (!requires(path.join(p.emitPkjs, 'index.js'), path.join(path.dirname(p.icaljsTo), 'ical.js'))) {
return false;
}
// the package is ESM behind an `exports` map and the SDK bundles with webpack 1, which reads
// neither, so asking for it by name would resolve its ESM build and break. the package ships a
// prebuilt ES5 CommonJS file for exactly this, and it lands where the framework's
// ts/calendar/icaljs.d.ts says it does. keeping it a file of its own is also what MPL 2.0 asks of
// a larger work
const from = icaljsBundle();
Iif (!from || !fs.existsSync(from)) {
throw new ToolError('ical.js is missing from node_modules, run npm install');
}
fs.mkdirSync(path.dirname(p.icaljsTo), { recursive: true });
fs.copyFileSync(from, p.icaljsTo);
return true;
}
/** What building a face's pkjs did, for the one line printed after it. */
export interface FaceBuild {
targets: string[];
generated: string[];
icaljs: boolean;
}
/**
* Builds emit/ for every target a face declares.
*
* The first target compiles. Every other one gets a copy of that emit/, since the tree keeps the
* source's shape and each sandbox sits at the same depth, so the files come out the same byte for
* byte. Only the compiling target gets a tsconfig written.
*
* @param face The face whose targets to build.
* @return The targets built, the generated components copied, and whether ical.js went in.
*/
export function buildFace(face: string): FaceBuild {
const targets = faceTargetNames(face);
const [first, ...rest] = targets.map((target) => facePaths(target, face));
cleanEmit(first);
writeTsconfig(face, first);
compile(first);
const generated = copyGenerated(first);
const icaljs = copyIcalJs(first);
for (const p of rest) {
cleanEmit(p);
fs.cpSync(first.emit, p.emit, { recursive: true });
}
return { targets, generated, icaljs };
}
/**
* The one line that says what a face's pkjs build did.
*
* @param face The face that was built.
* @param built What buildFace returned for it.
* @return The line, naming the targets, the components copied, and whether ical.js went in.
*/
export function describeBuild(face: string, built: FaceBuild): string {
const where = path.relative(ROOT, facePaths(built.targets[0], face).emitPkjs).split(path.sep).join('/');
return `built emit/ for ${built.targets.join(', ')} and copied ${built.generated.length} generated components into ${where}/${built.icaljs ? ', plus ical.js' : ''}`;
}
function main(): void {
try {
const face = process.argv[2];
if (!face) {
throw new ToolError('usage: node paf/tools/pkjs/build-pkjs.ts <face>, which paf build runs before every build');
}
console.log(describeBuild(face, buildFace(face)));
} catch (error) {
reportFailure(error);
}
}
Iif (isMainScript(import.meta)) {
main();
}
|