diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index bf0b8c9c177..5bd0390c62b 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -70,7 +70,7 @@ runs: node-version-file: 'package.json' cache: 'pnpm' - - uses: oven-sh/setup-bun@v1 + - uses: oven-sh/setup-bun@v2 with: bun-version: latest @@ -101,6 +101,11 @@ runs: sudo apt install libnss3-tools brew install mkcert + - name: 'Setup DBus for Chrome' + shell: bash + run: | + echo "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus" >> $GITHUB_ENV + - name: Configure Parallel Builds if: ${{ inputs.parallel-build == 'true' }} shell: bash diff --git a/.github/workflows/compat-tests.yml b/.github/workflows/compat-tests.yml index 19620590cd8..3ace12ed41f 100644 --- a/.github/workflows/compat-tests.yml +++ b/.github/workflows/compat-tests.yml @@ -54,18 +54,24 @@ jobs: repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Run Tests run: pnpm test:vite - floating-dependencies: - timeout-minutes: 9 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: ./.github/actions/setup - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install dependencies w/o lockfile - run: pnpm install --no-lockfile - - name: Basic Tests - run: pnpm test + ### + # This Test No Longer Works Because pnpm install --no-lockfile + # returns exit code 1 whenever there is a lockfile present and + # changes are made to node_modules. This is probably a bug in pnpm. + ### + # + # floating-dependencies: + # timeout-minutes: 9 + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + # - uses: ./.github/actions/setup + # with: + # repo-token: ${{ secrets.GITHUB_TOKEN }} + # - name: Install dependencies w/o lockfile + # run: pnpm install --no-lockfile + # - name: Basic Tests + # run: pnpm test node-version-test: name: Use Node.js ${{ matrix.node-version }} timeout-minutes: 10 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8a66fd1cdca..41e1233d404 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -174,7 +174,11 @@ jobs: timeout-minutes: 12 env: CI: true - run: pnpm test:try-one ${{ matrix.scenario }} -- ember test --test-port=0 + run: | + cd tests/main; + pnpm exec ember try:one ${{ matrix.scenario }} --skip-cleanup; + pnpm build:tests; + pnpm run test; releases: timeout-minutes: 12 @@ -201,4 +205,8 @@ jobs: - name: Basic tests with ${{ matrix.release }} env: CI: true - run: pnpm test:try-one ${{ matrix.release }} -- ember test --test-port=0 + run: | + cd tests/main; + pnpm exec ember try:one ${{ matrix.release }} --skip-cleanup; + pnpm build:tests; + pnpm run test; diff --git a/.github/workflows/perf-check.yml b/.github/workflows/perf-check.yml index 72d7cd3dd25..b0b18b30c8b 100644 --- a/.github/workflows/perf-check.yml +++ b/.github/workflows/perf-check.yml @@ -53,12 +53,14 @@ jobs: run: | BROWSER_FLAGS=$(node ./scripts/perf-tracking/browser-flags.mjs) echo "BROWSER_FLAGS=$BROWSER_FLAGS" >> $GITHUB_OUTPUT - - uses: tracerbench/tracerbench-compare-action@35f3ab44b512fd2caffbe81adf875ab47272b5b5 + #env: + # DEBUG: true + - uses: tracerbench/tracerbench-compare-action@6b56fb774f78e4a85cf02396412b0164870cdab3 with: - experiment-build-command: pnpm install && pnpm --filter performance-test-app exec ember build -e production --output-path dist-experiment --suppress-sizes - experiment-serve-command: pnpm --filter performance-test-app exec ember s --path dist-experiment --port 4201 - control-build-command: pnpm install && pnpm --filter performance-test-app exec ember build -e production --output-path dist-control --suppress-sizes - control-serve-command: pnpm --filter performance-test-app exec ember s --path dist-control + experiment-build-command: pnpm --filter performance-test-app build --outDir dist-experiment + experiment-serve-command: pnpm --filter performance-test-app start dist-experiment -p 4201 + control-build-command: pnpm --filter performance-test-app build --outDir dist-control + control-serve-command: pnpm --filter performance-test-app start dist-control -p 4200 control-sha: origin/main sample-timeout: 60 use-pnpm: true @@ -70,6 +72,16 @@ jobs: "experiment": "http://localhost:4201/basic-record-materialization", "markers": "start-data-generation,start-push-payload,start-peek-records,start-record-materialization,end-record-materialization" }, + "complex-record-materialization": { + "control": "http://localhost:4200/complex-record-materialization", + "experiment": "http://localhost:4201/complex-record-materialization", + "markers": "start-data-generation,start-push-payload,start-peek-records,start-record-materialization,end-record-materialization" + }, + "complex-record-materialization-with-relationship-materialization": { + "control": "http://localhost:4200/complex-record-materialization-with-relationship-materialization", + "experiment": "http://localhost:4201/complex-record-materialization-with-relationship-materialization", + "markers": "start-data-generation,start-push-payload,start-peek-records,start-record-materialization,start-field-access,start-relationship-access,end-relationship-access" + }, "relationship-materialization-simple": { "control": "http://localhost:4200/relationship-materialization-simple", "experiment": "http://localhost:4201/relationship-materialization-simple", @@ -78,7 +90,7 @@ jobs: "relationship-materialization-complex": { "control": "http://localhost:4200/relationship-materialization-complex", "experiment": "http://localhost:4201/relationship-materialization-complex", - "markers": "start-data-generation,start-push-payload,start-peek-records,start-record-materialization,start-relationship-materialization,end-relationship-materialization" + "markers": "start-data-generation,start-push-payload,start-peek-records,start-record-materialization,start-relationship-materialization,start-push-payload2,start-relationship-materialization2,end-relationship-materialization2" }, "unload": { "control": "http://localhost:4200/unload", @@ -104,11 +116,30 @@ jobs: "control": "http://localhost:4200/unused-relationships", "experiment": "http://localhost:4201/unused-relationships", "markers": "start-push-payload,end-push-payload" + }, + "update-with-same-state": { + "control": "http://localhost:4200/update-with-same-state", + "experiment": "http://localhost:4201/update-with-same-state", + "markers": "start-data-generation,start-push-initial-payload,start-peek-records,start-record-materialization,start-relationship-materialization,start-local-removal,start-push-minus-one-payload,start-local-addition,start-push-plus-one-payload,end-push-plus-one-payload" + }, + "update-with-same-state-m2m": { + "control": "http://localhost:4200/update-with-same-state-m2m", + "experiment": "http://localhost:4201/update-with-same-state-m2m", + "markers": "start-data-generation,start-push-initial-payload,start-peek-records,start-record-materialization,start-relationship-materialization,start-local-removal,start-push-minus-one-payload,start-local-addition,start-push-plus-one-payload,end-push-plus-one-payload" } } fidelity: 60 upload-traces: true upload-results: true + # env: + # DEBUG: '*,-babel*,-vite*,-rollup*,-ember*,-broccoli*,-pnpm*,-embroider*,-tree-sync*,-fs-tree-diff*' + # - name: Upload Assets + # if: failure() || success() + # uses: actions/upload-artifact@v4 + # with: + # name: built-files + # path: 'tests/performance/dist-*' + # retention-days: 1 - name: Report TracerBench Results if: failure() || success() env: diff --git a/.github/workflows/perf-over-release.yml b/.github/workflows/perf-over-release.yml index 10883e21622..2704e180ec7 100644 --- a/.github/workflows/perf-over-release.yml +++ b/.github/workflows/perf-over-release.yml @@ -45,7 +45,7 @@ jobs: registry-url: 'https://registry.npmjs.org' node-version-file: 'package.json' cache: 'pnpm' - - uses: oven-sh/setup-bun@v1 + - uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Get Browser Flags @@ -53,12 +53,14 @@ jobs: run: | BROWSER_FLAGS=$(node ./scripts/perf-tracking/browser-flags.mjs) echo "BROWSER_FLAGS=$BROWSER_FLAGS" >> $GITHUB_OUTPUT - - uses: tracerbench/tracerbench-compare-action@35f3ab44b512fd2caffbe81adf875ab47272b5b5 + # env: + # DEBUG: true + - uses: tracerbench/tracerbench-compare-action@6b56fb774f78e4a85cf02396412b0164870cdab3 with: - experiment-build-command: pnpm install && pnpm --filter performance-test-app exec ember build -e production --output-path dist-experiment --suppress-sizes - experiment-serve-command: pnpm --filter performance-test-app exec ember s --path dist-experiment --port 4201 - control-build-command: pnpm install && pnpm --filter performance-test-app exec ember build -e production --output-path dist-control --suppress-sizes - control-serve-command: pnpm --filter performance-test-app exec ember s --path dist-control + experiment-build-command: pnpm --filter performance-test-app build --outDir dist-experiment + experiment-serve-command: pnpm --filter performance-test-app start dist-experiment -p 4201 + control-build-command: pnpm --filter performance-test-app build --outDir dist-control + control-serve-command: pnpm --filter performance-test-app start dist-control -p 4200 sample-timeout: 60 use-pnpm: true browser-args: ${{ steps.browser-flags.outputs.BROWSER_FLAGS }} @@ -69,6 +71,16 @@ jobs: "experiment": "http://localhost:4201/basic-record-materialization", "markers": "start-data-generation,start-push-payload,start-peek-records,start-record-materialization,end-record-materialization" }, + "complex-record-materialization": { + "control": "http://localhost:4200/complex-record-materialization", + "experiment": "http://localhost:4201/complex-record-materialization", + "markers": "start-data-generation,start-push-payload,start-peek-records,start-record-materialization,end-record-materialization" + }, + "complex-record-materialization-with-relationship-materialization": { + "control": "http://localhost:4200/complex-record-materialization-with-relationship-materialization", + "experiment": "http://localhost:4201/complex-record-materialization-with-relationship-materialization", + "markers": "start-data-generation,start-push-payload,start-peek-records,start-record-materialization,start-field-access,start-relationship-access,end-relationship-access" + }, "relationship-materialization-simple": { "control": "http://localhost:4200/relationship-materialization-simple", "experiment": "http://localhost:4201/relationship-materialization-simple", @@ -77,7 +89,7 @@ jobs: "relationship-materialization-complex": { "control": "http://localhost:4200/relationship-materialization-complex", "experiment": "http://localhost:4201/relationship-materialization-complex", - "markers": "start-data-generation,start-push-payload,start-peek-records,start-record-materialization,start-relationship-materialization,end-relationship-materialization" + "markers": "start-data-generation,start-push-payload,start-peek-records,start-record-materialization,start-relationship-materialization,start-push-payload2,start-relationship-materialization2,end-relationship-materialization2" }, "unload": { "control": "http://localhost:4200/unload", @@ -104,11 +116,25 @@ jobs: "experiment": "http://localhost:4201/unused-relationships", "markers": "start-push-payload,end-push-payload" } + "update-with-same-state-m2m": { + "control": "http://localhost:4200/update-with-same-state-m2m", + "experiment": "http://localhost:4201/update-with-same-state-m2m", + "markers": "start-data-generation,start-push-initial-payload,start-peek-records,start-record-materialization,start-relationship-materialization,start-local-removal,start-push-minus-one-payload,start-local-addition,start-push-plus-one-payload,end-push-plus-one-payload" + } } fidelity: 60 control-sha: origin/release upload-traces: true upload-results: true + # env: + # DEBUG: '*,-babel*,-vite*,-rollup*,-ember*,-broccoli*,-pnpm*,-embroider*,-tree-sync*,-fs-tree-diff*' + # - name: Upload Assets + # if: failure() || success() + # uses: actions/upload-artifact@v4 + # with: + # name: built-files + # path: 'tests/performance/dist-*' + # retention-days: 1 - name: Report TracerBench Results if: failure() || success() env: diff --git a/.npmrc b/.npmrc index 32d24cf300f..801200cc7af 100644 --- a/.npmrc +++ b/.npmrc @@ -1,25 +1,156 @@ -# package-import-method=hardlink -# module-exists will report false answers for the test apps -# unless we avoid hoisting -# while this is "true" this actually sets hoisting to "false" -# because we have a hoist-pattern. This basically just lets us -# use the very narrowly scoped hoist-pattern. -hoist=true -# Fastboot Doesnt respect node_modules resolution for whitelisted deps -# https://github.com/ember-fastboot/ember-cli-fastboot/issues/901 -hoist-pattern[]=*node-fetch* - -# we want true but cannot use true until the below issue is fixed -# https://github.com/pnpm/pnpm/issues/5340 -strict-peer-dependencies=true -auto-install-peers=false # probably apps should set this to true, but we need to test with it false to be sure we aren't the bad citizen -dedupe-peer-dependents=true # this currently introduces more bugs than it fixes -resolve-peers-from-workspace-root=false # if its not declared we don't want it resolved: ensure tests are truly isolated +## Woo! A Config File! +## +## This file is used to configure the behavior of pnpm. +## If adjusting settings, please document the "why" in comments. +## +## It may also be good to understand that we intentionally are +## not using `hoisting` and using `injected` workspace packages +## to ensure properly isolated dep trees for test apps. +# +## things like `moduleExists` from @embroider/macros will report false answers +## for the test apps unless we avoid hoisting. +## +## Note, if we ever need to hoist something, we can use hoist-pattern[]="" +## For instance: hoist-pattern[]=*node-fetch* +## to hoist the specific thing we need and set this to `true`. When true +## and a hoist-pattern is present only the hoist-pattern will be hoisted. +# +hoist=false + +## We should consider removing these +## But historically this was the default for pnpm +## and there is cleanup to do to make things work +## without these +## Its also just useful to have these tools top-level +# +public-hoist-pattern[]=*eslint* +public-hoist-pattern[]=*prettier* + +## Ideally this would be dynamic as in CI we only have 4 CPUs +## While locally we have 10-16 CPUs. The more CPUs we use during +## install, generally the faster the install will be. +## +child-concurrency=10 + +## This is the default but its good to be explicit +## This helps us to ensure we are using the workspace +## version of a package. +## save-workspace-protocol=rolling + +## The current default is "highest" but we want to be explicit +## Since we also just want to ensure we are always using the +## latest versions of things we can. +## resolution-mode=highest -dedupe-direct-deps=true -child-concurrency=10 + +## This is documented in a slightly different place in the pnpm +## docs. If this setting is true (the default) then pnpm will +## run the pre* and post* versions of a script when running other +## scripts. For instance running "build" would also run "prebuild" +## and "postbuild". This is not the behavior we want. +## +enable-pre-post-scripts=false + +## We use volta to manage our node and pnpm versions, so we do not +## want pnpm to manage these for us. +## +manage-package-manager-versions=false + +## This is now the default but we want to be explicit +## This prevents security exploits from packages running +## arbitrary code during install. It also speeds up install times. +## Our own packages will still run their installation scripts +## ignore-dep-scripts=true -dedupe-injected-deps=false + +## Make sure we don't troll ourselves when updating deps +## +verify-deps-before-run=true + +## We do not want to auto-install peers because +## We want to ensure we understand what is actually required +## So that if a consuming app uses strict mode things will work +## +## This said, Apps should probably set this to true +## +auto-install-peers=false + +## We want to error if we did not setup required peers correctly +## However, this pnpm bug prevents us using this as it will +## error when using `--no-lockfile` without reporting any errors +## https://github.com/pnpm/pnpm/issues/8382 +## +strict-peer-dependencies=false + +## We use so many similarly grouped peers, we want to make the +## peer-groups easier to distinguish. +## This forces a shorter sha for all groups (vs the default of 1000) +## +peers-suffix-max-length=40 +virtual-store-dir-max-length=40 + +## If a dependency is not declared, we do not want to accidentally +## resolve it from the workspace root. This is a common source of +## bugs in monorepos. +## +resolve-peers-from-workspace-root=false + +## Our Workspace Packages are "injected" so prevent +## devDependencies from being exposed and to allow +## for us to test optional peerDependencies. +## +inject-workspace-packages=true + +## This also means we do not want to hoist them to the root +## As this would both expose them to all other packages AND +## results in them using symlinks instead of hardlinks +## hoist-workspace-packages=false -enable-pre-post-scripts=false + +## We use the `workspace:*` protocol for all workspace +## packages. +## In theory it would be nice to use `deep` here just in case +## we missed something so that we could tell pnpm to use the +## workspace version of a package if it exists. At any depth. +## +## However, it seems that deep/true result in workspace packages +## that are dependencies being symlinked instead of hardlinked +## more often, even at the top-level of a package, which is not +## what we want. +## +link-workspace-packages=false # deep + +## Update injected dependencies when needed +## This will rerun after various "build" scripts +## In our published packages. +## +## Unfortunately, this does not run after scripts in +## the monorepo root, so we have added a special "sync" +## script to handle this. +## +sync-injected-deps-after-scripts[]=build:pkg +sync-injected-deps-after-scripts[]=build:infra +sync-injected-deps-after-scripts[]=build:glint +sync-injected-deps-after-scripts[]=sync + +## In keeping with our "no hoisting" and "no auto-peers" and +## "isolated dep trees", we also want to avoid other things +## that lead to reliance on hoisting. +## In general, deduping leads to hoisting. This particular +## setting causes direct-dependencies to resolve from the +## workspace root if already in root. We don't want this. +## +dedupe-direct-deps=false + +## We do not want to dedupe peer dependencies as this +## results in hoisting and violates optional peer isolation. +## +dedupe-peer-dependents=false + +## We do not want to dedupe injected dependencies as this +## results in hoisting and violates optional peer isolation. +## +dedupe-injected-deps=false + +## Fin diff --git a/config/package.json b/config/package.json index f7e19081b3b..2e9910e059f 100644 --- a/config/package.json +++ b/config/package.json @@ -4,35 +4,35 @@ "version": "4.13.0-alpha.4", "type": "module", "dependencies": { - "@babel/cli": "^7.24.5", - "@babel/core": "^7.24.5", - "@babel/eslint-parser": "7.25.8", + "@babel/cli": "^7.26.4", + "@babel/core": "^7.26.9", + "@babel/eslint-parser": "7.26.8", "@rollup/plugin-babel": "^6.0.4", - "@typescript-eslint/eslint-plugin": "^8.10.0", - "@typescript-eslint/parser": "^8.10.0", - "typescript-eslint": "^8.10.0", + "@typescript-eslint/eslint-plugin": "^8.25.0", + "@typescript-eslint/parser": "^8.25.0", + "typescript-eslint": "^8.25.0", "@embroider/addon-dev": "^7.1.1", - "@eslint/js": "^9.13.0", - "globals": "^15.11.0", + "@eslint/js": "^9.21.0", + "globals": "^16.0.0", "glob": "^11.0.1", - "ember-eslint-parser": "^0.5.2", - "eslint": "^9.12.0", + "ember-eslint-parser": "^0.5.9", + "eslint": "^9.21.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-mocha": "^10.5.0", - "eslint-plugin-n": "^17.11.0", + "eslint-plugin-n": "^17.15.1", "eslint-plugin-qunit": "^8.1.2", "eslint-plugin-simple-import-sort": "^12.1.1", - "rollup": "^4.17.2", - "typescript": "^5.7.2", - "vite": "^5.2.11", + "rollup": "^4.34.9", + "typescript": "^5.8.2", + "vite": "^5.4.14", "vite-plugin-dts": "^3.9.1" }, "engines": { - "node": ">= 18.20.4" + "node": ">= 18.20.7" }, "volta": { "extends": "../package.json" }, - "packageManager": "pnpm@8.15.9" + "packageManager": "pnpm@10.5.2" } diff --git a/guides/manual/1-overview.md b/guides/manual/1-overview.md index 6757e2c387b..c43afbe3a31 100644 --- a/guides/manual/1-overview.md +++ b/guides/manual/1-overview.md @@ -18,9 +18,12 @@ We see this as one of the keys to scalability. Providing a stable framework for how data is requested, cached, mutated, and mocked allows developers to focus more time on the product requirements that matter. -This also means that a single WarpDrive configuration can power multiple web-apps -using varying frameworks sharing a single domain: bridging the gap between MPA -and SPA. +A single WarpDrive configuration can power multiple web-apps using differing +frameworks all sharing a single domain: bridging the gap between MPA and SPA. + +
+ +## A Very Brief History WarpDrive began in ~2006 as a suite of ORM-like data utilities in SproutCore that later evolved into EmberData. Beginning in ~2017 the team plotted a course towards @@ -51,14 +54,15 @@ quick to pickup the basics. Our patterns are portable and scalable, meaning that your app, team and data needs evolve we'll be right there with you. Because we are universal and also not tied to any API Format or backend architecture, -there's no lock-in. The data patterns you learn, the code you write is portable -between frontend frameworks and backends and can help smooth the evolution of both. +there's no lock-in. The data patterns you learn and the code you write is portable +between frontend frameworks and backend APIs and can help smooth the evolution of both. We're also not specific to a given frontend architecture. When serving on the same domain, you can dedupe and cache requests across multiple apps and tabs at once! This means we are as good for embedded content and MPAs as we are for SPAs. -Our core philosophy is to deliver value that lasts decades and evolves with your app. +Our core philosophy is to deliver value that lasts decades and evolves with your app, +helping you ship, iterate and deliver to your customers.
diff --git a/guides/manual/3-data.md b/guides/manual/3-data.md index fa250f130de..27c95f0c5fb 100644 --- a/guides/manual/3-data.md +++ b/guides/manual/3-data.md @@ -18,7 +18,33 @@ at each layer, making your application faster. Misalignment tends to occur when API and Application developers don't work together to understand requirements, or when the format in use is "lossy" (unable to accurately convey the full scope of -information being serialized) +information being serialized). + +We encourage the use of [JSON:API](https://jsonapi.org/) as the wire and cache format because unlike +most other formats in use today it encodes information about your data in a near-lossless and easily-cacheable manner. + +For the presentation format, we encourage applications to limit the amount of manual transformation +done. Applications should wherever possible align the interfaces of the data components expect to +the shape of the data available, rather than transforming data to fit into the component. This said, +WarpDrive offers powerful schema-defined transformation and derivation capabilities built-in to the +reactivity layer for presenting data from the cache. Handling transformation universally via schema +enables apps to align to component interfaces where needed in a safer, more performant manner. + +We'll explore these capabilities later on in the manual in the sections on [Presentation](./5-presentation.md) and [Schemas](./6-schemas.md). But first, lets take some time to look at some key +concepts surrounding the wire and cache format. + + +### StructuredDocuments + +### ResourceDocuments + +### Resources + +### CacheKeys + +### Membership + +### Fields
diff --git a/package.json b/package.json index 9d5741f52fa..91355a889a4 100644 --- a/package.json +++ b/package.json @@ -9,24 +9,23 @@ }, "scripts": { "takeoff": "FORCE_COLOR=2 pnpm install --prefer-offline --reporter=append-only", - "prepare": "turbo run build:infra; pnpm --filter './packages/*' run --parallel --if-present sync-hardlinks; turbo run build:pkg; pnpm run prepare:types; pnpm run _task:sync-hardlinks;", - "prepare:types": "tsc --build --force; turbo run build:glint;", + "prepare": "export TURBO_FORCE=true; turbo run build:pkg; pnpm run prepare:types;", + "prepare:types": "export TURBO_FORCE=true; tsc --build --force; turbo run build:glint;", "release": "./release/index.ts", - "build": "turbo _build --log-order=stream --filter=./packages/* --concurrency=10;", - "_task:sync-hardlinks": "pnpm run -r --parallel --if-present sync-hardlinks;", + "build": "turbo run build:pkg --log-order=stream --concurrency=10;", + "sync": "pnpm --filter './packages/*' run --parallel --if-present sync", "build:docs": "mkdir -p packages/-ember-data/dist && cd ./docs-generator && node ./compile-docs.js", "lint:tests": "turbo --log-order=stream lint --filter=./tests/* --continue --concurrency=10", "lint:pkg": "turbo --log-order=stream lint --filter=./packages/* --continue --concurrency=10", - "lint": "pnpm run _task:sync-hardlinks; turbo --log-order=stream lint --continue --concurrency=10", - "lint:fix": "pnpm run _task:sync-hardlinks; turbo --log-order=stream lint --continue --concurrency=10 -- --fix", + "lint": "turbo --log-order=stream lint --continue --concurrency=10", + "lint:fix": "turbo --log-order=stream lint --continue --concurrency=10 -- --fix", "lint:prettier": "prettier --check --cache --cache-location=.prettier-cache --log-level=warn .", "lint:prettier:fix": "prettier --write --cache --cache-location=.prettier-cache --log-level=warn .", "preinstall": "npx only-allow pnpm", "check:test-types": "turbo --log-order=stream check:types --filter=./{tests,config}/* --continue --concurrency=10", - "check:types": "pnpm run _task:sync-hardlinks; bun run check:test-types", - "test": "pnpm run _task:sync-hardlinks; pnpm turbo test --concurrency=1", - "test:production": "pnpm run _task:sync-hardlinks; pnpm turbo test:production --concurrency=1", - "test:try-one": "pnpm --filter main-test-app run test:try-one", + "check:types": "bun run check:test-types", + "test": "pnpm turbo test --concurrency=1", + "test:production": "pnpm turbo test:production --concurrency=1", "test:docs": "FORCE_COLOR=2 pnpm build:docs && pnpm run -r --workspace-concurrency=-1 --if-present --reporter=append-only --reporter-hide-prefix test:docs", "test:blueprints": "pnpm run -r --workspace-concurrency=-1 --if-present test:blueprints", "test:fastboot": "pnpm run -r --workspace-concurrency=-1 --if-present test:fastboot", @@ -34,32 +33,32 @@ "test:vite": "pnpm run -r ---workspace-concurrency=-1 --if-present test:vite" }, "devDependencies": { - "@babel/core": "^7.24.5", + "@babel/core": "^7.26.9", "@glimmer/component": "^1.1.2", - "@glint/core": "1.5.0", - "@glint/environment-ember-loose": "1.5.0", - "@glint/environment-ember-template-imports": "1.5.0", - "@glint/template": "1.5.0", + "@glint/core": "1.5.2", + "@glint/environment-ember-loose": "1.5.2", + "@glint/environment-ember-template-imports": "1.5.2", + "@glint/template": "1.5.2", "@types/semver": "^7.5.8", "badge-maker": "4.1.0", - "bun-types": "^1.2.2", + "bun-types": "^1.2.4", "chalk": "^4.1.2", "co": "^4.6.0", - "command-line-args": "^5.2.1", + "command-line-args": "^6.0.1", "comment-json": "^4.2.5", "common-tags": "^1.8.2", - "debug": "^4.3.7", + "debug": "^4.4.0", "ember-source": "~5.12.0", "execa": "^9.4.1", "git-repo-version": "^1.0.2", - "globby": "^14.0.2", + "globby": "^14.1.0", "lerna-changelog": "^2.2.0", - "prettier": "^3.3.2", - "prettier-plugin-ember-template-tag": "^2.0.2", - "rimraf": "^5.0.10", - "semver": "^7.6.3", + "prettier": "^3.5.2", + "prettier-plugin-ember-template-tag": "^2.0.4", + "rimraf": "^6.0.1", + "semver": "^7.7.1", "silent-error": "^1.1.1", - "typescript": "^5.7.2", + "typescript": "^5.8.2", "url": "^0.11.4", "yuidocjs": "^0.10.2", "zlib": "1.0.5" @@ -68,16 +67,16 @@ "turbo": "^1.13.4" }, "engines": { - "node": ">= 18.20.4", + "node": ">= 18.20.7", "yarn": "use pnpm", "npm": "use pnpm", - "pnpm": "8.15.9" + "pnpm": "10.5.2" }, "volta": { "node": "22.3.0", - "pnpm": "8.15.9" + "pnpm": "10.5.2" }, - "packageManager": "pnpm@8.15.9", + "packageManager": "pnpm@10.5.2", "changelog": { "labels": { ":label: breaking": ":boom: Breaking Change", @@ -135,6 +134,13 @@ "@glimmer/component": "*" } }, + "ember-exam": { + "peerDependencies": { + "ember-cli": "*", + "ember-qunit": "*", + "qunit": "*" + } + }, "@ember/test-helpers": { "dependencies": { "webpack": "*" @@ -145,15 +151,15 @@ "ember-auto-import": "^2.10.0", "broccoli-funnel": "^3.0.8", "broccoli-merge-trees": "^4.2.0", - "@glimmer/validator": "^0.92.3", - "@glint/core": "1.5.0", - "@glint/environment-ember-loose": "1.5.0", - "@glint/environment-ember-template-imports": "1.5.0", - "@glint/template": "1.5.0", + "@glimmer/validator": "^0.94.6", + "@glint/core": "1.5.2", + "@glint/environment-ember-loose": "1.5.2", + "@glint/environment-ember-template-imports": "1.5.2", + "@glint/template": "1.5.2", "ember-cli-babel": "^8.2.0", "ember-cli-htmlbars": "^6.3.0", "ember-cli-typescript": "^5.3.0", - "webpack": "5.94.0", + "webpack": "5.98.0", "qunit": "2.19.4", "ember-compatibility-helpers": "^1.2.7", "testem": "~3.11.0" diff --git a/packages/-ember-data/package.json b/packages/-ember-data/package.json index 2830de12d00..f561e7091bf 100644 --- a/packages/-ember-data/package.json +++ b/packages/-ember-data/package.json @@ -17,8 +17,7 @@ "scripts": { "lint": "eslint . --quiet --cache --cache-strategy=content", "build:pkg": "vite build;", - "prepack": "bun run build:pkg", - "sync-hardlinks": "bun run sync-dependencies-meta-injected" + "prepack": "pnpm run build:pkg" }, "ember-addon": { "main": "addon-main.cjs", @@ -66,47 +65,6 @@ }, "author": "", "license": "MIT", - "dependenciesMeta": { - "@ember-data/adapter": { - "injected": true - }, - "@ember-data/graph": { - "injected": true - }, - "@ember-data/debug": { - "injected": true - }, - "@ember-data/model": { - "injected": true - }, - "@ember-data/json-api": { - "injected": true - }, - "@ember-data/request": { - "injected": true - }, - "@ember-data/request-utils": { - "injected": true - }, - "@ember-data/legacy-compat": { - "injected": true - }, - "@ember-data/serializer": { - "injected": true - }, - "@ember-data/store": { - "injected": true - }, - "@ember-data/tracking": { - "injected": true - }, - "@warp-drive/core-types": { - "injected": true - }, - "@warp-drive/build-config": { - "injected": true - } - }, "dependencies": { "@ember-data/adapter": "workspace:*", "@ember-data/debug": "workspace:*", @@ -120,7 +78,7 @@ "@ember-data/store": "workspace:*", "@ember-data/tracking": "workspace:*", "@ember/edition-utils": "^1.2.0", - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.11", "@warp-drive/core-types": "workspace:*", "@warp-drive/build-config": "workspace:*" }, @@ -142,10 +100,10 @@ } }, "devDependencies": { - "@babel/core": "^7.24.5", - "@babel/plugin-transform-typescript": "^7.24.5", - "@babel/preset-env": "^7.24.5", - "@babel/preset-typescript": "^7.24.1", + "@babel/core": "^7.26.9", + "@babel/plugin-transform-typescript": "^7.26.8", + "@babel/preset-env": "^7.26.9", + "@babel/preset-typescript": "^7.26.0", "@ember/test-waiters": "^3.1.0", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", @@ -153,14 +111,13 @@ "@ember/test-helpers": "5.1.0", "@warp-drive/internal-config": "workspace:*", "ember-source": "~5.12.0", - "eslint": "^9.12.0", - "pnpm-sync-dependencies-meta-injected": "0.0.14", - "vite": "^5.2.11", - "typescript": "^5.7.2", + "eslint": "^9.21.0", + "vite": "^5.4.14", + "typescript": "^5.8.2", "qunit": "^2.18.0" }, "engines": { - "node": ">= 18.20.4" + "node": ">= 18.20.7" }, "ember": { "edition": "octane" @@ -168,5 +125,5 @@ "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@8.15.9" + "packageManager": "pnpm@10.5.2" } diff --git a/packages/active-record/package.json b/packages/active-record/package.json index e2df6a6f9b0..1a1733a0220 100644 --- a/packages/active-record/package.json +++ b/packages/active-record/package.json @@ -13,7 +13,7 @@ "homepage": "https://github.com/emberjs/data", "bugs": "https://github.com/emberjs/data/issues", "engines": { - "node": ">= 18.20.4" + "node": ">= 18.20.7" }, "keywords": [ "ember-addon" @@ -36,8 +36,8 @@ "scripts": { "lint": "eslint . --quiet --cache --cache-strategy=content", "build:pkg": "vite build;", - "prepack": "bun run build:pkg", - "sync-hardlinks": "bun run sync-dependencies-meta-injected" + "prepack": "pnpm run build:pkg", + "sync": "echo \"syncing\"" }, "ember-addon": { "main": "addon-main.cjs", @@ -45,7 +45,7 @@ "version": 2 }, "dependencies": { - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.11", "@warp-drive/build-config": "workspace:*" }, "peerDependencies": { @@ -54,9 +54,9 @@ "@warp-drive/core-types": "workspace:*" }, "devDependencies": { - "@babel/core": "^7.24.5", - "@babel/plugin-transform-typescript": "^7.24.5", - "@babel/preset-typescript": "^7.24.1", + "@babel/core": "^7.26.9", + "@babel/plugin-transform-typescript": "^7.26.8", + "@babel/preset-typescript": "^7.26.0", "@ember-data/request": "workspace:*", "@ember-data/request-utils": "workspace:*", "@ember-data/store": "workspace:*", @@ -65,29 +65,8 @@ "@warp-drive/core-types": "workspace:*", "@warp-drive/internal-config": "workspace:*", "ember-source": "~5.12.0", - "pnpm-sync-dependencies-meta-injected": "0.0.14", - "vite": "^5.2.11", - "typescript": "^5.7.2" - }, - "dependenciesMeta": { - "@warp-drive/core-types": { - "injected": true - }, - "@warp-drive/build-config": { - "injected": true - }, - "@ember-data/store": { - "injected": true - }, - "@ember-data/request-utils": { - "injected": true - }, - "@ember-data/request": { - "injected": true - }, - "@ember-data/tracking": { - "injected": true - } + "vite": "^5.4.14", + "typescript": "^5.8.2" }, "ember": { "edition": "octane" diff --git a/packages/adapter/package.json b/packages/adapter/package.json index a40e94c1ad5..0d7fada50b2 100644 --- a/packages/adapter/package.json +++ b/packages/adapter/package.json @@ -16,8 +16,7 @@ "scripts": { "lint": "eslint . --quiet --cache --cache-strategy=content", "build:pkg": "vite build;", - "prepack": "bun run build:pkg", - "sync-hardlinks": "bun run sync-dependencies-meta-injected" + "prepack": "pnpm run build:pkg" }, "ember-addon": { "main": "addon-main.cjs", @@ -54,37 +53,8 @@ "@ember-data/request-utils": "workspace:*", "@warp-drive/core-types": "workspace:*" }, - "dependenciesMeta": { - "@warp-drive/core-types": { - "injected": true - }, - "@ember-data/legacy-compat": { - "injected": true - }, - "@ember-data/store": { - "injected": true - }, - "@ember-data/request": { - "injected": true - }, - "@ember-data/tracking": { - "injected": true - }, - "@ember-data/graph": { - "injected": true - }, - "@ember-data/json-api": { - "injected": true - }, - "@ember-data/request-utils": { - "injected": true - }, - "@warp-drive/build-config": { - "injected": true - } - }, "dependencies": { - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.11", "ember-cli-test-info": "^1.0.0", "ember-cli-string-utils": "^1.1.0", "ember-cli-path-utils": "^1.0.0", @@ -92,9 +62,9 @@ "@warp-drive/build-config": "workspace:*" }, "devDependencies": { - "@babel/core": "^7.24.5", - "@babel/plugin-transform-typescript": "^7.24.5", - "@babel/preset-typescript": "^7.24.1", + "@babel/core": "^7.26.9", + "@babel/plugin-transform-typescript": "^7.26.8", + "@babel/preset-typescript": "^7.26.0", "@ember-data/graph": "workspace:*", "@ember-data/json-api": "workspace:*", "@ember-data/legacy-compat": "workspace:*", @@ -105,19 +75,18 @@ "@ember/test-waiters": "^3.1.0", "@glimmer/component": "^1.1.2", "decorator-transforms": "^2.3.0", - "@types/jquery": "^3.5.30", + "@types/jquery": "^3.5.32", "@warp-drive/core-types": "workspace:*", "@warp-drive/internal-config": "workspace:*", "ember-source": "~5.12.0", - "pnpm-sync-dependencies-meta-injected": "0.0.14", - "typescript": "^5.7.2", - "vite": "^5.2.11" + "typescript": "^5.8.2", + "vite": "^5.4.14" }, "engines": { - "node": ">= 18.20.4" + "node": ">= 18.20.7" }, "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@8.15.9" + "packageManager": "pnpm@10.5.2" } diff --git a/packages/build-config/cjs-src/transforms/babel-plugin-transform-logging.js b/packages/build-config/cjs-src/transforms/babel-plugin-transform-logging.js index c7645039673..e8e2088a4c5 100644 --- a/packages/build-config/cjs-src/transforms/babel-plugin-transform-logging.js +++ b/packages/build-config/cjs-src/transforms/babel-plugin-transform-logging.js @@ -25,6 +25,9 @@ export default function (babel) { } let localBindingName = specifier.node.local.name; let binding = specifier.scope.getBinding(localBindingName); + const enableRuntimeActivation = Boolean(state.opts.runtimeKey); + const strippableKey = enableRuntimeActivation ? state.opts.runtimeKey : state.opts.configKey; + binding.referencePaths.forEach((p) => { let negateStatement = false; let node = p; @@ -38,18 +41,85 @@ export default function (babel) { t.callExpression(state.importer.import(p, '@embroider/macros', 'getGlobalConfig'), []), t.identifier('WarpDrive') ), - t.identifier(state.opts.configKey) + t.identifier(strippableKey) ), t.identifier(name) ); + node.replaceWith( - // if (LOG_FOO) + // if (LOG_FOO) { + // // ... + // } // => - // if (macroCondition(getGlobalConfig('WarpDrive').debug.LOG_FOO)) + // if (macroCondition(getGlobalConfig('WarpDrive').debug.LOG_FOO)) { + // // ... + // } t.callExpression(state.importer.import(p, '@embroider/macros', 'macroCondition'), [ negateStatement ? t.unaryExpression('!', getConfig) : getConfig, ]) ); + + if (enableRuntimeActivation) { + // we do not yet support arbitrary runtime activation locations, + // the only supported locations are `if (LOG)` style statements, no + // ternaries or other more complex expressions + const parentIfStatement = node.parentPath.type === 'IfStatement' ? node.parentPath : null; + if (!parentIfStatement) { + throw new Error( + `Runtime activation of logging flags is only supported in if statements, but found node '${node.parentPath.type}'` + ); + } + + // if (LOG_FOO) { + // // ... + // } + // => + // if (macroCondition(getGlobalConfig('WarpDrive').activeLogging.LOG_FOO)) { + // if (getGlobalConfig('WarpDrive').debug.LOG_FOO || globalThis.getWarpDriveRuntimeConfig().debug.LOG_FOO) { + // // ... + // } + // } + // + // the outer-if is generated by the node-replace above. The inner if is generated here. + const originalBody = parentIfStatement.node.consequent; + + // getGlobalConfig('WarpDrive').debug.LOG_FOO + const getActualConfig = t.memberExpression( + t.memberExpression( + t.memberExpression( + t.callExpression(state.importer.import(p, '@embroider/macros', 'getGlobalConfig'), []), + t.identifier('WarpDrive') + ), + t.identifier(state.opts.configKey) + ), + t.identifier(name) + ); + + // globalThis.getWarpDriveRuntimeConfig().debug.LOG_FOO + const getRuntimeConfig = t.memberExpression( + t.memberExpression( + t.callExpression( + t.memberExpression(t.identifier('globalThis'), t.identifier('getWarpDriveRuntimeConfig')), + [] + ), + t.identifier(state.opts.configKey) + ), + t.identifier(name) + ); + + // || + const ifExp = t.logicalExpression('||', getActualConfig, getRuntimeConfig); + + // if (( || )) + const innerIfStatement = t.ifStatement( + negateStatement ? t.unaryExpression('!', ifExp) : ifExp, + originalBody + ); + + // replace the original body with the new if statement + parentIfStatement.node.consequent = t.blockStatement([innerIfStatement]); + } else { + } }); specifier.scope.removeOwnBinding(localBindingName); specifier.remove(); diff --git a/packages/build-config/package.json b/packages/build-config/package.json index ee95de7c98e..6655f8b6772 100644 --- a/packages/build-config/package.json +++ b/packages/build-config/package.json @@ -15,7 +15,8 @@ "author": "Chris Thoburn ", "scripts": { "build:infra": "vite build; vite build -c ./vite.config-cjs.mjs;", - "prepack": "bun run build:infra" + "prepack": "pnpm run build:infra", + "sync": "echo \"syncing\"" }, "type": "module", "files": [ @@ -41,29 +42,28 @@ } }, "dependencies": { - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.11", "@embroider/addon-shim": "^1.9.0", "babel-import-util": "^2.1.1", "broccoli-funnel": "^3.0.8", - "semver": "^7.6.3" + "semver": "^7.7.1" }, "devDependencies": { "@warp-drive/internal-config": "workspace:*", "@types/babel__core": "^7.20.5", - "@types/node": "^20.14.2", - "@babel/plugin-transform-typescript": "^7.24.5", - "@babel/preset-typescript": "^7.24.1", - "@babel/core": "^7.24.5", - "pnpm-sync-dependencies-meta-injected": "0.0.14", - "typescript": "^5.7.2", - "bun-types": "^1.2.2", - "vite": "^5.2.11" + "@types/node": "^20.17.22", + "@babel/plugin-transform-typescript": "^7.26.8", + "@babel/preset-typescript": "^7.26.0", + "@babel/core": "^7.26.9", + "typescript": "^5.8.2", + "bun-types": "^1.2.4", + "vite": "^5.4.14" }, "engines": { - "node": ">= 18.20.4" + "node": ">= 18.20.7" }, "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@8.15.9" + "packageManager": "pnpm@10.5.2" } diff --git a/packages/build-config/src/-private/utils/logging.ts b/packages/build-config/src/-private/utils/logging.ts new file mode 100644 index 00000000000..ce0d465fe20 --- /dev/null +++ b/packages/build-config/src/-private/utils/logging.ts @@ -0,0 +1,19 @@ +import * as LOGGING from '../../debugging.ts'; + +type LOG_CONFIG_KEY = keyof typeof LOGGING; +export type LOG_CONFIG = { [key in LOG_CONFIG_KEY]: boolean }; + +export function createLoggingConfig(env: { DEBUG: boolean; TESTING: boolean; PRODUCTION: boolean }, debug: LOG_CONFIG) { + const config = {} as LOG_CONFIG; + const keys = Object.keys(LOGGING) as LOG_CONFIG_KEY[]; + + for (const key of keys) { + if (env.DEBUG || env.TESTING) { + config[key] = true; + } else { + config[key] = debug[key] || false; + } + } + + return config; +} diff --git a/packages/build-config/src/babel-macros.ts b/packages/build-config/src/babel-macros.ts index df79474b854..d49172e3426 100644 --- a/packages/build-config/src/babel-macros.ts +++ b/packages/build-config/src/babel-macros.ts @@ -47,6 +47,7 @@ export function macros() { { source: '@warp-drive/build-config/debugging', configKey: 'debug', + runtimeKey: 'activeLogging', flags: config.debug, }, '@warp-drive/build-config/debugging-stripping', diff --git a/packages/build-config/src/debugging.ts b/packages/build-config/src/debugging.ts index ce19966afe2..5a4b1ec5fab 100644 --- a/packages/build-config/src/debugging.ts +++ b/packages/build-config/src/debugging.ts @@ -111,3 +111,11 @@ export const LOG_INSTANCE_CACHE: boolean = false; * @public */ export const LOG_METRIC_COUNTS: boolean = false; +/** + * Helps when debugging causes of a change notification + * when processing an update to a hasMany relationship. + * + * @property {boolean} DEBUG_RELATIONSHIP_NOTIFICATIONS + * @public + */ +export const DEBUG_RELATIONSHIP_NOTIFICATIONS: boolean = false; diff --git a/packages/build-config/src/index.ts b/packages/build-config/src/index.ts index 2b12add9fab..ef9c0126165 100644 --- a/packages/build-config/src/index.ts +++ b/packages/build-config/src/index.ts @@ -4,6 +4,7 @@ import { getDeprecations } from './-private/utils/deprecations.ts'; import { getFeatures } from './-private/utils/features.ts'; import * as LOGGING from './debugging.ts'; import type { MacrosConfig } from '@embroider/macros/src/node.js'; +import { createLoggingConfig } from './-private/utils/logging.ts'; const _MacrosConfig = EmbroiderMacros.MacrosConfig as unknown as typeof MacrosConfig; @@ -25,6 +26,7 @@ type InternalWarpDriveConfig = { compatWith: `${number}.${number}` | null; deprecations: ReturnType; features: ReturnType; + activeLogging: { [key in LOG_CONFIG_KEY]: boolean }; env: { TESTING: boolean; PRODUCTION: boolean; @@ -90,6 +92,7 @@ export function setConfig(context: object, appRoot: string, config: WarpDriveCon compatWith: config.compatWith ?? null, deprecations: DEPRECATIONS, features: FEATURES, + activeLogging: createLoggingConfig(env, debugOptions), env, }; diff --git a/packages/build-config/src/runtime.ts b/packages/build-config/src/runtime.ts new file mode 100644 index 00000000000..9e024dd9ff0 --- /dev/null +++ b/packages/build-config/src/runtime.ts @@ -0,0 +1,21 @@ +import { LOG_CONFIG } from './-private/utils/logging'; + +const RuntimeConfig = { + debug: {}, +}; + +export function getRuntimeConfig() { + return RuntimeConfig; +} + +/** + * Upserts the specified logging configuration into the runtime + * config. + * + * globalThis.setWarpDriveLogging({ LOG_PAYLOADS: true } }); + * + * @typedoc + */ +export function setLogging(config: Partial) { + Object.assign(RuntimeConfig.debug, config); +} diff --git a/packages/build-config/tsconfig.json b/packages/build-config/tsconfig.json index 62c40e41edd..a54770a4d6e 100644 --- a/packages/build-config/tsconfig.json +++ b/packages/build-config/tsconfig.json @@ -1,5 +1,5 @@ { - "include": ["src/**/*"], + "include": ["src/**/**/*"], "compilerOptions": { "lib": ["DOM", "ESNext"], "module": "ESNext", diff --git a/packages/build-config/vite.config.mjs b/packages/build-config/vite.config.mjs index 78033fcc611..33008e69858 100644 --- a/packages/build-config/vite.config.mjs +++ b/packages/build-config/vite.config.mjs @@ -10,6 +10,7 @@ export const entryPoints = [ './src/debugging.ts', './src/deprecations.ts', './src/canary-features.ts', + './src/runtime.ts', ]; export default createConfig( diff --git a/packages/core-types/package.json b/packages/core-types/package.json index f184273de79..43ba95d7bfa 100644 --- a/packages/core-types/package.json +++ b/packages/core-types/package.json @@ -15,8 +15,8 @@ "scripts": { "lint": "eslint . --quiet --cache --cache-strategy=content", "build:pkg": "vite build;", - "prepack": "bun run build:pkg", - "sync-hardlinks": "bun run sync-dependencies-meta-injected" + "prepack": "pnpm run build:pkg", + "sync": "echo \"syncing\"" }, "files": [ "dist", @@ -38,25 +38,24 @@ } }, "dependencies": { - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.11", "@warp-drive/build-config": "workspace:*" }, "devDependencies": { - "@babel/core": "^7.24.5", - "@babel/plugin-transform-typescript": "^7.24.5", - "@babel/preset-typescript": "^7.24.1", + "@babel/core": "^7.26.9", + "@babel/plugin-transform-typescript": "^7.26.8", + "@babel/preset-typescript": "^7.26.0", "@warp-drive/internal-config": "workspace:*", - "pnpm-sync-dependencies-meta-injected": "0.0.14", - "typescript": "^5.7.2", - "vite": "^5.2.11" + "typescript": "^5.8.2", + "vite": "^5.4.14" }, "engines": { - "node": ">= 18.20.4" + "node": ">= 18.20.7" }, "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@8.15.9", + "packageManager": "pnpm@10.5.2", "ember-addon": { "main": "addon-main.cjs", "type": "addon", @@ -64,10 +63,5 @@ }, "ember": { "edition": "octane" - }, - "dependenciesMeta": { - "@warp-drive/build-config": { - "injected": true - } } } diff --git a/packages/core-types/src/-private.ts b/packages/core-types/src/-private.ts index a08ec018525..4528646d9cc 100644 --- a/packages/core-types/src/-private.ts +++ b/packages/core-types/src/-private.ts @@ -67,6 +67,8 @@ type GlobalKey = // @ember-data/request | 'IS_FROZEN' | 'IS_CACHE_HANDLER' + // @ember-data/request-utils + | 'CONFIG' // @ember-data/store IdentityCache | 'DEBUG_MAP' | 'IDENTIFIERS' diff --git a/packages/core-types/src/cache.ts b/packages/core-types/src/cache.ts index a219c3b4f81..08abf403db8 100644 --- a/packages/core-types/src/cache.ts +++ b/packages/core-types/src/cache.ts @@ -141,6 +141,43 @@ export interface Cache { peek(identifier: StableRecordIdentifier>): T | null; peek(identifier: StableDocumentIdentifier): ResourceDocument | null; + /** + * Peek remote resource data from the Cache. + * + * This will give the data provided from the server without any local changes. + * + * In development, if the return value + * is JSON the return value + * will be deep-cloned and deep-frozen + * to prevent mutation thereby enforcing cache + * Immutability. + * + * This form of peek is useful for implementations + * that want to feed raw-data from cache to the UI + * or which want to interact with a blob of data + * directly from the presentation cache. + * + * An implementation might want to do this because + * de-referencing records which read from their own + * blob is generally safer because the record does + * not require retainining connections to the Store + * and Cache to present data on a per-field basis. + * + * This generally takes the place of `getAttr` as + * an API and may even take the place of `getRelationship` + * depending on implementation specifics, though this + * latter usage is less recommended due to the advantages + * of the Graph handling necessary entanglements and + * notifications for relational data. + * + * @method peek + * @public + * @param {StableRecordIdentifier | StableDocumentIdentifier} identifier + * @return {ResourceDocument | ResourceBlob | null} the known resource data + */ + peekRemoteState(identifier: StableRecordIdentifier>): T | null; + peekRemoteState(identifier: StableDocumentIdentifier): ResourceDocument | null; + /** * Peek the Cache for the existing request data associated with * a cacheable request @@ -341,6 +378,17 @@ export interface Cache { */ getAttr(identifier: StableRecordIdentifier, field: string | string[]): Value | undefined; + /** + * Retrieve remote state without any local changes for a specific attribute + * + * @method getRemoteAttr + * @public + * @param identifier + * @param field + * @return {unknown} + */ + getRemoteAttr(identifier: StableRecordIdentifier, field: string | string[]): Value | undefined; + /** * Mutate the data for an attribute in the cache * @@ -460,6 +508,21 @@ export interface Cache { isCollection?: boolean ): ResourceRelationship | CollectionRelationship; + /** + * Query the cache for the server state of a relationship property without any local changes + * + * @method getRelationship + * @public + * @param {StableRecordIdentifier} identifier + * @param {string} field + * @return resource relationship object + */ + getRemoteRelationship( + identifier: StableRecordIdentifier, + field: string, + isCollection?: boolean + ): ResourceRelationship | CollectionRelationship; + // Resource State // =============== diff --git a/packages/core-types/src/request.ts b/packages/core-types/src/request.ts index 115fa4fed29..21494968699 100644 --- a/packages/core-types/src/request.ts +++ b/packages/core-types/src/request.ts @@ -12,7 +12,17 @@ export const EnableHydration = getOrSetUniversal('EnableHydration', Symbol.for(' export const IS_FUTURE = getOrSetGlobal('IS_FUTURE', Symbol('IS_FUTURE')); export const STRUCTURED = getOrSetGlobal('DOC', Symbol('DOC')); -export type HTTPMethod = 'GET' | 'OPTIONS' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD'; +export type HTTPMethod = + | 'QUERY' + | 'GET' + | 'OPTIONS' + | 'POST' + | 'PUT' + | 'PATCH' + | 'DELETE' + | 'HEAD' + | 'CONNECT' + | 'TRACE'; /** * Use these options to adjust CacheHandler behavior for a request. @@ -93,7 +103,7 @@ export type PostQueryRequestOptions = { url: string; method: 'POST' | 'QUERY'; headers: Headers; - body: string; + body?: string | BodyInit | FormData; cacheOptions: CacheOptions & { key: string }; op: 'query'; [RequestSignature]?: RT; @@ -104,6 +114,7 @@ export type DeleteRequestOptions = { method: 'DELETE'; headers: Headers; op: 'deleteRecord'; + body?: string | BodyInit | FormData; data: { record: StableRecordIdentifier>; }; @@ -121,6 +132,7 @@ export type UpdateRequestOptions = { method: 'PATCH' | 'PUT'; headers: Headers; op: 'updateRecord'; + body?: string | BodyInit | FormData; data: { record: StableRecordIdentifier>; }; @@ -133,6 +145,7 @@ export type CreateRequestOptions = { method: 'POST'; headers: Headers; op: 'createRecord'; + body?: string | BodyInit | FormData; data: { record: StableRecordIdentifier>; }; diff --git a/packages/debug/package.json b/packages/debug/package.json index 1437dbc16e6..f30c77b353d 100644 --- a/packages/debug/package.json +++ b/packages/debug/package.json @@ -16,8 +16,7 @@ "scripts": { "lint": "eslint . --quiet --cache --cache-strategy=content", "build:pkg": "vite build;", - "prepack": "bun run build:pkg", - "sync-hardlinks": "bun run sync-dependencies-meta-injected" + "prepack": "pnpm run build:pkg" }, "files": [ "unstable-preview-types", @@ -46,42 +45,16 @@ "@ember-data/request-utils": "workspace:*", "@warp-drive/core-types": "workspace:*" }, - "dependenciesMeta": { - "@ember-data/store": { - "injected": true - }, - "@ember-data/model": { - "injected": true - }, - "@ember-data/legacy-compat": { - "injected": true - }, - "@ember-data/request": { - "injected": true - }, - "@ember-data/request-utils": { - "injected": true - }, - "@warp-drive/core-types": { - "injected": true - }, - "@ember-data/tracking": { - "injected": true - }, - "@warp-drive/build-config": { - "injected": true - } - }, "dependencies": { "@ember/edition-utils": "^1.2.0", - "@embroider/macros": "^1.16.10", + "@embroider/macros": "^1.16.11", "@warp-drive/build-config": "workspace:*" }, "devDependencies": { - "@babel/core": "^7.24.5", - "@babel/plugin-transform-typescript": "^7.24.5", - "@babel/preset-env": "^7.24.5", - "@babel/preset-typescript": "^7.24.1", + "@babel/core": "^7.26.9", + "@babel/plugin-transform-typescript": "^7.26.8", + "@babel/preset-env": "^7.26.9", + "@babel/preset-typescript": "^7.26.0", "@ember-data/request": "workspace:*", "@ember-data/legacy-compat": "workspace:*", "@ember-data/request-utils": "workspace:*", @@ -94,12 +67,11 @@ "@warp-drive/internal-config": "workspace:*", "ember-source": "~5.12.0", "decorator-transforms": "^2.3.0", - "pnpm-sync-dependencies-meta-injected": "0.0.14", - "typescript": "^5.7.2", - "vite": "^5.2.11" + "typescript": "^5.8.2", + "vite": "^5.4.14" }, "engines": { - "node": ">= 18.20.4" + "node": ">= 18.20.7" }, "ember-addon": { "main": "addon-main.cjs", @@ -112,5 +84,5 @@ "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@8.15.9" + "packageManager": "pnpm@10.5.2" } diff --git a/packages/diagnostic/package.json b/packages/diagnostic/package.json index 231db0c1c10..b304cbc23ea 100644 --- a/packages/diagnostic/package.json +++ b/packages/diagnostic/package.json @@ -70,8 +70,8 @@ "lint": "eslint . --quiet --cache --cache-strategy=content", "build:tests": "rm -rf dist-test && cp -R test dist-test && mkdir -p dist-test/@warp-drive && cp -R dist dist-test/@warp-drive/diagnostic", "build:pkg": "vite build;", - "prepack": "bun run build:pkg", - "sync-hardlinks": "bun run sync-dependencies-meta-injected" + "prepack": "pnpm run build:pkg", + "sync": "echo \"syncing\"" }, "peerDependencies": { "ember-source": "3.28.12 || ^4.0.4 || ^5.0.0 || ^6.0.0", @@ -91,46 +91,38 @@ }, "dependencies": { "chalk": "^5.3.0", - "debug": "^4.3.7", - "ember-cli-htmlbars": "^6.3.0", + "debug": "^4.4.0", "tmp": "^0.2.3", "@warp-drive/build-config": "workspace:*" }, "devDependencies": { - "@babel/core": "^7.24.5", - "@babel/plugin-transform-typescript": "^7.24.5", - "@babel/preset-env": "^7.24.5", - "@babel/preset-typescript": "^7.24.1", - "@babel/runtime": "^7.24.5", + "@babel/core": "^7.26.9", + "@babel/plugin-transform-typescript": "^7.26.8", + "@babel/preset-env": "^7.26.9", + "@babel/preset-typescript": "^7.26.0", + "@babel/runtime": "^7.26.9", "@warp-drive/internal-config": "workspace:*", - "bun-types": "^1.2.2", + "bun-types": "^1.2.4", "@ember/test-helpers": "5.1.0", "ember-source": "~5.12.0", "@glimmer/component": "^1.1.2", "ember-cli-test-loader": "^3.1.0", - "pnpm-sync-dependencies-meta-injected": "0.0.14", - "typescript": "^5.7.2", - "vite": "^5.2.11" + "typescript": "^5.8.2", + "vite": "^5.4.14" }, "engines": { - "node": ">= 18.20.4" + "node": ">= 18.20.7" }, "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@8.15.9", + "packageManager": "pnpm@10.5.2", "ember-addon": { "main": "addon-main.cjs", "type": "addon", - "version": 2, - "preventDownleveling": true + "version": 2 }, "ember": { "edition": "octane" - }, - "dependenciesMeta": { - "@warp-drive/build-config": { - "injected": true - } } } diff --git a/packages/diagnostic/server/NCC-1701-a-gold_100.svg b/packages/diagnostic/server/NCC-1701-a-gold_100.svg new file mode 100644 index 00000000000..024f6d7f905 --- /dev/null +++ b/packages/diagnostic/server/NCC-1701-a-gold_100.svg @@ -0,0 +1 @@ + diff --git a/packages/diagnostic/server/browsers/index.js b/packages/diagnostic/server/browsers/index.js index 83a34b7442f..05672e8a3e5 100644 --- a/packages/diagnostic/server/browsers/index.js +++ b/packages/diagnostic/server/browsers/index.js @@ -184,6 +184,7 @@ export function recommendedArgs(browser, options = {}) { '--no-sandbox', // these prevent user account // and extensions from mucking with things + '--disable-extensions', '--incognito', '--bwsi', '--enable-automation', diff --git a/packages/diagnostic/server/bun/fetch.js b/packages/diagnostic/server/bun/fetch.js index c8421eb4dd7..be4bdc288cd 100644 --- a/packages/diagnostic/server/bun/fetch.js +++ b/packages/diagnostic/server/bun/fetch.js @@ -31,17 +31,19 @@ export function handleBunFetch(config, state, req, server) { if (INDEX_PATHS.includes(url.pathname)) { if (bId && wId) { // serve test index.html - if (config.entry.indexOf('?')) { - config._realEntry = config.entry.substr(0, config.entry.indexOf('?')); + if (!config._realEntry && config.entry.indexOf('?')) { + config._realEntry = path.join(process.cwd(), config.entry.substr(0, config.entry.indexOf('?'))); } debug(`Serving entry ${config._realEntry} for browser ${bId} window ${wId}`); - return new Response(Bun.file(config._realEntry)); + + const asset = Bun.file(config._realEntry); + return new Response(asset); } const _bId = bId ?? state.lastBowserId ?? state.browserId; const _wId = wId ?? state.lastWindowId ?? state.windowId; debug(`Redirecting to ${config.entry} for browser ${_bId} window ${_wId}`); // redirect to index.html - return Response.redirect(`${protocol}://${state.hostname}:${state.port}?b=${_bId}&w=${_wId}`, { status: 302 }); + return Response.redirect(`/?b=${_bId}&w=${_wId}`, { status: 302 }); } else { const pathParts = url.pathname.split('/'); @@ -55,8 +57,11 @@ export function handleBunFetch(config, state, req, server) { } const route = pathParts.join('/'); - if (route === 'favicon.ico') { - return new Response('Not Found', { status: 404 }); + if (route === 'favicon.ico' || route === 'NCC-1701-a-gold_100.svg') { + const dir = import.meta.dir; + const asset = path.join(dir, '../NCC-1701-a-gold_100.svg'); + + return new Response(Bun.file(asset)); } // serve test assets diff --git a/packages/diagnostic/server/bun/socket-handler.js b/packages/diagnostic/server/bun/socket-handler.js index 5773ecc3454..ced50801b98 100644 --- a/packages/diagnostic/server/bun/socket-handler.js +++ b/packages/diagnostic/server/bun/socket-handler.js @@ -7,7 +7,7 @@ import { watchAssets } from './watch.js'; export function buildHandler(config, state) { const Connections = new Set(); if (config.serve && !config.noWatch) { - watchAssets(config.assets, () => { + watchAssets(state, config.assets, () => { Connections.forEach((ws) => { ws.send(JSON.stringify({ name: 'reload' })); }); @@ -18,7 +18,8 @@ export function buildHandler(config, state) { perMessageDeflate: true, async message(ws, message) { const msg = JSON.parse(message); - msg.launcher = state.browsers.get(msg.browserId).launcher; + msg.launcher = state.browsers.get(msg.browserId)?.launcher ?? ''; + info(`${chalk.green('➡')} [${chalk.cyan(msg.browserId)}/${chalk.cyan(msg.windowId)}] ${chalk.green(msg.name)}`); switch (msg.name) { @@ -53,23 +54,19 @@ export function buildHandler(config, state) { debug(`${chalk.green('✅ [All Complete]')} ${chalk.yellow('@' + sinceStart())}`); if (!config.serve) { - state.browsers.forEach((browser) => { - browser.proc.kill(); - browser.proc.unref(); - }); - state.server.stop(); - if (config.cleanup) { - debug(`Running configured cleanup hook`); - await config.cleanup(); - debug(`Configured cleanup hook completed`); - } + await state.safeCleanup(); + debug(`\n\nExiting with code ${exitCode}`); // 1. We expect all cleanup to have happened after // config.cleanup(), so exiting here should be safe. // 2. We also want to forcibly exit with a success code in this // case. // eslint-disable-next-line n/no-process-exit process.exit(exitCode); + } else { + state.completed = 0; } + } else { + console.log(`Waiting for ${state.expected - state.completed} more browsers to finish`); } break; diff --git a/packages/diagnostic/server/bun/watch.js b/packages/diagnostic/server/bun/watch.js index e6be7cc4cc0..6bb61a623e3 100644 --- a/packages/diagnostic/server/bun/watch.js +++ b/packages/diagnostic/server/bun/watch.js @@ -1,6 +1,10 @@ import { watch } from 'fs'; -export function addCloseHandler(cb) { +export function addCloseHandler(state, cb) { + state.closeHandlers.push(createCloseHandler(cb)); +} + +function createCloseHandler(cb) { let executed = false; process.on('SIGINT', () => { @@ -26,14 +30,20 @@ export function addCloseHandler(cb) { executed = true; cb(); }); + + return () => { + if (executed) return; + executed = true; + cb(); + }; } -export function watchAssets(directory, onAssetChange) { +export function watchAssets(state, directory, onAssetChange) { const watcher = watch(directory, { recursive: true }, (event, filename) => { onAssetChange(event, filename); }); - addCloseHandler(() => { + addCloseHandler(state, () => { watcher.close(); }); } diff --git a/packages/diagnostic/server/default-setup.js b/packages/diagnostic/server/default-setup.js index e0ac49ce1f6..589be7b8266 100644 --- a/packages/diagnostic/server/default-setup.js +++ b/packages/diagnostic/server/default-setup.js @@ -3,7 +3,7 @@ import fs from 'fs'; import path from 'path'; import { getBrowser, recommendedArgs } from './browsers/index.js'; -import launch from './index.js'; +import { launch } from './index.js'; import DefaultReporter from './reporters/default.js'; import { getFlags } from './utils/get-flags.js'; @@ -79,6 +79,11 @@ export default async function launchDefault(overrides = {}) { debug: overrides.debug ?? false, headless: overrides.headless ?? false, useExisting: overrides.useExisting ?? false, + protocol: overrides.protocol ?? 'http', + key: overrides.key ?? null, + cert: overrides.cert ?? null, + hostname: overrides.hostname ?? 'localhost', + port: overrides.port ?? null, entry: overrides.entry ?? `./dist-test/tests/index.html?${TEST_PAGE_FLAGS.join('&')}`, assets: overrides.assets ?? './dist-test', diff --git a/packages/diagnostic/server/index.js b/packages/diagnostic/server/index.js index b75e3bcd5a4..e12c5bb6e7e 100644 --- a/packages/diagnostic/server/index.js +++ b/packages/diagnostic/server/index.js @@ -5,11 +5,12 @@ import { launchBrowsers } from './bun/launch-browser.js'; import { buildHandler } from './bun/socket-handler.js'; import { debug, error, print } from './utils/debug.js'; import { getPort } from './utils/port.js'; +import { addCloseHandler } from './bun/watch.js'; /** @type {import('bun-types')} */ const isBun = typeof Bun !== 'undefined'; -export default async function launch(config) { +export async function launch(config) { if (isBun) { debug(`Bun detected, using Bun.serve()`); @@ -34,6 +35,23 @@ export default async function launch(config) { browsers: new Map(), completed: 0, expected: config.parallel ?? 1, + closeHandlers: [], + }; + async function runCloseHandler(handler) { + try { + await handler(); + } catch (e) { + error(`Error in close handler: ${e?.message ?? e}`); + } + } + state.safeCleanup = async () => { + debug(`Running close handlers`); + const promises = []; + for (const handler of state.closeHandlers) { + promises.push(runCloseHandler(handler)); + } + await Promise.allSettled(promises); + debug(`All close handlers completed`); }; if (protocol === 'https') { @@ -56,6 +74,16 @@ export default async function launch(config) { }, websocket: buildHandler(config, state), }); + + addCloseHandler(state, () => { + state.browsers?.forEach((browser) => { + browser.proc.kill(); + // browser.proc.unref(); + }); + state.server.stop(); + // state.server.unref(); + }); + print(chalk.magenta(`🚀 Serving on ${chalk.white(protocol + '://' + hostname + ':')}${chalk.magenta(port)}`)); config.reporter.serverConfig = { port, @@ -66,6 +94,7 @@ export default async function launch(config) { if (config.setup) { debug(`Running configured setup hook`); + await config.setup({ port, hostname, @@ -73,15 +102,20 @@ export default async function launch(config) { }); debug(`Configured setup hook completed`); } + if (config.cleanup) { + addCloseHandler(state, async () => { + debug(`Running configured cleanup hook`); + await config.cleanup(); + debug(`Configured cleanup hook completed`); + }); + } - await launchBrowsers(config, state); + if (!config.noLaunch) { + await launchBrowsers(config, state); + } } catch (e) { error(`Error: ${e?.message ?? e}`); - if (config.cleanup) { - debug(`Running configured cleanup hook`); - await config.cleanup(); - debug(`Configured cleanup hook completed`); - } + await state.safeCleanup(); throw e; } } else { diff --git a/packages/diagnostic/server/launcher.html b/packages/diagnostic/server/launcher.html index cea14159dba..363e7f05bc9 100644 --- a/packages/diagnostic/server/launcher.html +++ b/packages/diagnostic/server/launcher.html @@ -2,6 +2,7 @@ @warp-drive/diagnostic Parallel Test Launcher + - + + {{content-for "body-footer"}} diff --git a/tests/performance/package.json b/tests/performance/package.json index 242841d1c68..ca872b19b6c 100644 --- a/tests/performance/package.json +++ b/tests/performance/package.json @@ -15,56 +15,69 @@ "test": "tests" }, "scripts": { - "build": "ember build", - "start": "ember serve", + "build": "vite build", + "start": "bun ./server/index.ts", "lint": "eslint . --quiet --cache --cache-strategy=content", - "sync-hardlinks": "bun run sync-dependencies-meta-injected" - }, - "dependencies": { - "ember-auto-import": "2.10.0", - "ember-data": "workspace:*", - "pnpm-sync-dependencies-meta-injected": "0.0.14", - "webpack": "^5.92.0" - }, - "dependenciesMeta": { - "ember-data": { - "injected": true - } + "fixtures": "bun run ./fixtures/index.js" }, "devDependencies": { - "@babel/core": "^7.24.5", - "@babel/runtime": "^7.24.5", - "@ember/optional-features": "^2.1.0", + "@babel/core": "^7.26.9", + "@babel/runtime": "^7.26.9", + "@ember/optional-features": "^2.2.0", + "@babel/plugin-transform-runtime": "^7.25.9", + "@babel/plugin-transform-typescript": "^7.25.9", "@ember/test-helpers": "5.1.0", "@ember/test-waiters": "^3.1.0", - "@embroider/compat": "^3.8.0", - "@embroider/core": "^3.5.0", - "@embroider/webpack": "^4.0.9", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/model": "workspace:*", + "@ember-data/legacy-compat": "workspace:*", + "@ember-data/graph": "workspace:*", + "@ember-data/json-api": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@warp-drive/core-types": "workspace:*", + "@warp-drive/build-config": "workspace:*", + "@warp-drive/internal-config": "workspace:*", + "@embroider/compat": "4.0.0-alpha.6", + "@embroider/config-meta-loader": "1.0.0-alpha.1", + "@embroider/core": "4.0.0-alpha.6", + "@embroider/vite": "1.0.0-alpha.6", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", - "@warp-drive/internal-config": "workspace:*", - "@warp-drive/build-config": "workspace:*", - "ember-cli": "~5.12.0", + "@rollup/plugin-babel": "^6.0.4", + "babel-plugin-ember-template-compilation": "^2.3.0", + "bun-types": "^1.2.3", + "decorator-transforms": "^2.3.0", + "ember-auto-import": "2.10.0", + "ember-cli": "~6.2.2", "ember-cli-babel": "^8.2.0", - "ember-cli-dependency-checker": "^3.3.2", "ember-cli-htmlbars": "^6.3.0", - "ember-load-initializers": "^2.1.2", - "ember-maybe-import-regenerator": "^1.0.0", - "ember-resolver": "^11.0.1", - "ember-source": "~5.12.0", + "ember-load-initializers": "^3.0.1", + "ember-resolver": "^13.1.0", + "ember-source": "~6.2.0", "loader.js": "^4.7.0", - "terser-webpack-plugin": "^5.3.11", - "webpack": "^5.92.0", + "terser": "^5.39.0", + "vite": "^5.4.11", + "vite-bundle-analyzer": "^0.17.1", + "vite-plugin-compression2": "^1.3.3", "zlib": "1.0.5" }, "ember": { "edition": "octane" }, "engines": { - "node": ">= 18.20.4" + "node": ">= 18.20.7" }, "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@8.15.9" + "packageManager": "pnpm@10.5.2", + "ember-addon": { + "type": "app", + "version": 2 + }, + "exports": { + "./*": "./app/*" + } } diff --git a/tests/performance/server/index.js b/tests/performance/server/index.js index 7b010421eb5..ca9d712ce55 100644 --- a/tests/performance/server/index.js +++ b/tests/performance/server/index.js @@ -27,6 +27,11 @@ module.exports = function (app) { fs.createReadStream(filePath).pipe(res); return; } + // console.log({ + // url: req.url, + // filePath: filePath, + // error: err, + // }); return res.status(404).end(); }); diff --git a/tests/performance/server/index.ts b/tests/performance/server/index.ts new file mode 100644 index 00000000000..63027d823d4 --- /dev/null +++ b/tests/performance/server/index.ts @@ -0,0 +1,70 @@ +import { styleText } from 'node:util'; +import { join } from 'path'; +const FIXTURES_LOCATION = join(__dirname, '../fixtures/generated'); + +const dist = join(__dirname, '../', process.argv[2] ?? 'dist'); +const port = process.argv[3] && process.argv[3] === '-p' ? (Number(process.argv[4]) ?? 9999) : 9999; +const host = `http://localhost:${port}`; + +Bun.serve({ + port, + async fetch(request) { + let filePath = ''; + let fileName = ''; + + if (request.url.includes('/fixtures/')) { + fileName = request.url.split('/fixtures')[1]; + filePath = join(FIXTURES_LOCATION, fileName + '.br'); + } else { + fileName = request.url.split(host)[1]; + filePath = join(dist, fileName === '/' ? '/index.html.br' : request.url.split(host)[1] + '.br'); + } + + let fileRef = Bun.file(filePath); + let exists = await fileRef.exists(); + if (!exists && (fileName === '/' || fileName.endsWith('.js') || fileName.endsWith('.css'))) { + return new Response('Not Found', { status: 404 }); + } else if (!exists) { + filePath = join(dist, '/index.html.br'); + fileRef = Bun.file(filePath); + exists = await fileRef.exists(); + } + + if (!exists) { + // console.log(styleText('red', `File not found: ${filePath}`)); + return new Response('Not Found', { status: 404 }); + } + + // prettier-ignore + const mimeType = filePath.endsWith('.html.br') ? 'text/html' + : filePath.endsWith('.js.br') ? 'application/javascript' + : filePath.endsWith('.css.br') ? 'text/css' + : filePath.endsWith('.json.br') ? 'application/json' + : filePath.endsWith('.svg.br') ? 'image/svg+xml' + : filePath.endsWith('.png.br') ? 'image/png' + : filePath.endsWith('.jpg.br') ? 'image/jpeg' + : 'text/plain'; + + const headers = new Headers(); + // We always compress and chunk the response + headers.set('Content-Encoding', 'br'); + headers.set('Transfer-Encoding', 'chunked'); + // we don't cache since tests will often reuse similar urls for different payload + headers.set('Cache-Control', 'max-age=31536000, public'); + // streaming requires Content-Length + headers.set('Content-Length', String(fileRef.size)); + headers.set('Content-Type', mimeType); + + // console.log(styleText('green', `\tServing: ${filePath}`)); + return new Response(fileRef, { + headers, + }); + }, +}); + +console.log( + styleText( + 'grey', + `Application running at ${styleText('yellow', 'http://') + styleText('cyan', 'localhost') + styleText('yellow', `:${port}`)}` + ) +); diff --git a/tests/performance/server/tsconfig.json b/tests/performance/server/tsconfig.json new file mode 100644 index 00000000000..adf6cde8940 --- /dev/null +++ b/tests/performance/server/tsconfig.json @@ -0,0 +1,16 @@ +{ + "include": ["index.ts"], + // how do I set this up where it only applies to + // this sub-directory? + "compilerOptions": { + "lib": ["esnext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "alwaysStrict": true, + "types": ["bun-types"] + } +} diff --git a/tests/performance/tests/index.html b/tests/performance/tests/index.html new file mode 100644 index 00000000000..18ecdcb795c --- /dev/null +++ b/tests/performance/tests/index.html @@ -0,0 +1 @@ + diff --git a/tests/performance/tsconfig.json b/tests/performance/tsconfig.json new file mode 100644 index 00000000000..2a5833493c4 --- /dev/null +++ b/tests/performance/tsconfig.json @@ -0,0 +1,74 @@ +{ + "include": ["app/**/*", "config/**/*", "tests/**/*", "types/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "baseUrl": ".", + "noImplicitOverride": false, + "experimentalDecorators": true, + "incremental": true, + "noEmit": true, + "declaration": false, + "types": ["ember-source/types"], + "paths": { + "@ember-data/request": ["../../packages/request/unstable-preview-types"], + "@ember-data/request/*": ["../../packages/request/unstable-preview-types/*"], + "@ember-data/store": ["../../packages/store/unstable-preview-types"], + "@ember-data/store/*": ["../../packages/store/unstable-preview-types/*"], + "@ember-data/json-api": ["../../packages/json-api/unstable-preview-types"], + "@ember-data/json-api/*": ["../../packages/json-api/unstable-preview-types/*"], + "@ember-data/model": ["../../packages/model/unstable-preview-types"], + "@ember-data/model/*": ["../../packages/model/unstable-preview-types/*"], + "@ember-data/tracking": ["../../packages/tracking/unstable-preview-types"], + "@ember-data/tracking/*": ["../../packages/tracking/unstable-preview-types/*"], + "@warp-drive/core-types": ["../../packages/core-types/unstable-preview-types"], + "@warp-drive/core-types/*": ["../../packages/core-types/unstable-preview-types/*"], + "@warp-drive/build-config": ["../../packages/build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../../packages/build-config/unstable-preview-types/*"], + "@ember-data/legacy-compat": ["../../packages/legacy-compat/unstable-preview-types"], + "@ember-data/legacy-compat/*": ["../../packages/legacy-compat/unstable-preview-types/*"], + "@ember-data/request-utils": ["../../packages/request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../../packages/request-utils/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../../packages/request" + }, + { + "path": "../../packages/store" + }, + { + "path": "../../packages/json-api" + }, + { + "path": "../../packages/model" + }, + { + "path": "../../packages/tracking" + }, + { + "path": "../../packages/core-types" + }, + { + "path": "../../packages/build-config" + }, + { + "path": "../../packages/legacy-compat" + }, + { + "path": "../../packages/request-utils" + } + ] +} diff --git a/tests/performance/vite.config.mjs b/tests/performance/vite.config.mjs new file mode 100644 index 00000000000..1090ae7500f --- /dev/null +++ b/tests/performance/vite.config.mjs @@ -0,0 +1,86 @@ +import { defineConfig } from 'vite'; +import { extensions, classicEmberSupport, ember } from '@embroider/vite'; +import { babel } from '@rollup/plugin-babel'; +import { compression } from 'vite-plugin-compression2'; +// import { analyzer } from 'vite-bundle-analyzer'; + +import zlib from 'zlib'; + +export default defineConfig({ + plugins: [ + classicEmberSupport(), + ember(), + // extra plugins here + babel({ + babelHelpers: 'runtime', + extensions, + }), + // analyzer(), + compression({ + algorithm: 'brotliCompress', + compressionOptions: { + params: { + [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT, + // brotli currently defaults to 11 but lets be explicit + [zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY, + }, + }, + deleteOriginalAssets: true, + }), + ], + server: { + proxy: { + '/fixtures': `http://localhost:${process.env.FIXTURE_API_PORT || '9999'}`, + }, + }, + mode: 'production', + build: { + minify: 'terser', + reportCompressedSize: false, + rollupOptions: { + output: { + manualChunks(id) { + if (id.includes('@ember-data/model')) return 'warp-drive-legacy'; + if (id.includes('@ember-data/legacy-compat')) return 'warp-drive-legacy'; + if (id.includes('@ember-data')) return 'warp-drive'; + if (id.includes('@warp-drive')) return 'warp-drive'; + if (id.includes('@ember')) return 'ember'; + if (id.includes('ember-source')) return 'ember'; + if (id.includes('@glimmer')) return 'ember'; + if (id.includes('node_modules')) return 'vendor'; + return null; + }, + }, + }, + terserOptions: { + compress: { + ecma: 2024, + passes: 6, // slow, but worth it + negate_iife: false, + sequences: 30, + defaults: true, + drop_debugger: false, + arguments: false, + keep_fargs: false, + toplevel: false, + unsafe: true, + unsafe_comps: true, + unsafe_math: true, + unsafe_symbols: true, + unsafe_proto: true, + unsafe_undefined: true, + inline: 5, + reduce_funcs: false, + }, + mangle: { + keep_classnames: true, + keep_fnames: true, + module: true, + }, + format: { beautify: true }, + toplevel: false, + sourceMap: false, + ecma: 2024, + }, + }, +}); diff --git a/tests/smoke-tests/dt-types/package.json b/tests/smoke-tests/dt-types/package.json index 1406098ce68..7c24b969894 100644 --- a/tests/smoke-tests/dt-types/package.json +++ b/tests/smoke-tests/dt-types/package.json @@ -64,11 +64,10 @@ "@ember/string": "^4.0.0", "@ember/test-helpers": "5.1.0", "@ember/test-waiters": "^3.1.0", - "@embroider/compat": "3.7.1-unstable.4070ba7", - "@embroider/config-meta-loader": "0.0.1-unstable.4070ba7", - "@embroider/core": "3.4.20-unstable.4070ba7", - "@embroider/test-setup": "4.0.1-unstable.4070ba7", - "@embroider/vite": "0.2.2-unstable.4070ba7", + "@embroider/compat": "4.0.1-unstable.2aa8cae", + "@embroider/config-meta-loader": "1.0.1-unstable.2aa8cae", + "@embroider/core": "4.0.1-unstable.2aa8cae", + "@embroider/vite": "1.0.1-unstable.2aa8cae", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", "@glint/core": "1.5.0", diff --git a/tests/smoke-tests/native-types/package.json b/tests/smoke-tests/native-types/package.json index 53f36027e97..7457e3c49b3 100644 --- a/tests/smoke-tests/native-types/package.json +++ b/tests/smoke-tests/native-types/package.json @@ -19,8 +19,7 @@ "lint:js": "eslint . --cache", "lint:js:fix": "eslint . --fix", "start": "vite", - "test:vite": "vite build --mode test && ember test --path dist", - "sync-hardlinks": "bun run sync-dependencies-meta-injected" + "test:vite": "vite build --mode test && ember test --path dist" }, "devDependencies": { "@babel/core": "^7.24.5", @@ -47,11 +46,10 @@ "@ember/string": "^4.0.0", "@ember/test-helpers": "5.1.0", "@ember/test-waiters": "^3.1.0", - "@embroider/compat": "^3.8.0", - "@embroider/config-meta-loader": "0.0.1-unstable.4070ba7", - "@embroider/core": "^3.5.0", - "@embroider/test-setup": "4.0.1-unstable.4070ba7", - "@embroider/vite": "0.2.2-unstable.4070ba7", + "@embroider/compat": "4.0.1-unstable.2aa8cae", + "@embroider/config-meta-loader": "1.0.1-unstable.2aa8cae", + "@embroider/core": "4.0.1-unstable.2aa8cae", + "@embroider/vite": "1.0.1-unstable.2aa8cae", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", "@glint/core": "1.5.0", @@ -87,7 +85,6 @@ "expect-type": "^0.20.0", "globals": "^15.12.0", "loader.js": "^4.7.0", - "pnpm-sync-dependencies-meta-injected": "0.0.14", "prettier": "^3.3.3", "prettier-plugin-ember-template-tag": "^2.0.4", "qunit": "^2.22.0", diff --git a/tests/vite-basic-compat/ember-cli-build.js b/tests/vite-basic-compat/ember-cli-build.js index ebb76e53a57..d50a2c56ea4 100644 --- a/tests/vite-basic-compat/ember-cli-build.js +++ b/tests/vite-basic-compat/ember-cli-build.js @@ -1,10 +1,11 @@ 'use strict'; const EmberApp = require('ember-cli/lib/broccoli/ember-app'); -const { maybeEmbroider } = require('@embroider/test-setup'); +const { compatBuild } = require('@embroider/compat'); -module.exports = function (defaults) { - let app = new EmberApp(defaults, {}); +module.exports = async function (defaults) { + const { buildOnce } = await import('@embroider/vite'); + const app = new EmberApp(defaults, {}); - return maybeEmbroider(app); + return compatBuild(app, buildOnce); }; diff --git a/tests/vite-basic-compat/package.json b/tests/vite-basic-compat/package.json index 3fe2ee6cafa..e3a47636291 100644 --- a/tests/vite-basic-compat/package.json +++ b/tests/vite-basic-compat/package.json @@ -19,62 +19,14 @@ "lint:js": "eslint . --cache", "lint:js:fix": "eslint . --fix", "start": "vite", - "test:vite": "vite build --mode test && ember test --path dist", - "sync-hardlinks": "bun run sync-dependencies-meta-injected" - }, - "dependenciesMeta": { - "ember-data": { - "injected": true - }, - "@ember-data/tracking": { - "injected": true - }, - "@ember-data/store": { - "injected": true - }, - "@ember-data/request": { - "injected": true - }, - "@ember-data/adapter": { - "injected": true - }, - "@ember-data/graph": { - "injected": true - }, - "@ember-data/debug": { - "injected": true - }, - "@ember-data/model": { - "injected": true - }, - "@ember-data/json-api": { - "injected": true - }, - "@ember-data/request-utils": { - "injected": true - }, - "@ember-data/legacy-compat": { - "injected": true - }, - "@ember-data/serializer": { - "injected": true - }, - "@ember-data/unpublished-test-infra": { - "injected": true - }, - "@warp-drive/core-types": { - "injected": true - }, - "@warp-drive/build-config": { - "injected": true - } + "test:vite": "vite build --mode test && ember test --path dist" }, "devDependencies": { - "@babel/core": "^7.24.5", - "@babel/eslint-parser": "^7.25.9", - "@babel/plugin-transform-runtime": "^7.25.9", - "@babel/plugin-transform-typescript": "^7.25.9", - "@babel/runtime": "^7.26.0", + "@babel/core": "^7.26.9", + "@babel/eslint-parser": "^7.26.8", + "@babel/plugin-transform-runtime": "^7.26.9", + "@babel/plugin-transform-typescript": "^7.26.8", + "@babel/runtime": "^7.26.9", "@ember-data/adapter": "workspace:*", "@ember-data/debug": "workspace:*", "@ember-data/graph": "workspace:*", @@ -87,15 +39,14 @@ "@ember-data/store": "workspace:*", "@ember-data/tracking": "workspace:*", "@ember-data/unpublished-test-infra": "workspace:*", - "@ember/optional-features": "^2.1.0", - "@ember/string": "^4.0.0", + "@ember/optional-features": "^2.2.0", + "@ember/string": "^4.0.1", "@ember/test-helpers": "5.1.0", "@ember/test-waiters": "^3.1.0", - "@embroider/compat": "3.7.1-unstable.4070ba7", - "@embroider/config-meta-loader": "0.0.1-unstable.4070ba7", - "@embroider/core": "3.4.20-unstable.4070ba7", - "@embroider/test-setup": "4.0.1-unstable.4070ba7", - "@embroider/vite": "0.2.2-unstable.4070ba7", + "@embroider/compat": "4.0.1-unstable.f40acf0", + "@embroider/config-meta-loader": "1.0.1-unstable.9f1e74e", + "@embroider/core": "4.0.1-unstable.f40acf0", + "@embroider/vite": "1.0.1-unstable.f40acf0", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", "@rollup/plugin-babel": "^6.0.4", @@ -103,8 +54,8 @@ "@types/eslint__js": "^8.42.3", "@types/qunit": "2.19.10", "@types/rsvp": "^4.0.9", - "@typescript-eslint/eslint-plugin": "^8.14.0", - "@typescript-eslint/parser": "^8.14.0", + "@typescript-eslint/eslint-plugin": "^8.25.0", + "@typescript-eslint/parser": "^8.25.0", "@warp-drive/build-config": "workspace:*", "@warp-drive/core-types": "workspace:*", "@warp-drive/internal-config": "workspace:*", @@ -112,40 +63,38 @@ "concurrently": "^9.1.2", "decorator-transforms": "^2.3.0", "ember-auto-import": "2.10.0", - "ember-cli": "~5.12.0", + "ember-cli": "~6.2.2", "ember-cli-babel": "^8.2.0", "ember-cli-htmlbars": "^6.3.0", "ember-data": "workspace:*", "ember-load-initializers": "^3.0.1", "ember-modifier": "^4.2.0", - "ember-page-title": "^8.2.3", + "ember-page-title": "^8.2.4", "ember-qunit": "9.0.1", "ember-resolver": "^13.1.0", "ember-route-template": "^1.0.3", "ember-source": "~5.12.0", - "ember-template-lint": "^6.0.0", + "ember-template-lint": "^6.1.0", "ember-welcome-page": "^7.0.2", - "eslint": "^9.14.0", + "eslint": "^9.21.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-ember": "^12.3.1", - "eslint-plugin-n": "^17.13.1", - "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-ember": "^12.5.0", + "eslint-plugin-n": "^17.15.1", + "eslint-plugin-prettier": "^5.2.3", "eslint-plugin-qunit": "^8.1.2", - "globals": "^15.12.0", + "globals": "^16.0.0", "loader.js": "^4.7.0", - "pnpm-sync-dependencies-meta-injected": "0.0.14", - "prettier": "^3.3.3", + "prettier": "^3.5.2", "prettier-plugin-ember-template-tag": "^2.0.4", "qunit": "^2.22.0", "qunit-dom": "^3.3.0", - "tracked-built-ins": "^3.3.0", - "typescript": "^5.7.2", - "typescript-eslint": "^8.13.0", - "vite": "^5.4.11", - "webpack": "^5.95.0" + "tracked-built-ins": "^3.4.0", + "typescript": "^5.8.2", + "typescript-eslint": "^8.25.0", + "vite": "^5.4.14" }, "engines": { - "node": ">= 18" + "node": ">= 18.20.7" }, "ember": { "edition": "octane" diff --git a/tests/warp-drive__ember/.mock-cache/73ae9a7c/GET-0-users/1/res.body.br b/tests/warp-drive__ember/.mock-cache/73ae9a7c/GET-0-users/1/res.body.br new file mode 100644 index 00000000000..95a61de6c7d Binary files /dev/null and b/tests/warp-drive__ember/.mock-cache/73ae9a7c/GET-0-users/1/res.body.br differ diff --git a/tests/warp-drive__ember/.mock-cache/73ae9a7c/GET-0-users/1/res.meta.json b/tests/warp-drive__ember/.mock-cache/73ae9a7c/GET-0-users/1/res.meta.json new file mode 100644 index 00000000000..8e7c392cedc --- /dev/null +++ b/tests/warp-drive__ember/.mock-cache/73ae9a7c/GET-0-users/1/res.meta.json @@ -0,0 +1 @@ +{"url":"users/1","status":200,"statusText":"OK","headers":{"Content-Type":"application/vnd.api+json","Content-Encoding":"br","Cache-Control":"no-store","Content-Length":67},"method":"GET","requestBody":null} \ No newline at end of file diff --git a/tests/warp-drive__ember/.mock-cache/73ae9a7c/GET-1-users/1/res.body.br b/tests/warp-drive__ember/.mock-cache/73ae9a7c/GET-1-users/1/res.body.br new file mode 100644 index 00000000000..95a61de6c7d Binary files /dev/null and b/tests/warp-drive__ember/.mock-cache/73ae9a7c/GET-1-users/1/res.body.br differ diff --git a/tests/warp-drive__ember/.mock-cache/73ae9a7c/GET-1-users/1/res.meta.json b/tests/warp-drive__ember/.mock-cache/73ae9a7c/GET-1-users/1/res.meta.json new file mode 100644 index 00000000000..8e7c392cedc --- /dev/null +++ b/tests/warp-drive__ember/.mock-cache/73ae9a7c/GET-1-users/1/res.meta.json @@ -0,0 +1 @@ +{"url":"users/1","status":200,"statusText":"OK","headers":{"Content-Type":"application/vnd.api+json","Content-Encoding":"br","Cache-Control":"no-store","Content-Length":67},"method":"GET","requestBody":null} \ No newline at end of file diff --git a/tests/warp-drive__schema-record/tests/edit-workflow-test.ts b/tests/warp-drive__schema-record/tests/edit-workflow-test.ts new file mode 100644 index 00000000000..4522733c637 --- /dev/null +++ b/tests/warp-drive__schema-record/tests/edit-workflow-test.ts @@ -0,0 +1,149 @@ +import { module, test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import { serializePatch, serializeResources, updateRecord } from '@ember-data/json-api/request'; +import RequestManager from '@ember-data/request'; +import { CacheHandler, recordIdentifierFor } from '@ember-data/store'; +import type { Type } from '@warp-drive/core-types/symbols'; +import { Checkout, registerDerivations, withDefaults } from '@warp-drive/schema-record'; + +import type Store from '../app/services/store'; + +const UserSchema = withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + ], +}); +type User = Readonly<{ + id: string; + name: string; + $type: 'user'; + [Type]: 'user'; + [Checkout](): Promise; +}>; + +type EditableUser = { + readonly id: string; + name: string; + readonly $type: 'user'; + readonly [Type]: 'user'; +}; + +module('WarpDrive | SchemaRecord | Edit Workflow', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function (assert) { + const store = this.owner.lookup('service:store') as Store; + store.schema.registerResource(UserSchema); + registerDerivations(store.schema); + store.requestManager = new RequestManager() + .use([ + { + request({ request }) { + const { url, method, body } = request; + assert.step(`${method} ${url}`); + return Promise.resolve(JSON.parse(body as string)); + }, + }, + ]) + .useCache(CacheHandler); + }); + + test('we can edit a record', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { name: 'Rey Skybarker' }, + }, + }); + const editableUser = await user[Checkout](); + assert.strictEqual(editableUser.name, 'Rey Skybarker', 'name is accessible'); + editableUser.name = 'Rey Skywalker'; + assert.strictEqual(editableUser.name, 'Rey Skywalker', 'name is updated'); + assert.strictEqual(user.name, 'Rey Skybarker', 'immutable record shows original value'); + + // ensure identifier works as expected + const identifier = recordIdentifierFor(editableUser); + assert.strictEqual(identifier.id, '1', 'id is accessible'); + assert.strictEqual(identifier.type, 'user', 'type is accessible'); + + // ensure save works as expected + const saveInit = updateRecord(editableUser); + const patch = serializePatch(store.cache, recordIdentifierFor(editableUser)); + saveInit.body = JSON.stringify(patch); + + const saveResult = await store.request(saveInit); + assert.deepEqual(saveResult.content.data, user, 'we get the immutable version back from the request'); + assert.verifySteps(['PUT /users/1']); + assert.strictEqual(user.name, 'Rey Skywalker', 'name is updated in the cache and shows in the immutable record'); + }); + + test('we can serialize an editable record', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { name: 'Rey Skybarker' }, + }, + }); + const editableUser = await user[Checkout](); + assert.strictEqual(editableUser.name, 'Rey Skybarker', 'name is accessible'); + editableUser.name = 'Rey Skywalker'; + assert.strictEqual(editableUser.name, 'Rey Skywalker', 'name is updated'); + assert.strictEqual(user.name, 'Rey Skybarker', 'immutable record shows original value'); + + // ensure identifier works as expected + const identifier = recordIdentifierFor(editableUser); + assert.strictEqual(identifier.id, '1', 'id is accessible'); + assert.strictEqual(identifier.type, 'user', 'type is accessible'); + + // ensure save works as expected + const saveInit = updateRecord(editableUser); + const body = serializeResources(store.cache, saveInit.data.record); + saveInit.body = JSON.stringify(body); + + const saveResult = await store.request(saveInit); + assert.deepEqual(saveResult.content.data, user, 'we get the immutable version back from the request'); + assert.verifySteps(['PUT /users/1']); + assert.strictEqual(user.name, 'Rey Skywalker', 'name is updated in the cache and shows in the immutable record'); + }); + + test('serializing the immutable record serializes the edits', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { name: 'Rey Skybarker' }, + }, + }); + const editableUser = await user[Checkout](); + assert.strictEqual(editableUser.name, 'Rey Skybarker', 'name is accessible'); + editableUser.name = 'Rey Skywalker'; + assert.strictEqual(editableUser.name, 'Rey Skywalker', 'name is updated'); + assert.strictEqual(user.name, 'Rey Skybarker', 'immutable record shows original value'); + + // ensure identifier works as expected + const identifier = recordIdentifierFor(editableUser); + assert.strictEqual(identifier.id, '1', 'id is accessible'); + assert.strictEqual(identifier.type, 'user', 'type is accessible'); + + // ensure save works as expected + const saveInit = updateRecord(user); + const body = serializeResources(store.cache, saveInit.data.record); + saveInit.body = JSON.stringify(body); + + const saveResult = await store.request(saveInit); + assert.deepEqual(saveResult.content.data, user, 'we get the immutable version back from the request'); + assert.verifySteps(['PUT /users/1']); + assert.strictEqual(user.name, 'Rey Skywalker', 'name is updated in the cache and shows in the immutable record'); + }); +}); diff --git a/tests/warp-drive__schema-record/tests/reads/belongs-to-test.ts b/tests/warp-drive__schema-record/tests/reads/belongs-to-test.ts index e80b534df28..9b370e7496e 100644 --- a/tests/warp-drive__schema-record/tests/reads/belongs-to-test.ts +++ b/tests/warp-drive__schema-record/tests/reads/belongs-to-test.ts @@ -6,7 +6,7 @@ import { setupTest } from 'ember-qunit'; import type Store from '@ember-data/store'; import type { Type } from '@warp-drive/core-types/symbols'; -import { registerDerivations, withDefaults } from '@warp-drive/schema-record/schema'; +import { registerDerivations, withDefaults } from '@warp-drive/schema-record'; type User = { id: string | null; diff --git a/tests/warp-drive__schema-record/tests/reads/has-many-test.ts b/tests/warp-drive__schema-record/tests/reads/has-many-test.ts new file mode 100644 index 00000000000..7e3b308fa14 --- /dev/null +++ b/tests/warp-drive__schema-record/tests/reads/has-many-test.ts @@ -0,0 +1,919 @@ +import type { TestContext } from '@ember/test-helpers'; + +import { module, skip, test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import RequestManager from '@ember-data/request'; +import type { Context } from '@ember-data/request/-private/context'; +import type Store from '@ember-data/store'; +import { CacheHandler } from '@ember-data/store'; +import type { RelatedCollection } from '@ember-data/store/-private'; +import type { Type } from '@warp-drive/core-types/symbols'; +import { registerDerivations, withDefaults } from '@warp-drive/schema-record'; + +type User = { + id: string | null; + $type: 'user'; + name: string; + friends: User[] | null; + [Type]: 'user'; +}; + +module('Reads | hasMany in linksMode', function (hooks) { + setupTest(hooks); + + test('we can use sync hasMany in linksMode', function (this: TestContext, assert) { + const store = this.owner.lookup('service:store') as Store; + const { schema } = store; + + registerDerivations(schema); + + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'friends', + type: 'user', + kind: 'hasMany', + options: { inverse: 'friends', async: false, linksMode: true }, + }, + ], + }) + ); + + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Leo', + }, + relationships: { + friends: { + links: { related: '/user/1/friends' }, + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + ], + }, + }, + }, + included: [ + { + type: 'user', + id: '2', + attributes: { + name: 'Benedikt', + }, + relationships: { + friends: { + links: { related: '/user/2/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + { + type: 'user', + id: '3', + attributes: { + name: 'Jane', + }, + relationships: { + friends: { + links: { related: '/user/3/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + ], + }); + + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.$type, 'user', '$type is accessible'); + assert.strictEqual(record.name, 'Leo', 'name is accessible'); + assert.true(record.friends instanceof Array, 'Friends is an instance of Array'); + assert.true(Array.isArray(record.friends), 'Friends is an array'); + assert.strictEqual(record.friends?.length, 2, 'friends has 2 items'); + assert.strictEqual(record.friends?.[0].id, '2', 'friends[0].id is accessible'); + assert.strictEqual(record.friends?.[0].$type, 'user', 'friends[0].user is accessible'); + assert.strictEqual(record.friends?.[0].name, 'Benedikt', 'friends[0].name is accessible'); + assert.strictEqual(record.friends?.[0].friends?.[0].id, record.id, 'friends is reciprocal'); + }); + + test('we can update sync hasMany in linksMode', function (this: TestContext, assert) { + const store = this.owner.lookup('service:store') as Store; + const { schema } = store; + + registerDerivations(schema); + + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'attribute', + }, + { + name: 'friends', + type: 'user', + kind: 'hasMany', + options: { inverse: 'friends', async: false, linksMode: true }, + }, + ], + }) + ); + + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Leo', + }, + relationships: { + friends: { + links: { related: '/user/1/friends' }, + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + ], + }, + }, + }, + included: [ + { + type: 'user', + id: '2', + attributes: { + name: 'Benedikt', + }, + relationships: { + friends: { + links: { related: '/user/2/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + { + type: 'user', + id: '3', + attributes: { + name: 'Jane', + }, + relationships: { + friends: { + links: { related: '/user/3/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + ], + }); + + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.name, 'Leo', 'name is accessible'); + assert.strictEqual(record.friends?.length, 2, 'friends.length is accessible'); + assert.strictEqual(record.friends?.[0]?.id, '2', 'friends[0].id is accessible'); + assert.strictEqual(record.friends?.[0]?.name, 'Benedikt', 'friends[0].name is accessible'); + + store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Leo', + }, + relationships: { + friends: { + links: { related: '/user/1/friends' }, + data: [{ type: 'user', id: '3' }], + }, + }, + }, + included: [ + { + type: 'user', + id: '3', + attributes: { + name: 'Jane', + }, + relationships: { + friends: { + links: { related: '/user/3/friends' }, + data: [ + { type: 'user', id: '1' }, + { type: 'user', id: '2' }, + ], + }, + }, + }, + ], + }); + + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.name, 'Leo', 'name is accessible'); + assert.strictEqual(record.friends?.length, 1, 'friends.length is accessible'); + assert.strictEqual(record.friends?.[0]?.id, '3', 'friends[0].id is accessible'); + assert.strictEqual(record.friends?.[0]?.name, 'Jane', 'friends[0].name is accessible'); + assert.strictEqual(record.friends?.[0]?.friends?.length, 2, 'friends[0].friends.length is accessible'); + assert.strictEqual(record.friends?.[0]?.friends?.[0].id, '1', 'friends[0].friends[0].id is accessible'); + assert.strictEqual(record.friends?.[0]?.friends?.[0].name, 'Leo', 'friends[0].friends[0].name is accessible'); + }); + + test('we can update sync hasMany in linksMode with the same data in a different order', function (this: TestContext, assert) { + const store = this.owner.lookup('service:store') as Store; + const { schema } = store; + + registerDerivations(schema); + + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'attribute', + }, + { + name: 'friends', + type: 'user', + kind: 'hasMany', + options: { inverse: 'friends', async: false, linksMode: true }, + }, + ], + }) + ); + + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Leo', + }, + relationships: { + friends: { + links: { related: '/user/1/friends' }, + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + { type: 'user', id: '4' }, + ], + }, + }, + }, + included: [ + { + type: 'user', + id: '2', + attributes: { + name: 'Benedikt', + }, + relationships: { + friends: { + links: { related: '/user/2/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + { + type: 'user', + id: '3', + attributes: { + name: 'Jane', + }, + relationships: { + friends: { + links: { related: '/user/3/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + { + type: 'user', + id: '4', + attributes: { + name: 'Michael', + }, + relationships: { + friends: { + links: { related: '/user/4/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + ], + }); + + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.name, 'Leo', 'name is accessible'); + assert.strictEqual(record.friends?.length, 3, 'friends.length is accessible'); + assert.strictEqual(record.friends?.[0]?.id, '2', 'friends[0].id is accessible'); + assert.strictEqual(record.friends?.[0]?.name, 'Benedikt', 'friends[0].name is accessible'); + + store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Leo', + }, + relationships: { + friends: { + links: { related: '/user/1/friends' }, + data: [ + { type: 'user', id: '4' }, + { type: 'user', id: '3' }, + { type: 'user', id: '2' }, + ], + }, + }, + }, + included: [ + { + type: 'user', + id: '4', + attributes: { + name: 'Michael', + }, + relationships: { + friends: { + links: { related: '/user/4/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + { + type: 'user', + id: '3', + attributes: { + name: 'Jane', + }, + relationships: { + friends: { + links: { related: '/user/3/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + { + type: 'user', + id: '2', + attributes: { + name: 'Benedikt', + }, + relationships: { + friends: { + links: { related: '/user/2/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + ], + }); + + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.name, 'Leo', 'name is accessible'); + assert.strictEqual(record.friends?.length, 3, 'friends.length is accessible'); + assert.strictEqual(record.friends?.[0]?.id, '4', 'friends[0].id is accessible'); + assert.strictEqual(record.friends?.[0]?.name, 'Michael', 'friends[0].name is accessible'); + assert.strictEqual(record.friends?.[0]?.friends?.length, 1, 'friends[0].friends.length is accessible'); + assert.strictEqual(record.friends?.[0]?.friends?.[0].id, '1', 'friends[0].friends[0].id is accessible'); + assert.strictEqual(record.friends?.[0]?.friends?.[0].name, 'Leo', 'friends[0].friends[0].name is accessible'); + assert.strictEqual(record.friends?.[1]?.id, '3', 'friends[0].id is accessible'); + assert.strictEqual(record.friends?.[1]?.name, 'Jane', 'friends[0].name is accessible'); + assert.strictEqual(record.friends?.[1]?.friends?.length, 1, 'friends[0].friends.length is accessible'); + assert.strictEqual(record.friends?.[1]?.friends?.[0].id, '1', 'friends[0].friends[0].id is accessible'); + assert.strictEqual(record.friends?.[1]?.friends?.[0].name, 'Leo', 'friends[0].friends[0].name is accessible'); + }); + + test('we can reload a sync hasMany in linksMode, removing an item', async function (this: TestContext, assert) { + const handler = { + request(): Promise { + return Promise.resolve({ + data: [ + { + type: 'user', + id: '4', + attributes: { + name: 'Michael', + }, + relationships: { + friends: { + links: { related: '/user/4/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + ], + links: { self: '/user/1/friends' }, + } as T); + }, + }; + + const store = this.owner.lookup('service:store') as Store; + const requestManager = new RequestManager(); + requestManager.use([handler]); + requestManager.useCache(CacheHandler); + store.requestManager = requestManager; + const { schema } = store; + + registerDerivations(schema); + + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'attribute', + }, + { + name: 'friends', + type: 'user', + kind: 'hasMany', + options: { inverse: 'friends', async: false, linksMode: true }, + }, + ], + }) + ); + + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Leo', + }, + relationships: { + friends: { + links: { related: '/user/1/friends' }, + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + ], + }, + }, + }, + included: [ + { + type: 'user', + id: '2', + attributes: { + name: 'Benedikt', + }, + relationships: { + friends: { + links: { related: '/user/2/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + { + type: 'user', + id: '3', + attributes: { + name: 'Jane', + }, + relationships: { + friends: { + links: { related: '/user/3/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + ], + }); + + assert.strictEqual(record.friends?.length, 2, 'the user has 2 friends'); + + await (record.friends as RelatedCollection).reload(); + + assert.strictEqual(record.friends?.length, 1, 'the user has 1 friend after a reload'); + assert.strictEqual(record.friends?.[0]?.id, '4', 'friends[0].id is accessible'); + assert.strictEqual(record.friends?.[0]?.name, 'Michael', 'friends[0].name is accessible'); + }); + + test('we can reload a sync hasMany in linksMode, adding an item', async function (this: TestContext, assert) { + const handler = { + request(): Promise { + return Promise.resolve({ + data: [ + { + type: 'user', + id: '2', + attributes: { + name: 'Benedikt', + }, + relationships: { + friends: { + links: { related: '/user/2/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + { + type: 'user', + id: '3', + attributes: { + name: 'Jane', + }, + relationships: { + friends: { + links: { related: '/user/3/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + ], + links: { self: '/user/1/friends' }, + } as T); + }, + }; + + const store = this.owner.lookup('service:store') as Store; + const requestManager = new RequestManager(); + requestManager.use([handler]); + requestManager.useCache(CacheHandler); + store.requestManager = requestManager; + const { schema } = store; + + registerDerivations(schema); + + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'attribute', + }, + { + name: 'friends', + type: 'user', + kind: 'hasMany', + options: { inverse: 'friends', async: false, linksMode: true }, + }, + ], + }) + ); + + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Leo', + }, + relationships: { + friends: { + links: { related: '/user/1/friends' }, + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + ], + }, + }, + }, + included: [ + { + type: 'user', + id: '2', + attributes: { + name: 'Benedikt', + }, + relationships: { + friends: { + links: { related: '/user/2/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + ], + }); + + assert.strictEqual(record.friends?.length, 2, 'the user has 2 friends'); + + await (record.friends as RelatedCollection).reload(); + + assert.strictEqual(record.friends?.length, 2, 'the user has 1 friend after a reload'); + assert.strictEqual(record.friends?.[0]?.id, '2', 'friends[0].id is accessible'); + assert.strictEqual(record.friends?.[0]?.name, 'Benedikt', 'friends[0].name is accessible'); + assert.strictEqual(record.friends?.[1]?.id, '3', 'friends[1].id is accessible'); + assert.strictEqual(record.friends?.[1]?.name, 'Jane', 'friends[1].name is accessible'); + }); + + test('we can reload a sync hasMany in linksMode, for a new set of records', async function (this: TestContext, assert) { + const handler = { + request(): Promise { + return Promise.resolve({ + data: [ + { + type: 'user', + id: '4', + attributes: { + name: 'Michael', + }, + relationships: { + friends: { + links: { related: '/user/4/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + ], + links: { self: '/user/1/friends' }, + } as T); + }, + }; + + const store = this.owner.lookup('service:store') as Store; + const requestManager = new RequestManager(); + requestManager.use([handler]); + requestManager.useCache(CacheHandler); + store.requestManager = requestManager; + const { schema } = store; + + registerDerivations(schema); + + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'attribute', + }, + { + name: 'friends', + type: 'user', + kind: 'hasMany', + options: { inverse: 'friends', async: false, linksMode: true }, + }, + ], + }) + ); + + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Leo', + }, + relationships: { + friends: { + links: { related: '/user/1/friends' }, + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + ], + }, + }, + }, + included: [ + { + type: 'user', + id: '2', + attributes: { + name: 'Benedikt', + }, + relationships: { + friends: { + links: { related: '/user/2/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + { + type: 'user', + id: '3', + attributes: { + name: 'Jane', + }, + relationships: { + friends: { + links: { related: '/user/3/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + ], + }); + + assert.strictEqual(record.friends?.length, 2, 'the user has 2 friends'); + + await (record.friends as RelatedCollection).reload(); + + assert.strictEqual(record.friends?.length, 1, 'the user has 1 friend after a reload'); + assert.strictEqual(record.friends?.[0]?.id, '4', 'friends[0].id is accessible'); + assert.strictEqual(record.friends?.[0]?.name, 'Michael', 'friends[0].name is accessible'); + }); + + test('we have refenrece stability on sync hasMany in linksMode', async function (this: TestContext, assert) { + const handler = { + request(context: Context): Promise { + return Promise.resolve({ + data: [ + { + type: 'user', + id: '2', + attributes: { + name: 'Benedikt', + }, + relationships: { + friends: { + links: { related: '/user/2/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + { + type: 'user', + id: '3', + attributes: { + name: 'Jane', + }, + relationships: { + friends: { + links: { related: '/user/3/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + ], + } as T); + }, + }; + + const store = this.owner.lookup('service:store') as Store; + const requestManager = new RequestManager(); + requestManager.use([handler]); + requestManager.useCache(CacheHandler); + store.requestManager = requestManager; + const { schema } = store; + + registerDerivations(schema); + + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'attribute', + }, + { + name: 'friends', + type: 'user', + kind: 'hasMany', + options: { inverse: 'friends', async: false, linksMode: true }, + }, + ], + }) + ); + + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Leo', + }, + relationships: { + friends: { + links: { related: '/user/1/friends' }, + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + ], + }, + }, + }, + included: [ + { + type: 'user', + id: '2', + attributes: { + name: 'Benedikt', + }, + relationships: { + friends: { + links: { related: '/user/2/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + { + type: 'user', + id: '3', + attributes: { + name: 'Jane', + }, + relationships: { + friends: { + links: { related: '/user/3/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + ], + }); + + const friends = record.friends; + + assert.strictEqual(friends, record.friends, 'the friends relationship is stable'); + + await (record.friends as RelatedCollection).reload(); + + assert.strictEqual(friends, record.friends, 'the friends relationship is stable after reload'); + }); + + skip('we error for async hasMany access in linksMode because we are not implemented yet', function (this: TestContext, assert) { + const store = this.owner.lookup('service:store') as Store; + const { schema } = store; + + registerDerivations(schema); + + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'friends', + type: 'user', + kind: 'hasMany', + options: { inverse: 'friends', async: true, linksMode: true }, + }, + ], + }) + ); + + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Leo', + }, + relationships: { + friends: { + links: { related: '/user/1/friends' }, + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + ], + }, + }, + }, + included: [ + // NOTE: If this is included, we can assume the link is pre-fetched + { + type: 'user', + id: '2', + attributes: { + name: 'Benedikt', + }, + relationships: { + friends: { + links: { related: '/user/2/friends' }, + data: [{ type: 'user', id: '1' }], + }, + }, + }, + { + type: 'user', + id: '3', + attributes: { + name: 'Jane', + }, + relationships: { + friends: { + data: [{ type: 'user', id: '1' }], + }, + }, + }, + ], + }); + + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.$type, 'user', '$type is accessible'); + assert.strictEqual(record.name, 'Leo', 'name is accessible'); + + // assert.expectAssertion( + // () => record.friends, + // 'Cannot fetch user.friends because the field is in linksMode but async is not yet supported' + // ); + }); +}); diff --git a/turbo.json b/turbo.json index 0455661a591..01a70ed230d 100644 --- a/turbo.json +++ b/turbo.json @@ -90,18 +90,14 @@ "dist/**", "unstable-preview-types/**" ], - "dependsOn": [], - "cache": false, + "dependsOn": [ + // we should create a "triggers" repo and have it depend + // on a TS "trigger" + ], // https://turbo.build/repo/docs/reference/configuration#outputmode "outputMode": "new-only" }, - // virtual task - "crawl-graph": { - "dependsOn": ["^crawl-graph"], - "cache": false - }, - ///////////////////////////////////////////////// ///////////////////////////////////////////////// // @@ -110,7 +106,7 @@ ///////////////////////////////////////////////// ///////////////////////////////////////////////// "start": { - // "dependsOn": ["_task:sync-hardlinks", "^_build"], + // "dependsOn": ["^_build"], // "outputs": [], // "cache": false, // "persistent": true @@ -158,7 +154,18 @@ "declarations/**", "unstable-preview-types/**" ], - "dependsOn": ["^build:pkg"] + "dependsOn": ["^build:pkg"], + "env": [ + "EMBER_DATA_FEATURE_OVERRIDE", + "NODE_ENV", + "CI", + "EMBER_ENV", + "IS_TESTING", + "EMBER_CLI_TEST_COMMAND", + "IS_RECORDING", + "ASSERT_ALL_DEPRECATIONS", + "EMBER_DATA_FULL_COMPAT" + ] }, "build:production": { "inputs": [ @@ -179,7 +186,18 @@ "dist-test/**", "declarations/**" ], - "dependsOn": ["^build:pkg"] + "dependsOn": ["^build:pkg"], + "env": [ + "EMBER_DATA_FEATURE_OVERRIDE", + "NODE_ENV", + "CI", + "EMBER_ENV", + "IS_TESTING", + "EMBER_CLI_TEST_COMMAND", + "IS_RECORDING", + "ASSERT_ALL_DEPRECATIONS", + "EMBER_DATA_FULL_COMPAT" + ] }, "test": {