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
101 changes: 101 additions & 0 deletions assets/landing.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Landing page styles, layered on top of TypeDoc's generated style.css
* (copied to assets/style.css at build time). TypeDoc wraps its rules in
* @layer typedoc, so these unlayered rules win without specificity hacks.
* Only TypeDoc's unprefixed --color-* variables are used, so the landing
* page tracks the docs' light/dark/os theming automatically.
*/

.landing-cards {
list-style: none;
margin: 1rem 0 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
}

.landing-card {
display: flex;
flex-direction: column;
padding: 1rem;
background-color: var(--color-background-secondary);
border: 1px solid var(--color-accent);
border-radius: 0.5rem;
}

.landing-card h3 {
margin: 0 0 0.75rem;
font-size: 1rem;
}

.landing-card-version {
display: inline-block;
margin-left: 0.25rem;
padding: 0 0.4rem;
border: 1px solid var(--color-accent);
border-radius: 0.25rem;
color: var(--color-text-aside);
font-size: 0.75rem;
font-weight: 400;
line-height: 1.25rem;
vertical-align: middle;
white-space: nowrap;
}

.landing-card p {
flex: 1;
margin: 0 0 1rem;
color: var(--color-text-aside);
font-size: 0.875rem;
}

.landing-card-links {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}

.landing-link {
display: inline-block;
padding: 0.375rem 0.75rem;
border: 1px solid transparent;
border-radius: 0.25rem;
background-color: var(--color-link);
color: var(--color-background);
font-size: 0.875rem;
}

.landing-link:hover {
filter: brightness(1.1);
}

.landing-link.legacy {
background-color: transparent;
border-color: var(--color-accent);
color: var(--color-text-aside);
}

/* Single ↗ indicator in the button's text color; suppress TypeDoc's
black/white a.external[target="_blank"] background icon */
.landing-link.external {
background-image: none;
padding-right: 0.75rem;
}

.landing-link.external::after {
content: " ↗";
}

.landing-nav-category {
margin: 1rem 0 0.25rem;
color: var(--color-text-aside);
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
}

.site-menu .landing-nav-category:first-child {
margin-top: 0;
}
212 changes: 185 additions & 27 deletions build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ try {
*/
function parseBranchArgs() {
const args = process.argv.slice(2);

// Process arguments in the format: repo=branch (e.g., engine=dev)
for (const arg of args) {
if (arg === '--landing-only') {
continue;
}
const match = arg.match(/^([^=]+)=(.+)$/);
if (match) {
const [, repoName, branchName] = match;
Expand Down Expand Up @@ -96,6 +99,149 @@ function copyDirContents(src, dest) {
}
}

/**
* Read the version of each cloned repository from its package.json
*/
function getRepoVersions() {
const versions = {};

for (const repo of REPOS) {
const packagePath = path.join('repos', repo.name, 'package.json');
try {
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
versions[repo.name] = packageJson.version || '';
} catch (error) {
console.warn(`Warning: Could not read version for ${repo.name}: ${error.message}`);
versions[repo.name] = '';
}
}

return versions;
}

/**
* Generate the landing page from the index.html template, injecting the
* build date and per-repository version numbers
*/
function generateLandingPage(versions) {
const sourceIndexPath = path.join(__dirname, 'index.html');
if (!fs.existsSync(sourceIndexPath)) {
throw new Error(`Source index.html not found: ${sourceIndexPath}`);
}

let html = fs.readFileSync(sourceIndexPath, 'utf8');

const buildDate = new Date().toLocaleDateString('en-US', {
year: 'numeric', month: 'long', day: 'numeric'
});
html = html.replace(/\{\{BUILD_DATE\}\}/g, buildDate);

html = html.replace(/v\{\{VERSION:([\w-]+)\}\}/g, (match, name) => {
return versions[name] ? `v${versions[name]}` : '';
});

// Remove version badges left empty by missing versions
html = html.replace(/\s*<span class="landing-card-version">\s*<\/span>/g, '');

fs.writeFileSync(path.join('docs', 'index.html'), html);
console.log('Generated landing page with build date and versions');
}

/**
* Copy TypeDoc's generated stylesheet and icons into docs/assets so the
* landing page shares the exact theme of the product references
*/
function copySharedAssets() {
const engineAssets = path.join('docs', 'engine', 'assets');
const stylePath = path.join(engineAssets, 'style.css');
if (!fs.existsSync(stylePath)) {
throw new Error(`TypeDoc stylesheet not found: ${stylePath}. Run a full build first.`);
}

ensureDir(path.join('docs', 'assets'));
fs.copyFileSync(stylePath, path.join('docs', 'assets', 'style.css'));
fs.copyFileSync(path.join(engineAssets, 'icons.svg'), path.join('docs', 'assets', 'icons.svg'));
console.log('Copied shared TypeDoc assets (style.css, icons.svg)');
}

/**
* Post-process the generated TypeDoc pages so the way back to the landing
* page is obvious: turn the toolbar title into a breadcrumb (PlayCanvas /
* <product>) and rename the sidebar "Home" link. All replacements are
* idempotent so this can re-run over already-processed docs.
*/
function postProcessProductDocs() {
const breadcrumb = '<a href="/" class="title-home">Home</a><span class="title-sep" aria-hidden="true">/</span>';
const breadcrumbCss = `
/* api-reference: toolbar breadcrumb back to the landing page */
.tsd-toolbar-contents > .title-home {
font-weight: bold;
white-space: nowrap;
}
.tsd-toolbar-contents > .title-sep {
margin: 0 0.5rem;
color: var(--color-text-aside);
}
@media (max-width: 769px) {
.tsd-toolbar-contents > .title-home,
.tsd-toolbar-contents > .title-sep {
display: none;
}
}
`;

const processHtml = (file) => {
const html = fs.readFileSync(file, 'utf8');
let updated = html;

// Each replacement is independently idempotent
if (!updated.includes('class="title-home"')) {
updated = updated.replace(/<a href="[^"]*" class="title">/, match => breadcrumb + match);
}
updated = updated.replace(
/(<nav id="tsd-sidebar-links" class="tsd-navigation"><a href="[^"]*">)Home(<\/a>)/,
'$1← All API References$2'
);

if (updated === html) {
return false;
}
fs.writeFileSync(file, updated);
return true;
};

const walk = (dir) => {
let count = 0;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const entryPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
count += walk(entryPath);
} else if (entry.name.endsWith('.html')) {
count += processHtml(entryPath) ? 1 : 0;
}
}
return count;
};

for (const repo of REPOS) {
const targetFolderName = repo.name === 'editor-api' ? 'editor' : repo.name;
const targetDir = path.join('docs', targetFolderName);
if (!fs.existsSync(targetDir)) {
console.warn(`Warning: No docs found for ${repo.name} at ${targetDir}`);
continue;
}

const count = walk(targetDir);

const stylePath = path.join(targetDir, 'assets', 'style.css');
if (fs.existsSync(stylePath) && !fs.readFileSync(stylePath, 'utf8').includes('title-home')) {
fs.appendFileSync(stylePath, breadcrumbCss);
}

console.log(`Post-processed ${count} pages in ${targetFolderName}`);
}
}

/**
* Combine sitemap.xml files from all repositories
*/
Expand Down Expand Up @@ -362,22 +508,28 @@ function generateRedirects() {
*/
async function buildDocs() {
try {
// Skip cloning/building repos and only regenerate the landing page from
// an existing docs directory (fast local iteration)
const landingOnly = process.argv.includes('--landing-only');

// Parse command line arguments for branch overrides
parseBranchArgs();

// Create docs directory if it doesn't exist
ensureDir('docs');

// Create .nojekyll file to prevent GitHub Pages from using Jekyll
console.log('Creating .nojekyll file...');
fs.writeFileSync(path.join('docs', '.nojekyll'), '');

// Remove existing repos directory and create a new one
deleteDir('repos');
ensureDir('repos');


if (!landingOnly) {
// Remove existing repos directory and create a new one
deleteDir('repos');
ensureDir('repos');
}

// Process each repository
for (const repo of REPOS) {
for (const repo of landingOnly ? [] : REPOS) {
console.log(`\n========== Processing ${repo.name} (branch: ${repo.branch}) ==========`);

// Change to repos directory
Expand Down Expand Up @@ -412,33 +564,39 @@ async function buildDocs() {
console.log(`Completed processing ${repo.name}`);
}

// Copy the index.html to the docs directory
console.log('\nCopying index.html file...');
const sourceIndexPath = path.join(__dirname, 'index.html');
if (!fs.existsSync(sourceIndexPath)) {
throw new Error(`Source index.html not found: ${sourceIndexPath}`);
}
fs.copyFileSync(sourceIndexPath, path.join('docs', 'index.html'));

// Make the route back to the landing page obvious on every product page
console.log('\nPost-processing product docs...');
postProcessProductDocs();

// Copy TypeDoc's stylesheet and icons for the landing page to share
console.log('\nCopying shared TypeDoc assets...');
copySharedAssets();

// Generate the landing page with build date and repo versions
console.log('Generating landing page...');
generateLandingPage(getRepoVersions());

// Copy favicon
console.log('Copying favicon...');
fs.copyFileSync('favicon.ico', path.join('docs', 'favicon.ico'));

// Copy assets directory if it exists
if (fs.existsSync('assets')) {
console.log('Copying assets directory...');
ensureDir(path.join('docs', 'assets'));
copyDirContents('assets', path.join('docs', 'assets'));
}

// Generate combined sitemap
console.log('\nGenerating combined sitemap...');
combineSitemaps();

// Generate redirects for old URLs
console.log('\nGenerating redirects for old URL structure...');
generateRedirects();


if (!landingOnly) {
// Generate combined sitemap
console.log('\nGenerating combined sitemap...');
combineSitemaps();

// Generate redirects for old URLs
console.log('\nGenerating redirects for old URL structure...');
generateRedirects();
}

console.log('\nDocumentation build complete. Run "npm run serve" to view it.');
} catch (error) {
console.error(`\nError: ${error.message}`);
Expand Down
Loading