Skip to main content

Manipulating IM-Juggling Configurations

@intra-mart/juggling-core packages the domain logic of IM-Juggling (intra-mart's module composition and WAR generation tool) as a TypeScript library. Every use case — from retrieving the catalog, creating a project, selecting modules, and deploying settings, through generating WAR and static files, applying patches, and performing version upgrades — can be executed from the API without depending on the GUI.

Table of Contents​


0. System Requirements and Setup​

ItemContent
Package name@intra-mart/juggling-core
Module formatESM only (CommonJS is not provided)
Node.js22.19.0 or later is required. This package depends on undici@8, and on Node 20 it fails at import time with TypeError: webidl.util.markAsUncloneable is not a function. The engines field of package.json is also >=22.19.0
BunWorks with 1.3 or later as well
NetworkBy default it makes HTTP access to the remote repository (http://repository.intra-mart.jp/…)

Installation​

mkdir my-juggling && cd my-juggling
npm init -y
npm pkg set type=module # ESM is assumed
npm i @intra-mart/juggling-core
npm i -D tsx typescript @types/node # For execution (tsx) and type checking (tsc)

Set the TypeScript tsconfig.json to NodeNext resolution.

{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"exactOptionalPropertyTypes": true,
"skipLibCheck": true
}
}

Published Subpaths​

The root (@intra-mart/juggling-core) provides the aggregate facade. In addition, there are subpaths for specific purposes.

Import pathMain content
@intra-mart/juggling-corecreateJuggling() (the aggregate facade), ModuleKey, parseVersion, and re-exports of the main types
@intra-mart/juggling-core/modelModel types such as ModuleKey / parseVersion / CategoryData / CategoryModule
@intra-mart/juggling-core/rulesSelection-candidate filters (filterApplicationCandidates and others) and validation (checkBaseSelection and others)
@intra-mart/juggling-core/projectModuleStructure operations, computeCheckState, and low-level functions for deploying settings
@intra-mart/juggling-core/buildBuild templates, createImuiScriptPort, and others
@intra-mart/juggling-core/runtime/nodeNode runtime ports (createNodeFileSystemPort and others)

0.1 Overview of the Aggregate Facade createJuggling()​

createJuggling(options?) asynchronously returns a facade for which repository initialization (setup) has already been completed. Almost every operation can be reached starting from this return value, juggling.

import { createJuggling } from "@intra-mart/juggling-core";

const juggling = await createJuggling({
managedRepositoryPath: "./.managed", // Local managed repository (module cache)
locale: "ja", // If omitted, the host environment's locale
// repositories: [...] // If omitted, the intra-mart defaults are used (see §10)
});

console.log(Object.keys(juggling).sort());

Execution result (measured)

[ 'build', 'catalog', 'context', 'moduleService',
'projects', 'repositories', 'repositoryService', 'rules' ]

Main options (CreateJugglingOptions; all optional):

OptionDefaultDescription
managedRepositoryPath~/.juggling/repositoryLocal cache where retrieved modules accumulate. Specifying it explicitly is recommended
localeHost environmentJava locale notation ("ja" / "en" / "zh_CN"). Used to resolve catalog names
repositoriesThe 2 default remotesRepository settings (§10). An explicit specification takes highest precedence
runtimeruntime/node is loaded automaticallyThe bundle of I/O ports. Inject explicitly when using a bundler
enableVisafalseSignature verification of retrieved items
onSuppressedError—Callback for observing "I/O failures that are swallowed"
signal—Cancellation of the initial setup

The 8 properties of the return value juggling:

PropertyRole
juggling.catalogCatalog retrieval (base / application / recommended / patch lookup)
juggling.projectsProject create / createFromRecommended / open
juggling.buildtemplates / plan / exportWar / exportStatic
juggling.repositoriesRetrieving, saving, and initializing repository settings (§10)
juggling.repositoryServiceModule/metadata retrieval (with retries applied)
juggling.moduleServiceConversion of individual .imm files and the like
juggling.rulesThe rule registry (the list of edition kinds and so on)
juggling.contextThe execution context, such as locale and runtime

Release the repository handles when your work is done.

await juggling.repositories.close();

0.2 Error Design (Exceptions or Return Values)​

juggling-core divides how it expresses failures according to their kind. This is an important premise shared across the entire manual.

  • I/O and consistency errors → a typed exception (BuildError / JugglingError / ArgumentError, etc.) is thrown.
  • "Cases that do not hold under business rules" → expressed as a return value, not an exception.
    • Validation results: AggregateStatus (severity: "OK" | "WARNING" | "NG").
    • Adding user modules: AddUserModulesResult (kind: "completed" | "rejected" | "rolled-back" | "aborted").
    • Batch deployment of settings: DeployBatchResult (the aborted field).
    • Applying patches/upgrades: ApplyPatchesResult / ApplyUpgradeResult (applied: true | false).

The caller handles both with try/catch and by "discriminating on the return value" (Appendix B).


1. Creating a Project​

Purpose: Choose a base and applications from the catalog and create a new Juggling project (a directory that has <dir>/juggling.im).

Procedure: Retrieve the list of bases, the list of applications, and the list of recommended configurations from juggling.catalog. A category (CategoryData) returns "only the available modules" via availableModules(). Each module (CategoryModule) has .id / .version / .status / .requiredProducts (the module itself has no name property). The name is on the category side, in CategoryData.name (LocalizedText | null); use .get() for the resolved value and .raw for the original text.

const bases = await juggling.catalog.getBaseCategories(); // CategoryData[]
const apps = await juggling.catalog.getApplicationCategories(); // CategoryData[]
const recommended = await juggling.catalog.getRecommendedData();

console.log("base categories:", bases.length);
console.log("app categories:", apps.length);
console.log("recommended:", recommended.length);

for (const cat of bases.slice(0, 3)) {
console.log(cat.id, "|", cat.name?.get() ?? "", "| modules:", cat.availableModules().length);
}
const firstBase = bases.flatMap((c) => c.availableModules())[0]!;
console.log("first base module:", String(firstBase.id), firstBase.version.toString(), firstBase.status);

Execution result (measured)

base categories: 7
app categories: 48
recommended: 6
jp.co.intra_mart.module.pack.im_basic.category | intra-mart Accel Platform Basic Edition | modules: 7
jp.co.intra_mart.module.pack.im_advance.category | intra-mart Accel Platform Advance Edition | modules: 6
jp.co.intra_mart.module.pack.im_professional.category | intra-mart Accel Platform Professional Edition | modules: 7
first base module: jp.co.intra_mart.module.pack.im_basic 8.0.33 available

Note: Grouping the same base product by id and sorting in descending order with version.compareTo produces a list well suited for UI display.

1.2 Narrowing Down Target Applications from the Base​

The application list (48 categories / 616 modules) also includes applications that do not fit the selected base. Once you have decided on a base, narrow it down to "the applications that can be placed on that base" with filterApplicationCandidates from @intra-mart/juggling-core/rules. This narrowing is a prime example of an intermediate step that is hard to notice just by scanning the API list.

Narrowing logic: A module becomes a candidate if any one of the products in that application module's requiredProducts (a set of OR groups) matches the base's id+version. Applications with an empty requiredProducts are candidates unconditionally.

import { filterApplicationCandidates } from "@intra-mart/juggling-core/rules";

const bases = await juggling.catalog.getBaseCategories();
const base = bases
.flatMap((c) => c.availableModules())
.find((m) => String(m.id) === "jp.co.intra_mart.module.pack.im_basic")!;

const appCats = await juggling.catalog.getApplicationCategories();
console.log("before filter:", appCats.flatMap((c) => c.availableModules()).length, "modules");

const candidateCats = filterApplicationCandidates({
base: { id: String(base.id), version: base.version.toString() }, // BaseRef = { id, version (string) }
categories: appCats,
filters: [], // Additional filters are normally unnecessary
});
const candidateModules = candidateCats.flatMap((c) => c.availableModules());
console.log("after filter:", candidateCats.length, "categories /", candidateModules.length, "modules");

Execution result (measured; base = im_basic 8.0.33)

before filter: 616 modules
after filter: 21 categories / 48 modules

In other words, it is narrowed from 616 down to 48. The candidates include, for example, im_theme_customize_pack (standard theme customization), iac_suite_pack (Accel Collaboration), and im_spreadsheet_pack.

Narrowing bases by edition kind (optional): Use filterBaseCategories.

import { filterBaseCategories, EditionKindSelection } from "@intra-mart/juggling-core/rules";

const kinds = juggling.rules.editionKinds.list(); // e.g., edition-for-package / edition-for-csl
const narrowedBases = filterBaseCategories(bases, new EditionKindSelection(kinds.slice(0, 1)));
console.log("bases after edition filter:", narrowedBases.length); // → 2

1.3 Validating the Selection​

Before creating the project, you can check the validity of the selection with the validation functions from @intra-mart/juggling-core/rules. What is returned is RuleFinding | null (null if there is no problem). Use createRuleMessageResolver(locale) to resolve messages.

import {
checkBaseSelection,
checkApplicationSelection,
checkProductSelection,
createRuleMessageResolver,
} from "@intra-mart/juggling-core/rules";

const app = candidateModules.find((m) => String(m.id) === "jp.co.intra_mart.module.pack.im_theme_customize_pack")!;
const resolver = createRuleMessageResolver(juggling.context.locale);

const findings = [
checkBaseSelection(base, resolver),
checkApplicationSelection(
{ base: { id: String(base.id), version: base.version.toString() }, candidates: candidateCats, selection: [app] },
resolver,
),
checkProductSelection([base, app]),
].filter((f) => f && f.message);

console.log("findings:", findings.length); // 0 if there is no problem

Execution result (measured): findings: 0

Configuration gaps such as "missing modules (MV-01)" are handled not here but by project.validate() after creation (§1.6).

1.4 Creating the Project​

Pass the base, the applications, and the destination directory to juggling.projects.create(req). The return value is a JugglingProject (the starting point for all subsequent operations). You can receive progress via onProgress (the task name is a localized string that follows locale, and total is 2500).

const project = await juggling.projects.create({
dir: "./.work/proj1", // An existing directory is rejected with NP-01
name: "sample-basic", // The name in .project
base, // CategoryModule (1 item)
applications: [app], // CategoryModule[]
// additionalResourcePlatformIds: [...], // If omitted, those with default=true and in scope are deployed
onProgress: (e) => console.log(e.task, e.total),
});

console.log("dir:", project.dir);
console.log("base:", String(project.latest.getBase()?.key().id));
console.log("apps:", project.latest.getApplications().length);

Execution result (measured)

<localized task name> 2500
dir: /…/.work/proj1
base: jp.co.intra_mart.module.pack.im_basic
apps: 1

To create from a recommended configuration, use createFromRecommended. The difference is that the input becomes recommended: RecommendedData. Because getRecommendedData() returns WithRepository<RecommendedData>[] with source information attached, unwrap it with .data.

const recs = await juggling.catalog.getRecommendedData(); // WithRepository<RecommendedData>[]
const project2 = await juggling.projects.createFromRecommended({
dir: "./.work/proj-rec",
name: "from-recommended",
recommended: recs[0]!.data,
});

1.5 Deploying Additional Resources​

You can deploy additional resources for the application server (startup scripts and so on) after the fact. The platform definitions that target the current configuration (the product ids of base+application) are obtained with listAdditionalResourcePlatforms().

const platforms = project.listAdditionalResourcePlatforms(); // AdditionalResourcePlatform[]
console.log("platforms:", platforms.map((p) => `${p.id}(default=${p.default})`).slice(0, 4));

// Example of deploying only the default targets (default=true). Files that already exist at `to` are skipped, not overwritten.
const defaults = platforms.filter((p) => p.default).map((p) => p.id);
await project.deployAdditionalResources(defaults);

Execution result (measured): platforms: [ 'resin-4.0(default=true)', 'payara-5(default=false)', 'weblogic-12c(default=false)', 'seasar2-sastruts(default=false)' ] (18 in total)

AdditionalResourcePlatform is { id, name, default, requireModulePackIds, description, resources }.

1.6 Inspecting the Project (validate)​

project.validate() validates the structure after re-reading from the files ("existence check of conf + reload of user modules") and returns an AggregateStatus (a value, not an exception).

const status = await project.validate(); // AggregateStatus
console.log("severity:", status.severity, "| findings:", status.flatten().length);
for (const f of status.flatten()) {
console.log(f.severity, f.kind, f.ruleId, "-", f.message);
}

Execution result (measured; immediately after creation): severity: OK | findings: 0

AggregateStatus = { severity: "OK"|"WARNING"|"NG"; children; isOk(); isWarning(); isNg(); flatten() }. An individual finding, ValidationStatus = { severity, kind, ruleId, messageId, message, target?, notes? }. kind is "missing-module" | "missing-configuration" | "invalid-user-module" | "duplicate-id" | "duplicate-short-id" | "rule".

1.7 Saving the Project​

Saving involves a history transition. planSave() lets you check in advance "which transition the save will be (and whether a description is required)", and save(opts?) actually persists it. It is written to <dir>/juggling.im.

console.log("isModified:", project.isModified());

const plan = await project.planSave(); // SavePlan { transition, diffs }
const t = plan.transition;
const requiresDescription =
t.kind === "overwrite" || t.kind === "append" ? t.requiresDescription : false;
console.log("transition:", t.kind, "| requiresDescription:", requiresDescription);

const result = await project.save({ description: "Initial configuration" }); // SaveResult
console.log("saved:", result.transition.kind, "| status:", result.status.severity);
console.log("history versions:", project.history.getAllVersions().length);

Execution result (measured)

isModified: false
transition: overwrite-unchanged | requiresDescription: false
saved: overwrite-unchanged | status: OK
history versions: 1

Because create internally completes the first save, immediately afterwards isModified()===false and the transition is overwrite-unchanged. HistoryTransition is a discriminated union, and only overwrite / append carry requiresDescription.

Points to note:

  • Omitting description for a transition where requiresDescription is set raises an ArgumentError (the empty string "" is valid).
  • To delete all past history and truncate to just the latest entry, use await project.compact().

2. Opening a Project​

Purpose: Load a saved project and restore a JugglingProject.

Procedure: juggling.projects.open(dir, opts?). It reads <dir>/juggling.im, cross-checks it against the repository, and re-resolves the structure. Setting downloadModules: false suppresses downloading of not-yet-retrieved modules, for offline use. Immediately after opening, nothing has been validated (getStatus() returns null), so call validate() as needed.

const project = await juggling.projects.open("./.work/proj1", { downloadModules: false });

console.log("base:", String(project.latest.getBase()?.key().id), project.latest.getBase()?.key().version.toString());
console.log("apps:", project.latest.getApplications().map((p) => String(p.key().id)));
console.log("locale:", project.latest.getLocale());
console.log("available modules:", project.latest.listAvailableModules().length, "/ all:", project.latest.listAllModules().length);
console.log("getStatus() before validate:", project.latest.getStatus()); // null
console.log("validate:", (await project.validate()).severity);

Execution result (measured)

base: jp.co.intra_mart.module.pack.im_basic 8.0.33
apps: [ 'jp.co.intra_mart.module.pack.im_theme_customize_pack' ]
locale: ja
available modules: 225 / all: 352
getStatus() before validate: null
validate: OK

Points to note: open returns a new instance each time it is called (it does not unify sessions). OpenProjectOptions is { signal?, downloadModules? }.


3. Selecting Modules​

Purpose: Change the project's configuration (which modules to include). The edit target is project.structure (current). applyCheckState and selectWithDependencies are synchronous, while unselectWithDependencies / unselectUsages / resolveMissingModules are asynchronous (they return a Promise, so await is required). Check the state with validate() after operating.

Exploring the structure — the main methods of ModuleStructure (project.latest / project.structure):

MethodReturn valuePurpose
getBase() / getApplications()Product | null / Product[]Base / applications
listAvailableModules()Module[]Selected modules + enabled user modules
listAllModules()Module[]All modules
findModule(key)Module | nullExact ModuleKey match
findModuleById(id)Module[]id match

Each Module (project layer) has key(): ModuleKey / getId() / getName() / getVersion() / isSelected() / getConfigurations().

Selection operations — methods of JugglingProject:

const project = await juggling.projects.open("./.work/proj1", { downloadModules: false });

// Identify the target module (here, the application's product module)
const target = project.latest.listAvailableModules().find((m) => m.getId().includes("im_theme_customize"))!;
console.log("target:", target.getId(), target.getVersion());

// Uncheck it (propagates to descendants and recomputes ancestor ModulePack/Product. Synchronous)
project.applyCheckState(target, false);
const afterOff = await project.validate();
console.log("after uncheck:", afterOff.severity, afterOff.flatten().length);

// Resolve missing modules (MV-01) by selecting them automatically
const resolved = await project.resolveMissingModules({ selectParents: true });
console.log("resolved:", resolved.selected.length, "unknown:", resolved.unknown.length);

// Restore the check
project.applyCheckState(target, true);
console.log("after recheck:", (await project.validate()).severity);

Execution result (measured)

target: jp.co.intra_mart.module.pack.im_theme_customize_pack 8.0.0
after uncheck: OK 0
resolved: 0 unknown: 0
after recheck: OK

The theme application above is an optional application that no other module depends on, so removing it does not result in NG. Removing a module that has required dependencies makes missing-module findings appear, which resolveMissingModules() can fill in automatically.

Other selection operations:

MethodDescription
selectWithDependencies(m)Select including dependencies (synchronous)
unselectWithDependencies(m)Deselect including dependencies (asynchronous; await required. Internally obtains snapshotFiles())
unselectUsages(m)Deselect every feature that uses that module (asynchronous; await required)
computeCheckState(structure, modulePack)The three display values "checked" | "unchecked" | "grayed" (from @intra-mart/juggling-core/project. Synchronous)

Note: It is safer to call project.validate() after each selection operation to check the state.


4. Adding User Modules​

Purpose: Add a custom .imm (a user-developed module) that does not exist in the repository to the project.

Procedure: Pass an array of absolute paths to .imm files to project.addUserModules(sourcePaths, opts?). The added .imm files are copied into the modules/ directory directly under the project, and reflection into juggling.im happens at save time. The return value is a discriminated union AddUserModulesResult, not an exception.

type AddUserModulesResult =
| { kind: "completed"; added: UserModule[] }
| { kind: "rejected"; finding: RuleFinding } // Validation violation in phase 1 → the whole operation is aborted (unchanged)
| { kind: "rolled-back"; added: UserModule[]; rejected: { path; messages }; notes }
| { kind: "aborted"; added: UserModule[]; error: JugglingError };
const project = await juggling.projects.open("./.work/proj1", { downloadModules: false });

const result = await project.addUserModules(["/abs/path/to/my-module.imm"]);
switch (result.kind) {
case "completed": console.log("added:", result.added.length); break;
case "rejected": console.log("rejected:", result.finding.ruleId, result.finding.message); break;
case "rolled-back": console.log("rolled-back at:", result.rejected.path); break;
case "aborted": console.log("aborted:", result.error.message); break;
}

A validation violation (an .imm that exists but is invalid) results in rejected (measured):

addUserModules(invalid file) kind: rejected
finding: BLOCK UM-02 - The selected file (not-a-module.imm is not a module.

Removal and replacement:

const userModules = project.structure.getUserModules();
await project.removeUserModule(userModules[0]!); // Remove
await project.replaceUserModule(userModules[0]!, "/abs/new.imm"); // Replace (remove → add)

5. Deploying Configuration Files​

Purpose: Expand the configuration files (conf/*.xml) and schemas (schema/*.xsd) contained in modules directly under the project.

Procedure: Because the Configuration objects to deploy cannot be round-tripped, enumerate them from the live structure each time. Obtain them from each module's getConfigurations().

import type { Configuration } from "@intra-mart/juggling-core/project";

const project = await juggling.projects.open("./.work/proj1", { downloadModules: false });

// Enumerate Configuration objects from the current structure
const configs: Configuration[] = [];
for (const module of project.structure.listAvailableModules()) {
for (const c of module.getConfigurations()) {
configs.push(c);
// Properties: c.file = output file name, c.schema = xsd or null, c.require = "REQUIRE"|"OPTIONAL" / Method: c.getName()
}
}
console.log("configurations:", configs.length);
console.log("deployed?:", await project.isDeployedConfiguration(configs[0]!));

// Batch deployment (the return value is DeployBatchResult)
const result = await project.deployConfigurations(configs.slice(0, 3));
console.log("deployed:", result.deployed.length,
"| aborted:", result.aborted ? result.aborted.at.getName() : null,
"| notes:", result.notes.length);

// Deploy only the "missing-configuration" targets from the validate results, all at once
const missing = await project.deployMissingConfigurations();
console.log("missing deployed:", missing.deployed.length);

Execution result (measured)

configurations: 217
deployed?: false
deployed: 3 | aborted: null | notes: 0
missing deployed: 0

DeployBatchResult = { deployed: Configuration[]; aborted: { at, remaining } | null; notes: { id }[] }.

Points to note: When batch deployment hits a setting that "has no schema and whose conf/<file> already exists", it stops at that point, leaving the remainder undone, and aborted holds the stop position and the remaining count (safe behavior that does not overwrite existing files). The individual APIs are deployConfiguration(c) / deploySchema(c) (schema only) / isDeployedConfiguration(c) / isDeployedSchema(c).


6. Generating a WAR File​

Purpose: Generate a WAR from a saved project (the project must already be saved, because juggling.im is embedded in the WAR).

Procedure: ① check the templates → ② pre-validate with plan() (blockers, warnings, licenses) → ③ generate with exportWar(). When the configuration includes modules that have scripts, such as im_ui, you must inject the external script port createImuiScriptPort.

import { createImuiScriptPort } from "@intra-mart/juggling-core/build";
import { createNodeFileSystemPort } from "@intra-mart/juggling-core/runtime/node";

const project = await juggling.projects.open("./.work/proj1", { downloadModules: false });

// ① List the templates
console.log(juggling.build.templates().map((t) => `${t.id}(${t.kind})`));

// The build inputs for the WAR
const inputs = { licenseType: "product", environment: "product", includeSamples: false } as const;

// ② Pre-validation (nothing is generated)
const plan = await juggling.build.plan(project, { template: "resin40", inputs });
console.log("blockers:", plan.blockers.length, "| warnings:", plan.warnings.length,
"| licenses:", plan.licenses.products.length, "/", plan.licenses.others.length);
// If plan.blockers has even one entry, exportWar throws a BuildError

// ③ Generate (injecting the external script port for im_ui)
const externalScripts = createImuiScriptPort({ fs: createNodeFileSystemPort() });
const result = await juggling.build.exportWar(project, {
template: "resin40", // resin40 / payara5 / weblogic12c / was80
inputs,
destDir: "./.work/build",
fileName: "imart", // No extension → imart.war
externalScripts,
onProgress: (e) => console.log("phase:", e.phase),
onLog: (e) => { if (e.level === "conflict") console.log("conflict:", e.message); },
// onMissingModule: "skip", // Default is "throw"
});

console.log("artifactPath:", result.artifactPath);
console.log("entries:", result.entryNames.length, "| modules:", result.extractedModules.length, "| warnings:", result.warnings.length);

Execution result (measured; resin40 / product / no samples)

[ 'weblogic12c(war)', 'payara5(war)', 'resin40(war)', 'was80(war)', 'static(static-zip)' ]
blockers: 0 | warnings: 1 | licenses: 0 / 23
phase: extractModuleBuilder
phase: WebXMLBuilder
phase: buildPropertiesBuilder_message
phase: CopyModifiedConfigratuinsBuilder
phase: ExternalScriptRunner
phase: ant.build
phase: builder_message
artifactPath: /…/.work/build/imart.war
entries: 20532 | modules: 224 | warnings: 1

The generated WAR was about 180 MB (20,532 entries). The single entry in plan.warnings is a storage-config warning (stating that a location under /tmp is specified as the storage root); because it is not a blocker, generation continues.

WarBuildInputs = { licenseType: "product"|"trial"; environment: "ut"|"si"|"pt"|"product"; includeSamples: boolean }. BuildResult = { artifactPath; extractedModules; warnings; entryNames }. The build phases are reported through onProgress's phase as the seven stages above.

Points to note: The license consent gate is handled internally by the facade (as long as you use juggling.build.exportWar, you do not need to be aware of acceptLicenses). Interrupting with an AbortSignal produces BuildError("build.aborted").


7. Generating Static Files​

Purpose: Generate a zip for static delivery that collects only PUBLIC resources.

Procedure: Use the "static" template with juggling.build.exportStatic(). Unlike the WAR, there are no WEB-INF artifacts and no external scripts — only PUBLIC extraction plus zipping (the output is a .zip).

const project = await juggling.projects.open("./.work/proj1", { downloadModules: false });
const inputs = { licenseType: "product", environment: "product", includeSamples: false } as const;

const result = await juggling.build.exportStatic(project, {
template: "static",
inputs, // StaticBuildInputs has the same shape as WarBuildInputs
destDir: "./.work/static",
fileName: "imart-static", // → imart-static.zip
onProgress: (e) => console.log("phase:", e.phase),
});

console.log("artifactPath:", result.artifactPath);
console.log("entries:", result.entryNames.length, "| modules:", result.extractedModules.length);

Execution result (measured)

phase: extractModuleBuilder
phase: ant.build
phase: builder_message
artifactPath: /…/.work/static/imart-static.zip
entries: 7144 | modules: 225

The generated zip was about 45 MB (7,144 entries — PUBLIC resources such as alert/*.html). There are three phases, versus the WAR's seven (the four phases present for the WAR but absent for static are WebXMLBuilder / buildPropertiesBuilder_message / CopyModifiedConfigratuinsBuilder / ExternalScriptRunner).


8. Searching for and Applying Patches​

Purpose: Search for patches applicable to the current configuration, and apply them.

Procedure: project.findPatches() scans the entire configuration and yields a PatchSearchResult. Application is done with project.applyPatches(patches). Application proceeds in the order "trial application to a copy → dependency-scope validation (dry-run) → apply for real if OK", and when the result is NG it returns applied:false without modifying the original.

import { ModuleKey } from "@intra-mart/juggling-core/model";

const project = await juggling.projects.open("./.work/proj1", { downloadModules: false });

// ① Search
const found = await project.findPatches(); // PatchSearchResult { candidates, all(), notes }
console.log("candidates:", found.candidates.length);
for (const c of found.candidates.slice(0, 3)) {
// c.target: ModuleKey (the target) / c.patch: Module (the patch to apply)
console.log(String(c.target.id), c.target.version.toString(), "->", c.patch.key().version.toString());
}

// ② Apply (an empty array raises ArgumentError; at least one entry is required)
if (found.candidates.length > 0) {
const result = await project.applyPatches(found.all()); // ApplyPatchesResult
if (result.applied) console.log("applied");
else console.log("not applied:", result.reason); // "dependency-validation"
}

// A one-off patch lookup (the latest patch for that module; null if there is none)
const single = await juggling.catalog.findPatch(ModuleKey.of("jp.co.intra_mart.module.pack.im_basic", "8.0.33"));
console.log("latest patch:", single ? single.version.toString() : null);

Execution result (measured)

candidates: 30
jp.co.intra_mart.im_workflow 8.0.33 -> 8.0.33-PATCH_004
jp.co.intra_mart.im_portal 8.0.26 -> 8.0.26-PATCH_001
jp.co.intra_mart.imbox_smartphone 8.0.20 -> 8.0.20-PATCH_001
applied
latest patch: null

applyPatches([]) (an empty array) yields ArgumentError: applyPatches requires at least one patch candidate (measured). Note that catalog.findPatch returns null because the base product pack (im_basic) itself has no patches (patches exist on individual modules such as im_workflow). ApplyPatchesResult = { applied: true; notes } | { applied: false; reason: "dependency-validation"; validation }.

Points to note: If an exception occurs mid-search, it abandons further collection and returns only what it has collected so far. User modules are out of scope for patch search. For selective application, pass a subset of found.candidates to applyPatches. To persist the state after application, project.save() is required.


9. Version Upgrades​

Purpose: Update the base product to a newer version (two stages: plan → apply).

Procedure: ① list the upgradable base candidates → ② check the update status of the applications → ③ obtain the diff and validation together with planUpgrade() → ④ if there are no blockers, apply with applyUpgrade().

const project = await juggling.projects.open("./.work/proj1", { downloadModules: false });
console.log("current base:", project.latest.getBase()?.key().version.toString());

// ① Base upgrade candidates (UpgradeBaseCandidate { category, module, isCurrent })
const candidates = await project.listUpgradeBaseCandidates();
for (const c of candidates) console.log(String(c.module.id), c.module.version.toString(), "isCurrent:", c.isCurrent);

// Target the highest version newer than the current one
const target = [...candidates]
.filter((c) => String(c.module.id) === "jp.co.intra_mart.module.pack.im_basic" && !c.isCurrent)
.sort((a, b) => b.module.version.compareTo(a.module.version))[0]!;

// ② The update status of each application (ApplicationUpdateStatusEntry { application, state, candidates })
const appStatuses = await project.getApplicationUpdateStatuses(target.module);
for (const s of appStatuses) console.log(String(s.application.key().id), "state:", s.state, "candidates:", s.candidates.length);

// ③ plan (validation + diff preview)
const plan = await project.planUpgrade({ base: target.module, mode: "automatic" }); // "automatic" | "individual"
console.log("blockers:", plan.blockers.length, "| diff:", plan.diff.length,
"| requiredUpdates:", plan.requiredUpdates.length,
"| updateModuleCandidates:", plan.updateModuleCandidates.length,
"| impossibleApplications:", plan.impossibleApplications.length);
for (const d of plan.diff.slice(0, 3)) console.log(d.kind, d.module.getId(), d.module.getVersion(), "<-", d.oldVersion?.toString() ?? "");

// ④ Apply (when there are no blockers)
if (plan.blockers.length === 0) {
const result = await project.applyUpgrade(plan); // ApplyUpgradeResult
console.log("applied:", result.applied);
console.log("new base:", project.structure.getBase()?.key().version.toString());
console.log("validate:", (await project.validate()).severity);
}

Execution result (measured; im_basic 8.0.33 → 8.0.39)

current base: 8.0.33
jp.co.intra_mart.module.pack.im_basic 8.0.33 isCurrent: true
jp.co.intra_mart.module.pack.im_basic 8.0.34 isCurrent: false
… (8.0.35 … 8.0.39)
app update statuses: 1 (im_theme_customize_pack state: NONE candidates: 23)
blockers: 0 | diff: 128 | requiredUpdates: 0 | updateModuleCandidates: 0 | impossibleApplications: 0
applied: true
new base: 8.0.39
validate: NG

In the run above, validate() after application returned NG (1 finding). Raising the base to 8.0.39 caused the theme application, which remained at the old version (8.0.0), to produce a configuration inconsistency. After a version upgrade, check validate() and, as needed, update the application side (updateModuleCandidates / individual mode) or run resolveMissingModules().

Points to note:

  • Unless the plan was generated from the same project, applyUpgrade raises an ArgumentError.
  • For mode: "individual", specify the individual modules with applyUpgrade(plan, { updateModules }).
  • ApplyUpgradeResult = { applied: true; autoSelected; unresolvedMissing; postUpdateGuidanceUrl; notes } | { applied: false; reason: "dependency-validation" | "repair-not-converged"; validation }.
  • To persist the state after application, use project.save().

10. Changing Settings and the Remote Repository​

Purpose: Check and change the settings of the module repository (the retrieval source).

Procedure: Handle the settings with juggling.repositories (RepositoryManager). If location is http/https it is treated as remote; otherwise it is treated as a local (FILESET) path.

// Check the current settings
const settings = await juggling.repositories.getSettings(); // RepositorySettingsEntry[]
for (const e of settings) {
console.log(e.sort, e.name, "|", e.location, "| erasable:", e.erasable, "| available:", e.available);
}

// Obtain the values that restore the factory defaults
const defaults = await juggling.repositories.systemDefaultSettings();
console.log("managed path:", juggling.repositories.getManagedRepositoryPath());

Execution result (measured)

1 intra-mart Remote Repository | http://repository.intra-mart.jp/base | erasable: true | available: true
3 intra-mart Application Repository | http://repository.intra-mart.jp/app | erasable: true | available: true
managed path: /…/.managed

There are two default repositories: base and app.

RepositorySettingsEntry = { name; location; sort; description; erasable; available }. Ascending sort is the search priority order, erasable: false means it cannot be edited or deleted, and available: false means registration is skipped at setup time.

Two ways to change the repositories:

  1. Specify them at initialization (recommended, highest precedence) — replace them wholesale with createJuggling({ repositories }). When using a local FILESET, pass the directory path as location.

    const juggling = await createJuggling({
    managedRepositoryPath: "./.managed",
    repositories: [
    { name: "base", location: "http://repository.intra-mart.jp/base", sort: 0, description: "", erasable: true, available: true },
    { name: "app", location: "http://repository.intra-mart.jp/app", sort: 1, description: "", erasable: true, available: true },
    // For a local one: { name: "local", location: "/path/to/fileset-repo", sort: 2, description: "", erasable: true, available: true },
    ],
    });
  2. Change them persistently at run time — save the settings with saveSettings(entries). Note, however, that saving only reflects them into the persistent store (KVS); to actually reflect them into the registered repositories, re-running setup() (or a new createJuggling) is required.

    const entries = await juggling.repositories.getSettings();
    entries.push({ name: "extra", location: "http://example/app2", sort: 9, description: "", erasable: true, available: true });
    await juggling.repositories.saveSettings(entries);
    await juggling.repositories.setup(); // Reflect

Maintenance methods: cleanManagedRepository() (erase the entire local cache), clearHttpCache() (delete the HTTP cache), and systemDefaultSettings() (the values that restore the defaults).

Points to note: The validation in saveSettings rejects deletion, disabling, or content changes of entries with erasable: false with a JacklingArgumentError (changing sort — that is, reordering — is permitted). However, the two default repositories (base/app) have erasable: true (that is, they can be edited and deleted). What is protected from deletion is limited to entries with erasable: false, such as the managed repository (getManagedRepository()).


Appendix A. Aggregate Facade Quick Reference​

createJuggling(options?) : Promise<Juggling>
└─ juggling
├─ catalog
│ getBaseCategories() / getApplicationCategories() / getRecommendedData()
│ findPatch(key)
├─ projects
│ create(req) / createFromRecommended(req) / open(dir, opts?) → JugglingProject
├─ build
│ templates() / findTemplate(id)
│ plan(project, {template, inputs}) → BuildPlan
│ exportWar(project, opts) / exportStatic(project, opts) → BuildResult
├─ repositories getSettings() / saveSettings() / systemDefaultSettings() / setup()
│ cleanManagedRepository() / clearHttpCache() / close()
├─ repositoryService / moduleService / rules / context

JugglingProject (the return value of projects.create/open)
dir / structure / latest / history
validate() / isModified() / planSave() / save(opts?) / compact()
applyCheckState() / selectWithDependencies() / unselectWithDependencies()
unselectUsages() / resolveMissingModules(opts?)
addUserModules() / removeUserModule() / replaceUserModule()
deployConfigurations() / deployMissingConfigurations() / deploySchema()
isDeployedConfiguration() / isDeployedSchema()
findPatches() / applyPatches()
listUpgradeBaseCandidates() / getApplicationUpdateStatuses() / planUpgrade() / applyUpgrade()
listAdditionalResourcePlatforms() / deployAdditionalResources()

Appendix B. List of "Business Failures Expressed as Return Values"​

OperationReturn typeHow failure is expressed
validate()AggregateStatusseverity === "NG" (not an exception)
save()SaveResultstatus.severity (the save itself succeeded)
addUserModules()AddUserModulesResultkind: "rejected"/"rolled-back"/"aborted"
deployConfigurations()DeployBatchResultaborted !== null (stopped to protect existing files)
applyPatches()ApplyPatchesResultapplied: false, reason: "dependency-validation"
applyUpgrade()ApplyUpgradeResultapplied: false, reason: "dependency-validation"/"repair-not-converged"

In contrast, ArgumentError (empty arrays, required values not specified), BuildError (no-base / unknown template / aborted), and JugglingError (I/O) are thrown as exceptions.


This manual was produced by running every pattern with @intra-mart/juggling-core@0.1.2 on Node.js 22.19.0 while connected to http://repository.intra-mart.jp/, and by type-checking (tsc) the code shown.