@xova/matrix 0.1.0-rc.0 → 0.1.1

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
@@ -121,12 +121,37 @@ export default defineMatrixConfig({
121
121
 
122
122
  `defineMatrixEnv()` is syntax sugar for the c12-compatible `$env.<environment>.env` shape. The raw shape remains supported.
123
123
 
124
- Values are merged from low to high precedence:
124
+ Ordinary application variables are merged from low to high precedence:
125
125
 
126
126
  ```text
127
- global env/$env < product env/$env < .env layers < process.env < MATRIX_*
127
+ global env/$env < product env/$env < .env layers < process.env
128
128
  ```
129
129
 
130
+ Product identity overrides are resolved from the merged environment before suffixes are applied:
131
+
132
+ ```text
133
+ product identity < variant identity < merged identity override < environment suffix
134
+ ```
135
+
136
+ `MATRIX_PRODUCT_NAME`, `MATRIX_PRODUCT_SLUG`, and `MATRIX_PRODUCT_APP_ID` may provide identity overrides through the environment. The final values, including suffixes, are written back to both task metadata and the corresponding `MATRIX_PRODUCT_*` variables. `MATRIX_PRODUCT_ID`, `MATRIX_PRODUCT_KEY`, execution-context variables, and `NODE_ENV` are generated by Matrix and cannot be overridden by the environment.
137
+
138
+ Matrix injects the following execution-context variables into every child process:
139
+
140
+ ```text
141
+ MATRIX_ENV_NAME
142
+ MATRIX_TARGET
143
+ MATRIX_PRODUCT_KEY
144
+ MATRIX_PRODUCT_ID
145
+ MATRIX_PRODUCT_NAME
146
+ MATRIX_PRODUCT_SLUG
147
+ MATRIX_PRODUCT_APP_ID
148
+ MATRIX_VARIANT
149
+ MATRIX_PROJECT
150
+ NODE_ENV
151
+ ```
152
+
153
+ `MATRIX_ENV_NAME` is the selected Matrix configuration environment and may be a custom name such as `staging` or `qa`. `NODE_ENV` describes the target process mode: `dev` uses `development`, while `build` and `preview` use `production`. Therefore a staging build normally receives `MATRIX_ENV_NAME=staging` and `NODE_ENV=production`. `MATRIX_PRODUCT_APP_ID` is emitted only when the resolved product identity has an `appId`; environment suffixes are applied before it is exported.
154
+
130
155
  Product-level environment values are resolved independently for each product. This lets two products reuse the same Desktop project while connecting it to different Web variants or services.
131
156
 
132
157
  Custom environment names are supported. Define them with the same helper and pass the name explicitly to the CLI:
@@ -162,7 +187,7 @@ The built-in targets use these defaults:
162
187
  | `build` | `production` | No |
163
188
  | `preview` | `production` | Yes |
164
189
 
165
- Custom targets are non-continuous by default and use `development` unless `--env` is provided. Project roots default to `.`, target output directories default to `dist`, and archive output defaults to `artifacts`. Archives are disabled by default, are only valid for the build target, and use zip when enabled. A configured archive fails the build when its output directory does not exist.
190
+ Custom targets are non-continuous by default and use `development` unless `--env` is provided. The built-in target runtime modes are `development` for `dev` and `production` for `build` and `preview`. Custom targets may set `nodeEnv` to `development`, `production`, or `test`. Project roots default to `.`, target output directories default to `dist`, and archive output defaults to `artifacts`. Archives are disabled by default, are only valid for the build target, and use zip when enabled. A configured archive fails the build when its output directory does not exist.
166
191
 
167
192
  Interactive Variant selection comes before Target selection. Target options are derived from the common targets available to the selected Variants; use `--variant` to provide the scope in non-interactive runs.
168
193
 
package/README.zh-CN.md CHANGED
@@ -121,12 +121,37 @@ export default defineMatrixConfig({
121
121
 
122
122
  `defineMatrixEnv()` 是 c12 兼容的 `$env.<environment>.env` 写法的语法糖,底层结构仍然保持不变,也继续支持直接使用原始写法。
123
123
 
124
- 变量覆盖优先级从低到高为:
124
+ 普通应用变量的覆盖优先级从低到高为:
125
125
 
126
126
  ```text
127
- global env/$env < product env/$env < .env layers < process.env < MATRIX_*
127
+ global env/$env < product env/$env < .env layers < process.env
128
128
  ```
129
129
 
130
+ 产品 identity override 会先从合并后的环境变量中解析,然后再应用 suffix:
131
+
132
+ ```text
133
+ product identity < variant identity < 合并后的 identity override < environment suffix
134
+ ```
135
+
136
+ `MATRIX_PRODUCT_NAME`、`MATRIX_PRODUCT_SLUG` 和 `MATRIX_PRODUCT_APP_ID` 可以通过环境变量提供 identity override。包含 suffix 的最终值会同时写入任务元数据和对应的 `MATRIX_PRODUCT_*` 变量。`MATRIX_PRODUCT_ID`、`MATRIX_PRODUCT_KEY`、执行上下文变量和 `NODE_ENV` 由 Matrix 生成,不能被环境变量覆盖。
137
+
138
+ Matrix 会向每个子进程注入以下执行上下文变量:
139
+
140
+ ```text
141
+ MATRIX_ENV_NAME
142
+ MATRIX_TARGET
143
+ MATRIX_PRODUCT_KEY
144
+ MATRIX_PRODUCT_ID
145
+ MATRIX_PRODUCT_NAME
146
+ MATRIX_PRODUCT_SLUG
147
+ MATRIX_PRODUCT_APP_ID
148
+ MATRIX_VARIANT
149
+ MATRIX_PROJECT
150
+ NODE_ENV
151
+ ```
152
+
153
+ `MATRIX_ENV_NAME` 是当前选择的 Matrix 配置环境,可以是 `staging`、`qa` 等自定义名称。`NODE_ENV` 表示目标进程的运行模式:`dev` 使用 `development`,`build` 和 `preview` 使用 `production`。因此 staging 构建通常会同时得到 `MATRIX_ENV_NAME=staging` 和 `NODE_ENV=production`。只有最终解析出的产品 identity 配置了 `appId` 时,才会注入 `MATRIX_PRODUCT_APP_ID`;环境 suffix 会在导出前生效。
154
+
130
155
  产品级环境变量会为每个产品独立解析。这样多个产品可以复用同一个 Desktop 项目,同时连接不同的 Web 变体或服务。
131
156
 
132
157
  支持自定义环境名称。使用相同的 helper 定义,并在 CLI 中显式传入环境名:
@@ -162,7 +187,7 @@ dotenv 文件按 `.env`、`.env.local`、`.env.<environment>` 和 `.env.<environ
162
187
  | `build` | `production` | 否 |
163
188
  | `preview` | `production` | 是 |
164
189
 
165
- 自定义目标默认不会持续运行,未指定 `--env` 时使用 `development`。项目默认使用当前目录,目标输出目录默认为 `dist`,归档输出目录默认为 `artifacts`。归档默认关闭,只允许配置在 build 目标上,启用后默认使用 zip 格式。配置了归档但输出目录不存在时,构建会失败。
190
+ 自定义目标默认不会持续运行,未指定 `--env` 时使用 `development`。内置目标的运行模式为:`dev` 使用 `development`,`build` 和 `preview` 使用 `production`。自定义目标也可以将 `nodeEnv` 配置为 `development`、`production` 或 `test`。项目默认使用当前目录,目标输出目录默认为 `dist`,归档输出目录默认为 `artifacts`。归档默认关闭,只允许配置在 build 目标上,启用后默认使用 zip 格式。配置了归档但输出目录不存在时,构建会失败。
166
191
 
167
192
  交互式运行会先选择 Variant,再选择 Target。Target 选项来自已选 Variant 共同支持的目标;非交互式运行可以使用 `--variant` 提前指定执行范围。
168
193
 
package/dist/config.js CHANGED
@@ -1 +1 @@
1
- import{MATRIX_DEFAULTS as e,defaultEnvironmentForTarget as t}from"./defaults.js";import{assertMatrixConfig as n}from"./schema.js";import{MATRIX_ENVIRONMENTS as r}from"./types.js";import i from"node:path";import a from"node:process";import{loadConfig as o}from"c12";function s(e){return e}function c(e){return Object.fromEntries(Object.entries(e).map(([e,t])=>[e,{env:t}]))}async function l(e={}){let t=i.resolve(e.cwd??a.cwd()),n=(await o({name:`matrix`,cwd:t,...e.configFile?{configFile:e.configFile}:{},envName:!1,dotenv:!1,omit$Keys:!1,rcFile:!1,packageJson:!1})).config,s=new Set(r);for(let e of Object.keys(n.$env??{}))s.add(e);let c=e.productName?n.products?.[e.productName]:void 0,l=e.productName?c?[c]:[]:Object.values(n.products??{});for(let e of l)for(let t of Object.keys(e.$env??{}))s.add(t);return[...s]}function u(t,n){let r=typeof n==`string`?{command:n}:{...n};if(!r.command)throw Error(`Target ${t} must define a command`);let i=typeof r.archive==`boolean`?{enabled:r.archive,format:e.archive.format}:{enabled:r.archive?.enabled??e.archive.enabled,format:r.archive?.format??e.archive.format},a=e.targets[t];return{...r,name:t,continuous:r.continuous??a?.continuous??!1,outputDir:r.outputDir??e.outputDir,archive:i,dependsOn:(r.dependsOn??[]).map(e=>typeof e==`string`?{variant:e}:e)}}function d(e,t){if(e!==`build`&&typeof t!=`string`&&t.archive!==void 0)throw Error(`Archive is only supported for build targets: ${e}`)}function f(e,t){if(typeof t==`string`)return t;let n=t.readyWhen===void 0?e.readyWhen:{...e.readyWhen,...t.readyWhen},r=typeof e.archive==`object`?e.archive:{enabled:e.archive},i=typeof t.archive==`object`&&t.archive!==null?{...r,...t.archive}:t.archive??r;return{...e,...t,archive:i,...n?{readyWhen:n}:{}}}function p(...e){return Object.assign({},...e.filter(Boolean))}function m(e,t){let n=Object.fromEntries(Object.entries(e.products).map(([e,n])=>{let{$env:r,...i}=n,a=p(n.env,r?.[t]?.env);return[e,{...i,...Object.keys(a).length?{env:a}:{}}]}));return{...e,products:n}}function h(e){let t=Object.fromEntries(Object.entries(e.projects).map(([e,t])=>[e,{...t,id:e,targets:Object.fromEntries(Object.entries(t.targets).map(([e,t])=>(d(e,t),[e,u(e,t)])))}]));return{config:e,projects:t,products:Object.fromEntries(Object.entries(e.products).map(([e,n])=>{let r=Object.fromEntries(Object.entries(n.variants).map(([n,r])=>{let i=typeof r==`string`?{project:r}:r,a=t[i.project];if(!a)throw Error(`Product ${e} variant ${n} references unknown project ${i.project}`);let o=Object.fromEntries(Object.entries(a.targets).map(([e,t])=>{let n=i.targets?.[e];return n!==void 0&&d(e,n),[e,u(e,n===void 0?t:f(t,n))]}));for(let[t,r]of Object.entries(i.targets??{}))if(!(t in o)){if(d(t,r),typeof r==`string`)o[t]=u(t,r);else if(r.command)o[t]=u(t,r);else throw Error(`Variant ${e}/${n} target ${t} must define a command`)}return[n,{...i,id:n,targets:o}]}));return[e,{...n,id:n.id??e,name:n.name??e,slug:n.slug??e,key:e,variants:r}]}))}}async function g(t={}){let r=i.resolve(t.cwd??a.cwd()),s=t.envName??e.environment,c={name:`matrix`,cwd:r,...t.configFile?{configFile:t.configFile}:{},envName:s,dotenv:{fileName:[`.env`,`.env.local`,`.env.${s}`,`.env.${s}.local`]},omit$Keys:!0,rcFile:!1,packageJson:!1},l=await o(c),u=m(n(l.config),s),d=Object.fromEntries(Object.entries(a.env).filter(e=>e[1]!==void 0));return{...h(u),configFile:l.configFile,layers:l.layers,cwd:r,envName:s,externalEnv:d}}export{e as MATRIX_DEFAULTS,t as defaultEnvironmentForTarget,s as defineMatrixConfig,c as defineMatrixEnv,l as listMatrixEnvironments,g as loadMatrixConfig,h as normalizeMatrixConfig};
1
+ import{MATRIX_DEFAULTS as e,defaultEnvironmentForTarget as t}from"./defaults.js";import{assertMatrixConfig as n}from"./schema.js";import{MATRIX_ENVIRONMENTS as r}from"./types.js";import i from"node:path";import a from"node:process";import{loadConfig as o}from"c12";function s(e){return e}function c(e){return Object.fromEntries(Object.entries(e).map(([e,t])=>[e,{env:t}]))}async function l(e={}){let t=i.resolve(e.cwd??a.cwd()),n=(await o({name:`matrix`,cwd:t,...e.configFile?{configFile:e.configFile}:{},envName:!1,dotenv:!1,omit$Keys:!1,rcFile:!1,packageJson:!1})).config,s=new Set(r);for(let e of Object.keys(n.$env??{}))s.add(e);let c=e.productName?n.products?.[e.productName]:void 0,l=e.productName?c?[c]:[]:Object.values(n.products??{});for(let e of l)for(let t of Object.keys(e.$env??{}))s.add(t);return[...s]}function u(t,n){let r=typeof n==`string`?{command:n}:{...n};if(!r.command)throw Error(`Target ${t} must define a command`);let i=typeof r.archive==`boolean`?{enabled:r.archive,format:e.archive.format}:{enabled:r.archive?.enabled??e.archive.enabled,format:r.archive?.format??e.archive.format},a=e.targets[t];return{...r,name:t,continuous:r.continuous??a?.continuous??!1,nodeEnv:r.nodeEnv??a?.nodeEnv??`development`,outputDir:r.outputDir??e.outputDir,archive:i,dependsOn:(r.dependsOn??[]).map(e=>typeof e==`string`?{variant:e}:e)}}function d(e,t){if(e!==`build`&&typeof t!=`string`&&t.archive!==void 0)throw Error(`Archive is only supported for build targets: ${e}`)}function f(e,t){if(typeof t==`string`)return t;let n=t.readyWhen===void 0?e.readyWhen:{...e.readyWhen,...t.readyWhen},r=typeof e.archive==`object`?e.archive:{enabled:e.archive},i=typeof t.archive==`object`&&t.archive!==null?{...r,...t.archive}:t.archive??r;return{...e,...t,archive:i,...n?{readyWhen:n}:{}}}function p(...e){return Object.assign({},...e.filter(Boolean))}function m(e,t){let n=Object.fromEntries(Object.entries(e.products).map(([e,n])=>{let{$env:r,...i}=n,a=p(n.env,r?.[t]?.env);return[e,{...i,...Object.keys(a).length?{env:a}:{}}]}));return{...e,products:n}}function h(e){let t=Object.fromEntries(Object.entries(e.projects).map(([e,t])=>[e,{...t,id:e,targets:Object.fromEntries(Object.entries(t.targets).map(([e,t])=>(d(e,t),[e,u(e,t)])))}]));return{config:e,projects:t,products:Object.fromEntries(Object.entries(e.products).map(([e,n])=>{let r=Object.fromEntries(Object.entries(n.variants).map(([n,r])=>{let i=typeof r==`string`?{project:r}:r,a=t[i.project];if(!a)throw Error(`Product ${e} variant ${n} references unknown project ${i.project}`);let o=Object.fromEntries(Object.entries(a.targets).map(([e,t])=>{let n=i.targets?.[e];return n!==void 0&&d(e,n),[e,u(e,n===void 0?t:f(t,n))]}));for(let[t,r]of Object.entries(i.targets??{}))if(!(t in o)){if(d(t,r),typeof r==`string`)o[t]=u(t,r);else if(r.command)o[t]=u(t,r);else throw Error(`Variant ${e}/${n} target ${t} must define a command`)}return[n,{...i,id:n,targets:o}]}));return[e,{...n,id:n.id??e,name:n.name??e,slug:n.slug??e,key:e,variants:r}]}))}}async function g(t={}){let r=i.resolve(t.cwd??a.cwd()),s=t.envName??e.environment,c={name:`matrix`,cwd:r,...t.configFile?{configFile:t.configFile}:{},envName:s,dotenv:{fileName:[`.env`,`.env.local`,`.env.${s}`,`.env.${s}.local`]},omit$Keys:!0,rcFile:!1,packageJson:!1},l=await o(c),u=m(n(l.config),s),d=Object.fromEntries(Object.entries(a.env).filter(e=>e[1]!==void 0));return{...h(u),configFile:l.configFile,layers:l.layers,cwd:r,envName:s,externalEnv:d}}export{e as MATRIX_DEFAULTS,t as defaultEnvironmentForTarget,s as defineMatrixConfig,c as defineMatrixEnv,l as listMatrixEnvironments,g as loadMatrixConfig,h as normalizeMatrixConfig};
@@ -13,14 +13,17 @@ export declare const MATRIX_DEFAULTS: {
13
13
  readonly targets: {
14
14
  readonly dev: {
15
15
  readonly environment: "development";
16
+ readonly nodeEnv: "development";
16
17
  readonly continuous: true;
17
18
  };
18
19
  readonly build: {
19
20
  readonly environment: "production";
21
+ readonly nodeEnv: "production";
20
22
  readonly continuous: false;
21
23
  };
22
24
  readonly preview: {
23
25
  readonly environment: "production";
26
+ readonly nodeEnv: "production";
24
27
  readonly continuous: true;
25
28
  };
26
29
  };
package/dist/defaults.js CHANGED
@@ -1 +1 @@
1
- const e={target:`dev`,environment:`development`,projectRoot:`.`,outputDir:`dist`,artifactsRoot:`artifacts`,archive:{enabled:!1,format:`zip`},targets:{dev:{environment:`development`,continuous:!0},build:{environment:`production`,continuous:!1},preview:{environment:`production`,continuous:!0}}};function t(t){return e.targets[t]?.environment??e.environment}export{e as MATRIX_DEFAULTS,t as defaultEnvironmentForTarget};
1
+ const e={target:`dev`,environment:`development`,projectRoot:`.`,outputDir:`dist`,artifactsRoot:`artifacts`,archive:{enabled:!1,format:`zip`},targets:{dev:{environment:`development`,nodeEnv:`development`,continuous:!0},build:{environment:`production`,nodeEnv:`production`,continuous:!1},preview:{environment:`production`,nodeEnv:`production`,continuous:!0}}};function t(t){return e.targets[t]?.environment??e.environment}export{e as MATRIX_DEFAULTS,t as defaultEnvironmentForTarget};
package/dist/exec.js CHANGED
@@ -1 +1 @@
1
- import{archiveDirectory as e}from"./archive.js";import t from"node:path";import n from"node:process";import r from"consola";import i from"node:net";import{execaCommand as a}from"execa";const o=new Promise(()=>void 0);function s(e){return new Promise(t=>setTimeout(t,e))}function c(e,t){return t.size?Promise.race([e,...t]):e}function l(e,t,n){return new Promise(r=>{let a=i.createConnection({host:e,port:t}),o=!1,s=e=>{o||(o=!0,a.destroy(),r(e))};a.once(`connect`,()=>s(!0)),a.once(`error`,()=>s(!1)),a.setTimeout(n,()=>s(!1))})}async function u(e,t,n){let r=t.readyWhen;if(!r)return;let i=r.timeout??3e4,a=Date.now()+i;for(;;){let i=a-Date.now();if(i<=0)break;if(await c(l(r.host??`127.0.0.1`,r.port,Math.min(1e3,i)),n))return;if(await c(Promise.race([e.then(e=>e),new Promise(e=>setTimeout(e,Math.min(200,Math.max(1,a-Date.now()))))]),n)!==void 0)throw Error(`${t.id} exited before becoming ready`)}throw Error(`Timed out waiting for ${t.id} on port ${r.port}`)}async function d(e,t,n){for(let[n,r]of e)t.has(n)||(t.add(n),r.kill(`SIGTERM`));await Promise.race([Promise.allSettled([...e.values()]),s(5e3)]);for(let[r,i]of e)n.has(r)||(t.add(r),i.kill(`SIGKILL`));await Promise.allSettled([...e.values()])}async function f(i){let s=/* @__PURE__ */ new Map,l=/* @__PURE__ */ new Set,f=/* @__PURE__ */ new Set,p=/* @__PURE__ */ new Set,m=()=>{for(let[e,t]of s)l.has(e)||(l.add(e),t.kill(`SIGTERM`))};n.on(`SIGINT`,m),n.on(`SIGTERM`,m);try{for(let d of i.tasks){for(let e of d.dependsOn){let t=s.get(e.id);if(!t)throw Error(`Dependency ${e.id} was not started`);if(e.condition===`completed`){let n=await c(t,p);if(n.exitCode!==0)throw Error(`${e.id} exited with code ${n.exitCode}`)}else{let n=i.tasks.find(t=>t.id===e.id);if(!n)throw Error(`Dependency task ${e.id} is missing`);await u(t,n,p)}}r.info(`${d.id} → ${d.command}`);let m=a(d.command,{cwd:d.cwd,env:{...n.env,...Object.fromEntries(Object.entries(d.env).map(([e,t])=>[e,String(t)]))},stdio:`inherit`,reject:!1,killDescendants:!0});if(s.set(d.id,m),m.then(()=>f.add(d.id),()=>f.add(d.id)),d.continuous){let e=m.then(e=>{if(l.size||e.exitCode===0)return o;throw Error(`${d.id} exited with code ${e.exitCode}`)});p.add(e),e.catch(()=>void 0)}if(!d.continuous){let n=await c(m,p);if(n.exitCode!==0)throw Error(`${d.id} exited with code ${n.exitCode}`);if(d.target===`build`&&d.archive.enabled){let n=d.archive.format===`zip`?`zip`:`tar.gz`,r=t.join(i.artifactsRoot,d.product,i.envName,`${d.variant}.${n}`);await e(d.outputDir,r,d.archive.format)}}}let d=i.tasks.filter(e=>e.continuous).map(e=>s.get(e.id));return d.length&&await c(Promise.race(d.map(async e=>{let t=await e;if(!l.size&&t.exitCode!==0)throw Error(`Service exited with code ${t.exitCode}`)})),p),{children:s}}finally{i.tasks.some(e=>e.continuous)?await d(s,l,f):await Promise.allSettled([...s.values()]),n.removeListener(`SIGINT`,m),n.removeListener(`SIGTERM`,m)}}export{f as runExecutionPlan};
1
+ import{archiveDirectory as e}from"./archive.js";import t from"node:path";import n from"node:process";import r from"consola";import i from"node:net";import{execaCommand as a}from"execa";const o=new Promise(()=>void 0);function s(e){return new Promise(t=>setTimeout(t,e))}function c(e,t){return t.size?Promise.race([e,...t]):e}function l(e,t,n){return new Promise(r=>{let a=i.createConnection({host:e,port:t}),o=!1,s=e=>{o||(o=!0,a.destroy(),r(e))};a.once(`connect`,()=>s(!0)),a.once(`error`,()=>s(!1)),a.setTimeout(n,()=>s(!1))})}async function u(e,t,n){let r=t.readyWhen;if(!r)return;let i=r.timeout??3e4,a=Date.now()+i;for(;;){let i=a-Date.now();if(i<=0)break;if(await c(l(r.host??`127.0.0.1`,r.port,Math.min(1e3,i)),n))return;if(await c(Promise.race([e.then(e=>e),new Promise(e=>setTimeout(e,Math.min(200,Math.max(1,a-Date.now()))))]),n)!==void 0)throw Error(`${t.id} exited before becoming ready`)}throw Error(`Timed out waiting for ${t.id} on port ${r.port}`)}async function d(e,t,n){for(let[n,r]of e)t.has(n)||(t.add(n),r.kill(`SIGTERM`));await Promise.race([Promise.allSettled([...e.values()]),s(5e3)]);for(let[r,i]of e)n.has(r)||(t.add(r),i.kill(`SIGKILL`));await Promise.allSettled([...e.values()])}async function f(i){let s=/* @__PURE__ */ new Map,l=/* @__PURE__ */ new Set,f=/* @__PURE__ */ new Set,p=/* @__PURE__ */ new Set,m=()=>{for(let[e,t]of s)l.has(e)||(l.add(e),t.kill(`SIGTERM`))};n.on(`SIGINT`,m),n.on(`SIGTERM`,m);try{for(let n of i.tasks){for(let e of n.dependsOn){let t=s.get(e.id);if(!t)throw Error(`Dependency ${e.id} was not started`);if(e.condition===`completed`){let n=await c(t,p);if(n.exitCode!==0)throw Error(`${e.id} exited with code ${n.exitCode}`)}else{let n=i.tasks.find(t=>t.id===e.id);if(!n)throw Error(`Dependency task ${e.id} is missing`);await u(t,n,p)}}r.info(`${n.id} → ${n.command}`);let d=a(n.command,{cwd:n.cwd,env:Object.fromEntries(Object.entries(n.env).map(([e,t])=>[e,String(t)])),extendEnv:!0,stdio:`inherit`,reject:!1,killDescendants:!0});if(s.set(n.id,d),d.then(()=>f.add(n.id),()=>f.add(n.id)),n.continuous){let e=d.then(e=>{if(l.size||e.exitCode===0)return o;throw Error(`${n.id} exited with code ${e.exitCode}`)});p.add(e),e.catch(()=>void 0)}if(!n.continuous){let r=await c(d,p);if(r.exitCode!==0)throw Error(`${n.id} exited with code ${r.exitCode}`);if(n.target===`build`&&n.archive.enabled){let r=n.archive.format===`zip`?`zip`:`tar.gz`,a=t.join(i.artifactsRoot,n.product,i.envName,`${n.variant}.${r}`);await e(n.outputDir,a,n.archive.format)}}}let n=i.tasks.filter(e=>e.continuous).map(e=>s.get(e.id));return n.length&&await c(Promise.race(n.map(async e=>{let t=await e;if(!l.size&&t.exitCode!==0)throw Error(`Service exited with code ${t.exitCode}`)})),p),{children:s}}finally{i.tasks.some(e=>e.continuous)?await d(s,l,f):await Promise.allSettled([...s.values()]),n.removeListener(`SIGINT`,m),n.removeListener(`SIGTERM`,m)}}export{f as runExecutionPlan};
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { CommandTarget, CreateExecutionPlanInput, EnvMap, EnvironmentConfig, ExecutionPlan, ExecutionTask, MATRIX_ENVIRONMENTS, MatrixConfig, MatrixEnvironment, NormalizedProduct, NormalizedProject, NormalizedTarget, NormalizedVariant, ProductConfig, ProjectConfig, Scalar, SuffixConfig, TargetConfig, TargetDependency, TargetOverride, VariantConfig } from "./types.js";
1
+ import { CommandTarget, CreateExecutionPlanInput, EnvMap, EnvironmentConfig, ExecutionPlan, ExecutionTask, MATRIX_ENVIRONMENTS, MatrixConfig, MatrixEnvironment, NodeEnvironment, NormalizedProduct, NormalizedProject, NormalizedTarget, NormalizedVariant, ProductConfig, ProjectConfig, Scalar, SuffixConfig, TargetConfig, TargetDependency, TargetOverride, VariantConfig } from "./types.js";
2
2
  import { MATRIX_DEFAULTS, defaultEnvironmentForTarget } from "./defaults.js";
3
3
  import { defineMatrixConfig, defineMatrixEnv, listMatrixEnvironments, loadMatrixConfig, normalizeMatrixConfig } from "./config.js";
4
4
  import { createExecutionPlan } from "./plan.js";
5
5
  import { assertMatrixConfig, matrixConfigSchema } from "./schema.js";
6
- export { type CommandTarget, type CreateExecutionPlanInput, type EnvMap, type EnvironmentConfig, type ExecutionPlan, type ExecutionTask, MATRIX_DEFAULTS, MATRIX_ENVIRONMENTS, type MatrixConfig, type MatrixEnvironment, type NormalizedProduct, type NormalizedProject, type NormalizedTarget, type NormalizedVariant, type ProductConfig, type ProjectConfig, type Scalar, type SuffixConfig, type TargetConfig, type TargetDependency, type TargetOverride, type VariantConfig, assertMatrixConfig, createExecutionPlan, defaultEnvironmentForTarget, defineMatrixConfig, defineMatrixEnv, listMatrixEnvironments, loadMatrixConfig, matrixConfigSchema, normalizeMatrixConfig };
6
+ export { type CommandTarget, type CreateExecutionPlanInput, type EnvMap, type EnvironmentConfig, type ExecutionPlan, type ExecutionTask, MATRIX_DEFAULTS, MATRIX_ENVIRONMENTS, type MatrixConfig, type MatrixEnvironment, type NodeEnvironment, type NormalizedProduct, type NormalizedProject, type NormalizedTarget, type NormalizedVariant, type ProductConfig, type ProjectConfig, type Scalar, type SuffixConfig, type TargetConfig, type TargetDependency, type TargetOverride, type VariantConfig, assertMatrixConfig, createExecutionPlan, defaultEnvironmentForTarget, defineMatrixConfig, defineMatrixEnv, listMatrixEnvironments, loadMatrixConfig, matrixConfigSchema, normalizeMatrixConfig };
package/dist/plan.js CHANGED
@@ -1 +1 @@
1
- import{MATRIX_DEFAULTS as e}from"./defaults.js";import t from"node:path";import n from"node:process";function r(...e){return Object.assign({},...e.filter(Boolean))}function i(){return Object.fromEntries(Object.entries(n.env).filter(e=>e[1]!==void 0))}function a(e,t){return e===void 0||t===void 0?e:`${e}${t}`}function o(...e){let t={};for(let n of e)for(let[e,r]of Object.entries(n??{}))t[e]={...t[e],...r};return t}function s(e,t,n){let r={...e.suffixes?.[n]??{},...t.suffixes?.[n]??{}};return{id:e.id,name:a(t.name??e.name,r.name),slug:a(t.slug??e.slug,r.slug),appId:a(t.appId??e.appId,r.appId)}}function c(e,t){return e.condition??(t.continuous?`ready`:`completed`)}function l(n){let a=/* @__PURE__ */ new Map,l=/* @__PURE__ */ new Set,u=[],d=(f,p,m)=>{let h=`${f}:${p}:${m}`,g=a.get(h);if(g)return g;if(l.has(h))throw Error(`Dependency cycle detected at ${h}`);let _=n.products[f];if(!_)throw Error(`Unknown product: ${f}`);let v=_.variants[p];if(!v)throw Error(`Unknown variant ${p} for product ${f}`);let y=n.projects[v.project];if(!y)throw Error(`Variant ${f}/${p} references unknown project ${v.project}`);let b=v.targets[m];if(!b)throw Error(`Variant ${f}/${p} has no target ${m}`);l.add(h);let x=b.dependsOn.map(e=>{let t=e.target??m,n=_.variants[e.variant];if(!n)throw Error(`Unknown dependency variant ${f}/${e.variant}`);let r=n.targets[t];if(!r)throw Error(`Variant ${f}/${e.variant} has no target ${t}`);return{id:d(f,e.variant,t).id,condition:c(e,r)}});l.delete(h);let S=s({..._,suffixes:o(n.config.suffixes,_.suffixes)},v,n.envName),C=t.resolve(n.cwd,y.root??e.projectRoot),w={id:h,product:f,variant:p,project:v.project,projectRoot:C,target:m,name:S.name,slug:S.slug,...S.appId?{appId:S.appId}:{},command:b.command,cwd:C,env:r(n.config.env,_.env,n.externalEnv??i(),{MATRIX_ENV_NAME:n.envName,MATRIX_PRODUCT_ID:S.id,MATRIX_PRODUCT_NAME:S.name,MATRIX_PRODUCT_SLUG:S.slug}),continuous:b.continuous,archive:b.archive,...b.readyWhen?{readyWhen:b.readyWhen}:{},outputDir:t.resolve(C,b.outputDir),dependsOn:x};return a.set(h,w),u.push(w),w};for(let e of n.productNames){let t=n.products[e];if(!t)throw Error(`Unknown product: ${e}`);let r=n.variantNames?.length?n.variantNames:Object.keys(t.variants);for(let t of r)d(e,t,n.target)}return{envName:n.envName,tasks:u,artifactsRoot:t.resolve(n.cwd,n.config.artifacts?.root??e.artifactsRoot)}}function u(e){for(let[t,n]of Object.entries(e.products))for(let[r,i]of Object.entries(n.variants))for(let n of Object.keys(i.targets))l({...e,productNames:[t],variantNames:[r],target:n})}export{l as createExecutionPlan,u as validateExecutionGraph};
1
+ import{MATRIX_DEFAULTS as e}from"./defaults.js";import t from"node:path";import n from"node:process";function r(...e){return Object.assign({},...e.filter(Boolean))}function i(){return Object.fromEntries(Object.entries(n.env).filter(e=>e[1]!==void 0))}function a(e,t){return e===void 0||t===void 0?e:`${e}${t}`}function o(e,t){let n=e[t];return n===void 0?void 0:String(n)}function s(...e){let t={};for(let n of e)for(let[e,r]of Object.entries(n??{}))t[e]={...t[e],...r};return t}function c(e,t,n,r){let i={...e.suffixes?.[n]??{},...t.suffixes?.[n]??{}},s=o(r,`MATRIX_PRODUCT_NAME`)??t.name??e.name,c=o(r,`MATRIX_PRODUCT_SLUG`)??t.slug??e.slug,l=o(r,`MATRIX_PRODUCT_APP_ID`)??t.appId??e.appId;return{id:e.id,name:a(s,i.name),slug:a(c,i.slug),appId:a(l,i.appId)}}function l(e,t){return e.condition??(t.continuous?`ready`:`completed`)}function u(n){let a=/* @__PURE__ */ new Map,o=/* @__PURE__ */ new Set,u=[],d=(f,p,m)=>{let h=`${f}:${p}:${m}`,g=a.get(h);if(g)return g;if(o.has(h))throw Error(`Dependency cycle detected at ${h}`);let _=n.products[f];if(!_)throw Error(`Unknown product: ${f}`);let v=_.variants[p];if(!v)throw Error(`Unknown variant ${p} for product ${f}`);let y=n.projects[v.project];if(!y)throw Error(`Variant ${f}/${p} references unknown project ${v.project}`);let b=v.targets[m];if(!b)throw Error(`Variant ${f}/${p} has no target ${m}`);o.add(h);let x=b.dependsOn.map(e=>{let t=e.target??m,n=_.variants[e.variant];if(!n)throw Error(`Unknown dependency variant ${f}/${e.variant}`);let r=n.targets[t];if(!r)throw Error(`Variant ${f}/${e.variant} has no target ${t}`);return{id:d(f,e.variant,t).id,condition:l(e,r)}});o.delete(h);let S=r(n.config.env,_.env,n.externalEnv??i()),C=c({..._,suffixes:s(n.config.suffixes,_.suffixes)},v,n.envName,S),w=t.resolve(n.cwd,y.root??e.projectRoot),T={id:h,product:f,variant:p,project:v.project,projectRoot:w,target:m,name:C.name,slug:C.slug,...C.appId?{appId:C.appId}:{},command:b.command,cwd:w,env:r(S,{MATRIX_ENV_NAME:n.envName,MATRIX_TARGET:m,MATRIX_PRODUCT_KEY:f,MATRIX_PRODUCT_ID:C.id,MATRIX_PRODUCT_NAME:C.name,MATRIX_PRODUCT_SLUG:C.slug,MATRIX_VARIANT:p,MATRIX_PROJECT:v.project,NODE_ENV:b.nodeEnv,...C.appId?{MATRIX_PRODUCT_APP_ID:C.appId}:{}}),continuous:b.continuous,archive:b.archive,...b.readyWhen?{readyWhen:b.readyWhen}:{},outputDir:t.resolve(w,b.outputDir),dependsOn:x};return a.set(h,T),u.push(T),T};for(let e of n.productNames){let t=n.products[e];if(!t)throw Error(`Unknown product: ${e}`);let r=n.variantNames?.length?n.variantNames:Object.keys(t.variants);for(let t of r)d(e,t,n.target)}return{envName:n.envName,tasks:u,artifactsRoot:t.resolve(n.cwd,n.config.artifacts?.root??e.artifactsRoot)}}function d(e){for(let[t,n]of Object.entries(e.products))for(let[r,i]of Object.entries(n.variants))for(let n of Object.keys(i.targets))u({...e,productNames:[t],variantNames:[r],target:n})}export{u as createExecutionPlan,d as validateExecutionGraph};
package/dist/schema.d.ts CHANGED
@@ -16,6 +16,7 @@ export declare const matrixConfigSchema: v.ObjectSchema<{
16
16
  readonly targets: v.RecordSchema<v.StringSchema<undefined>, v.UnionSchema<[v.StringSchema<undefined>, v.ObjectSchema<{
17
17
  readonly command: v.StringSchema<undefined>;
18
18
  readonly continuous: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
19
+ readonly nodeEnv: v.OptionalSchema<v.PicklistSchema<["development", "production", "test"], undefined>, undefined>;
19
20
  readonly readyWhen: v.OptionalSchema<v.ObjectSchema<{
20
21
  readonly type: v.LiteralSchema<"port", undefined>;
21
22
  readonly host: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
@@ -61,6 +62,7 @@ export declare const matrixConfigSchema: v.ObjectSchema<{
61
62
  readonly targets: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnionSchema<[v.StringSchema<undefined>, v.ObjectSchema<{
62
63
  readonly command: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
63
64
  readonly continuous: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
65
+ readonly nodeEnv: v.OptionalSchema<v.PicklistSchema<["development", "production", "test"], undefined>, undefined>;
64
66
  readonly readyWhen: v.OptionalSchema<v.ObjectSchema<{
65
67
  readonly type: v.LiteralSchema<"port", undefined>;
66
68
  readonly host: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
package/dist/schema.js CHANGED
@@ -1 +1 @@
1
- import*as e from"valibot";const t=e.union([e.string(),e.number(),e.boolean()]),n=e.optional(e.record(e.string(),t)),r=e.object({name:e.optional(e.string()),slug:e.optional(e.string()),appId:e.optional(e.string())}),i=e.object({variant:e.string(),target:e.optional(e.string()),condition:e.optional(e.picklist([`completed`,`ready`]))}),a=e.union([e.string(),i]),o=e.pipe(e.number(),e.integer(),e.minValue(1),e.maxValue(65535)),s=e.pipe(e.number(),e.integer(),e.minValue(1)),c=e.union([e.string(),e.object({command:e.string(),continuous:e.optional(e.boolean()),readyWhen:e.optional(e.object({type:e.literal(`port`),host:e.optional(e.string()),port:o,timeout:e.optional(s)})),outputDir:e.optional(e.string()),archive:e.optional(e.union([e.boolean(),e.object({enabled:e.boolean(),format:e.optional(e.picklist([`zip`,`tar.gz`]))})])),dependsOn:e.optional(e.array(a))})]),l=e.union([e.string(),e.object({command:e.optional(e.string()),continuous:e.optional(e.boolean()),readyWhen:e.optional(e.object({type:e.literal(`port`),host:e.optional(e.string()),port:o,timeout:e.optional(s)})),outputDir:e.optional(e.string()),archive:e.optional(e.union([e.boolean(),e.object({enabled:e.boolean(),format:e.optional(e.picklist([`zip`,`tar.gz`]))})])),dependsOn:e.optional(e.array(a))})]),u=e.object({root:e.optional(e.string()),targets:e.record(e.string(),c)}),d=e.object({env:n}),f=e.union([e.string(),e.object({project:e.string(),name:e.optional(e.string()),slug:e.optional(e.string()),appId:e.optional(e.string()),suffixes:e.optional(e.record(e.string(),r)),targets:e.optional(e.record(e.string(),l))})]),p=e.object({id:e.optional(e.string()),name:e.optional(e.string()),slug:e.optional(e.string()),appId:e.optional(e.string()),env:n,$env:e.optional(e.record(e.string(),d)),suffixes:e.optional(e.record(e.string(),r)),variants:e.record(e.string(),f)}),m=e.object({suffixes:e.optional(e.record(e.string(),r)),env:n,$env:e.optional(e.record(e.string(),d)),projects:e.record(e.string(),u),products:e.record(e.string(),p),artifacts:e.optional(e.object({root:e.optional(e.string())}))});function h(t){return e.parse(m,t)}export{h as assertMatrixConfig,m as matrixConfigSchema};
1
+ import*as e from"valibot";const t=e.union([e.string(),e.number(),e.boolean()]),n=e.optional(e.record(e.string(),t)),r=e.object({name:e.optional(e.string()),slug:e.optional(e.string()),appId:e.optional(e.string())}),i=e.object({variant:e.string(),target:e.optional(e.string()),condition:e.optional(e.picklist([`completed`,`ready`]))}),a=e.union([e.string(),i]),o=e.pipe(e.number(),e.integer(),e.minValue(1),e.maxValue(65535)),s=e.pipe(e.number(),e.integer(),e.minValue(1)),c=e.union([e.string(),e.object({command:e.string(),continuous:e.optional(e.boolean()),nodeEnv:e.optional(e.picklist([`development`,`production`,`test`])),readyWhen:e.optional(e.object({type:e.literal(`port`),host:e.optional(e.string()),port:o,timeout:e.optional(s)})),outputDir:e.optional(e.string()),archive:e.optional(e.union([e.boolean(),e.object({enabled:e.boolean(),format:e.optional(e.picklist([`zip`,`tar.gz`]))})])),dependsOn:e.optional(e.array(a))})]),l=e.union([e.string(),e.object({command:e.optional(e.string()),continuous:e.optional(e.boolean()),nodeEnv:e.optional(e.picklist([`development`,`production`,`test`])),readyWhen:e.optional(e.object({type:e.literal(`port`),host:e.optional(e.string()),port:o,timeout:e.optional(s)})),outputDir:e.optional(e.string()),archive:e.optional(e.union([e.boolean(),e.object({enabled:e.boolean(),format:e.optional(e.picklist([`zip`,`tar.gz`]))})])),dependsOn:e.optional(e.array(a))})]),u=e.object({root:e.optional(e.string()),targets:e.record(e.string(),c)}),d=e.object({env:n}),f=e.union([e.string(),e.object({project:e.string(),name:e.optional(e.string()),slug:e.optional(e.string()),appId:e.optional(e.string()),suffixes:e.optional(e.record(e.string(),r)),targets:e.optional(e.record(e.string(),l))})]),p=e.object({id:e.optional(e.string()),name:e.optional(e.string()),slug:e.optional(e.string()),appId:e.optional(e.string()),env:n,$env:e.optional(e.record(e.string(),d)),suffixes:e.optional(e.record(e.string(),r)),variants:e.record(e.string(),f)}),m=e.object({suffixes:e.optional(e.record(e.string(),r)),env:n,$env:e.optional(e.record(e.string(),d)),projects:e.record(e.string(),u),products:e.record(e.string(),p),artifacts:e.optional(e.object({root:e.optional(e.string())}))});function h(t){return e.parse(m,t)}export{h as assertMatrixConfig,m as matrixConfigSchema};
package/dist/types.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  //#region src/types.d.ts
2
2
  /** Values that can be passed to a process as environment variables. */
3
3
  export type Scalar = string | number | boolean;
4
+ /** Node.js runtime mode exported as `NODE_ENV` for child processes. */
5
+ export type NodeEnvironment = 'development' | 'production' | 'test';
4
6
  /** A flat environment variable map. */
5
7
  export type EnvMap = Record<string, Scalar>;
6
8
  /** Configuration for a project command such as `dev`, `build`, or `preview`. */
@@ -9,6 +11,8 @@ export interface CommandTarget {
9
11
  command: string;
10
12
  /** Whether the command keeps running after it starts. Defaults from the target name. */
11
13
  continuous?: boolean;
14
+ /** Node.js runtime mode passed to the target process. */
15
+ nodeEnv?: NodeEnvironment;
12
16
  /** Readiness probe used by dependent continuous targets. */
13
17
  readyWhen?: {
14
18
  /** Probe type. Port probing is currently supported. */
@@ -123,6 +127,7 @@ export type MatrixEnvironment = typeof MATRIX_ENVIRONMENTS[number];
123
127
  export type NormalizedTarget = Omit<CommandTarget, 'outputDir' | 'dependsOn'> & {
124
128
  name: string;
125
129
  continuous: boolean;
130
+ nodeEnv: NodeEnvironment;
126
131
  outputDir: string;
127
132
  archive: {
128
133
  enabled: boolean;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xova/matrix",
3
3
  "type": "module",
4
- "version": "0.1.0-rc.0",
4
+ "version": "0.1.1",
5
5
  "description": "A configuration-driven multi-app workspace orchestration engine and CLI.",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/xova-dev/matrix#readme",
@@ -55,6 +55,7 @@
55
55
  "@antfu/eslint-config": "^9.5.1",
56
56
  "@types/archiver": "^6.0.3",
57
57
  "@types/node": "^24.9.2",
58
+ "changelogen": "^0.6.2",
58
59
  "eslint": "^10.10.0",
59
60
  "eslint-plugin-format": "^2.0.1",
60
61
  "prettier": "^3.9.8",
@@ -65,6 +66,8 @@
65
66
  },
66
67
  "scripts": {
67
68
  "build": "tsdown",
69
+ "release:prepare": "changelogen --clean --release --push",
70
+ "release:github": "changelogen gh release",
68
71
  "test:pack": "node scripts/pack-smoke.mjs",
69
72
  "typecheck": "tsc -p tsconfig.json --noEmit",
70
73
  "test": "vitest run",