@duffcloudservices/cli 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -158
- package/dist/index.js +318 -207
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
import chalk8 from "chalk";
|
|
6
6
|
|
|
7
7
|
// package.json
|
|
8
|
-
var version = "0.1.
|
|
8
|
+
var version = "0.1.3";
|
|
9
9
|
|
|
10
10
|
// src/commands/auth.ts
|
|
11
11
|
import chalk2 from "chalk";
|
|
@@ -441,181 +441,198 @@ import chalk4 from "chalk";
|
|
|
441
441
|
import ora2 from "ora";
|
|
442
442
|
|
|
443
443
|
// src/templates/site-deploy.yml
|
|
444
|
-
var site_deploy_default = `# Site Deployment Workflow
|
|
445
|
-
# Generated by @duffcloudservices/cli
|
|
446
|
-
# This workflow deploys the site to Azure Static Web Apps using OIDC authentication
|
|
447
|
-
|
|
448
|
-
name: Site Deployment
|
|
449
|
-
|
|
450
|
-
on
|
|
451
|
-
push
|
|
452
|
-
branches
|
|
453
|
-
- 'release/**'
|
|
454
|
-
- 'master'
|
|
455
|
-
workflow_dispatch
|
|
456
|
-
inputs
|
|
457
|
-
trigger_reason
|
|
458
|
-
description: 'Reason for manual deployment'
|
|
459
|
-
required: false
|
|
460
|
-
type: string
|
|
461
|
-
default: 'Manual deployment'
|
|
462
|
-
|
|
463
|
-
# Prevent concurrent deployments for the same branch
|
|
464
|
-
concurrency
|
|
465
|
-
group: site-deploy-\${{ github.ref }}
|
|
466
|
-
cancel-in-progress: false
|
|
467
|
-
|
|
468
|
-
permissions
|
|
469
|
-
contents: read
|
|
470
|
-
issues: write
|
|
471
|
-
pull-requests: read
|
|
472
|
-
id-token: write # Required for Azure OIDC authentication
|
|
473
|
-
|
|
474
|
-
jobs
|
|
475
|
-
deploy
|
|
476
|
-
name: Deploy to Azure Static Web App
|
|
477
|
-
runs-on: ubuntu-latest
|
|
478
|
-
# Skip deployment on branch creation events
|
|
479
|
-
if: github.event.before != '0000000000000000000000000000000000000000'
|
|
480
|
-
|
|
481
|
-
steps
|
|
482
|
-
- name: Determine deployment environment
|
|
483
|
-
id: environment
|
|
484
|
-
run:
|
|
485
|
-
BRANCH="\${{ github.ref_name }}"
|
|
486
|
-
echo "Branch: $BRANCH"
|
|
487
|
-
|
|
488
|
-
if [ "$BRANCH" == "master" ]; then
|
|
489
|
-
echo "environment=Production" >> $GITHUB_OUTPUT
|
|
490
|
-
echo "environment_name=Production" >> $GITHUB_OUTPUT
|
|
491
|
-
echo "swa_environment=" >> $GITHUB_OUTPUT
|
|
492
|
-
else
|
|
493
|
-
echo "environment=preview" >> $GITHUB_OUTPUT
|
|
494
|
-
echo "environment_name=Preview" >> $GITHUB_OUTPUT
|
|
495
|
-
echo "swa_environment=preview" >> $GITHUB_OUTPUT
|
|
496
|
-
fi
|
|
497
|
-
|
|
498
|
-
- name: Checkout repository
|
|
499
|
-
uses: actions/checkout@v4
|
|
500
|
-
|
|
501
|
-
- name: Read site configuration
|
|
502
|
-
id: config
|
|
503
|
-
run:
|
|
504
|
-
CONFIG_FILE=".dcs/site.yaml"
|
|
505
|
-
|
|
506
|
-
if [ ! -f "$CONFIG_FILE" ]; then
|
|
507
|
-
echo "::error::Missing DCS site configuration file: $CONFIG_FILE"
|
|
508
|
-
exit 1
|
|
509
|
-
fi
|
|
510
|
-
|
|
511
|
-
SITE_NAME=$(grep -E '^site_name:' "$CONFIG_FILE" | sed 's/site_name:\\s*//' | tr -d '"' | tr -d "'")
|
|
512
|
-
SITE_SLUG=$(grep -E '^site_slug:' "$CONFIG_FILE" | sed 's/site_slug:\\s*//' | tr -d '"' | tr -d "'")
|
|
513
|
-
SWA_RESOURCE_ID=$(grep -E '^swa_resource_id:' "$CONFIG_FILE" | sed 's/swa_resource_id:\\s*//' | tr -d '"' | tr -d "'")
|
|
514
|
-
PORTAL_API_URL=$(grep -E '^portal_api_url:' "$CONFIG_FILE" | sed 's/portal_api_url:\\s*//' | tr -d '"' | tr -d "'")
|
|
515
|
-
PORTAL_API_URL="\${PORTAL_API_URL:-https://portal.duffcloudservices.com}"
|
|
516
|
-
GOOGLE_ANALYTICS_ID=$(grep -E '^google_analytics_id:' "$CONFIG_FILE" | sed 's/google_analytics_id:\\s*//' | tr -d '"' | tr -d "'")
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
echo "
|
|
538
|
-
echo "
|
|
539
|
-
echo "
|
|
540
|
-
echo "
|
|
541
|
-
echo "
|
|
542
|
-
echo "
|
|
543
|
-
echo "
|
|
544
|
-
echo "
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
EOF
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
444
|
+
var site_deploy_default = `# Site Deployment Workflow
|
|
445
|
+
# Generated by @duffcloudservices/cli
|
|
446
|
+
# This workflow deploys the site to Azure Static Web Apps using OIDC authentication.
|
|
447
|
+
|
|
448
|
+
name: Site Deployment
|
|
449
|
+
|
|
450
|
+
on:
|
|
451
|
+
push:
|
|
452
|
+
branches:
|
|
453
|
+
- 'release/**'
|
|
454
|
+
- 'master'
|
|
455
|
+
workflow_dispatch:
|
|
456
|
+
inputs:
|
|
457
|
+
trigger_reason:
|
|
458
|
+
description: 'Reason for manual deployment'
|
|
459
|
+
required: false
|
|
460
|
+
type: string
|
|
461
|
+
default: 'Manual deployment'
|
|
462
|
+
|
|
463
|
+
# Prevent concurrent deployments for the same branch
|
|
464
|
+
concurrency:
|
|
465
|
+
group: site-deploy-\${{ github.ref }}
|
|
466
|
+
cancel-in-progress: false
|
|
467
|
+
|
|
468
|
+
permissions:
|
|
469
|
+
contents: read
|
|
470
|
+
issues: write
|
|
471
|
+
pull-requests: read
|
|
472
|
+
id-token: write # Required for Azure OIDC authentication
|
|
473
|
+
|
|
474
|
+
jobs:
|
|
475
|
+
deploy:
|
|
476
|
+
name: Deploy to Azure Static Web App
|
|
477
|
+
runs-on: ubuntu-latest
|
|
478
|
+
# Skip deployment on branch creation events
|
|
479
|
+
if: github.event.before != '0000000000000000000000000000000000000000'
|
|
480
|
+
|
|
481
|
+
steps:
|
|
482
|
+
- name: Determine deployment environment
|
|
483
|
+
id: environment
|
|
484
|
+
run: |
|
|
485
|
+
BRANCH="\${{ github.ref_name }}"
|
|
486
|
+
echo "Branch: $BRANCH"
|
|
487
|
+
|
|
488
|
+
if [ "$BRANCH" == "master" ]; then
|
|
489
|
+
echo "environment=Production" >> $GITHUB_OUTPUT
|
|
490
|
+
echo "environment_name=Production" >> $GITHUB_OUTPUT
|
|
491
|
+
echo "swa_environment=" >> $GITHUB_OUTPUT
|
|
492
|
+
else
|
|
493
|
+
echo "environment=preview" >> $GITHUB_OUTPUT
|
|
494
|
+
echo "environment_name=Preview" >> $GITHUB_OUTPUT
|
|
495
|
+
echo "swa_environment=preview" >> $GITHUB_OUTPUT
|
|
496
|
+
fi
|
|
497
|
+
|
|
498
|
+
- name: Checkout repository
|
|
499
|
+
uses: actions/checkout@v4
|
|
500
|
+
|
|
501
|
+
- name: Read site configuration
|
|
502
|
+
id: config
|
|
503
|
+
run: |
|
|
504
|
+
CONFIG_FILE=".dcs/site.yaml"
|
|
505
|
+
|
|
506
|
+
if [ ! -f "$CONFIG_FILE" ]; then
|
|
507
|
+
echo "::error::Missing DCS site configuration file: $CONFIG_FILE"
|
|
508
|
+
exit 1
|
|
509
|
+
fi
|
|
510
|
+
|
|
511
|
+
SITE_NAME=$(grep -E '^site_name:' "$CONFIG_FILE" | sed 's/site_name:\\s*//' | tr -d '"' | tr -d "'")
|
|
512
|
+
SITE_SLUG=$(grep -E '^site_slug:' "$CONFIG_FILE" | sed 's/site_slug:\\s*//' | tr -d '"' | tr -d "'")
|
|
513
|
+
SWA_RESOURCE_ID=$(grep -E '^swa_resource_id:' "$CONFIG_FILE" | sed 's/swa_resource_id:\\s*//' | tr -d '"' | tr -d "'")
|
|
514
|
+
PORTAL_API_URL=$(grep -E '^portal_api_url:' "$CONFIG_FILE" | sed 's/portal_api_url:\\s*//' | tr -d '"' | tr -d "'")
|
|
515
|
+
PORTAL_API_URL="\${PORTAL_API_URL:-https://portal.duffcloudservices.com}"
|
|
516
|
+
GOOGLE_ANALYTICS_ID=$(grep -E '^google_analytics_id:' "$CONFIG_FILE" | sed 's/google_analytics_id:\\s*//' | tr -d '"' | tr -d "'")
|
|
517
|
+
PROD_APPINSIGHTS_CONNECTION_STRING=$(grep -E '^prod_appinsights_connection_string:' "$CONFIG_FILE" | sed 's/prod_appinsights_connection_string:\\s*//' | tr -d '"' | tr -d "'" || true)
|
|
518
|
+
DEV_APPINSIGHTS_CONNECTION_STRING=$(grep -E '^dev_appinsights_connection_string:' "$CONFIG_FILE" | sed 's/dev_appinsights_connection_string:\\s*//' | tr -d '"' | tr -d "'" || true)
|
|
519
|
+
|
|
520
|
+
# Azure Authentication
|
|
521
|
+
AZURE_CLIENT_ID=$(grep -E '^\\s*client_id:' "$CONFIG_FILE" | sed 's/.*client_id:\\s*//' | tr -d '"' | tr -d "'")
|
|
522
|
+
AZURE_TENANT_ID=$(grep -E '^\\s*tenant_id:' "$CONFIG_FILE" | sed 's/.*tenant_id:\\s*//' | tr -d '"' | tr -d "'")
|
|
523
|
+
AZURE_SUBSCRIPTION_ID=$(grep -E '^\\s*subscription_id:' "$CONFIG_FILE" | sed 's/.*subscription_id:\\s*//' | tr -d '"' | tr -d "'")
|
|
524
|
+
|
|
525
|
+
# Build Configuration
|
|
526
|
+
APP_LOCATION=$(grep -E '^\\s*app_location:' "$CONFIG_FILE" | sed 's/.*app_location:\\s*//' | tr -d '"' | tr -d "'")
|
|
527
|
+
OUTPUT_LOCATION=$(grep -E '^\\s*output_location:' "$CONFIG_FILE" | sed 's/.*output_location:\\s*//' | tr -d '"' | tr -d "'")
|
|
528
|
+
|
|
529
|
+
APP_LOCATION="\${APP_LOCATION:-dist}"
|
|
530
|
+
OUTPUT_LOCATION="\${OUTPUT_LOCATION:-.}"
|
|
531
|
+
|
|
532
|
+
if [ -z "$SWA_RESOURCE_ID" ]; then
|
|
533
|
+
echo "::error::Missing swa_resource_id in $CONFIG_FILE"
|
|
534
|
+
exit 1
|
|
535
|
+
fi
|
|
536
|
+
|
|
537
|
+
echo "site_name=$SITE_NAME" >> $GITHUB_OUTPUT
|
|
538
|
+
echo "site_slug=$SITE_SLUG" >> $GITHUB_OUTPUT
|
|
539
|
+
echo "swa_resource_id=$SWA_RESOURCE_ID" >> $GITHUB_OUTPUT
|
|
540
|
+
echo "app_location=$APP_LOCATION" >> $GITHUB_OUTPUT
|
|
541
|
+
echo "output_location=$OUTPUT_LOCATION" >> $GITHUB_OUTPUT
|
|
542
|
+
echo "azure_client_id=$AZURE_CLIENT_ID" >> $GITHUB_OUTPUT
|
|
543
|
+
echo "azure_tenant_id=$AZURE_TENANT_ID" >> $GITHUB_OUTPUT
|
|
544
|
+
echo "azure_subscription_id=$AZURE_SUBSCRIPTION_ID" >> $GITHUB_OUTPUT
|
|
545
|
+
echo "portal_api_url=$PORTAL_API_URL" >> $GITHUB_OUTPUT
|
|
546
|
+
echo "google_analytics_id=$GOOGLE_ANALYTICS_ID" >> $GITHUB_OUTPUT
|
|
547
|
+
echo "prod_appinsights_connection_string=$PROD_APPINSIGHTS_CONNECTION_STRING" >> $GITHUB_OUTPUT
|
|
548
|
+
echo "dev_appinsights_connection_string=$DEV_APPINSIGHTS_CONNECTION_STRING" >> $GITHUB_OUTPUT
|
|
549
|
+
|
|
550
|
+
- name: Azure login via OIDC
|
|
551
|
+
uses: azure/login@v2
|
|
552
|
+
with:
|
|
553
|
+
client-id: \${{ steps.config.outputs.azure_client_id || vars.AZURE_CLIENT_ID }}
|
|
554
|
+
tenant-id: \${{ steps.config.outputs.azure_tenant_id || vars.AZURE_TENANT_ID }}
|
|
555
|
+
subscription-id: \${{ steps.config.outputs.azure_subscription_id || vars.AZURE_SUBSCRIPTION_ID }}
|
|
556
|
+
|
|
557
|
+
- name: Get deployment tokens
|
|
558
|
+
id: tokens
|
|
559
|
+
run: |
|
|
560
|
+
# Get access token for DCS API
|
|
561
|
+
ACCESS_TOKEN=$(az account get-access-token --resource "api://304711b9-8532-4659-beb7-c85726de9ae5" --query accessToken -o tsv)
|
|
562
|
+
|
|
563
|
+
SITE_NAME="\${{ steps.config.outputs.site_name }}"
|
|
564
|
+
SITE_SLUG="\${{ steps.config.outputs.site_slug }}"
|
|
565
|
+
PORTAL_API_URL="\${{ steps.config.outputs.portal_api_url }}"
|
|
566
|
+
|
|
567
|
+
RESPONSE=$(curl -s -X POST "\${PORTAL_API_URL}/api/v1/sites/deployment-tokens" \\
|
|
568
|
+
-H "Authorization: Bearer $ACCESS_TOKEN" \\
|
|
569
|
+
-H "Content-Type: application/json" \\
|
|
570
|
+
-d "{\\"siteName\\": \\"$SITE_NAME\\", \\"siteSlug\\": \\"$SITE_SLUG\\"}")
|
|
571
|
+
|
|
572
|
+
SWA_TOKEN=$(echo $RESPONSE | jq -r .swaToken)
|
|
573
|
+
|
|
574
|
+
if [ -z "$SWA_TOKEN" ] || [ "$SWA_TOKEN" == "null" ]; then
|
|
575
|
+
echo "::error::Failed to retrieve SWA token from DCS API"
|
|
576
|
+
echo "Response: $RESPONSE"
|
|
577
|
+
exit 1
|
|
578
|
+
fi
|
|
579
|
+
|
|
580
|
+
echo "::add-mask::$SWA_TOKEN"
|
|
581
|
+
echo "swa_token=$SWA_TOKEN" >> $GITHUB_OUTPUT
|
|
582
|
+
|
|
583
|
+
- name: Setup Node.js
|
|
584
|
+
uses: actions/setup-node@v4
|
|
585
|
+
with:
|
|
586
|
+
node-version: "22"
|
|
587
|
+
|
|
588
|
+
- name: Install pnpm
|
|
589
|
+
uses: pnpm/action-setup@v4
|
|
590
|
+
|
|
591
|
+
- name: Setup pnpm cache
|
|
592
|
+
uses: actions/cache@v4
|
|
593
|
+
with:
|
|
594
|
+
path: ~/.pnpm-store
|
|
595
|
+
key: \${{ runner.os }}-pnpm-\${{ hashFiles('**/pnpm-lock.yaml') }}
|
|
596
|
+
restore-keys: |
|
|
597
|
+
\${{ runner.os }}-pnpm-
|
|
598
|
+
|
|
599
|
+
- name: Install dependencies
|
|
600
|
+
run: pnpm install --frozen-lockfile
|
|
601
|
+
|
|
602
|
+
- name: Create environment file
|
|
603
|
+
run: |
|
|
604
|
+
cat > .env << EOF
|
|
605
|
+
VITE_APP_VERSION="\${{ github.sha }}"
|
|
606
|
+
VITE_BACKEND_URI="\${{ steps.config.outputs.portal_api_url }}"
|
|
607
|
+
VITE_SITE_SLUG="\${{ steps.config.outputs.site_slug }}"
|
|
608
|
+
VITE_GOOGLE_ANALYTICS_ID="\${{ steps.config.outputs.google_analytics_id }}"
|
|
609
|
+
VITE_PROD_APPINSIGHTS_CONNECTION_STRING="\${{ steps.config.outputs.prod_appinsights_connection_string }}"
|
|
610
|
+
VITE_DEV_APPINSIGHTS_CONNECTION_STRING="\${{ steps.config.outputs.dev_appinsights_connection_string }}"
|
|
611
|
+
EOF
|
|
612
|
+
|
|
613
|
+
- name: Build site
|
|
614
|
+
run: pnpm run build
|
|
615
|
+
|
|
616
|
+
- name: Inject Google Analytics
|
|
617
|
+
if: steps.config.outputs.google_analytics_id != ''
|
|
618
|
+
run: |
|
|
619
|
+
GA_ID="\${{ steps.config.outputs.google_analytics_id }}"
|
|
620
|
+
OUTPUT_DIR="\${{ steps.config.outputs.app_location }}"
|
|
621
|
+
echo "Injecting Google Analytics tag \${GA_ID} into \${OUTPUT_DIR}..."
|
|
622
|
+
find "$OUTPUT_DIR" -name "*.html" -exec sed -i "s|</head>|<script async src=\\"https://www.googletagmanager.com/gtag/js?id=\${GA_ID}\\"></script><script>window.dataLayer=window.dataLayer\\|\\|[];function gtag(){dataLayer.push(arguments)}gtag('js',new Date());gtag('config','\${GA_ID}')</script>\\n</head>|" {} +
|
|
623
|
+
INJECTED=$(grep -rl "googletagmanager" "$OUTPUT_DIR" --include="*.html" | wc -l)
|
|
624
|
+
echo "Injected GA tag into \${INJECTED} HTML file(s)"
|
|
625
|
+
|
|
626
|
+
- name: Deploy to Azure Static Web Apps
|
|
627
|
+
uses: Azure/static-web-apps-deploy@v1
|
|
628
|
+
with:
|
|
629
|
+
azure_static_web_apps_api_token: \${{ steps.tokens.outputs.swa_token }}
|
|
630
|
+
repo_token: \${{ secrets.GITHUB_TOKEN }}
|
|
631
|
+
action: "upload"
|
|
632
|
+
app_location: \${{ steps.config.outputs.app_location }}
|
|
633
|
+
output_location: \${{ steps.config.outputs.output_location }}
|
|
634
|
+
skip_app_build: true
|
|
635
|
+
deployment_environment: \${{ steps.environment.outputs.swa_environment != '' && steps.environment.outputs.swa_environment || '' }}
|
|
619
636
|
`;
|
|
620
637
|
|
|
621
638
|
// src/commands/init.ts
|
|
@@ -2088,6 +2105,85 @@ import path4 from "path";
|
|
|
2088
2105
|
import yaml3 from "js-yaml";
|
|
2089
2106
|
import chalk7 from "chalk";
|
|
2090
2107
|
import ora3 from "ora";
|
|
2108
|
+
var CARD_THUMBNAIL_ASPECT_RATIO = 16 / 9;
|
|
2109
|
+
var ASSET_WAIT_TIMEOUT_MS = 5e3;
|
|
2110
|
+
async function disableCaptureMotion(page) {
|
|
2111
|
+
await page.addStyleTag({
|
|
2112
|
+
content: `
|
|
2113
|
+
*, *::before, *::after {
|
|
2114
|
+
animation-delay: -1ms !important;
|
|
2115
|
+
animation-duration: 1ms !important;
|
|
2116
|
+
animation-iteration-count: 1 !important;
|
|
2117
|
+
scroll-behavior: auto !important;
|
|
2118
|
+
transition-delay: 0s !important;
|
|
2119
|
+
transition-duration: 0s !important;
|
|
2120
|
+
}
|
|
2121
|
+
`
|
|
2122
|
+
}).catch(() => {
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
async function waitForAboveFoldAssets(page) {
|
|
2126
|
+
await page.evaluate(async (timeoutMs) => {
|
|
2127
|
+
const waitForFonts = typeof document.fonts?.ready?.then === "function" ? document.fonts.ready.catch(() => void 0) : Promise.resolve();
|
|
2128
|
+
const visibleImages = Array.from(document.images).filter((img) => {
|
|
2129
|
+
const rect = img.getBoundingClientRect();
|
|
2130
|
+
return rect.bottom >= -100 && rect.top <= window.innerHeight * 1.5;
|
|
2131
|
+
});
|
|
2132
|
+
const waitForImages = Promise.all(visibleImages.map((img) => {
|
|
2133
|
+
if (img.complete && img.naturalWidth > 0) {
|
|
2134
|
+
return Promise.resolve();
|
|
2135
|
+
}
|
|
2136
|
+
return new Promise((resolve) => {
|
|
2137
|
+
const done = () => resolve();
|
|
2138
|
+
img.addEventListener("load", done, { once: true });
|
|
2139
|
+
img.addEventListener("error", done, { once: true });
|
|
2140
|
+
});
|
|
2141
|
+
}));
|
|
2142
|
+
await Promise.race([
|
|
2143
|
+
Promise.all([waitForFonts, waitForImages]),
|
|
2144
|
+
new Promise((resolve) => window.setTimeout(resolve, timeoutMs))
|
|
2145
|
+
]);
|
|
2146
|
+
}, ASSET_WAIT_TIMEOUT_MS).catch((error) => {
|
|
2147
|
+
console.warn(` Warning: could not verify above-fold assets: ${error instanceof Error ? error.message : String(error)}`);
|
|
2148
|
+
});
|
|
2149
|
+
}
|
|
2150
|
+
async function lazyLoadPage(page, viewportHeight) {
|
|
2151
|
+
let pageHeight = await page.evaluate(() => document.documentElement.scrollHeight);
|
|
2152
|
+
const scrollStep = Math.max(viewportHeight, 1);
|
|
2153
|
+
for (let scrollY = 0; scrollY < pageHeight; scrollY += scrollStep) {
|
|
2154
|
+
await page.evaluate((y) => window.scrollTo(0, y), scrollY);
|
|
2155
|
+
await page.waitForTimeout(100);
|
|
2156
|
+
}
|
|
2157
|
+
await page.waitForTimeout(200);
|
|
2158
|
+
pageHeight = await page.evaluate(() => document.documentElement.scrollHeight);
|
|
2159
|
+
return pageHeight;
|
|
2160
|
+
}
|
|
2161
|
+
async function resetScrollForFullPageCapture(page) {
|
|
2162
|
+
await page.evaluate(() => {
|
|
2163
|
+
window.scrollTo(0, 0);
|
|
2164
|
+
window.dispatchEvent(new Event("scroll"));
|
|
2165
|
+
});
|
|
2166
|
+
await page.waitForFunction(() => Math.abs(window.scrollY) < 1, void 0, { timeout: 1e3 }).catch(() => void 0);
|
|
2167
|
+
await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))).catch(() => void 0);
|
|
2168
|
+
await page.waitForTimeout(350);
|
|
2169
|
+
await waitForAboveFoldAssets(page);
|
|
2170
|
+
}
|
|
2171
|
+
async function captureHeaderThumbnail(page, outputPath, viewport) {
|
|
2172
|
+
await page.setViewportSize(viewport);
|
|
2173
|
+
await page.evaluate(() => window.scrollTo(0, 0));
|
|
2174
|
+
await page.waitForTimeout(200);
|
|
2175
|
+
await waitForAboveFoldAssets(page);
|
|
2176
|
+
const clip = await page.evaluate((aspectRatio) => {
|
|
2177
|
+
const width = Math.max(document.documentElement.clientWidth, window.innerWidth, 1);
|
|
2178
|
+
const pageHeight = Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0, 1);
|
|
2179
|
+
const height = Math.min(Math.round(width / aspectRatio), pageHeight);
|
|
2180
|
+
return { x: 0, y: 0, width, height };
|
|
2181
|
+
}, CARD_THUMBNAIL_ASPECT_RATIO);
|
|
2182
|
+
await page.screenshot({
|
|
2183
|
+
path: outputPath,
|
|
2184
|
+
clip
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2091
2187
|
var SECTION_SELECTORS = {
|
|
2092
2188
|
explicit: "[data-section]",
|
|
2093
2189
|
header: 'header, [role="banner"], .header, #header',
|
|
@@ -2201,14 +2297,16 @@ async function generateSectionDisplayName(element, sectionType, index) {
|
|
|
2201
2297
|
}
|
|
2202
2298
|
async function detectTextKeys(_page, sectionElement, _pageSlug, _validTextKeys) {
|
|
2203
2299
|
const textKeys = [];
|
|
2204
|
-
const textKeyElements = await sectionElement.$$("[data-text-key]");
|
|
2300
|
+
const textKeyElements = await sectionElement.$$("[data-text-key], [data-dcs-text], [data-dcs-image-key]");
|
|
2205
2301
|
for (const element of textKeyElements) {
|
|
2206
2302
|
const keyInfo = await element.evaluate((el) => {
|
|
2207
2303
|
const rect = el.getBoundingClientRect();
|
|
2304
|
+
const explicitImageUrl = el.getAttribute("data-dcs-image-url");
|
|
2305
|
+
const imageUrl = el instanceof HTMLImageElement ? el.currentSrc || el.src : "";
|
|
2208
2306
|
return {
|
|
2209
|
-
key: el.getAttribute("data-text-key") || "",
|
|
2307
|
+
key: el.getAttribute("data-text-key") || el.getAttribute("data-dcs-text") || el.getAttribute("data-dcs-image-key") || "",
|
|
2210
2308
|
elementType: el.tagName.toLowerCase(),
|
|
2211
|
-
currentValue: el.textContent?.trim().slice(0, 500)
|
|
2309
|
+
currentValue: (explicitImageUrl || imageUrl || el.textContent?.trim() || "").slice(0, 500),
|
|
2212
2310
|
bounds: {
|
|
2213
2311
|
x: rect.left + window.scrollX,
|
|
2214
2312
|
y: rect.top + window.scrollY,
|
|
@@ -2217,7 +2315,7 @@ async function detectTextKeys(_page, sectionElement, _pageSlug, _validTextKeys)
|
|
|
2217
2315
|
}
|
|
2218
2316
|
};
|
|
2219
2317
|
});
|
|
2220
|
-
if (keyInfo.key) {
|
|
2318
|
+
if (keyInfo.key && !textKeys.some((textKey) => textKey.key === keyInfo.key)) {
|
|
2221
2319
|
const domPath = await getDomPath(element);
|
|
2222
2320
|
textKeys.push({
|
|
2223
2321
|
key: keyInfo.key,
|
|
@@ -2259,15 +2357,21 @@ async function detectFallbackContentSections(page, existingSections) {
|
|
|
2259
2357
|
}
|
|
2260
2358
|
async function capturePageSnapshot(browser, pageInfo, snapshotConfig, siteSlug, validTextKeys, outputDir, verbose) {
|
|
2261
2359
|
const context = await browser.newContext({
|
|
2262
|
-
viewport: snapshotConfig.viewport
|
|
2360
|
+
viewport: snapshotConfig.viewport,
|
|
2361
|
+
ignoreHTTPSErrors: true,
|
|
2362
|
+
reducedMotion: "reduce"
|
|
2263
2363
|
});
|
|
2264
2364
|
const page = await context.newPage();
|
|
2265
|
-
const
|
|
2365
|
+
const rawUrl = pageInfo.path.startsWith("http") ? pageInfo.path : `${process.env.SITE_BASE_URL || "http://localhost:5173"}${pageInfo.path}`;
|
|
2366
|
+
const pageUrl = new URL(rawUrl);
|
|
2367
|
+
pageUrl.searchParams.set("dcs-hide-ribbon", "");
|
|
2368
|
+
const url = pageUrl.toString();
|
|
2266
2369
|
if (verbose) {
|
|
2267
2370
|
console.log(` Capturing: ${pageInfo.slug} (${url})`);
|
|
2268
2371
|
}
|
|
2269
2372
|
try {
|
|
2270
2373
|
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
2374
|
+
await disableCaptureMotion(page);
|
|
2271
2375
|
await page.waitForLoadState("networkidle", { timeout: 1e4 }).catch(() => {
|
|
2272
2376
|
if (verbose) console.log(` Note: networkidle timeout, continuing anyway`);
|
|
2273
2377
|
});
|
|
@@ -2277,29 +2381,21 @@ async function capturePageSnapshot(browser, pageInfo, snapshotConfig, siteSlug,
|
|
|
2277
2381
|
throw error;
|
|
2278
2382
|
}
|
|
2279
2383
|
await page.waitForTimeout(snapshotConfig.waitAfterLoad);
|
|
2384
|
+
await waitForAboveFoldAssets(page);
|
|
2280
2385
|
const pageOutputDir = path4.join(outputDir, siteSlug, pageInfo.slug);
|
|
2281
2386
|
fs4.mkdirSync(path4.join(pageOutputDir, "sections"), { recursive: true });
|
|
2282
2387
|
const title = await page.title();
|
|
2283
|
-
let pageHeight = await page
|
|
2284
|
-
|
|
2285
|
-
for (let scrollY2 = 0; scrollY2 < pageHeight; scrollY2 += scrollStep) {
|
|
2286
|
-
await page.evaluate((y) => window.scrollTo(0, y), scrollY2);
|
|
2287
|
-
await page.waitForTimeout(100);
|
|
2288
|
-
}
|
|
2289
|
-
await page.evaluate(() => window.scrollTo(0, 0));
|
|
2290
|
-
await page.waitForTimeout(200);
|
|
2291
|
-
pageHeight = await page.evaluate(() => document.documentElement.scrollHeight);
|
|
2292
|
-
await page.evaluate(() => window.scrollTo(0, 0));
|
|
2293
|
-
await page.waitForTimeout(100);
|
|
2294
|
-
const scrollY = await page.evaluate(() => window.scrollY);
|
|
2295
|
-
if (scrollY !== 0) {
|
|
2296
|
-
console.warn(` Warning: Could not scroll to top (scrollY=${scrollY})`);
|
|
2297
|
-
}
|
|
2388
|
+
let pageHeight = await lazyLoadPage(page, snapshotConfig.viewport.height);
|
|
2389
|
+
await resetScrollForFullPageCapture(page);
|
|
2298
2390
|
const fullPagePath = path4.join(pageOutputDir, "full-page.png");
|
|
2299
2391
|
await page.screenshot({
|
|
2300
2392
|
path: fullPagePath,
|
|
2301
2393
|
fullPage: snapshotConfig.captureFullPage
|
|
2302
2394
|
});
|
|
2395
|
+
const thumbnailPath = path4.join(pageOutputDir, "thumbnail.png");
|
|
2396
|
+
await captureHeaderThumbnail(page, thumbnailPath, snapshotConfig.viewport);
|
|
2397
|
+
await page.evaluate(() => window.scrollTo(0, 0));
|
|
2398
|
+
await page.waitForTimeout(100);
|
|
2303
2399
|
const sections = [];
|
|
2304
2400
|
const seenDomPaths = /* @__PURE__ */ new Set();
|
|
2305
2401
|
const getAccurateBounds = async (el, sectionType) => {
|
|
@@ -2508,8 +2604,8 @@ async function capturePageSnapshot(browser, pageInfo, snapshotConfig, siteSlug,
|
|
|
2508
2604
|
sections.sort((a, b) => a.bounds.y - b.bounds.y);
|
|
2509
2605
|
return {
|
|
2510
2606
|
pageSlug: pageInfo.slug,
|
|
2511
|
-
|
|
2512
|
-
|
|
2607
|
+
path: pageInfo.path,
|
|
2608
|
+
type: pageInfo.config.type,
|
|
2513
2609
|
deletable: pageInfo.config.deletable,
|
|
2514
2610
|
title: title || pageInfo.config.title,
|
|
2515
2611
|
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2544,6 +2640,9 @@ async function captureSnapshotsCommand(options) {
|
|
|
2544
2640
|
console.log(chalk7.gray(`Content: Loaded ${pageCount} pages, ${globalCount} global keys`));
|
|
2545
2641
|
}
|
|
2546
2642
|
const targetedConfig = loadTargetedSnapshotsConfig(targetDir);
|
|
2643
|
+
const isTargetedCapture = Boolean(
|
|
2644
|
+
targetedPages && targetedPages.length > 0 || targetedConfig?.mode === "targeted" && targetedConfig.pages.length > 0
|
|
2645
|
+
);
|
|
2547
2646
|
if (targetedConfig?.mode === "skip") {
|
|
2548
2647
|
console.log("");
|
|
2549
2648
|
console.log(chalk7.yellow("\u23ED\uFE0F Skip mode activated!"));
|
|
@@ -2600,6 +2699,7 @@ async function captureSnapshotsCommand(options) {
|
|
|
2600
2699
|
const browser = await chromium.launch();
|
|
2601
2700
|
spinner.succeed("Browser launched");
|
|
2602
2701
|
const snapshots = [];
|
|
2702
|
+
const failures = [];
|
|
2603
2703
|
for (const pageInfo of pagesToCapture) {
|
|
2604
2704
|
const pageSpinner = ora3(`Capturing ${pageInfo.slug}...`).start();
|
|
2605
2705
|
try {
|
|
@@ -2618,34 +2718,45 @@ async function captureSnapshotsCommand(options) {
|
|
|
2618
2718
|
fs4.writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2));
|
|
2619
2719
|
pageSpinner.succeed(`${pageInfo.slug}: ${snapshot.sections.length} sections captured`);
|
|
2620
2720
|
} catch (error) {
|
|
2721
|
+
const message = `${pageInfo.slug}: ${error.message}`;
|
|
2722
|
+
failures.push(message);
|
|
2621
2723
|
pageSpinner.fail(`${pageInfo.slug}: Failed - ${error.message}`);
|
|
2622
2724
|
}
|
|
2623
2725
|
}
|
|
2624
2726
|
await browser.close();
|
|
2625
2727
|
const manifest = {
|
|
2626
2728
|
siteSlug: config2.siteSlug,
|
|
2627
|
-
|
|
2729
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2730
|
+
capturedVersion: process.env.GITHUB_SHA,
|
|
2628
2731
|
deploymentRef: process.env.GITHUB_REF || "local",
|
|
2629
2732
|
pagesConfigVersion: config2.version,
|
|
2630
2733
|
pages: snapshots.map((s) => ({
|
|
2631
2734
|
pageSlug: s.pageSlug,
|
|
2632
|
-
pagePath: s.
|
|
2633
|
-
pageType: s.
|
|
2735
|
+
pagePath: s.path,
|
|
2736
|
+
pageType: s.type,
|
|
2634
2737
|
deletable: s.deletable,
|
|
2635
2738
|
title: s.title,
|
|
2636
2739
|
capturedAt: s.capturedAt,
|
|
2637
|
-
sectionCount: s.sections.length
|
|
2740
|
+
sectionCount: s.sections.length,
|
|
2741
|
+
hasSnapshot: true
|
|
2638
2742
|
}))
|
|
2639
2743
|
};
|
|
2640
2744
|
const manifestPath = path4.join(outputDir, config2.siteSlug, "manifest.json");
|
|
2641
|
-
|
|
2745
|
+
if (isTargetedCapture) {
|
|
2746
|
+
console.log(chalk7.gray(" Targeted capture: leaving the existing remote manifest intact"));
|
|
2747
|
+
} else {
|
|
2748
|
+
fs4.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
2749
|
+
}
|
|
2642
2750
|
console.log("");
|
|
2643
2751
|
console.log(chalk7.green("\u2705 Snapshot capture complete!"));
|
|
2644
2752
|
console.log(chalk7.gray(` Captured ${snapshots.length} pages`));
|
|
2645
|
-
console.log(chalk7.gray(` - Static: ${snapshots.filter((s) => s.
|
|
2646
|
-
console.log(chalk7.gray(` - Index: ${snapshots.filter((s) => s.
|
|
2647
|
-
console.log(chalk7.gray(` - Dynamic: ${snapshots.filter((s) => s.
|
|
2753
|
+
console.log(chalk7.gray(` - Static: ${snapshots.filter((s) => s.type === "static").length}`));
|
|
2754
|
+
console.log(chalk7.gray(` - Index: ${snapshots.filter((s) => s.type === "index").length}`));
|
|
2755
|
+
console.log(chalk7.gray(` - Dynamic: ${snapshots.filter((s) => s.type === "dynamic").length}`));
|
|
2648
2756
|
console.log(chalk7.gray(` Output: ${outputDir}`));
|
|
2757
|
+
if (failures.length > 0) {
|
|
2758
|
+
throw new Error(`Snapshot capture failed for ${failures.length} page(s): ${failures.join("; ")}`);
|
|
2759
|
+
}
|
|
2649
2760
|
}
|
|
2650
2761
|
|
|
2651
2762
|
// src/index.ts
|