Files
Shadowbroker/desktop-shell/tauri-skeleton/scripts/build-frontend-export.cjs
T
Shadowbroker ef6b8ec181 fix(desktop-build): strip layout.tsx force-dynamic on CRLF checkouts too (#320)
build-frontend-export.cjs stages a desktop-only frontend export tree and
strips the ``force-dynamic`` + ``revalidate`` directives from
``frontend/src/app/layout.tsx`` so Next's ``output: "export"`` can
prerender every route.

The strip regexes only matched LF (``\n``). Any Windows checkout without
``core.autocrlf=input`` has CRLF line endings, the strip silently
no-op'd, and the desktop build failed at the static-export step:

    Error: Page with `dynamic = "force-dynamic"` couldn't be exported.
    `output: "export"` requires all pages be renderable statically
    because there is no runtime server to dynamically render routes
    in this output format.
    Export encountered an error on /_not-found/page: /_not-found

Reaches every Windows contributor who hasn't normalized line endings
locally. Replacing each ``\n`` in the strip regexes with ``\r?\n``
makes the strip CRLF-tolerant; LF behavior is unchanged.

Verified by running both regexes against the actual layout.tsx (302
bytes removed, force-dynamic + revalidate both gone) and against a
synthetic LF input (296 bytes removed, same outcome).

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 16:07:11 -06:00

120 lines
3.6 KiB
JavaScript

#!/usr/bin/env node
const fs = require('node:fs');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const scriptDir = __dirname;
const tauriDir = path.resolve(scriptDir, '..');
const repoRoot = path.resolve(tauriDir, '..', '..');
const frontendDir = path.join(repoRoot, 'frontend');
const buildRoot = path.join(repoRoot, '.desktop-export-build');
const buildFrontendDir = path.join(buildRoot, 'frontend');
const buildOutDir = path.join(buildFrontendDir, 'out');
const liveOutDir = path.join(frontendDir, 'out');
const excludedPaths = [
'node_modules',
'.next',
'out',
'src/app/api',
'src/middleware.ts',
];
function normalizeRelativePath(target) {
return target.split(path.sep).join('/');
}
function shouldCopy(srcPath) {
const relativePath = path.relative(frontendDir, srcPath);
if (!relativePath) {
return true;
}
const normalized = normalizeRelativePath(relativePath);
return !excludedPaths.some(
(excluded) => normalized === excluded || normalized.startsWith(`${excluded}/`),
);
}
function prepareBuildTree() {
fs.rmSync(buildRoot, { recursive: true, force: true });
fs.cpSync(frontendDir, buildFrontendDir, {
recursive: true,
filter: shouldCopy,
});
const stagedLayoutPath = path.join(buildFrontendDir, 'src', 'app', 'layout.tsx');
if (fs.existsSync(stagedLayoutPath)) {
const layoutSource = fs.readFileSync(stagedLayoutPath, 'utf8');
// CRLF compatibility: on Windows checkouts without ``core.autocrlf=input``
// (the default) layout.tsx has CRLF line endings, but the original regexes
// only matched LF. The strip silently no-op'd, ``force-dynamic`` stayed,
// and Next's static-export refused to render ``/_not-found`` ("Page with
// `dynamic = \"force-dynamic\"` couldn't be exported"). Use ``\r?\n`` so
// the strip works regardless of line-ending normalization.
fs.writeFileSync(
stagedLayoutPath,
layoutSource
.replace(/\r?\n\/\/ The dashboard is a live local runtime[\s\S]*?client polling ever hydrates\.\r?\n/g, '\n')
.replace(/\r?\nexport const dynamic = ['"]force-dynamic['"];\r?\n/g, '\n')
.replace(/\r?\nexport const revalidate = 0;\r?\n/g, '\n'),
);
}
const liveNodeModules = path.join(frontendDir, 'node_modules');
const stagedNodeModules = path.join(buildFrontendDir, 'node_modules');
if (!fs.existsSync(liveNodeModules)) {
throw new Error(`Missing frontend/node_modules at ${liveNodeModules}`);
}
fs.symlinkSync(liveNodeModules, stagedNodeModules, 'junction');
}
function runExportBuild() {
const env = {
...process.env,
NEXT_OUTPUT: 'export',
};
const result =
process.platform === 'win32'
? spawnSync(
process.env.ComSpec || 'cmd.exe',
['/d', '/s', '/c', 'npm.cmd run build -- --webpack'],
{
cwd: buildFrontendDir,
env,
stdio: 'inherit',
},
)
: spawnSync('npm', ['run', 'build', '--', '--webpack'], {
cwd: buildFrontendDir,
env,
stdio: 'inherit',
});
if (result.error) {
throw result.error;
}
if (typeof result.status === 'number' && result.status !== 0) {
throw new Error(`Frontend export build failed with exit code ${result.status}.`);
}
}
function syncBuildOutput() {
if (!fs.existsSync(buildOutDir)) {
throw new Error(`Desktop export did not produce ${buildOutDir}`);
}
fs.rmSync(liveOutDir, { recursive: true, force: true });
fs.cpSync(buildOutDir, liveOutDir, {
recursive: true,
});
}
try {
prepareBuildTree();
runExportBuild();
syncBuildOutput();
} finally {
fs.rmSync(buildRoot, { recursive: true, force: true });
}