Problem: the hexagonal refactor churns the backend tree for nine more phases; the UI delivery stack (web/ SPA, cmd/desktop Wails wrapper, compose/web image) must move to its own repo first so doc/layout rewrites land once on a backend-only tree. Change: - New repo git.hubris.network/dtoro/oikos-web (v0.33.0): web/, desktop/ (updateURL repointed to oikos-web releases), compose/, own CI (web + desktop jobs), own deploy script (CI-green gate, TOCTOU guard, version-tagged images, prune-to-3), own webhook receiver on :9798 + launchd unit, own compose project publishing the same 8091:80. - Cutover executed on mac-mini in order: oikos stack's web service stopped+removed, oikos-web project brought up on 8091; outer Caddy untouched (targets the published port) — serving + Authentik flow + /wails 404 quirk verified post-cutover. - Stripped from oikos: web/, cmd/desktop/, compose/web/, desktop CI workflow, ci.yml web job, Makefile ui/desktop/desktop-package/install targets, the compose web service, oikos-web from deploy.sh's fallback prune list; wails + go-keyring dropped from go.mod, vendor synced. - README / CONTRIBUTING / AGENTS.md / .agents dev+operations docs now point at the new repo; mbse + mascot design docs carry a path note. Risk: production SPA serving depends on the new pipeline now; rollback is versioned-image re-up of the old web service from a pre-split checkout (port 8091). Desktop builds installed before the split still check dtoro/oikos releases — one manual reinstall, noted in the oikos-web release notes. Verification: go vet, make test (race), make generate-check, golangci (no new findings; baseline down 400→365); post-cutover curls — localhost:8091 200, /wails/runtime.js 404, outer Caddy 302 Authentik.
98 lines
3.9 KiB
JavaScript
98 lines
3.9 KiB
JavaScript
const BREAK = Symbol('break visit');
|
||
const SKIP = Symbol('skip children');
|
||
const REMOVE = Symbol('remove item');
|
||
/**
|
||
* Apply a visitor to a CST document or item.
|
||
*
|
||
* Walks through the tree (depth-first) starting from the root, calling a
|
||
* `visitor` function with two arguments when entering each item:
|
||
* - `item`: The current item, which included the following members:
|
||
* - `start: SourceToken[]` – Source tokens before the key or value,
|
||
* possibly including its anchor or tag.
|
||
* - `key?: Token | null` – Set for pair values. May then be `null`, if
|
||
* the key before the `:` separator is empty.
|
||
* - `sep?: SourceToken[]` – Source tokens between the key and the value,
|
||
* which should include the `:` map value indicator if `value` is set.
|
||
* - `value?: Token` – The value of a sequence item, or of a map pair.
|
||
* - `path`: The steps from the root to the current node, as an array of
|
||
* `['key' | 'value', number]` tuples.
|
||
*
|
||
* The return value of the visitor may be used to control the traversal:
|
||
* - `undefined` (default): Do nothing and continue
|
||
* - `visit.SKIP`: Do not visit the children of this token, continue with
|
||
* next sibling
|
||
* - `visit.BREAK`: Terminate traversal completely
|
||
* - `visit.REMOVE`: Remove the current item, then continue with the next one
|
||
* - `number`: Set the index of the next step. This is useful especially if
|
||
* the index of the current token has changed.
|
||
* - `function`: Define the next visitor for this item. After the original
|
||
* visitor is called on item entry, next visitors are called after handling
|
||
* a non-empty `key` and when exiting the item.
|
||
*/
|
||
function visit(cst, visitor) {
|
||
if ('type' in cst && cst.type === 'document')
|
||
cst = { start: cst.start, value: cst.value };
|
||
_visit(Object.freeze([]), cst, visitor);
|
||
}
|
||
// Without the `as symbol` casts, TS declares these in the `visit`
|
||
// namespace using `var`, but then complains about that because
|
||
// `unique symbol` must be `const`.
|
||
/** Terminate visit traversal completely */
|
||
visit.BREAK = BREAK;
|
||
/** Do not visit the children of the current item */
|
||
visit.SKIP = SKIP;
|
||
/** Remove the current item */
|
||
visit.REMOVE = REMOVE;
|
||
/** Find the item at `path` from `cst` as the root */
|
||
visit.itemAtPath = (cst, path) => {
|
||
let item = cst;
|
||
for (const [field, index] of path) {
|
||
const tok = item?.[field];
|
||
if (tok && 'items' in tok) {
|
||
item = tok.items[index];
|
||
}
|
||
else
|
||
return undefined;
|
||
}
|
||
return item;
|
||
};
|
||
/**
|
||
* Get the immediate parent collection of the item at `path` from `cst` as the root.
|
||
*
|
||
* Throws an error if the collection is not found, which should never happen if the item itself exists.
|
||
*/
|
||
visit.parentCollection = (cst, path) => {
|
||
const parent = visit.itemAtPath(cst, path.slice(0, -1));
|
||
const field = path[path.length - 1][0];
|
||
const coll = parent?.[field];
|
||
if (coll && 'items' in coll)
|
||
return coll;
|
||
throw new Error('Parent collection not found');
|
||
};
|
||
function _visit(path, item, visitor) {
|
||
let ctrl = visitor(item, path);
|
||
if (typeof ctrl === 'symbol')
|
||
return ctrl;
|
||
for (const field of ['key', 'value']) {
|
||
const token = item[field];
|
||
if (token && 'items' in token) {
|
||
for (let i = 0; i < token.items.length; ++i) {
|
||
const ci = _visit(Object.freeze(path.concat([[field, i]])), token.items[i], visitor);
|
||
if (typeof ci === 'number')
|
||
i = ci - 1;
|
||
else if (ci === BREAK)
|
||
return BREAK;
|
||
else if (ci === REMOVE) {
|
||
token.items.splice(i, 1);
|
||
i -= 1;
|
||
}
|
||
}
|
||
if (typeof ctrl === 'function' && field === 'key')
|
||
ctrl = ctrl(item, path);
|
||
}
|
||
}
|
||
return typeof ctrl === 'function' ? ctrl(item, path) : ctrl;
|
||
}
|
||
|
||
export { visit };
|