startx 1.1.3 → 1.1.5
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/_gitignore +3 -0
- package/apps/core-server/Dockerfile +2 -2
- package/apps/core-server/package.json +1 -2
- package/apps/core-server/src/middlewares/logger-middleware.ts +32 -71
- package/apps/queue-worker/Dockerfile +2 -2
- package/apps/startx-cli/dist/index.mjs +2 -2
- package/apps/startx-cli/src/commands/package.ts +31 -24
- package/apps/web-client/Dockerfile +2 -2
- package/apps/web-client/nginx.conf +5 -1
- package/apps/web-client/package.json +2 -0
- package/apps/web-client/src/components/utils/navigation-loader.tsx +106 -0
- package/apps/web-client/src/components/utils/pagination-footer.tsx +205 -0
- package/apps/web-client/src/hooks/pagination/usePagination.tsx +22 -0
- package/apps/web-client/src/hooks/utils/use-url-params.ts +95 -0
- package/apps/web-client/src/root.tsx +2 -0
- package/apps/web-client/tsconfig.json +1 -1
- package/apps/web-client/vite.config.ts +3 -0
- package/package.json +1 -1
- package/packages/@repo/lib/src/session-module/i-session.ts +11 -0
- package/packages/ui/package.json +1 -0
- package/packages/ui/src/api/index.ts +3 -3
- package/packages/ui/src/api/query-provider.tsx +32 -25
- package/packages/ui/src/api/use-api/react-query/use-api.ts +8 -0
- package/packages/ui/src/components/custom/form-wrapper.tsx +11 -4
- package/pnpm-workspace.yaml +11 -1
- package/turbo.json +10 -2
package/_gitignore
CHANGED
|
@@ -14,7 +14,7 @@ COPY --parents packages/*/package.json ./
|
|
|
14
14
|
COPY --parents packages/*/*/package.json ./
|
|
15
15
|
COPY --parents configs/*/package.json ./
|
|
16
16
|
|
|
17
|
-
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
|
|
17
|
+
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store,sharing=locked \
|
|
18
18
|
pnpm install --frozen-lockfile
|
|
19
19
|
|
|
20
20
|
COPY apps/core-server/ ./apps/core-server/
|
|
@@ -24,7 +24,7 @@ COPY assets ./assets
|
|
|
24
24
|
COPY turbo.json ./
|
|
25
25
|
|
|
26
26
|
# Build the required packages with Turbo cache mounted
|
|
27
|
-
RUN --mount=type=cache,id=turbo,target=/app/.turbo/cache \
|
|
27
|
+
RUN --mount=type=cache,id=turbo,target=/app/.turbo/cache,sharing=locked \
|
|
28
28
|
pnpm build --filter=core-server
|
|
29
29
|
|
|
30
30
|
# --- Final production image ---
|
|
@@ -32,8 +32,7 @@
|
|
|
32
32
|
"country-state-city": "catalog:",
|
|
33
33
|
"express": "catalog:",
|
|
34
34
|
"express-fileupload": "catalog:",
|
|
35
|
-
"express-list-endpoints": "catalog:"
|
|
36
|
-
"morgan": "catalog:"
|
|
35
|
+
"express-list-endpoints": "catalog:"
|
|
37
36
|
},
|
|
38
37
|
"devDependencies": {
|
|
39
38
|
"@types/cookie-parser": "catalog:",
|
|
@@ -1,81 +1,42 @@
|
|
|
1
|
+
import { ENV } from "@repo/env";
|
|
1
2
|
import { logger } from "@repo/logger";
|
|
2
|
-
import type { Request, Response } from "express";
|
|
3
|
-
import morgan from "morgan";
|
|
3
|
+
import type { NextFunction, Request, Response } from "express";
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const method = tokens.method(req, res) || "-";
|
|
7
|
-
const url = tokens.url(req, res) || "-";
|
|
8
|
-
const status = Number(tokens.status(req, res) || 0);
|
|
9
|
-
const responseTime = tokens["response-time"](req, res) ?? "-";
|
|
10
|
-
const ip =
|
|
11
|
-
(req.ip as string) || (req.headers["x-forwarded-for"] as string) || req.socket?.remoteAddress;
|
|
12
|
-
const user = req.user ? { id: req.user.id, email: req.user.email } : null;
|
|
5
|
+
const SKIPPED_PREFIXES = ["/static", "/_next"];
|
|
13
6
|
|
|
14
|
-
|
|
15
|
-
method,
|
|
16
|
-
url,
|
|
17
|
-
status,
|
|
18
|
-
responseTimeMs: responseTime,
|
|
19
|
-
user,
|
|
20
|
-
ip,
|
|
21
|
-
ts: new Date().toISOString(),
|
|
22
|
-
});
|
|
23
|
-
};
|
|
7
|
+
const shouldSkip = (path: string) => path === "/health" || SKIPPED_PREFIXES.some(prefix => path.startsWith(prefix));
|
|
24
8
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
const fn = lg[level];
|
|
36
|
-
if (typeof fn === "function") {
|
|
37
|
-
fn(message, meta);
|
|
9
|
+
const resolveIp = (req: Request) =>
|
|
10
|
+
req.ip || (req.headers["x-forwarded-for"] as string | undefined) || req.socket?.remoteAddress || "-";
|
|
11
|
+
|
|
12
|
+
const levelForStatus = (status: number) => (status >= 500 ? "error" : status >= 400 ? "warn" : "http");
|
|
13
|
+
|
|
14
|
+
const loggerMiddleware = (req: Request, res: Response, next: NextFunction) => {
|
|
15
|
+
if (shouldSkip(req.path) || ENV.LOG_LEVEL !== "debug") {
|
|
16
|
+
next();
|
|
38
17
|
return;
|
|
39
18
|
}
|
|
40
|
-
// fallback
|
|
41
|
-
if (level && console[level]) {
|
|
42
|
-
console[level]?.(message, meta ?? {});
|
|
43
|
-
} else console.warn(message, meta ?? {});
|
|
44
|
-
};
|
|
45
19
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
url: string;
|
|
54
|
-
status: number;
|
|
55
|
-
responseTimeMs: string | number;
|
|
56
|
-
user?: { id: string; email: string } | null;
|
|
57
|
-
ip?: string;
|
|
58
|
-
ts?: string;
|
|
59
|
-
};
|
|
60
|
-
const level = meta.status >= 500 ? "error" : meta.status >= 400 ? "warn" : "info";
|
|
61
|
-
safeLog(level, "http_request", {
|
|
62
|
-
...meta,
|
|
63
|
-
logType: "routeInfo",
|
|
64
|
-
});
|
|
65
|
-
} catch (err) {
|
|
66
|
-
console.error(err);
|
|
67
|
-
safeLog("info", "http_request_parse_error", { raw: trimmed, logType: "routeInfo" });
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
};
|
|
20
|
+
const startedAt = process.hrtime.bigint();
|
|
21
|
+
|
|
22
|
+
res.on("finish", () => {
|
|
23
|
+
const { method, originalUrl: url } = req;
|
|
24
|
+
const { statusCode: status } = res;
|
|
25
|
+
const responseTimeMs = Number(process.hrtime.bigint() - startedAt) / 1e6;
|
|
26
|
+
const user = req.user ? { id: req.user.id, email: req.user.email } : null;
|
|
71
27
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
28
|
+
logger.log(levelForStatus(status), `${method} ${url} ${status} - ${responseTimeMs.toFixed(1)}ms`, {
|
|
29
|
+
logType: "routeInfo",
|
|
30
|
+
method,
|
|
31
|
+
url,
|
|
32
|
+
status,
|
|
33
|
+
responseTimeMs: Number(responseTimeMs.toFixed(1)),
|
|
34
|
+
ip: resolveIp(req),
|
|
35
|
+
user,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
next();
|
|
40
|
+
};
|
|
80
41
|
|
|
81
42
|
export { loggerMiddleware };
|
|
@@ -14,7 +14,7 @@ COPY --parents packages/*/package.json ./
|
|
|
14
14
|
COPY --parents packages/*/*/package.json ./
|
|
15
15
|
COPY --parents configs/*/package.json ./
|
|
16
16
|
|
|
17
|
-
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
|
|
17
|
+
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store,sharing=locked \
|
|
18
18
|
pnpm install --frozen-lockfile
|
|
19
19
|
|
|
20
20
|
COPY apps/queue-worker/ ./apps/queue-worker/
|
|
@@ -24,7 +24,7 @@ COPY assets ./assets
|
|
|
24
24
|
COPY turbo.json ./
|
|
25
25
|
|
|
26
26
|
# Build the required packages with Turbo cache mounted
|
|
27
|
-
RUN --mount=type=cache,id=turbo,target=/app/.turbo/cache \
|
|
27
|
+
RUN --mount=type=cache,id=turbo,target=/app/.turbo/cache,sharing=locked \
|
|
28
28
|
pnpm build --filter=queue-worker
|
|
29
29
|
|
|
30
30
|
# --- Final production image ---
|
|
@@ -215,7 +215,7 @@ $&`).replace(/(?:^|\n)([\t ].*)(?:([\n\t ]*)\n(?![\n\t ]))?/g,`$1$2`).replace(/\
|
|
|
215
215
|
`+t:``),o=Math.floor(n.length/i)-this.cursorPos.rows+(t?Cx(t):0);o>0&&(a+=vx(o)),a+=bx(this.cursorPos.cols),this.write(yx(this.extraLinesUnderPrompt)+Sx(this.height)+a),this.extraLinesUnderPrompt=o,this.height=Cx(a)}checkCursorPos(){let e=this.rl.getCursorPos();e.cols!==this.cursorPos.cols&&(this.write(bx(e.cols)),this.cursorPos=e)}done({clearContent:e}){this.rl.setPrompt(``);let t=yx(this.extraLinesUnderPrompt);t+=e?Sx(this.height):`
|
|
216
216
|
`,t+=`\x1B[?25h`,this.write(t),this.rl.close()}},Ex=class extends Promise{static withResolver(){let e,t;return{promise:new Promise((n,r)=>{e=n,t=r}),resolve:e,reject:t}}},Dx=x(nx(),1);const Ox=globalThis.setImmediate;function kx(){let e=Error.prepareStackTrace,t=[];try{Error.prepareStackTrace=(e,n)=>{let r=n.slice(1);return t=r,r},Error().stack}catch{return t}return Error.prepareStackTrace=e,t}function Ax(e){let t=kx();return(n,r={})=>{let{input:i=process.stdin,signal:a}=r,o=new Set,s=new Dx.default;s.pipe(r.output??process.stdout),s.mute();let l=d.createInterface({terminal:!0,input:i,output:s}),u=new Tx(l),{promise:f,resolve:p,reject:m}=Ex.withResolver(),h=()=>m(new Yy);if(a){let e=()=>m(new Jy({cause:a.reason}));if(a.aborted)return e(),Object.assign(f,{cancel:h});a.addEventListener(`abort`,e),o.add(()=>a.removeEventListener(`abort`,e))}o.add(mx((e,t)=>{m(new Xy(`User force closed the prompt with ${e} ${t}`))}));let g=()=>m(new Xy(`User force closed the prompt with SIGINT`));return l.on(`SIGINT`,g),o.add(()=>l.removeListener(`SIGINT`,g)),tb(l,a=>{let d=c.bind(()=>sb.clearAll());l.on(`close`,d),o.add(()=>l.removeListener(`close`,d));let g=()=>{let r=()=>u.checkCursorPos();l.input.on(`keypress`,r),o.add(()=>l.input.removeListener(`keypress`,r)),a(()=>{try{let r=e(n,e=>{setImmediate(()=>p(e))});if(r===void 0){let e=t[1]?.getFileName();throw Error(`Prompt functions must return a string.\n at ${e}`)}let[i,a]=typeof r==`string`?[r]:r;u.render(i,a),sb.run()}catch(e){m(e)}})};return`readableFlowing`in i?Ox(g):g(),Object.assign(f.then(e=>(sb.clearAll(),e),e=>{throw sb.clearAll(),e}).finally(()=>{o.forEach(e=>e()),u.done({clearContent:!!r.clearPromptOnDone}),s.end()}).then(()=>f),{cancel:h})})}}var jx=class{separator=u(`dim`,Array.from({length:15}).join(gb.line));type=`separator`;constructor(e){e&&(this.separator=e)}static isSeparator(e){return!!(e&&typeof e==`object`&&`type`in e&&e.type===`separator`)}};const Mx={icon:{checked:u(`green`,gb.circleFilled),unchecked:gb.circle,cursor:gb.pointer,disabledChecked:u(`green`,gb.circleDouble),disabledUnchecked:`-`},style:{disabled:e=>u(`dim`,e),renderSelectedChoices:e=>e.map(e=>e.short).join(`, `),description:e=>u(`cyan`,e),keysHelpTip:e=>e.map(([e,t])=>`${u(`bold`,e)} ${u(`dim`,t)}`).join(u(`dim`,` • `))},i18n:{disabledError:`This option is disabled and cannot be toggled.`},keybindings:[]};function Nx(e){return!jx.isSeparator(e)&&!e.disabled}function Px(e){return!jx.isSeparator(e)}function Fx(e){return!jx.isSeparator(e)&&e.checked}function Ix(e){return Nx(e)?{...e,checked:!e.checked}:e}function Lx(e){return function(t){return Nx(t)?{...t,checked:e}:t}}function Rx(e){return e.map(e=>{if(jx.isSeparator(e))return e;if(typeof e==`string`)return{value:e,name:e,short:e,checkedName:e,disabled:!1,checked:!1};let t=e.name??String(e.value),n={value:e.value,name:t,short:e.short??t,checkedName:e.checkedName??t,disabled:e.disabled??!1,checked:e.checked??!1};return e.description&&(n.description=e.description),n})}var zx=Ax((e,t)=>{let{pageSize:n=7,loop:r=!0,required:i,validate:a=()=>!0}=e,o={all:`a`,invert:`i`,...e.shortcuts},s=bb(Mx,e.theme),{keybindings:c}=s,[l,u]=cb(`idle`),d=xb({status:l,theme:s}),[f,p]=cb(Rx(e.choices)),m=Sb(()=>{let e=f.findIndex(Px),t=f.findLastIndex(Px);if(e===-1)throw new Qy(`[checkbox prompt] No selectable choices. All choices are disabled.`);return{first:e,last:t}},[f]),[h,g]=cb(m.first),[_,v]=cb();wb(async e=>{if(qy(e)){let e=f.filter(Fx),n=await a([...e]);i&&!e.length?v(`At least one choice must be selected`):n===!0?(u(`done`),t(e.map(e=>e.value))):v(n||`You must select a valid value`)}else if(Vy(e,c)||Hy(e,c)){if(_&&v(void 0),r||Vy(e,c)&&h!==m.first||Hy(e,c)&&h!==m.last){let t=Vy(e,c)?-1:1,n=h;do n=(n+t+f.length)%f.length;while(!Px(f[n]));g(n)}}else if(Uy(e)){let e=f[h];e&&!jx.isSeparator(e)&&(e.disabled?v(s.i18n.disabledError):(v(void 0),p(f.map((e,t)=>t===h?Ix(e):e))))}else if(e.name===o.all){let e=f.some(e=>Nx(e)&&!e.checked);p(f.map(Lx(e)))}else if(e.name===o.invert)p(f.map(Ix));else if(Ky(e)){let t=Number(e.name)-1,n=-1,r=f.findIndex(e=>jx.isSeparator(e)?!1:(n++,n===t)),i=f[r];i&&Nx(i)&&(g(r),p(f.map((e,t)=>t===r?Ix(e):e)))}});let y=s.style.message(e.message,l),b,x=tx({items:f,active:h,renderItem({item:e,isActive:t}){if(jx.isSeparator(e))return` ${e.separator}`;let n=t?s.icon.cursor:` `;if(e.disabled){let t=typeof e.disabled==`string`?e.disabled:`(disabled)`,r=e.checked?s.icon.disabledChecked:s.icon.disabledUnchecked;return s.style.disabled(`${n}${r} ${e.name} ${t}`)}t&&(b=e.description);let r=e.checked?s.icon.checked:s.icon.unchecked,i=e.checked?e.checkedName:e.name;return(t?s.style.highlight:e=>e)(`${n}${r} ${i}`)},pageSize:n,loop:r});if(l===`done`){let e=f.filter(Fx);return[d,y,s.style.answer(s.style.renderSelectedChoices(e,f))].filter(Boolean).join(` `)}let S=[[`↑↓`,`navigate`],[`space`,`select`]];o.all&&S.push([o.all,`all`]),o.invert&&S.push([o.invert,`invert`]),S.push([`⏎`,`submit`]);let C=s.style.keysHelpTip(S);return`${[[d,y].filter(Boolean).join(` `),x,` `,b?s.style.description(b):``,_?s.style.error(_):``,C].filter(Boolean).join(`
|
|
217
217
|
`).trimEnd()}${_x}`});function Bx(e,t){let n=t!==!1;return/^(y|yes)/i.test(e)?n=!0:/^(n|no)/i.test(e)&&(n=!1),n}function Vx(e){return e?`Yes`:`No`}var Hx=Ax((e,t)=>{let{transformer:n=Vx}=e,[r,i]=cb(`idle`),[a,o]=cb(``),s=bb(e.theme),c=xb({status:r,theme:s});wb((s,c)=>{if(r===`idle`)if(qy(s)){let r=Bx(a,e.default);o(n(r)),i(`done`),t(r)}else if(Gy(s)){let t=Vx(!Bx(a,e.default));c.clearLine(0),c.write(t),o(t)}else o(c.line)});let l=a,u=``;return r===`done`?l=s.style.answer(a):u=` ${s.style.defaultAnswer(e.default===!1?`y/N`:`Y/n`)}`,`${c} ${s.style.message(e.message,r)}${u} ${l}`});const Ux={validationFailureMode:`keep`};var Wx=Ax((e,t)=>{let{prefill:n=`tab`}=e,r=bb(Ux,e.theme),[i,a]=cb(`idle`),[o,s]=cb(String(e.default??``)),[c,l]=cb(),[u,d]=cb(``),f=xb({status:i,theme:r});async function p(t){let{required:n,pattern:r,patternError:i=`Invalid input`}=e;return n&&!t?`You must provide a value`:r&&!r.test(t)?i:typeof e.validate==`function`?await e.validate(t)||`You must provide a valid value`:!0}wb(async(e,n)=>{if(i===`idle`)if(qy(e)){let e=u||o;a(`loading`);let i=await p(e);i===!0?(d(e),a(`done`),t(e)):(r.validationFailureMode===`clear`?d(``):n.write(u),l(i),a(`idle`))}else Wy(e)&&!u?s(``):Gy(e)&&!u?(s(``),n.clearLine(0),n.write(o),d(o)):(d(n.line),l(void 0))}),lb(e=>{n===`editable`&&o&&(e.write(o),d(o))},[]);let m=r.style.message(e.message,i),h=u;typeof e.transformer==`function`?h=e.transformer(u,{isFinal:i===`done`}):i===`done`&&(h=r.style.answer(u));let g;o&&i!==`done`&&!u&&(g=r.style.defaultAnswer(o));let _=``;return c&&(_=r.style.error(c)),[[f,m,g,h].filter(e=>e!==void 0).join(` `),_]});const Gx={icon:{cursor:gb.pointer},style:{disabled:e=>u(`dim`,e),description:e=>u(`cyan`,e),keysHelpTip:e=>e.map(([e,t])=>`${u(`bold`,e)} ${u(`dim`,t)}`).join(u(`dim`,` • `))},i18n:{disabledError:`This option is disabled and cannot be selected.`},indexMode:`hidden`,keybindings:[]};function Kx(e){return!jx.isSeparator(e)&&!e.disabled}function qx(e){return!jx.isSeparator(e)}function Jx(e){return e.map(e=>{if(jx.isSeparator(e))return e;if(typeof e!=`object`||!e||!(`value`in e)){let t=String(e);return{value:e,name:t,short:t,disabled:!1}}let t=e.name??String(e.value),n={value:e.value,name:t,short:e.short??t,disabled:e.disabled??!1};return e.description&&(n.description=e.description),n})}var Yx=Ax((e,t)=>{let{loop:n=!0,pageSize:r=7}=e,i=bb(Gx,e.theme),{keybindings:a}=i,[o,s]=cb(`idle`),c=xb({status:o,theme:i}),l=Cb(),u=!a.includes(`vim`),d=Sb(()=>Jx(e.choices),[e.choices]),f=Sb(()=>{let e=d.findIndex(qx),t=d.findLastIndex(qx);if(e===-1)throw new Qy(`[select prompt] No selectable choices. All choices are disabled.`);return{first:e,last:t}},[d]),p=Sb(()=>`default`in e?d.findIndex(t=>Kx(t)&&t.value===e.default):-1,[e.default,d]),[m,h]=cb(p===-1?f.first:p),g=d[m],[_,v]=cb();wb((e,r)=>{if(clearTimeout(l.current),_&&v(void 0),qy(e))g.disabled?v(i.i18n.disabledError):(s(`done`),t(g.value));else if(Vy(e,a)||Hy(e,a)){if(r.clearLine(0),n||Vy(e,a)&&m!==f.first||Hy(e,a)&&m!==f.last){let t=Vy(e,a)?-1:1,n=m;do n=(n+t+d.length)%d.length;while(!qx(d[n]));h(n)}}else if(Ky(e)&&!Number.isNaN(Number(r.line))){let e=Number(r.line)-1,t=-1,n=d.findIndex(n=>jx.isSeparator(n)?!1:(t++,t===e)),i=d[n];i!=null&&Kx(i)&&h(n),l.current=setTimeout(()=>{r.clearLine(0)},700)}else if(Wy(e))r.clearLine(0);else if(u){let e=r.line.toLowerCase(),t=d.findIndex(t=>jx.isSeparator(t)||!Kx(t)?!1:t.name.toLowerCase().startsWith(e));t!==-1&&h(t),l.current=setTimeout(()=>{r.clearLine(0)},700)}}),lb(()=>()=>{clearTimeout(l.current)},[]);let y=i.style.message(e.message,o),b=i.style.keysHelpTip([[`↑↓`,`navigate`],[`⏎`,`select`]]),x=0,S=tx({items:d,active:m,renderItem({item:e,isActive:t,index:n}){if(jx.isSeparator(e))return x++,` ${e.separator}`;let r=t?i.icon.cursor:` `,a=i.indexMode===`number`?`${n+1-x}. `:``;if(e.disabled){let n=typeof e.disabled==`string`?e.disabled:`(disabled)`,r=t?i.icon.cursor:`-`;return i.style.disabled(`${r} ${a}${e.name} ${n}`)}return(t?i.style.highlight:e=>e)(`${r} ${a}${e.name}`)},pageSize:r,loop:n});if(o===`done`)return[c,y,i.style.answer(g.short)].filter(Boolean).join(` `);let{description:C}=g;return`${[[c,y].filter(Boolean).join(` `),S,` `,C?i.style.description(C):``,_?i.style.error(_):``,b].filter(Boolean).join(`
|
|
218
|
-
`).trimEnd()}${_x}`}),Xx=class{static async confirm(e){return await Hx({message:e.message,default:e.default})}static async getText(e){let{message:t,schema:n,default:r}=e,i;if(n&&r!==void 0){let e=n.safeParse(r);e.success&&(i=e.data)}else !n&&r!==void 0&&(i=r);if(n&&n instanceof Tf)return await Yx({message:t,choices:n.options.map(e=>({value:e})),default:i});let a=await Wx({message:t,default:i,validate:e=>{if(!n)return!0;let t=e;if(n instanceof Sd){if(e.trim()===``)return`Input cannot be empty`;let n=Number(e);if(Number.isNaN(n))return`Please enter a valid number`;t=n}let r=n.safeParse(t);return r.success?!0:r.error.issues[0]?.message??`Invalid input`}});if(!n)return a;let o=n instanceof Sd?Number(a):a;return n.parse(o)}static async choose(e){let{message:t,options:n,mode:r=`single`,default:i,includeAllOption:a=!1,required:o=!1}=e,s=`__all__`,c=r===`single`?[i]:[...i||[]],l=[...r===`multiple`&&a?[{name:`All`,value:s,checked:c.includes(s)}]:[],...n.map(e=>({name:e,value:e,checked:c.includes(e)}))];if(r===`multiple`){let e=await zx({message:t,choices:l,validate:e=>o&&e.length===0?`You must select at least one option.`:!0});return a&&e.includes(s)?n:e}return await Yx({message:t,choices:n.map(e=>({name:e,value:e})),default:typeof i==`string`?i:void 0})}},Zx=class e{static command=new dv(`init`).argument(`[projectName]`).option(`-d, --dir <path>`,`workspace directory`).action(e.run.bind(e));static async run(e,t){let n=await Iy.getPackageList(),r=n.filter(e=>e.type===`apps`&&e.packageJson?.startx?.mode!==`silent`),i=await this.getPrefs({projectName:e,options:t,projects:r}),a=n.filter(e=>e.type!==`apps`);await this.checkTargetDirectory(i.directory.workspace);let o=await this.getConfigPrefs({selectedApps:i.selectedApps,packages:a}),s=await this.getPackagesPrefs({selectedPackages:o.selectedConfigs,packages:a,tags:o.gTags}),c=[...s.gTags,`runnable`];await this.installWorkspace({name:i.projectName,tags:c,dir:i.directory});let l=[...s.selectedPackages,...i.selectedApps];await Promise.all(l.map(async e=>{let t={},n=new Set(s.gTags);e.packageJson?.startx?.mode===`standalone`&&n.add(`runnable`),e.type===`apps`&&(n.add(`runnable`),s.selectedPackages.filter(t=>t.type!==`packages`||t.packageJson?.startx?.mode===`standalone`?!1:t.packageJson?.startx?.iTags?.every(t=>e.packageJson?.startx?.gTags?.includes(t))).forEach(e=>{let n=e.packageJson?.name||e.name;t[n]=`workspace:^`})),await this.installPackage({pkg:e,directory:i.directory,tags:Array.from(n),dependencies:t})}))}static async getPrefs(e){let n=await Xx.getText({message:`Project name`,name:`projectName`,default:e.projectName,schema:Vp.string().min(1,`Package name is required`).max(214,`Package name too long`).regex(/^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/,`Invalid package name`)});if(e.projects.length===0)throw Error(`No apps found to install.`);let r=Iy.getDirectory(),i=e.options.dir?t.resolve(r.workspace,e.options.dir):t.join(r.workspace,n),a=await Xx.choose({message:`Select apps to install`,options:e.projects.map(e=>e.name),includeAllOption:!0,mode:`multiple`,required:!0});return{projectName:n,directory:{workspace:i,template:r.template},selectedApps:e.projects.filter(e=>a.includes(e.name))}}static async getConfigPrefs(e){let t=new Set([`common`]),n=new Map;this.getGlobalTags({pkgs:e.selectedApps}).forEach(e=>t.add(e)),this.getPackageDeps({allPkgs:e.packages,pkgs:e.selectedApps}).forEach(e=>n.set(e.name,e));let r=e.packages.filter(e=>e.type!==`configs`||e.packageJson?.startx?.mode===`silent`||n.has(e.name)?!1:e.packageJson?.startx?.iTags?.every(e=>t.has(e))??!0);if(r.length>0){let e=await Xx.choose({message:`Select configs to install`,options:r.map(e=>e.name),includeAllOption:!0,mode:`multiple`,required:!1});r.filter(t=>e.includes(t.name)).forEach(e=>n.set(e.name,e))}return t.has(`node`)&&(await Xx.choose({message:`Select formatter`,options:[`prettier + biome`,`prettier`],mode:`single`,default:`prettier`,required:!0})===`prettier`||t.add(`biome`),t.add(`prettier`)),this.getPackageDeps({allPkgs:e.packages,pkgs:Array.from(n.values())}).forEach(e=>n.set(e.name,e)),this.getGlobalTags({pkgs:Array.from(n.values())}).forEach(e=>t.add(e)),{gTags:Array.from(t),selectedConfigs:Array.from(n.values())}}static async getPackagesPrefs(e){let t=new Set(e.tags),n=new Map(e.selectedPackages.map(e=>[e.name,e])),r=e.packages.filter(e=>e.type!==`packages`||e.packageJson?.startx?.mode===`silent`||n.has(e.name)?!1:e.packageJson?.startx?.iTags?.every(e=>t.has(e))??!1);if(r.length>0){let e=await Xx.choose({message:`Select packages to install`,options:r.map(e=>e.name),includeAllOption:!0,mode:`multiple`,required:!1});r.filter(t=>e.includes(t.name)).forEach(e=>n.set(e.name,e))}return this.getPackageDeps({allPkgs:e.packages,pkgs:Array.from(n.values())}).forEach(e=>n.set(e.name,e)),this.getGlobalTags({pkgs:Array.from(n.values())}).forEach(e=>t.add(e)),{gTags:Array.from(t),selectedPackages:Array.from(n.values())}}static async installPackage(e){if(!e.pkg.packageJson)throw Error(`Missing package.json for ${e.pkg.name}`);let n=new Set([...e.tags,...e.pkg.packageJson.startx?.tags||[]]),r=e.pkg.packageJson.startx?.ignore||[];r.includes(`eslint-config`)&&n.delete(`eslint`),r.includes(`vitest-config`)&&n.delete(`vitest`);let{packageJson:i,isWorkspace:a}=By.handlePackageJson({app:e.pkg.packageJson,tags:Array.from(n),name:e.packageName||e.pkg.packageJson.name||e.pkg.name,dependencies:e.dependencies});if(a)throw Error(`Cannot install workspace as a package: ${e.pkg.name}`);let o=t.join(e.directory.workspace,e.pkg.relativePath),s=t.join(e.pkg.path);await Ny.writeJSONFile({dir:o,file:`package`,content:i}),await this.copyValidatedFilesFromFolder(s,o,n),await Ny.copyDirectory({from:t.join(s,`src`),to:t.join(o,`src`),exclude:n.has(`vitest`)?void 0:/\.test\.tsx?$/}),Z_.info(`Successfully installed ${e.pkg.name}`)}static async installWorkspace(e){let t=await Iy.parsePackageJson({dir:e.dir.template}),n=await Iy.parsePackageJson({dir:e.dir.template,file:`startx`});if(!t)throw Error(`Failed to parse root package.json`);t.dependencies={...t.dependencies,...n?.dependencies||{}},t.devDependencies={...t.devDependencies,...n?.devDependencies||{}};let{packageJson:r}=By.handlePackageJson({app:t,tags:[`root`,...e.tags],name:e.name});await Ny.writeJSONFile({dir:e.dir.workspace,file:`package`,content:r}),await this.copyValidatedFilesFromFolder(e.dir.template,e.dir.workspace,new Set([`root`,...e.tags])),await this.writeVscodeSettings({workspace:e.dir.workspace,tags:e.tags})}static async writeVscodeSettings(e){let n=e.tags.includes(`biome`),r=t.join(e.workspace,`.vscode`),i={"editor.formatOnSave":!0,"editor.defaultFormatter":n?`biomejs.biome`:`esbenp.prettier-vscode`,"editor.codeActionsOnSave":{...n?{"source.organizeImports.biome":`explicit`,"source.fixAll.biome":`explicit`}:{},"source.fixAll.eslint":`explicit`,"source.fixAll":`explicit`},"eslint.workingDirectories":[{mode:`auto`}]},a={recommendations:[`dbaeumer.vscode-eslint`,...n?[`biomejs.biome`]:[`esbenp.prettier-vscode`]]};await Promise.all([Ny.writeJSONFile({dir:r,file:`settings`,content:i}),Ny.writeJSONFile({dir:r,file:`extensions`,content:a})])}static async checkTargetDirectory(e){let[t,n]=await Promise.all([Ny.listFiles({dir:e}),Ny.listDirectories({dir:e})]);if(!(t.length===0&&n.length===0)&&!await Xx.confirm({message:`Directory "${e}" already exists and is not empty. Overwrite?`,default:!1}))throw Error(`Aborted: target directory already exists.`)}static getPackageDeps(e){let t=new Map(e.pkgs.map(e=>[e.name,e]));return Array.from(t.values()).forEach(n=>{[...n.packageJson?.startx?.requiredDeps||[],...n.packageJson?.startx?.requiredDevDeps||[]].forEach(n=>{let r=e.allPkgs.find(e=>e.packageJson?.name===n);r&&t.set(r.name,r)})}),e.pkgs.forEach(e=>t.delete(e.name)),Array.from(t.values())}static getGlobalTags(e){let t=new Set(e.gTags||[]);return e.pkgs.forEach(e=>{e.packageJson?.startx?.gTags?.forEach(e=>t.add(e))}),Array.from(t)}static async copyValidatedFilesFromFolder(e,n,r){let i=await Ny.listFiles({dir:e}).catch(()=>[]);for(let a of i){let i=Py[a];if(i&&!i.tags.every(e=>r.has(e)))continue;let o=a===`_gitignore`?`.gitignore`:a;try{await Ny.copyFile({from:t.join(e,a),to:t.join(n,o)})}catch(e){Z_.error(`Failed to copy file ${a}:`,e)}}}};const Qx=Vp.string().min(1,`Package name is required`).max(214,`Package name too long`).regex(/^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/,`Invalid package name`);var $x=class e{static command=new dv(`package`).alias(`pkg`).description(`List and add packages in the current monorepo.`).addCommand(new dv(`list`).alias(`ls`).description(`List packages available from the StartX template.`).action(e.list.bind(e))).addCommand(new dv(`add`).description(`Add an existing StartX app or package, optionally with a new name.`).argument(`[packageName]`).option(`-n, --name <name>`,`override the name for the added package`).option(`--eslint`,`enable ESLint support for the added package`).option(`--no-eslint`,`skip ESLint support for the added package`).option(`--no-install`,`do not run the package manager after updating dependencies`).action(e.add.bind(e))).addCommand(new dv(`new`).alias(`create`).description(`Create a new package from scratch.`).argument(`[packageName]`).option(`-d, --dir <path>`,`package directory relative to the current workspace`).option(`--eslint`,`enable ESLint support for the new package`).option(`--no-eslint`,`skip ESLint support for the new package`).option(`--no-install`,`do not run the package manager after updating ESLint`).action(e.create.bind(e)));static async list(){let e=await Iy.getPackageList(),t=new Map;for(let n of e){let e=t.get(n.type)??[];e.push(n),t.set(n.type,e)}for(let e of[`apps`,`packages`,`configs`]){let n=t.get(e)??[];if(n.length!==0){Z_.info(`${e}:`);for(let e of n.sort((e,t)=>e.name.localeCompare(t.name)))Z_.info(` ${e.name} (${e.relativePath})`)}}}static async add(e,t){let n=await Iy.getPackageList(),r=n.filter(e=>e.packageJson?.startx?.mode!==`silent`),i=e??await Xx.choose({message:`Select app or package to add`,options:r.map(e=>e.name),mode:`single`,required:!0}),a=this.findPackage(n,i);if(!a)throw Error(`Package "${i}" was not found in the StartX template.`);let o=a.packageJson?.name??a.name,s=t.name??await Xx.getText({message:`Name for the new package (leave unchanged to keep the original)`,name:`overrideName`,default:o,schema:Qx}),c=Iy.getDirectory(),l=await this.resolveEslintPreference(t),u=this.resolvePackageClosure({packages:n,selectedPackage:a,includeEslintConfig:l}),d=await this.getInstallTags({workspace:c.workspace,packages:u,eslintEnabled:l});await this.checkAndInstallMissingDeps({workspace:c.workspace,tags:d,install:t.install});for(let e of u){let t=e.name===a.name;await this.installTemplatePackage({pkg:e,directory:c,tags:d,overrideName:t?s:void 0,overrideRelativePath:t?this.getDestinationPath(e.relativePath,s):void 0})}Z_.info("Done! Run `pnpm install` to link the new package.")}static async create(e,n){let r=await Xx.getText({message:`Package name`,name:`packageName`,default:e,schema:Qx}),i=Iy.getDirectory(),a=n.dir?t.resolve(i.workspace,n.dir):t.resolve(i.workspace,this.getDefaultPackagePath(r));if(await this.pathExists(a))throw Error(`Package directory already exists: ${a}`);let s=await this.readRootPackage(i.workspace),c=await this.resolveEslintPreference(n),l=this.hasDependency(s,`vitest`),u=await Iy.getPackageList();await this.ensureTemplatePackage({packages:u,name:`typescript-config`,directory:i,tags:[`common`,`node`]}),c&&await this.ensureTemplatePackage({packages:u,name:`eslint-config`,directory:i,tags:[`common`,`node`,`eslint`]}),l&&await this.ensureTemplatePackage({packages:u,name:`vitest-config`,directory:i,tags:[`common`,`node`,`vitest`]}),await o.mkdir(t.join(a,`src`),{recursive:!0}),await this.writeJson(t.join(a,`package.json`),this.createPackageJson({name:r,eslintEnabled:c,vitestEnabled:l})),await o.writeFile(t.join(a,`tsconfig.json`),`${JSON.stringify({extends:`typescript-config/tsconfig.node.json`,compilerOptions:{moduleResolution:`bundler`,module:`esnext`,target:`es2022`},include:[`src/**/*.ts`]},null,2)}\n`),await o.writeFile(t.join(a,`src`,`index.ts`),`export {};
|
|
218
|
+
`).trimEnd()}${_x}`}),Xx=class{static async confirm(e){return await Hx({message:e.message,default:e.default})}static async getText(e){let{message:t,schema:n,default:r}=e,i;if(n&&r!==void 0){let e=n.safeParse(r);e.success&&(i=e.data)}else !n&&r!==void 0&&(i=r);if(n&&n instanceof Tf)return await Yx({message:t,choices:n.options.map(e=>({value:e})),default:i});let a=await Wx({message:t,default:i,validate:e=>{if(!n)return!0;let t=e;if(n instanceof Sd){if(e.trim()===``)return`Input cannot be empty`;let n=Number(e);if(Number.isNaN(n))return`Please enter a valid number`;t=n}let r=n.safeParse(t);return r.success?!0:r.error.issues[0]?.message??`Invalid input`}});if(!n)return a;let o=n instanceof Sd?Number(a):a;return n.parse(o)}static async choose(e){let{message:t,options:n,mode:r=`single`,default:i,includeAllOption:a=!1,required:o=!1}=e,s=`__all__`,c=r===`single`?[i]:[...i||[]],l=[...r===`multiple`&&a?[{name:`All`,value:s,checked:c.includes(s)}]:[],...n.map(e=>({name:e,value:e,checked:c.includes(e)}))];if(r===`multiple`){let e=await zx({message:t,choices:l,validate:e=>o&&e.length===0?`You must select at least one option.`:!0});return a&&e.includes(s)?n:e}return await Yx({message:t,choices:n.map(e=>({name:e,value:e})),default:typeof i==`string`?i:void 0})}},Zx=class e{static command=new dv(`init`).argument(`[projectName]`).option(`-d, --dir <path>`,`workspace directory`).action(e.run.bind(e));static async run(e,t){let n=await Iy.getPackageList(),r=n.filter(e=>e.type===`apps`&&e.packageJson?.startx?.mode!==`silent`),i=await this.getPrefs({projectName:e,options:t,projects:r}),a=n.filter(e=>e.type!==`apps`);await this.checkTargetDirectory(i.directory.workspace);let o=await this.getConfigPrefs({selectedApps:i.selectedApps,packages:a}),s=await this.getPackagesPrefs({selectedPackages:o.selectedConfigs,packages:a,tags:o.gTags}),c=[...s.gTags,`runnable`];await this.installWorkspace({name:i.projectName,tags:c,dir:i.directory});let l=[...s.selectedPackages,...i.selectedApps];await Promise.all(l.map(async e=>{let t={},n=new Set(s.gTags);e.packageJson?.startx?.mode===`standalone`&&n.add(`runnable`),e.type===`apps`&&(n.add(`runnable`),s.selectedPackages.filter(t=>t.type!==`packages`||t.packageJson?.startx?.mode===`standalone`?!1:t.packageJson?.startx?.iTags?.every(t=>e.packageJson?.startx?.gTags?.includes(t))).forEach(e=>{let n=e.packageJson?.name||e.name;t[n]=`workspace:^`})),await this.installPackage({pkg:e,directory:i.directory,tags:Array.from(n),dependencies:t})}))}static async getPrefs(e){let n=await Xx.getText({message:`Project name`,name:`projectName`,default:e.projectName,schema:Vp.string().min(1,`Package name is required`).max(214,`Package name too long`).regex(/^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/,`Invalid package name`)});if(e.projects.length===0)throw Error(`No apps found to install.`);let r=Iy.getDirectory(),i=e.options.dir?t.resolve(r.workspace,e.options.dir):t.join(r.workspace,n),a=await Xx.choose({message:`Select apps to install`,options:e.projects.map(e=>e.name),includeAllOption:!0,mode:`multiple`,required:!0});return{projectName:n,directory:{workspace:i,template:r.template},selectedApps:e.projects.filter(e=>a.includes(e.name))}}static async getConfigPrefs(e){let t=new Set([`common`]),n=new Map;this.getGlobalTags({pkgs:e.selectedApps}).forEach(e=>t.add(e)),this.getPackageDeps({allPkgs:e.packages,pkgs:e.selectedApps}).forEach(e=>n.set(e.name,e));let r=e.packages.filter(e=>e.type!==`configs`||e.packageJson?.startx?.mode===`silent`||n.has(e.name)?!1:e.packageJson?.startx?.iTags?.every(e=>t.has(e))??!0);if(r.length>0){let e=await Xx.choose({message:`Select configs to install`,options:r.map(e=>e.name),includeAllOption:!0,mode:`multiple`,required:!1});r.filter(t=>e.includes(t.name)).forEach(e=>n.set(e.name,e))}return t.has(`node`)&&(await Xx.choose({message:`Select formatter`,options:[`prettier + biome`,`prettier`],mode:`single`,default:`prettier`,required:!0})===`prettier`||t.add(`biome`),t.add(`prettier`)),this.getPackageDeps({allPkgs:e.packages,pkgs:Array.from(n.values())}).forEach(e=>n.set(e.name,e)),this.getGlobalTags({pkgs:Array.from(n.values())}).forEach(e=>t.add(e)),{gTags:Array.from(t),selectedConfigs:Array.from(n.values())}}static async getPackagesPrefs(e){let t=new Set(e.tags),n=new Map(e.selectedPackages.map(e=>[e.name,e])),r=e.packages.filter(e=>e.type!==`packages`||e.packageJson?.startx?.mode===`silent`||n.has(e.name)?!1:e.packageJson?.startx?.iTags?.every(e=>t.has(e))??!1);if(r.length>0){let e=await Xx.choose({message:`Select packages to install`,options:r.map(e=>e.name),includeAllOption:!0,mode:`multiple`,required:!1});r.filter(t=>e.includes(t.name)).forEach(e=>n.set(e.name,e))}return this.getPackageDeps({allPkgs:e.packages,pkgs:Array.from(n.values())}).forEach(e=>n.set(e.name,e)),this.getGlobalTags({pkgs:Array.from(n.values())}).forEach(e=>t.add(e)),{gTags:Array.from(t),selectedPackages:Array.from(n.values())}}static async installPackage(e){if(!e.pkg.packageJson)throw Error(`Missing package.json for ${e.pkg.name}`);let n=new Set([...e.tags,...e.pkg.packageJson.startx?.tags||[]]),r=e.pkg.packageJson.startx?.ignore||[];r.includes(`eslint-config`)&&n.delete(`eslint`),r.includes(`vitest-config`)&&n.delete(`vitest`);let{packageJson:i,isWorkspace:a}=By.handlePackageJson({app:e.pkg.packageJson,tags:Array.from(n),name:e.packageName||e.pkg.packageJson.name||e.pkg.name,dependencies:e.dependencies});if(a)throw Error(`Cannot install workspace as a package: ${e.pkg.name}`);let o=t.join(e.directory.workspace,e.pkg.relativePath),s=t.join(e.pkg.path);await Ny.writeJSONFile({dir:o,file:`package`,content:i}),await this.copyValidatedFilesFromFolder(s,o,n),await Ny.copyDirectory({from:t.join(s,`src`),to:t.join(o,`src`),exclude:n.has(`vitest`)?void 0:/\.test\.tsx?$/}),Z_.info(`Successfully installed ${e.pkg.name}`)}static async installWorkspace(e){let t=await Iy.parsePackageJson({dir:e.dir.template}),n=await Iy.parsePackageJson({dir:e.dir.template,file:`startx`});if(!t)throw Error(`Failed to parse root package.json`);t.dependencies={...t.dependencies,...n?.dependencies||{}},t.devDependencies={...t.devDependencies,...n?.devDependencies||{}};let{packageJson:r}=By.handlePackageJson({app:t,tags:[`root`,...e.tags],name:e.name});await Ny.writeJSONFile({dir:e.dir.workspace,file:`package`,content:r}),await this.copyValidatedFilesFromFolder(e.dir.template,e.dir.workspace,new Set([`root`,...e.tags])),await this.writeVscodeSettings({workspace:e.dir.workspace,tags:e.tags})}static async writeVscodeSettings(e){let n=e.tags.includes(`biome`),r=t.join(e.workspace,`.vscode`),i={"editor.formatOnSave":!0,"editor.defaultFormatter":n?`biomejs.biome`:`esbenp.prettier-vscode`,"editor.codeActionsOnSave":{...n?{"source.organizeImports.biome":`explicit`,"source.fixAll.biome":`explicit`}:{},"source.fixAll.eslint":`explicit`,"source.fixAll":`explicit`},"eslint.workingDirectories":[{mode:`auto`}]},a={recommendations:[`dbaeumer.vscode-eslint`,...n?[`biomejs.biome`]:[`esbenp.prettier-vscode`]]};await Promise.all([Ny.writeJSONFile({dir:r,file:`settings`,content:i}),Ny.writeJSONFile({dir:r,file:`extensions`,content:a})])}static async checkTargetDirectory(e){let[t,n]=await Promise.all([Ny.listFiles({dir:e}),Ny.listDirectories({dir:e})]);if(!(t.length===0&&n.length===0)&&!await Xx.confirm({message:`Directory "${e}" already exists and is not empty. Overwrite?`,default:!1}))throw Error(`Aborted: target directory already exists.`)}static getPackageDeps(e){let t=new Map(e.pkgs.map(e=>[e.name,e]));return Array.from(t.values()).forEach(n=>{[...n.packageJson?.startx?.requiredDeps||[],...n.packageJson?.startx?.requiredDevDeps||[]].forEach(n=>{let r=e.allPkgs.find(e=>e.packageJson?.name===n);r&&t.set(r.name,r)})}),e.pkgs.forEach(e=>t.delete(e.name)),Array.from(t.values())}static getGlobalTags(e){let t=new Set(e.gTags||[]);return e.pkgs.forEach(e=>{e.packageJson?.startx?.gTags?.forEach(e=>t.add(e))}),Array.from(t)}static async copyValidatedFilesFromFolder(e,n,r){let i=await Ny.listFiles({dir:e}).catch(()=>[]);for(let a of i){let i=Py[a];if(i&&!i.tags.every(e=>r.has(e)))continue;let o=a===`_gitignore`?`.gitignore`:a;try{await Ny.copyFile({from:t.join(e,a),to:t.join(n,o)})}catch(e){Z_.error(`Failed to copy file ${a}:`,e)}}}};const Qx=Vp.string().min(1,`Package name is required`).max(214,`Package name too long`).regex(/^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/,`Invalid package name`);var $x=class e{static command=new dv(`package`).alias(`pkg`).description(`List and add packages in the current monorepo.`).addCommand(new dv(`list`).alias(`ls`).description(`List packages available from the StartX template.`).action(e.list.bind(e))).addCommand(new dv(`add`).description(`Add an existing StartX app or package, optionally with a new name.`).argument(`[packageName]`).option(`-n, --name <name>`,`override the name for the added package`).option(`--eslint`,`enable ESLint support for the added package`).option(`--no-eslint`,`skip ESLint support for the added package`).option(`--no-install`,`do not run the package manager after updating dependencies`).action(e.add.bind(e))).addCommand(new dv(`new`).alias(`create`).description(`Create a new package from scratch.`).argument(`[packageName]`).option(`-d, --dir <path>`,`package directory relative to the current workspace`).option(`--eslint`,`enable ESLint support for the new package`).option(`--no-eslint`,`skip ESLint support for the new package`).option(`--no-install`,`do not run the package manager after updating ESLint`).action(e.create.bind(e)));static async list(){let e=await Iy.getPackageList(),t=new Map;for(let n of e){let e=t.get(n.type)??[];e.push(n),t.set(n.type,e)}for(let e of[`apps`,`packages`,`configs`]){let n=t.get(e)??[];if(n.length!==0){Z_.info(`${e}:`);for(let e of n.sort((e,t)=>e.name.localeCompare(t.name)))Z_.info(` ${e.name} (${e.relativePath})`)}}}static async add(e,t){let n=await Iy.getPackageList(),r=n.filter(e=>e.packageJson?.startx?.mode!==`silent`),i=e??await Xx.choose({message:`Select app or package to add`,options:r.map(e=>e.name),mode:`single`,required:!0}),a=this.findPackage(n,i);if(!a)throw Error(`Package "${i}" was not found in the StartX template.`);let o=a.packageJson?.name??a.name,s=t.name??await Xx.getText({message:`Name for the new package (leave unchanged to keep the original)`,name:`overrideName`,default:o,schema:Qx}),c=Iy.getDirectory(),l=await this.resolveEslintPreference(t),u=this.resolvePackageClosure({packages:n,selectedPackage:a,includeEslintConfig:l}),d=await this.getInstallTags({workspace:c.workspace,packages:u,eslintEnabled:l});await this.checkAndInstallMissingDeps({directory:c,tags:d,install:t.install});for(let e of u){let t=e.name===a.name;await this.installTemplatePackage({pkg:e,directory:c,tags:d,overrideName:t?s:void 0,overrideRelativePath:t?this.getDestinationPath(e.relativePath,s):void 0})}Z_.info("Done! Run `pnpm install` to link the new package.")}static async create(e,n){let r=await Xx.getText({message:`Package name`,name:`packageName`,default:e,schema:Qx}),i=Iy.getDirectory(),a=n.dir?t.resolve(i.workspace,n.dir):t.resolve(i.workspace,this.getDefaultPackagePath(r));if(await this.pathExists(a))throw Error(`Package directory already exists: ${a}`);let s=await this.readRootPackage(i.workspace),c=await this.resolveEslintPreference(n),l=this.hasDependency(s,`vitest`),u=await Iy.getPackageList();await this.ensureTemplatePackage({packages:u,name:`typescript-config`,directory:i,tags:[`common`,`node`]}),c&&await this.ensureTemplatePackage({packages:u,name:`eslint-config`,directory:i,tags:[`common`,`node`,`eslint`]}),l&&await this.ensureTemplatePackage({packages:u,name:`vitest-config`,directory:i,tags:[`common`,`node`,`vitest`]}),await o.mkdir(t.join(a,`src`),{recursive:!0}),await this.writeJson(t.join(a,`package.json`),this.createPackageJson({name:r,eslintEnabled:c,vitestEnabled:l})),await o.writeFile(t.join(a,`tsconfig.json`),`${JSON.stringify({extends:`typescript-config/tsconfig.node.json`,compilerOptions:{moduleResolution:`bundler`,module:`esnext`,target:`es2022`},include:[`src/**/*.ts`]},null,2)}\n`),await o.writeFile(t.join(a,`src`,`index.ts`),`export {};
|
|
219
219
|
`),c&&await o.writeFile(t.join(a,`eslint.config.ts`),`import { baseConfig } from "eslint-config/base";
|
|
220
220
|
import { extend } from "eslint-config/extend";
|
|
221
221
|
|
|
@@ -223,4 +223,4 @@ export default extend(baseConfig);
|
|
|
223
223
|
`),l&&await o.writeFile(t.join(a,`vitest.config.ts`),`import vitestConfig from "vitest-config/node";
|
|
224
224
|
|
|
225
225
|
export default vitestConfig;
|
|
226
|
-
`),Z_.info(`Created package ${r} at ${t.relative(i.workspace,a)}`),Z_.info("Run `pnpm install` to link the new package.")}static async resolveEslintPreference(e){if(e.eslint===!1)return!1;let n=Iy.getDirectory(),r=await this.readRootPackage(n.workspace);return this.hasDependency(r,`eslint`)?e.eslint??!0:e.eslint===!0||await Xx.confirm({message:`ESLint is not installed in this monorepo. Install and enable it?`,default:!0})?(r.devDependencies={...r.devDependencies,eslint:await this.resolveDependencyVersion(n.workspace,`eslint`)},await this.writeJson(t.join(n.workspace,`package.json`),r),Z_.info(`Added eslint to the root devDependencies.`),e.install!==!1&&await this.installRootDependencies(n.workspace),!0):!1}static async getInstallTags(e){let t=new Set([`common`,`node`]),n=await this.readRootPackage(e.workspace);e.eslintEnabled&&t.add(`eslint`),this.hasDependency(n,`@biomejs/biome`)&&t.add(`biome`),this.hasDependency(n,`prettier`)&&t.add(`prettier`),this.hasDependency(n,`vitest`)&&t.add(`vitest`),this.hasDependency(n,`tsdown`)&&t.add(`tsdown`);for(let n of e.packages)n.packageJson?.startx?.tags?.forEach(e=>t.add(e)),n.packageJson?.startx?.iTags?.forEach(e=>t.add(e)),n.packageJson?.startx?.gTags?.forEach(e=>t.add(e)),(n.type===`apps`||n.packageJson?.startx?.mode===`standalone`)&&t.add(`runnable`);return Array.from(t)}static resolvePackageClosure(e){let t=new Map,n=[e.selectedPackage],r=r=>{let i=this.findPackage(e.packages,r);i&&!t.has(i.name)&&n.push(i)};for(e.includeEslintConfig&&r(`eslint-config`);n.length>0;){let e=n.shift();if(!t.has(e.name)){t.set(e.name,e);for(let t of[...e.packageJson?.startx?.requiredDeps??[],...e.packageJson?.startx?.requiredDevDeps??[]])r(t)}}return Array.from(t.values())}static async ensureTemplatePackage(e){let t=this.findPackage(e.packages,e.name);if(!t){Z_.warn(`Could not find template package ${e.name}; skipping.`);return}await this.installTemplatePackage({pkg:t,directory:e.directory,tags:e.tags})}static async installTemplatePackage(e){if(!e.pkg.packageJson)throw Error(`Missing package.json for ${e.pkg.name}`);let n=e.overrideRelativePath??e.pkg.relativePath,r=t.join(e.directory.workspace,n);if(await this.pathExists(t.join(r,`package.json`))&&!await Xx.confirm({message:`"${n}" already exists. Overwrite?`,default:!1})){Z_.info(`Skipping ${e.pkg.name}.`);return}let i=new Set([...e.tags,...e.pkg.packageJson.startx?.tags??[]]),a=e.pkg.packageJson.startx?.ignore??[];a.includes(`eslint-config`)&&i.delete(`eslint`),a.includes(`vitest-config`)&&i.delete(`vitest`);let{packageJson:o,isWorkspace:s}=By.handlePackageJson({app:e.pkg.packageJson,tags:Array.from(i),name:e.overrideName??e.pkg.packageJson.name??e.pkg.name});if(s)throw Error(`Cannot install workspace as a package: ${e.pkg.name}`);await this.syncDepsWithCatalog({workspace:e.directory.workspace,templateDir:e.directory.template,packageJson:o}),await Ny.writeJSONFile({dir:r,file:`package`,content:o}),await this.copyValidatedFilesFromFolder(e.pkg.path,r,i),await Ny.copyDirectory({from:t.join(e.pkg.path,`src`),to:t.join(r,`src`),exclude:i.has(`vitest`)?void 0:/\.test\.tsx?$/}),Z_.info(`Installed ${e.overrideName??e.pkg.name} at ${n}`)}static async copyValidatedFilesFromFolder(e,n,r){let i=await Ny.listFiles({dir:e}).catch(()=>[]);for(let a of i){let i=Py[a];i&&!i.tags.every(e=>r.has(e))||a!==`package.json`&&await Ny.copyFile({from:t.join(e,a),to:t.join(n,a)})}}static createPackageJson(e){let t={typecheck:`tsc --noEmit`,clean:`rimraf dist .turbo`},n={"typescript-config":`workspace:*`},r=[];return e.eslintEnabled?(t.lint=`eslint .`,t[`lint:fix`]=`eslint . --fix`,n[`eslint-config`]=`workspace:*`):r.push(`eslint-config`),e.vitestEnabled?(t.test=`vitest run`,n[`vitest-config`]=`workspace:*`):r.push(`vitest-config`),{name:e.name,version:`1.0.0`,type:`module`,scripts:t,exports:`./src/index.ts`,devDependencies:n,startx:{iTags:[`node`],requiredDevDeps:[`typescript-config`],...r.length>0?{ignore:r}:{}}}}static async checkAndInstallMissingDeps(e){let n=await this.readRootPackage(e.workspace),r=await Iy.parsePnpmWorkspace({dir:e.workspace}),i=[],a=[];for(let[t,o]of Object.entries(Ly))if(o.tags.every(t=>e.tags.includes(t))&&!o.tags.includes(`root`))if(o.version.startsWith(`workspace:`))await this.workspacePackageExists(e.workspace,t)||a.push(t);else{if(this.hasDependency(n,t))continue;let e=r?.catalog?.[t]?`catalog:`:o.version;i.push({name:t,version:e,isDev:o.isDevDependency??!0})}if(a.length>0){Z_.warn(`The following workspace packages are missing from this monorepo:`);for(let e of a)Z_.warn(` - ${e} → run: startx package add ${e}`)}if(i.length!==0){Z_.warn(`The following npm dependencies are required but not installed:`);for(let e of i)Z_.warn(` - ${e.name}`);if(!await Xx.confirm({message:`Add them to the workspace root package.json?`,default:!0})){Z_.warn(`Skipping. Some features may not work correctly without these dependencies.`);return}for(let e of i)e.isDev?n.devDependencies[e.name]=e.version:n.dependencies[e.name]=e.version;await this.writeJson(t.join(e.workspace,`package.json`),n),Z_.info(`Added missing dependencies to root package.json.`),e.install!==!1&&await this.installRootDependencies(e.workspace)}}static async workspacePackageExists(e,n){let r=n.startsWith(`@`)?t.join(...n.split(`/`)):n,i=[t.join(e,`configs`,r,`package.json`),t.join(e,`packages`,r,`package.json`),t.join(e,`apps`,r,`package.json`)];for(let e of i)if(await this.pathExists(e))return!0;return!1}static getDestinationPath(e,n){let r=t.dirname(e),i=n.includes(`/`)?n.split(`/`).pop():n;return t.join(r,i)}static getDefaultPackagePath(e){if(e.startsWith(`@`)){let[n,r]=e.split(`/`);return t.join(`packages`,n,r)}return t.join(`packages`,e)}static findPackage(e,t){return e.find(e=>e.name===t||e.packageJson?.name===t)}static hasDependency(e,t){return!!(e.dependencies?.[t]||e.devDependencies?.[t]||e.peerDependencies?.[t])}static async readRootPackage(e){let n=await o.readFile(t.join(e,`package.json`),`utf-8`);return JSON.parse(n)}static async resolveDependencyVersion(e,t){return(await Iy.parsePnpmWorkspace({dir:e}))?.catalog?.[t]?`catalog:`:t===`eslint`?`^9.0.0`:`latest`}static async installRootDependencies(e){let t=(await this.readRootPackage(e)).packageManager?.split(`@`)[0]||`pnpm`,r=t===`yarn`?`yarn`:t,i=[`install`];Z_.info(`Running ${r} ${i.join(` `)} to install ESLint...`),await new Promise((t,a)=>{let o=n(r,i,{cwd:e,stdio:`inherit`,shell:process.platform===`win32`});o.on(`error`,a),o.on(`close`,e=>{if(e===0){t();return}a(Error(`${r} ${i.join(` `)} exited with code ${e}`))})}).catch(t=>{Z_.warn(`Could not install dependencies automatically: ${t instanceof Error?t.message:t}`),Z_.warn(`Run "${r} ${i.join(` `)}" manually in ${e}.`)})}static async syncDepsWithCatalog(e){let n=t.join(e.workspace,`pnpm-workspace.yaml`),r;try{r=await o.readFile(n,`utf-8`)}catch{return}let i=jy.parseDocument(r)
|
|
226
|
+
`),Z_.info(`Created package ${r} at ${t.relative(i.workspace,a)}`),Z_.info("Run `pnpm install` to link the new package.")}static async resolveEslintPreference(e){if(e.eslint===!1)return!1;let n=Iy.getDirectory(),r=await this.readRootPackage(n.workspace);return this.hasDependency(r,`eslint`)?e.eslint??!0:e.eslint===!0||await Xx.confirm({message:`ESLint is not installed in this monorepo. Install and enable it?`,default:!0})?(r.devDependencies={...r.devDependencies,eslint:await this.resolveDependencyVersion(n.workspace,`eslint`)},await this.writeJson(t.join(n.workspace,`package.json`),r),Z_.info(`Added eslint to the root devDependencies.`),e.install!==!1&&await this.installRootDependencies(n.workspace),!0):!1}static async getInstallTags(e){let t=new Set([`common`,`node`]),n=await this.readRootPackage(e.workspace);e.eslintEnabled&&t.add(`eslint`),this.hasDependency(n,`@biomejs/biome`)&&t.add(`biome`),this.hasDependency(n,`prettier`)&&t.add(`prettier`),this.hasDependency(n,`vitest`)&&t.add(`vitest`),this.hasDependency(n,`tsdown`)&&t.add(`tsdown`);for(let n of e.packages)n.packageJson?.startx?.tags?.forEach(e=>t.add(e)),n.packageJson?.startx?.iTags?.forEach(e=>t.add(e)),n.packageJson?.startx?.gTags?.forEach(e=>t.add(e)),(n.type===`apps`||n.packageJson?.startx?.mode===`standalone`)&&t.add(`runnable`);return Array.from(t)}static resolvePackageClosure(e){let t=new Map,n=[e.selectedPackage],r=r=>{let i=this.findPackage(e.packages,r);i&&!t.has(i.name)&&n.push(i)};for(e.includeEslintConfig&&r(`eslint-config`);n.length>0;){let e=n.shift();if(!t.has(e.name)){t.set(e.name,e);for(let t of[...e.packageJson?.startx?.requiredDeps??[],...e.packageJson?.startx?.requiredDevDeps??[]])r(t)}}return Array.from(t.values())}static async ensureTemplatePackage(e){let t=this.findPackage(e.packages,e.name);if(!t){Z_.warn(`Could not find template package ${e.name}; skipping.`);return}await this.installTemplatePackage({pkg:t,directory:e.directory,tags:e.tags})}static async installTemplatePackage(e){if(!e.pkg.packageJson)throw Error(`Missing package.json for ${e.pkg.name}`);let n=e.overrideRelativePath??e.pkg.relativePath,r=t.join(e.directory.workspace,n);if(await this.pathExists(t.join(r,`package.json`))&&!await Xx.confirm({message:`"${n}" already exists. Overwrite?`,default:!1})){Z_.info(`Skipping ${e.pkg.name}.`);return}let i=new Set([...e.tags,...e.pkg.packageJson.startx?.tags??[]]),a=e.pkg.packageJson.startx?.ignore??[];a.includes(`eslint-config`)&&i.delete(`eslint`),a.includes(`vitest-config`)&&i.delete(`vitest`);let{packageJson:o,isWorkspace:s}=By.handlePackageJson({app:e.pkg.packageJson,tags:Array.from(i),name:e.overrideName??e.pkg.packageJson.name??e.pkg.name});if(s)throw Error(`Cannot install workspace as a package: ${e.pkg.name}`);await this.syncDepsWithCatalog({workspace:e.directory.workspace,templateDir:e.directory.template,packageJson:o}),await Ny.writeJSONFile({dir:r,file:`package`,content:o}),await this.copyValidatedFilesFromFolder(e.pkg.path,r,i),await Ny.copyDirectory({from:t.join(e.pkg.path,`src`),to:t.join(r,`src`),exclude:i.has(`vitest`)?void 0:/\.test\.tsx?$/}),Z_.info(`Installed ${e.overrideName??e.pkg.name} at ${n}`)}static async copyValidatedFilesFromFolder(e,n,r){let i=await Ny.listFiles({dir:e}).catch(()=>[]);for(let a of i){let i=Py[a];i&&!i.tags.every(e=>r.has(e))||a!==`package.json`&&await Ny.copyFile({from:t.join(e,a),to:t.join(n,a)})}}static createPackageJson(e){let t={typecheck:`tsc --noEmit`,clean:`rimraf dist .turbo`},n={"typescript-config":`workspace:*`},r=[];return e.eslintEnabled?(t.lint=`eslint .`,t[`lint:fix`]=`eslint . --fix`,n[`eslint-config`]=`workspace:*`):r.push(`eslint-config`),e.vitestEnabled?(t.test=`vitest run`,n[`vitest-config`]=`workspace:*`):r.push(`vitest-config`),{name:e.name,version:`1.0.0`,type:`module`,scripts:t,exports:`./src/index.ts`,devDependencies:n,startx:{iTags:[`node`],requiredDevDeps:[`typescript-config`],...r.length>0?{ignore:r}:{}}}}static async checkAndInstallMissingDeps(e){let n=await this.readRootPackage(e.directory.workspace),r=await Iy.parsePnpmWorkspace({dir:e.directory.workspace}),i=[],a=[];for(let[t,o]of Object.entries(Ly))if(o.tags.every(t=>e.tags.includes(t))&&!o.tags.includes(`root`))if(o.version.startsWith(`workspace:`))await this.workspacePackageExists(e.directory.workspace,t)||a.push(t);else{if(this.hasDependency(n,t))continue;let e=r?.catalog?.[t]?`catalog:`:o.version;i.push({name:t,version:e,isDev:o.isDevDependency??!0})}if(a.length>0){Z_.warn(`The following workspace packages are missing from this monorepo:`);for(let e of a)Z_.warn(` - ${e} → run: startx package add ${e}`)}if(i.length!==0){Z_.warn(`The following npm dependencies are required but not installed:`);for(let e of i)Z_.warn(` - ${e.name}`);if(!await Xx.confirm({message:`Add them to the workspace root package.json?`,default:!0})){Z_.warn(`Skipping. Some features may not work correctly without these dependencies.`);return}n.devDependencies??={},n.dependencies??={};for(let e of i)e.isDev?n.devDependencies[e.name]=e.version:n.dependencies[e.name]=e.version;await this.writeJson(t.join(e.directory.workspace,`package.json`),n),Z_.info(`Added missing dependencies to root package.json.`),e.install!==!1&&await this.installRootDependencies(e.directory.workspace)}}static async workspacePackageExists(e,n){let r=n.startsWith(`@`)?t.join(...n.split(`/`)):n,i=[t.join(e,`configs`,r,`package.json`),t.join(e,`packages`,r,`package.json`),t.join(e,`apps`,r,`package.json`)];for(let e of i)if(await this.pathExists(e))return!0;return!1}static getDestinationPath(e,n){let r=t.dirname(e),i=n.includes(`/`)?n.split(`/`).pop():n;return t.join(r,i)}static getDefaultPackagePath(e){if(e.startsWith(`@`)){let[n,r]=e.split(`/`);return t.join(`packages`,n,r)}return t.join(`packages`,e)}static findPackage(e,t){return e.find(e=>e.name===t||e.packageJson?.name===t)}static hasDependency(e,t){return!!(e.dependencies?.[t]||e.devDependencies?.[t]||e.peerDependencies?.[t])}static async readRootPackage(e){let n=await o.readFile(t.join(e,`package.json`),`utf-8`);return JSON.parse(n)}static async resolveDependencyVersion(e,t){return(await Iy.parsePnpmWorkspace({dir:e}))?.catalog?.[t]?`catalog:`:t===`eslint`?`^9.0.0`:`latest`}static async installRootDependencies(e){let t=(await this.readRootPackage(e)).packageManager?.split(`@`)[0]||`pnpm`,r=t===`yarn`?`yarn`:t,i=[`install`];Z_.info(`Running ${r} ${i.join(` `)} to install ESLint...`),await new Promise((t,a)=>{let o=n(r,i,{cwd:e,stdio:`inherit`,shell:process.platform===`win32`});o.on(`error`,a),o.on(`close`,e=>{if(e===0){t();return}a(Error(`${r} ${i.join(` `)} exited with code ${e}`))})}).catch(t=>{Z_.warn(`Could not install dependencies automatically: ${t instanceof Error?t.message:t}`),Z_.warn(`Run "${r} ${i.join(` `)}" manually in ${e}.`)})}static async syncDepsWithCatalog(e){let n=t.join(e.workspace,`pnpm-workspace.yaml`),r;try{r=await o.readFile(n,`utf-8`)}catch{Z_.warn(`Could not find pnpm workspace file at ${n}.`);return}let i=jy.parseDocument(r);i.has(`catalog`)||i.set(`catalog`,{});let a=await this.loadTemplateCatalog(e.templateDir),s=e.packageJson.dependencies,c=e.packageJson.devDependencies,l={},u=e=>{if(e)for(let[t,n]of Object.entries(e)){if(n.startsWith(`workspace:`))continue;let r=i.hasIn([`catalog`,t]);if(n===`catalog:`){if(!r){let e=a[t];e&&(l[t]=e)}}else e[t]=`catalog:`,r||(l[t]=n)}};if(u(s),u(c),Object.keys(l).length!==0){for(let[e,t]of Object.entries(l))i.setIn([`catalog`,e],t);await o.writeFile(n,i.toString()),Z_.info(`Added to pnpm-workspace.yaml catalog:`);for(let[e,t]of Object.entries(l))Z_.info(` + ${e}: ${t}`)}}static async loadTemplateCatalog(e){try{let n=await o.readFile(t.join(e,`pnpm-workspace.yaml`),`utf-8`),r=jy.parseDocument(n).get(`catalog`);return r?r.toJSON():{}}catch{return Z_.warn(`Could not find pnpm-workspace.yaml template in ${e}.`),{}}}static async writeJson(e,t){await o.writeFile(e,`${JSON.stringify(t,null,2)}\n`)}static async pathExists(e){try{return await o.access(e),!0}catch{return!1}}},eS=`1.1.5`;const tS=new dv;tS.name(`startx`).description(`StartX CLI - Your all in one monorepo startup tool.`).version(eS),tS.command(`ping`).action(()=>{Z_.info(`pong`)}),tS.addCommand(Zx.command),tS.addCommand($x.command),tS.parse(process.argv);export{};
|
|
@@ -124,7 +124,7 @@ export class PackageCommand {
|
|
|
124
124
|
});
|
|
125
125
|
|
|
126
126
|
await this.checkAndInstallMissingDeps({
|
|
127
|
-
|
|
127
|
+
directory,
|
|
128
128
|
tags,
|
|
129
129
|
install: options.install,
|
|
130
130
|
});
|
|
@@ -444,10 +444,16 @@ export class PackageCommand {
|
|
|
444
444
|
},
|
|
445
445
|
};
|
|
446
446
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
447
|
+
private static async checkAndInstallMissingDeps(props: {
|
|
448
|
+
directory: {
|
|
449
|
+
template: string;
|
|
450
|
+
workspace: string;
|
|
451
|
+
};
|
|
452
|
+
tags: TAGS[];
|
|
453
|
+
install?: boolean;
|
|
454
|
+
}) {
|
|
455
|
+
const rootPackage = await this.readRootPackage(props.directory.workspace);
|
|
456
|
+
const pnpmWorkspace = await CliUtils.parsePnpmWorkspace({ dir: props.directory.workspace });
|
|
451
457
|
|
|
452
458
|
const missingNpm: Array<{ name: string; version: string; isDev: boolean }> = [];
|
|
453
459
|
const missingWorkspace: string[] = [];
|
|
@@ -457,8 +463,7 @@ export class PackageCommand {
|
|
|
457
463
|
if (config.tags.includes("root")) continue;
|
|
458
464
|
|
|
459
465
|
if (config.version.startsWith("workspace:")) {
|
|
460
|
-
|
|
461
|
-
const exists = await this.workspacePackageExists(props.workspace, dep);
|
|
466
|
+
const exists = await this.workspacePackageExists(props.directory.workspace, dep);
|
|
462
467
|
if (!exists) missingWorkspace.push(dep);
|
|
463
468
|
} else {
|
|
464
469
|
if (this.hasDependency(rootPackage, dep)) continue;
|
|
@@ -490,6 +495,9 @@ export class PackageCommand {
|
|
|
490
495
|
return;
|
|
491
496
|
}
|
|
492
497
|
|
|
498
|
+
rootPackage.devDependencies ??= {};
|
|
499
|
+
rootPackage.dependencies ??= {};
|
|
500
|
+
|
|
493
501
|
for (const dep of missingNpm) {
|
|
494
502
|
if (dep.isDev) {
|
|
495
503
|
(rootPackage.devDependencies as Record<string, string>)[dep.name] = dep.version;
|
|
@@ -498,19 +506,16 @@ export class PackageCommand {
|
|
|
498
506
|
}
|
|
499
507
|
}
|
|
500
508
|
|
|
501
|
-
await this.writeJson(path.join(props.workspace, "package.json"), rootPackage);
|
|
509
|
+
await this.writeJson(path.join(props.directory.workspace, "package.json"), rootPackage);
|
|
502
510
|
logger.info("Added missing dependencies to root package.json.");
|
|
503
511
|
|
|
504
512
|
if (props.install !== false) {
|
|
505
|
-
await this.installRootDependencies(props.workspace);
|
|
513
|
+
await this.installRootDependencies(props.directory.workspace);
|
|
506
514
|
}
|
|
507
515
|
}
|
|
508
|
-
|
|
509
516
|
private static async workspacePackageExists(workspace: string, packageName: string): Promise<boolean> {
|
|
510
517
|
// Resolve scoped names: @repo/lib → packages/@repo/lib
|
|
511
|
-
const subPath = packageName.startsWith("@")
|
|
512
|
-
? path.join(...packageName.split("/"))
|
|
513
|
-
: packageName;
|
|
518
|
+
const subPath = packageName.startsWith("@") ? path.join(...packageName.split("/")) : packageName;
|
|
514
519
|
|
|
515
520
|
const candidates = [
|
|
516
521
|
path.join(workspace, "configs", subPath, "package.json"),
|
|
@@ -591,7 +596,6 @@ export class PackageCommand {
|
|
|
591
596
|
logger.warn(`Run "${command} ${args.join(" ")}" manually in ${workspace}.`);
|
|
592
597
|
});
|
|
593
598
|
}
|
|
594
|
-
|
|
595
599
|
private static async syncDepsWithCatalog(props: {
|
|
596
600
|
workspace: string;
|
|
597
601
|
templateDir: string;
|
|
@@ -602,14 +606,16 @@ export class PackageCommand {
|
|
|
602
606
|
try {
|
|
603
607
|
content = await fs.readFile(workspacePath, "utf-8");
|
|
604
608
|
} catch {
|
|
609
|
+
logger.warn(`Could not find pnpm workspace file at ${workspacePath}.`);
|
|
605
610
|
return;
|
|
606
611
|
}
|
|
607
612
|
|
|
608
613
|
const doc = YAML.parseDocument(content);
|
|
609
|
-
const catalog = doc.getIn(["catalog"]) as Record<string, string> | undefined;
|
|
610
|
-
if (!catalog) return;
|
|
611
614
|
|
|
612
|
-
|
|
615
|
+
if (!doc.has("catalog")) {
|
|
616
|
+
doc.set("catalog", {});
|
|
617
|
+
}
|
|
618
|
+
|
|
613
619
|
const templateCatalog = await this.loadTemplateCatalog(props.templateDir);
|
|
614
620
|
|
|
615
621
|
const deps = props.packageJson.dependencies as Record<string, string> | undefined;
|
|
@@ -621,17 +627,16 @@ export class PackageCommand {
|
|
|
621
627
|
for (const [name, version] of Object.entries(depMap)) {
|
|
622
628
|
if (version.startsWith("workspace:")) continue;
|
|
623
629
|
|
|
630
|
+
const existsInUserCatalog = doc.hasIn(["catalog", name]);
|
|
631
|
+
|
|
624
632
|
if (version === "catalog:") {
|
|
625
|
-
|
|
626
|
-
// If not, resolve the real version from the template's catalog and add it.
|
|
627
|
-
if (!catalog[name]) {
|
|
633
|
+
if (!existsInUserCatalog) {
|
|
628
634
|
const templateVersion = templateCatalog[name];
|
|
629
635
|
if (templateVersion) newEntries[name] = templateVersion;
|
|
630
636
|
}
|
|
631
637
|
} else {
|
|
632
|
-
// Hardcoded version — normalize to catalog:
|
|
633
638
|
depMap[name] = "catalog:";
|
|
634
|
-
if (!
|
|
639
|
+
if (!existsInUserCatalog) newEntries[name] = version;
|
|
635
640
|
}
|
|
636
641
|
}
|
|
637
642
|
};
|
|
@@ -656,12 +661,14 @@ export class PackageCommand {
|
|
|
656
661
|
try {
|
|
657
662
|
const raw = await fs.readFile(path.join(templateDir, "pnpm-workspace.yaml"), "utf-8");
|
|
658
663
|
const doc = YAML.parseDocument(raw);
|
|
659
|
-
|
|
664
|
+
const catalogNode = doc.get("catalog") as YAML.Document | undefined;
|
|
665
|
+
|
|
666
|
+
return catalogNode ? (catalogNode.toJSON() as Record<string, string>) : {};
|
|
660
667
|
} catch {
|
|
668
|
+
logger.warn(`Could not find pnpm-workspace.yaml template in ${templateDir}.`);
|
|
661
669
|
return {};
|
|
662
670
|
}
|
|
663
671
|
}
|
|
664
|
-
|
|
665
672
|
private static async writeJson(file: string, content: object) {
|
|
666
673
|
await fs.writeFile(file, `${JSON.stringify(content, null, 2)}\n`);
|
|
667
674
|
}
|
|
@@ -14,7 +14,7 @@ COPY --parents packages/common/package.json ./
|
|
|
14
14
|
COPY --parents packages/ui/package.json ./
|
|
15
15
|
COPY --parents configs/*/package.json ./
|
|
16
16
|
|
|
17
|
-
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
|
|
17
|
+
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store,sharing=locked \
|
|
18
18
|
pnpm install --frozen-lockfile
|
|
19
19
|
|
|
20
20
|
COPY apps/web-client/ ./apps/web-client/
|
|
@@ -24,7 +24,7 @@ COPY configs ./configs
|
|
|
24
24
|
COPY turbo.json ./
|
|
25
25
|
|
|
26
26
|
# Build the required packages with Turbo cache mounted
|
|
27
|
-
RUN --mount=type=cache,id=turbo,target=/app/.turbo/cache \
|
|
27
|
+
RUN --mount=type=cache,id=turbo,target=/app/.turbo/cache,sharing=locked \
|
|
28
28
|
pnpm build --filter=web-client
|
|
29
29
|
|
|
30
30
|
# --- Final production image ---
|
|
@@ -18,10 +18,12 @@
|
|
|
18
18
|
"@dotenvx/dotenvx": "catalog:",
|
|
19
19
|
"@react-router/node": "catalog:",
|
|
20
20
|
"@react-router/serve": "catalog:",
|
|
21
|
+
"@hookform/resolvers": "catalog:",
|
|
21
22
|
"@repo/ui": "workspace:*",
|
|
22
23
|
"isbot": "catalog:",
|
|
23
24
|
"react": "catalog:",
|
|
24
25
|
"react-dom": "catalog:",
|
|
26
|
+
"motion": "catalog:",
|
|
25
27
|
"react-router": "catalog:",
|
|
26
28
|
"zustand": "catalog:",
|
|
27
29
|
"@repo/common": "workspace:*"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { cn } from "@repo/ui/lib/utils";
|
|
2
|
+
import { motion, useReducedMotion, AnimatePresence } from "motion/react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useNavigation } from "react-router";
|
|
5
|
+
|
|
6
|
+
type LoaderState = "idle" | "loading" | "complete";
|
|
7
|
+
|
|
8
|
+
export function NavigationLoader({ className }: { className?: string }) {
|
|
9
|
+
const navigation = useNavigation();
|
|
10
|
+
const prefersReducedMotion = useReducedMotion();
|
|
11
|
+
const [loaderState, setLoaderState] = React.useState<LoaderState>("idle");
|
|
12
|
+
|
|
13
|
+
const stateRef = React.useRef<LoaderState>(loaderState);
|
|
14
|
+
stateRef.current = loaderState;
|
|
15
|
+
|
|
16
|
+
React.useEffect(() => {
|
|
17
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
18
|
+
|
|
19
|
+
const isNavigating = navigation.state === "loading" || navigation.state === "submitting";
|
|
20
|
+
|
|
21
|
+
if (isNavigating) {
|
|
22
|
+
if (stateRef.current === "idle") {
|
|
23
|
+
timer = setTimeout(() => {
|
|
24
|
+
setLoaderState("loading");
|
|
25
|
+
}, 150);
|
|
26
|
+
}
|
|
27
|
+
} else if (navigation.state === "idle") {
|
|
28
|
+
if (stateRef.current === "loading") {
|
|
29
|
+
setLoaderState("complete");
|
|
30
|
+
timer = setTimeout(() => {
|
|
31
|
+
setLoaderState("idle");
|
|
32
|
+
}, 500);
|
|
33
|
+
} else {
|
|
34
|
+
setLoaderState("idle");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return () => clearTimeout(timer);
|
|
39
|
+
}, [navigation.state]);
|
|
40
|
+
|
|
41
|
+
if (prefersReducedMotion) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const progressVariants = {
|
|
46
|
+
idle: {
|
|
47
|
+
scaleX: 0,
|
|
48
|
+
opacity: 0,
|
|
49
|
+
transition: { duration: 0 },
|
|
50
|
+
},
|
|
51
|
+
loading: {
|
|
52
|
+
scaleX: 0.85,
|
|
53
|
+
opacity: 1,
|
|
54
|
+
transition: {
|
|
55
|
+
duration: 3.5,
|
|
56
|
+
ease: [0.08, 0.82, 0.17, 1],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
complete: {
|
|
60
|
+
scaleX: 1,
|
|
61
|
+
opacity: 0,
|
|
62
|
+
transition: {
|
|
63
|
+
scaleX: { duration: 0.3, ease: [0.22, 1, 0.36, 1] },
|
|
64
|
+
opacity: { duration: 0.3, delay: 0.1, ease: "linear" },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
} as const;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<AnimatePresence>
|
|
71
|
+
{loaderState !== "idle" && (
|
|
72
|
+
<div
|
|
73
|
+
role="progressbar"
|
|
74
|
+
aria-hidden={loaderState === "complete"}
|
|
75
|
+
aria-valuemin={0}
|
|
76
|
+
aria-valuemax={100}
|
|
77
|
+
className="pointer-events-none fixed inset-x-0 top-0 z-[9999] h-[3px] w-full bg-transparent"
|
|
78
|
+
>
|
|
79
|
+
<motion.div
|
|
80
|
+
className={cn("relative h-full w-full origin-left bg-primary/50", className)}
|
|
81
|
+
initial="idle"
|
|
82
|
+
animate={loaderState}
|
|
83
|
+
exit="idle"
|
|
84
|
+
variants={progressVariants}
|
|
85
|
+
style={{ willChange: "transform, opacity" }}
|
|
86
|
+
>
|
|
87
|
+
<motion.div
|
|
88
|
+
className="absolute inset-0 w-full bg-gradient-to-r from-transparent via-white/50 to-transparent dark:via-white/30"
|
|
89
|
+
initial={{ x: "-100%" }}
|
|
90
|
+
animate={{ x: "100%" }}
|
|
91
|
+
transition={{
|
|
92
|
+
repeat: Infinity,
|
|
93
|
+
duration: 1.2,
|
|
94
|
+
ease: "linear",
|
|
95
|
+
}}
|
|
96
|
+
/>
|
|
97
|
+
|
|
98
|
+
<div className="absolute right-0 top-0 h-full w-64 bg-gradient-to-r from-transparent to-primary opacity-100" />
|
|
99
|
+
|
|
100
|
+
<div className="absolute right-0 top-1/2 h-[5px] w-[15px] -translate-y-1/2 rounded-full bg-primary shadow-[0_0_12px_4px_hsl(var(--primary))]" />
|
|
101
|
+
</motion.div>
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</AnimatePresence>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { IPaginatedData } from "@repo/ui/api/use-api/api-types";
|
|
2
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@repo/ui/components/ui/select";
|
|
3
|
+
import { cn } from "@repo/ui/lib/utils";
|
|
4
|
+
import { ChevronFirst, ChevronLast, ChevronLeft, ChevronRight } from "@repo/ui/lucide";
|
|
5
|
+
import { motion, AnimatePresence, useReducedMotion } from "motion/react";
|
|
6
|
+
import React, { forwardRef, useMemo, type ButtonHTMLAttributes } from "react";
|
|
7
|
+
import usePagination from "~/hooks/pagination/usePagination";
|
|
8
|
+
|
|
9
|
+
export type PaginationVariant = "default" | "compact" | "ghost";
|
|
10
|
+
|
|
11
|
+
interface PaginationSectionProps<T> {
|
|
12
|
+
pagination?: IPaginatedData<T>["pagination"];
|
|
13
|
+
pageKey?: string;
|
|
14
|
+
limitKey?: string;
|
|
15
|
+
limit?: number;
|
|
16
|
+
variant?: PaginationVariant;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const emptyPagination: IPaginatedData<unknown>["pagination"] = {
|
|
21
|
+
total: 0,
|
|
22
|
+
totalPages: 0,
|
|
23
|
+
currentPage: 0,
|
|
24
|
+
pageSize: 0,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const DEFAULT_LIMIT_LIST = [5, 10, 20, 30, 40, 50];
|
|
28
|
+
|
|
29
|
+
const SPRING_TRANSITION = {
|
|
30
|
+
type: "spring",
|
|
31
|
+
stiffness: 500,
|
|
32
|
+
damping: 30,
|
|
33
|
+
mass: 1,
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
interface PaginationNavButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
37
|
+
icon: React.ElementType;
|
|
38
|
+
label: string;
|
|
39
|
+
isHiddenOnMobile?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const PaginationNavButton = forwardRef<HTMLButtonElement, PaginationNavButtonProps>(
|
|
43
|
+
({ icon: Icon, label, disabled, isHiddenOnMobile, onClick, ...props }, ref) => {
|
|
44
|
+
const shouldReduceMotion = useReducedMotion();
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<motion.button
|
|
48
|
+
ref={ref}
|
|
49
|
+
aria-label={label}
|
|
50
|
+
aria-disabled={disabled}
|
|
51
|
+
disabled={disabled}
|
|
52
|
+
onClick={onClick}
|
|
53
|
+
whileHover={!disabled && !shouldReduceMotion ? { scale: 1.05 } : undefined}
|
|
54
|
+
whileTap={!disabled && !shouldReduceMotion ? { scale: 0.95 } : undefined}
|
|
55
|
+
transition={SPRING_TRANSITION}
|
|
56
|
+
className={cn(
|
|
57
|
+
"relative flex h-9 w-9 md:h-8 md:w-8 items-center justify-center rounded-md border border-transparent bg-transparent text-muted-foreground outline-none transition-colors",
|
|
58
|
+
"focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/50",
|
|
59
|
+
"hover:bg-secondary/80 hover:text-foreground",
|
|
60
|
+
"disabled:pointer-events-none disabled:opacity-40",
|
|
61
|
+
isHiddenOnMobile && "hidden sm:flex",
|
|
62
|
+
props.className
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
<Icon className="h-4 w-4" aria-hidden="true" />
|
|
66
|
+
</motion.button>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
PaginationNavButton.displayName = "PaginationNavButton";
|
|
71
|
+
|
|
72
|
+
function PaginationFooter<T>({
|
|
73
|
+
pagination,
|
|
74
|
+
pageKey,
|
|
75
|
+
limitKey,
|
|
76
|
+
limit,
|
|
77
|
+
variant = "default",
|
|
78
|
+
className,
|
|
79
|
+
}: PaginationSectionProps<T>) {
|
|
80
|
+
const prefersReducedMotion = useReducedMotion();
|
|
81
|
+
const { next, prev, setLimit, setPage } = usePagination({
|
|
82
|
+
pageKey,
|
|
83
|
+
limitKey,
|
|
84
|
+
limit,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const data = pagination || emptyPagination;
|
|
88
|
+
|
|
89
|
+
const limitList = useMemo(() => {
|
|
90
|
+
const list = [...DEFAULT_LIMIT_LIST];
|
|
91
|
+
if (data.pageSize > 0 && !list.includes(data.pageSize)) {
|
|
92
|
+
list.push(data.pageSize);
|
|
93
|
+
list.sort((a, b) => a - b);
|
|
94
|
+
}
|
|
95
|
+
return list;
|
|
96
|
+
}, [data.pageSize]);
|
|
97
|
+
|
|
98
|
+
if (data.total === 0) return null;
|
|
99
|
+
|
|
100
|
+
const isFirstPage = data.currentPage <= 1;
|
|
101
|
+
const isLastPage = data.currentPage >= data.totalPages;
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<nav
|
|
105
|
+
aria-label="Pagination Navigation"
|
|
106
|
+
className={cn(
|
|
107
|
+
"flex w-full flex-col-reverse items-center justify-between gap-4 sm:flex-row",
|
|
108
|
+
variant === "default" && "rounded-xl border bg-background/50 p-2 shadow-sm backdrop-blur-sm",
|
|
109
|
+
variant === "compact" && "rounded-lg border border-transparent py-1",
|
|
110
|
+
variant === "ghost" && "py-2",
|
|
111
|
+
className
|
|
112
|
+
)}
|
|
113
|
+
>
|
|
114
|
+
<div className="flex w-full items-center justify-between sm:w-auto sm:justify-start gap-4">
|
|
115
|
+
<div className="flex items-center gap-2">
|
|
116
|
+
<label
|
|
117
|
+
htmlFor="rows-per-page-select"
|
|
118
|
+
className={cn(
|
|
119
|
+
"whitespace-nowrap font-medium text-muted-foreground",
|
|
120
|
+
variant === "compact" ? "text-xs" : "text-sm"
|
|
121
|
+
)}
|
|
122
|
+
>
|
|
123
|
+
Rows per page
|
|
124
|
+
</label>
|
|
125
|
+
<Select value={`${data.pageSize}`} onValueChange={value => setLimit(parseInt(value, 10))}>
|
|
126
|
+
<SelectTrigger
|
|
127
|
+
id="rows-per-page-select"
|
|
128
|
+
className={cn(
|
|
129
|
+
"h-8 w-[72px] transition-colors hover:bg-secondary/50 focus-visible:ring-2 focus-visible:ring-ring/50",
|
|
130
|
+
variant === "compact" && "h-7 text-xs"
|
|
131
|
+
)}
|
|
132
|
+
>
|
|
133
|
+
<SelectValue placeholder={`${data.pageSize}`} />
|
|
134
|
+
</SelectTrigger>
|
|
135
|
+
<SelectContent side="top" className="min-w-[72px]">
|
|
136
|
+
{limitList.map(pageSize => (
|
|
137
|
+
<SelectItem key={pageSize} value={`${pageSize}`} className="cursor-pointer transition-colors">
|
|
138
|
+
{pageSize}
|
|
139
|
+
</SelectItem>
|
|
140
|
+
))}
|
|
141
|
+
</SelectContent>
|
|
142
|
+
</Select>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div className="flex w-full items-center justify-between sm:w-auto gap-4 sm:gap-6">
|
|
147
|
+
<div
|
|
148
|
+
className={cn(
|
|
149
|
+
"flex flex-1 items-center justify-center font-medium tabular-nums text-muted-foreground sm:justify-end",
|
|
150
|
+
variant === "compact" ? "text-xs" : "text-sm"
|
|
151
|
+
)}
|
|
152
|
+
aria-live="polite"
|
|
153
|
+
>
|
|
154
|
+
<span className="mr-1">Page</span>
|
|
155
|
+
<div className="relative flex min-w-[1ch] items-center justify-center overflow-hidden">
|
|
156
|
+
<AnimatePresence mode="popLayout" initial={false}>
|
|
157
|
+
<motion.span
|
|
158
|
+
key={data.currentPage}
|
|
159
|
+
initial={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 10, filter: "blur(4px)" }}
|
|
160
|
+
animate={prefersReducedMotion ? { opacity: 1 } : { opacity: 1, y: 0, filter: "blur(0px)" }}
|
|
161
|
+
exit={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: -10, filter: "blur(4px)" }}
|
|
162
|
+
transition={SPRING_TRANSITION}
|
|
163
|
+
className="inline-block text-foreground"
|
|
164
|
+
>
|
|
165
|
+
{data.currentPage}
|
|
166
|
+
</motion.span>
|
|
167
|
+
</AnimatePresence>
|
|
168
|
+
</div>
|
|
169
|
+
<span className="ml-1">of {data.totalPages}</span>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div className="flex items-center gap-1 sm:gap-1.5">
|
|
173
|
+
<PaginationNavButton
|
|
174
|
+
icon={ChevronFirst}
|
|
175
|
+
label="Go to first page"
|
|
176
|
+
onClick={() => setPage(1)}
|
|
177
|
+
disabled={isFirstPage}
|
|
178
|
+
isHiddenOnMobile
|
|
179
|
+
/>
|
|
180
|
+
<PaginationNavButton
|
|
181
|
+
icon={ChevronLeft}
|
|
182
|
+
label="Go to previous page"
|
|
183
|
+
onClick={() => prev()}
|
|
184
|
+
disabled={isFirstPage}
|
|
185
|
+
/>
|
|
186
|
+
<PaginationNavButton
|
|
187
|
+
icon={ChevronRight}
|
|
188
|
+
label="Go to next page"
|
|
189
|
+
onClick={() => next()}
|
|
190
|
+
disabled={isLastPage}
|
|
191
|
+
/>
|
|
192
|
+
<PaginationNavButton
|
|
193
|
+
icon={ChevronLast}
|
|
194
|
+
label="Go to last page"
|
|
195
|
+
onClick={() => setPage(data.totalPages)}
|
|
196
|
+
disabled={isLastPage}
|
|
197
|
+
isHiddenOnMobile
|
|
198
|
+
/>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</nav>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export default PaginationFooter;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useUrlParams } from "../utils/use-url-params";
|
|
2
|
+
|
|
3
|
+
const usePagination = (options?: { limit?: number; pageKey?: string; limitKey?: string }) => {
|
|
4
|
+
const pageKey = options?.pageKey ?? "page";
|
|
5
|
+
const limitKey = options?.limitKey ?? "limit";
|
|
6
|
+
|
|
7
|
+
const { params, setParams } = useUrlParams({
|
|
8
|
+
[pageKey]: 1,
|
|
9
|
+
[limitKey]: 10,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
limit: params[limitKey],
|
|
14
|
+
page: params[pageKey],
|
|
15
|
+
setPage: (page: number) => setParams({ [pageKey]: page }),
|
|
16
|
+
setLimit: (limit: number) => setParams({ [limitKey]: limit }),
|
|
17
|
+
next: () => setParams({ [pageKey]: Number(params[pageKey]) + 1 }),
|
|
18
|
+
prev: () => setParams({ [pageKey]: Number(params[pageKey]) - 1 }),
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default usePagination;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useCallback, useEffect } from "react";
|
|
2
|
+
import { useSearchParams } from "react-router";
|
|
3
|
+
|
|
4
|
+
type StringRecord = Record<string, string | number>;
|
|
5
|
+
|
|
6
|
+
export type UseUrlParamsReturn<T extends StringRecord> = {
|
|
7
|
+
params: T;
|
|
8
|
+
setParam: <K extends keyof T>(key: K, value: T[K]) => void;
|
|
9
|
+
setParams: (values: Partial<T>) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function getParams<T extends StringRecord>(searchParams: URLSearchParams, defaults: T): T {
|
|
13
|
+
const result = {} as T;
|
|
14
|
+
|
|
15
|
+
for (const key of Object.keys(defaults) as Array<keyof T>) {
|
|
16
|
+
result[key] = (searchParams.get(key as string) ?? defaults[key]) as T[keyof T];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useUrlParams<T extends StringRecord>(defaults: T): UseUrlParamsReturn<T> {
|
|
23
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
24
|
+
|
|
25
|
+
const params = getParams(searchParams, defaults);
|
|
26
|
+
|
|
27
|
+
// Ensure non-empty defaults exist in URL
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
let changed = false;
|
|
30
|
+
const next = new URLSearchParams(searchParams);
|
|
31
|
+
|
|
32
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
33
|
+
if (value === "") continue;
|
|
34
|
+
|
|
35
|
+
if (!next.has(key)) {
|
|
36
|
+
if (typeof value === "string") next.set(key, value);
|
|
37
|
+
else next.set(key, String(value));
|
|
38
|
+
changed = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (changed) {
|
|
43
|
+
setSearchParams(next, { replace: true });
|
|
44
|
+
}
|
|
45
|
+
}, [searchParams, defaults, setSearchParams]);
|
|
46
|
+
|
|
47
|
+
const setParam = useCallback(
|
|
48
|
+
<K extends keyof T>(key: K, value: T[K]) => {
|
|
49
|
+
setSearchParams((current) => {
|
|
50
|
+
const next = new URLSearchParams(current);
|
|
51
|
+
|
|
52
|
+
if (value === "") {
|
|
53
|
+
next.delete(key as string);
|
|
54
|
+
} else {
|
|
55
|
+
if (typeof value === "string") next.set(key as string, value);
|
|
56
|
+
else next.set(key as string, String(value));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return next;
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
[setSearchParams],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const setParams = useCallback(
|
|
66
|
+
(values: Partial<T>) => {
|
|
67
|
+
setSearchParams((current) => {
|
|
68
|
+
const next = new URLSearchParams(current);
|
|
69
|
+
|
|
70
|
+
for (const [key, value] of Object.entries(values)) {
|
|
71
|
+
if (value == null || value === "") {
|
|
72
|
+
next.delete(key);
|
|
73
|
+
} else {
|
|
74
|
+
next.set(key, value as string);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return next;
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
[setSearchParams],
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
params,
|
|
86
|
+
setParam,
|
|
87
|
+
setParams,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function useUrlParamsValue<T extends StringRecord>(defaults: T): T {
|
|
92
|
+
const [searchParams] = useSearchParams();
|
|
93
|
+
|
|
94
|
+
return getParams(searchParams, defaults);
|
|
95
|
+
}
|
|
@@ -4,6 +4,7 @@ import { ThemeProvider } from "@repo/ui/components/custom/theme-provider";
|
|
|
4
4
|
import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
|
|
5
5
|
import type { Route } from "./+types/root";
|
|
6
6
|
import "./app.css";
|
|
7
|
+
import { NavigationLoader } from "./components/utils/navigation-loader";
|
|
7
8
|
import { AuthStartup } from "./config/auth/auth-state";
|
|
8
9
|
import { ENV } from "./config/env";
|
|
9
10
|
|
|
@@ -31,6 +32,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|
|
31
32
|
<Links />
|
|
32
33
|
</head>
|
|
33
34
|
<body>
|
|
35
|
+
<NavigationLoader />
|
|
34
36
|
{children}
|
|
35
37
|
<ScrollRestoration />
|
|
36
38
|
<Scripts />
|
package/package.json
CHANGED
|
@@ -163,7 +163,18 @@ export abstract class IUserSession {
|
|
|
163
163
|
refreshToken: newRefreshToken,
|
|
164
164
|
};
|
|
165
165
|
}
|
|
166
|
+
public async createAccessToken(sessionId: string): Promise<string | null> {
|
|
167
|
+
const session = await this.getSession(sessionId);
|
|
168
|
+
if (!session) return null;
|
|
166
169
|
|
|
170
|
+
const accessToken = TokenModule.signAccessToken({
|
|
171
|
+
userID: session.user.id,
|
|
172
|
+
email: session.user.email,
|
|
173
|
+
sessionID: sessionId,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return accessToken;
|
|
177
|
+
}
|
|
167
178
|
public async endSession(sessionId: string): Promise<void> {
|
|
168
179
|
const session = await this.getSession(sessionId);
|
|
169
180
|
if (!session) return;
|
package/packages/ui/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { QueryProvider } from "./query-provider";
|
|
1
|
+
import { QueryProvider, queryClient } from "./query-provider";
|
|
2
2
|
import { ApiSchema } from "./use-api/api-builder";
|
|
3
3
|
import { useApi } from "./use-api/react-query/use-api";
|
|
4
4
|
import { useApiClient } from "./use-api/react-query/use-api-client";
|
|
5
|
-
|
|
6
|
-
export { QueryProvider, ApiSchema, useApi, useApiClient };
|
|
5
|
+
export * from "@tanstack/react-query";
|
|
6
|
+
export { QueryProvider, ApiSchema, useApi, useApiClient, queryClient };
|
|
@@ -1,32 +1,39 @@
|
|
|
1
|
-
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
1
|
+
import { QueryClient, QueryClientProvider, type QueryClientConfig } from "@tanstack/react-query";
|
|
2
2
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
|
3
|
-
import {
|
|
3
|
+
import { useState } from "react";
|
|
4
4
|
import { ApiHelper } from "./use-api/api-helpers";
|
|
5
|
-
|
|
6
|
-
const queryClient = new QueryClient({
|
|
7
|
-
defaultOptions: {
|
|
8
|
-
queries: {
|
|
9
|
-
refetchOnMount: false,
|
|
10
|
-
refetchOnWindowFocus: false,
|
|
11
|
-
staleTime: ApiHelper.parseTime("5:min"),
|
|
12
|
-
},
|
|
13
|
-
mutations: {
|
|
14
|
-
onError(error: any) {
|
|
15
|
-
console.error(error?.response?.data);
|
|
16
|
-
// toast.error(error.response?.data?.message ?? error.message);
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
});
|
|
21
|
-
export const QueryProvider = ({
|
|
22
|
-
children,
|
|
23
|
-
mode,
|
|
24
|
-
}: {
|
|
25
|
-
children: ReactNode;
|
|
5
|
+
type QueryProviderProps = Omit<QueryClientConfig, "client"> & {
|
|
26
6
|
mode?: "development" | "production" | "staging";
|
|
27
|
-
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
};
|
|
9
|
+
export const createQueryClient = (config?: QueryClientConfig) =>
|
|
10
|
+
new QueryClient(
|
|
11
|
+
ApiHelper.merge(
|
|
12
|
+
{
|
|
13
|
+
defaultOptions: {
|
|
14
|
+
queries: {
|
|
15
|
+
refetchOnMount: false,
|
|
16
|
+
refetchOnWindowFocus: false,
|
|
17
|
+
staleTime: ApiHelper.parseTime("5:min"),
|
|
18
|
+
},
|
|
19
|
+
mutations: {
|
|
20
|
+
onError(error: any) {
|
|
21
|
+
console.error(error?.response?.data);
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
config
|
|
27
|
+
)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export const queryClient = createQueryClient();
|
|
31
|
+
|
|
32
|
+
export const QueryProvider = ({ children, mode, ...config }: QueryProviderProps) => {
|
|
33
|
+
const [client] = useState(() => createQueryClient(config));
|
|
34
|
+
|
|
28
35
|
return (
|
|
29
|
-
<QueryClientProvider client={
|
|
36
|
+
<QueryClientProvider client={client}>
|
|
30
37
|
{mode === "development" && <ReactQueryDevtools initialIsOpen={false} />}
|
|
31
38
|
{children}
|
|
32
39
|
</QueryClientProvider>
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "@tanstack/react-query";
|
|
9
9
|
import { type AxiosError, type AxiosInstance, type AxiosRequestConfig } from "axios";
|
|
10
10
|
import { useMemo, useRef } from "react";
|
|
11
|
+
import { toast } from "sonner";
|
|
11
12
|
import { z } from "zod";
|
|
12
13
|
import { FormUtils } from "@repo/ui/lib/utils";
|
|
13
14
|
import { ApiHelper } from "../api-helpers";
|
|
@@ -250,6 +251,13 @@ function useMutationApi<Schema extends RawSchema, K extends keyof Schema & strin
|
|
|
250
251
|
queryClient,
|
|
251
252
|
options?.overwriteEvents
|
|
252
253
|
);
|
|
254
|
+
if (!options?.onError && !endpoint.onError) {
|
|
255
|
+
try {
|
|
256
|
+
toast.error((error as any)?.response?.data?.message ?? error.message);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.error(error);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
253
261
|
},
|
|
254
262
|
onMutate: options?.onMutate,
|
|
255
263
|
});
|
|
@@ -103,7 +103,7 @@ export const FormSelectField = <
|
|
|
103
103
|
}}
|
|
104
104
|
value={field.value ?? defaultValueProp ?? ""}
|
|
105
105
|
>
|
|
106
|
-
<SelectTrigger className={cn("bg-background w-full", inputClassName)}>
|
|
106
|
+
<SelectTrigger aria-invalid={!!fieldState.error} className={cn("bg-background w-full", inputClassName)}>
|
|
107
107
|
<SelectValue placeholder={placeholder ?? "Select"} />
|
|
108
108
|
</SelectTrigger>
|
|
109
109
|
<SelectContent>
|
|
@@ -155,7 +155,7 @@ export const FormTextField = <
|
|
|
155
155
|
<Field data-invalid={!!fieldState.error} className="flex flex-col">
|
|
156
156
|
{label ? <FieldLabel className="self-start">{label}</FieldLabel> : null}
|
|
157
157
|
<FieldContent>
|
|
158
|
-
<Input {...field} {...rest} />
|
|
158
|
+
<Input aria-invalid={!!fieldState.error} {...field} {...rest} />
|
|
159
159
|
</FieldContent>
|
|
160
160
|
{description ? <FieldDescription>{description}</FieldDescription> : null}
|
|
161
161
|
<FieldError errors={[fieldState.error]} />
|
|
@@ -187,7 +187,7 @@ export const FormTextAreaField = <
|
|
|
187
187
|
<Field data-invalid={!!fieldState.error} className="flex flex-col">
|
|
188
188
|
{label ? <FieldLabel className="self-start">{label}</FieldLabel> : null}
|
|
189
189
|
<FieldContent>
|
|
190
|
-
<Textarea {...field} {...rest} ref={field.ref} />
|
|
190
|
+
<Textarea aria-invalid={!!fieldState.error} {...field} {...rest} ref={field.ref} />
|
|
191
191
|
</FieldContent>
|
|
192
192
|
{description ? <FieldDescription>{description}</FieldDescription> : null}
|
|
193
193
|
<FieldError errors={[fieldState.error]} />
|
|
@@ -218,6 +218,7 @@ export const FormNumberField = <
|
|
|
218
218
|
<FieldContent>
|
|
219
219
|
<Input
|
|
220
220
|
inputMode="numeric"
|
|
221
|
+
aria-invalid={!!fieldState.error}
|
|
221
222
|
{...field}
|
|
222
223
|
onChange={e => {
|
|
223
224
|
if (!isNaN(Number(e.currentTarget.value))) field.onChange(Number(e.currentTarget.value));
|
|
@@ -363,6 +364,7 @@ export const FormDefaultDateField = <
|
|
|
363
364
|
{label ? <FieldLabel className="self-start">{label}</FieldLabel> : null}
|
|
364
365
|
<FieldContent>
|
|
365
366
|
<Input
|
|
367
|
+
aria-invalid={!!fieldState.error}
|
|
366
368
|
{...field}
|
|
367
369
|
{...rest}
|
|
368
370
|
value={field.value || ""}
|
|
@@ -430,7 +432,12 @@ export function FormMultiSelectField<
|
|
|
430
432
|
<FieldContent className="w-full">
|
|
431
433
|
<Popover open={open} onOpenChange={setOpen}>
|
|
432
434
|
<PopoverTrigger asChild disabled={disabled}>
|
|
433
|
-
<Button
|
|
435
|
+
<Button
|
|
436
|
+
aria-invalid={!!fieldState.error}
|
|
437
|
+
variant="outline"
|
|
438
|
+
size={"sm"}
|
|
439
|
+
className={cn("w-full justify-between", inputClassName)}
|
|
440
|
+
>
|
|
434
441
|
{selected.length > 0 ? `${selected.length} selected` : (placeholder ?? "Select")}
|
|
435
442
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
436
443
|
</Button>
|
package/pnpm-workspace.yaml
CHANGED
|
@@ -4,6 +4,15 @@ packages:
|
|
|
4
4
|
- "packages/*/*"
|
|
5
5
|
- "configs/*"
|
|
6
6
|
|
|
7
|
+
allowBuilds:
|
|
8
|
+
"@firebase/util": false
|
|
9
|
+
better-sqlite3: false
|
|
10
|
+
esbuild: false
|
|
11
|
+
msgpackr-extract: false
|
|
12
|
+
protobufjs: false
|
|
13
|
+
sharp: false
|
|
14
|
+
unrs-resolver: false
|
|
15
|
+
|
|
7
16
|
catalog:
|
|
8
17
|
# configs
|
|
9
18
|
typescript: "^5.9.2"
|
|
@@ -75,6 +84,7 @@ catalog:
|
|
|
75
84
|
tailwindcss: "^4.2.2"
|
|
76
85
|
"@tailwindcss/vite": "^4.2.2"
|
|
77
86
|
zustand: "^5.0.13"
|
|
87
|
+
motion: "^12.40.0"
|
|
78
88
|
|
|
79
89
|
# email
|
|
80
90
|
react-email: "^5.2.11"
|
|
@@ -88,7 +98,7 @@ catalog:
|
|
|
88
98
|
"@tanstack/react-query-devtools": "^5.100.9"
|
|
89
99
|
"@fontsource-variable/eb-garamond": "^5.2.7"
|
|
90
100
|
"@fontsource-variable/inter": "^5.2.8"
|
|
91
|
-
"@hookform/resolvers": "^
|
|
101
|
+
"@hookform/resolvers": "^5.4.0"
|
|
92
102
|
"@phosphor-icons/react": "^2.1.10"
|
|
93
103
|
"@tailwindcss/postcss": "^4"
|
|
94
104
|
"@tailwindcss/typography": "^0.5.19"
|
package/turbo.json
CHANGED
|
@@ -9,11 +9,19 @@
|
|
|
9
9
|
|
|
10
10
|
"build": {
|
|
11
11
|
"cache": true,
|
|
12
|
-
"inputs": [
|
|
12
|
+
"inputs": [
|
|
13
|
+
"src/**",
|
|
14
|
+
"public/**",
|
|
15
|
+
"tsdown.config.ts",
|
|
16
|
+
"vite.config.*",
|
|
17
|
+
"react-router.config.*",
|
|
18
|
+
"eslint.config.*",
|
|
19
|
+
"tsconfig*.json",
|
|
20
|
+
"package.json"
|
|
21
|
+
],
|
|
13
22
|
"outputs": ["dist/**", "build/**"],
|
|
14
23
|
"dependsOn": ["lint", "^build"]
|
|
15
24
|
},
|
|
16
|
-
|
|
17
25
|
"typecheck": {
|
|
18
26
|
"cache": true,
|
|
19
27
|
"dependsOn": ["^typecheck", "build"],
|