Skip to content

[DEV] Enable Perseus within Local Dev Server#3821

Open
mark-fitzgerald wants to merge 2 commits into
mainfrom
perseus-in-local-dev-server
Open

[DEV] Enable Perseus within Local Dev Server#3821
mark-fitzgerald wants to merge 2 commits into
mainfrom
perseus-in-local-dev-server

Conversation

@mark-fitzgerald

@mark-fitzgerald mark-fitzgerald commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary:

This PR adds a script that dynamically sets up the local development environment to work with local Perseus code. The script adds the Perseus build directory to the Docker container so that when anything in Perseus changes, the package is rebuilt and HMR (hot module reload) picks up the changes and refreshes the browser page. The script does a lot of "configuring" that is required to handle things that sym-linking won't accomplish. For instance, the catalog entries for the various libraries used in Perseus are hard-wired so that Docker can pick up those libraries accordingly. The script needs to update a couple of files in the Frontend repo so that it knows how to properly reference the Perseus items. When the stop command is executed, those changes are reverted.

Test plan:

  1. Executing pnpm start:frontend launches the local dev server, and changes in Perseus code are reflected in the browser window showing the dev server.
  2. Executing pnpm stop:frontend stops the local dev server, and reverts all the changes made to the Frontend repo needed to accommodate the Perseus references.

@mark-fitzgerald mark-fitzgerald self-assigned this Jun 25, 2026
@mark-fitzgerald mark-fitzgerald requested a review from a team June 25, 2026 23:28
@github-actions

Copy link
Copy Markdown
Contributor

Size Change: 0 B

Total Size: 518 kB

ℹ️ View Unchanged
Filename Size
packages/kas/dist/es/index.js 20.6 kB
packages/keypad-context/dist/es/index.js 1 kB
packages/kmath/dist/es/index.js 6.31 kB
packages/math-input/dist/es/index.js 98.5 kB
packages/math-input/dist/es/strings.js 1.61 kB
packages/perseus-core/dist/es/index.item-splitting.js 12.4 kB
packages/perseus-core/dist/es/index.js 27.5 kB
packages/perseus-editor/dist/es/index.js 105 kB
packages/perseus-linter/dist/es/index.js 9.79 kB
packages/perseus-score/dist/es/index.js 10.2 kB
packages/perseus-utils/dist/es/index.js 403 B
packages/perseus/dist/es/index.js 205 kB
packages/perseus/dist/es/strings.js 12.4 kB
packages/pure-markdown/dist/es/index.js 1.39 kB
packages/simple-markdown/dist/es/index.js 6.71 kB

compressed-size-action

@github-actions

Copy link
Copy Markdown
Contributor

npm Snapshot: Published

Good news!! We've packaged up the latest commit from this PR (bea0005) and published it to npm. You
can install it using the tag PR3821.

Example:

pnpm add @khanacademy/perseus@PR3821

If you are working in Khan Academy's frontend, you can run the below command.

./dev/tools/bump_perseus_version.ts -t PR3821

If you are working in Khan Academy's webapp, you can run the below command.

./dev/tools/bump_perseus_version.js -t PR3821

@jeremywiebe jeremywiebe left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've partially reviewed this, but the more I did, the more I'm concerned about the amount of info about our Frontend repo that's being revealed here.

I actually think it might be a script that should live in that repo instead of here (especially as the really nitpicky patching stuff is relevant to that repo and less-so in Perseus).

Happy to talk when we review this together.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm 99% sure you don't need to name this .mts because we're using swc-node. All other "TypeScript scripts" in this folder just use .ts extensions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried that, and it failed. I'm open to investigating this when we pair.

process.exit(0);
}

// ---------------------------------------------------------------------------

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stylistically, I find it much clearer (and easier to read/maintain) if all of the code that comprises the main part of hte script is in a main() function which is called from the bottom of the file.

As this file stands, it's very hard to discover where the behaviour starts and function definitions end.

Basically, there should be no code other than the last main() function call that executes as the file is parsed.

// Main flow
// ---------------------------------------------------------------------------

const perseusGitResult = spawnSync(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we put this in a function?

Comment on lines +622 to +628
const packagesDir = path.join(PERSEUS_ROOT, "packages");
for (const entry of fs.readdirSync(packagesDir)) {
const pkgDir = path.join(packagesDir, entry);
const pkgJsonPath = path.join(pkgDir, "package.json");
if (!fs.existsSync(pkgJsonPath)) {
continue;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use fast-glob here to make this a bit more efficient:

for (const pkgJson of await fg(PERSEUS_ROOT, "packages", "*", "package.json")) {

Comment on lines +639 to +646
// Step 3: Write docker-compose.perseus.env (supplies PERSEUS_ROOT to docker compose)
// Step 4: Patch containers.ts and rspack.config.ts
// Step 5: Apply pnpm.overrides to package.json
console.log("Patching frontend config...");
writePerseusEnvFile();
patchContainerTs();
patchRspackConfig(pkgDirs);
applyOverrides(overrides);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we put the comments with the steps?

Suggested change
// Step 3: Write docker-compose.perseus.env (supplies PERSEUS_ROOT to docker compose)
// Step 4: Patch containers.ts and rspack.config.ts
// Step 5: Apply pnpm.overrides to package.json
console.log("Patching frontend config...");
writePerseusEnvFile();
patchContainerTs();
patchRspackConfig(pkgDirs);
applyOverrides(overrides);
console.log("Patching frontend config...");
// Step 3: Write docker-compose.perseus.env (supplies PERSEUS_ROOT to docker compose)
writePerseusEnvFile();
// Step 4: Patch containers.ts and rspack.config.ts
patchContainerTs();
// Step 5: Apply pnpm.overrides to package.json
patchRspackConfig(pkgDirs);
applyOverrides(overrides);

// ---------------------------------------------------------------------------

const SENTINEL_START =
"// LOCAL PERSEUS DEV — inserted by utils/start-perseus-in-local-server.mts";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd generate this filename/path to protect against refactors (guessing at this exact syntax):

Suggested change
"// LOCAL PERSEUS DEV — inserted by utils/start-perseus-in-local-server.mts";
`/ LOCAL PERSEUS DEV — inserted by ${path.relative(PERSEUS_ROOT, __dirname)}/${__filename}`;

// Catalog resolution
// ---------------------------------------------------------------------------

function parseCatalogs(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'd be better off using a yaml parsing library. We already do very similar parsing in the sync-dependencies.ts script in getCatalogMap().

function getCatalogMap(doc: yaml.Document, catalogName: string): yaml.YAMLMap {

Comment on lines +174 to +182
for (const entry of fs.readdirSync(pkgDir)) {
if (entry === "package.json") {
continue;
}
fs.symlinkSync(
path.join(pkgDir, entry),
path.join(tmpPkgDir, entry),
);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we create a directory for each package in a tmp dir and symlink everything in that package's dir to the tmp package dir except the package.json?

// Build shallow Perseus package copies in tmp dir with catalogs resolved
// ---------------------------------------------------------------------------

function buildPerseusTmpPackages(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: These would make great JSDoc comments... :)

Suggested change
function buildPerseusTmpPackages(
/**
* Build shallow Perseus package copies in tmp dir with catalogs resolved
*/
function buildPerseusTmpPackages(

Comment on lines +204 to +213
const m = String(spec).match(/^catalog:(.+)$/);
if (!m) {
continue;
}
const resolved = catalogs[m[1]]?.[name];
if (!resolved) {
throw new Error(
`Could not resolve catalog:${m[1]} for ${name} in ${pkgDir}`,
);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Named capture groups make this type of thing a ton clearer.

Suggested change
const m = String(spec).match(/^catalog:(.+)$/);
if (!m) {
continue;
}
const resolved = catalogs[m[1]]?.[name];
if (!resolved) {
throw new Error(
`Could not resolve catalog:${m[1]} for ${name} in ${pkgDir}`,
);
}
const m = String(spec).match(/^catalog:(?<catalog>.+)$/);
if (!m) {
continue;
}
const resolved = catalogs[m.groups("catalog")]?.[name];
if (!resolved) {
throw new Error(
`Could not resolve catalog:${m.groups("catalog")} for ${name} in ${pkgDir}`,
);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants