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
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- The remaining absolute asset URLs in the built stylesheets are now relative
too. [#139](https://github.com/QuantEcon/quantecon-theme.mystmd/pull/139)
rewrote the stylesheets in `_assets/`, but Remix also emits route and
shared-chunk CSS into the build root, `_shared/` and `routes/`, and the
rewriter enumerated a single directory so it never saw them. Four such
stylesheets shipped in 2.3.1 still pointing `--jp-icon-plotly` at an absolute
`/myst_assets_folder/_assets/plotly-*.svg`, which resolves under `myst start`
but 404s in `myst build --html` output and under a `baseurl` — the same defect
[#138](https://github.com/QuantEcon/quantecon-theme.mystmd/issues/138)
described, in the files that fix did not cover. The script now walks the build
directory and computes each stylesheet's prefix from its own location, since
the depth differs (`./_assets/` from the build root, `../_assets/` from
`routes/`), and checks every rewritten target from that stylesheet's own
directory rather than assuming one. A production build now emits **zero**
absolute asset URLs and all 78 references resolve
([#150](https://github.com/QuantEcon/quantecon-theme.mystmd/issues/150)).

## [2.3.1] - 2026-08-26

> Headline: corrects two defects in what 2.3.0 shipped. Code cells nested inside
Expand Down
46 changes: 36 additions & 10 deletions scripts/relative-css-asset-urls.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
* `.css` — so the path inside the stylesheet keeps pointing at a directory the
* output does not contain, and every font 404s.
*
* The stylesheets and the files they reference are emitted into the same
* `_assets/` directory in both layouts, so a reference relative to the
* stylesheet resolves in all of them:
* The referenced files always land in `_assets/`, and the whole build directory
* moves as a unit between layouts, so a reference relative to the stylesheet
* resolves in all of them:
*
* myst start /myst_assets_folder/_assets/x.css -> /myst_assets_folder/_assets/font.woff2
* static build /build/_assets/x.css -> /build/_assets/font.woff2
Expand All @@ -23,7 +23,15 @@
* ignores `baseurl` and breaks on project-scoped deployments such as the
* per-PR GitHub Pages previews.
*
* Every rewritten target is checked to exist on disk, so a wrong assumption
* Not every stylesheet sits in `_assets/` (#150): Remix also emits route and
* shared-chunk CSS into the build root, `_shared/` and `routes/`, and those
* reference `_assets/` from a directory above it. So the relative prefix is
* computed per stylesheet from its own location rather than assumed to be
* `./` — the whole tree is walked, and a stylesheet in the build root gets
* `./_assets/` where one in `routes/` gets `../_assets/`.
*
* Every rewritten target is checked to exist at the path the stylesheet now
* names, resolved from that stylesheet's own directory, so a wrong assumption
* here fails the build rather than shipping silent 404s.
*/
import fs from 'node:fs';
Expand All @@ -35,7 +43,8 @@ const require = createRequire(import.meta.url);
// out of step with where the build actually puts things.
const { publicPath = '/', assetsBuildDirectory = 'public/build' } = require('../remix.config.prod.js');

const assetsDir = path.resolve(assetsBuildDirectory, '_assets');
const buildDir = path.resolve(assetsBuildDirectory);
const assetsDir = path.join(buildDir, '_assets');
const prefix = `${publicPath.endsWith('/') ? publicPath : `${publicPath}/`}_assets/`;
// url( optional-quote PREFIX file optional-quote )
const URL_RE = new RegExp(`url\\((\\s*['"]?)${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'g');
Expand All @@ -45,21 +54,38 @@ if (!fs.existsSync(assetsDir)) {
process.exit(1);
}

/** Every `.css` file under the build directory, at any depth. */
function* stylesheets(dir) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) yield* stylesheets(full);
else if (entry.name.endsWith('.css')) yield full;
}
}

/** How this stylesheet must spell `_assets/` to reach it from where it sits. */
function assetsPrefixFrom(file) {
const rel = path.relative(path.dirname(file), assetsDir).split(path.sep).join('/');
if (rel === '') return './';
return rel.startsWith('..') ? `${rel}/` : `./${rel}/`;
}

let rewritten = 0;
const missing = [];
// Validate everything before writing anything: a partial rewrite would leave
// the build output in a half-corrected state that is confusing to debug and
// worse to inherit if a later step ever runs despite the failure.
const pending = [];

for (const name of fs.readdirSync(assetsDir).filter((f) => f.endsWith('.css'))) {
const file = path.join(assetsDir, name);
for (const file of stylesheets(buildDir)) {
const before = fs.readFileSync(file, 'utf8');
const after = before.replace(URL_RE, 'url($1./');
const assetsPrefix = assetsPrefixFrom(file);
const after = before.replace(URL_RE, `url($1${assetsPrefix}`);
if (after === before) continue;

for (const [, target] of after.matchAll(/url\(\s*['"]?\.\/([^)'"]+)['"]?\s*\)/g)) {
const resolved = path.join(assetsDir, target.split(/[?#]/)[0]);
const name = path.relative(buildDir, file).split(path.sep).join('/');
for (const [, target] of after.matchAll(/url\(\s*['"]?(\.[^)'"]+)['"]?\s*\)/g)) {
const resolved = path.resolve(path.dirname(file), target.split(/[?#]/)[0]);
if (!fs.existsSync(resolved)) missing.push(`${name} -> ${target}`);
}

Expand Down
Loading