archicat 0.1.0 → 0.1.2
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 +10 -8
- package/dist/cli/index.mjs +1502 -18
- package/dist/src/index.cjs +112 -1
- package/dist/src/index.d.cts +15 -15
- package/dist/src/index.d.mts +15 -15
- package/dist/src/index.mjs +108 -1
- package/package.json +12 -9
package/dist/cli/index.mjs
CHANGED
|
@@ -1,28 +1,1512 @@
|
|
|
1
|
-
import
|
|
2
|
-
`);if(e.file&&e.start!==void 0){let n=e.file.getLineAndCharacterOfPosition(e.start);return`${e.file.fileName}:${n.line+1}:${n.character+1} - ${t}`}return t}async function ne(e,t=o.configFileName){let i=r.resolve(e,t);if(!n.existsSync(i))throw Error(`Archicat config was not found: ${i}`);let a=await re(i,e);ue(a,i);let s=ie(a);me(s.modules.alias,s.libraries.alias,i);let c=r.resolve(e,s.root),l=r.resolve(c,s.outDir),u=r.resolve(l,o.generated.reportsDirName),d=le(c,s.typescript.tsConfig.extends);return{configFilePath:i,rootDir:c,outDir:l,reportsDir:u,...d?{tsconfigPath:d}:{},resolvedConfig:s}}async function re(e,t){return await a(t,{interopDefault:!0,extensions:[`.js`,`.cjs`,`.mjs`,`.ts`,`.cts`,`.mts`,`.json`]}).import(e,{default:!0})}function ie(e){return{root:e.root??o.root,outDir:e.outDir??o.outDir,typescript:ae(e),alias:{...o.alias,...e.alias??{}},modules:oe(e.modules),libraries:se(e.libraries),apps:ce(e.apps)}}function ae(e){let t=e.typescript?.tsConfig,n=o.typescript.tsConfig,r={include:[...t?.include??n.include],exclude:[...t?.exclude??n.exclude],files:[...t?.files??n.files]};return t?.extends&&(r.extends=t.extends),{tsConfig:r}}function oe(e){return{include:[...e?.include??o.modules.include],alias:e?.alias??o.modules.alias}}function se(e){return{include:[...e?.include??o.libraries.include],alias:e?.alias??o.libraries.alias}}function ce(e){return{include:[...e?.include??o.apps.include]}}function le(e,t){if(t)return u(e,t)}function ue(e,t){if(typeof e!=`object`||!e)throw Error(`Invalid Archicat config: ${t}`);let n=e;m(n.root,`root`,t),m(n.outDir,`outDir`,t),_(n.typescript,`typescript`,t),_(n.modules,`modules`,t),_(n.libraries,`libraries`,t),_(n.apps,`apps`,t),de(n.typescript?.tsConfig,t),h(n.modules?.include,`modules.include`,t),h(n.libraries?.include,`libraries.include`,t),h(n.apps?.include,`apps.include`,t),g(n.modules?.alias,`modules.alias`,t),g(n.libraries?.alias,`libraries.alias`,t),pe(n.alias,`alias`,t)}function de(e,t){if(e===void 0)return;if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Archicat config typescript.tsConfig must be an object: ${t}`);let n=e;m(n.extends,`typescript.tsConfig.extends`,t),h(n.include,`typescript.tsConfig.include`,t),h(n.exclude,`typescript.tsConfig.exclude`,t),h(n.files,`typescript.tsConfig.files`,t),fe(n.compilerOptions,t)}function fe(e,t){if(e===void 0)return;if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Archicat config typescript.tsConfig.compilerOptions must be an object: ${t}`);let n=e;if(Object.hasOwn(n,`paths`))throw Error(`Archicat config typescript.tsConfig.compilerOptions.paths is not supported. Move aliases into archicat.config.ts alias.`);if(Object.hasOwn(n,`baseUrl`))throw Error(`Archicat config typescript.tsConfig.compilerOptions.baseUrl is not supported. Move aliases into archicat.config.ts alias.`);if(Object.keys(n).length>0)throw Error(`Archicat config typescript.tsConfig.compilerOptions is not supported. Put compiler options in the base or app tsconfig.`)}function m(e,t,n){if(e!==void 0&&(typeof e!=`string`||e.trim()===``))throw Error(`Archicat ${t} must be a non-empty string when defined: ${n}`)}function h(e,t,n){if(e!==void 0&&(!Array.isArray(e)||e.some(e=>typeof e!=`string`||e.trim()===``)))throw Error(`Archicat config ${t} must be an array of non-empty strings: ${n}`)}function g(e,t,n){if(e!==void 0&&(typeof e!=`string`||e.trim()===``||e.includes(`*`)||e.endsWith(`/`)))throw Error(`Archicat config ${t} must be a non-empty alias without wildcard or trailing slash: ${n}`)}function pe(e,t,n){if(e!==void 0){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Archicat config ${t} must be an object of non-empty string aliases: ${n}`);for(let[r,i]of Object.entries(e))if(r.trim()===``||typeof i!=`string`||i.trim()===``)throw Error(`Archicat config ${t} must contain non-empty string aliases: ${n}`)}}function _(e,t,n){if(e!==void 0&&(typeof e!=`object`||!e||Array.isArray(e)))throw Error(`Archicat config ${t} must be an object: ${n}`)}function me(e,t,n){if(e===t||e.startsWith(`${t}/`)||t.startsWith(`${e}/`))throw Error(`Archicat module and library aliases must use separate roots: "${e}" and "${t}" (${n})`)}function v(e){return e.split(r.sep).join(`/`)}function he(e,t){let n=r.dirname(e),i=r.parse(t),a=r.join(i.dir,i.name),o=v(r.relative(n,a));return o.startsWith(`.`)||(o=`./${o}`),`${o}.js`}function y(e,t){let n=r.relative(t,e);return n===``||!!n&&!n.startsWith(`..`)&&!r.isAbsolute(n)}function ge(e,t){let n=b(e),r=b(t);if(r===n||!y(r,n))throw Error(`Archicat outDir must be a directory inside the project root: ${t}`)}function b(e){let t=r.resolve(e),i=[],a=t;for(;!n.existsSync(a);){let e=r.dirname(a);if(e===a)return t;i.unshift(r.basename(a)),a=e}return r.join(n.realpathSync(a),...i)}function x(e){return e.replace(/\.(?:js|mjs|cjs|ts|mts|cts|tsx)$/u,``)}function S(e,t){return v(r.relative(e,t))}function C(e,t,n,r=[]){let i=t.flatMap(t=>_e(e,t,n,r));return Array.from(new Set(i)).sort((e,t)=>e.localeCompare(t))}function _e(e,t,i,a){let o=r.resolve(e,t);if(t.includes(`*`))return ye(e,t).filter(e=>!a.some(t=>y(e,t))).filter(e=>r.basename(e)===i);if(!n.existsSync(o))return[];let s=n.statSync(o);return s.isFile()?r.basename(o)===i?[o]:[]:s.isDirectory()?ve(o,i,a):[]}function ve(e,t,i){let a=[],o=n.readdirSync(e,{withFileTypes:!0});for(let n of o){if(xe(n.name))continue;let o=r.join(e,n.name);if(!i.some(e=>y(o,e))){if(n.isDirectory()){a.push(...ve(o,t,i));continue}n.isFile()&&n.name===t&&a.push(o)}}return a}function ye(e,t){let i=r.resolve(e,t).split(r.sep);if(i.filter(e=>e.includes(`*`)).length!==1)throw Error(`Archicat supports exactly one wildcard segment per include pattern: ${t}`);let a=i.findIndex(e=>e.includes(`*`)),o=i.slice(0,a).join(r.sep)||r.sep,s=i[a]??`*`,c=i.slice(a+1),l=be(s);return n.existsSync(o)?n.readdirSync(o,{withFileTypes:!0}).filter(e=>e.isDirectory()).filter(e=>l.test(e.name)).map(e=>r.join(o,e.name,...c)).filter(e=>n.existsSync(e)&&n.statSync(e).isFile()):[]}function be(e){let t=e.replace(/[.+?^${}()|[\]\\]/gu,`\\$&`).replace(/\*/gu,`.*`);return RegExp(`^${t}$`,`u`)}function xe(e){return o.generated.ignoredDirectoryNames.includes(e)}async function w(e,t){switch(t){case`module`:return Se(e);case`library`:return Ce(e);case`app`:return we(e)}}async function Se(e){let t=await T(e,r.dirname(e));return E(t,e,`module`),{kind:`module`,contractFilePath:e,definitionDir:r.dirname(e),contract:t}}async function Ce(e){let t=await T(e,r.dirname(e));return E(t,e,`library`),{kind:`library`,contractFilePath:e,definitionDir:r.dirname(e),contract:t}}async function we(e){let t=await T(e,r.dirname(e));return E(t,e,`app`),{kind:`app`,contractFilePath:e,definitionDir:r.dirname(e),contract:t}}async function T(e,t){return await a(t,{interopDefault:!0,extensions:[`.js`,`.cjs`,`.mjs`,`.ts`,`.cts`,`.mts`,`.json`]}).import(e,{default:!0})}function E(e,t,n){if(typeof e!=`object`||!e)throw Error(`Invalid Archicat ${n} definition: ${t}`);let r=e;if(r.kind!==n)throw Error(`Archicat ${n} file must export a ${n} definition: ${t}`);if(typeof r.name!=`string`||r.name.trim()===``)throw Error(`Archicat ${n} must define a non-empty name: ${t}`);if(n===`app`){k(r.root,`root`,t),O(r.dependencies,t,n);return}let i=r;D(i.api,`api`,t,n),D(i.impl,`impl`,t,n)}function D(e,t,n,r){if(typeof e!=`object`||!e)throw Error(`Archicat ${r}.${t} must be a surface object: ${n}`);k(e.root,`${t}.root`,n),O(e.dependencies,n,`${r}.${t}`)}function O(e,t,n){if(!Array.isArray(e)||e.some(e=>typeof e!=`string`||e.trim()===``))throw Error(`Archicat ${n} dependencies must be an array of non-empty strings: ${t}`)}function k(e,t,n){if(e!==void 0&&(typeof e!=`string`||e.trim()===``))throw Error(`Archicat ${t} must be a non-empty string when defined: ${n}`)}function Te(e){let t=new Map;for(let n of e){let e=t.get(n.from)??[];e.push(n.to),t.set(n.from,e)}let n=new Set,r=new Set;for(let e of t.keys())A(e,t,n,r,[])}function A(e,t,n,r,i){if(!r.has(e)){if(n.has(e)){let t=i.indexOf(e),n=[...i.slice(t<0?0:t),e];throw Error(`Cyclic Archicat dependency detected: ${n.join(` -> `)}`)}n.add(e);for(let a of t.get(e)??[])A(a,t,n,r,[...i,e]);n.delete(e),r.add(e)}}function Ee(e){let t=/^(module|library)\.([a-z][a-z0-9-]*)\.(api|impl)$/u.exec(e);if(t)return{kind:t[1],name:t[2],surface:t[3]}}function j(e,t,n){let r=Ee(t);if(!r)throw Error(`${M(e)} declares invalid dependency target "${t}".`);if(!n.has(t))throw Error(`${M(e)} declares unknown dependency "${t}".`);if(e.target===t)throw Error(`${M(e)} cannot depend on itself: ${t}`);if(e.surface===`api`&&r.surface===`impl`)throw Error(`${M(e)} cannot depend on implementation target "${t}" from an API surface.`)}function M(e){return e.kind===`app`?`App "${e.name}"`:`${De(e.kind)} "${e.name}" ${e.surface}`}function De(e){return`${e.charAt(0).toUpperCase()}${e.slice(1)}`}function Oe(e,t){let n=t.filter(e=>e.kind===`module`).map(t=>ke(e,t)),r=t.filter(e=>e.kind===`library`).map(t=>Ae(e,t)),i=t.filter(e=>e.kind===`app`).map(e=>je(e)),a=[...n,...r];Ne([...a,...i]);let o=Me(a,i);return Pe(a,i,o.targets),Te(o.dependencies),{rootDir:e.rootDir,outDir:e.outDir,reportsDir:e.reportsDir,...e.tsconfigPath?{tsconfigPath:e.tsconfigPath}:{},configFilePath:e.configFilePath,config:e.resolvedConfig,modules:n,libraries:r,apps:i,definitions:a,graph:o}}function ke(e,t){let{contract:n,contractFilePath:i,definitionDir:a}=t;F(n.name,i,`module`);let s=n.api.root?P(a,n.api.root,`api`,n.name):void 0,c=n.impl.root?P(a,n.impl.root,`impl`,n.name):void 0,l=`${e.resolvedConfig.modules.alias}/${n.name}`,u=`${l}/api`,d=`${l}/impl`;return{kind:`module`,name:n.name,apiTarget:`module.${n.name}.api`,implTarget:`module.${n.name}.impl`,alias:u,aliasGlob:`${u}/*`,implAlias:d,implAliasGlob:`${d}/*`,contractFilePath:i,definitionDir:a,api:N(s,n.api.dependencies,r.join(e.outDir,o.generated.modulesDirName,n.name,`api`)),impl:N(c,n.impl.dependencies,r.join(e.outDir,o.generated.modulesDirName,n.name,`impl`))}}function Ae(e,t){let{contract:n,contractFilePath:i,definitionDir:a}=t;F(n.name,i,`library`);let s=n.api.root?P(a,n.api.root,`api`,n.name):void 0,c=n.impl.root?P(a,n.impl.root,`impl`,n.name):void 0,l=`${e.resolvedConfig.libraries.alias}/${n.name}`,u=`${l}/api`,d=`${l}/impl`;return{kind:`library`,name:n.name,apiTarget:`library.${n.name}.api`,implTarget:`library.${n.name}.impl`,alias:u,aliasGlob:`${u}/*`,implAlias:d,implAliasGlob:`${d}/*`,contractFilePath:i,definitionDir:a,api:N(s,n.api.dependencies,r.join(e.outDir,o.generated.librariesDirName,n.name,`api`)),impl:N(c,n.impl.dependencies,r.join(e.outDir,o.generated.librariesDirName,n.name,`impl`))}}function je(e){let{contract:t,contractFilePath:n,definitionDir:r}=e;return F(t.name,n,`app`),{kind:`app`,name:t.name,target:`app.${t.name}`,contractFilePath:n,rootPath:t.root?P(r,t.root,`app`,t.name):r,dependencies:[...t.dependencies]}}function N(e,t,n){return{...e?{rootPath:e}:{},mirrorRootPath:n,dependencies:[...t]}}function P(e,t,i,a){let o=r.resolve(e,t);if(!n.existsSync(o))throw Error(`Definition "${a}" declares ${i} root that does not exist: ${o}`);if(!n.statSync(o).isDirectory())throw Error(`Definition "${a}" declares ${i} root that is not a directory: ${o}`);return o}function Me(e,t){let n=e.flatMap(e=>[{key:e.apiTarget,surface:`api`},{key:e.implTarget,surface:`impl`}]),r=[...e.flatMap(e=>[...e.api.dependencies.map(t=>({from:e.apiTarget,to:t,origin:`declared`})),...e.impl.dependencies.map(t=>({from:e.implTarget,to:t,origin:`declared`}))]),...t.flatMap(e=>e.dependencies.map(t=>({from:e.target,to:t,origin:`declared`})))];return{targets:n,dependencies:[...e.map(e=>({from:e.implTarget,to:e.apiTarget,origin:`derived`})),...r]}}function F(e,t,n){if(!/^[a-z][a-z0-9-]*$/u.test(e))throw Error(`Invalid Archicat ${n} name "${e}" in ${t}. Use ^[a-z][a-z0-9-]*$`)}function Ne(e){let t=new Map;for(let n of e){let e=`${n.kind}.${n.name}`,r=t.get(e);if(r)throw Error(`Duplicate Archicat ${n.kind} name "${n.name}" in ${r} and ${n.contractFilePath}`);t.set(e,n.contractFilePath)}}function Pe(e,t,n){let r=new Set(n.map(e=>e.key));for(let t of e){for(let e of t.api.dependencies)j(Fe(t,`api`),e,r);for(let e of t.impl.dependencies)j(Fe(t,`impl`),e,r)}for(let e of t)for(let t of e.dependencies)j({kind:`app`,name:e.name,surface:`app`,target:e.target},t,r)}function Fe(e,t){return{kind:e.kind,name:e.name,surface:t,target:t===`api`?e.apiTarget:e.implTarget}}async function Ie(e,t){let n=await ne(e,t),r=C(n.rootDir,n.resolvedConfig.modules.include,o.definitions.moduleFileName,[n.outDir]),i=C(n.rootDir,n.resolvedConfig.libraries.include,o.definitions.libraryFileName,[n.outDir]),a=C(n.rootDir,n.resolvedConfig.apps.include,o.definitions.appFileName,[n.outDir]);if(r.length===0&&i.length===0&&a.length===0)throw Error(`No Archicat definition files matched configured include roots.`);return Oe(n,[...await Promise.all(r.map(e=>w(e,`module`))),...await Promise.all(i.map(e=>w(e,`library`))),...await Promise.all(a.map(e=>w(e,`app`)))])}function Le(e){let t=[];Re(t,`Modules`,e.modules.length,e.modules,e.graph.dependencies),Re(t,`Libraries`,e.libraries.length,e.libraries,e.graph.dependencies),t.push(`Apps: ${e.apps.length}`),e.apps.length>0&&t.push(``);for(let n of e.apps)t.push(n.name),t.push(` app: ${n.target}`),I(t,e.graph.dependencies.filter(e=>e.from===n.target),` dependsOn`),t.push(``);return ze(t)}function Re(e,t,n,r,i){e.push(`${t}: ${n}`),r.length>0&&e.push(``);for(let t of r)e.push(t.name),e.push(` api: ${t.apiTarget}`),I(e,i.filter(e=>e.from===t.apiTarget),` api dependsOn`),e.push(` impl: ${t.implTarget}`),I(e,i.filter(e=>e.from===t.implTarget),` impl dependsOn`),e.push(``)}function I(e,t,n){if(t.length===0){e.push(`${n}: none`);return}e.push(`${n}:`);for(let n of t){let t=n.origin===`derived`?` (derived)`:``;e.push(` ${n.to}${t}`)}}function ze(e){for(;e.at(-1)===``;)e.pop();return e}async function Be(e,t){return{exitCode:0,lines:Le(await Ie(t,e.config)).map(e=>({kind:`info`,message:e}))}}function Ve(e){return[...He(e),...Xe(e)]}function He(e){let t=Ze(e);if(!n.existsSync(t))return[`Consumer tsconfig was not found: ${t}`];let r=[],i=Ue(t,r);if(!i)return r;let a=We(i,t,r);return a?(Ge(r,e,i),Ke(r,a),qe(r,a),Je(r,a),Ye(r,i),r):r}function Ue(e,t){try{return s(e)}catch(e){t.push(`Failed to parse consumer tsconfig: ${tt(e)}`);return}}function We(e,t,n){try{return c(e,t)}catch(e){n.push(tt(e));return}}function Ge(e,t,n){let r=$e(t);n.extends!==r&&e.push(`Consumer tsconfig should extend ${r} for generated Archicat aliases to work.`)}function Ke(e,t){let n=t.rootDir;(n===`src`||n===`./src`)&&e.push(`compilerOptions.rootDir is set to src. Generated Archicat files live outside src and may break tsc.`)}function qe(e,t){Object.hasOwn(t,`paths`)&&e.push(`compilerOptions.paths should move to archicat.config.ts alias. Consumer tsconfig paths can override generated Archicat aliases.`)}function Je(e,t){Object.hasOwn(t,`baseUrl`)&&e.push(`compilerOptions.baseUrl is not supported. Move aliases into archicat.config.ts alias.`)}function Ye(e,t){(t.include!==void 0||t.exclude!==void 0||t.files!==void 0)&&e.push(`Consumer tsconfig include/exclude/files should move to archicat.config.ts typescript.tsConfig so generated Archicat types stay included.`)}function Xe(e){let t=[];for(let i of e.definitions){let e=r.join(i.definitionDir,`api`),a=r.join(i.definitionDir,`impl`);!i.api.rootPath&&n.existsSync(e)&&t.push(`${L(i.kind)} "${i.name}" has a physical api directory but its contract omits api.root. Archicat treats the public API as empty.`),!i.impl.rootPath&&n.existsSync(a)&&t.push(`${L(i.kind)} "${i.name}" has a physical impl directory but its contract omits impl.root. Archicat treats the implementation as no-op.`)}return t}function Ze(e){return r.join(e.rootDir,o.typescript.consumerTsconfigFileName)}function Qe(e){return r.join(e.outDir,o.generated.tsconfigFileName)}function $e(e){let t=v(r.relative(e.rootDir,Qe(e)));return et(t)||(t=`./${t}`),t}function et(e){return e.startsWith(`./`)||e.startsWith(`../`)}function tt(e){return e instanceof Error?e.message:String(e)}function L(e){return`${e.charAt(0).toUpperCase()}${e.slice(1)}`}function nt(e,t){ge(e,t),rt(e,t),n.rmSync(t,{recursive:!0,force:!0}),n.mkdirSync(t,{recursive:!0})}function R(e,t){n.mkdirSync(r.dirname(e),{recursive:!0}),n.writeFileSync(e,t,`utf8`)}function z(e,t){R(e,`${JSON.stringify(t,null,2)}\n`)}function rt(e,t){if(!(!n.existsSync(t)||n.readdirSync(t).length===0)&&!it(e,t))throw Error(`Archicat refuses to replace a non-owned outDir: ${t}`)}function it(e,t){let i=r.join(t,o.generated.reportsDirName,o.generated.buildReportFileName);if(!n.existsSync(i))return!1;try{let a=JSON.parse(n.readFileSync(i,`utf8`));return a.generatedBy===`archicat`&&a.schemaVersion===2&&typeof a.outputs?.outDir==`string`&&r.resolve(e,a.outputs.outDir)===r.resolve(t)}catch{return!1}}function at(e){let t=e.graph.targets.map(e=>e.key),n=e.graph.targets.filter(e=>e.surface===`api`).map(e=>e.key),i=o.packageName,a=`import '${i}';
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
import { createJiti } from "jiti";
|
|
6
|
+
import consola from "consola";
|
|
3
7
|
|
|
4
|
-
|
|
5
|
-
|
|
8
|
+
//#region src-internal/configuration/archicat-defaults.ts
|
|
9
|
+
const ArchicatDefaults = {
|
|
10
|
+
packageName: "archicat",
|
|
11
|
+
configFileName: "archicat.config.ts",
|
|
12
|
+
root: ".",
|
|
13
|
+
outDir: ".archicat",
|
|
14
|
+
alias: {},
|
|
15
|
+
definitions: {
|
|
16
|
+
moduleFileName: "archicat.module.ts",
|
|
17
|
+
libraryFileName: "archicat.library.ts",
|
|
18
|
+
appFileName: "archicat.app.ts"
|
|
19
|
+
},
|
|
20
|
+
generated: {
|
|
21
|
+
modulesDirName: "modules",
|
|
22
|
+
librariesDirName: "libraries",
|
|
23
|
+
typesDirName: "types",
|
|
24
|
+
reportsDirName: "reports",
|
|
25
|
+
buildReportFileName: "build.report.json",
|
|
26
|
+
graphReportFileName: "graph.report.json",
|
|
27
|
+
tsconfigFileName: "tsconfig.json",
|
|
28
|
+
typesInclude: "./types/**/*.d.ts",
|
|
29
|
+
ignoredDirectoryNames: [
|
|
30
|
+
"node_modules",
|
|
31
|
+
".git",
|
|
32
|
+
".archicat",
|
|
33
|
+
"dist",
|
|
34
|
+
"build",
|
|
35
|
+
"coverage"
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
typescript: {
|
|
39
|
+
consumerTsconfigFileName: "tsconfig.json",
|
|
40
|
+
tsConfig: {
|
|
41
|
+
include: [],
|
|
42
|
+
exclude: [],
|
|
43
|
+
files: []
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
modules: {
|
|
47
|
+
include: ["./src/modules"],
|
|
48
|
+
alias: "#modules"
|
|
49
|
+
},
|
|
50
|
+
libraries: {
|
|
51
|
+
include: [],
|
|
52
|
+
alias: "#library"
|
|
53
|
+
},
|
|
54
|
+
apps: { include: [] }
|
|
55
|
+
};
|
|
6
56
|
|
|
7
|
-
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src-internal/tsconfig/tsconfig-utils.ts
|
|
59
|
+
function readTsconfigFile(tsconfigPath) {
|
|
60
|
+
const raw = fs.readFileSync(tsconfigPath, "utf8");
|
|
61
|
+
const parsed = ts.parseConfigFileTextToJson(tsconfigPath, raw);
|
|
62
|
+
if (parsed.error) throw new Error(formatTsDiagnostic(parsed.error));
|
|
63
|
+
if (parsed.config == null || typeof parsed.config !== "object" || Array.isArray(parsed.config)) throw new Error(`Invalid tsconfig object: ${tsconfigPath}`);
|
|
64
|
+
return parsed.config;
|
|
65
|
+
}
|
|
66
|
+
function readTsconfigCompilerOptions(tsconfig, source = "tsconfig") {
|
|
67
|
+
const compilerOptions = tsconfig.compilerOptions;
|
|
68
|
+
if (compilerOptions == null) return {};
|
|
69
|
+
if (typeof compilerOptions !== "object" || Array.isArray(compilerOptions)) throw new Error(`Tsconfig compilerOptions must be an object: ${source}`);
|
|
70
|
+
return compilerOptions;
|
|
71
|
+
}
|
|
72
|
+
function readTsconfigExtendsPaths(tsconfig, tsconfigPath) {
|
|
73
|
+
const extended = tsconfig.extends;
|
|
74
|
+
if (extended === void 0) return [];
|
|
75
|
+
if (typeof extended === "string") return [resolveTsconfigExtends(tsconfigPath, extended)];
|
|
76
|
+
if (Array.isArray(extended) && extended.every((entry) => typeof entry === "string")) return extended.map((entry) => resolveTsconfigExtends(tsconfigPath, entry));
|
|
77
|
+
throw new Error(`Tsconfig extends must be a string or string array: ${tsconfigPath}`);
|
|
78
|
+
}
|
|
79
|
+
function resolveProjectTsconfig(rootDir, configuredExtends) {
|
|
80
|
+
if (path.isAbsolute(configuredExtends) || configuredExtends.startsWith(".")) return resolveTsconfigFile(path.resolve(rootDir, configuredExtends), configuredExtends);
|
|
81
|
+
return resolvePackageTsconfig(createRequire(path.join(rootDir, ArchicatDefaults.configFileName)), configuredExtends, configuredExtends);
|
|
82
|
+
}
|
|
83
|
+
function resolveTsconfigExtends(tsconfigPath, extended) {
|
|
84
|
+
if (path.isAbsolute(extended) || extended.startsWith(".")) return resolveTsconfigFile(path.resolve(path.dirname(tsconfigPath), extended), tsconfigPath);
|
|
85
|
+
return resolvePackageTsconfig(createRequire(tsconfigPath), extended, tsconfigPath);
|
|
86
|
+
}
|
|
87
|
+
function resolvePackageTsconfig(resolver, value, source) {
|
|
88
|
+
for (const candidate of makeTsconfigCandidates(value)) try {
|
|
89
|
+
return resolver.resolve(candidate);
|
|
90
|
+
} catch {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`Unable to resolve tsconfig extends "${value}": ${source}`);
|
|
94
|
+
}
|
|
95
|
+
function resolveTsconfigFile(candidate, source) {
|
|
96
|
+
for (const entry of makeTsconfigCandidates(candidate)) if (fs.existsSync(entry) && fs.statSync(entry).isFile()) return entry;
|
|
97
|
+
throw new Error(`Unable to resolve tsconfig extends "${candidate}": ${source}`);
|
|
98
|
+
}
|
|
99
|
+
function makeTsconfigCandidates(value) {
|
|
100
|
+
return [
|
|
101
|
+
value,
|
|
102
|
+
`${value}.json`,
|
|
103
|
+
path.join(value, ArchicatDefaults.typescript.consumerTsconfigFileName)
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
function formatTsDiagnostic(diagnostic) {
|
|
107
|
+
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
|
|
108
|
+
if (diagnostic.file && diagnostic.start !== void 0) {
|
|
109
|
+
const position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
|
|
110
|
+
return `${diagnostic.file.fileName}:${position.line + 1}:${position.character + 1} - ${message}`;
|
|
111
|
+
}
|
|
112
|
+
return message;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src-internal/configuration/load-config.ts
|
|
117
|
+
async function loadArchicatConfig(cwd, configFileName = ArchicatDefaults.configFileName) {
|
|
118
|
+
const configFilePath = path.resolve(cwd, configFileName);
|
|
119
|
+
if (!fs.existsSync(configFilePath)) throw new Error(`Archicat config was not found: ${configFilePath}`);
|
|
120
|
+
const config = await importDefault$1(configFilePath, cwd);
|
|
121
|
+
assertArchicatConfig(config, configFilePath);
|
|
122
|
+
const resolvedConfig = resolveConfig(config);
|
|
123
|
+
assertDistinctReservedAliases(resolvedConfig.modules.alias, resolvedConfig.libraries.alias, configFilePath);
|
|
124
|
+
const rootDir = path.resolve(cwd, resolvedConfig.root);
|
|
125
|
+
const outDir = path.resolve(rootDir, resolvedConfig.outDir);
|
|
126
|
+
const reportsDir = path.resolve(outDir, ArchicatDefaults.generated.reportsDirName);
|
|
127
|
+
const tsconfigPath = resolveTsconfigPath(rootDir, resolvedConfig.typescript.tsConfig.extends);
|
|
128
|
+
return {
|
|
129
|
+
configFilePath,
|
|
130
|
+
rootDir,
|
|
131
|
+
outDir,
|
|
132
|
+
reportsDir,
|
|
133
|
+
...tsconfigPath ? { tsconfigPath } : {},
|
|
134
|
+
resolvedConfig
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async function importDefault$1(filePath, rootDir) {
|
|
138
|
+
return await createJiti(rootDir, {
|
|
139
|
+
interopDefault: true,
|
|
140
|
+
extensions: [
|
|
141
|
+
".js",
|
|
142
|
+
".cjs",
|
|
143
|
+
".mjs",
|
|
144
|
+
".ts",
|
|
145
|
+
".cts",
|
|
146
|
+
".mts",
|
|
147
|
+
".json"
|
|
148
|
+
]
|
|
149
|
+
}).import(filePath, { default: true });
|
|
150
|
+
}
|
|
151
|
+
function resolveConfig(config) {
|
|
152
|
+
return {
|
|
153
|
+
root: config.root ?? ArchicatDefaults.root,
|
|
154
|
+
outDir: config.outDir ?? ArchicatDefaults.outDir,
|
|
155
|
+
typescript: resolveTypeScriptConfig(config),
|
|
156
|
+
alias: {
|
|
157
|
+
...ArchicatDefaults.alias,
|
|
158
|
+
...config.alias ?? {}
|
|
159
|
+
},
|
|
160
|
+
modules: resolveModulesConfig(config.modules),
|
|
161
|
+
libraries: resolveLibrariesConfig(config.libraries),
|
|
162
|
+
apps: resolveAppsConfig(config.apps)
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function resolveTypeScriptConfig(config) {
|
|
166
|
+
const tsConfig = config.typescript?.tsConfig;
|
|
167
|
+
const defaults = ArchicatDefaults.typescript.tsConfig;
|
|
168
|
+
const resolvedTsConfig = {
|
|
169
|
+
include: [...tsConfig?.include ?? defaults.include],
|
|
170
|
+
exclude: [...tsConfig?.exclude ?? defaults.exclude],
|
|
171
|
+
files: [...tsConfig?.files ?? defaults.files]
|
|
172
|
+
};
|
|
173
|
+
if (tsConfig?.extends) resolvedTsConfig.extends = tsConfig.extends;
|
|
174
|
+
return { tsConfig: resolvedTsConfig };
|
|
175
|
+
}
|
|
176
|
+
function resolveModulesConfig(config) {
|
|
177
|
+
return {
|
|
178
|
+
include: [...config?.include ?? ArchicatDefaults.modules.include],
|
|
179
|
+
alias: config?.alias ?? ArchicatDefaults.modules.alias
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function resolveLibrariesConfig(config) {
|
|
183
|
+
return {
|
|
184
|
+
include: [...config?.include ?? ArchicatDefaults.libraries.include],
|
|
185
|
+
alias: config?.alias ?? ArchicatDefaults.libraries.alias
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function resolveAppsConfig(config) {
|
|
189
|
+
return { include: [...config?.include ?? ArchicatDefaults.apps.include] };
|
|
190
|
+
}
|
|
191
|
+
function resolveTsconfigPath(rootDir, configuredExtends) {
|
|
192
|
+
if (!configuredExtends) return;
|
|
193
|
+
return resolveProjectTsconfig(rootDir, configuredExtends);
|
|
194
|
+
}
|
|
195
|
+
function assertArchicatConfig(input, filePath) {
|
|
196
|
+
if (input == null || typeof input !== "object") throw new Error(`Invalid Archicat config: ${filePath}`);
|
|
197
|
+
const config = input;
|
|
198
|
+
assertOptionalNonEmptyString$1(config.root, "root", filePath);
|
|
199
|
+
assertOptionalNonEmptyString$1(config.outDir, "outDir", filePath);
|
|
200
|
+
assertOptionalObject(config.typescript, "typescript", filePath);
|
|
201
|
+
assertOptionalObject(config.modules, "modules", filePath);
|
|
202
|
+
assertOptionalObject(config.libraries, "libraries", filePath);
|
|
203
|
+
assertOptionalObject(config.apps, "apps", filePath);
|
|
204
|
+
assertOptionalTsConfig(config.typescript?.tsConfig, filePath);
|
|
205
|
+
assertOptionalInclude(config.modules?.include, "modules.include", filePath);
|
|
206
|
+
assertOptionalInclude(config.libraries?.include, "libraries.include", filePath);
|
|
207
|
+
assertOptionalInclude(config.apps?.include, "apps.include", filePath);
|
|
208
|
+
assertOptionalImportAlias(config.modules?.alias, "modules.alias", filePath);
|
|
209
|
+
assertOptionalImportAlias(config.libraries?.alias, "libraries.alias", filePath);
|
|
210
|
+
assertOptionalAlias(config.alias, "alias", filePath);
|
|
211
|
+
}
|
|
212
|
+
function assertOptionalTsConfig(value, filePath) {
|
|
213
|
+
if (value === void 0) return;
|
|
214
|
+
if (value == null || typeof value !== "object" || Array.isArray(value)) throw new Error(`Archicat config typescript.tsConfig must be an object: ${filePath}`);
|
|
215
|
+
const config = value;
|
|
216
|
+
assertOptionalNonEmptyString$1(config.extends, "typescript.tsConfig.extends", filePath);
|
|
217
|
+
assertOptionalInclude(config.include, "typescript.tsConfig.include", filePath);
|
|
218
|
+
assertOptionalInclude(config.exclude, "typescript.tsConfig.exclude", filePath);
|
|
219
|
+
assertOptionalInclude(config.files, "typescript.tsConfig.files", filePath);
|
|
220
|
+
assertNoUnsupportedCompilerOptions(config.compilerOptions, filePath);
|
|
221
|
+
}
|
|
222
|
+
function assertNoUnsupportedCompilerOptions(value, filePath) {
|
|
223
|
+
if (value === void 0) return;
|
|
224
|
+
if (value == null || typeof value !== "object" || Array.isArray(value)) throw new Error(`Archicat config typescript.tsConfig.compilerOptions must be an object: ${filePath}`);
|
|
225
|
+
const compilerOptions = value;
|
|
226
|
+
if (Object.hasOwn(compilerOptions, "paths")) throw new Error("Archicat config typescript.tsConfig.compilerOptions.paths is not supported. Move aliases into archicat.config.ts alias.");
|
|
227
|
+
if (Object.hasOwn(compilerOptions, "baseUrl")) throw new Error("Archicat config typescript.tsConfig.compilerOptions.baseUrl is not supported. Move aliases into archicat.config.ts alias.");
|
|
228
|
+
if (Object.keys(compilerOptions).length > 0) throw new Error("Archicat config typescript.tsConfig.compilerOptions is not supported. Put compiler options in the base or app tsconfig.");
|
|
229
|
+
}
|
|
230
|
+
function assertOptionalNonEmptyString$1(value, key, filePath) {
|
|
231
|
+
if (value !== void 0 && (typeof value !== "string" || value.trim() === "")) throw new Error(`Archicat ${key} must be a non-empty string when defined: ${filePath}`);
|
|
232
|
+
}
|
|
233
|
+
function assertOptionalInclude(value, key, filePath) {
|
|
234
|
+
if (value !== void 0 && (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.trim() === ""))) throw new Error(`Archicat config ${key} must be an array of non-empty strings: ${filePath}`);
|
|
235
|
+
}
|
|
236
|
+
function assertOptionalImportAlias(value, key, filePath) {
|
|
237
|
+
if (value === void 0) return;
|
|
238
|
+
if (typeof value !== "string" || value.trim() === "" || value.includes("*") || value.endsWith("/")) throw new Error(`Archicat config ${key} must be a non-empty alias without wildcard or trailing slash: ${filePath}`);
|
|
239
|
+
}
|
|
240
|
+
function assertOptionalAlias(value, key, filePath) {
|
|
241
|
+
if (value === void 0) return;
|
|
242
|
+
if (value == null || typeof value !== "object" || Array.isArray(value)) throw new Error(`Archicat config ${key} must be an object of non-empty string aliases: ${filePath}`);
|
|
243
|
+
for (const [alias, target] of Object.entries(value)) if (alias.trim() === "" || typeof target !== "string" || target.trim() === "") throw new Error(`Archicat config ${key} must contain non-empty string aliases: ${filePath}`);
|
|
244
|
+
}
|
|
245
|
+
function assertOptionalObject(value, key, filePath) {
|
|
246
|
+
if (value !== void 0 && (value == null || typeof value !== "object" || Array.isArray(value))) throw new Error(`Archicat config ${key} must be an object: ${filePath}`);
|
|
247
|
+
}
|
|
248
|
+
function assertDistinctReservedAliases(moduleAlias, libraryAlias, filePath) {
|
|
249
|
+
if (moduleAlias === libraryAlias || moduleAlias.startsWith(`${libraryAlias}/`) || libraryAlias.startsWith(`${moduleAlias}/`)) throw new Error(`Archicat module and library aliases must use separate roots: "${moduleAlias}" and "${libraryAlias}" (${filePath})`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
//#endregion
|
|
253
|
+
//#region src-internal/path/path-utils.ts
|
|
254
|
+
function normalizePath(value) {
|
|
255
|
+
return value.split(path.sep).join("/");
|
|
256
|
+
}
|
|
257
|
+
function toPosixRelativeImport(fromFilePath, targetFilePath) {
|
|
258
|
+
const fromDir = path.dirname(fromFilePath);
|
|
259
|
+
const parsedTarget = path.parse(targetFilePath);
|
|
260
|
+
const targetWithoutExtension = path.join(parsedTarget.dir, parsedTarget.name);
|
|
261
|
+
let relative = normalizePath(path.relative(fromDir, targetWithoutExtension));
|
|
262
|
+
if (!relative.startsWith(".")) relative = `./${relative}`;
|
|
263
|
+
return `${relative}.js`;
|
|
264
|
+
}
|
|
265
|
+
function isPathInside(childPath, parentPath) {
|
|
266
|
+
const relative = path.relative(parentPath, childPath);
|
|
267
|
+
return relative === "" || !!relative && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
268
|
+
}
|
|
269
|
+
function assertSafeGeneratedDirectory(rootDir, directoryPath) {
|
|
270
|
+
const root = resolvePhysicalPath(rootDir);
|
|
271
|
+
const directory = resolvePhysicalPath(directoryPath);
|
|
272
|
+
if (directory === root || !isPathInside(directory, root)) throw new Error(`Archicat outDir must be a directory inside the project root: ${directoryPath}`);
|
|
273
|
+
}
|
|
274
|
+
function resolvePhysicalPath(filePath) {
|
|
275
|
+
const absolutePath = path.resolve(filePath);
|
|
276
|
+
const missing = [];
|
|
277
|
+
let currentPath = absolutePath;
|
|
278
|
+
while (!fs.existsSync(currentPath)) {
|
|
279
|
+
const parentPath = path.dirname(currentPath);
|
|
280
|
+
if (parentPath === currentPath) return absolutePath;
|
|
281
|
+
missing.unshift(path.basename(currentPath));
|
|
282
|
+
currentPath = parentPath;
|
|
283
|
+
}
|
|
284
|
+
return path.join(fs.realpathSync(currentPath), ...missing);
|
|
285
|
+
}
|
|
286
|
+
function stripKnownExtension(filePath) {
|
|
287
|
+
return filePath.replace(/\.(?:js|mjs|cjs|ts|mts|cts|tsx)$/u, "");
|
|
288
|
+
}
|
|
289
|
+
function makeRelativeDisplayPath(rootDir, filePath) {
|
|
290
|
+
return normalizePath(path.relative(rootDir, filePath));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
//#endregion
|
|
294
|
+
//#region src-internal/definitions/discover-definition-files.ts
|
|
295
|
+
function discoverDefinitionFiles(rootDir, includes, markerFileName, excludedRootPaths = []) {
|
|
296
|
+
const files = includes.flatMap((include) => expandInclude(rootDir, include, markerFileName, excludedRootPaths));
|
|
297
|
+
return Array.from(new Set(files)).sort((a, b) => a.localeCompare(b));
|
|
298
|
+
}
|
|
299
|
+
function expandInclude(rootDir, include, markerFileName, excludedRootPaths) {
|
|
300
|
+
const resolved = path.resolve(rootDir, include);
|
|
301
|
+
if (include.includes("*")) return expandSingleStarPattern(rootDir, include).filter((filePath) => !excludedRootPaths.some((excludedRootPath) => isPathInside(filePath, excludedRootPath))).filter((filePath) => path.basename(filePath) === markerFileName);
|
|
302
|
+
if (!fs.existsSync(resolved)) return [];
|
|
303
|
+
const stat = fs.statSync(resolved);
|
|
304
|
+
if (stat.isFile()) return path.basename(resolved) === markerFileName ? [resolved] : [];
|
|
305
|
+
if (!stat.isDirectory()) return [];
|
|
306
|
+
return findMarkerFiles(resolved, markerFileName, excludedRootPaths);
|
|
307
|
+
}
|
|
308
|
+
function findMarkerFiles(rootPath, markerFileName, excludedRootPaths) {
|
|
309
|
+
const result = [];
|
|
310
|
+
const entries = fs.readdirSync(rootPath, { withFileTypes: true });
|
|
311
|
+
for (const entry of entries) {
|
|
312
|
+
if (shouldSkip(entry.name)) continue;
|
|
313
|
+
const entryPath = path.join(rootPath, entry.name);
|
|
314
|
+
if (excludedRootPaths.some((excludedRootPath) => isPathInside(entryPath, excludedRootPath))) continue;
|
|
315
|
+
if (entry.isDirectory()) {
|
|
316
|
+
result.push(...findMarkerFiles(entryPath, markerFileName, excludedRootPaths));
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (entry.isFile() && entry.name === markerFileName) result.push(entryPath);
|
|
320
|
+
}
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
function expandSingleStarPattern(rootDir, pattern) {
|
|
324
|
+
const parts = path.resolve(rootDir, pattern).split(path.sep);
|
|
325
|
+
if (parts.filter((part) => part.includes("*")).length !== 1) throw new Error(`Archicat supports exactly one wildcard segment per include pattern: ${pattern}`);
|
|
326
|
+
const starIndex = parts.findIndex((part) => part.includes("*"));
|
|
327
|
+
const beforeStar = parts.slice(0, starIndex).join(path.sep) || path.sep;
|
|
328
|
+
const wildcard = parts[starIndex] ?? "*";
|
|
329
|
+
const afterStar = parts.slice(starIndex + 1);
|
|
330
|
+
const regexp = makeWildcardRegExp(wildcard);
|
|
331
|
+
if (!fs.existsSync(beforeStar)) return [];
|
|
332
|
+
return fs.readdirSync(beforeStar, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => regexp.test(entry.name)).map((entry) => path.join(beforeStar, entry.name, ...afterStar)).filter((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
|
|
333
|
+
}
|
|
334
|
+
function makeWildcardRegExp(pattern) {
|
|
335
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/gu, "\\$&").replace(/\*/gu, ".*");
|
|
336
|
+
return new RegExp(`^${escaped}$`, "u");
|
|
337
|
+
}
|
|
338
|
+
function shouldSkip(name) {
|
|
339
|
+
return ArchicatDefaults.generated.ignoredDirectoryNames.includes(name);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
//#endregion
|
|
343
|
+
//#region src-internal/definitions/load-definition.ts
|
|
344
|
+
async function loadArchicatDefinition(filePath, kind) {
|
|
345
|
+
switch (kind) {
|
|
346
|
+
case "module": return loadArchicatModule(filePath);
|
|
347
|
+
case "library": return loadArchicatLibrary(filePath);
|
|
348
|
+
case "app": return loadArchicatApp(filePath);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async function loadArchicatModule(filePath) {
|
|
352
|
+
const contract = await importDefault(filePath, path.dirname(filePath));
|
|
353
|
+
assertArchicatDefinition(contract, filePath, "module");
|
|
354
|
+
return {
|
|
355
|
+
kind: "module",
|
|
356
|
+
contractFilePath: filePath,
|
|
357
|
+
definitionDir: path.dirname(filePath),
|
|
358
|
+
contract
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
async function loadArchicatLibrary(filePath) {
|
|
362
|
+
const contract = await importDefault(filePath, path.dirname(filePath));
|
|
363
|
+
assertArchicatDefinition(contract, filePath, "library");
|
|
364
|
+
return {
|
|
365
|
+
kind: "library",
|
|
366
|
+
contractFilePath: filePath,
|
|
367
|
+
definitionDir: path.dirname(filePath),
|
|
368
|
+
contract
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
async function loadArchicatApp(filePath) {
|
|
372
|
+
const contract = await importDefault(filePath, path.dirname(filePath));
|
|
373
|
+
assertArchicatDefinition(contract, filePath, "app");
|
|
374
|
+
return {
|
|
375
|
+
kind: "app",
|
|
376
|
+
contractFilePath: filePath,
|
|
377
|
+
definitionDir: path.dirname(filePath),
|
|
378
|
+
contract
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
async function importDefault(filePath, rootDir) {
|
|
382
|
+
return await createJiti(rootDir, {
|
|
383
|
+
interopDefault: true,
|
|
384
|
+
extensions: [
|
|
385
|
+
".js",
|
|
386
|
+
".cjs",
|
|
387
|
+
".mjs",
|
|
388
|
+
".ts",
|
|
389
|
+
".cts",
|
|
390
|
+
".mts",
|
|
391
|
+
".json"
|
|
392
|
+
]
|
|
393
|
+
}).import(filePath, { default: true });
|
|
394
|
+
}
|
|
395
|
+
function assertArchicatDefinition(input, filePath, expectedKind) {
|
|
396
|
+
if (input == null || typeof input !== "object") throw new Error(`Invalid Archicat ${expectedKind} definition: ${filePath}`);
|
|
397
|
+
const definition = input;
|
|
398
|
+
if (definition.kind !== expectedKind) throw new Error(`Archicat ${expectedKind} file must export a ${expectedKind} definition: ${filePath}`);
|
|
399
|
+
if (typeof definition.name !== "string" || definition.name.trim() === "") throw new Error(`Archicat ${expectedKind} must define a non-empty name: ${filePath}`);
|
|
400
|
+
if (expectedKind === "app") {
|
|
401
|
+
assertOptionalNonEmptyString(definition.root, "root", filePath);
|
|
402
|
+
assertDependencyArray(definition.dependencies, filePath, expectedKind);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const surfaced = definition;
|
|
406
|
+
assertSurface(surfaced.api, "api", filePath, expectedKind);
|
|
407
|
+
assertSurface(surfaced.impl, "impl", filePath, expectedKind);
|
|
408
|
+
}
|
|
409
|
+
function assertSurface(surface, surfaceName, filePath, definitionKind) {
|
|
410
|
+
if (surface == null || typeof surface !== "object") throw new Error(`Archicat ${definitionKind}.${surfaceName} must be a surface object: ${filePath}`);
|
|
411
|
+
assertOptionalNonEmptyString(surface.root, `${surfaceName}.root`, filePath);
|
|
412
|
+
assertDependencyArray(surface.dependencies, filePath, `${definitionKind}.${surfaceName}`);
|
|
413
|
+
}
|
|
414
|
+
function assertDependencyArray(value, filePath, owner) {
|
|
415
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.trim() === "")) throw new Error(`Archicat ${owner} dependencies must be an array of non-empty strings: ${filePath}`);
|
|
416
|
+
}
|
|
417
|
+
function assertOptionalNonEmptyString(value, key, filePath) {
|
|
418
|
+
if (value !== void 0 && (typeof value !== "string" || value.trim() === "")) throw new Error(`Archicat ${key} must be a non-empty string when defined: ${filePath}`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
//#endregion
|
|
422
|
+
//#region src-internal/dependency-graph/detect-dependency-cycles.ts
|
|
423
|
+
function assertNoDependencyCycles(dependencies) {
|
|
424
|
+
const graph = /* @__PURE__ */ new Map();
|
|
425
|
+
for (const dependency of dependencies) {
|
|
426
|
+
const targets = graph.get(dependency.from) ?? [];
|
|
427
|
+
targets.push(dependency.to);
|
|
428
|
+
graph.set(dependency.from, targets);
|
|
429
|
+
}
|
|
430
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
431
|
+
const visited = /* @__PURE__ */ new Set();
|
|
432
|
+
for (const node of graph.keys()) visit(node, graph, visiting, visited, []);
|
|
433
|
+
}
|
|
434
|
+
function visit(node, graph, visiting, visited, stack) {
|
|
435
|
+
if (visited.has(node)) return;
|
|
436
|
+
if (visiting.has(node)) {
|
|
437
|
+
const cycleStart = stack.indexOf(node);
|
|
438
|
+
const cycle = [...stack.slice(cycleStart < 0 ? 0 : cycleStart), node];
|
|
439
|
+
throw new Error(`Cyclic Archicat dependency detected: ${cycle.join(" -> ")}`);
|
|
440
|
+
}
|
|
441
|
+
visiting.add(node);
|
|
442
|
+
for (const target of graph.get(node) ?? []) visit(target, graph, visiting, visited, [...stack, node]);
|
|
443
|
+
visiting.delete(node);
|
|
444
|
+
visited.add(node);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
//#endregion
|
|
448
|
+
//#region src-internal/dependency-graph/parse-dependency-target.ts
|
|
449
|
+
function parseDependencyTarget(target) {
|
|
450
|
+
const match = /^(module|library)\.([a-z][a-z0-9-]*)\.(api|impl)$/u.exec(target);
|
|
451
|
+
if (!match) return;
|
|
452
|
+
return {
|
|
453
|
+
kind: match[1],
|
|
454
|
+
name: match[2],
|
|
455
|
+
surface: match[3]
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
//#endregion
|
|
460
|
+
//#region src-internal/dependency-graph/validate-dependency-rules.ts
|
|
461
|
+
function validateDeclaredDependency(owner, dependency, knownTargets) {
|
|
462
|
+
const target = parseDependencyTarget(dependency);
|
|
463
|
+
if (!target) throw new Error(`${formatOwner$1(owner)} declares invalid dependency target "${dependency}".`);
|
|
464
|
+
if (!knownTargets.has(dependency)) throw new Error(`${formatOwner$1(owner)} declares unknown dependency "${dependency}".`);
|
|
465
|
+
if (owner.target === dependency) throw new Error(`${formatOwner$1(owner)} cannot depend on itself: ${dependency}`);
|
|
466
|
+
if (owner.surface === "api" && target.surface === "impl") throw new Error(`${formatOwner$1(owner)} cannot depend on implementation target "${dependency}" from an API surface.`);
|
|
467
|
+
}
|
|
468
|
+
function formatOwner$1(owner) {
|
|
469
|
+
if (owner.kind === "app") return `App "${owner.name}"`;
|
|
470
|
+
return `${capitalize$2(owner.kind)} "${owner.name}" ${owner.surface}`;
|
|
471
|
+
}
|
|
472
|
+
function capitalize$2(value) {
|
|
473
|
+
return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
//#endregion
|
|
477
|
+
//#region src-internal/resolver/resolve-project.ts
|
|
478
|
+
function resolveArchicatProject(loadedConfig, loadedDefinitions) {
|
|
479
|
+
const modules = loadedDefinitions.filter((definition) => definition.kind === "module").map((definition) => resolveModule(loadedConfig, definition));
|
|
480
|
+
const libraries = loadedDefinitions.filter((definition) => definition.kind === "library").map((definition) => resolveLibrary(loadedConfig, definition));
|
|
481
|
+
const apps = loadedDefinitions.filter((definition) => definition.kind === "app").map((definition) => resolveApp(definition));
|
|
482
|
+
const definitions = [...modules, ...libraries];
|
|
483
|
+
assertUniqueDefinitionNames([...definitions, ...apps]);
|
|
484
|
+
const graph = buildGraph(definitions, apps);
|
|
485
|
+
assertDependencyReferences(definitions, apps, graph.targets);
|
|
486
|
+
assertNoDependencyCycles(graph.dependencies);
|
|
487
|
+
return {
|
|
488
|
+
rootDir: loadedConfig.rootDir,
|
|
489
|
+
outDir: loadedConfig.outDir,
|
|
490
|
+
reportsDir: loadedConfig.reportsDir,
|
|
491
|
+
...loadedConfig.tsconfigPath ? { tsconfigPath: loadedConfig.tsconfigPath } : {},
|
|
492
|
+
configFilePath: loadedConfig.configFilePath,
|
|
493
|
+
config: loadedConfig.resolvedConfig,
|
|
494
|
+
modules,
|
|
495
|
+
libraries,
|
|
496
|
+
apps,
|
|
497
|
+
definitions,
|
|
498
|
+
graph
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function resolveModule(loadedConfig, loadedModule) {
|
|
502
|
+
const { contract, contractFilePath, definitionDir } = loadedModule;
|
|
503
|
+
assertValidName(contract.name, contractFilePath, "module");
|
|
504
|
+
const apiRootPath = contract.api.root ? resolveDeclaredRoot(definitionDir, contract.api.root, "api", contract.name) : void 0;
|
|
505
|
+
const implRootPath = contract.impl.root ? resolveDeclaredRoot(definitionDir, contract.impl.root, "impl", contract.name) : void 0;
|
|
506
|
+
const aliasBase = `${loadedConfig.resolvedConfig.modules.alias}/${contract.name}`;
|
|
507
|
+
const apiAlias = `${aliasBase}/api`;
|
|
508
|
+
const implAlias = `${aliasBase}/impl`;
|
|
509
|
+
return {
|
|
510
|
+
kind: "module",
|
|
511
|
+
name: contract.name,
|
|
512
|
+
apiTarget: `module.${contract.name}.api`,
|
|
513
|
+
implTarget: `module.${contract.name}.impl`,
|
|
514
|
+
alias: apiAlias,
|
|
515
|
+
aliasGlob: `${apiAlias}/*`,
|
|
516
|
+
implAlias,
|
|
517
|
+
implAliasGlob: `${implAlias}/*`,
|
|
518
|
+
contractFilePath,
|
|
519
|
+
definitionDir,
|
|
520
|
+
api: resolveSurface(apiRootPath, contract.api.dependencies, path.join(loadedConfig.outDir, ArchicatDefaults.generated.modulesDirName, contract.name, "api")),
|
|
521
|
+
impl: resolveSurface(implRootPath, contract.impl.dependencies, path.join(loadedConfig.outDir, ArchicatDefaults.generated.modulesDirName, contract.name, "impl"))
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function resolveLibrary(loadedConfig, loadedLibrary) {
|
|
525
|
+
const { contract, contractFilePath, definitionDir } = loadedLibrary;
|
|
526
|
+
assertValidName(contract.name, contractFilePath, "library");
|
|
527
|
+
const apiRootPath = contract.api.root ? resolveDeclaredRoot(definitionDir, contract.api.root, "api", contract.name) : void 0;
|
|
528
|
+
const implRootPath = contract.impl.root ? resolveDeclaredRoot(definitionDir, contract.impl.root, "impl", contract.name) : void 0;
|
|
529
|
+
const aliasBase = `${loadedConfig.resolvedConfig.libraries.alias}/${contract.name}`;
|
|
530
|
+
const apiAlias = `${aliasBase}/api`;
|
|
531
|
+
const implAlias = `${aliasBase}/impl`;
|
|
532
|
+
return {
|
|
533
|
+
kind: "library",
|
|
534
|
+
name: contract.name,
|
|
535
|
+
apiTarget: `library.${contract.name}.api`,
|
|
536
|
+
implTarget: `library.${contract.name}.impl`,
|
|
537
|
+
alias: apiAlias,
|
|
538
|
+
aliasGlob: `${apiAlias}/*`,
|
|
539
|
+
implAlias,
|
|
540
|
+
implAliasGlob: `${implAlias}/*`,
|
|
541
|
+
contractFilePath,
|
|
542
|
+
definitionDir,
|
|
543
|
+
api: resolveSurface(apiRootPath, contract.api.dependencies, path.join(loadedConfig.outDir, ArchicatDefaults.generated.librariesDirName, contract.name, "api")),
|
|
544
|
+
impl: resolveSurface(implRootPath, contract.impl.dependencies, path.join(loadedConfig.outDir, ArchicatDefaults.generated.librariesDirName, contract.name, "impl"))
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function resolveApp(loadedApp) {
|
|
548
|
+
const { contract, contractFilePath, definitionDir } = loadedApp;
|
|
549
|
+
assertValidName(contract.name, contractFilePath, "app");
|
|
550
|
+
return {
|
|
551
|
+
kind: "app",
|
|
552
|
+
name: contract.name,
|
|
553
|
+
target: `app.${contract.name}`,
|
|
554
|
+
contractFilePath,
|
|
555
|
+
rootPath: contract.root ? resolveDeclaredRoot(definitionDir, contract.root, "app", contract.name) : definitionDir,
|
|
556
|
+
dependencies: [...contract.dependencies]
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
function resolveSurface(rootPath, dependencies, mirrorRootPath) {
|
|
560
|
+
return {
|
|
561
|
+
...rootPath ? { rootPath } : {},
|
|
562
|
+
mirrorRootPath,
|
|
563
|
+
dependencies: [...dependencies]
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
function resolveDeclaredRoot(definitionDir, relativeRoot, kind, name) {
|
|
567
|
+
const resolved = path.resolve(definitionDir, relativeRoot);
|
|
568
|
+
if (!fs.existsSync(resolved)) throw new Error(`Definition "${name}" declares ${kind} root that does not exist: ${resolved}`);
|
|
569
|
+
if (!fs.statSync(resolved).isDirectory()) throw new Error(`Definition "${name}" declares ${kind} root that is not a directory: ${resolved}`);
|
|
570
|
+
return resolved;
|
|
571
|
+
}
|
|
572
|
+
function buildGraph(definitions, apps) {
|
|
573
|
+
const targets = definitions.flatMap((definition) => [{
|
|
574
|
+
key: definition.apiTarget,
|
|
575
|
+
surface: "api"
|
|
576
|
+
}, {
|
|
577
|
+
key: definition.implTarget,
|
|
578
|
+
surface: "impl"
|
|
579
|
+
}]);
|
|
580
|
+
const declaredDependencies = [...definitions.flatMap((definition) => [...definition.api.dependencies.map((dependency) => ({
|
|
581
|
+
from: definition.apiTarget,
|
|
582
|
+
to: dependency,
|
|
583
|
+
origin: "declared"
|
|
584
|
+
})), ...definition.impl.dependencies.map((dependency) => ({
|
|
585
|
+
from: definition.implTarget,
|
|
586
|
+
to: dependency,
|
|
587
|
+
origin: "declared"
|
|
588
|
+
}))]), ...apps.flatMap((app) => app.dependencies.map((dependency) => ({
|
|
589
|
+
from: app.target,
|
|
590
|
+
to: dependency,
|
|
591
|
+
origin: "declared"
|
|
592
|
+
})))];
|
|
593
|
+
return {
|
|
594
|
+
targets,
|
|
595
|
+
dependencies: [...definitions.map((definition) => ({
|
|
596
|
+
from: definition.implTarget,
|
|
597
|
+
to: definition.apiTarget,
|
|
598
|
+
origin: "derived"
|
|
599
|
+
})), ...declaredDependencies]
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
function assertValidName(name, filePath, kind) {
|
|
603
|
+
if (!/^[a-z][a-z0-9-]*$/u.test(name)) throw new Error(`Invalid Archicat ${kind} name "${name}" in ${filePath}. Use ^[a-z][a-z0-9-]*$`);
|
|
604
|
+
}
|
|
605
|
+
function assertUniqueDefinitionNames(definitions) {
|
|
606
|
+
const seen = /* @__PURE__ */ new Map();
|
|
607
|
+
for (const definition of definitions) {
|
|
608
|
+
const key = `${definition.kind}.${definition.name}`;
|
|
609
|
+
const current = seen.get(key);
|
|
610
|
+
if (current) throw new Error(`Duplicate Archicat ${definition.kind} name "${definition.name}" in ${current} and ${definition.contractFilePath}`);
|
|
611
|
+
seen.set(key, definition.contractFilePath);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function assertDependencyReferences(definitions, apps, targets) {
|
|
615
|
+
const targetKeys = new Set(targets.map((target) => target.key));
|
|
616
|
+
for (const definition of definitions) {
|
|
617
|
+
for (const dependency of definition.api.dependencies) validateDeclaredDependency(makeOwner(definition, "api"), dependency, targetKeys);
|
|
618
|
+
for (const dependency of definition.impl.dependencies) validateDeclaredDependency(makeOwner(definition, "impl"), dependency, targetKeys);
|
|
619
|
+
}
|
|
620
|
+
for (const app of apps) for (const dependency of app.dependencies) validateDeclaredDependency({
|
|
621
|
+
kind: "app",
|
|
622
|
+
name: app.name,
|
|
623
|
+
surface: "app",
|
|
624
|
+
target: app.target
|
|
625
|
+
}, dependency, targetKeys);
|
|
626
|
+
}
|
|
627
|
+
function makeOwner(definition, surface) {
|
|
628
|
+
return {
|
|
629
|
+
kind: definition.kind,
|
|
630
|
+
name: definition.name,
|
|
631
|
+
surface,
|
|
632
|
+
target: surface === "api" ? definition.apiTarget : definition.implTarget
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
//#endregion
|
|
637
|
+
//#region src-internal/context/load-project.ts
|
|
638
|
+
async function loadArchicatProject(cwd, configFileName) {
|
|
639
|
+
const loadedConfig = await loadArchicatConfig(cwd, configFileName);
|
|
640
|
+
const moduleFiles = discoverDefinitionFiles(loadedConfig.rootDir, loadedConfig.resolvedConfig.modules.include, ArchicatDefaults.definitions.moduleFileName, [loadedConfig.outDir]);
|
|
641
|
+
const libraryFiles = discoverDefinitionFiles(loadedConfig.rootDir, loadedConfig.resolvedConfig.libraries.include, ArchicatDefaults.definitions.libraryFileName, [loadedConfig.outDir]);
|
|
642
|
+
const appFiles = discoverDefinitionFiles(loadedConfig.rootDir, loadedConfig.resolvedConfig.apps.include, ArchicatDefaults.definitions.appFileName, [loadedConfig.outDir]);
|
|
643
|
+
if (moduleFiles.length === 0 && libraryFiles.length === 0 && appFiles.length === 0) throw new Error("No Archicat definition files matched configured include roots.");
|
|
644
|
+
const loadedDefinitions = [
|
|
645
|
+
...await Promise.all(moduleFiles.map((file) => loadArchicatDefinition(file, "module"))),
|
|
646
|
+
...await Promise.all(libraryFiles.map((file) => loadArchicatDefinition(file, "library"))),
|
|
647
|
+
...await Promise.all(appFiles.map((file) => loadArchicatDefinition(file, "app")))
|
|
648
|
+
];
|
|
649
|
+
return resolveArchicatProject(loadedConfig, loadedDefinitions);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
//#endregion
|
|
653
|
+
//#region src-internal/graph/format-project-graph.ts
|
|
654
|
+
function formatProjectGraph(project) {
|
|
655
|
+
const lines = [];
|
|
656
|
+
pushSection(lines, "Modules", project.modules.length, project.modules, project.graph.dependencies);
|
|
657
|
+
pushSection(lines, "Libraries", project.libraries.length, project.libraries, project.graph.dependencies);
|
|
658
|
+
lines.push(`Apps: ${project.apps.length}`);
|
|
659
|
+
if (project.apps.length > 0) lines.push("");
|
|
660
|
+
for (const app of project.apps) {
|
|
661
|
+
lines.push(app.name);
|
|
662
|
+
lines.push(` app: ${app.target}`);
|
|
663
|
+
pushDependencies(lines, project.graph.dependencies.filter((dependency) => dependency.from === app.target), " dependsOn");
|
|
664
|
+
lines.push("");
|
|
665
|
+
}
|
|
666
|
+
return trimTrailingEmptyLines(lines);
|
|
667
|
+
}
|
|
668
|
+
function pushSection(lines, title, count, definitions, dependencies) {
|
|
669
|
+
lines.push(`${title}: ${count}`);
|
|
670
|
+
if (definitions.length > 0) lines.push("");
|
|
671
|
+
for (const definition of definitions) {
|
|
672
|
+
lines.push(definition.name);
|
|
673
|
+
lines.push(` api: ${definition.apiTarget}`);
|
|
674
|
+
pushDependencies(lines, dependencies.filter((dependency) => dependency.from === definition.apiTarget), " api dependsOn");
|
|
675
|
+
lines.push(` impl: ${definition.implTarget}`);
|
|
676
|
+
pushDependencies(lines, dependencies.filter((dependency) => dependency.from === definition.implTarget), " impl dependsOn");
|
|
677
|
+
lines.push("");
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function pushDependencies(lines, dependencies, label) {
|
|
681
|
+
if (dependencies.length === 0) {
|
|
682
|
+
lines.push(`${label}: none`);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
lines.push(`${label}:`);
|
|
686
|
+
for (const dependency of dependencies) {
|
|
687
|
+
const suffix = dependency.origin === "derived" ? " (derived)" : "";
|
|
688
|
+
lines.push(` ${dependency.to}${suffix}`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
function trimTrailingEmptyLines(lines) {
|
|
692
|
+
while (lines.at(-1) === "") lines.pop();
|
|
693
|
+
return lines;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
//#endregion
|
|
697
|
+
//#region src-cli/commands/run-graph-command.ts
|
|
698
|
+
async function runGraphCommand(options, cwd) {
|
|
699
|
+
const project = await loadArchicatProject(cwd, options.config);
|
|
700
|
+
return {
|
|
701
|
+
exitCode: 0,
|
|
702
|
+
lines: formatProjectGraph(project).map((message) => ({
|
|
703
|
+
kind: "info",
|
|
704
|
+
message
|
|
705
|
+
}))
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
//#endregion
|
|
710
|
+
//#region src-cli/output/cli-output.ts
|
|
711
|
+
var CliOutput = class {
|
|
712
|
+
log(message) {
|
|
713
|
+
consola.log(message);
|
|
714
|
+
}
|
|
715
|
+
info(message) {
|
|
716
|
+
consola.info(message);
|
|
717
|
+
}
|
|
718
|
+
success(message) {
|
|
719
|
+
consola.success(message);
|
|
720
|
+
}
|
|
721
|
+
warning(message) {
|
|
722
|
+
consola.warn(message);
|
|
723
|
+
}
|
|
724
|
+
error(message) {
|
|
725
|
+
consola.error(message);
|
|
726
|
+
}
|
|
727
|
+
emptyLine() {
|
|
728
|
+
consola.log("");
|
|
729
|
+
}
|
|
730
|
+
title(product, command, rows = []) {
|
|
731
|
+
consola.log([`${product} ${command}`, ...formatRows(rows)].join("\n"));
|
|
732
|
+
}
|
|
733
|
+
panel(title, rows, badge) {
|
|
734
|
+
const heading = badge === void 0 ? title : `${title} (${badge})`;
|
|
735
|
+
consola.log([heading, ...formatRows(rows)].join("\n"));
|
|
736
|
+
}
|
|
737
|
+
step(label, message) {
|
|
738
|
+
return `${label}: ${message}`;
|
|
739
|
+
}
|
|
740
|
+
duration(milliseconds) {
|
|
741
|
+
if (milliseconds < 1e3) return `${milliseconds}ms`;
|
|
742
|
+
const seconds = milliseconds / 1e3;
|
|
743
|
+
return `${seconds.toFixed(seconds < 10 ? 2 : 1)}s`;
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
function formatRows(rows) {
|
|
747
|
+
if (rows.length === 0) return [];
|
|
748
|
+
const width = Math.max(...rows.map((row) => row.label.length));
|
|
749
|
+
return rows.map((row) => ` ${row.label.padEnd(width)} ${String(row.value)}`);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
//#endregion
|
|
753
|
+
//#region src-internal/doctor/doctor.ts
|
|
754
|
+
function doctorProject(project) {
|
|
755
|
+
return checkPhysicalOmissions(project);
|
|
756
|
+
}
|
|
757
|
+
function checkPhysicalOmissions(project) {
|
|
758
|
+
const issues = [];
|
|
759
|
+
for (const definition of project.definitions) {
|
|
760
|
+
const apiPath = path.join(definition.definitionDir, "api");
|
|
761
|
+
const implPath = path.join(definition.definitionDir, "impl");
|
|
762
|
+
if (!definition.api.rootPath && fs.existsSync(apiPath)) issues.push(`${capitalize$1(definition.kind)} "${definition.name}" has a physical api directory but its contract omits api.root. Archicat treats the public API as empty.`);
|
|
763
|
+
if (!definition.impl.rootPath && fs.existsSync(implPath)) issues.push(`${capitalize$1(definition.kind)} "${definition.name}" has a physical impl directory but its contract omits impl.root. Archicat treats the implementation as no-op.`);
|
|
764
|
+
}
|
|
765
|
+
return issues;
|
|
766
|
+
}
|
|
767
|
+
function capitalize$1(value) {
|
|
768
|
+
return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
//#endregion
|
|
772
|
+
//#region src-internal/generator/file-writer.ts
|
|
773
|
+
function resetDirectory(rootDir, directoryPath) {
|
|
774
|
+
assertSafeGeneratedDirectory(rootDir, directoryPath);
|
|
775
|
+
assertReplaceableDirectory(rootDir, directoryPath);
|
|
776
|
+
fs.rmSync(directoryPath, {
|
|
777
|
+
recursive: true,
|
|
778
|
+
force: true
|
|
779
|
+
});
|
|
780
|
+
fs.mkdirSync(directoryPath, { recursive: true });
|
|
781
|
+
}
|
|
782
|
+
function writeTextFile(filePath, content) {
|
|
783
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
784
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
785
|
+
}
|
|
786
|
+
function writeJsonFile(filePath, value) {
|
|
787
|
+
writeTextFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
788
|
+
}
|
|
789
|
+
function assertReplaceableDirectory(rootDir, directoryPath) {
|
|
790
|
+
if (!fs.existsSync(directoryPath) || fs.readdirSync(directoryPath).length === 0) return;
|
|
791
|
+
if (isOwnedGeneratedDirectory(rootDir, directoryPath)) return;
|
|
792
|
+
throw new Error(`Archicat refuses to replace a non-owned outDir: ${directoryPath}`);
|
|
793
|
+
}
|
|
794
|
+
function isOwnedGeneratedDirectory(rootDir, directoryPath) {
|
|
795
|
+
const reportPath = path.join(directoryPath, ArchicatDefaults.generated.reportsDirName, ArchicatDefaults.generated.buildReportFileName);
|
|
796
|
+
if (!fs.existsSync(reportPath)) return false;
|
|
797
|
+
try {
|
|
798
|
+
const report = JSON.parse(fs.readFileSync(reportPath, "utf8"));
|
|
799
|
+
return report.generatedBy === "archicat" && report.schemaVersion === 2 && typeof report.outputs?.outDir === "string" && path.resolve(rootDir, report.outputs.outDir) === path.resolve(directoryPath);
|
|
800
|
+
} catch {
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
//#endregion
|
|
806
|
+
//#region src-internal/generator/generate-graph-types.ts
|
|
807
|
+
function generateGraphTypes(project) {
|
|
808
|
+
const allTargets = project.graph.targets.map((target) => target.key);
|
|
809
|
+
const apiTargets = project.graph.targets.filter((target) => target.surface === "api").map((target) => target.key);
|
|
810
|
+
const packageName = ArchicatDefaults.packageName;
|
|
811
|
+
const content = `import '${packageName}';
|
|
812
|
+
|
|
813
|
+
declare module '${packageName}' {
|
|
814
|
+
${renderInterface("ArchicatModuleApiDependencies", apiTargets)}
|
|
8
815
|
|
|
9
|
-
${
|
|
816
|
+
${renderInterface("ArchicatModuleImplDependencies", allTargets)}
|
|
10
817
|
|
|
11
|
-
${
|
|
818
|
+
${renderInterface("ArchicatLibraryApiDependencies", apiTargets)}
|
|
12
819
|
|
|
13
|
-
${
|
|
820
|
+
${renderInterface("ArchicatLibraryImplDependencies", allTargets)}
|
|
821
|
+
|
|
822
|
+
${renderInterface("ArchicatAppDependencies", allTargets)}
|
|
14
823
|
}
|
|
15
824
|
|
|
16
825
|
export {};
|
|
17
|
-
`;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
826
|
+
`;
|
|
827
|
+
writeTextFile(path.join(project.outDir, ArchicatDefaults.generated.typesDirName, "graph.d.ts"), content);
|
|
828
|
+
}
|
|
829
|
+
function renderInterface(name, entries) {
|
|
830
|
+
return ` interface ${name} {\n${Array.from(new Set(entries)).sort((a, b) => a.localeCompare(b)).map((entry) => ` '${entry}': true;`).join("\n")}\n }`;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
//#endregion
|
|
834
|
+
//#region src-internal/scanner/file-scanner.ts
|
|
835
|
+
function listTypeScriptFiles(rootPath) {
|
|
836
|
+
if (!fs.existsSync(rootPath)) return [];
|
|
837
|
+
if (fs.statSync(rootPath).isFile()) return isTypeScriptSourceFile(rootPath) ? [rootPath] : [];
|
|
838
|
+
return walk(rootPath).filter(isTypeScriptSourceFile).sort((a, b) => a.localeCompare(b));
|
|
839
|
+
}
|
|
840
|
+
function isTypeScriptSourceFile(filePath) {
|
|
841
|
+
return /\.(?:ts|mts|cts|tsx)$/u.test(filePath) && !/\.d\.(?:ts|mts|cts)$/u.test(filePath);
|
|
842
|
+
}
|
|
843
|
+
function walk(rootPath) {
|
|
844
|
+
const result = [];
|
|
845
|
+
const entries = fs.readdirSync(rootPath, { withFileTypes: true });
|
|
846
|
+
for (const entry of entries) {
|
|
847
|
+
const entryPath = path.join(rootPath, entry.name);
|
|
848
|
+
if (entry.isDirectory()) {
|
|
849
|
+
result.push(...walk(entryPath));
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
if (entry.isFile()) result.push(entryPath);
|
|
853
|
+
}
|
|
854
|
+
return result;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
//#endregion
|
|
858
|
+
//#region src-internal/scanner/import-scanner.ts
|
|
859
|
+
function scanImports(filePath) {
|
|
860
|
+
const source = ts.createSourceFile(filePath, fs.readFileSync(filePath, "utf8"), ts.ScriptTarget.Latest, true);
|
|
861
|
+
const imports = [];
|
|
862
|
+
const visit = (node) => {
|
|
863
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) imports.push({
|
|
864
|
+
moduleSpecifier: node.moduleSpecifier.text,
|
|
865
|
+
kind: "import"
|
|
866
|
+
});
|
|
867
|
+
if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) imports.push({
|
|
868
|
+
moduleSpecifier: node.moduleSpecifier.text,
|
|
869
|
+
kind: "export"
|
|
870
|
+
});
|
|
871
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
872
|
+
const [argument] = node.arguments;
|
|
873
|
+
if (argument && ts.isStringLiteral(argument)) imports.push({
|
|
874
|
+
moduleSpecifier: argument.text,
|
|
875
|
+
kind: "dynamic-import"
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
ts.forEachChild(node, visit);
|
|
879
|
+
};
|
|
880
|
+
visit(source);
|
|
881
|
+
return imports;
|
|
882
|
+
}
|
|
883
|
+
function hasDefaultExport(filePath) {
|
|
884
|
+
const source = ts.createSourceFile(filePath, fs.readFileSync(filePath, "utf8"), ts.ScriptTarget.Latest, true);
|
|
885
|
+
let found = false;
|
|
886
|
+
const visit = (node) => {
|
|
887
|
+
if (found) return;
|
|
888
|
+
if (ts.isExportAssignment(node) && !node.isExportEquals) {
|
|
889
|
+
found = true;
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
if (hasExportDefaultModifiers(node)) {
|
|
893
|
+
found = true;
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (ts.isExportDeclaration(node) && node.exportClause && ts.isNamedExports(node.exportClause)) for (const element of node.exportClause.elements) {
|
|
897
|
+
const exportedName = element.name.text;
|
|
898
|
+
const sourceName = element.propertyName?.text;
|
|
899
|
+
if (exportedName === "default" || sourceName === "default") {
|
|
900
|
+
found = true;
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
ts.forEachChild(node, visit);
|
|
905
|
+
};
|
|
906
|
+
visit(source);
|
|
907
|
+
return found;
|
|
908
|
+
}
|
|
909
|
+
function hasExportDefaultModifiers(node) {
|
|
910
|
+
const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0;
|
|
911
|
+
if (!modifiers) return false;
|
|
912
|
+
const hasExport = modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
913
|
+
const hasDefault = modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword);
|
|
914
|
+
return hasExport && hasDefault;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
//#endregion
|
|
918
|
+
//#region src-internal/generator/generate-mirrors.ts
|
|
919
|
+
function generateMirrors(definitions) {
|
|
920
|
+
for (const definition of definitions) {
|
|
921
|
+
generateApiMirror(definition.api);
|
|
922
|
+
generateImplMirror(definition);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
function generateApiMirror(surface) {
|
|
926
|
+
if (!surface.rootPath) {
|
|
927
|
+
writeEmptyMirror(path.join(surface.mirrorRootPath, "index.ts"));
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
const mirroredPaths = /* @__PURE__ */ new Set();
|
|
931
|
+
for (const sourceFilePath of listTypeScriptFiles(surface.rootPath)) {
|
|
932
|
+
const relativePath = normalizePath(path.relative(surface.rootPath, sourceFilePath));
|
|
933
|
+
mirroredPaths.add(relativePath);
|
|
934
|
+
writeMirrorFile(path.join(surface.mirrorRootPath, relativePath), sourceFilePath);
|
|
935
|
+
}
|
|
936
|
+
if (!mirroredPaths.has("index.ts")) writeEmptyMirror(path.join(surface.mirrorRootPath, "index.ts"));
|
|
937
|
+
}
|
|
938
|
+
function generateImplMirror(definition) {
|
|
939
|
+
const indexPath = definition.impl.rootPath ? findIndexFile(definition.impl.rootPath) : void 0;
|
|
940
|
+
if (indexPath) {
|
|
941
|
+
writeMirrorFile(path.join(definition.impl.mirrorRootPath, "index.ts"), indexPath);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
const constName = definition.kind === "module" ? "ArchicatModuleImplementation" : "ArchicatLibraryImplementation";
|
|
945
|
+
const content = `${makeMirrorHeader()}
|
|
946
|
+
export const ${constName} = {
|
|
947
|
+
name: '${definition.name}',
|
|
21
948
|
} as const;
|
|
22
949
|
|
|
23
|
-
export default ${
|
|
24
|
-
`;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
950
|
+
export default ${constName};
|
|
951
|
+
`;
|
|
952
|
+
writeTextFile(path.join(definition.impl.mirrorRootPath, "index.ts"), content);
|
|
953
|
+
}
|
|
954
|
+
function writeEmptyMirror(filePath) {
|
|
955
|
+
writeTextFile(filePath, `${makeMirrorHeader()}export {};\n`);
|
|
956
|
+
}
|
|
957
|
+
function writeMirrorFile(targetFilePath, sourceFilePath) {
|
|
958
|
+
const sourceImport = toPosixRelativeImport(targetFilePath, sourceFilePath);
|
|
959
|
+
const defaultExport = hasDefaultExport(sourceFilePath) ? `export { default } from '${sourceImport}';\n` : "";
|
|
960
|
+
writeTextFile(targetFilePath, `${makeMirrorHeader()}
|
|
961
|
+
export * from '${sourceImport}';
|
|
962
|
+
${defaultExport}`);
|
|
963
|
+
}
|
|
964
|
+
function findIndexFile(rootPath) {
|
|
965
|
+
for (const fileName of [
|
|
966
|
+
"index.ts",
|
|
967
|
+
"index.mts",
|
|
968
|
+
"index.cts",
|
|
969
|
+
"index.tsx"
|
|
970
|
+
]) {
|
|
971
|
+
const filePath = path.join(rootPath, fileName);
|
|
972
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) return filePath;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
function makeMirrorHeader() {
|
|
976
|
+
return "// Mirrored by Archicat.\n";
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
//#endregion
|
|
980
|
+
//#region src-internal/generator/generate-report.ts
|
|
981
|
+
function generateReport(project) {
|
|
982
|
+
writeJsonFile(path.join(project.reportsDir, ArchicatDefaults.generated.buildReportFileName), makeBuildReport(project));
|
|
983
|
+
writeJsonFile(path.join(project.reportsDir, ArchicatDefaults.generated.graphReportFileName), makeGraphReport(project));
|
|
984
|
+
}
|
|
985
|
+
function makeBuildReport(project) {
|
|
986
|
+
return {
|
|
987
|
+
generatedBy: "archicat",
|
|
988
|
+
schemaVersion: 2,
|
|
989
|
+
aliases: {
|
|
990
|
+
module: project.config.modules.alias,
|
|
991
|
+
library: project.config.libraries.alias
|
|
992
|
+
},
|
|
993
|
+
outputs: {
|
|
994
|
+
outDir: makeRelativeDisplayPath(project.rootDir, project.outDir),
|
|
995
|
+
reportsDir: makeRelativeDisplayPath(project.rootDir, project.reportsDir)
|
|
996
|
+
},
|
|
997
|
+
targets: project.graph.targets.map((target) => target.key),
|
|
998
|
+
definitions: [...project.definitions.map((definition) => makeDefinitionReport(project, definition)), ...project.apps.map((app) => ({
|
|
999
|
+
kind: app.kind,
|
|
1000
|
+
name: app.name,
|
|
1001
|
+
targets: { app: app.target },
|
|
1002
|
+
aliases: {},
|
|
1003
|
+
dependencies: app.dependencies,
|
|
1004
|
+
contractFilePath: makeRelativeDisplayPath(project.rootDir, app.contractFilePath),
|
|
1005
|
+
source: { root: makeRelativeDisplayPath(project.rootDir, app.rootPath) },
|
|
1006
|
+
mirror: {}
|
|
1007
|
+
}))],
|
|
1008
|
+
dependencies: project.graph.dependencies
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
function makeGraphReport(project) {
|
|
1012
|
+
return {
|
|
1013
|
+
generatedBy: "archicat",
|
|
1014
|
+
schemaVersion: 1,
|
|
1015
|
+
targets: project.graph.targets.map((target) => target.key),
|
|
1016
|
+
dependencies: project.graph.dependencies
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
function makeDefinitionReport(project, definition) {
|
|
1020
|
+
return {
|
|
1021
|
+
kind: definition.kind,
|
|
1022
|
+
name: definition.name,
|
|
1023
|
+
targets: {
|
|
1024
|
+
api: definition.apiTarget,
|
|
1025
|
+
impl: definition.implTarget
|
|
1026
|
+
},
|
|
1027
|
+
aliases: {
|
|
1028
|
+
api: definition.alias,
|
|
1029
|
+
impl: definition.implAlias
|
|
1030
|
+
},
|
|
1031
|
+
dependencies: {
|
|
1032
|
+
api: definition.api.dependencies,
|
|
1033
|
+
impl: definition.impl.dependencies
|
|
1034
|
+
},
|
|
1035
|
+
contractFilePath: makeRelativeDisplayPath(project.rootDir, definition.contractFilePath),
|
|
1036
|
+
source: {
|
|
1037
|
+
root: makeRelativeDisplayPath(project.rootDir, definition.definitionDir),
|
|
1038
|
+
api: definition.api.rootPath ? makeRelativeDisplayPath(project.rootDir, definition.api.rootPath) : void 0,
|
|
1039
|
+
impl: definition.impl.rootPath ? makeRelativeDisplayPath(project.rootDir, definition.impl.rootPath) : void 0
|
|
1040
|
+
},
|
|
1041
|
+
mirror: {
|
|
1042
|
+
api: makeRelativeDisplayPath(project.rootDir, definition.api.mirrorRootPath),
|
|
1043
|
+
impl: makeRelativeDisplayPath(project.rootDir, definition.impl.mirrorRootPath)
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
//#endregion
|
|
1049
|
+
//#region src-internal/generator/generate-tsconfig.ts
|
|
1050
|
+
function generateTsconfig(project) {
|
|
1051
|
+
assertSupportedBaseTsconfigChain(project.tsconfigPath);
|
|
1052
|
+
writeJsonFile(path.join(project.outDir, ArchicatDefaults.generated.tsconfigFileName), makeGeneratedTsconfig(project));
|
|
1053
|
+
}
|
|
1054
|
+
function makeGeneratedTsconfig(project) {
|
|
1055
|
+
const tsconfig = {
|
|
1056
|
+
compilerOptions: makeCompilerOptions(project),
|
|
1057
|
+
include: makeInclude(project)
|
|
1058
|
+
};
|
|
1059
|
+
const extendedTsconfig = makeExtends(project);
|
|
1060
|
+
const exclude = makeExclude(project);
|
|
1061
|
+
const files = makeFiles(project);
|
|
1062
|
+
if (extendedTsconfig) tsconfig.extends = extendedTsconfig;
|
|
1063
|
+
if (exclude.length > 0) tsconfig.exclude = exclude;
|
|
1064
|
+
if (files.length > 0) tsconfig.files = files;
|
|
1065
|
+
return tsconfig;
|
|
1066
|
+
}
|
|
1067
|
+
function makeCompilerOptions(project) {
|
|
1068
|
+
const userAliasPaths = makeConfiguredAliasPaths(project);
|
|
1069
|
+
const archicatPaths = makeArchicatAliasPaths(project);
|
|
1070
|
+
assertNoAliasConflicts(project, userAliasPaths, archicatPaths);
|
|
1071
|
+
return { paths: {
|
|
1072
|
+
...userAliasPaths,
|
|
1073
|
+
...archicatPaths
|
|
1074
|
+
} };
|
|
1075
|
+
}
|
|
1076
|
+
function makeExtends(project) {
|
|
1077
|
+
return project.tsconfigPath ? makeRelativeTsconfigPath(project.outDir, project.tsconfigPath) : void 0;
|
|
1078
|
+
}
|
|
1079
|
+
function makeInclude(project) {
|
|
1080
|
+
return unique([...rewriteProjectPaths(project, project.config.typescript.tsConfig.include), ArchicatDefaults.generated.typesInclude]);
|
|
1081
|
+
}
|
|
1082
|
+
function makeExclude(project) {
|
|
1083
|
+
return rewriteProjectPaths(project, project.config.typescript.tsConfig.exclude);
|
|
1084
|
+
}
|
|
1085
|
+
function makeFiles(project) {
|
|
1086
|
+
return rewriteProjectPaths(project, project.config.typescript.tsConfig.files);
|
|
1087
|
+
}
|
|
1088
|
+
function makeArchicatAliasPaths(project) {
|
|
1089
|
+
const paths = {};
|
|
1090
|
+
for (const definition of project.definitions) {
|
|
1091
|
+
if (definition.api.rootPath) paths[definition.aliasGlob] = [makeRelativeTsconfigPath(project.outDir, path.join(definition.api.rootPath, "*"))];
|
|
1092
|
+
if (definition.impl.rootPath) paths[definition.implAliasGlob] = [makeRelativeTsconfigPath(project.outDir, path.join(definition.impl.rootPath, "*"))];
|
|
1093
|
+
}
|
|
1094
|
+
return paths;
|
|
1095
|
+
}
|
|
1096
|
+
function makeConfiguredAliasPaths(project) {
|
|
1097
|
+
const result = {};
|
|
1098
|
+
for (const [alias, target] of Object.entries(project.config.alias)) {
|
|
1099
|
+
const absolute = path.isAbsolute(target) ? target : path.resolve(project.rootDir, target);
|
|
1100
|
+
result[alias] = [makeRelativeTsconfigPath(project.outDir, absolute)];
|
|
1101
|
+
}
|
|
1102
|
+
return result;
|
|
1103
|
+
}
|
|
1104
|
+
function rewriteProjectPaths(project, values) {
|
|
1105
|
+
return values.map((value) => {
|
|
1106
|
+
const absolute = path.isAbsolute(value) ? value : path.resolve(project.rootDir, value);
|
|
1107
|
+
return makeRelativeTsconfigPath(project.outDir, absolute);
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
function makeRelativeTsconfigPath(fromDir, targetPath) {
|
|
1111
|
+
let relative = normalizePath(path.relative(fromDir, targetPath));
|
|
1112
|
+
if (!relative.startsWith(".")) relative = `./${relative}`;
|
|
1113
|
+
return relative;
|
|
1114
|
+
}
|
|
1115
|
+
function unique(values) {
|
|
1116
|
+
return [...new Set(values)];
|
|
1117
|
+
}
|
|
1118
|
+
function assertSupportedBaseTsconfigChain(tsconfigPath) {
|
|
1119
|
+
if (!tsconfigPath) return;
|
|
1120
|
+
validateBaseTsconfig(tsconfigPath, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
|
|
1121
|
+
}
|
|
1122
|
+
function validateBaseTsconfig(tsconfigPath, visiting, visited) {
|
|
1123
|
+
const resolvedPath = path.resolve(tsconfigPath);
|
|
1124
|
+
if (visited.has(resolvedPath)) return;
|
|
1125
|
+
if (visiting.has(resolvedPath)) throw new Error(`Circular tsconfig extends chain detected: ${resolvedPath}`);
|
|
1126
|
+
visiting.add(resolvedPath);
|
|
1127
|
+
const config = readTsconfigFile(resolvedPath);
|
|
1128
|
+
const compilerOptions = readTsconfigCompilerOptions(config, resolvedPath);
|
|
1129
|
+
assertNoBaseTsconfigPaths(compilerOptions, resolvedPath);
|
|
1130
|
+
assertNoBaseTsconfigBaseUrl(compilerOptions, resolvedPath);
|
|
1131
|
+
for (const extendedPath of readTsconfigExtendsPaths(config, resolvedPath)) validateBaseTsconfig(extendedPath, visiting, visited);
|
|
1132
|
+
visiting.delete(resolvedPath);
|
|
1133
|
+
visited.add(resolvedPath);
|
|
1134
|
+
}
|
|
1135
|
+
function assertNoBaseTsconfigPaths(compilerOptions, tsconfigPath) {
|
|
1136
|
+
if (Object.hasOwn(compilerOptions, "paths")) throw new Error(`Base tsconfig compilerOptions.paths is not supported by Archicat. Move aliases into archicat.config.ts alias: ${tsconfigPath}`);
|
|
1137
|
+
}
|
|
1138
|
+
function assertNoBaseTsconfigBaseUrl(compilerOptions, tsconfigPath) {
|
|
1139
|
+
if (Object.hasOwn(compilerOptions, "baseUrl")) throw new Error(`Base tsconfig compilerOptions.baseUrl is not supported by Archicat. Move aliases into archicat.config.ts alias: ${tsconfigPath}`);
|
|
1140
|
+
}
|
|
1141
|
+
function assertNoAliasConflicts(project, userAliases, archicatPaths) {
|
|
1142
|
+
const configuredAliases = Object.keys(userAliases);
|
|
1143
|
+
const archicatAliases = Object.keys(archicatPaths);
|
|
1144
|
+
const reservedAliases = [project.config.modules.alias, project.config.libraries.alias];
|
|
1145
|
+
for (const alias of configuredAliases) {
|
|
1146
|
+
if (archicatAliases.includes(alias)) throw new Error(`Alias conflict: archicat.config.ts alias already defines "${alias}", but Archicat needs it.`);
|
|
1147
|
+
for (const reservedAlias of reservedAliases) if (alias === reservedAlias || alias === `${reservedAlias}/*` || alias.startsWith(`${reservedAlias}/`)) throw new Error(`Alias conflict: archicat.config.ts alias "${alias}" is inside Archicat reserved alias "${reservedAlias}". Remove the alias or configure another module/library alias.`);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
//#endregion
|
|
1152
|
+
//#region src-internal/generator/generator.ts
|
|
1153
|
+
function generateArtifacts(project) {
|
|
1154
|
+
resetDirectory(project.rootDir, project.outDir);
|
|
1155
|
+
fs.mkdirSync(path.join(project.outDir, ArchicatDefaults.generated.modulesDirName), { recursive: true });
|
|
1156
|
+
fs.mkdirSync(path.join(project.outDir, ArchicatDefaults.generated.librariesDirName), { recursive: true });
|
|
1157
|
+
fs.mkdirSync(path.join(project.outDir, ArchicatDefaults.generated.typesDirName), { recursive: true });
|
|
1158
|
+
fs.mkdirSync(project.reportsDir, { recursive: true });
|
|
1159
|
+
generateMirrors(project.definitions);
|
|
1160
|
+
generateGraphTypes(project);
|
|
1161
|
+
generateTsconfig(project);
|
|
1162
|
+
generateReport(project);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
//#endregion
|
|
1166
|
+
//#region src-internal/validator/validator.ts
|
|
1167
|
+
function validateProject(project) {
|
|
1168
|
+
const graph = makeDependencyGraph(project.graph.dependencies);
|
|
1169
|
+
const violations = [];
|
|
1170
|
+
const sourceFiles = [...project.definitions.flatMap((definition) => [...definition.api.rootPath ? listTypeScriptFiles(definition.api.rootPath) : [], ...definition.impl.rootPath ? listTypeScriptFiles(definition.impl.rootPath) : []]), ...project.apps.flatMap((app) => listTypeScriptFiles(app.rootPath))];
|
|
1171
|
+
for (const filePath of sourceFiles) {
|
|
1172
|
+
const owner = findOwner(project, filePath);
|
|
1173
|
+
if (!owner) continue;
|
|
1174
|
+
for (const scannedImport of scanImports(filePath)) {
|
|
1175
|
+
const violation = validateImport(project, graph, owner, filePath, scannedImport.moduleSpecifier);
|
|
1176
|
+
if (violation) violations.push(violation);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return violations;
|
|
1180
|
+
}
|
|
1181
|
+
function formatViolation(violation) {
|
|
1182
|
+
return `${violation.filePath}\n import: ${violation.importPath}\n ${violation.message}`;
|
|
1183
|
+
}
|
|
1184
|
+
function validateImport(project, graph, owner, filePath, importPath) {
|
|
1185
|
+
const unsupportedAlias = resolveUnsupportedArchicatAlias(project, importPath);
|
|
1186
|
+
if (unsupportedAlias) return makeViolation(project, filePath, importPath, `Unsupported Archicat alias "${importPath}". Use an explicit file import under "${unsupportedAlias.apiAlias}/*" or "${unsupportedAlias.implAlias}/*".`);
|
|
1187
|
+
const target = resolveAlias(project, importPath);
|
|
1188
|
+
if (target) {
|
|
1189
|
+
if (isOwnApiImport(owner, target) || canReach(graph, owner.target, target.target)) return;
|
|
1190
|
+
return makeViolation(project, filePath, importPath, `${formatOwner(owner)} imports "${target.target}" but does not declare a dependency that allows it.`);
|
|
1191
|
+
}
|
|
1192
|
+
if (!importPath.startsWith(".") && !importPath.startsWith("/")) return;
|
|
1193
|
+
const targetOwner = findOwner(project, resolveImportPath(filePath, importPath));
|
|
1194
|
+
if (!targetOwner || targetOwner.target === owner.target) return;
|
|
1195
|
+
return makeViolation(project, filePath, importPath, `${formatOwner(owner)} imports ${formatOwner(targetOwner)} through a source path. Use an Archicat alias instead.`);
|
|
1196
|
+
}
|
|
1197
|
+
function findOwner(project, filePath) {
|
|
1198
|
+
const extensionless = stripKnownExtension(filePath);
|
|
1199
|
+
for (const definition of project.definitions) {
|
|
1200
|
+
if (definition.api.rootPath && isPathInside(extensionless, stripKnownExtension(definition.api.rootPath))) return {
|
|
1201
|
+
kind: definition.kind,
|
|
1202
|
+
name: definition.name,
|
|
1203
|
+
surface: "api",
|
|
1204
|
+
target: definition.apiTarget
|
|
1205
|
+
};
|
|
1206
|
+
if (definition.impl.rootPath && isPathInside(extensionless, stripKnownExtension(definition.impl.rootPath))) return {
|
|
1207
|
+
kind: definition.kind,
|
|
1208
|
+
name: definition.name,
|
|
1209
|
+
surface: "impl",
|
|
1210
|
+
target: definition.implTarget
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
for (const app of project.apps) if (isPathInside(extensionless, stripKnownExtension(app.rootPath))) return {
|
|
1214
|
+
kind: "app",
|
|
1215
|
+
name: app.name,
|
|
1216
|
+
surface: "app",
|
|
1217
|
+
target: app.target
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
function resolveAlias(project, importPath) {
|
|
1221
|
+
for (const definition of project.definitions) {
|
|
1222
|
+
if (importPath.startsWith(`${definition.implAlias}/`)) return {
|
|
1223
|
+
kind: definition.kind,
|
|
1224
|
+
name: definition.name,
|
|
1225
|
+
surface: "impl",
|
|
1226
|
+
target: definition.implTarget
|
|
1227
|
+
};
|
|
1228
|
+
if (importPath.startsWith(`${definition.alias}/`)) return {
|
|
1229
|
+
kind: definition.kind,
|
|
1230
|
+
name: definition.name,
|
|
1231
|
+
surface: "api",
|
|
1232
|
+
target: definition.apiTarget
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
function resolveUnsupportedArchicatAlias(project, importPath) {
|
|
1237
|
+
for (const definition of project.definitions) {
|
|
1238
|
+
const prefix = `${definition.kind === "module" ? project.config.modules.alias : project.config.libraries.alias}/${definition.name}`;
|
|
1239
|
+
const implAlias = definition.implAlias;
|
|
1240
|
+
if (importPath === prefix || importPath === definition.alias || importPath === implAlias) return {
|
|
1241
|
+
apiAlias: definition.alias,
|
|
1242
|
+
implAlias
|
|
1243
|
+
};
|
|
1244
|
+
if (importPath.startsWith(`${prefix}/`) && !importPath.startsWith(`${definition.alias}/`) && !importPath.startsWith(`${implAlias}/`)) return {
|
|
1245
|
+
apiAlias: definition.alias,
|
|
1246
|
+
implAlias
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
function isOwnApiImport(owner, target) {
|
|
1251
|
+
return owner.kind === target.kind && owner.name === target.name && target.surface === "api";
|
|
1252
|
+
}
|
|
1253
|
+
function makeDependencyGraph(dependencies) {
|
|
1254
|
+
const graph = /* @__PURE__ */ new Map();
|
|
1255
|
+
for (const dependency of dependencies) {
|
|
1256
|
+
const targets = graph.get(dependency.from) ?? [];
|
|
1257
|
+
targets.push(dependency.to);
|
|
1258
|
+
graph.set(dependency.from, targets);
|
|
1259
|
+
}
|
|
1260
|
+
return graph;
|
|
1261
|
+
}
|
|
1262
|
+
function canReach(graph, from, to) {
|
|
1263
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1264
|
+
const queue = [from];
|
|
1265
|
+
while (queue.length > 0) {
|
|
1266
|
+
const current = queue.shift();
|
|
1267
|
+
if (!current) continue;
|
|
1268
|
+
if (current === to) return true;
|
|
1269
|
+
if (visited.has(current)) continue;
|
|
1270
|
+
visited.add(current);
|
|
1271
|
+
queue.push(...graph.get(current) ?? []);
|
|
1272
|
+
}
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
function resolveImportPath(filePath, importPath) {
|
|
1276
|
+
return stripKnownExtension(importPath.startsWith("/") ? importPath : path.resolve(path.dirname(filePath), importPath));
|
|
1277
|
+
}
|
|
1278
|
+
function makeViolation(project, filePath, importPath, message) {
|
|
1279
|
+
return {
|
|
1280
|
+
filePath: makeRelativeDisplayPath(project.rootDir, filePath),
|
|
1281
|
+
importPath,
|
|
1282
|
+
message
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
function formatOwner(owner) {
|
|
1286
|
+
if (owner.kind === "app") return `App "${owner.name}"`;
|
|
1287
|
+
return `${capitalize(owner.kind)} "${owner.name}" ${owner.surface}`;
|
|
1288
|
+
}
|
|
1289
|
+
function capitalize(value) {
|
|
1290
|
+
return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
//#endregion
|
|
1294
|
+
//#region src-cli/commands/run-project-command.ts
|
|
1295
|
+
const output$1 = new CliOutput();
|
|
1296
|
+
async function runProjectCommand(command, options, cwd) {
|
|
1297
|
+
const startedAt = Date.now();
|
|
1298
|
+
const project = await loadArchicatProject(cwd, options.config);
|
|
1299
|
+
const title = makeTitle(command, cwd, project);
|
|
1300
|
+
const panels = [];
|
|
1301
|
+
const lines = [];
|
|
1302
|
+
if (command === "doctor" || command === "generate") lines.push(...makeDoctorLines(project));
|
|
1303
|
+
if (command === "validate" || command === "generate") {
|
|
1304
|
+
const validation = makeValidationResult(project);
|
|
1305
|
+
lines.push(...validation.lines);
|
|
1306
|
+
if (validation.exitCode !== 0) return finishResult(title, panels, lines, validation.exitCode, startedAt);
|
|
1307
|
+
}
|
|
1308
|
+
if (command === "generate") {
|
|
1309
|
+
generateArtifacts(project);
|
|
1310
|
+
panels.push({
|
|
1311
|
+
kind: "panel",
|
|
1312
|
+
title: "mirrored",
|
|
1313
|
+
rows: [
|
|
1314
|
+
{
|
|
1315
|
+
label: "modules",
|
|
1316
|
+
value: project.modules.length
|
|
1317
|
+
},
|
|
1318
|
+
{
|
|
1319
|
+
label: "libraries",
|
|
1320
|
+
value: project.libraries.length
|
|
1321
|
+
},
|
|
1322
|
+
{
|
|
1323
|
+
label: "apps",
|
|
1324
|
+
value: project.apps.length
|
|
1325
|
+
}
|
|
1326
|
+
]
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
return finishResult(title, panels, lines, 0, startedAt);
|
|
1330
|
+
}
|
|
1331
|
+
function makeDoctorLines(project) {
|
|
1332
|
+
const issues = doctorProject(project);
|
|
1333
|
+
if (issues.length === 0) return [{
|
|
1334
|
+
kind: "success",
|
|
1335
|
+
label: "doctor",
|
|
1336
|
+
message: "Project diagnostics passed"
|
|
1337
|
+
}];
|
|
1338
|
+
return [{
|
|
1339
|
+
kind: "warning",
|
|
1340
|
+
label: "doctor",
|
|
1341
|
+
message: "Project diagnostics completed with warnings"
|
|
1342
|
+
}, ...issues.map((message) => ({
|
|
1343
|
+
kind: "warning",
|
|
1344
|
+
message: ` ${message}`
|
|
1345
|
+
}))];
|
|
1346
|
+
}
|
|
1347
|
+
function makeValidationResult(project) {
|
|
1348
|
+
const violations = validateProject(project);
|
|
1349
|
+
if (violations.length === 0) return {
|
|
1350
|
+
exitCode: 0,
|
|
1351
|
+
lines: [{
|
|
1352
|
+
kind: "success",
|
|
1353
|
+
label: "validate",
|
|
1354
|
+
message: "Architecture boundaries passed"
|
|
1355
|
+
}]
|
|
1356
|
+
};
|
|
1357
|
+
return {
|
|
1358
|
+
exitCode: 1,
|
|
1359
|
+
lines: [{
|
|
1360
|
+
kind: "error",
|
|
1361
|
+
label: "validate",
|
|
1362
|
+
message: "Architecture validation failed"
|
|
1363
|
+
}, ...violations.map((violation) => ({
|
|
1364
|
+
kind: "error",
|
|
1365
|
+
message: formatViolation(violation)
|
|
1366
|
+
}))]
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
function finishResult(title, panels, lines, exitCode, startedAt) {
|
|
1370
|
+
const status = exitCode === 0 ? {
|
|
1371
|
+
kind: "success",
|
|
1372
|
+
label: "done",
|
|
1373
|
+
message: `Completed in ${output$1.duration(Date.now() - startedAt)}`
|
|
1374
|
+
} : {
|
|
1375
|
+
kind: "error",
|
|
1376
|
+
label: "failed",
|
|
1377
|
+
message: `Failed in ${output$1.duration(Date.now() - startedAt)}`
|
|
1378
|
+
};
|
|
1379
|
+
return {
|
|
1380
|
+
exitCode,
|
|
1381
|
+
lines: [
|
|
1382
|
+
title,
|
|
1383
|
+
...panels,
|
|
1384
|
+
...lines,
|
|
1385
|
+
status,
|
|
1386
|
+
{
|
|
1387
|
+
kind: "info",
|
|
1388
|
+
message: ""
|
|
1389
|
+
}
|
|
1390
|
+
]
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
function makeTitle(command, cwd, project) {
|
|
1394
|
+
return {
|
|
1395
|
+
kind: "title",
|
|
1396
|
+
product: "ArchiCat",
|
|
1397
|
+
command,
|
|
1398
|
+
rows: [{
|
|
1399
|
+
label: "config",
|
|
1400
|
+
value: formatPath(cwd, project.configFilePath)
|
|
1401
|
+
}, {
|
|
1402
|
+
label: "output",
|
|
1403
|
+
value: formatPath(cwd, project.outDir)
|
|
1404
|
+
}]
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
function formatPath(cwd, filePath) {
|
|
1408
|
+
const relativePath = path.relative(cwd, filePath);
|
|
1409
|
+
if (!relativePath || relativePath.startsWith("..")) return filePath;
|
|
1410
|
+
return relativePath;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
//#endregion
|
|
1414
|
+
//#region src-cli/run-main.ts
|
|
1415
|
+
const output = new CliOutput();
|
|
1416
|
+
async function runMain(argv = process.argv.slice(2), cwd = process.cwd()) {
|
|
1417
|
+
const [command, ...rest] = argv;
|
|
1418
|
+
try {
|
|
1419
|
+
const result = await runCommand(command, parseOptions(rest), cwd);
|
|
1420
|
+
if (!result) return;
|
|
1421
|
+
printResult(result);
|
|
1422
|
+
process.exitCode = result.exitCode;
|
|
1423
|
+
} catch (error) {
|
|
1424
|
+
output.error(error instanceof Error ? error.message : String(error));
|
|
1425
|
+
process.exitCode = 1;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
async function runCommand(command, options, cwd) {
|
|
1429
|
+
switch (command) {
|
|
1430
|
+
case "generate": return await runProjectCommand("generate", options, cwd);
|
|
1431
|
+
case "validate":
|
|
1432
|
+
case "check": return await runProjectCommand("validate", options, cwd);
|
|
1433
|
+
case "graph": return await runGraphCommand(options, cwd);
|
|
1434
|
+
case "doctor": return await runProjectCommand("doctor", options, cwd);
|
|
1435
|
+
case "help":
|
|
1436
|
+
case "--help":
|
|
1437
|
+
case "-h":
|
|
1438
|
+
case void 0:
|
|
1439
|
+
printHelp();
|
|
1440
|
+
return;
|
|
1441
|
+
default:
|
|
1442
|
+
output.error(`Unknown command: ${command}`);
|
|
1443
|
+
printHelp();
|
|
1444
|
+
return {
|
|
1445
|
+
exitCode: 1,
|
|
1446
|
+
lines: []
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
function printResult(result) {
|
|
1451
|
+
for (const line of result.lines) printLine(line);
|
|
1452
|
+
}
|
|
1453
|
+
function printLine(line) {
|
|
1454
|
+
switch (line.kind) {
|
|
1455
|
+
case "title":
|
|
1456
|
+
output.title(line.product, line.command, line.rows ?? []);
|
|
1457
|
+
return;
|
|
1458
|
+
case "panel":
|
|
1459
|
+
output.panel(line.title, line.rows, line.badge);
|
|
1460
|
+
return;
|
|
1461
|
+
case "success":
|
|
1462
|
+
output.success(formatStatusLine(line));
|
|
1463
|
+
return;
|
|
1464
|
+
case "info":
|
|
1465
|
+
if (line.message.length === 0) {
|
|
1466
|
+
output.emptyLine();
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
output.info(formatStatusLine(line));
|
|
1470
|
+
return;
|
|
1471
|
+
case "warning":
|
|
1472
|
+
output.warning(formatStatusLine(line));
|
|
1473
|
+
return;
|
|
1474
|
+
case "error":
|
|
1475
|
+
output.error(formatStatusLine(line));
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
function formatStatusLine(line) {
|
|
1480
|
+
return line.label ? output.step(line.label, line.message) : line.message;
|
|
1481
|
+
}
|
|
1482
|
+
function printHelp() {
|
|
1483
|
+
output.log([
|
|
1484
|
+
"ArchiCat",
|
|
1485
|
+
"",
|
|
1486
|
+
"Usage:",
|
|
1487
|
+
" archicat generate [--config archicat.config.ts]",
|
|
1488
|
+
" archicat validate [--config archicat.config.ts]",
|
|
1489
|
+
" archicat graph [--config archicat.config.ts]",
|
|
1490
|
+
" archicat doctor [--config archicat.config.ts]",
|
|
1491
|
+
"",
|
|
1492
|
+
"Aliases:",
|
|
1493
|
+
" archicat check -> archicat validate",
|
|
1494
|
+
""
|
|
1495
|
+
].join("\n"));
|
|
1496
|
+
}
|
|
1497
|
+
function parseOptions(args) {
|
|
1498
|
+
const options = {};
|
|
1499
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1500
|
+
const arg = args[index];
|
|
1501
|
+
if (arg === "--config" || arg === "-c") {
|
|
1502
|
+
const value = args[index + 1];
|
|
1503
|
+
if (!value) throw new Error(`${arg} requires a value.`);
|
|
1504
|
+
options.config = value;
|
|
1505
|
+
index += 1;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
return options;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
//#endregion
|
|
1512
|
+
export { runMain };
|