Skip to main content

Shared Code Projection

Status: prototype. The projection script lives in io-oidis-labs/bin/project/projection/ and is verified locally (Labs → projection repository → consumer build). The projection repositories are not published to GitLab yet.

A projection publishes a subset of one solution's code (the source) as a standalone Oidis package in its own repository (the projection repository). Other solutions (the consumers) depend on the projection repository like on any other package. The source stays the single source of truth; the projection repository is generated from it.

Typical case: a feature built in io-oidis-labs (split into io-oidis-labs + io-oidis-labs-gui) is needed by a customer solution. The customer must be able to see and contribute to that feature, but must not see the rest of Labs, and the code must not be copied.

When to Use It​

SituationUse
Code is generic framework functionalityMove it to CORE (io-oidis-hub, io-oidis-hub-gui, …) instead
A subset of a solution is needed by another solutionProjection
A subset must be visible to a third party without exposing the source repositoriesProjection
Code is needed once and will diverge immediatelyCopy it — a projection only pays off for code that stays shared

Why not the existing Builder mechanisms alone:

  • A Builder dependency is always a whole repository (git+url#branch or dir+path); a sub-folder of a repository cannot be a dependency.
  • A source split into backend and GUI repositories would require exposing both of them.
  • git subtree / submodules map one path of one repository and cannot rewrite cross-package imports.

Concepts​

 io-oidis-labs (BE)                         io-oidis-labs-gui (FE)
source/typescript/Io/Oidis/Labs/ source/typescript/Io/Oidis/Labs/Gui/
DocFlow/** ──────────┐ DocFlow/** ──────────┐
(rest of Labs) │ (rest of Labs GUI) │
▼ ▼
io-oidis-docflow (projection repository, one package, BE + FE)
source/typescript/Io/Oidis/Labs/DocFlow/**
source/typescript/Io/Oidis/Labs/Gui/DocFlow/**
package.conf.jsonp (generated)
│
▼ "io-oidis-docflow": "git+https://…/io-oidis-docflow.git#develop"
cz-ema-application (consumer)
  • Shared subtree — a dedicated folder in each source repository that holds the shared code.
  • Projection repository — one repository per projection, containing the shared subtrees of all source repositories merged into one package. One package may hold backend and GUI code together (as most solutions do), so a source split into two repositories still produces a single projection repository.
  • Manifest — describes the sources, paths, and the generated package (see below).
  • Projection script — an internal TypeScript tool with the commands init, check, sync, apply.

Layout Rules in the Source​

  1. Shared code lives in a dedicated folder in each source repository, e.g. Io/Oidis/Labs/DocFlow (backend) and Io/Oidis/Labs/Gui/DocFlow (GUI). The manifest maps whole folders, so new files are projected automatically and the boundary is visible from the path.
  2. The namespace stays under the source project. There is no need to mask the origin with a new top-level namespace. Fully-qualified class names are persisted by the consumer (@Extern authorization names, connector symbol mappings), so the folder must be chosen once and never moved afterwards.
  3. The shared subtree imports only:
    • files inside the same shared subtree (relative imports),
    • the paired shared subtree of the other source repository (via its package alias, e.g. @io-oidis-labs-gui/Io/Oidis/Labs/Gui/DocFlow/...),
    • packages allowed by the manifest (typically CORE).
  4. No imports of source-specific code — not the source Loader, IProject, models, AuthManager, mail templates, or demo guards. Source-specific behavior is injected through interfaces declared in the shared subtree and implemented outside of it (e.g. a content provider, an audience provider, workflow labels).
  5. Tests and styles of the shared code follow the same rule — they live in paired folders (test/unit/typescript/.../DocFlow, resource/sass/.../DocFlow) listed in the manifest.

Manifest​

Each projection is a folder next to the script in the primary source repository:

io-oidis-labs/bin/project/projection/
projection.mts # the tool
io-oidis-docflow/
projection.jsonp # manifest
README.md # optional README template (title line + body)
LICENSE.txt # optional, defaults to the primary source LICENSE.txt
Io.Oidis.Builder.DAO.Resources.Data({
name: "io-oidis-docflow",
target: {
path: "../io-oidis-docflow", // local working clone of the projection repository
branch: "develop"
},
sources: {
"io-oidis-labs": {
path: ".", // relative to the primary source repository
ref: "develop",
include: [
"source/typescript/Io/Oidis/Labs/DocFlow/**",
"test/unit/typescript/Io/Oidis/Labs/DocFlow/**",
"resource/sass/Io/Oidis/Labs/DocFlow/**"
]
},
"io-oidis-labs-gui": {
path: "../io-oidis-labs-gui",
ref: "develop",
include: [
"source/typescript/Io/Oidis/Labs/Gui/DocFlow/**",
"test/unit/typescript/Io/Oidis/Labs/Gui/DocFlow/**",
"resource/sass/Io/Oidis/Labs/Gui/DocFlow/**"
]
}
},
allowedImports: ["@io-oidis-commons", "@io-oidis-gui", "@io-oidis-services", "@io-oidis-usercontrols",
"@io-oidis-localhost", "@io-oidis-hub", "@io-oidis-hub-gui", "@nodejs"],
package: {
$interface: "IProjectSolution",
name: "io-oidis-docflow",
version: "2026.3.0",
repository: "https://gitlab.com/oidis/solutions/io-oidis-docflow.git",
dependencies: {
"io-oidis-hub": "git+https://gitlab.com/oidis/io-oidis-hub.git#develop"
},
imports: {
commons: "dependencies/io-oidis-commons/bin/project/configs/commons.jsonp"
}
}
});
  • Source keys are the package names — they are also the import aliases (@io-oidis-labs-gui/...) that get rewritten.
  • allowedImports entries match an exact specifier or a path prefix; an entry ending with * is a plain prefix (e.g. node:*).
  • The package block is rendered into the projection's package.conf.jsonp. imports.commons is mandatory: it brings target.toolchainSettings.version: "module", without which the consumer's Builder compiles the dependency in the legacy CommonJS cache step and fails on modern syntax (replaceAll, accessor).
  • The projection also gets a generated .gitignore and a contribution notice inserted into README.md.

Import Rewriting​

Inside a source, the backend reaches the GUI subtree through the GUI package alias. In the projection both subtrees are one package, so the script rewrites these imports to relative paths — and back when applying changes to the source:

// source: io-oidis-labs/source/typescript/Io/Oidis/Labs/DocFlow/Models/Document.ts
import { Document as Base } from "@io-oidis-labs-gui/Io/Oidis/Labs/Gui/DocFlow/Models/Document.js";

// projection: io-oidis-docflow/source/typescript/Io/Oidis/Labs/DocFlow/Models/Document.ts
import { Document as Base } from "../../Gui/DocFlow/Models/Document.js";

The rewrite is deterministic in both directions; any other cross-subtree import fails the boundary check.

Commands​

The script runs on the framework Node (native TypeScript type stripping), so it needs no build step and no extra packages. Run it from the primary source repository:

dependencies/nodejs/node.exe bin/project/projection/projection.mts <command> <projection> [options]
CommandWhere it runsWhat it does
initIntegrator, onceCreates the local projection repository at target.path on target.branch.
checkSource lint / CIVerifies the boundary rules for all shared subtrees; exit code 1 lists every violating import.
syncIntegrator or source CI after push to developCollects the shared subtrees, rewrites imports, renders the package files, and commits the result to the projection repository as one squashed commit per run. Does nothing when the content did not change.
applyIntegratorTakes a merge request branch of the projection repository, splits the diff by source, rewrites imports back, and writes it into the source working trees for review and commit.
OptionMeaning
--ref <ref>Source revision for all sources (default: ref of each source in the manifest, else HEAD).
--working-treeRead the sources from their working trees instead of a committed ref — for local trials before committing. The state file then records ref: "working-tree" and a dirty flag.
--ticket <ID>Ticket of the sync commit, default PROJECTION.
--message <summary>What the sync brings, used as the commit subject after the ticket; default Sync <projection>.
--branch <branch>apply only — the projection branch with the contributed change.

sync records the source commit SHAs in .projection.json in the projection repository. It refuses to run when the projection repository is dirty, not on target.branch, or when its head is not the last sync commit (someone committed to the projection directly).

apply never overwrites diverged code. For each file it compares the source's current content (forward-rewritten) with the projection content the contribution was based on:

  • equal → the contributed version is written to the source,
  • equal to the contributed version → reported as already applied,
  • otherwise → reported as CONFLICT and skipped; merge it manually.

Changes to generated files (package.conf.jsonp, README.md, LICENSE.txt, .gitignore, .projection.json) and to paths outside the shared subtrees are reported and skipped.

The squashed commit message does not carry source ticket IDs or commit messages, so the projection history does not expose the source project: [<TICKET>] Sync io-oidis-docflow where <TICKET> is passed by the integrator (the consumer project's ticket when the sync delivers work for it).

Local Trial Without GitLab​

The whole loop can be verified before the projection repository exists on GitLab:

  1. In the source: projection.mts init <projection>, then projection.mts sync <projection> --working-tree.

  2. In the consumer's private.conf.jsonp (never committed) point the dependency to the local repository — dir+ with a .git folder clones the given branch through git archive, so only committed projection content is installed:

    dependencies: {
    "io-oidis-docflow": "dir+D:/__PROJECTS__/Oidis/Solutions/io-oidis-docflow#develop"
    }
  3. oidis install (once, to add the new dependency) and oidis build dev in the consumer; the Builder generates the @io-oidis-docflow/* alias. From then on, follow the Workflow below.

  4. Once the repository is published, add the git+https://…#develop dependency to the consumer's package.conf.jsonp and drop the override.

Workflow​

The consumer treats the projection as an ordinary dependency: day-to-day work never needs oidis install. Files move between the consumer's dependencies/<projection>/ and a standalone clone of the projection repository with the sync-dependency skill (see Cross-Repository Workflow); the paths are identical, so it is a plain file transfer. oidis install is only for a fresh checkout or CI.

The projection develop branch is written only by sync (protect the branch); every other change reaches it through a feature branch and apply.

Changes made in the consumer (default for consumer projects)​

  1. Edit the code in the consumer's dependencies/<projection>/ and verify it with the consumer's oidis build dev.
  2. Transfer the files to the standalone projection clone with sync-dependency <projection>, commit them there on a feature/<TICKET> branch and push it. A third party without access to the source ends here — the feature branch (merge request) is the hand-over.
  3. The integrator, who has the source repositories, runs apply --branch feature/<TICKET>: the change lands in the source working trees, split into the backend and GUI repositories with imports rewritten back.
  4. The integrator builds the source, fixes whatever the source needs for compatibility, commits in the source (under the consumer's ticket) and runs sync --ticket <TICKET> --message "<what it brings>". The feature branch is then closed as delivered by the sync.
  5. Fixes made in step 4 return to the consumer with sync-dependency <projection> --reverse — again without install.

Changes made in the source​

  1. Develop in the source as usual; check runs with lint. Commit in the source.
  2. Run sync --ticket <TICKET> --message "<what it brings>".
  3. Bring the change to the consumer's dependencies/<projection>/ with sync-dependency <projection> --reverse and build the consumer.

The source therefore always stays the single source of truth and the projection never diverges, so there are no two-way merge conflicts.

Read-only projections​

For consumers whose code is maintained only by Oidis, skip the merge-request flow and run sync automatically from the source CI.

Consumer Rules​

  • Depend on the projection like on any package: "io-oidis-docflow": "git+https://…/io-oidis-docflow.git#develop". Imports use the projection alias, e.g. @io-oidis-docflow/Io/Oidis/Labs/DocFlow/....
  • Never depend on the projection and its source at the same time — the classes would be defined twice.
  • The source never depends on its own projection.
  • Implement the injected interfaces (content, audience, labels, auth registration) in the consumer's own namespace.

See also: Builder Configuration | Solutions | Version Control