Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84772ffdd6 | ||
|
|
bdd9fc4602 | ||
|
|
d8b20fd513 | ||
|
|
9200529bf0 | ||
|
|
649d6c3fd0 | ||
|
|
0e77ea1139 | ||
|
|
47c70471e1 | ||
|
|
24f3311006 | ||
|
|
9a175925b6 | ||
|
|
716cc5f6f1 |
No files matched your search
@@ -12,10 +12,11 @@ DOTENV_PUBLIC_KEY_DEV="02a9d0f6522086b04ac7763e008a6ad0273c58258715e613c977fd6c8
|
||||
DOTENV_PUBLIC_KEY="02a9d0f6522086b04ac7763e008a6ad0273c58258715e613c977fd6c82a2030d92"
|
||||
|
||||
# .env
|
||||
COM_ERMNVLDMR_STATIC_S3_BUCKET="encrypted:BL1vJLB9uPSvsUvrJah4UegRU8uPdFjE7sya+Fae3rwOmVzWsK5+TP4z4YzigTAuwit4FANvANvhRV46se+ZgprYplU8Cw1tZ5mHKO4rWAjif2sJCnsGXUKp8GZ2J7PrMH35MY0YrXX4NVqe18b2NEv2B9K7"
|
||||
COM_ERMNVLDMR_STATIC_S3_REGION="encrypted:BAj3iuo7tWWNR2V4yQ+LHnFCB4JcZp7TTfUtvv+2FPntNhwc/fV95IeFWz6S0z6aPRkDG+UQzPXNwdMz7y8ZbTHbduDDrgydHu1fST1NfQd2s1Bbpf+yJTG7dPBSc5RSNKZkkpB4v55qg/UC"
|
||||
COM_ERMNVLDMR_STATIC_S3_ENDPOINT_URL="encrypted:BNkQrgsrxtz544vuJbgwWUh5cPjHG5wbmHNcxTUWrzbFJJsdncaAitviCvIrS7Q41w+FiWkssv3D05Tu8Inn2P62E6G3PU4gNxO2oLdmGEdrzUqB/xvxOvLvos42j54QOZf7lOqMPKH6SL2/rheCAo10gSCicCqkfh4xsCAbYpyL"
|
||||
COM_ERMNVLDMR_STATIC_AWS_ACCESS_KEY_ID="encrypted:BCsp8p4bp3cB+9grxHo634isRUaFGU3MOc1lr8ClSFRYLjqjqukyrbJCTZWV2Xdo8iQ33EylmbzUltbwzTWMQcjJSvIskesVLyV93q60o/Mu9L9jg/apwHuBgHv4zVuHstJ0zkHWgapbNTIlWD3RCm0ujeX/H1gBH3U="
|
||||
COM_ERMNVLDMR_STATIC_AWS_SECRET_ACCESS_KEY="encrypted:BM7aPliOyiPrqFPZMfqpn534Nu+Y0Nbh/YRoxv2KY6XAi2dNO0LSEf97DqSUQYdsnu9iibZmVNktF2K/D6mtHr9+gumF6KsmaLH65KE7c9ly7NdFQHL9I26wxRq1Sr3Zgrfs954J6oURSlBRKqJFdcTsBltuuupGuqJHyLS9+DE0jD+bl0Qg0lA="
|
||||
COM_ERMNVLDMR_STATIC_S3_BUCKET="encrypted:BE65d5I9m1jrHGVNKDmfGL3Cjb7LjhB4tVWyNzSPU1Zu5Y00+R8NXgqSBrzBnXVj3vVinibjihVt3fStegyiqI52GtSycX1NlFSVJlRFWadUJ6SVK+SYfB7sFKSJ2jKGfwRaXKMzaKCdPWY+ni685oPXU9Rp"
|
||||
COM_ERMNVLDMR_STATIC_S3_REGION="encrypted:BPQ2+ms76A+PLFUeKL1Cnz8bdOcsJou03e8AyGwzdhozpcAwhP5yWXSqhTK8ZccAvmfjJcM4NK+x2a2r4g0ALd/UaQDa9xy2eZ6QnXwkoHXu2ce+F86/HuFnzX0qWqYTWi4q+N4="
|
||||
COM_ERMNVLDMR_STATIC_S3_ENDPOINT_URL="encrypted:BCcK2ih1QrABMImpUj31KIQYlbp9zhnafzrXINAwvyCaCjbOst6UGnhhi5M3LtQ+Fhgpx52ZKGjmke1oVxnvU6BBGz/lAbSD5euhHNbJ6EIoSqc25XpcmgucIQGx1cfiJxsL4sDR95FyR3fNaS9KGr005DwQaIRKGg86gALUCPhaLekq/Sd6Nn+TDoqmigLJPozdxp33AFpmWF+EwayYae2u"
|
||||
COM_ERMNVLDMR_STATIC_AWS_ACCESS_KEY_ID="encrypted:BPaBvzNo1eH3Z2ro+NFq4zLi4krZQT48h20r0etBfVFLDNS01gh5Uosy70Ip29rC8sa1JoP4Q8isJ2Xz0pr5Vxxa62vVFOJ9wJ6LkXbYF2bUm+UNyjfsIztJsMyiVZfPEEPEEKcniXZ3faqePwgLwCZSTa/Qry6znNmMV5FshSsP"
|
||||
COM_ERMNVLDMR_STATIC_AWS_SECRET_ACCESS_KEY="encrypted:BFiOxOo/TNvSIMOjyeOswLcl+9AVdqp4axW+I46c1CL0ckSucQS+7CLpO8fNfakmW2bSibqru+xfWUbQT7FDH1YB3CPXUc0qPm1PsG0FiLixDefQuDrC/JHuA0Z3/dOAkn/e94BBuPtyJXxcLRP1v517MijPa0MuNqhI9BS3jFHKjWivqc9AVPs2ybglo14b/nEO1GqlM0TYflPoDhHoWjM="
|
||||
SOPS_AGE_KEY="encrypted:BKiJVxKYhiadu0kNCw8wVLYdaEWZreAVu2PaujxhvcXeCowCGjivEuQD/uhduAy/CwwDWyQkgQJ2FJMEKA47e7QDjS9TMdPCXO19/teR7LThgqTf6AbSM3sebln5zalgyplay4A/7cVytPXuOvVkaYyoh9CgzTVsPJKXRq5yMaDEx/9/91Yo8zUMF3z1vq3/preI5baK4/1xXewR3OwfZqnNGyd7LXkdqyT5"
|
||||
COM_ERMNVLDMR_DEV_ROOT_CA_KEY="encrypted:BP+GhCwutIMF2hSxIs92VNSoEAvfdhYRFSi5MUpFfCs+pNxJkZ7tFOAadMqSlPiLBcm9+A3yIn/04/oEtRNjXk/jlZQ2G21mt30sd89NARjJO+94ui6XbllW65E5/T+d8fDmRhz4c4OcNoKk70aXcdJqd1CJT7IlYQn++zQvB2el9cp9e7E22rvGtvum40iISEt/pVU84uHyQg6ZeCn+1czM+nLhPiWOAgveTz3EAgExlJW2de1ilDIuBCFXJy5+KvnCj3WNwRTAbIYgCwFnZMOUE5jYlhP6OZ2DQ3sWv7noG/Xaa/P+EomP9dwcamlAU32tRruyA8ZSp/ah9E8hFfmBqgKfSbRlWq3qGkq/F935DZfU/C+PjRcepS/eLZQ0mfD23B21VX2CjOhEkp5V+9I5L8XkXDjDg56JDRGvF/umpdqF/KpmtimqcgB0yDVY1Z3yT1U5IZ6Yf4WlqzhchdJXRWfo0bqK8Tk0b442/6AQgRP3n6DIrCjcpmr4jx+RJiejOr3xv3QtHGafDdkF2gO7DrYztdd6XpRjij0orBUiyYECgPsLHuwd+/2jXnxLVTavVTQ0Ddz0LLNjaYxqr8KK/B+0MeS/iuLXmACs8XCKwQmV8hPYQ0C/JXGiL4GOH0V70r6ghBxCDDMpJjPasCbiCBxSAMLkQ7ZmLbO0WVPPUTMAsZ8aKUy2MLe466zizXN640B4H6bkI7x/KC29sroEJWpUufD7f+t2tnVx/KLeAxAC6afuUCGi4ibFiwLHZjfimYKaYpuMcSZHQ64iONixyfUDIRNIOWQSiX1V5EBJgBDE0JVDnExmQYAIG5isYQEorC/7z6IJeZHc4NuiKDWuvd+jwwj8tN8PVAcACJKpQHpyUU20kvp4dYgBEeiQZpBfXWrnmOJZ0HgDHbvVxPW+SHTL/uoi/Tl9xdKe+z3Q/MoT8iYXggMPcFQIqVemQUTw9Tu0oNsSj3xueR9awJc2Le9aYbKTg54MAtcBnfKXN+3cYQkNVbqC5RfiAe2D3gfbT1OgApTUNz8n8v1dgAzTa+EB0i+w8nTsb3N/N4yAXMNSKdGeUl2rFRuQO7qlWorKc3pE+nBC2ex0sOMEUFYUXcyULaxsKM3ddakCAv9/jesnkrZl2gX7YDp8iqiFzh7l8jMAFHtFCMp3CSXlBAOpfQlGLY80qTSi1e8GrnV9Ow9t0DdS1wP7J6ES9DiNuEep5atHD+0MC6u0Dw0GtmsNnNRp77my/KI8ILhF4VBVBFQ5BiJDfdpAA3RNlANo2gK5ZCKPw4tXCYUknjYdwkqvUaIQCJcr2n+WkE2eCUCqjXTFr/KMvRByY6Fb1vAwcGoCWjP2g0Tliz3hcyh/72+umn05NO3cJSAfDEjfcYBUQZ/LCcy86G0EsHXP3NabgeHNXf/Z/VG7zWqQAPX0KaVrF9XC6I2FLkg1aNGWZfJGv4uohdIny7rWU7FWPaxy9GMk6x7xBh/Hk/5/rrGHCNGs+SegLglDs2+v4P56316k4kg//dmVl/vh3h0KC2P/1xiJZ10I91k7HqZLb2qdXToa4yOHB5Euu49UV/Qtv+cZU03fuT81sEv3YT819OdmwUZgkq0PT/Lrck+pbNDaUOakpgkGAbXy6SCvjM87jYN9Ia7druulWWO0pPi4T8daQ26ZQmZp57gdkpuh/He4kC5m1aWpT2s5STSbapChd9tFNjPhjCG1DZb7h0X8jg1VkxqutDDu17jOXrhElT1ejvYqYy9lIG1b1r7Jmjxo7LgGQuPMX6CIo/BLTsuNJyC5SfSCxEm56LvJgoNk2Vaeq6mLDOaKm2PNwPuYPnJw9xWBM0EetJBnsYMutPDMXhrB7x/8DKy5/hCsKICTfhlsboUHmbSfsikvGPYx7YOaOo8xFh4yAQ73T8hFX7kn2DBfRzGkJm8SQJRPj0Hjsf5mEFUt/OrieK/Jw10HKD6hDoEFmUDv5svC3nUY0FX0fTyi0jRAri+burrCQ0HXk/qj4Xs+WLiJgfQbprV2TwTIlzOdLb/jD8PeiElNzBxmOonRj76QKcbkOnPkCWCMFKapOJskxe0ASJVjYt142D0ndTa6F7oFu72ue5U+hyoToKeb7ONZilhF0+yL7akNfFKURUa/X+cEoewuSM7r8whWwZyG+M2cwGUTRMJiAC9wK7yE8MqdrEuQtILnCA0yRTPOh1Z9ifUY0asL1Tw9cbdGRW8svHH/oVHkWe3P+yepTNrn7xQlX/O1/p4FXRm0gSE3Fdnbli8VZdwVn2SET3WkPH9rJT2F7/hovfhx+lQzBQq9ZBpNLrWRdhXzCpiaC04o7q/eef+8dZwU62A1pd8uZ+0dI5T7lOVXoo03uF863YSOKX8ad2CiYUJ8gMvMA8FSWnsUbxiDaiXPj8kni5TLsvrkH/CKY/okG5SdRCF/XCcRcXP5dxdsS90tIWdd0xi/2wbalmn3CPQ2eVeDI3dP/jCRBkfiNtD70flhxVngZMZW+Rr1c0Cpo//fMCiV5ksDIBGin5AfOPmvSnPYWV5zcU3QEfl2F5fLXgDCbpaYjaOYjyRt9ZTqabSf1Ar4L9yQFBr4N9cNg4jKskvbha4BDmUL8J7xz/F/OXHFnEDV2YkMcpBfGM2XhKDevceOQcjrPkF5/e5LdwQeYhqgzzm6ur42OwHB2HBU7ulXn/LCk2eYCFa2D1f+2F3ExbwAvnDMhymdQdUniF/NzOfG/1L3yplFsqi270T/2gT1LdOQ/Fgp3FA81k/sMLbICcF/qCp/nS3x+iMZn9B+WLjUQvJy2PehXNcpXEvXWM/e1X4PWDUhwQi7+wjJwERX2W5SB1XjQa7n/osZqpOUG2ShssPNpS3mecaV+MTpRPVYTPFiDTIgkG7F/DSND0c2PDWsBLFus0h4jG70L94uDxVNiMJb/xq/7d2+7FMNPh9UJSK75XcKDhrRYJo3iU7mfl1U89vJB179ulGrdMpWd8v5DHWEDrn3NRBa0KWVYKpqeQ0195XXH1YlZjzK7615K/5ooOGUL42PlX2Hw1zxOJubsIl6LZt8V6wa6zJgywuVLsZb+kpjtHTHmVZyhY8IUnAn6Uj8erYXapvZ8rlhUpjvVuosn8gB9JM6vNct5dVn4e8Wbt6KnRcogHYUJT32QRSWsHCaZNEqwbX+dYaLx1FQZ5daYbXSWwiHhtAJRuYutbmWwWCRTVzyNNMh3sMAI1EcmMoOIX0DEhXYNe7daErR6vMyizQ+h5YKUYK4nUu270M0x/YKRHlxwK8WkEqWv8vfhZqzV0YGud1Hwd5JwWhIEZQ7cc1uPeYJJ5kAaIFqCffmwecS2gzVvOgDkNC5eENXSk7SzDpVgm8IcfZV47L17ni75tyJCS/kreODjap6VdpDNTyS1YJaaO2TMX/s4kWiEZ22Ma4CEv2eQq1uGazGamli+ZwExAtehw=="
|
||||
COM_ERMNVLDMR_STATIC_PUBLIC_URL="encrypted:BEZbNy1WbNKhgUWmKO648nvuVNyMNXrMnq64WkjsScTOcE/HFtMX3nQktbIvEYiTSNHvoceUeW10E9i4n+C5ZEHKIBi9VUk3nuqVIcwAr6xTfqKjM1cJlRNKFQzwhHbBTYcImLoxFvM56KU4L/9Ei/XuQ/QRz0KTabhvGMM="
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.STATIC_AWS_ACCESS_KEY_ID || secrets.COM_ERMNVLDMR_STATIC_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.STATIC_AWS_SECRET_ACCESS_KEY || secrets.COM_ERMNVLDMR_STATIC_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: ${{ secrets.STATIC_AWS_REGION || secrets.COM_ERMNVLDMR_STATIC_S3_REGION || 'ru-central1' }}
|
||||
AWS_DEFAULT_REGION: ${{ secrets.STATIC_AWS_REGION || secrets.COM_ERMNVLDMR_STATIC_S3_REGION || 'auto' }}
|
||||
S3_ENDPOINT: ${{ secrets.STATIC_S3_ENDPOINT_URL || secrets.COM_ERMNVLDMR_STATIC_S3_ENDPOINT_URL }}
|
||||
S3_BUCKET: ${{ secrets.STATIC_S3_BUCKET || secrets.COM_ERMNVLDMR_STATIC_S3_BUCKET }}
|
||||
IMAGE_NAME: ${{ needs.build-image.outputs.image_name }}
|
||||
|
||||
@@ -6,4 +6,4 @@ resources:
|
||||
- service.yaml
|
||||
images:
|
||||
- name: ghcr.io/deytenit/ermnvldmr.com/docs
|
||||
newTag: sha-fcf6895
|
||||
newTag: sha-9200529
|
||||
@@ -6,4 +6,4 @@ metadata:
|
||||
data:
|
||||
DATABASE_TYPE: "postgresql"
|
||||
CLIENT_IP_HEADER: "X-Forwarded-For"
|
||||
TRACKER_SCRIPT_NAME: "beacon.js"
|
||||
TRACKER_SCRIPT_NAME: "trace.js"
|
||||
@@ -18,7 +18,7 @@ spec:
|
||||
labels:
|
||||
app: umami
|
||||
annotations:
|
||||
config/tracker-script-name: "beacon.js"
|
||||
config/tracker-script-name: "trace.js"
|
||||
spec:
|
||||
containers:
|
||||
- name: umami
|
||||
|
||||
@@ -6,4 +6,4 @@ resources:
|
||||
- service.yaml
|
||||
images:
|
||||
- name: ghcr.io/deytenit/ermnvldmr.com/www
|
||||
newTag: sha-968f41d
|
||||
newTag: sha-9200529
|
||||
@@ -20,7 +20,7 @@ spec:
|
||||
- host: www.ermnvldmr.com
|
||||
http:
|
||||
paths:
|
||||
- path: /beacon
|
||||
- path: /trace
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
@@ -30,7 +30,7 @@ spec:
|
||||
- host: docs.ermnvldmr.com
|
||||
http:
|
||||
paths:
|
||||
- path: /beacon
|
||||
- path: /trace
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
|
||||
@@ -6,4 +6,4 @@ metadata:
|
||||
spec:
|
||||
stripPrefix:
|
||||
prefixes:
|
||||
- /beacon
|
||||
- /trace
|
||||
@@ -162,7 +162,7 @@ export function loadDevRootCaKey(workspaceRoot = findWorkspaceRoot()) {
|
||||
* Remote public URL for the Ermnvldmr development root CA certificate.
|
||||
*/
|
||||
export const REMOTE_ROOT_CA_URL =
|
||||
'https://static.ermnvldmr.com/ssl/certs/ErmnvldmrDevelopmentRootCA.pem';
|
||||
'https://static.ermnvldmr.com/dev/ssl/certs/ErmnvldmrDevelopmentRootCA.pem';
|
||||
|
||||
/**
|
||||
* Ensures valid development certificates exist for localhost.ermnvldmr.com.
|
||||
|
||||
@@ -376,4 +376,33 @@ describe('runS3Sync', () => {
|
||||
const content = fs.readFileSync(srcPath, 'utf-8');
|
||||
expect(content).toContain('https://cdn.example.com/www/img.webp');
|
||||
});
|
||||
|
||||
it('should fallback to COM_ERMNVLDMR_STATIC_PUBLIC_URL when publicUrl is not explicitly configured', async () => {
|
||||
process.env.COM_ERMNVLDMR_STATIC_PUBLIC_URL = 'https://static.ermnvldmr.com';
|
||||
|
||||
const staticDir = path.join(tempDir, 'static');
|
||||
fs.mkdirSync(staticDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(staticDir, 'asset.png'), 'png-content');
|
||||
|
||||
const srcDir = path.join(tempDir, 'src');
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
const srcPath = path.join(srcDir, 'index.ts');
|
||||
fs.writeFileSync(srcPath, 'const img = "/static/asset.png";');
|
||||
|
||||
const cmd: S3SyncCommandConfig = {
|
||||
name: 'sync',
|
||||
description: 'desc',
|
||||
bucket: 'com-ermnvldmr-static',
|
||||
targetPrefix: 'www',
|
||||
sourceDir: './static',
|
||||
type: 's3-sync',
|
||||
};
|
||||
|
||||
await runS3Sync(cmd, {});
|
||||
|
||||
const content = fs.readFileSync(srcPath, 'utf-8');
|
||||
expect(content).toContain('https://static.ermnvldmr.com/www/asset.png');
|
||||
|
||||
delete process.env.COM_ERMNVLDMR_STATIC_PUBLIC_URL;
|
||||
});
|
||||
});
|
||||
@@ -53,6 +53,7 @@ export async function runS3Sync(
|
||||
const bucket = cmd.bucket ?? process.env.S3_BUCKET;
|
||||
const region = cmd.region ?? process.env.AWS_REGION ?? process.env.S3_REGION;
|
||||
const endpoint = cmd.endpoint ?? process.env.S3_ENDPOINT_URL ?? process.env.S3_ENDPOINT;
|
||||
const publicUrl = cmd.publicUrl ?? process.env.COM_ERMNVLDMR_STATIC_PUBLIC_URL ?? process.env.S3_PUBLIC_URL;
|
||||
const accessKey = cmd.accessKeyId ?? process.env.AWS_ACCESS_KEY_ID ?? process.env.S3_ACCESS_KEY_ID;
|
||||
const secretKey = cmd.secretAccessKey ?? process.env.AWS_SECRET_ACCESS_KEY ?? process.env.S3_SECRET_ACCESS_KEY;
|
||||
|
||||
@@ -149,8 +150,8 @@ export async function runS3Sync(
|
||||
*/
|
||||
const getS3Url = (finalName: string): string => {
|
||||
const normalizedKey = normalizeKey(cmd.targetPrefix, finalName);
|
||||
if (cmd.publicUrl) {
|
||||
const cleanPublicUrl = cmd.publicUrl.replace(/\/+$/, '');
|
||||
if (publicUrl) {
|
||||
const cleanPublicUrl = publicUrl.replace(/\/+$/, '');
|
||||
return `${cleanPublicUrl}/${normalizedKey}`;
|
||||
}
|
||||
if (bucket.includes('.')) {
|
||||
|
||||
@@ -26,9 +26,6 @@ export function defineServiceConfig(config: RsbuildConfig = {}): RsbuildConfig {
|
||||
distPath: {
|
||||
font: 'static/font',
|
||||
},
|
||||
filename: {
|
||||
font: '[name][ext]',
|
||||
},
|
||||
},
|
||||
html: {
|
||||
template: './src/static/index.html',
|
||||
|
||||
@@ -214,7 +214,7 @@ export function loadDevRootCaKey(
|
||||
* Remote public URL for the Ermnvldmr development root CA certificate.
|
||||
*/
|
||||
export const REMOTE_ROOT_CA_URL =
|
||||
'https://static.ermnvldmr.com/ssl/certs/ErmnvldmrDevelopmentRootCA.pem';
|
||||
'https://static.ermnvldmr.com/dev/ssl/certs/ErmnvldmrDevelopmentRootCA.pem';
|
||||
|
||||
/**
|
||||
* Ensures that valid TLS development certificates exist for localhost.ermnvldmr.com.
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"build": "tsc",
|
||||
"lint": "eslint src --max-warnings 0",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -39,6 +40,8 @@
|
||||
"@rsbuild/core": "^1.7.3",
|
||||
"@rsbuild/plugin-react": "^1.1.1",
|
||||
"@rsbuild/plugin-type-check": "^1.1.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.1",
|
||||
"@types/node": "*",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
@@ -46,7 +49,8 @@
|
||||
"fast-glob": "^3.3.3",
|
||||
"jsdom": "^28.0.0",
|
||||
"rsbuild-plugin-dts": "^0.19.5",
|
||||
"typescript": "^5.8.2"
|
||||
"typescript": "^5.8.2",
|
||||
"vitest": "^4.1.11"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.1.0",
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { umamiAnalytics } from './umami';
|
||||
|
||||
describe('umamiAnalytics', () => {
|
||||
it('suppresses script injection in non-production environments when not forced', () => {
|
||||
const provider = umamiAnalytics({
|
||||
serverUrl: '/pulse',
|
||||
websiteId: 'test-website-id',
|
||||
});
|
||||
|
||||
expect(provider.getHtmlTags(false)).toEqual([]);
|
||||
});
|
||||
|
||||
it('injects script tag in non-production when force is true', () => {
|
||||
const provider = umamiAnalytics({
|
||||
serverUrl: '/pulse',
|
||||
websiteId: 'test-website-id',
|
||||
force: true,
|
||||
});
|
||||
|
||||
const tags = provider.getHtmlTags(false);
|
||||
expect(tags).toHaveLength(1);
|
||||
expect(tags[0]?.attrs?.src).toBe('/pulse/script.js');
|
||||
});
|
||||
|
||||
it('generates expected script attributes in production builds', () => {
|
||||
const provider = umamiAnalytics({
|
||||
serverUrl: '/trace/',
|
||||
scriptName: 'trace.js',
|
||||
hostUrl: '/trace',
|
||||
websiteId: '41c7699a-0675-4f49-bcd0-3df47a66b6b1',
|
||||
domains: 'ermnvldmr.com,www.ermnvldmr.com',
|
||||
doNotTrack: true,
|
||||
tag: 'landing',
|
||||
});
|
||||
|
||||
const tags = provider.getHtmlTags(true);
|
||||
expect(tags).toEqual([
|
||||
{
|
||||
tag: 'script',
|
||||
head: true,
|
||||
publicPath: false,
|
||||
attrs: {
|
||||
defer: true,
|
||||
src: '/trace/trace.js',
|
||||
'data-website-id': '41c7699a-0675-4f49-bcd0-3df47a66b6b1',
|
||||
'data-host-url': '/trace',
|
||||
'data-domains': 'ermnvldmr.com,www.ermnvldmr.com',
|
||||
'data-do-not-track': 'true',
|
||||
'data-tag': 'landing',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -5,14 +5,36 @@ import path from 'node:path';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { getPageOptions } from '../core/createPage.js';
|
||||
|
||||
import type { RsbuildPlugin } from '@rsbuild/core';
|
||||
|
||||
/**
|
||||
* Escapes HTML special characters for safe injection into markup.
|
||||
*
|
||||
* @param str - The raw string to escape.
|
||||
* @returns The escaped HTML string.
|
||||
* @example
|
||||
* ```ts
|
||||
* escapeHtml('<script>alert("xss")</script>');
|
||||
* ```
|
||||
*/
|
||||
function escapeHtml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
const SSR_RUNNER_SCRIPT = `
|
||||
import { createRsbuild } from '@rsbuild/core';
|
||||
|
||||
async function main() {
|
||||
const entries = JSON.parse(process.argv[1]);
|
||||
const ssrDistPath = process.argv[2];
|
||||
const locale = process.argv[3] || process.env.LOCALE || 'en';
|
||||
|
||||
const plugins = [];
|
||||
try {
|
||||
@@ -31,10 +53,35 @@ async function main() {
|
||||
// MDX plugins not installed or needed in this service
|
||||
}
|
||||
|
||||
if (locale !== 'en') {
|
||||
plugins.push({
|
||||
name: 'ermnvldmr:ssr-locale-mdx-replacement',
|
||||
setup(api) {
|
||||
api.modifyRspackConfig((config, { rspack }) => {
|
||||
config.plugins = [
|
||||
...(config.plugins ?? []),
|
||||
new rspack.NormalModuleReplacementPlugin(
|
||||
/\\.en\\.mdx$/,
|
||||
(resource) => {
|
||||
resource.request = resource.request.replace(/\\.en\\.mdx$/, '.' + locale + '.mdx');
|
||||
}
|
||||
),
|
||||
];
|
||||
return config;
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const ssrBuild = await createRsbuild({
|
||||
rsbuildConfig: {
|
||||
plugins,
|
||||
source: { entry: entries },
|
||||
source: {
|
||||
entry: entries,
|
||||
define: {
|
||||
'process.env.LOCALE': JSON.stringify(locale),
|
||||
},
|
||||
},
|
||||
output: {
|
||||
target: 'node',
|
||||
distPath: { root: ssrDistPath, js: '.' },
|
||||
@@ -119,11 +166,16 @@ export function ssgPlugin(): RsbuildPlugin {
|
||||
);
|
||||
const ssrDistPath = path.join(path.dirname(webDistPath), 'dist-ssr');
|
||||
|
||||
const locale = process.env.LOCALE ?? 'en';
|
||||
|
||||
console.log('[ssg-plugin] Building SSR bundles...');
|
||||
execFileSync(
|
||||
process.execPath,
|
||||
['--input-type=module', '-e', SSR_RUNNER_SCRIPT, JSON.stringify(entries), ssrDistPath],
|
||||
{ stdio: 'inherit' }
|
||||
['--input-type=module', '-e', SSR_RUNNER_SCRIPT, JSON.stringify(entries), ssrDistPath, locale],
|
||||
{
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, LOCALE: locale },
|
||||
}
|
||||
);
|
||||
console.log('[ssg-plugin] SSR bundles ready.');
|
||||
|
||||
@@ -175,8 +227,10 @@ export function ssgPlugin(): RsbuildPlugin {
|
||||
React.createElement(React.StrictMode, null, React.createElement(Component))
|
||||
);
|
||||
|
||||
const pageOptions = getPageOptions(Component);
|
||||
|
||||
const source = await fs.readFile(webHtmlPath, 'utf8');
|
||||
const injected = source.replace(
|
||||
let injected = source.replace(
|
||||
/<div id="root"[^>]*>\s*<\/div>/,
|
||||
`<div id="root">${renderedHtml}</div>`
|
||||
);
|
||||
@@ -187,9 +241,29 @@ export function ssgPlugin(): RsbuildPlugin {
|
||||
);
|
||||
}
|
||||
|
||||
if (pageOptions?.title) {
|
||||
injected = injected.replace(
|
||||
/<title>[^<]*<\/title>/i,
|
||||
`<title>${escapeHtml(pageOptions.title)}</title>`
|
||||
);
|
||||
}
|
||||
|
||||
if (pageOptions?.description) {
|
||||
if (/<meta\s+[^>]*name=["']description["'][^>]*>/i.test(injected)) {
|
||||
injected = injected.replace(
|
||||
/<meta\s+[^>]*name=["']description["'][^>]*>/i,
|
||||
`<meta name="description" content="${escapeHtml(pageOptions.description)}">`
|
||||
);
|
||||
} else {
|
||||
injected = injected.replace(
|
||||
/<\/head>/i,
|
||||
` <meta name="description" content="${escapeHtml(pageOptions.description)}">\n</head>`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const processed = await critters.process(injected);
|
||||
|
||||
const locale = process.env.LOCALE ?? 'en';
|
||||
const localized = processed.replace(
|
||||
/<html([^>]*)\slang="[^"]*"/,
|
||||
`<html$1 lang="${locale}"`
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { createPage, getPageOptions } from './createPage';
|
||||
|
||||
describe('createPage and getPageOptions', () => {
|
||||
beforeEach(() => {
|
||||
document.title = 'Default Title';
|
||||
const existingMeta = document.querySelector('meta[name="description"]');
|
||||
if (existingMeta) {
|
||||
existingMeta.remove();
|
||||
}
|
||||
});
|
||||
|
||||
it('attaches metadata options to component and retrieves them via getPageOptions', () => {
|
||||
function SamplePage(): React.JSX.Element {
|
||||
return <div>Sample</div>;
|
||||
}
|
||||
|
||||
createPage(SamplePage, {
|
||||
title: 'Sample Title',
|
||||
description: 'Sample Description',
|
||||
});
|
||||
|
||||
const options = getPageOptions(SamplePage);
|
||||
expect(options).toEqual({
|
||||
title: 'Sample Title',
|
||||
description: 'Sample Description',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns undefined for entities without metadata options', () => {
|
||||
function PlainComponent(): React.JSX.Element {
|
||||
return <div>Plain</div>;
|
||||
}
|
||||
|
||||
expect(getPageOptions(PlainComponent)).toBeUndefined();
|
||||
expect(getPageOptions(null)).toBeUndefined();
|
||||
expect(getPageOptions(undefined)).toBeUndefined();
|
||||
expect(getPageOptions('string')).toBeUndefined();
|
||||
expect(getPageOptions(123)).toBeUndefined();
|
||||
expect(getPageOptions({})).toBeUndefined();
|
||||
});
|
||||
|
||||
it('updates document.title and creates meta description in DOM', () => {
|
||||
function MetaPage(): React.JSX.Element {
|
||||
return <div>Meta</div>;
|
||||
}
|
||||
|
||||
createPage(MetaPage, {
|
||||
title: 'Custom Title',
|
||||
description: 'Custom Description',
|
||||
});
|
||||
|
||||
expect(document.title).toBe('Custom Title');
|
||||
const meta = document.querySelector('meta[name="description"]');
|
||||
expect(meta).not.toBeNull();
|
||||
expect(meta?.getAttribute('content')).toBe('Custom Description');
|
||||
});
|
||||
|
||||
it('updates existing meta description if already present in DOM', () => {
|
||||
const existingMeta = document.createElement('meta');
|
||||
existingMeta.setAttribute('name', 'description');
|
||||
existingMeta.setAttribute('content', 'Old Description');
|
||||
document.head.appendChild(existingMeta);
|
||||
|
||||
function AnotherMetaPage(): React.JSX.Element {
|
||||
return <div>Another Meta</div>;
|
||||
}
|
||||
|
||||
createPage(AnotherMetaPage, {
|
||||
description: 'Updated Description',
|
||||
});
|
||||
|
||||
const meta = document.querySelector('meta[name="description"]');
|
||||
expect(meta?.getAttribute('content')).toBe('Updated Description');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Options for page creation.
|
||||
*/
|
||||
export interface PageOptions {
|
||||
/** Document title to set on mount. */
|
||||
title?: string;
|
||||
/** Content of the meta description tag. */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal interface representing a component with attached page options.
|
||||
*/
|
||||
interface HasPageOptions {
|
||||
/** The attached page options. */
|
||||
__pageOptions?: PageOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type predicate checking if a value is an object or function with attached page options.
|
||||
*
|
||||
* @param value - The value to inspect.
|
||||
* @returns True if value has __pageOptions.
|
||||
* @example
|
||||
* ```ts
|
||||
* if (hasPageOptions(Component)) {
|
||||
* console.log(Component.__pageOptions);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
function hasPageOptions(value: unknown): value is HasPageOptions {
|
||||
return (
|
||||
(typeof value === 'function' || (typeof value === 'object' && value !== null)) &&
|
||||
'__pageOptions' in value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the page metadata options attached to a component via createPage.
|
||||
*
|
||||
* @param component - The component to inspect.
|
||||
* @returns The associated PageOptions, or undefined if none are set.
|
||||
* @example
|
||||
* ```ts
|
||||
* const options = getPageOptions(MyPageComponent);
|
||||
* ```
|
||||
*/
|
||||
export function getPageOptions(component: unknown): PageOptions | undefined {
|
||||
if (hasPageOptions(component)) {
|
||||
return component.__pageOptions;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mounts or hydrates a React component into the `#root` element.
|
||||
*
|
||||
* Attaches metadata options for SSG extraction. Skips DOM mounting during the SSG
|
||||
* server-side render pass (`typeof window === 'undefined'`). On the client, hydrates
|
||||
* asynchronously during idle time to unblock First Contentful Paint.
|
||||
*
|
||||
* @param Component - The React component to render.
|
||||
* @param options - Optional page metadata (title, description).
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* createPage(App, { title: 'My Page' });
|
||||
* ```
|
||||
*/
|
||||
export function createPage(Component: React.ComponentType, options: PageOptions = {}): void {
|
||||
Object.assign(Component, { __pageOptions: options });
|
||||
|
||||
if (typeof window === 'undefined') return; // SSG build context: skip DOM mounting
|
||||
const rootElement = document.getElementById('root');
|
||||
|
||||
if (options.title && document.title !== options.title) {
|
||||
document.title = options.title;
|
||||
}
|
||||
|
||||
if (options.description) {
|
||||
let metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (!metaDescription) {
|
||||
metaDescription = document.createElement('meta');
|
||||
metaDescription.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDescription);
|
||||
}
|
||||
if (metaDescription.getAttribute('content') !== options.description) {
|
||||
metaDescription.setAttribute('content', options.description);
|
||||
}
|
||||
}
|
||||
|
||||
if (!rootElement) return;
|
||||
|
||||
const mountOrHydrate = () => {
|
||||
void import('react-dom/client')
|
||||
.then(({ createRoot, hydrateRoot }) => {
|
||||
const node = React.createElement(
|
||||
React.StrictMode,
|
||||
null,
|
||||
React.createElement(Component)
|
||||
);
|
||||
|
||||
if (rootElement.hasChildNodes()) {
|
||||
hydrateRoot(rootElement, node);
|
||||
} else {
|
||||
createRoot(rootElement).render(node);
|
||||
}
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
console.error('[createPage] Client hydration failed:', err);
|
||||
});
|
||||
};
|
||||
|
||||
if ('requestIdleCallback' in window) {
|
||||
window.requestIdleCallback(mountOrHydrate);
|
||||
} else {
|
||||
setTimeout(mountOrHydrate, 1);
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Options for page creation.
|
||||
*/
|
||||
export interface PageOptions {
|
||||
/** Document title to set on mount. */
|
||||
title?: string;
|
||||
/** Content of the meta description tag. */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mounts or hydrates a React component into the `#root` element.
|
||||
*
|
||||
* Skips DOM mounting during the SSG server-side render pass
|
||||
* (`typeof window === 'undefined'`). On the client, hydrates asynchronously
|
||||
* during idle time to unblock First Contentful Paint.
|
||||
*
|
||||
* @param Component - The React component to render.
|
||||
* @param options - Optional page metadata (title, description).
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* createPage(App, { title: 'My Page' });
|
||||
* ```
|
||||
*/
|
||||
export function createPage(Component: React.ComponentType, options: PageOptions = {}): void {
|
||||
if (typeof window === 'undefined') return; // SSG build context: skip DOM mounting
|
||||
const rootElement = document.getElementById('root');
|
||||
|
||||
if (options.title) {
|
||||
document.title = options.title;
|
||||
}
|
||||
|
||||
if (options.description) {
|
||||
let metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (!metaDescription) {
|
||||
metaDescription = document.createElement('meta');
|
||||
metaDescription.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDescription);
|
||||
}
|
||||
metaDescription.setAttribute('content', options.description);
|
||||
}
|
||||
|
||||
if (!rootElement) return;
|
||||
|
||||
const mountOrHydrate = () => {
|
||||
void import('react-dom/client')
|
||||
.then(({ createRoot, hydrateRoot }) => {
|
||||
const node = (
|
||||
<React.StrictMode>
|
||||
<Component />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
if (rootElement.hasChildNodes()) {
|
||||
hydrateRoot(rootElement, node);
|
||||
} else {
|
||||
createRoot(rootElement).render(node);
|
||||
}
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
console.error('[createPage] Client hydration failed:', err);
|
||||
});
|
||||
};
|
||||
|
||||
if ('requestIdleCallback' in window) {
|
||||
window.requestIdleCallback(mountOrHydrate);
|
||||
} else {
|
||||
setTimeout(mountOrHydrate, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { baseVitestConfig } from '@ermnvldmr/rsbuild-config/dev';
|
||||
import { mergeConfig } from 'vitest/config';
|
||||
|
||||
export default mergeConfig(baseVitestConfig, {
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
exclude: ['dist/**', 'node_modules/**'],
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Switch } from './Switch';
|
||||
import { VStack } from '../VStack/VStack';
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
@@ -8,6 +9,16 @@ const meta: Meta<typeof Switch> = {
|
||||
title: 'Components/Switch',
|
||||
component: Switch,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['s', 'm', 'l'],
|
||||
},
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['solid', 'outline', 'ghost'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -19,6 +30,16 @@ export const Default: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const Sizes: Story = {
|
||||
render: () => (
|
||||
<VStack gap={4}>
|
||||
<Switch size="s">Small Switch</Switch>
|
||||
<Switch size="m">Medium Switch</Switch>
|
||||
<Switch size="l">Large Switch</Switch>
|
||||
</VStack>
|
||||
),
|
||||
};
|
||||
|
||||
export const Selected: Story = {
|
||||
args: {
|
||||
children: 'Selected State',
|
||||
|
||||
@@ -66,5 +66,18 @@ describe('components/Switch', () => {
|
||||
expect(track).toHaveClass('data-[selected=true]:bg-primary');
|
||||
expect(thumb).toHaveClass('data-[selected=true]:bg-primary-text');
|
||||
});
|
||||
|
||||
it('applies size variant classes to track and thumb', () => {
|
||||
const { container, rerender } = render(<Switch size="s">Small</Switch>);
|
||||
const track = container.querySelector('[aria-hidden="true"]');
|
||||
const thumb = track?.firstElementChild;
|
||||
|
||||
expect(track).toHaveClass('w-7', 'h-4');
|
||||
expect(thumb).toHaveClass('w-2.5', 'h-2.5');
|
||||
|
||||
rerender(<Switch size="l">Large</Switch>);
|
||||
expect(track).toHaveClass('w-14', 'h-8');
|
||||
expect(thumb).toHaveClass('w-6', 'h-6');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,11 @@ import type { AriaSwitchProps } from 'react-aria';
|
||||
*/
|
||||
export type SwitchVariant = 'solid' | 'outline' | 'ghost';
|
||||
|
||||
/**
|
||||
* Switch size variants.
|
||||
*/
|
||||
export type SwitchSize = 's' | 'm' | 'l';
|
||||
|
||||
/**
|
||||
* Switch color themes.
|
||||
*/
|
||||
@@ -39,12 +44,14 @@ export interface SwitchProps extends AriaSwitchProps, ClassNameProps, DataAttrib
|
||||
children?: React.ReactNode;
|
||||
/** Visual style variant */
|
||||
variant?: SwitchVariant;
|
||||
/** Switch size */
|
||||
size?: SwitchSize;
|
||||
/** Color theme */
|
||||
color?: SwitchColor;
|
||||
}
|
||||
|
||||
const trackVariants = cva(
|
||||
'relative w-11 h-6 transition-all duration-200 ease-in-out rounded-full outline-none flex items-center',
|
||||
'relative transition-all duration-200 ease-in-out rounded-full outline-none flex items-center',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -52,6 +59,11 @@ const trackVariants = cva(
|
||||
outline: 'border-2 data-[selected=false]:bg-transparent data-[selected=false]:border-muted',
|
||||
ghost: 'data-[selected=false]:bg-transparent data-[selected=false]:border-transparent',
|
||||
},
|
||||
size: {
|
||||
s: 'w-7 h-4',
|
||||
m: 'w-11 h-6',
|
||||
l: 'w-14 h-8',
|
||||
},
|
||||
color: {
|
||||
primary: '',
|
||||
'primary-negative': '',
|
||||
@@ -118,19 +130,25 @@ const trackVariants = cva(
|
||||
],
|
||||
defaultVariants: {
|
||||
variant: 'solid',
|
||||
size: 'm',
|
||||
color: 'primary',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const thumbVariants = cva(
|
||||
'w-4 h-4 transition-all duration-200 ease-in-out transform rounded-full shadow-sm data-[selected=false]:bg-[var(--rb-text)] data-[selected=false]:translate-x-0',
|
||||
'transition-all duration-200 ease-in-out transform rounded-full shadow-sm data-[selected=false]:bg-[var(--rb-text)] data-[selected=false]:translate-x-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
solid: 'ml-1 data-[selected=true]:translate-x-5',
|
||||
outline: 'ml-0.5 data-[selected=true]:translate-x-4.5',
|
||||
ghost: 'ml-1 data-[selected=true]:translate-x-5',
|
||||
solid: '',
|
||||
outline: '',
|
||||
ghost: '',
|
||||
},
|
||||
size: {
|
||||
s: 'w-2.5 h-2.5',
|
||||
m: 'w-4 h-4',
|
||||
l: 'w-6 h-6',
|
||||
},
|
||||
color: {
|
||||
primary: '',
|
||||
@@ -146,6 +164,17 @@ const thumbVariants = cva(
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
// size + variant offsets
|
||||
{ size: 's', variant: 'solid', className: 'ml-0.5 data-[selected=true]:translate-x-3.5' },
|
||||
{ size: 's', variant: 'outline', className: 'ml-0.5 data-[selected=true]:translate-x-2.5' },
|
||||
{ size: 's', variant: 'ghost', className: 'ml-0.5 data-[selected=true]:translate-x-3.5' },
|
||||
{ size: 'm', variant: 'solid', className: 'ml-1 data-[selected=true]:translate-x-5' },
|
||||
{ size: 'm', variant: 'outline', className: 'ml-0.5 data-[selected=true]:translate-x-4.5' },
|
||||
{ size: 'm', variant: 'ghost', className: 'ml-1 data-[selected=true]:translate-x-5' },
|
||||
{ size: 'l', variant: 'solid', className: 'ml-1 data-[selected=true]:translate-x-6' },
|
||||
{ size: 'l', variant: 'outline', className: 'ml-0.5 data-[selected=true]:translate-x-5.5' },
|
||||
{ size: 'l', variant: 'ghost', className: 'ml-1 data-[selected=true]:translate-x-6' },
|
||||
|
||||
// primary
|
||||
{ variant: 'solid', color: 'primary', className: 'data-[selected=true]:bg-primary-text' },
|
||||
{ variant: 'outline', color: 'primary', className: 'data-[selected=true]:bg-primary-text' },
|
||||
@@ -198,6 +227,7 @@ const thumbVariants = cva(
|
||||
],
|
||||
defaultVariants: {
|
||||
variant: 'solid',
|
||||
size: 'm',
|
||||
color: 'primary',
|
||||
},
|
||||
}
|
||||
@@ -216,7 +246,7 @@ const thumbVariants = cva(
|
||||
* ```
|
||||
*/
|
||||
export function Switch(props: SwitchProps): React.JSX.Element {
|
||||
const { children, className, variant = 'solid', color = 'primary' } = props;
|
||||
const { children, className, variant = 'solid', size = 'm', color = 'primary' } = props;
|
||||
const state = useToggleState(props);
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const { inputProps } = useSwitch(props, state, ref);
|
||||
@@ -238,14 +268,14 @@ export function Switch(props: SwitchProps): React.JSX.Element {
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
trackVariants({ variant, color }),
|
||||
trackVariants({ variant, size, color }),
|
||||
isFocusVisible && 'ring-2 ring-offset-2 ring-[var(--rb-ring)]',
|
||||
props.isDisabled && 'grayscale-[0.5]'
|
||||
)}
|
||||
data-selected={state.isSelected}
|
||||
>
|
||||
<div
|
||||
className={thumbVariants({ variant, color })}
|
||||
className={thumbVariants({ variant, size, color })}
|
||||
data-selected={state.isSelected}
|
||||
/>
|
||||
</div>
|
||||
@@ -253,7 +283,7 @@ export function Switch(props: SwitchProps): React.JSX.Element {
|
||||
<Text
|
||||
bold={state.isSelected}
|
||||
color={state.isSelected ? 'default' : 'muted'}
|
||||
size="s"
|
||||
size={size}
|
||||
type="label"
|
||||
>
|
||||
{children}
|
||||
|
||||
Generated
+104
-278
@@ -69,7 +69,7 @@ importers:
|
||||
version: 5.8.3
|
||||
vitest:
|
||||
specifier: ^4.1.11
|
||||
version: 4.1.11(@types/node@22.20.0)(jsdom@28.0.0)(vite@8.1.6(@types/node@22.20.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.51.2)(yaml@2.8.2))
|
||||
version: 4.1.11(@types/node@22.20.0)(@vitest/coverage-v8@4.0.18)(jsdom@28.0.0)(vite@8.1.6(@types/node@22.20.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.51.2)(yaml@2.8.2))
|
||||
|
||||
packages/eslint-config:
|
||||
dependencies:
|
||||
@@ -176,7 +176,7 @@ importers:
|
||||
version: 8.6.15(storybook@8.6.17(prettier@3.9.10))
|
||||
'@storybook/addon-coverage':
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0(vite@8.1.6(@types/node@22.20.0)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.51.2)(yaml@2.8.2))
|
||||
version: 3.0.0(vite@8.1.6(@types/node@22.20.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.51.2)(yaml@2.8.2))
|
||||
'@storybook/addon-essentials':
|
||||
specifier: ^8.6.15
|
||||
version: 8.6.15(@types/react@19.2.9)(storybook@8.6.17(prettier@3.9.10))
|
||||
@@ -236,13 +236,13 @@ importers:
|
||||
version: 8.6.17(prettier@3.9.10)
|
||||
storybook-react-rsbuild:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3(@rsbuild/core@1.7.3)(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.9.10)))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.9.10))(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
version: 1.0.3(@rsbuild/core@1.7.3)(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.9.10)))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.9.10))(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.8.3
|
||||
vitest:
|
||||
specifier: ^4.1.11
|
||||
version: 4.1.11(@types/node@22.20.0)(@vitest/coverage-v8@4.0.18)(jsdom@28.0.0)(vite@8.1.6(@types/node@22.20.0)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.51.2)(yaml@2.8.2))
|
||||
version: 4.1.11(@types/node@22.20.0)(@vitest/coverage-v8@4.0.18)(jsdom@28.0.0)(vite@8.1.6(@types/node@22.20.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.51.2)(yaml@2.8.2))
|
||||
wait-on:
|
||||
specifier: ^8.0.3
|
||||
version: 8.0.5
|
||||
@@ -300,7 +300,7 @@ importers:
|
||||
dependencies:
|
||||
'@rsbuild/plugin-mdx':
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1(@rsbuild/core@1.7.3)(webpack@5.105.1)
|
||||
version: 1.1.1(@rsbuild/core@1.7.3)(webpack@5.105.1(esbuild@0.27.7))
|
||||
react:
|
||||
specifier: ^19.1.0
|
||||
version: 19.2.3
|
||||
@@ -329,6 +329,12 @@ importers:
|
||||
'@rsbuild/plugin-type-check':
|
||||
specifier: ^1.1.1
|
||||
version: 1.3.3(@rsbuild/core@1.7.3)(@rspack/core@1.7.6(@swc/helpers@0.5.18))(tslib@2.8.1)(typescript@5.8.3)
|
||||
'@testing-library/jest-dom':
|
||||
specifier: ^6.9.1
|
||||
version: 6.9.1
|
||||
'@testing-library/react':
|
||||
specifier: ^16.3.1
|
||||
version: 16.3.2(@testing-library/dom@10.4.0)(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@types/node':
|
||||
specifier: '*'
|
||||
version: 22.20.0
|
||||
@@ -353,6 +359,9 @@ importers:
|
||||
typescript:
|
||||
specifier: ^5.8.2
|
||||
version: 5.8.3
|
||||
vitest:
|
||||
specifier: ^4.1.11
|
||||
version: 4.1.11(@types/node@22.20.0)(@vitest/coverage-v8@4.0.18)(jsdom@28.0.0)(vite@8.1.6(@types/node@22.20.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.51.2)(yaml@2.8.2))
|
||||
|
||||
packages/stl:
|
||||
dependencies:
|
||||
@@ -444,7 +453,7 @@ importers:
|
||||
version: 7.0.1
|
||||
storybook-react-rsbuild:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3(@rsbuild/core@1.7.3)(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.9.10)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.9.10))(typescript@5.8.3)(webpack@5.105.1(esbuild@0.27.7))
|
||||
version: 1.0.3(@rsbuild/core@1.7.3)(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.9.10)))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.9.10))(typescript@5.8.3)(webpack@5.105.1(esbuild@0.27.7))
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.8.3
|
||||
@@ -523,7 +532,7 @@ importers:
|
||||
version: 8.6.15(storybook@8.6.17(prettier@3.9.10))
|
||||
'@storybook/addon-coverage':
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0(vite@8.1.6(@types/node@25.6.2)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.51.2)(yaml@2.8.2))
|
||||
version: 3.0.0(vite@8.1.6(@types/node@22.20.0)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.51.2)(yaml@2.8.2))
|
||||
'@storybook/addon-essentials':
|
||||
specifier: ^8.6.15
|
||||
version: 8.6.15(@types/react@19.2.9)(storybook@8.6.17(prettier@3.9.10))
|
||||
@@ -541,7 +550,7 @@ importers:
|
||||
version: 8.6.15(storybook@8.6.17(prettier@3.9.10))
|
||||
'@storybook/test-runner':
|
||||
specifier: ^0.19.1
|
||||
version: 0.19.1(@swc/helpers@0.5.18)(@types/node@25.6.2)(storybook@8.6.17(prettier@3.9.10))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3))
|
||||
version: 0.19.1(@swc/helpers@0.5.18)(@types/node@22.20.0)(storybook@8.6.17(prettier@3.9.10))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@22.20.0)(typescript@5.8.3))
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4.1.18
|
||||
version: 4.1.18
|
||||
@@ -589,13 +598,13 @@ importers:
|
||||
version: 8.6.17(prettier@3.9.10)
|
||||
storybook-react-rsbuild:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3(@rsbuild/core@1.7.3)(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.9.10)))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.9.10))(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7)(postcss@8.5.31))
|
||||
version: 1.0.3(@rsbuild/core@1.7.3)(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.9.10)))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.9.10))(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)(postcss@8.5.31))
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.8.3
|
||||
vitest:
|
||||
specifier: ^4.1.11
|
||||
version: 4.1.11(@types/node@25.6.2)(@vitest/coverage-v8@4.0.18)(jsdom@28.0.0)(vite@8.1.6(@types/node@25.6.2)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.51.2)(yaml@2.8.2))
|
||||
version: 4.1.11(@types/node@22.20.0)(@vitest/coverage-v8@4.0.18)(jsdom@28.0.0)(vite@8.1.6(@types/node@22.20.0)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.51.2)(yaml@2.8.2))
|
||||
wait-on:
|
||||
specifier: ^8.0.3
|
||||
version: 8.0.5
|
||||
@@ -10259,41 +10268,6 @@ snapshots:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
'@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3))':
|
||||
dependencies:
|
||||
'@jest/console': 29.7.0
|
||||
'@jest/reporters': 29.7.0
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/transform': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 22.20.0
|
||||
ansi-escapes: 4.3.2
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.9.0
|
||||
exit: 0.1.2
|
||||
graceful-fs: 4.2.11
|
||||
jest-changed-files: 29.7.0
|
||||
jest-config: 29.7.0(@types/node@22.20.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3))
|
||||
jest-haste-map: 29.7.0
|
||||
jest-message-util: 29.7.0
|
||||
jest-regex-util: 29.6.3
|
||||
jest-resolve: 29.7.0
|
||||
jest-resolve-dependencies: 29.7.0
|
||||
jest-runner: 29.7.0
|
||||
jest-runtime: 29.7.0
|
||||
jest-snapshot: 29.7.0
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
jest-watcher: 29.7.0
|
||||
micromatch: 4.0.8
|
||||
pretty-format: 29.7.0
|
||||
slash: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
'@jest/create-cache-key-function@30.2.0':
|
||||
dependencies:
|
||||
'@jest/types': 30.2.0
|
||||
@@ -10628,12 +10602,12 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@mdx-js/loader@3.1.1(webpack@5.105.1)':
|
||||
'@mdx-js/loader@3.1.1(webpack@5.105.1(esbuild@0.27.7))':
|
||||
dependencies:
|
||||
'@mdx-js/mdx': 3.1.1
|
||||
source-map: 0.7.6
|
||||
optionalDependencies:
|
||||
webpack: 5.105.1
|
||||
webpack: 5.105.1(esbuild@0.27.7)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -12563,9 +12537,9 @@ snapshots:
|
||||
- supports-color
|
||||
- webpack
|
||||
|
||||
'@rsbuild/plugin-mdx@1.1.1(@rsbuild/core@1.7.3)(webpack@5.105.1)':
|
||||
'@rsbuild/plugin-mdx@1.1.1(@rsbuild/core@1.7.3)(webpack@5.105.1(esbuild@0.27.7))':
|
||||
dependencies:
|
||||
'@mdx-js/loader': 3.1.1(webpack@5.105.1)
|
||||
'@mdx-js/loader': 3.1.1(webpack@5.105.1(esbuild@0.27.7))
|
||||
optionalDependencies:
|
||||
'@rsbuild/core': 1.7.3
|
||||
transitivePeerDependencies:
|
||||
@@ -13083,6 +13057,20 @@ snapshots:
|
||||
- supports-color
|
||||
- vite
|
||||
|
||||
'@storybook/addon-coverage@3.0.0(vite@8.1.6(@types/node@22.20.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.51.2)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@istanbuljs/load-nyc-config': 1.1.0
|
||||
'@jsdevtools/coverage-istanbul-loader': 3.0.5
|
||||
'@types/istanbul-lib-coverage': 2.0.6
|
||||
convert-source-map: 2.0.0
|
||||
espree: 9.6.1
|
||||
istanbul-lib-instrument: 6.0.3
|
||||
test-exclude: 6.0.0
|
||||
vite-plugin-istanbul: 6.0.2(vite@8.1.6(@types/node@22.20.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.51.2)(yaml@2.8.2))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- vite
|
||||
|
||||
'@storybook/addon-coverage@3.0.0(vite@8.1.6(@types/node@25.0.10)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.51.2)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@istanbuljs/load-nyc-config': 1.1.0
|
||||
@@ -13408,7 +13396,7 @@ snapshots:
|
||||
dependencies:
|
||||
storybook: 8.6.17(prettier@3.9.10)
|
||||
|
||||
'@storybook/react-docgen-typescript-plugin@1.0.1(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))':
|
||||
'@storybook/react-docgen-typescript-plugin@1.0.1(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)(postcss@8.5.31))':
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
endent: 2.1.0
|
||||
@@ -13418,7 +13406,7 @@ snapshots:
|
||||
react-docgen-typescript: 2.4.0(typescript@5.8.3)
|
||||
tslib: 2.8.1
|
||||
typescript: 5.8.3
|
||||
webpack: 5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)
|
||||
webpack: 5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)(postcss@8.5.31)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -13436,6 +13424,20 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@storybook/react-docgen-typescript-plugin@1.0.1(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))':
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
endent: 2.1.0
|
||||
find-cache-dir: 3.3.2
|
||||
flat-cache: 3.2.0
|
||||
micromatch: 4.0.8
|
||||
react-docgen-typescript: 2.4.0(typescript@5.8.3)
|
||||
tslib: 2.8.1
|
||||
typescript: 5.8.3
|
||||
webpack: 5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@storybook/react-docgen-typescript-plugin@1.0.1(typescript@5.8.3)(webpack@5.105.1(esbuild@0.27.7))':
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
@@ -13604,40 +13606,6 @@ snapshots:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
'@storybook/test-runner@0.19.1(@swc/helpers@0.5.18)(@types/node@25.6.2)(storybook@8.6.17(prettier@3.9.10))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3))':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
'@babel/generator': 7.28.6
|
||||
'@babel/template': 7.28.6
|
||||
'@babel/types': 7.28.6
|
||||
'@jest/types': 29.6.3
|
||||
'@storybook/core-common': 8.6.14(storybook@8.6.17(prettier@3.9.10))
|
||||
'@storybook/csf': 0.1.13
|
||||
'@storybook/csf-tools': 8.6.14(storybook@8.6.17(prettier@3.9.10))
|
||||
'@storybook/preview-api': 8.6.15(storybook@8.6.17(prettier@3.9.10))
|
||||
'@swc/core': 1.15.10(@swc/helpers@0.5.18)
|
||||
'@swc/jest': 0.2.39(@swc/core@1.15.10(@swc/helpers@0.5.18))
|
||||
expect-playwright: 0.8.0
|
||||
jest: 29.7.0(@types/node@25.6.2)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3))
|
||||
jest-circus: 29.7.0
|
||||
jest-environment-node: 29.7.0
|
||||
jest-junit: 16.0.0
|
||||
jest-playwright-preset: 4.0.0(jest-circus@29.7.0)(jest-environment-node@29.7.0)(jest-runner@29.7.0)(jest@29.7.0(@types/node@25.6.2)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3)))
|
||||
jest-runner: 29.7.0
|
||||
jest-serializer-html: 7.1.0
|
||||
jest-watch-typeahead: 2.2.2(jest@29.7.0(@types/node@25.6.2)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3)))
|
||||
nyc: 15.1.0
|
||||
playwright: 1.58.0
|
||||
transitivePeerDependencies:
|
||||
- '@swc/helpers'
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
- debug
|
||||
- node-notifier
|
||||
- storybook
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
'@storybook/test@8.6.15(storybook@8.6.17(prettier@3.5.3))':
|
||||
dependencies:
|
||||
'@storybook/global': 5.0.0
|
||||
@@ -14972,21 +14940,6 @@ snapshots:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
create-jest@29.7.0(@types/node@25.6.2)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3)):
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
chalk: 4.1.2
|
||||
exit: 0.1.2
|
||||
graceful-fs: 4.2.11
|
||||
jest-config: 29.7.0(@types/node@25.6.2)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3))
|
||||
jest-util: 29.7.0
|
||||
prompts: 2.4.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
create-require@1.1.1: {}
|
||||
|
||||
critters@0.0.25:
|
||||
@@ -16629,25 +16582,6 @@ snapshots:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jest-cli@29.7.0(@types/node@25.6.2)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3)):
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3))
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
chalk: 4.1.2
|
||||
create-jest: 29.7.0(@types/node@25.6.2)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3))
|
||||
exit: 0.1.2
|
||||
import-local: 3.2.0
|
||||
jest-config: 29.7.0(@types/node@25.6.2)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3))
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
yargs: 17.7.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jest-config@29.7.0(@types/node@22.20.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@22.20.0)(typescript@5.8.3)):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
@@ -16710,37 +16644,6 @@ snapshots:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
||||
jest-config@29.7.0(@types/node@22.20.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3)):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
'@jest/test-sequencer': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
babel-jest: 29.7.0(@babel/core@7.28.6)
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.9.0
|
||||
deepmerge: 4.3.1
|
||||
glob: 7.2.3
|
||||
graceful-fs: 4.2.11
|
||||
jest-circus: 29.7.0
|
||||
jest-environment-node: 29.7.0
|
||||
jest-get-type: 29.6.3
|
||||
jest-regex-util: 29.6.3
|
||||
jest-resolve: 29.7.0
|
||||
jest-runner: 29.7.0
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
micromatch: 4.0.8
|
||||
parse-json: 5.2.0
|
||||
pretty-format: 29.7.0
|
||||
slash: 3.0.0
|
||||
strip-json-comments: 3.1.1
|
||||
optionalDependencies:
|
||||
'@types/node': 22.20.0
|
||||
ts-node: 10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3)
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
||||
jest-config@29.7.0(@types/node@25.0.10)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3)):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
@@ -16772,37 +16675,6 @@ snapshots:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
||||
jest-config@29.7.0(@types/node@25.6.2)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3)):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
'@jest/test-sequencer': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
babel-jest: 29.7.0(@babel/core@7.28.6)
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.9.0
|
||||
deepmerge: 4.3.1
|
||||
glob: 7.2.3
|
||||
graceful-fs: 4.2.11
|
||||
jest-circus: 29.7.0
|
||||
jest-environment-node: 29.7.0
|
||||
jest-get-type: 29.6.3
|
||||
jest-regex-util: 29.6.3
|
||||
jest-resolve: 29.7.0
|
||||
jest-runner: 29.7.0
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
micromatch: 4.0.8
|
||||
parse-json: 5.2.0
|
||||
pretty-format: 29.7.0
|
||||
slash: 3.0.0
|
||||
strip-json-comments: 3.1.1
|
||||
optionalDependencies:
|
||||
'@types/node': 25.6.2
|
||||
ts-node: 10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3)
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
||||
jest-diff@29.7.0:
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
@@ -16918,22 +16790,6 @@ snapshots:
|
||||
- debug
|
||||
- supports-color
|
||||
|
||||
jest-playwright-preset@4.0.0(jest-circus@29.7.0)(jest-environment-node@29.7.0)(jest-runner@29.7.0)(jest@29.7.0(@types/node@25.6.2)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3))):
|
||||
dependencies:
|
||||
expect-playwright: 0.8.0
|
||||
jest: 29.7.0(@types/node@25.6.2)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3))
|
||||
jest-circus: 29.7.0
|
||||
jest-environment-node: 29.7.0
|
||||
jest-process-manager: 0.4.0
|
||||
jest-runner: 29.7.0
|
||||
nyc: 15.1.0
|
||||
playwright-core: 1.58.0
|
||||
rimraf: 3.0.2
|
||||
uuid: 8.3.2
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
- supports-color
|
||||
|
||||
jest-pnp-resolver@1.2.3(jest-resolve@29.7.0):
|
||||
optionalDependencies:
|
||||
jest-resolve: 29.7.0
|
||||
@@ -17099,17 +16955,6 @@ snapshots:
|
||||
string-length: 5.0.1
|
||||
strip-ansi: 7.1.2
|
||||
|
||||
jest-watch-typeahead@2.2.2(jest@29.7.0(@types/node@25.6.2)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3))):
|
||||
dependencies:
|
||||
ansi-escapes: 6.2.1
|
||||
chalk: 5.6.2
|
||||
jest: 29.7.0(@types/node@25.6.2)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3))
|
||||
jest-regex-util: 29.6.3
|
||||
jest-watcher: 29.7.0
|
||||
slash: 5.1.0
|
||||
string-length: 5.0.1
|
||||
strip-ansi: 7.1.2
|
||||
|
||||
jest-watcher@29.7.0:
|
||||
dependencies:
|
||||
'@jest/test-result': 29.7.0
|
||||
@@ -17158,18 +17003,6 @@ snapshots:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jest@29.7.0(@types/node@25.6.2)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3)):
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3))
|
||||
'@jest/types': 29.6.3
|
||||
import-local: 3.2.0
|
||||
jest-cli: 29.7.0(@types/node@25.6.2)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3))
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jiti@2.6.1: {}
|
||||
|
||||
jju@1.4.0:
|
||||
@@ -18765,14 +18598,14 @@ snapshots:
|
||||
ajv: 6.12.6
|
||||
ajv-keywords: 3.5.2(ajv@6.12.6)
|
||||
|
||||
schema-utils@4.8.1(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)):
|
||||
schema-utils@4.8.1(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)(postcss@8.5.31)):
|
||||
dependencies:
|
||||
'@types/json-schema': 7.0.15
|
||||
ajv: 8.20.0
|
||||
ajv-formats: 3.0.1(ajv@8.20.0)
|
||||
ajv-keywords: 5.1.0(ajv@8.20.0)
|
||||
optionalDependencies:
|
||||
webpack: 5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)
|
||||
webpack: 5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)(postcss@8.5.31)
|
||||
|
||||
schema-utils@4.8.1(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7)(postcss@8.5.31)):
|
||||
dependencies:
|
||||
@@ -18783,6 +18616,15 @@ snapshots:
|
||||
optionalDependencies:
|
||||
webpack: 5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7)(postcss@8.5.31)
|
||||
|
||||
schema-utils@4.8.1(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7)):
|
||||
dependencies:
|
||||
'@types/json-schema': 7.0.15
|
||||
ajv: 8.20.0
|
||||
ajv-formats: 3.0.1(ajv@8.20.0)
|
||||
ajv-keywords: 5.1.0(ajv@8.20.0)
|
||||
optionalDependencies:
|
||||
webpack: 5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7)
|
||||
|
||||
schema-utils@4.8.1(webpack@5.105.1(esbuild@0.27.7)):
|
||||
dependencies:
|
||||
'@types/json-schema': 7.0.15
|
||||
@@ -18792,16 +18634,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
webpack: 5.105.1(esbuild@0.27.7)
|
||||
|
||||
schema-utils@4.8.1(webpack@5.105.1):
|
||||
dependencies:
|
||||
'@types/json-schema': 7.0.15
|
||||
ajv: 8.20.0
|
||||
ajv-formats: 3.0.1(ajv@8.20.0)
|
||||
ajv-keywords: 5.1.0(ajv@8.20.0)
|
||||
optionalDependencies:
|
||||
webpack: 5.105.1
|
||||
optional: true
|
||||
|
||||
secure-compare@3.0.1: {}
|
||||
|
||||
semver@6.3.1: {}
|
||||
@@ -19120,12 +18952,12 @@ snapshots:
|
||||
- tslib
|
||||
- webpack
|
||||
|
||||
storybook-react-rsbuild@1.0.3(@rsbuild/core@1.7.3)(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.9.10)))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.9.10))(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)):
|
||||
storybook-react-rsbuild@1.0.3(@rsbuild/core@1.7.3)(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.9.10)))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.9.10))(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)(postcss@8.5.31)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
'@rsbuild/core': 1.7.3
|
||||
'@storybook/react': 8.6.15(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.9.10)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@8.6.17(prettier@3.9.10))(typescript@5.8.3)
|
||||
'@storybook/react-docgen-typescript-plugin': 1.0.1(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
'@storybook/react-docgen-typescript-plugin': 1.0.1(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)(postcss@8.5.31))
|
||||
'@types/node': 18.19.130
|
||||
find-up: 5.0.0
|
||||
magic-string: 0.30.21
|
||||
@@ -19148,12 +18980,12 @@ snapshots:
|
||||
- tslib
|
||||
- webpack
|
||||
|
||||
storybook-react-rsbuild@1.0.3(@rsbuild/core@1.7.3)(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.9.10)))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.9.10))(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7)(postcss@8.5.31)):
|
||||
storybook-react-rsbuild@1.0.3(@rsbuild/core@1.7.3)(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.9.10)))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.9.10))(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
'@rsbuild/core': 1.7.3
|
||||
'@storybook/react': 8.6.15(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.9.10)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@8.6.17(prettier@3.9.10))(typescript@5.8.3)
|
||||
'@storybook/react-docgen-typescript-plugin': 1.0.1(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7)(postcss@8.5.31))
|
||||
'@storybook/react-docgen-typescript-plugin': 1.0.1(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
'@types/node': 18.19.130
|
||||
find-up: 5.0.0
|
||||
magic-string: 0.30.21
|
||||
@@ -19176,7 +19008,7 @@ snapshots:
|
||||
- tslib
|
||||
- webpack
|
||||
|
||||
storybook-react-rsbuild@1.0.3(@rsbuild/core@1.7.3)(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.9.10)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.9.10))(typescript@5.8.3)(webpack@5.105.1(esbuild@0.27.7)):
|
||||
storybook-react-rsbuild@1.0.3(@rsbuild/core@1.7.3)(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.9.10)))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.9.10))(typescript@5.8.3)(webpack@5.105.1(esbuild@0.27.7)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
'@rsbuild/core': 1.7.3
|
||||
@@ -19373,16 +19205,17 @@ snapshots:
|
||||
|
||||
tapable@2.3.3: {}
|
||||
|
||||
terser-webpack-plugin@5.6.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)):
|
||||
terser-webpack-plugin@5.6.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)(postcss@8.5.31)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)(postcss@8.5.31)):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
jest-worker: 27.5.1
|
||||
schema-utils: 4.8.1(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
schema-utils: 4.8.1(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)(postcss@8.5.31))
|
||||
terser: 5.51.2
|
||||
webpack: 5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)
|
||||
webpack: 5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)(postcss@8.5.31)
|
||||
optionalDependencies:
|
||||
'@swc/core': 1.15.10(@swc/helpers@0.5.18)
|
||||
esbuild: 0.25.12
|
||||
postcss: 8.5.31
|
||||
|
||||
terser-webpack-plugin@5.6.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7)(postcss@8.5.31)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7)(postcss@8.5.31)):
|
||||
dependencies:
|
||||
@@ -19396,6 +19229,17 @@ snapshots:
|
||||
esbuild: 0.27.7
|
||||
postcss: 8.5.31
|
||||
|
||||
terser-webpack-plugin@5.6.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7)):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
jest-worker: 27.5.1
|
||||
schema-utils: 4.8.1(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
terser: 5.51.2
|
||||
webpack: 5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7)
|
||||
optionalDependencies:
|
||||
'@swc/core': 1.15.10(@swc/helpers@0.5.18)
|
||||
esbuild: 0.27.7
|
||||
|
||||
terser-webpack-plugin@5.6.1(esbuild@0.27.7)(webpack@5.105.1(esbuild@0.27.7)):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
@@ -19406,15 +19250,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
esbuild: 0.27.7
|
||||
|
||||
terser-webpack-plugin@5.6.1(webpack@5.105.1):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
jest-worker: 27.5.1
|
||||
schema-utils: 4.8.1(webpack@5.105.1)
|
||||
terser: 5.51.2
|
||||
webpack: 5.105.1
|
||||
optional: true
|
||||
|
||||
terser@5.46.0:
|
||||
dependencies:
|
||||
'@jridgewell/source-map': 0.3.11
|
||||
@@ -19574,27 +19409,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@swc/core': 1.15.10(@swc/helpers@0.5.18)
|
||||
|
||||
ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.6.2)(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@cspotcode/source-map-support': 0.8.1
|
||||
'@tsconfig/node10': 1.0.12
|
||||
'@tsconfig/node12': 1.0.11
|
||||
'@tsconfig/node14': 1.0.3
|
||||
'@tsconfig/node16': 1.0.4
|
||||
'@types/node': 25.6.2
|
||||
acorn: 8.15.0
|
||||
acorn-walk: 8.3.4
|
||||
arg: 4.1.3
|
||||
create-require: 1.1.1
|
||||
diff: 4.0.4
|
||||
make-error: 1.3.6
|
||||
typescript: 5.8.3
|
||||
v8-compile-cache-lib: 3.0.1
|
||||
yn: 3.1.1
|
||||
optionalDependencies:
|
||||
'@swc/core': 1.15.10(@swc/helpers@0.5.18)
|
||||
optional: true
|
||||
|
||||
tsconfck@3.1.6(typescript@5.8.3):
|
||||
optionalDependencies:
|
||||
typescript: 5.8.3
|
||||
@@ -19849,6 +19663,18 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-istanbul@6.0.2(vite@8.1.6(@types/node@22.20.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.51.2)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
'@istanbuljs/load-nyc-config': 1.1.0
|
||||
espree: 10.4.0
|
||||
istanbul-lib-instrument: 6.0.3
|
||||
picocolors: 1.1.1
|
||||
source-map: 0.7.6
|
||||
test-exclude: 6.0.0
|
||||
vite: 8.1.6(@types/node@22.20.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.51.2)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-istanbul@6.0.2(vite@8.1.6(@types/node@25.0.10)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.51.2)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
'@istanbuljs/load-nyc-config': 1.1.0
|
||||
@@ -19972,7 +19798,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- msw
|
||||
|
||||
vitest@4.1.11(@types/node@22.20.0)(jsdom@28.0.0)(vite@8.1.6(@types/node@22.20.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.51.2)(yaml@2.8.2)):
|
||||
vitest@4.1.11(@types/node@22.20.0)(@vitest/coverage-v8@4.0.18)(jsdom@28.0.0)(vite@8.1.6(@types/node@22.20.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.51.2)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.1.11
|
||||
'@vitest/mocker': 4.1.11(vite@8.1.6(@types/node@22.20.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.51.2)(yaml@2.8.2))
|
||||
@@ -19996,6 +19822,7 @@ snapshots:
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 22.20.0
|
||||
'@vitest/coverage-v8': 4.0.18(vitest@4.1.11)
|
||||
jsdom: 28.0.0
|
||||
transitivePeerDependencies:
|
||||
- msw
|
||||
@@ -20104,7 +19931,7 @@ snapshots:
|
||||
|
||||
webpack-virtual-modules@0.6.2: {}
|
||||
|
||||
webpack@5.105.1:
|
||||
webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)(postcss@8.5.31):
|
||||
dependencies:
|
||||
'@types/eslint-scope': 3.7.7
|
||||
'@types/estree': 1.0.9
|
||||
@@ -20126,9 +19953,9 @@ snapshots:
|
||||
loader-runner: 4.3.2
|
||||
mime-types: 2.1.35
|
||||
neo-async: 2.6.2
|
||||
schema-utils: 4.8.1(webpack@5.105.1)
|
||||
schema-utils: 4.8.1(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)(postcss@8.5.31))
|
||||
tapable: 2.3.3
|
||||
terser-webpack-plugin: 5.6.1(webpack@5.105.1)
|
||||
terser-webpack-plugin: 5.6.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)(postcss@8.5.31)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)(postcss@8.5.31))
|
||||
watchpack: 2.6.3
|
||||
webpack-sources: 3.6.0
|
||||
transitivePeerDependencies:
|
||||
@@ -20144,9 +19971,8 @@ snapshots:
|
||||
- lightningcss
|
||||
- postcss
|
||||
- uglify-js
|
||||
optional: true
|
||||
|
||||
webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12):
|
||||
webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7):
|
||||
dependencies:
|
||||
'@types/eslint-scope': 3.7.7
|
||||
'@types/estree': 1.0.9
|
||||
@@ -20168,9 +19994,9 @@ snapshots:
|
||||
loader-runner: 4.3.2
|
||||
mime-types: 2.1.35
|
||||
neo-async: 2.6.2
|
||||
schema-utils: 4.8.1(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
schema-utils: 4.8.1(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
tapable: 2.3.3
|
||||
terser-webpack-plugin: 5.6.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
terser-webpack-plugin: 5.6.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
watchpack: 2.6.3
|
||||
webpack-sources: 3.6.0
|
||||
transitivePeerDependencies:
|
||||
|
||||
@@ -6,16 +6,17 @@ export default defineCli({
|
||||
name: 'sync:static',
|
||||
description: 'Sync static assets to S3',
|
||||
targetPrefix: 'docs',
|
||||
sourceDir: './static',
|
||||
sourceDir: './public/static',
|
||||
sourceFilesPattern: 'content/**/*.{md,html}',
|
||||
urlPrefixes: ['/static/', '/images/'],
|
||||
bucket: env('COM_ERMNVLDMR_STATIC_S3_BUCKET'),
|
||||
publicUrl: env('COM_ERMNVLDMR_STATIC_PUBLIC_URL', 'https://static.ermnvldmr.com'),
|
||||
region: env('COM_ERMNVLDMR_STATIC_S3_REGION'),
|
||||
endpoint: env('COM_ERMNVLDMR_STATIC_S3_ENDPOINT_URL'),
|
||||
accessKeyId: env('COM_ERMNVLDMR_STATIC_AWS_ACCESS_KEY_ID'),
|
||||
secretAccessKey: env('COM_ERMNVLDMR_STATIC_AWS_SECRET_ACCESS_KEY'),
|
||||
hooks: {
|
||||
beforeSync: 'bash ../../ci/scripts/optimize-images.sh ./static',
|
||||
beforeSync: 'bash ../../ci/scripts/optimize-images.sh ./public/static',
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
(function () {
|
||||
var KEY = "umami.disabled";
|
||||
var toggles = document.querySelectorAll("[data-umami-toggle]");
|
||||
if (!toggles.length) return;
|
||||
|
||||
function sync() {
|
||||
var enabled = true;
|
||||
try {
|
||||
enabled = window.localStorage.getItem(KEY) !== "1";
|
||||
} catch (e) {}
|
||||
toggles.forEach(function (el) {
|
||||
el.checked = enabled;
|
||||
});
|
||||
}
|
||||
|
||||
sync();
|
||||
|
||||
toggles.forEach(function (el) {
|
||||
el.addEventListener("change", function () {
|
||||
try {
|
||||
if (el.checked) {
|
||||
window.localStorage.removeItem(KEY);
|
||||
} else {
|
||||
window.localStorage.setItem(KEY, "1");
|
||||
}
|
||||
} catch (e) {}
|
||||
sync();
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener("storage", function (e) {
|
||||
if (e.key === KEY || e.key === null) {
|
||||
sync();
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -53,15 +53,13 @@ Listing (`apex --help`) never imports action modules; summaries and disabled ma
|
||||
|
||||
## The capture-up model {#capture-up}
|
||||
|
||||
Node repositories are ledgers, not just sources: the host itself records what it runs.
|
||||
`apex sync/repository` records the working tree and commons pin in a
|
||||
`sync/<node>` branch, rebases onto `origin/main`, and pushes with
|
||||
`--force-with-lease`. It refuses to run when commons has uncommitted changes
|
||||
or the `compositions/` directory is missing. A rebase conflict aborts the sync.
|
||||
|
||||
1. A scheduled `sync/repository` run on the host commits the working tree to a `sync/<node>` branch (including the commons submodule pointer), rebases it onto `origin/main`, and pushes with `--force-with-lease`.
|
||||
2. A scheduled CI job in the node repository merges `sync/<node>` back into `main` under a bot identity.
|
||||
|
||||
This [capture-up](/apex/glossary#capture-up) loop means drift on a host becomes a commit — visible, diffable, and revertable — instead of silent divergence. The sync aborts rather than committing when the commons submodule is dirty or the `compositions/` tree is missing, so a broken checkout never becomes the recorded state.
|
||||
|
||||
> [!NOTE]
|
||||
> The CI merge job checks out the repository *without* submodules: the commons submodule uses an SSH URL that the default CI token cannot fetch, and the merge only moves the gitlink pointer anyway.
|
||||
Review and integration of that branch are repository policies. APEX does not
|
||||
supply a CI merge workflow or require a particular repository hosting setup.
|
||||
|
||||
## Design constraints {#constraints}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ Opt-in services, gated by compose profiles a node enables in `COMPOSE_PROFILE
|
||||
|---|---|---|
|
||||
| `xray` | xray | Host-network proxy edge for the enclave. |
|
||||
| `adguard` | adguard | DNS filtering, sharing traefik's network namespace; routed at `/adguard` on the node FQDN. |
|
||||
| `alloy` | alloy | Per-node observability agent (Grafana Alloy). Collects host + container metrics and journal + container logs and **pushes** them outbound to a central Prometheus and Loki (see [Concept: Security model](/apex/concepts/security-model#observability)). Its container `hostname` is set to `APEX_NODE_HOST`, and that value stamps the `node` and `instance` labels on every metric and log stream. |
|
||||
| `alloy` | alloy | Optional Grafana Alloy agent. Sends host/container metrics and journal/container logs to configured compatible endpoints. See [observability configuration](/apex/concepts/security-model#observability). |
|
||||
| `manual` | resticontainer | One-shot label-driven backup runner (`ghcr.io/deytenit/resticontainer`) — never starts with `up`; invoked by the `backup/run` action, which discovers per-service backup intent from `restic.*` compose labels. |
|
||||
|
||||
## Networks {#networks}
|
||||
|
||||
@@ -21,7 +21,7 @@ It declares identity explicitly through these keys:
|
||||
```ini {filename="node.env"}
|
||||
APEX_NODE_FQDN=node1.example.com
|
||||
APEX_NODE_HOST=node1.example.com
|
||||
APEX_SUBNET=198.18.16.0/24
|
||||
APEX_SUBNET=198.18.0.0/24
|
||||
```
|
||||
|
||||
### The OS hostname
|
||||
|
||||
@@ -34,7 +34,11 @@ The [core composition](/apex/glossary#core-composition) encodes container-level
|
||||
|
||||
## Observability {#observability}
|
||||
|
||||
Observability is an **outbound push**, not an exposed route. The optional `alloy` core service (Grafana Alloy, one agent per node) collects host metrics (a `node_exporter`-equivalent), container metrics (cadvisor), the systemd journal, and docker container logs, and pushes them to a **central** Prometheus and Loki that live on a designated node — the metrics via `prometheus.remote_write` to `APEX_PROM_PUSH_URL`, the logs via `loki.write` to `APEX_LOKI_PUSH_URL`. Both targets are authenticated with HTTP basic auth using `APEX_OBS_BASIC_AUTH_USER` / `APEX_OBS_BASIC_AUTH_PASS`. Because the push is outbound, Alloy attaches to the `direct` network (which has egress) rather than the `internal` `enclave`; there is no node-local log store and no `/loki` route to protect.
|
||||
The optional `alloy` service sends host and container metrics to
|
||||
`APEX_PROM_PUSH_URL` and logs to `APEX_LOKI_PUSH_URL`. Configure compatible
|
||||
endpoints and HTTP basic-auth credentials through `APEX_OBS_BASIC_AUTH_USER`
|
||||
and `APEX_OBS_BASIC_AUTH_PASS`. The service requires outbound connectivity to
|
||||
those endpoints; their hosting and access policy are deployment choices.
|
||||
|
||||
## Secrets boundary {#secrets}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Terminology used within the apex fleet operations framework.
|
||||
|
||||
## Capture-up {#capture-up}
|
||||
|
||||
**Capture-up** - The flow that records a host's live state back into its repository: a scheduled commit to the `sync/<node>` branch, rebased onto `main` and folded back by CI. Drift becomes a diff instead of divergence.
|
||||
**Capture-up** — Recording a host's working state in a `sync/<node>` branch through `apex sync/repository`. The repository's own policy determines review and integration.
|
||||
|
||||
## Commons {#commons}
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ weight: 24
|
||||
|
||||
| Step | Effect |
|
||||
|---|---|
|
||||
| `configure/base` | Ensures a Debian base: package index update, essential packages (`curl`, `wget`, `git`, `rsyslog`), rsyslog running, `ufw` installed, and the `ufw-docker` helper at `~/.local/bin/ufw-docker`. |
|
||||
| `configure/base` | Installs missing Debian packages after checking that APT's simulation contains no upgrades or removals, ensures rsyslog is running, and verifies the pinned `/usr/local/bin/ufw-docker` before privileged execution. Avoid concurrent package operations. |
|
||||
| `configure/timezone` | Applies `configs/timezone` when present and refreshes active cron after a timezone change. Supports `--dry-run`. |
|
||||
| `configure/ufw` | Applies host rules, injects the managed kernel-rule blocks, applies container rules via `ufw-docker`. Supports `--dry-run`. |
|
||||
| `configure/crowdsec` | Installs CrowdSec if absent, deploys `configs/crowdsec/` to `/etc/crowdsec/`, installs collections, and the firewall bouncer when configured. Supports `--dry-run`. |
|
||||
| `configure/cron` | Renders `configs/cron/crontab` through the template layer and installs it — with preview, empty-render refusal (exit `65`), and rollback on a failed install. Supports `--dry-run`. |
|
||||
@@ -27,10 +28,39 @@ After `configure/ufw`, restart `ufw` deliberately (`sudo systemctl restart ufw`)
|
||||
apex compose up # core first, then services alphabetically
|
||||
apex compose down # reverse order
|
||||
apex compose restart
|
||||
apex compose up --dry-run # print the commands only
|
||||
apex compose --dry-run up # print the commands only
|
||||
apex compose -p apex restart traefik
|
||||
apex compose -p app up -d
|
||||
```
|
||||
|
||||
Extra arguments pass through to `docker compose` per project. Each project's `.env` and `apex.env` are supplied automatically when present.
|
||||
Put wrapper options (`-p` and `--dry-run`) before the action. Extra arguments pass
|
||||
through to Docker Compose for the selected project. Each project's `.env` and
|
||||
`apex.env` are supplied automatically when present. Positional targeting remains
|
||||
available as `apex compose restart apex traefik` or `apex compose up -d apex`;
|
||||
prefer `-p` when arguments could also name a composition.
|
||||
|
||||
Replace `app` with a composition defined in your repository. When a mount or
|
||||
other container definition changes, use targeted `up -d` to recreate it:
|
||||
`restart` retains the existing definition.
|
||||
|
||||
### Direct core Compose commands
|
||||
|
||||
```bash
|
||||
apex env --sync
|
||||
cd compositions/apex
|
||||
docker compose config --quiet
|
||||
docker compose restart traefik
|
||||
```
|
||||
|
||||
Sync preserves existing secret and UID/GID text in `.env` and its tier-storage
|
||||
symlink. A managed block loads committed `apex.env` settings, including profiles,
|
||||
then synthesized node/path values. Writes use mode `0600`. Resync after changing
|
||||
those settings or paths; do not hand-edit the managed block. Docker's shell
|
||||
environment still overrides file values.
|
||||
|
||||
`eval "$(apex env)"`, `apex env --dotenv`, and `apex env --write generated.env`
|
||||
export or save generated APEX values only. They do not load composition secrets
|
||||
or replace the full `--sync` bridge. Output modes are mutually exclusive.
|
||||
|
||||
## Backups {#backups}
|
||||
|
||||
@@ -46,7 +76,7 @@ Runs [resticontainer](https://github.com/deytenit/resticontainer) one-shot under
|
||||
apex sync/repository <telegram-bot-url>
|
||||
```
|
||||
|
||||
Commits the live checkout to `sync/<node>`, rebases onto `origin/main`, and force-pushes with lease; CI folds the branch back into `main`. The action refuses to run — with a paged error — when the commons submodule is dirty or the `compositions/` tree is missing, and aborts cleanly on a rebase conflict (manual intervention required). See [Concept: Framework architecture](/apex/concepts/architecture#capture-up) for the model.
|
||||
Commits the live checkout to `sync/<node>`, rebases onto `origin/main`, and force-pushes with lease. The action refuses to run — with a paged error — when the commons submodule is dirty or the `compositions/` tree is missing, and aborts cleanly on a rebase conflict (manual intervention required). See [Concept: Framework architecture](/apex/concepts/architecture#capture-up) for the model.
|
||||
|
||||
## Update awareness {#updates}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ The gitlink pins the exact engine version the node runs. Upgrading later mean
|
||||
```ini {filename="node.env"}
|
||||
APEX_NODE_FQDN=node1.example.com
|
||||
APEX_NODE_HOST=node1.example.com
|
||||
APEX_SUBNET=198.18.16.0/24
|
||||
APEX_SUBNET=198.18.0.0/24
|
||||
APEX_GIT_AUTHOR_NAME=apex [bot]
|
||||
APEX_GIT_AUTHOR_EMAIL=apex@localhost
|
||||
```
|
||||
@@ -41,7 +41,7 @@ include:
|
||||
```
|
||||
|
||||
```ini {filename="compositions/apex/apex.env"}
|
||||
APEX_TRAEFIK_DIRECT_IP=198.18.16.10
|
||||
APEX_TRAEFIK_DIRECT_IP=198.18.0.10
|
||||
APEX_RESTIC_REPOSITORY=s3:<endpoint>/<bucket>/<node>
|
||||
COMPOSE_PROFILES=adguard,xray,alloy
|
||||
# observability push targets (only when the alloy profile is enabled)
|
||||
@@ -77,9 +77,12 @@ __pycache__/
|
||||
|
||||
Create `compositions/ADDRESSING.md` with the node's subnet (`APEX_SUBNET`) and the reserved ranges (`.10`–`.19` core, `.20`+ services). Every [anchor IP](/apex/glossary#anchor-ip) allocated later is recorded here.
|
||||
|
||||
### Wire capture-up CI
|
||||
### Choose a sync review policy
|
||||
|
||||
Add a scheduled workflow that merges `sync/<node>` into `main` when the branch exists, committing under a bot identity. Check out **without** submodules — the merge only moves the commons pointer, and an SSH submodule URL is not fetchable with the default CI token. See [Concept: Framework architecture](/apex/concepts/architecture) for the full [capture-up](/apex/glossary#capture-up) loop.
|
||||
Decide how to review and integrate the `sync/<node>` branch produced by
|
||||
`apex sync/repository`. If you automate integration, configure repository
|
||||
permissions and submodule access for that workflow. See the
|
||||
[capture-up model](/apex/concepts/architecture#capture-up).
|
||||
|
||||
### Validate
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ services:
|
||||
- ./@tier2/<anchor>/data:/data
|
||||
networks:
|
||||
direct:
|
||||
ipv4_address: 198.18.16.20
|
||||
ipv4_address: 198.18.0.20
|
||||
enclave: {}
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
|
||||
@@ -4,7 +4,7 @@ description: "Every commons action: arguments and behavior."
|
||||
weight: 32
|
||||
---
|
||||
|
||||
The sixteen [commons](/apex/glossary#commons) actions, by group. Arguments are shown as the parser accepts them; optional arguments are bracketed. Exit codes follow the [engine convention](/apex/reference/cli#exit-codes).
|
||||
The [commons](/apex/glossary#commons) actions, by group. Arguments are shown as the parser accepts them; optional arguments are bracketed. Exit codes follow the [engine convention](/apex/reference/cli#exit-codes).
|
||||
|
||||
## configure {#configure}
|
||||
|
||||
@@ -12,13 +12,19 @@ The sixteen [commons](/apex/glossary#commons) actions, by group. Arguments are
|
||||
|
||||
`apex configure [--dry-run]`
|
||||
|
||||
Runs the whole suite in fixed order — `base`, `ufw`, `crowdsec`, `cron`, `systemd`, `routing` — and stops at the first failing step, propagating its exit code. `--dry-run` is forwarded to the steps that support it.
|
||||
Runs the suite in order: `base`, `timezone`, `ufw`, `crowdsec`, `cron`, `systemd`, `routing`, then additional node actions. A failing step contributes a nonzero exit status; later steps may still run. `--dry-run` is forwarded only to steps that support it, so it is not a guarantee that the entire umbrella command is read-only.
|
||||
|
||||
### configure/base
|
||||
|
||||
`apex configure/base`
|
||||
|
||||
Debian-only guard (exit `1` elsewhere). Updates the package index, installs the essential packages (`curl`, `wget`, `git`, `rsyslog`), ensures rsyslog is running, installs `ufw` when absent, and downloads the `ufw-docker` helper to `~/.local/bin/ufw-docker` (skipped when already present).
|
||||
Debian-only guard (exit `1` elsewhere). Checks installed package state for `curl`, `wget`, `git`, `rsyslog`, and `ufw`; installs missing packages and ensures rsyslog is running. Refuses an APT simulation containing upgrades or removals and uses `--no-upgrade --no-remove`. Avoid concurrent package operations: simulation and installation are separate transactions. Verifies the immutable revision and SHA-256 of `/usr/local/bin/ufw-docker` before running it as root. Unsafe paths and unverified downloads are rejected; an executable file alone is not trusted.
|
||||
|
||||
### configure/timezone
|
||||
|
||||
`apex configure/timezone [--dry-run]`
|
||||
|
||||
Applies the IANA zone named in `configs/timezone` and refreshes active cron after a change. An absent declaration preserves the current timezone; an invalid zone fails before changing it.
|
||||
|
||||
### configure/ufw
|
||||
|
||||
@@ -110,9 +116,19 @@ Two passes: (1) [anchor-IP](/apex/glossary#anchor-ip) validation over `compositi
|
||||
|
||||
### compose
|
||||
|
||||
`apex compose <action> [--dry-run] [extra...]`
|
||||
`apex compose [-p <composition>] [--dry-run] <action> [extra...]`
|
||||
|
||||
Orchestrates `docker compose <action>` across the node: the core project first, then services alphabetically (reversed for `down`). `up` implies `-d`; `.env`/`apex.env` are passed per project when present; extra arguments pass through; `--dry-run` (accepted in any position) prints commands instead of executing.
|
||||
Without a target, visits core first and other compositions alphabetically, reversed for `down`. `-p` selects one composition or `all`; `up` defaults to detached mode. `.env` then `apex.env` are passed per project, with generated identity/path values in the process environment. Remaining arguments pass through to Docker Compose. Wrapper options precede the action; `--dry-run` prints commands without executing them.
|
||||
|
||||
Compatibility forms include `apex compose restart apex traefik` and `apex compose up -d apex`. Use explicit `-p` to distinguish a composition from a service or option value.
|
||||
|
||||
## env {#env}
|
||||
|
||||
`apex env [--export | --dotenv | --write <path> | --sync]`
|
||||
|
||||
Default output is shell-quoted APEX exports for `eval "$(apex env)"`. `--dotenv` emits dotenv syntax; `--write` writes generated values to a separate file, including a relative filename. These modes do not read composition credentials or merge `apex.env`.
|
||||
|
||||
`--sync` preserves existing `compositions/apex/.env` text and updates only its managed block: committed `apex.env` settings first, generated identity/path values last. This includes non-APEX settings such as `COMPOSE_PROFILES` and `TZ`, allowing bare Docker Compose commands in the core directory. Existing tier symlinks remain intact, and synchronized files use mode `0600`. Resync after changing source settings or tier paths. Malformed managed markers fail without rewriting uncertain content. Shell environment values still have Docker Compose precedence.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -53,15 +53,14 @@ flowchart TD
|
||||
|
||||
## Модель «capture-up» {#capture-up}
|
||||
|
||||
Репозитории узлов служат реестрами состояния, а не просто источниками конфигурации: хост сам фиксирует то, что выполняет на практике.
|
||||
Команда `apex sync/repository` фиксирует рабочее дерево и версию commons
|
||||
в ветке `sync/<node>`, выполняет rebase на `origin/main` и отправляет ветку
|
||||
с флагом `--force-with-lease`. Если в commons есть незакоммиченные изменения
|
||||
или каталог `compositions/` отсутствует, команда отказывается работать.
|
||||
Конфликт rebase прерывает синхронизацию.
|
||||
|
||||
1. Регулярная задача `sync/repository` на хосте коммитит рабочее дерево в ветку `sync/<node>` (включая указатель субмодуля commons), делает rebase поверх `origin/main` и выполняет push с флагом `--force-with-lease`.
|
||||
2. Запланированное CI-задание в репозитории узла вливает ветку `sync/<node>` обратно в `main` от имени бота.
|
||||
|
||||
Благодаря циклу [«capture-up»](/apex/glossary#capture-up) любой дрейф конфигурации на хосте превращается в коммит: изменение наглядно отображается в diff и легко откатывается, исключая незаметные расхождения. Если субмодуль commons имеет незакоммиченные изменения («грязный») или каталог `compositions/` отсутствует, синхронизация прерывается без коммита, поэтому повреждённое состояние чекаута никогда не попадёт в историю.
|
||||
|
||||
> [!NOTE]
|
||||
> CI-задание слияния клонирует репозиторий *без* субмодулей: субмодуль commons подключён по SSH-URL, недоступному стандартному токену CI, а самому слиянию требуется лишь обновить указатель gitlink.
|
||||
Проверку и слияние этой ветки определяют правила репозитория. APEX не поставляет
|
||||
CI-сценарий слияния и не требует определённого способа размещения репозиториев.
|
||||
|
||||
## Проектные ограничения {#constraints}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ include:
|
||||
|---|---|---|
|
||||
| `xray` | xray | Прокси-шлюз с режимом host-network для анклава. |
|
||||
| `adguard` | adguard | DNS-фильтрация в сетевом пространстве имён traefik; маршрутизируется по пути `/adguard` на FQDN узла. |
|
||||
| `alloy` | alloy | Агент наблюдаемости на каждом узле (Grafana Alloy). Собирает метрики хоста и контейнеров, а также логи systemd journal и контейнеров, после чего **проталкивает** их исходящим потоком в центральные Prometheus и Loki (см. [Концепция: Модель безопасности](/apex/concepts/security-model#observability)). Имя хоста (`hostname`) контейнера совпадает с `APEX_NODE_HOST`, благодаря чему метки `node` и `instance` автоматически проставляются на всех метриках и потоках логов. |
|
||||
| `alloy` | alloy | Необязательный агент Grafana Alloy. Отправляет метрики хоста и контейнеров, системный журнал и логи контейнеров на заданные совместимые конечные точки. См. [настройку наблюдаемости](/apex/concepts/security-model#observability). |
|
||||
| `manual` | resticontainer | Одноразовый контейнер резервного копирования на основе меток (`ghcr.io/deytenit/resticontainer`). Никогда не запускается командой `up`; вызывается действием `backup/run`, которое определяет необходимость бэкапа каждого сервиса по compose-меткам `restic.*`. |
|
||||
|
||||
## Сети {#networks}
|
||||
|
||||
@@ -21,7 +21,7 @@ weight: 12
|
||||
```ini {filename="node.env"}
|
||||
APEX_NODE_FQDN=node1.example.com
|
||||
APEX_NODE_HOST=node1.example.com
|
||||
APEX_SUBNET=198.18.16.0/24
|
||||
APEX_SUBNET=198.18.0.0/24
|
||||
```
|
||||
|
||||
### Имя хоста ОС
|
||||
|
||||
@@ -34,7 +34,11 @@ weight: 18
|
||||
|
||||
## Наблюдаемость {#observability}
|
||||
|
||||
Система мониторинга построена по модели **исходящей отправки (push)**, а не входящих опросов. Опциональный сервис `alloy` (Grafana Alloy, один агент на узел) собирает метрики хоста (аналог `node_exporter`), метрики контейнеров (cadvisor), системный журнал systemd journal и логи docker-контейнеров, после чего передаёт их в **центральные** хранилища Prometheus и Loki на выделенном узле. Метрики отправляются через `prometheus.remote_write` на адрес `APEX_PROM_PUSH_URL`, логи — через `loki.write` на `APEX_LOKI_PUSH_URL`. Обе конечные точки защищены базовой аутентификацией HTTP basic auth (`APEX_OBS_BASIC_AUTH_USER` / `APEX_OBS_BASIC_AUTH_PASS`). Поскольку трафик отправляется наружу, Alloy подключён к сети `direct` с доступом в интернет, а не к закрытой сети `enclave`. На самих узлах логи не накапливаются, и открывать внешний доступ к эндпоинтам сервисов не требуется.
|
||||
Необязательный сервис `alloy` отправляет метрики хоста и контейнеров на адрес
|
||||
`APEX_PROM_PUSH_URL`, а логи — на `APEX_LOKI_PUSH_URL`. Укажите совместимые
|
||||
конечные точки и учётные данные HTTP basic auth в `APEX_OBS_BASIC_AUTH_USER`
|
||||
и `APEX_OBS_BASIC_AUTH_PASS`. Сервису нужен исходящий доступ к этим адресам.
|
||||
Размещение хранилищ и правила доступа определяет оператор.
|
||||
|
||||
## Изоляция секретов {#secrets}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ weight: 100
|
||||
|
||||
## Capture-up {#capture-up}
|
||||
|
||||
**Capture-up** — механизм фиксации состояния хоста в репозитории: регулярный коммит в ветку `sync/<node>`, который перебазируется на `main` и сливается средствами CI. В результате любой дрейф конфигурации становится наглядным diff, а не скрытым расхождением.
|
||||
**Capture-up** — фиксация рабочего состояния хоста в ветке `sync/<node>` командой `apex sync/repository`. Проверку и слияние определяют правила репозитория.
|
||||
|
||||
## Commons {#commons}
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ weight: 24
|
||||
|
||||
| Шаг | Эффект |
|
||||
|---|---|
|
||||
| `configure/base` | Готовит базовую систему Debian: обновляет списки пакетов, ставит базовые утилиты (`curl`, `wget`, `git`, `rsyslog`), запускает службу rsyslog, устанавливает `ufw` и вспомогательный скрипт `ufw-docker` в `~/.local/bin/ufw-docker`. |
|
||||
| `configure/base` | Устанавливает недостающие пакеты Debian после проверки плана APT на обновления и удаления, запускает rsyslog и проверяет закреплённую версию `/usr/local/bin/ufw-docker` перед запуском от root. Не запускайте другие пакетные операции одновременно. |
|
||||
| `configure/timezone` | Применяет `configs/timezone`, если файл есть, и перезапускает активную службу cron после смены часового пояса. Поддерживает `--dry-run`. |
|
||||
| `configure/ufw` | Применяет правила хоста, настраивает цепочки правил ядра и пробрасывает правила для контейнеров через `ufw-docker`. Поддерживает флаг `--dry-run`. |
|
||||
| `configure/crowdsec` | Устанавливает CrowdSec (если он ещё не поставлен), копирует `configs/crowdsec/` в `/etc/crowdsec/`, ставит коллекции и firewall bouncer (если включён в настройках). Поддерживает флаг `--dry-run`. |
|
||||
| `configure/cron` | Рендерит `configs/cron/crontab` через шаблонизатор и устанавливает расписание. Предусмотрены предпросмотр, аварийная остановка при пустом результате (код выхода 65) и откат при ошибке установки. Поддерживает флаг `--dry-run`. |
|
||||
@@ -27,10 +28,39 @@ weight: 24
|
||||
apex compose up # сначала core, затем сервисы по алфавиту
|
||||
apex compose down # обратный порядок
|
||||
apex compose restart
|
||||
apex compose up --dry-run # только напечатать команды
|
||||
apex compose --dry-run up # только напечатать команды
|
||||
apex compose -p apex restart traefik
|
||||
apex compose -p app up -d
|
||||
```
|
||||
|
||||
Все дополнительные аргументы движок передаёт напрямую в `docker compose` каждого проекта. Файлы `.env` и `apex.env` проектных каталогов подставляются автоматически при их наличии.
|
||||
Указывайте параметры APEX (`-p` и `--dry-run`) перед действием. Остальные аргументы
|
||||
передаются Docker Compose для выбранного проекта. Файлы `.env` и `apex.env`
|
||||
подключаются автоматически, если существуют. Формы `apex compose restart apex traefik`
|
||||
и `apex compose up -d apex` сохраняются; если имя сервиса или значение параметра
|
||||
совпадает с именем композиции, выбирайте цель через `-p`.
|
||||
|
||||
Замените `app` именем композиции из своего репозитория. После изменения
|
||||
монтирования или конфигурации контейнера используйте `up -d` для нужной
|
||||
композиции; `restart` сохраняет прежнюю конфигурацию контейнера.
|
||||
|
||||
### Прямой запуск Compose для core
|
||||
|
||||
```bash
|
||||
apex env --sync
|
||||
cd compositions/apex
|
||||
docker compose config --quiet
|
||||
docker compose restart traefik
|
||||
```
|
||||
|
||||
Синхронизация сохраняет текст секретов и UID/GID в `.env`, а также ссылку
|
||||
на файл в хранилище tier. Управляемый блок содержит настройки `apex.env`, включая
|
||||
профили, затем вычисленные значения узла и путей. Файл записывается с правами
|
||||
`0600`. Повторите синхронизацию после изменения настроек или путей; не редактируйте
|
||||
управляемый блок вручную. Переменные оболочки сохраняют приоритет Docker Compose.
|
||||
|
||||
`eval "$(apex env)"`, `apex env --dotenv` и `apex env --write generated.env`
|
||||
выводят или записывают только вычисленные значения APEX. Они не загружают секреты
|
||||
композиции и не заменяют `--sync`. Выбирайте один режим вывода за вызов.
|
||||
|
||||
## Резервные копии {#backups}
|
||||
|
||||
@@ -53,7 +83,7 @@ apex backup/run "$(cat <path-to-telegram-url-file>)"
|
||||
apex sync/repository <telegram-bot-url>
|
||||
```
|
||||
|
||||
Действие фиксирует изменения рабочей копии в ветке `sync/<node>`, выполняет rebase на `origin/main` и отправляет ветку через force-push с lease. Затем CI автоматически вливает изменения обратно в `main`. Этот механизм (capture-up) сохраняет актуальное состояние узла в репозиторий.
|
||||
Действие фиксирует изменения рабочей копии в ветке `sync/<node>`, выполняет rebase на `origin/main` и отправляет ветку через force-push с lease. Этот механизм (capture-up) сохраняет актуальное состояние узла в репозиторий.
|
||||
|
||||
Действие прерывается с подробным сообщением, если подмодуль commons содержит незакоммиченные изменения или каталог `compositions/` отсутствует. При конфликтах rebase выполнение останавливается для ручного вмешательства. Подробнее архитектурная модель описана в разделе [Concept: Framework architecture](/apex/concepts/architecture#capture-up).
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ mkdir -p proprietaries/actions proprietaries/lib configs compositions/apex .gith
|
||||
```ini {filename="node.env"}
|
||||
APEX_NODE_FQDN=node1.example.com
|
||||
APEX_NODE_HOST=node1.example.com
|
||||
APEX_SUBNET=198.18.16.0/24
|
||||
APEX_SUBNET=198.18.0.0/24
|
||||
APEX_GIT_AUTHOR_NAME=apex [bot]
|
||||
APEX_GIT_AUTHOR_EMAIL=apex@localhost
|
||||
```
|
||||
@@ -46,7 +46,7 @@ include:
|
||||
```
|
||||
|
||||
```ini {filename="compositions/apex/apex.env"}
|
||||
APEX_TRAEFIK_DIRECT_IP=198.18.16.10
|
||||
APEX_TRAEFIK_DIRECT_IP=198.18.0.10
|
||||
APEX_RESTIC_REPOSITORY=s3:<endpoint>/<bucket>/<node>
|
||||
COMPOSE_PROFILES=adguard,xray,alloy
|
||||
# цели push-наблюдаемости (только когда включён профиль alloy)
|
||||
@@ -84,9 +84,12 @@ __pycache__/
|
||||
|
||||
Создайте файл `compositions/ADDRESSING.md` с подсетью узла (`APEX_SUBNET`) и схемой распределения адресов: диапазоны `.10`–`.19` зарезервированы за базовыми сервисами (core), а `.20` и выше выделяются под проекты. Все назначаемые в будущем [якорные IP](/apex/glossary#anchor-ip) фиксируются в этом документе.
|
||||
|
||||
### Подключите CI capture-up
|
||||
### Определите правила проверки синхронизации
|
||||
|
||||
Настройте периодический CI-workflow, который автоматически вливает ветку `sync/<node>` в `main` (если ветка существует) с авторством бота. Шаг checkout должен выполняться **без** загрузки подмодулей: слияние только сдвигает указатель `commons`, а SSH-ссылка на подмодуль недоступна по стандартному CI-токену. Полный цикл описан в разделе [Concept: Framework architecture](/apex/concepts/architecture) ([capture-up](/apex/glossary#capture-up)).
|
||||
Решите, как проверять и вливать ветку `sync/<node>`, которую создаёт
|
||||
`apex sync/repository`. Для автоматического слияния настройте разрешения
|
||||
репозитория и доступ к субмодулям под выбранный сценарий.
|
||||
См. [модель capture-up](/apex/concepts/architecture#capture-up).
|
||||
|
||||
### Проверьте работу
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ services:
|
||||
- ./@tier2/<anchor>/data:/data
|
||||
networks:
|
||||
direct:
|
||||
ipv4_address: 198.18.16.20
|
||||
ipv4_address: 198.18.0.20
|
||||
enclave: {}
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
|
||||
@@ -4,7 +4,7 @@ description: "Каждое действие commons: аргументы и по
|
||||
weight: 32
|
||||
---
|
||||
|
||||
Шестнадцать действий [«commons»](/apex/glossary#commons) (общего репозитория), разбитых по группам. Аргументы показаны в том виде, в котором их принимает парсер; необязательные аргументы заключены в квадратные скобки. Коды возврата следуют [соглашению движка](/apex/reference/cli#exit-codes).
|
||||
Действия [commons](/apex/glossary#commons) по группам. Аргументы показаны в том виде, в котором их принимает парсер; необязательные аргументы заключены в квадратные скобки. Коды возврата следуют [соглашению движка](/apex/reference/cli#exit-codes).
|
||||
|
||||
## configure {#configure}
|
||||
|
||||
@@ -12,13 +12,19 @@ weight: 32
|
||||
|
||||
`apex configure [--dry-run]`
|
||||
|
||||
Запускает весь набор в фиксированном порядке (`base`, `ufw`, `crowdsec`, `cron`, `systemd`, `routing`) и останавливается на первом неудачном шаге, возвращая его код возврата. Флаг `--dry-run` передаётся шагам, которые его поддерживают.
|
||||
Запускает `base`, `timezone`, `ufw`, `crowdsec`, `cron`, `systemd`, `routing`, затем дополнительные действия узла. Ошибка шага даёт ненулевой итоговый код; последующие шаги могут продолжить работу. Флаг `--dry-run` передаётся только тем шагам, которые его поддерживают, поэтому не гарантирует отсутствие изменений для всей команды.
|
||||
|
||||
### configure/base
|
||||
|
||||
`apex configure/base`
|
||||
|
||||
Проверяет, что операционная система — Debian (иначе завершается с кодом `1`). Обновляет индекс пакетов, устанавливает необходимые пакеты (`curl`, `wget`, `git`, `rsyslog`), проверяет статус службы `rsyslog`, устанавливает `ufw`, если его нет, и загружает вспомогательную утилиту `ufw-docker` в `~/.local/bin/ufw-docker` (если утилита уже есть, загрузка пропускается).
|
||||
Проверяет, что операционная система — Debian; иначе завершается с кодом `1`. Проверяет состояние пакетов `curl`, `wget`, `git`, `rsyslog` и `ufw`, устанавливает недостающие и запускает rsyslog. Отклоняет план APT с обновлениями или удалениями и использует `--no-upgrade --no-remove`. Не запускайте другие пакетные операции одновременно: проверка плана и установка выполняются отдельно. Перед запуском `/usr/local/bin/ufw-docker` от root проверяет закреплённую ревизию и SHA-256. Небезопасные пути и непроверенные загрузки отклоняются; одних прав на исполнение недостаточно.
|
||||
|
||||
### configure/timezone
|
||||
|
||||
`apex configure/timezone [--dry-run]`
|
||||
|
||||
Применяет часовой пояс IANA из `configs/timezone` и перезапускает активную службу cron после изменения. Если файла нет, сохраняет текущий пояс; неверное имя приводит к ошибке до внесения изменений.
|
||||
|
||||
### configure/ufw
|
||||
|
||||
@@ -110,9 +116,19 @@ weight: 32
|
||||
|
||||
### compose
|
||||
|
||||
`apex compose <action> [--dry-run] [extra...]`
|
||||
`apex compose [-p <composition>] [--dry-run] <action> [extra...]`
|
||||
|
||||
Оркеструет запуск `docker compose <action>` по всему узлу: сначала основной проект, затем сервисы в алфавитном порядке (в обратном — для команды `down`). Команда `up` по умолчанию использует ключ `-d`; файлы `.env` и `apex.env` подключаются для каждого проекта при их наличии; дополнительные аргументы пробрасываются дальше; флаг `--dry-run` (принимается в любой позиции) выводит сформированные команды без их выполнения.
|
||||
Без цели обходит сначала core, затем остальные композиции по алфавиту; для `down` порядок обратный. Параметр `-p` выбирает одну композицию или `all`. Команда `up` по умолчанию добавляет `-d`; файлы `.env` и `apex.env` подключаются в этом порядке, вычисленные значения узла и путей передаются через окружение. Параметры APEX ставятся перед действием, остальные аргументы передаются Docker Compose. Флаг `--dry-run` печатает команды без запуска.
|
||||
|
||||
Сохраняются формы `apex compose restart apex traefik` и `apex compose up -d apex`. Используйте `-p`, чтобы явно отличить композицию от сервиса или значения параметра.
|
||||
|
||||
## env {#env}
|
||||
|
||||
`apex env [--export | --dotenv | --write <path> | --sync]`
|
||||
|
||||
По умолчанию выводит экспорты APEX для `eval "$(apex env)"` с экранированием оболочки. `--dotenv` выводит формат dotenv; `--write` записывает вычисленные значения в отдельный файл, в том числе по относительному пути. Эти режимы не читают секреты композиции и не объединяют `apex.env`.
|
||||
|
||||
Режим `--sync` сохраняет пользовательский текст `compositions/apex/.env` и обновляет управляемый блок: сначала настройки `apex.env`, затем вычисленные значения. В блок входят `COMPOSE_PROFILES` и `TZ`, поэтому Compose можно запускать напрямую из каталога core. Ссылки tier сохраняются, файл получает права `0600`. Повторяйте синхронизацию после изменения исходных настроек или путей. Некорректные маркеры останавливают запись; переменные оболочки сохраняют приоритет Docker Compose.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -48,10 +48,10 @@ params:
|
||||
# Analytics configuration
|
||||
analytics:
|
||||
umami:
|
||||
serverURL: '/beacon'
|
||||
scriptName: 'beacon.js'
|
||||
hostURL: '/beacon'
|
||||
websiteID: 'fbdadf3f-3484-4186-a2c2-cedcf76cf44f'
|
||||
serverURL: '/trace'
|
||||
scriptName: 'trace.js'
|
||||
hostURL: '/trace'
|
||||
websiteID: '6e65136b-48c2-4519-816f-023932ceb775'
|
||||
domains: 'docs.ermnvldmr.com'
|
||||
doNotTrack: 'true'
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
copyright: '© 2025 Eremin Vladimir. CC-BY-4.0. All rights reserved.'
|
||||
copyright: '© 2026 Vladimir Eremin · <a class="hx:underline hx:decoration-from-font hx:underline-offset-2 hx:hover:text-gray-900 hx:dark:hover:text-gray-100" href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener noreferrer">CC-BY 4.0</a><br/><span class="hx:inline-flex hx:items-center hx:flex-wrap hx:gap-1.5 hx:mt-1 hx:text-[11px] hx:text-gray-400 hx:dark:text-neutral-500">To make this site better, I collect anonymous analytics <label class="hx:inline-flex hx:items-center hx:cursor-pointer hx:align-middle"><input type="checkbox" data-umami-toggle aria-label="Toggle anonymous telemetry" checked class="hx:cursor-pointer hx:h-3.5 hx:w-3.5" /></label> · <a class="hx:underline hx:decoration-from-font hx:underline-offset-2 hx:hover:text-gray-600 hx:dark:hover:text-neutral-400" href="https://static.ermnvldmr.com/docs/privacy-policy.txt" target="_blank" rel="noopener noreferrer">Privacy Policy</a></span>'
|
||||
@@ -1,4 +1,4 @@
|
||||
copyright: '© 2025 Ерёмин Владимир. CC-BY-4.0. Все права защищены.'
|
||||
copyright: '© 2026 Владимир Ерёмин · <a class="hx:underline hx:decoration-from-font hx:underline-offset-2 hx:hover:text-gray-900 hx:dark:hover:text-gray-100" href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener noreferrer">CC-BY 4.0</a><br/><span class="hx:inline-flex hx:items-center hx:flex-wrap hx:gap-1.5 hx:mt-1 hx:text-[11px] hx:text-gray-400 hx:dark:text-neutral-500">Из желания сделать этот сайт лучше — я собираю анонимную аналитику <label class="hx:inline-flex hx:items-center hx:cursor-pointer hx:align-middle"><input type="checkbox" data-umami-toggle aria-label="Переключить анонимную телеметрию" checked class="hx:cursor-pointer hx:h-3.5 hx:w-3.5" /></label> · <a class="hx:underline hx:decoration-from-font hx:underline-offset-2 hx:hover:text-gray-600 hx:dark:hover:text-neutral-400" href="https://static.ermnvldmr.com/docs/privacy-policy.txt" target="_blank" rel="noopener noreferrer">Политика конфиденциальности</a></span>'
|
||||
note: Кстати
|
||||
tip: Полезно
|
||||
important: Важно
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.8 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 311 KiB |
@@ -2,13 +2,14 @@ import { localDevServer } from '@ermnvldmr/rsbuild-config/dev';
|
||||
import { defineSSGServiceConfig, discoverEntries, umamiAnalytics } from '@ermnvldmr/ssg/dev';
|
||||
import { THEME_INIT_SCRIPT, THEME_INIT_STYLES } from '@ermnvldmr/ui/dev';
|
||||
|
||||
const UMAMI_STATIC_WEBSITE_ID = '0831532a-2cec-4955-8f5b-452d8bba96d8';
|
||||
const UMAMI_STATIC_WEBSITE_ID = 'de274e99-9e1e-41bb-981a-9a4fd21d39c5';
|
||||
|
||||
export default defineSSGServiceConfig({
|
||||
server: localDevServer({ port: 3001 }),
|
||||
analytics: [
|
||||
umamiAnalytics({
|
||||
serverUrl: 'https://umami.ermnvldmr.com',
|
||||
scriptName: 'trace.js',
|
||||
websiteId: UMAMI_STATIC_WEBSITE_ID,
|
||||
domains: 'static.ermnvldmr.com',
|
||||
doNotTrack: true,
|
||||
|
||||
@@ -7,6 +7,7 @@ export default defineCli({
|
||||
description: 'Sync static assets to S3',
|
||||
targetPrefix: 'www',
|
||||
bucket: env('COM_ERMNVLDMR_STATIC_S3_BUCKET'),
|
||||
publicUrl: env('COM_ERMNVLDMR_STATIC_PUBLIC_URL', 'https://static.ermnvldmr.com'),
|
||||
region: env('COM_ERMNVLDMR_STATIC_S3_REGION'),
|
||||
endpoint: env('COM_ERMNVLDMR_STATIC_S3_ENDPOINT_URL'),
|
||||
accessKeyId: env('COM_ERMNVLDMR_STATIC_AWS_ACCESS_KEY_ID'),
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
"cli": "ermnvldmr-cli",
|
||||
"sync:static": "pnpm cli sync:static",
|
||||
"dev": "rsbuild dev",
|
||||
"build": "rsbuild build",
|
||||
"dev:en": "LOCALE=en rsbuild dev",
|
||||
"dev:ru": "LOCALE=ru rsbuild dev",
|
||||
"build": "pnpm run build:all",
|
||||
"build:en": "LOCALE=en rsbuild build",
|
||||
"build:ru": "LOCALE=ru rsbuild build",
|
||||
"build:all": "pnpm run build:en && pnpm run build:ru",
|
||||
|
||||
@@ -33,20 +33,18 @@ const MIME: Record<string, string> = {
|
||||
'.txt': 'text/plain',
|
||||
};
|
||||
|
||||
const UMAMI_WWW_WEBSITE_ID = '4fca5ac2-4fe4-4626-b708-933c37bfe918';
|
||||
const locale = process.env.LOCALE ?? 'en';
|
||||
const fontBase = `/${locale}/static/font`;
|
||||
const UMAMI_WWW_WEBSITE_ID = '41c7699a-0675-4f49-bcd0-3df47a66b6b1';
|
||||
|
||||
export default defineSSGServiceConfig(
|
||||
mergeConfig(localeRsbuildConfig(), {
|
||||
server: localDevServer({ port: 3000 }),
|
||||
analytics: [
|
||||
umamiAnalytics({
|
||||
serverUrl: '/beacon',
|
||||
scriptName: 'beacon.js',
|
||||
hostUrl: '/beacon',
|
||||
serverUrl: '/trace',
|
||||
scriptName: 'trace.js',
|
||||
hostUrl: '/trace',
|
||||
websiteId: UMAMI_WWW_WEBSITE_ID,
|
||||
domains: 'ermnvldmr.com,www.ermnvldmr.com',
|
||||
domains: 'www.ermnvldmr.com',
|
||||
doNotTrack: true,
|
||||
}),
|
||||
],
|
||||
@@ -99,44 +97,17 @@ export default defineSSGServiceConfig(
|
||||
entry: discoverEntries(import.meta.dirname, './src/app/**/*.tsx'),
|
||||
preEntry: ['./src/static/global.css'],
|
||||
},
|
||||
performance: {
|
||||
preload: {
|
||||
type: 'all-assets',
|
||||
include: [/(lato-400|lato-700|eb-garamond-var)\..*\.woff2$/],
|
||||
},
|
||||
},
|
||||
html: {
|
||||
tags: [
|
||||
{ tag: 'script', children: TRUSTED_TYPES_INIT_SCRIPT, head: true },
|
||||
{ tag: 'script', children: THEME_INIT_SCRIPT, head: true },
|
||||
{ tag: 'style', children: THEME_INIT_STYLES, head: true },
|
||||
{
|
||||
tag: 'link',
|
||||
attrs: {
|
||||
rel: 'preload',
|
||||
as: 'font',
|
||||
type: 'font/woff2',
|
||||
href: `${fontBase}/lato-400.woff2`,
|
||||
crossorigin: true,
|
||||
},
|
||||
publicPath: false,
|
||||
},
|
||||
{
|
||||
tag: 'link',
|
||||
attrs: {
|
||||
rel: 'preload',
|
||||
as: 'font',
|
||||
type: 'font/woff2',
|
||||
href: `${fontBase}/lato-700.woff2`,
|
||||
crossorigin: true,
|
||||
},
|
||||
publicPath: false,
|
||||
},
|
||||
{
|
||||
tag: 'link',
|
||||
attrs: {
|
||||
rel: 'preload',
|
||||
as: 'font',
|
||||
type: 'font/woff2',
|
||||
href: `${fontBase}/eb-garamond-var.woff2`,
|
||||
crossorigin: true,
|
||||
},
|
||||
publicPath: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { createKeyset } from '@ermnvldmr/i18n';
|
||||
|
||||
export const { t } = createKeyset({
|
||||
export const { t, tRaw } = createKeyset({
|
||||
'Vladimir Eremin': { en: 'Vladimir Eremin', ru: 'Владимир Ерёмин' },
|
||||
'Privacy Policy': { en: 'Privacy Policy', ru: 'Политика конфиденциальности' },
|
||||
'Toggle anonymous telemetry': {
|
||||
en: 'Toggle anonymous telemetry',
|
||||
ru: 'Переключить анонимную телеметрию',
|
||||
},
|
||||
'To make this site better, I collect anonymous analytics · {privacyLink}': {
|
||||
en: 'To\u00A0make this site better, I\u00A0collect anonymous analytics · {privacyLink}',
|
||||
ru: 'Из\u00A0желания сделать этот сайт лучше\u00A0— я\u00A0собираю анонимную аналитику · {privacyLink}',
|
||||
},
|
||||
});
|
||||
@@ -1,11 +1,14 @@
|
||||
import { ThemeSwitch } from '@ermnvldmr/kits';
|
||||
import { Container, HStack, Icon, Link, Text, VStack } from '@ermnvldmr/ui';
|
||||
import { createLocalStorage, useLocalStorage } from '@ermnvldmr/stl';
|
||||
import { Container, HStack, Icon, Link, Switch, Text, VStack } from '@ermnvldmr/ui';
|
||||
import { Monitor, Moon, Sun } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { LocaleSwitch } from '../LocaleSwitch/LocaleSwitch';
|
||||
import { RainbowSeparator } from '../RainbowSeparator';
|
||||
import { t } from './Footer.i18n';
|
||||
import { t, tRaw } from './Footer.i18n';
|
||||
|
||||
const umamiDisabledStorage = createLocalStorage<number | null>('umami.disabled', null);
|
||||
|
||||
/**
|
||||
* Props for the Footer component.
|
||||
@@ -17,7 +20,7 @@ export interface FooterProps {
|
||||
|
||||
/**
|
||||
* The standard footer for the www service.
|
||||
* Displays copyright information, license, and theme switcher.
|
||||
* Displays copyright information, license, privacy policy, and theme switcher.
|
||||
*
|
||||
* @returns A Footer component.
|
||||
* @example
|
||||
@@ -27,33 +30,65 @@ export interface FooterProps {
|
||||
*/
|
||||
export const Footer = ({ currentPath }: FooterProps) => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const [umamiDisabled, setUmamiDisabled] = useLocalStorage(umamiDisabledStorage);
|
||||
const isTelemetryEnabled = umamiDisabled !== 1;
|
||||
|
||||
const handleTelemetryToggle = useCallback(
|
||||
(isSelected: boolean): void => {
|
||||
if (isSelected) {
|
||||
umamiDisabledStorage.remove();
|
||||
} else {
|
||||
setUmamiDisabled(1);
|
||||
}
|
||||
},
|
||||
[setUmamiDisabled]
|
||||
);
|
||||
|
||||
return (
|
||||
<VStack as="footer" className="w-full" gap={0}>
|
||||
<VStack align="stretch" as="footer" gap={0}>
|
||||
<RainbowSeparator />
|
||||
<Container bg="transparent" className="w-full max-sm:p-8" padding={12}>
|
||||
<HStack
|
||||
align="center"
|
||||
className="w-full max-sm:flex-col max-sm:items-start max-sm:gap-8"
|
||||
justify="between"
|
||||
>
|
||||
<VStack align="start" gap={1}>
|
||||
<Container bg="transparent" padding={12}>
|
||||
<VStack align="stretch" gap={1}>
|
||||
<HStack align="center" gap={4} justify="between" wrap="wrap">
|
||||
<Text color="muted" size="s">
|
||||
© {currentYear} {t('Vladimir Eremin')}
|
||||
© {currentYear} {t('Vladimir Eremin')} ·{' '}
|
||||
<Link href="https://creativecommons.org/licenses/by/4.0/" size="s">
|
||||
CC-BY 4.0
|
||||
</Link>
|
||||
</Text>
|
||||
<Link href="https://creativecommons.org/licenses/by/4.0/" size="s">
|
||||
CC-BY 4.0
|
||||
</Link>
|
||||
</VStack>
|
||||
<VStack align="end" gap={4}>
|
||||
<ThemeSwitch
|
||||
darkIcon={<Icon icon={Moon} />}
|
||||
lightIcon={<Icon icon={Sun} />}
|
||||
systemIcon={<Icon icon={Monitor} />}
|
||||
/>
|
||||
</HStack>
|
||||
<HStack align="center" gap={4} justify="between" wrap="wrap">
|
||||
<HStack align="center" gap={2} wrap="wrap">
|
||||
<Text color="muted" size="s" type="label">
|
||||
{tRaw('To make this site better, I collect anonymous analytics · {privacyLink}', {
|
||||
privacyLink: (
|
||||
<Link
|
||||
key="privacy-link"
|
||||
href="https://static.ermnvldmr.com/www/privacy-policy.txt"
|
||||
size="s"
|
||||
type="label"
|
||||
>
|
||||
{t('Privacy Policy')}
|
||||
</Link>
|
||||
),
|
||||
})}
|
||||
</Text>
|
||||
<Switch
|
||||
aria-label={t('Toggle anonymous telemetry')}
|
||||
isSelected={isTelemetryEnabled}
|
||||
size="s"
|
||||
variant="outline"
|
||||
onChange={handleTelemetryToggle}
|
||||
/>
|
||||
</HStack>
|
||||
<LocaleSwitch currentPath={currentPath} />
|
||||
</VStack>
|
||||
</HStack>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Container>
|
||||
</VStack>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
@@ -12,7 +13,10 @@ describe('ArticleLayout', () => {
|
||||
createdDate: new Date('2026-01-01T00:00:00Z'),
|
||||
};
|
||||
|
||||
it('renders title, created date, and content', () => {
|
||||
it('renders title, created date, and content', async () => {
|
||||
const user = userEvent.setup();
|
||||
window.localStorage.removeItem('umami.disabled');
|
||||
|
||||
render(
|
||||
<ArticleLayout {...defaultProps}>
|
||||
Article content body
|
||||
@@ -21,6 +25,21 @@ describe('ArticleLayout', () => {
|
||||
|
||||
expect(screen.getByRole('heading', { level: 1, name: 'Test Article Title' })).toBeInTheDocument();
|
||||
expect(screen.getByText('Article content body')).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Privacy Policy' })).toHaveAttribute(
|
||||
'href',
|
||||
'https://static.ermnvldmr.com/www/privacy-policy.txt'
|
||||
);
|
||||
|
||||
const telemetrySwitch = screen.getByRole('switch', { name: 'Toggle anonymous telemetry' });
|
||||
expect(telemetrySwitch).toBeChecked();
|
||||
|
||||
await user.click(telemetrySwitch);
|
||||
expect(telemetrySwitch).not.toBeChecked();
|
||||
expect(window.localStorage.getItem('umami.disabled')).toBe('1');
|
||||
|
||||
await user.click(telemetrySwitch);
|
||||
expect(telemetrySwitch).toBeChecked();
|
||||
expect(window.localStorage.getItem('umami.disabled')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders updated date when updatedDate prop is provided', () => {
|
||||
|
||||
@@ -23,11 +23,12 @@ export const LocaleSwitch: React.FC<LocaleSwitchProps> = ({ currentPath }) => {
|
||||
|
||||
return (
|
||||
<Link
|
||||
className="text-neutral-400 hover:text-neutral-500 dark:text-neutral-500 dark:hover:text-neutral-400"
|
||||
color="muted"
|
||||
data-umami-event="locale-switch"
|
||||
data-umami-event-target={OTHER_LOCALE}
|
||||
href={href}
|
||||
size="s"
|
||||
type="label"
|
||||
>
|
||||
{OTHER_LOCALE_LABEL}
|
||||
</Link>
|
||||
|
||||
Reference in new issue
Block a user