diff --git a/.github/actions/start-verdaccio/action.yml b/.github/actions/start-verdaccio/action.yml new file mode 100644 index 0000000000..d03f75c99c --- /dev/null +++ b/.github/actions/start-verdaccio/action.yml @@ -0,0 +1,35 @@ +name: Start Verdaccio +description: Start a Verdaccio Docker container and configure npm authentication + +inputs: + port: + description: Port to expose Verdaccio on + required: true + config: + description: Path to Verdaccio config file (relative to workspace) + required: true + storage: + description: Path to Verdaccio storage directory + required: true + +runs: + using: composite + steps: + - shell: bash + run: | + REGISTRY_URL="http://localhost:${{ inputs.port }}" + mkdir -p ${{ inputs.storage }} + chmod 777 ${{ inputs.storage }} + docker run -d --name verdaccio \ + -p ${{ inputs.port }}:${{ inputs.port }} \ + -v ${{ github.workspace }}/${{ inputs.config }}:/verdaccio/conf/config.yaml \ + -v ${{ inputs.storage }}:/tmp/verdaccio-storage \ + verdaccio/verdaccio + npx wait-on ${REGISTRY_URL}/-/ping --timeout 30000 + TOKEN=$(curl -s -X PUT ${REGISTRY_URL}/-/user/org.couchdb.user:ci \ + -H "Content-Type: application/json" \ + -d '{"name":"ci","password":"ci-password","type":"user"}' \ + | node -e "process.stdin.resume();process.stdin.on('data',d=>console.log(JSON.parse(d).token))") + + echo "@daffodil:registry=${REGISTRY_URL}" >> "$HOME/.npmrc" + echo "//localhost:${{ inputs.port }}/:_authToken=${TOKEN}" >> "$HOME/.npmrc" diff --git a/.github/workflows/build-commerce-schematic.yml b/.github/workflows/build-commerce-schematic.yml new file mode 100644 index 0000000000..12acc50f7c --- /dev/null +++ b/.github/workflows/build-commerce-schematic.yml @@ -0,0 +1,275 @@ +name: Daffodil Build Schematic + +on: + pull_request: + branches: + - develop + paths: + - 'tools/schematics/**' + +env: + WORK_DIR: /tmp/daffodil-commerce-test + VERDACCIO_STORAGE: /tmp/verdaccio-storage + TEST_VERSION: 0.0.0-test.${{ github.run_id }} + REGISTRY_PORT: 4873 + VERDACCIO_CONFIG: tools/schematics/e2e/verdaccio-config.yml + DAFFODIL_PACKAGES: >- + core + driver + product + dev-tools + navigation + external-router + +jobs: + setup: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - uses: graycoreio/github-actions/setup-node@main + with: + node-version: 22.21.x + use-stamp-cache: true + + - name: Start Verdaccio + uses: ./.github/actions/start-verdaccio + with: + port: ${{ env.REGISTRY_PORT }} + config: ${{ env.VERDACCIO_CONFIG }} + storage: ${{ env.VERDACCIO_STORAGE }} + + - name: Set test version + run: | + npm version "${{ env.TEST_VERSION }}" --no-git-tag-version + sed -i "s/version = '.*'/version = '${{ env.TEST_VERSION }}'/" \ + tools/schematics/ng-add/generators/version.ts + + - name: Build @daffodil/* packages + run: | + npx nx run-many -t build \ + --projects=@daffodil/core,@daffodil/driver,@daffodil/product,@daffodil/dev-tools,@daffodil/navigation,@daffodil/external-router + cd tools/schematics && npm run build + + - uses: graycoreio/github-actions/set-versions-from-root@main + + - name: Publish @daffodil/* packages to Verdaccio + run: | + for pkg in ${{ env.DAFFODIL_PACKAGES }}; do + echo "--- Publishing @daffodil/$pkg ---" + cd "${{ github.workspace }}/dist/$pkg" + npm publish --ignore-scripts + done + + echo "--- Publishing @daffodil/commerce ---" + cd "${{ github.workspace }}/dist/commerce" + npm publish --ignore-scripts + + - name: Scaffold base apps + run: bash tools/schematics/e2e/test-commerce-schematics.sh + + - name: Cache base apps and Verdaccio storage + uses: actions/cache/save@v4 + with: + path: | + ${{ env.WORK_DIR }} + ${{ env.VERDACCIO_STORAGE }} + key: commerce-test-${{ github.run_id }} + + test: + name: "${{ matrix.name }}" + needs: setup + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: demo-driver + driver: demo + base: scss + app_type: standalone + routing: true + skip_package_json: false + succeed: true + build: true + + - name: magento-driver + driver: magento + base: scss + app_type: standalone + routing: true + skip_package_json: false + succeed: true + build: true + + - name: shopify-driver + driver: shopify + base: scss + app_type: standalone + routing: true + skip_package_json: false + succeed: true + build: true + + - name: in-memory-driver + driver: in-memory + base: scss + app_type: standalone + routing: true + skip_package_json: false + succeed: true + build: true + + - name: skip-package-json + driver: in-memory + base: scss + app_type: standalone + routing: true + skip_package_json: true + succeed: true + build: false + + - name: module-app-rejection + driver: in-memory + base: scss + app_type: module + routing: true + skip_package_json: false + succeed: false + build: false + + - name: main-ts-fallback + driver: in-memory + base: scss + app_type: main-ts-fallback + routing: true + skip_package_json: false + succeed: true + build: true + + - name: css-style-failure + driver: in-memory + base: css + app_type: standalone + routing: true + skip_package_json: false + succeed: false + build: true + + - name: no-app-routes + driver: in-memory + base: scss + app_type: standalone + routing: false + skip_package_json: false + succeed: true + build: true + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - uses: actions/setup-node@v4 + with: + node-version: 22.21.x + + - name: Restore base apps and Verdaccio storage + uses: actions/cache/restore@v4 + with: + path: | + ${{ env.WORK_DIR }} + ${{ env.VERDACCIO_STORAGE }} + key: commerce-test-${{ github.run_id }} + + - name: Start Verdaccio + uses: ./.github/actions/start-verdaccio + with: + port: ${{ env.REGISTRY_PORT }} + config: ${{ env.VERDACCIO_CONFIG }} + storage: ${{ env.VERDACCIO_STORAGE }} + + - name: Copy base app + run: | + BASE_APP="${{ env.WORK_DIR }}/test-daff-app" + if [ "${{ matrix.base }}" = "css" ]; then + BASE_APP="${{ env.WORK_DIR }}/test-daff-app-css" + fi + TEST_DIR="${{ env.WORK_DIR }}/test-${{ matrix.name }}" + rm -rf "$TEST_DIR" + cp -a "$BASE_APP" "$TEST_DIR" + + - name: Apply app modifications + working-directory: ${{ env.WORK_DIR }}/test-${{ matrix.name }} + run: | + if [ "${{ matrix.app_type }}" = "module" ]; then + cat > "src/app/app-module.ts" <<'APPMOD' + import { NgModule } from '@angular/core'; + import { BrowserModule } from '@angular/platform-browser'; + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [AppComponent], + imports: [BrowserModule], + bootstrap: [AppComponent] + }) + export class AppModule {} + APPMOD + fi + + if [ "${{ matrix.app_type }}" = "main-ts-fallback" ]; then + cat > "src/main.ts" <<'MAIN' + import { bootstrapApplication } from '@angular/platform-browser'; + import { App } from './app/app'; + + bootstrapApplication(App, { + providers: [] + }); + MAIN + rm -f "src/app/app.config.ts" + fi + + if [ "${{ matrix.routing }}" = "false" ]; then + rm -f "src/app/app.routes.ts" + fi + + - name: Generate schematic + working-directory: ${{ env.WORK_DIR }}/test-${{ matrix.name }} + run: | + EXTRA_FLAGS="" + if [ "${{ matrix.skip_package_json }}" = "true" ]; then + EXTRA_FLAGS="--skip-package-json" + fi + + if [ "${{ matrix.succeed }}" = "true" ] || [ "${{ matrix.build }}" = "true" ]; then + CI=true npx ng generate @daffodil/commerce:ng-add \ + --driver="${{ matrix.driver }}" \ + --is-new-project \ + --defaults \ + $EXTRA_FLAGS + else + ! CI=true npx ng generate @daffodil/commerce:ng-add \ + --driver="${{ matrix.driver }}" \ + --is-new-project \ + --defaults \ + $EXTRA_FLAGS + fi + + - name: Raise budget + if: matrix.driver == 'demo' + working-directory: ${{ env.WORK_DIR }}/test-${{ matrix.name }} + run: | + sed -i 's/"maximumWarning": "[^"]*"/"maximumWarning": "2mb"/; s/"maximumError": "[^"]*"/"maximumError": "3mb"/' angular.json + + - name: Run ng build + if: matrix.build + working-directory: ${{ env.WORK_DIR }}/test-${{ matrix.name }} + run: | + if [ "${{ matrix.succeed }}" = "true" ]; then + npx ng build + else + ! npx ng build + fi \ No newline at end of file diff --git a/tools/schematics/e2e/test-commerce-schematics.sh b/tools/schematics/e2e/test-commerce-schematics.sh new file mode 100755 index 0000000000..6601888be9 --- /dev/null +++ b/tools/schematics/e2e/test-commerce-schematics.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================ +# Scaffold base Angular apps for @daffodil/commerce e2e tests. +# +# Called by the GitHub Actions setup job. Creates 2 base apps +# (scss + css) with @daffodil/commerce installed from Verdaccio. +# +# Prerequisites: +# - Verdaccio running on localhost:4873 +# - npm config set @daffodil:registry http://localhost:4873 +# - @daffodil/commerce published to Verdaccio +# +# Output: +# $WORK_DIR/test-daff-app (scss base app) +# $WORK_DIR/test-daff-app-css (css base app) +# ============================================================ + +WORK_DIR="${WORK_DIR:-/tmp/daffodil-commerce-test}" +APP_NAME="test-daff-app" + +rm -rf "$WORK_DIR" +mkdir -p "$WORK_DIR" + +# --- SCSS Base App --- +echo "=== Creating scss base app ===" +cd "$WORK_DIR" +npx -y @angular/cli@20 new "$APP_NAME" \ + --style=scss \ + --skip-tests \ + --defaults + +echo "--- Installing @daffodil/commerce into scss base app ---" +cd "$WORK_DIR/$APP_NAME" +npm install @daffodil/commerce + +# --- CSS Base App --- +echo "" +echo "=== Creating css base app ===" +cd "$WORK_DIR" +npx -y @angular/cli@20 new "${APP_NAME}-css" \ + --style=css \ + --skip-tests \ + --defaults + +echo "--- Installing @daffodil/commerce into css base app ---" +cd "$WORK_DIR/${APP_NAME}-css" +npm install @daffodil/commerce + +echo "" +echo "=== Base apps scaffolded in $WORK_DIR ===" diff --git a/tools/schematics/e2e/verdaccio-config.yml b/tools/schematics/e2e/verdaccio-config.yml new file mode 100644 index 0000000000..cb2f61ac06 --- /dev/null +++ b/tools/schematics/e2e/verdaccio-config.yml @@ -0,0 +1,21 @@ +storage: /tmp/verdaccio-storage +auth: + htpasswd: + file: /tmp/verdaccio-htpasswd + max_users: 100 +uplinks: + npmjs: + url: https://registry.npmjs.org/ +packages: + '@daffodil/*': + access: $all + publish: $all + '**': + access: $all + proxy: npmjs +server: + keepAliveTimeout: 60 +log: + type: stdout + format: pretty + level: warn diff --git a/tools/schematics/utils/module-utils.ts b/tools/schematics/utils/module-utils.ts index c407265776..ef121e3836 100644 --- a/tools/schematics/utils/module-utils.ts +++ b/tools/schematics/utils/module-utils.ts @@ -8,12 +8,14 @@ export function addProvidersToStandaloneApp(tree: Tree, project: any, providers: const mainPath = `${project.sourceRoot}/main.ts`; let configPath = ''; + let localImportPrefix = '.'; // Determine which file to modify if (tree.exists(appConfigPath)) { configPath = appConfigPath; } else if (tree.exists(mainPath)) { configPath = mainPath; + localImportPrefix = './app'; } else { throw new Error('No app.config.ts or main.ts found for standalone app'); } @@ -45,7 +47,7 @@ export function addProvidersToStandaloneApp(tree: Tree, project: any, providers: // Add DynamicSwitchDriverService import when using provideDaffProductDriver if (provider.includes('provideDaffProductDriver')) { - const driverChange = insertImport(source, configPath, 'DynamicSwitchDriverService', './daff/product/drivers/dynamic/dynamic-switch.service'); + const driverChange = insertImport(source, configPath, 'DynamicSwitchDriverService', `${localImportPrefix}/daff/product/drivers/dynamic/dynamic-switch.service`); if (driverChange instanceof InsertChange) { importChanges.push(driverChange); } @@ -53,7 +55,7 @@ export function addProvidersToStandaloneApp(tree: Tree, project: any, providers: // Add DynamicSwitchNavigationService import when using provideDaffNavigationDriver if (provider.includes('provideDaffNavigationDriver')) { - const navDriverChange = insertImport(source, configPath, 'DynamicSwitchNavigationService', './daff/navigation/drivers/dynamic/dynamic-switch.service'); + const navDriverChange = insertImport(source, configPath, 'DynamicSwitchNavigationService', `${localImportPrefix}/daff/navigation/drivers/dynamic/dynamic-switch.service`); if (navDriverChange instanceof InsertChange) { importChanges.push(navDriverChange); } @@ -61,7 +63,7 @@ export function addProvidersToStandaloneApp(tree: Tree, project: any, providers: // Add DynamicExternalRouterDriver import when using provideDaffExternalRouterDriver if (provider.includes('provideDaffExternalRouterDriver')) { - const routerDriverChange = insertImport(source, configPath, 'DynamicExternalRouterDriver', './daff/external-router/drivers/dynamic'); + const routerDriverChange = insertImport(source, configPath, 'DynamicExternalRouterDriver', `${localImportPrefix}/daff/external-router/drivers/dynamic`); if (routerDriverChange instanceof InsertChange) { importChanges.push(routerDriverChange); } @@ -69,7 +71,7 @@ export function addProvidersToStandaloneApp(tree: Tree, project: any, providers: // Add DEMO_MAGENTO_ENDPOINT_SWITCH import when using provideMagentoDriver if (provider.includes('DEMO_MAGENTO_ENDPOINT_SWITCH')) { - const endpointChange = insertImport(source, configPath, 'DEMO_MAGENTO_ENDPOINT_SWITCH', './daff/driver/magento/endpoint-switch.token'); + const endpointChange = insertImport(source, configPath, 'DEMO_MAGENTO_ENDPOINT_SWITCH', `${localImportPrefix}/daff/driver/magento/endpoint-switch.token`); if (endpointChange instanceof InsertChange) { importChanges.push(endpointChange); } @@ -77,7 +79,7 @@ export function addProvidersToStandaloneApp(tree: Tree, project: any, providers: // Add DEMO_SHOPIFY_ENDPOINT_SWITCH import when using provideShopifyDriver if (provider.includes('DEMO_SHOPIFY_ENDPOINT_SWITCH')) { - const shopifyEndpointChange = insertImport(source, configPath, 'DEMO_SHOPIFY_ENDPOINT_SWITCH', './daff/driver/shopify/endpoint-switch'); + const shopifyEndpointChange = insertImport(source, configPath, 'DEMO_SHOPIFY_ENDPOINT_SWITCH', `${localImportPrefix}/daff/driver/shopify/endpoint-switch`); if (shopifyEndpointChange instanceof InsertChange) { importChanges.push(shopifyEndpointChange); } @@ -93,10 +95,17 @@ export function addProvidersToStandaloneApp(tree: Tree, project: any, providers: }); // Apply import changes - let updatedContent = sourceText; - importChanges.reverse().forEach(change => { - updatedContent = updatedContent.slice(0, change.pos) + change.toAdd + updatedContent.slice(change.pos); + const recorder = tree.beginUpdate(configPath); + importChanges.forEach(change => { + recorder.insertLeft(change.pos, change.toAdd); }); + tree.commitUpdate(recorder); + + const updatedSource = tree.read(configPath); + if (!updatedSource) { + throw new Error(`Config file ${configPath} not found after update`); + } + let updatedContent = updatedSource.toString(); // Add providers to the providers array const providersToAdd = providers @@ -220,6 +229,7 @@ function getPackageForProvider(providerName: string): string { DaffNavigationShopifyDriver: '@daffodil/navigation/driver/shopify', provideDaffNavigationDriver: '@daffodil/navigation/driver', provideExternalRouter: '@daffodil/external-router', + provideDaffExternalRouterInMemoryDriver: '@daffodil/external-router/driver/in-memory', provideDaffExternalRouterMagentoDriver: '@daffodil/external-router/driver/magento/2.4.3', DaffExternalRouterMagentoDriver: '@daffodil/external-router/driver/magento/2.4.3', provideDaffExternalRouterShopifyDriver: '@daffodil/external-router/driver/shopify',