@decohere/forge 0.0.1-dev.100 → 0.0.1-dev.102

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -111,6 +111,27 @@ CMD ["node", "es6/src/main.js"]
111
111
 
112
112
  `forge docker` does not package for you: Dockerfiles that do not use `.package/` (the `create` and Dev Container images here) must keep building as before. If a Dockerfile copies `.package/` and it does not exist, `forge docker` stops before Docker runs and tells you to run `forge package <project>`.
113
113
 
114
+ ### Shipping the UI with the API
115
+
116
+ A module deploys its API and its UI as one image: the API serves the UI's build ([`withStatic` in Cortex](../cortex/README.md#serving-a-built-ui)). To put that build in the package, declare the UI project on the deployable in `.decohere.js`, next to `docker`:
117
+
118
+ ```js
119
+ /** @type {import("@decohere/mesh").WorkspaceOptions} */
120
+ export default {
121
+ projects: {
122
+ "@app/shop.api": {
123
+ ui: {
124
+ project: "@app/shop.web", // the UI project (package name)
125
+ output: "dist", // its build output, relative to that project. Default: "dist"
126
+ to: "ui", // folder inside .package/. Default: "ui"
127
+ },
128
+ },
129
+ },
130
+ };
131
+ ```
132
+
133
+ `forge package @app/shop.api` then copies `projects/shop.web/dist/` to `projects/shop.api/.package/ui/` (real files, never links). The API calls `.withStatic("/ui/", "ui")` and the Dockerfile stays `COPY .package/ ./`. `forge package` does **not** build the UI: if the folder is missing it fails and tells you to build the UI first. `to` must be a new folder inside `.package/` (not `es6`, `node_modules` or `config`). Running the UI's own toolchain from `forge build` is [#381](https://github.com/typesmiths/decohere/issues/381); see [ADR 0007](../../docs/adr/0007-serving-a-built-ui-from-cortex.md).
134
+
114
135
  Design record: [ADR 0006](../../docs/adr/0006-forge-package-deployables.md).
115
136
 
116
137
  ## `forge link` clears `dist`
@@ -21,6 +21,7 @@ const _dependencypinnerservice = require("../packaging/dependency-pinner.service
21
21
  const _deployable = require("../packaging/deployable.js");
22
22
  const _importgraphservice = require("../packaging/import-graph.service.js");
23
23
  const _releaseexports = require("../packaging/release-exports.js");
24
+ const _uiassets = require("../packaging/ui-assets.js");
24
25
  const _workspaceconfig = require("../workspace.config.js");
25
26
  function _interop_require_default(obj) {
26
27
  return obj && obj.__esModule ? obj : {
@@ -60,7 +61,7 @@ class PackageDeployable {
60
61
  const releaseProjects = await this.releaseProjects(projects);
61
62
  const responses = [];
62
63
  for (const project of selected){
63
- responses.push(await this.packageProject(project, releaseProjects));
64
+ responses.push(await this.packageProject(project, releaseProjects, projects));
64
65
  }
65
66
  return responses.some((response)=>!response.succeeded) ? _cli.ActionResponse.Error(PackageDeployable.name, responses) : _cli.ActionResponse.Success(PackageDeployable.name, responses);
66
67
  }
@@ -100,7 +101,7 @@ class PackageDeployable {
100
101
  }));
101
102
  return new Map(entries);
102
103
  }
103
- async packageProject(project, releaseProjects) {
104
+ async packageProject(project, releaseProjects, projects) {
104
105
  const outDir = _nodepath.default.join(_nodepath.default.resolve(project.projectRoot), _deployable.PACKAGE_DIR);
105
106
  const base = {
106
107
  projectName: project.name,
@@ -113,6 +114,7 @@ class PackageDeployable {
113
114
  if (!await _io.io.directoryExists(_nodepath.default.join(entry.releaseDir, "es6"))) {
114
115
  throw new _errors.ForgeError(`No release build for ${project.name}. Run \`forge build --configuration release\` first.`);
115
116
  }
117
+ const ui = await _uiassets.uiAssets.resolve(project, projects);
116
118
  const reached = await this.importGraph.walk(entry, releaseProjects);
117
119
  const libraries = [
118
120
  ...reached.workspace
@@ -124,7 +126,7 @@ class PackageDeployable {
124
126
  dir: _nodepath.default.resolve(this.workspace.rootDir),
125
127
  pkg: await this.workspace.getPackageJson()
126
128
  });
127
- await this.write(entry, libraries, dependencies, outDir);
129
+ await this.write(entry, libraries, dependencies, outDir, ui);
128
130
  const workspacePackages = [
129
131
  ...reached.workspace
130
132
  ].sort();
@@ -150,7 +152,7 @@ class PackageDeployable {
150
152
  };
151
153
  }
152
154
  }
153
- async write(entry, libraries, dependencies, outDir) {
155
+ async write(entry, libraries, dependencies, outDir, ui) {
154
156
  await _promises.default.rm(outDir, {
155
157
  recursive: true,
156
158
  force: true
@@ -180,6 +182,9 @@ class PackageDeployable {
180
182
  if (await _io.io.directoryExists(config)) {
181
183
  await this.copyTree(config, _nodepath.default.join(outDir, "config"));
182
184
  }
185
+ if (ui != null) {
186
+ await this.copyTree(ui.from, _nodepath.default.join(outDir, ui.to));
187
+ }
183
188
  for (const library of libraries){
184
189
  const target = _nodepath.default.join(outDir, "node_modules", ...library.name.split("/"));
185
190
  await this.copyTree(_nodepath.default.join(library.releaseDir, "es6"), _nodepath.default.join(target, "es6"));
@@ -19,7 +19,9 @@ function _interop_require_default(obj) {
19
19
  async function createWorkspace(options = {}) {
20
20
  const rootDir = options.rootDir ?? defaultRootDir();
21
21
  const configFile = options.configFile ?? (rootDir === _workspaceconfig.WorkspaceDefaults.rootDir ? undefined : _nodepath.default.join(rootDir, _mesh.WORKSPACE_CONFIG));
22
- const { configurations: _, ...forgeDefaults } = _workspaceconfig.WorkspaceDefaults;
22
+ // `projects` is dropped too: forge's defaults set it to undefined, which would override the
23
+ // user's per-project settings (docker, ui) in the merge.
24
+ const { configurations: _, projects: __, ...forgeDefaults } = _workspaceconfig.WorkspaceDefaults;
23
25
  const userConfig = await _mesh.UserConfigFactory.getUserConfig(configFile);
24
26
  // Only apply forge default configurations when user config doesn't provide any.
25
27
  // This ensures user-defined configurations take priority while still providing
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ function _export(target, all) {
6
+ for(var name in all)Object.defineProperty(target, name, {
7
+ enumerable: true,
8
+ get: Object.getOwnPropertyDescriptor(all, name).get
9
+ });
10
+ }
11
+ _export(exports, {
12
+ get DEFAULT_UI_OUTPUT () {
13
+ return DEFAULT_UI_OUTPUT;
14
+ },
15
+ get DEFAULT_UI_TARGET () {
16
+ return DEFAULT_UI_TARGET;
17
+ },
18
+ get uiAssets () {
19
+ return uiAssets;
20
+ }
21
+ });
22
+ const _nodepath = /*#__PURE__*/ _interop_require_default(require("node:path"));
23
+ const _io = require("@decohere/nexus/io");
24
+ const _errors = require("../errors.js");
25
+ function _interop_require_default(obj) {
26
+ return obj && obj.__esModule ? obj : {
27
+ default: obj
28
+ };
29
+ }
30
+ const DEFAULT_UI_OUTPUT = "dist";
31
+ const DEFAULT_UI_TARGET = "ui";
32
+ /** Folders `forge package` writes itself; a UI cannot land on top of them. */ const RESERVED_TARGETS = new Set([
33
+ "es6",
34
+ "node_modules",
35
+ "config",
36
+ "package.json"
37
+ ]);
38
+ /**
39
+ * What the deployable's `ui` declaration (`.decohere.js`) asks for, checked before anything
40
+ * is written: the UI project exists and has been built. Forge does not build it (see ADR 0007).
41
+ * Returns `undefined` when the deployable declares no UI.
42
+ */ async function resolve(deployable, projects) {
43
+ const ui = deployable.uiConfig;
44
+ if (ui == null) {
45
+ return undefined;
46
+ }
47
+ const to = _nodepath.default.normalize(ui.to ?? DEFAULT_UI_TARGET);
48
+ const output = ui.output ?? DEFAULT_UI_OUTPUT;
49
+ if (_nodepath.default.isAbsolute(to) || to.split(_nodepath.default.sep).includes("..") || to === ".") {
50
+ throw new _errors.ForgeError(`ui.to of ${deployable.name} must be a folder inside .package/, but is "${ui.to}".`);
51
+ }
52
+ if (RESERVED_TARGETS.has(to.split(_nodepath.default.sep)[0])) {
53
+ throw new _errors.ForgeError(`ui.to of ${deployable.name} is "${ui.to}", which forge package uses for something else.`);
54
+ }
55
+ const uiProject = projects.find((candidate)=>candidate.name === ui.project);
56
+ if (uiProject == null) {
57
+ throw new _errors.ForgeError(`${deployable.name} declares the UI project ${ui.project}, which is not a workspace project. ` + `Known projects: ${projects.map((p)=>p.name).join(", ")}`);
58
+ }
59
+ const root = _nodepath.default.resolve(uiProject.projectRoot);
60
+ const from = _nodepath.default.resolve(root, output);
61
+ if (!from.startsWith(root + _nodepath.default.sep)) {
62
+ throw new _errors.ForgeError(`ui.output of ${deployable.name} must stay inside ${ui.project}.`);
63
+ }
64
+ if (!await _io.io.directoryExists(from)) {
65
+ throw new _errors.ForgeError(`${deployable.name} declares the UI project ${ui.project}, but it has no build output at ${from}. ` + "Build the UI first (forge does not build it), or set ui.output if it builds somewhere else.");
66
+ }
67
+ return {
68
+ project: ui.project,
69
+ from,
70
+ to
71
+ };
72
+ }
73
+ const uiAssets = {
74
+ resolve
75
+ };
@@ -20,6 +20,7 @@ import { DependencyPinner } from "../packaging/dependency-pinner.service.js";
20
20
  import { deployable, PACKAGE_DIR } from "../packaging/deployable.js";
21
21
  import { ImportGraph } from "../packaging/import-graph.service.js";
22
22
  import { releaseExports } from "../packaging/release-exports.js";
23
+ import { uiAssets } from "../packaging/ui-assets.js";
23
24
  import { Release } from "../workspace.config.js";
24
25
  export class PackageDeployable {
25
26
  workspace;
@@ -45,7 +46,7 @@ export class PackageDeployable {
45
46
  const releaseProjects = await this.releaseProjects(projects);
46
47
  const responses = [];
47
48
  for (const project of selected){
48
- responses.push(await this.packageProject(project, releaseProjects));
49
+ responses.push(await this.packageProject(project, releaseProjects, projects));
49
50
  }
50
51
  return responses.some((response)=>!response.succeeded) ? ActionResponse.Error(PackageDeployable.name, responses) : ActionResponse.Success(PackageDeployable.name, responses);
51
52
  }
@@ -85,7 +86,7 @@ export class PackageDeployable {
85
86
  }));
86
87
  return new Map(entries);
87
88
  }
88
- async packageProject(project, releaseProjects) {
89
+ async packageProject(project, releaseProjects, projects) {
89
90
  const outDir = path.join(path.resolve(project.projectRoot), PACKAGE_DIR);
90
91
  const base = {
91
92
  projectName: project.name,
@@ -98,6 +99,7 @@ export class PackageDeployable {
98
99
  if (!await io.directoryExists(path.join(entry.releaseDir, "es6"))) {
99
100
  throw new ForgeError(`No release build for ${project.name}. Run \`forge build --configuration release\` first.`);
100
101
  }
102
+ const ui = await uiAssets.resolve(project, projects);
101
103
  const reached = await this.importGraph.walk(entry, releaseProjects);
102
104
  const libraries = [
103
105
  ...reached.workspace
@@ -109,7 +111,7 @@ export class PackageDeployable {
109
111
  dir: path.resolve(this.workspace.rootDir),
110
112
  pkg: await this.workspace.getPackageJson()
111
113
  });
112
- await this.write(entry, libraries, dependencies, outDir);
114
+ await this.write(entry, libraries, dependencies, outDir, ui);
113
115
  const workspacePackages = [
114
116
  ...reached.workspace
115
117
  ].sort();
@@ -135,7 +137,7 @@ export class PackageDeployable {
135
137
  };
136
138
  }
137
139
  }
138
- async write(entry, libraries, dependencies, outDir) {
140
+ async write(entry, libraries, dependencies, outDir, ui) {
139
141
  await fs.rm(outDir, {
140
142
  recursive: true,
141
143
  force: true
@@ -165,6 +167,9 @@ export class PackageDeployable {
165
167
  if (await io.directoryExists(config)) {
166
168
  await this.copyTree(config, path.join(outDir, "config"));
167
169
  }
170
+ if (ui != null) {
171
+ await this.copyTree(ui.from, path.join(outDir, ui.to));
172
+ }
168
173
  for (const library of libraries){
169
174
  const target = path.join(outDir, "node_modules", ...library.name.split("/"));
170
175
  await this.copyTree(path.join(library.releaseDir, "es6"), path.join(target, "es6"));
@@ -7,7 +7,9 @@ import { DefaultConfigurations, WorkspaceDefaults } from "./workspace.config.js"
7
7
  */ export async function createWorkspace(options = {}) {
8
8
  const rootDir = options.rootDir ?? defaultRootDir();
9
9
  const configFile = options.configFile ?? (rootDir === WorkspaceDefaults.rootDir ? undefined : path.join(rootDir, WORKSPACE_CONFIG));
10
- const { configurations: _, ...forgeDefaults } = WorkspaceDefaults;
10
+ // `projects` is dropped too: forge's defaults set it to undefined, which would override the
11
+ // user's per-project settings (docker, ui) in the merge.
12
+ const { configurations: _, projects: __, ...forgeDefaults } = WorkspaceDefaults;
11
13
  const userConfig = await UserConfigFactory.getUserConfig(configFile);
12
14
  // Only apply forge default configurations when user config doesn't provide any.
13
15
  // This ensures user-defined configurations take priority while still providing
@@ -0,0 +1,49 @@
1
+ import path from "node:path";
2
+ import { io } from "@decohere/nexus/io";
3
+ import { ForgeError } from "../errors.js";
4
+ export const DEFAULT_UI_OUTPUT = "dist";
5
+ export const DEFAULT_UI_TARGET = "ui";
6
+ /** Folders `forge package` writes itself; a UI cannot land on top of them. */ const RESERVED_TARGETS = new Set([
7
+ "es6",
8
+ "node_modules",
9
+ "config",
10
+ "package.json"
11
+ ]);
12
+ /**
13
+ * What the deployable's `ui` declaration (`.decohere.js`) asks for, checked before anything
14
+ * is written: the UI project exists and has been built. Forge does not build it (see ADR 0007).
15
+ * Returns `undefined` when the deployable declares no UI.
16
+ */ async function resolve(deployable, projects) {
17
+ const ui = deployable.uiConfig;
18
+ if (ui == null) {
19
+ return undefined;
20
+ }
21
+ const to = path.normalize(ui.to ?? DEFAULT_UI_TARGET);
22
+ const output = ui.output ?? DEFAULT_UI_OUTPUT;
23
+ if (path.isAbsolute(to) || to.split(path.sep).includes("..") || to === ".") {
24
+ throw new ForgeError(`ui.to of ${deployable.name} must be a folder inside .package/, but is "${ui.to}".`);
25
+ }
26
+ if (RESERVED_TARGETS.has(to.split(path.sep)[0])) {
27
+ throw new ForgeError(`ui.to of ${deployable.name} is "${ui.to}", which forge package uses for something else.`);
28
+ }
29
+ const uiProject = projects.find((candidate)=>candidate.name === ui.project);
30
+ if (uiProject == null) {
31
+ throw new ForgeError(`${deployable.name} declares the UI project ${ui.project}, which is not a workspace project. ` + `Known projects: ${projects.map((p)=>p.name).join(", ")}`);
32
+ }
33
+ const root = path.resolve(uiProject.projectRoot);
34
+ const from = path.resolve(root, output);
35
+ if (!from.startsWith(root + path.sep)) {
36
+ throw new ForgeError(`ui.output of ${deployable.name} must stay inside ${ui.project}.`);
37
+ }
38
+ if (!await io.directoryExists(from)) {
39
+ throw new ForgeError(`${deployable.name} declares the UI project ${ui.project}, but it has no build output at ${from}. ` + "Build the UI first (forge does not build it), or set ui.output if it builds somewhere else.");
40
+ }
41
+ return {
42
+ project: ui.project,
43
+ from,
44
+ to
45
+ };
46
+ }
47
+ export const uiAssets = {
48
+ resolve
49
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decohere/forge",
3
- "version": "0.0.1-dev.100",
3
+ "version": "0.0.1-dev.102",
4
4
  "description": "TypeScript monorepo build system — zero config, fast compilation, streamlined developer workflow",
5
5
  "keywords": [
6
6
  "typescript",
@@ -29,10 +29,10 @@
29
29
  "test": "echo \"Error: no test specified\" && exit 1"
30
30
  },
31
31
  "dependencies": {
32
- "@decohere/crucible": "0.0.1-dev.100",
33
- "@decohere/mesh": "0.0.1-dev.100",
34
- "@decohere/nexus": "0.0.1-dev.100",
35
- "@decohere/scribe": "0.0.1-dev.100",
32
+ "@decohere/crucible": "0.0.1-dev.102",
33
+ "@decohere/mesh": "0.0.1-dev.102",
34
+ "@decohere/nexus": "0.0.1-dev.102",
35
+ "@decohere/scribe": "0.0.1-dev.102",
36
36
  "@swc/core": "^1.11.11",
37
37
  "async": "^3.2.6",
38
38
  "parse-imports": "^2.2.1",
@@ -0,0 +1,21 @@
1
+ import type { Project } from "@decohere/mesh";
2
+ export declare const DEFAULT_UI_OUTPUT = "dist";
3
+ export declare const DEFAULT_UI_TARGET = "ui";
4
+ export type UiAssets = {
5
+ /** The UI project's name. */
6
+ project: string;
7
+ /** Absolute path of the built UI. */
8
+ from: string;
9
+ /** Folder name inside `.package/`. */
10
+ to: string;
11
+ };
12
+ /**
13
+ * What the deployable's `ui` declaration (`.decohere.js`) asks for, checked before anything
14
+ * is written: the UI project exists and has been built. Forge does not build it (see ADR 0007).
15
+ * Returns `undefined` when the deployable declares no UI.
16
+ */
17
+ declare function resolve(deployable: Project, projects: readonly Project[]): Promise<UiAssets | undefined>;
18
+ export declare const uiAssets: {
19
+ resolve: typeof resolve;
20
+ };
21
+ export {};