Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,27 @@ filegroup(
],
)

filegroup(
name = "sveltekit_build_srcs",
srcs = glob(
[
"packages/**",
"scripts/**",
"src/**",
"static/**",
],
allow_empty = True,
) + [
".npmrc",
"package-lock.json",
"package.json",
"pnpm-lock.yaml",
"svelte.config.js",
"tsconfig.json",
"vite.config.ts",
],
)

js_binary(
name = "vitest_bazel",
data = [
Expand Down Expand Up @@ -66,6 +87,25 @@ js_test(
visibility = ["//visibility:public"],
)

js_test(
name = "sveltekit_vite_build_smoke",
copy_data_to_bin = False,
data = [
":node_modules",
":sveltekit_build_srcs",
],
entry_point = "scripts/run-sveltekit-vite-build-bazel.mjs",
patch_node_fs = False,
tags = [
"gloriousflywheel-rbe-candidate",
"sveltekit",
"vite",
"web-build-runtime-authority",
],
timeout = "long",
visibility = ["//visibility:public"],
)

js_test(
name = "playwright_chromium_smoke",
copy_data_to_bin = False,
Expand Down Expand Up @@ -115,6 +155,7 @@ exports_files(
"pnpm-lock.yaml",
"scripts/run-playwright-chromium-bazel.mjs",
"scripts/run-puppeteer-chromium-bazel.mjs",
"scripts/run-sveltekit-vite-build-bazel.mjs",
"scripts/run-vitest-bazel.mjs",
"vitest.bazel.config.ts",
],
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,14 @@ flowchart LR
This repo still uses the npm/SvelteKit workflow for normal local development and deployment. The Bazel files are a narrow GloriousFlywheel consumer proof surface, not a wholesale migration of the blog build.

- `//:types_unit_tests` wraps Vitest through `vitest.bazel.config.ts` and runs the existing `src/lib/types.test.ts` slice.
- `//:sveltekit_vite_build_smoke` runs a copied-workdir SvelteKit/Vite production build smoke. It proves the build target class, not the full npm prebuild/postbuild publication chain.
- `//:playwright_chromium_smoke` launches Playwright against the pinned GloriousFlywheel Chromium runtime path. It is a browser-runtime smoke target, not the full hosted Playwright regression suite.
- `//:puppeteer_chromium_smoke` launches Puppeteer against the same pinned Chromium runtime path. It proves Puppeteer can consume browser runtime authority without lifecycle downloads.
- `package-lock.json` remains the npm dependency authority for the app. `pnpm-lock.yaml` is the generated `rules_js` lock consumed by Bazel.
- Bazel npm lifecycle hooks skip Playwright and Puppeteer browser downloads. Browser-backed RBE must use the pinned worker Chromium path rather than downloading browsers during proof actions.
- GloriousFlywheel proof runs should use the external GF REAPI proof harness against this public repo checkout.

Current boundary: this proves narrow public SvelteKit/Vite/Vitest, Playwright/Chromium, and Puppeteer/Chromium target classes for remote execution evidence. It does not prove default repo-wide RBE, the full hosted Playwright suite, the full SvelteKit build, or deployment.
Current boundary: this proves narrow public SvelteKit/Vite/Vitest, SvelteKit/Vite build-smoke, Playwright/Chromium, and Puppeteer/Chromium target classes for remote execution evidence. It does not prove default repo-wide RBE, the full hosted Playwright suite, the full npm prebuild/postbuild publication chain, or deployment.



Expand Down
242 changes: 242 additions & 0 deletions scripts/run-sveltekit-vite-build-bazel.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import {
accessSync,
constants,
cpSync,
existsSync,
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
statSync,
symlinkSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { spawnSync } from 'node:child_process';

const workspaceRoot = process.cwd();
const runtimeRoot = mkdtempSync(join(tmpdir(), 'ghio-sveltekit-vite-build-'));
const buildRoot = join(runtimeRoot, 'workspace');

mkdirSync(buildRoot, { recursive: true });
ensureWritableEnvDir('HOME', join(runtimeRoot, 'home'));
ensureWritableEnvDir('XDG_CONFIG_HOME', join(runtimeRoot, 'xdg-config'));
ensureWritableEnvDir('XDG_CACHE_HOME', join(runtimeRoot, 'xdg-cache'));
process.env.CI = 'true';
process.env.NODE_ENV = 'production';
process.env.MERMAID_PRERENDER = process.env.MERMAID_PRERENDER ?? 'optional';

copyInputsToBuildRoot();
linkNodeModules();

const packageJson = JSON.parse(readFileSync(join(buildRoot, 'package.json'), 'utf8'));

for (const command of [
['tsx', 'scripts/ingest-tinyland-posts.mts', '--check'],
['tsx', 'scripts/generate-search-index.mts'],
['tsx', 'scripts/generate-blog-stats.mts'],
['tsx', 'scripts/generate-tag-graph.mts'],
['tsx', 'scripts/generate-photo-gallery.mts'],
['tsx', 'scripts/validate-pulse-snapshot.mts'],
['svelte-kit', 'sync'],
['vite', 'build'],
]) {
run(command[0], command.slice(1));
}

const indexPath = join(buildRoot, 'build', 'index.html');
const searchIndexPath = join(buildRoot, 'static', 'search-index.json');
if (!existsSync(indexPath)) {
throw new Error(`SvelteKit build did not write ${indexPath}`);
}
if (!existsSync(searchIndexPath)) {
throw new Error(`Build preflight did not write ${searchIndexPath}`);
}

const indexHtml = readFileSync(indexPath, 'utf8');
if (!indexHtml.includes('<!doctype html>')) {
throw new Error('SvelteKit build output index.html is missing doctype');
}

console.log(
`SvelteKit/Vite build smoke passed for ${packageJson.name}; output=${indexPath}; mermaid=${process.env.MERMAID_PRERENDER}`,
);

function copyInputsToBuildRoot() {
for (const dir of ['packages', 'scripts', 'src', 'static']) {
copyPath(resolve(workspaceRoot, dir), resolve(buildRoot, dir));
}

for (const file of [
'.npmrc',
'package-lock.json',
'package.json',
'pnpm-lock.yaml',
'svelte.config.js',
'tsconfig.json',
'vite.config.ts',
]) {
copyPath(resolve(workspaceRoot, file), resolve(buildRoot, file));
}
}

function copyPath(source, destination) {
if (!existsSync(source)) {
throw new Error(`Missing declared build input: ${source}`);
}

mkdirSync(dirname(destination), { recursive: true });
cpSync(source, destination, {
dereference: true,
errorOnExist: false,
force: true,
preserveTimestamps: false,
recursive: true,
});
}

function linkNodeModules() {
const sourceNodeModules = resolve(workspaceRoot, 'node_modules');
const buildNodeModules = resolve(buildRoot, 'node_modules');
if (!existsSync(sourceNodeModules)) {
throw new Error(`Missing Bazel node_modules tree: ${sourceNodeModules}`);
}

mkdirSync(buildNodeModules, { recursive: true });
for (const entry of readdirSync(sourceNodeModules, { withFileTypes: true })) {
if (entry.name === '@blog') {
continue;
}

const sourcePath = resolve(sourceNodeModules, entry.name);
const destinationPath = resolve(buildNodeModules, entry.name);
if (entry.name.startsWith('@') && entry.isDirectory()) {
mkdirSync(destinationPath, { recursive: true });
for (const scopedEntry of readdirSync(sourcePath, { withFileTypes: true })) {
symlinkSync(resolve(sourcePath, scopedEntry.name), resolve(destinationPath, scopedEntry.name), 'dir');
}
} else {
symlinkSync(sourcePath, destinationPath, entry.isDirectory() ? 'dir' : 'file');
}
}

const blogScope = resolve(buildNodeModules, '@blog');
mkdirSync(blogScope, { recursive: true });
for (const [name, packageDir] of [
['pulse-client', 'pulse-client'],
['pulse-core', 'pulse-core'],
]) {
symlinkSync(resolve(buildRoot, 'packages', packageDir), resolve(blogScope, name), 'dir');
}

linkWorkspacePackageDependencies(buildNodeModules);
}

function linkWorkspacePackageDependencies(buildNodeModules) {
for (const packageDir of ['pulse-client', 'pulse-core']) {
const packageJsonPath = resolve(buildRoot, 'packages', packageDir, 'package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
for (const dependencyName of Object.keys(packageJson.dependencies ?? {})) {
if (dependencyName.startsWith('@blog/')) {
continue;
}
linkRootPackageIfMissing(buildNodeModules, dependencyName);
}
}
}

function linkRootPackageIfMissing(buildNodeModules, packageName) {
const destination = resolvePackagePath(buildNodeModules, packageName);
if (existsSync(destination)) {
return;
}

const source = findAspectPackage(buildNodeModules, packageName);
if (!source) {
throw new Error(`Missing workspace dependency ${packageName} in Bazel npm store`);
}

mkdirSync(dirname(destination), { recursive: true });
symlinkSync(source, destination, 'dir');
}

function resolvePackagePath(nodeModules, packageName) {
return resolve(nodeModules, ...packageName.split('/'));
}

function findAspectPackage(buildNodeModules, packageName) {
const aspectStore = resolve(buildNodeModules, '.aspect_rules_js');
const packagePath = packageName.split('/');
if (!existsSync(aspectStore)) {
return '';
}

for (const entry of readdirSync(aspectStore, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
const candidate = resolve(aspectStore, entry.name, 'node_modules', ...packagePath);
if (existsSync(candidate)) {
return candidate;
}
}

return '';
}

function run(binaryName, args) {
const binary = resolveBinEntrypoint(binaryName);
const result = spawnSync(process.execPath, [binary, ...args], {
cwd: buildRoot,
env: process.env,
stdio: 'inherit',
});

if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(`${binaryName} ${args.join(' ')} failed with exit code ${result.status}`);
}
}

function resolveBinEntrypoint(name) {
const entrypoints = {
'svelte-kit': ['@sveltejs/kit', 'svelte-kit.js'],
tsx: ['tsx', 'dist/cli.mjs'],
vite: ['vite', 'bin/vite.js'],
};
const [packageName, relativePath] = entrypoints[name] ?? [];
if (!packageName) {
throw new Error(`Unknown npm binary ${name}`);
}

const entrypoint = resolve(buildRoot, 'node_modules', packageName, relativePath);
if (!existsSync(entrypoint)) {
throw new Error(`Missing npm binary ${name}: ${entrypoint}`);
}
return entrypoint;
}

function ensureWritableEnvDir(name, fallback) {
const current = process.env[name];
if (current && isWritableDirectory(current)) {
return current;
}

mkdirSync(fallback, { recursive: true });
process.env[name] = fallback;
return fallback;
}

function isWritableDirectory(path) {
try {
if (!existsSync(path) || !statSync(path).isDirectory()) {
return false;
}
accessSync(path, constants.W_OK);
return true;
} catch {
return false;
}
}
Loading