From 0cc3c228da1d7f589dcd6b03201bd4a70107331b Mon Sep 17 00:00:00 2001 From: Kingston Date: Sun, 22 Mar 2026 08:30:58 +0100 Subject: [PATCH 1/2] feat: Sort entity arrays by name in project-definition.json for deterministic output. --- .changeset/sort-entity-arrays.md | 5 + .../baseplate/project-definition.json | 60 +-- .../apps/admin/.paths-metadata.json | 6 +- .../baseplate/generated/.paths-metadata.json | 2 +- .../apps/backend/.paths-metadata.json | 6 +- .../baseplate/generated/.paths-metadata.json | 2 +- .../apps/web/.paths-metadata.json | 6 +- .../baseplate/generated/.paths-metadata.json | 2 +- .../baseplate/project-definition.json | 432 +++++++++--------- .../project-definition-container.ts | 4 +- .../src/references/definition-ref-builder.ts | 2 + .../src/references/definition-ref-registry.ts | 2 + .../extend-parser-context-with-refs.ts | 1 + .../src/schema/features/feature.ts | 2 +- .../src/schema/libraries/library.ts | 1 + .../src/schema/models/enums.ts | 1 + .../src/schema/models/models.ts | 1 + .../src/schema/plugins/definition.ts | 2 + .../src/schema/project-definition.ts | 1 + .../project-builder-lib/src/tools/index.ts | 1 + .../src/tools/sort-entity-arrays.ts | 47 ++ .../src/tools/sort-entity-arrays.unit.test.ts | 147 ++++++ .../stripe/core/schema/plugin-definition.ts | 5 +- .../core/schema/schema-issue-checker.ts | 6 +- scripts/format-and-lint.ts | 187 +++++--- tests/simple/project-definition.json | 6 +- 26 files changed, 613 insertions(+), 324 deletions(-) create mode 100644 .changeset/sort-entity-arrays.md create mode 100644 packages/project-builder-lib/src/tools/sort-entity-arrays.ts create mode 100644 packages/project-builder-lib/src/tools/sort-entity-arrays.unit.test.ts diff --git a/.changeset/sort-entity-arrays.md b/.changeset/sort-entity-arrays.md new file mode 100644 index 000000000..ea138d1ca --- /dev/null +++ b/.changeset/sort-entity-arrays.md @@ -0,0 +1,5 @@ +--- +'@baseplate-dev/project-builder-lib': patch +--- + +Sort entity arrays by name in project-definition.json for deterministic output. Entity schemas with `sortByName: true` in their `withEnt` annotation are sorted alphabetically during serialization. diff --git a/examples/blog-with-auth/baseplate/project-definition.json b/examples/blog-with-auth/baseplate/project-definition.json index 95ac72e74..aaad5735a 100644 --- a/examples/blog-with-auth/baseplate/project-definition.json +++ b/examples/blog-with-auth/baseplate/project-definition.json @@ -97,10 +97,6 @@ "cliVersion": "0.6.4", "features": [ { "id": "feature:I_EnFXnHjbGQ", "name": "accounts" }, - { "id": "feature:ffs0IFfakTk0", "name": "articles" }, - { "id": "feature:XM8fb3xFc7ve", "name": "auth" }, - { "id": "feature:c28pJNS_89Oz", "name": "blogs" }, - { "id": "feature:IFU3VXMq1CM7", "name": "utilities" }, { "id": "feature:RLpN26dwQNkw", "name": "accounts/auth", @@ -110,7 +106,11 @@ "id": "feature:Cus91nOEvzmO", "name": "accounts/users", "parentRef": "accounts" - } + }, + { "id": "feature:ffs0IFfakTk0", "name": "articles" }, + { "id": "feature:XM8fb3xFc7ve", "name": "auth" }, + { "id": "feature:c28pJNS_89Oz", "name": "blogs" }, + { "id": "feature:IFU3VXMq1CM7", "name": "utilities" } ], "isInitialized": true, "libraries": [ @@ -941,16 +941,6 @@ } ], "plugins": [ - { - "config": { - "additionalUserAdminRoles": ["admin"], - "requireNameOnRegistration": true - }, - "id": "plugin:baseplate-dev_plugin-auth_local-auth", - "name": "local-auth", - "packageName": "@baseplate-dev/plugin-auth", - "version": "0.1.0" - }, { "config": { "accountsFeatureRef": "accounts/users", @@ -995,11 +985,21 @@ }, { "config": { - "implementationPluginKey": "baseplate-dev_plugin-queue_pg-boss" + "implementationPluginKey": "baseplate-dev_plugin-email_postmark" }, - "id": "plugin:baseplate-dev_plugin-queue_queue", - "name": "queue", - "packageName": "@baseplate-dev/plugin-queue", + "id": "plugin:baseplate-dev_plugin-email_email", + "name": "email", + "packageName": "@baseplate-dev/plugin-email", + "version": "0.1.0" + }, + { + "config": { + "additionalUserAdminRoles": ["admin"], + "requireNameOnRegistration": true + }, + "id": "plugin:baseplate-dev_plugin-auth_local-auth", + "name": "local-auth", + "packageName": "@baseplate-dev/plugin-auth", "version": "0.1.0" }, { @@ -1009,13 +1009,20 @@ "packageName": "@baseplate-dev/plugin-queue", "version": "0.1.0" }, + { + "config": { "postmarkOptions": {} }, + "id": "plugin:baseplate-dev_plugin-email_postmark", + "name": "postmark", + "packageName": "@baseplate-dev/plugin-email", + "version": "0.1.0" + }, { "config": { - "implementationPluginKey": "baseplate-dev_plugin-email_postmark" + "implementationPluginKey": "baseplate-dev_plugin-queue_pg-boss" }, - "id": "plugin:baseplate-dev_plugin-email_email", - "name": "email", - "packageName": "@baseplate-dev/plugin-email", + "id": "plugin:baseplate-dev_plugin-queue_queue", + "name": "queue", + "packageName": "@baseplate-dev/plugin-queue", "version": "0.1.0" }, { @@ -1031,13 +1038,6 @@ "name": "sentry", "packageName": "@baseplate-dev/plugin-observability", "version": "0.1.0" - }, - { - "config": { "postmarkOptions": {} }, - "id": "plugin:baseplate-dev_plugin-email_postmark", - "name": "postmark", - "packageName": "@baseplate-dev/plugin-email", - "version": "0.1.0" } ], "schemaVersion": 30, diff --git a/examples/todo-with-better-auth/apps/admin/.paths-metadata.json b/examples/todo-with-better-auth/apps/admin/.paths-metadata.json index f0b5b9c73..8a58f4c9e 100644 --- a/examples/todo-with-better-auth/apps/admin/.paths-metadata.json +++ b/examples/todo-with-better-auth/apps/admin/.paths-metadata.json @@ -32,15 +32,15 @@ "pathRootName": "routes-root" }, { - "canonicalPath": "@/src/routes/billing", + "canonicalPath": "@/src/routes/todos", "pathRootName": "routes-root" }, { - "canonicalPath": "@/src/routes/todos", + "canonicalPath": "@/src/routes/storage", "pathRootName": "routes-root" }, { - "canonicalPath": "@/src/routes/storage", + "canonicalPath": "@/src/routes/billing", "pathRootName": "routes-root" }, { diff --git a/examples/todo-with-better-auth/apps/admin/baseplate/generated/.paths-metadata.json b/examples/todo-with-better-auth/apps/admin/baseplate/generated/.paths-metadata.json index 25a1e5a05..0b9f7ed60 100644 --- a/examples/todo-with-better-auth/apps/admin/baseplate/generated/.paths-metadata.json +++ b/examples/todo-with-better-auth/apps/admin/baseplate/generated/.paths-metadata.json @@ -22,9 +22,9 @@ "canonicalPath": "@/src/routes/admin/accounts/users/user", "pathRootName": "routes-root" }, - { "canonicalPath": "@/src/routes/billing", "pathRootName": "routes-root" }, { "canonicalPath": "@/src/routes/todos", "pathRootName": "routes-root" }, { "canonicalPath": "@/src/routes/storage", "pathRootName": "routes-root" }, + { "canonicalPath": "@/src/routes/billing", "pathRootName": "routes-root" }, { "canonicalPath": "@/src/routes/accounts", "pathRootName": "routes-root" }, { "canonicalPath": "@/src/routes/accounts/users", diff --git a/examples/todo-with-better-auth/apps/backend/.paths-metadata.json b/examples/todo-with-better-auth/apps/backend/.paths-metadata.json index 1b756ab00..4730de73a 100644 --- a/examples/todo-with-better-auth/apps/backend/.paths-metadata.json +++ b/examples/todo-with-better-auth/apps/backend/.paths-metadata.json @@ -16,15 +16,15 @@ "pathRootName": "module-root" }, { - "canonicalPath": "@/src/modules/billing", + "canonicalPath": "@/src/modules/todos", "pathRootName": "module-root" }, { - "canonicalPath": "@/src/modules/todos", + "canonicalPath": "@/src/modules/storage", "pathRootName": "module-root" }, { - "canonicalPath": "@/src/modules/storage", + "canonicalPath": "@/src/modules/billing", "pathRootName": "module-root" }, { diff --git a/examples/todo-with-better-auth/apps/backend/baseplate/generated/.paths-metadata.json b/examples/todo-with-better-auth/apps/backend/baseplate/generated/.paths-metadata.json index 20c8c84b1..3ba0378b1 100644 --- a/examples/todo-with-better-auth/apps/backend/baseplate/generated/.paths-metadata.json +++ b/examples/todo-with-better-auth/apps/backend/baseplate/generated/.paths-metadata.json @@ -3,9 +3,9 @@ { "canonicalPath": "@/src", "pathRootName": "src-root" }, { "canonicalPath": "@/src/modules", "pathRootName": "module-root" }, { "canonicalPath": "@/src/modules/graphql", "pathRootName": "module-root" }, - { "canonicalPath": "@/src/modules/billing", "pathRootName": "module-root" }, { "canonicalPath": "@/src/modules/todos", "pathRootName": "module-root" }, { "canonicalPath": "@/src/modules/storage", "pathRootName": "module-root" }, + { "canonicalPath": "@/src/modules/billing", "pathRootName": "module-root" }, { "canonicalPath": "@/src/modules/accounts", "pathRootName": "module-root" }, { "canonicalPath": "@/src/modules/accounts/users", diff --git a/examples/todo-with-better-auth/apps/web/.paths-metadata.json b/examples/todo-with-better-auth/apps/web/.paths-metadata.json index 021ec9434..eb37566ed 100644 --- a/examples/todo-with-better-auth/apps/web/.paths-metadata.json +++ b/examples/todo-with-better-auth/apps/web/.paths-metadata.json @@ -8,15 +8,15 @@ "pathRootName": "routes-root" }, { - "canonicalPath": "@/src/routes/billing", + "canonicalPath": "@/src/routes/todos", "pathRootName": "routes-root" }, { - "canonicalPath": "@/src/routes/todos", + "canonicalPath": "@/src/routes/storage", "pathRootName": "routes-root" }, { - "canonicalPath": "@/src/routes/storage", + "canonicalPath": "@/src/routes/billing", "pathRootName": "routes-root" }, { diff --git a/examples/todo-with-better-auth/apps/web/baseplate/generated/.paths-metadata.json b/examples/todo-with-better-auth/apps/web/baseplate/generated/.paths-metadata.json index 7f9a7f0a7..3bba1d837 100644 --- a/examples/todo-with-better-auth/apps/web/baseplate/generated/.paths-metadata.json +++ b/examples/todo-with-better-auth/apps/web/baseplate/generated/.paths-metadata.json @@ -1,9 +1,9 @@ [ { "canonicalPath": "@/src/routes", "pathRootName": "routes-root" }, { "canonicalPath": "@/src/routes/auth", "pathRootName": "routes-root" }, - { "canonicalPath": "@/src/routes/billing", "pathRootName": "routes-root" }, { "canonicalPath": "@/src/routes/todos", "pathRootName": "routes-root" }, { "canonicalPath": "@/src/routes/storage", "pathRootName": "routes-root" }, + { "canonicalPath": "@/src/routes/billing", "pathRootName": "routes-root" }, { "canonicalPath": "@/src/routes/accounts", "pathRootName": "routes-root" }, { "canonicalPath": "@/src/routes/accounts/users", diff --git a/examples/todo-with-better-auth/baseplate/project-definition.json b/examples/todo-with-better-auth/baseplate/project-definition.json index a9b124492..3397ec6b4 100644 --- a/examples/todo-with-better-auth/baseplate/project-definition.json +++ b/examples/todo-with-better-auth/baseplate/project-definition.json @@ -307,9 +307,9 @@ "name": "accounts/users", "parentRef": "accounts" }, + { "id": "feature:QL2I7hoOmjBW", "name": "billing" }, { "id": "feature:d2gEAaWMHAeL", "name": "storage" }, - { "id": "feature:5yQn3gjSzKEB", "name": "todos" }, - { "id": "feature:QL2I7hoOmjBW", "name": "billing" } + { "id": "feature:5yQn3gjSzKEB", "name": "todos" } ], "isInitialized": true, "libraries": [ @@ -434,6 +434,152 @@ "name": "Account", "service": { "transformers": [] } }, + { + "featureRef": "billing", + "id": "model:yQ8vn0mn0sG0", + "model": { + "fields": [ + { + "id": "model-scalar-field:sLwgpRj1rBQu", + "isOptional": false, + "name": "id", + "options": { "default": "", "genUuid": true }, + "type": "uuid" + }, + { + "id": "model-scalar-field:__Cd5sxmnA0J", + "isOptional": false, + "name": "stripeCustomerId", + "options": { "default": "" }, + "type": "string" + }, + { + "id": "model-scalar-field:MGYyEd8mvzn0", + "isOptional": false, + "name": "createdAt", + "options": { "defaultToNow": true }, + "type": "dateTime" + }, + { + "id": "model-scalar-field:onoGYXPFAG1R", + "isOptional": false, + "name": "updatedAt", + "options": { "defaultToNow": true, "updatedAt": true }, + "type": "dateTime" + } + ], + "primaryKeyFieldRefs": ["id"], + "uniqueConstraints": [ + { + "fields": [{ "fieldRef": "stripeCustomerId" }], + "id": "model-unique-constraint:9HIKDJ4uJBRz" + } + ] + }, + "name": "BillingAccount", + "service": { "transformers": [] } + }, + { + "featureRef": "billing", + "id": "model:rQOnZgMWGHWP", + "model": { + "fields": [ + { + "id": "model-scalar-field:5SAuLT4bAV5_", + "isOptional": false, + "name": "id", + "options": { "default": "", "genUuid": true }, + "type": "uuid" + }, + { + "id": "model-scalar-field:0j9aW2_oVZi7", + "isOptional": false, + "name": "billingAccountId", + "options": { "default": "" }, + "type": "uuid" + }, + { + "id": "model-scalar-field:iFOTvtC8ySj_", + "isOptional": false, + "name": "planKey", + "options": { "default": "" }, + "type": "string" + }, + { + "id": "model-scalar-field:vDEdhOpEBcgh", + "isOptional": false, + "name": "status", + "options": { "enumRef": "BillingSubscriptionStatus" }, + "type": "enum" + }, + { + "id": "model-scalar-field:jLzXegb1IqTo", + "isOptional": false, + "name": "stripeSubscriptionId", + "options": { "default": "" }, + "type": "string" + }, + { + "id": "model-scalar-field:XR_8ZVHS7aB7", + "isOptional": false, + "name": "currentPeriodStart", + "options": {}, + "type": "dateTime" + }, + { + "id": "model-scalar-field:EucV0WREtHCq", + "isOptional": false, + "name": "currentPeriodEnd", + "options": {}, + "type": "dateTime" + }, + { + "id": "model-scalar-field:_gldE8Uqnq4q", + "isOptional": false, + "name": "cancelAtPeriodEnd", + "options": { "default": "false" }, + "type": "boolean" + }, + { + "id": "model-scalar-field:8TD82_zwouGj", + "isOptional": false, + "name": "createdAt", + "options": { "defaultToNow": true }, + "type": "dateTime" + }, + { + "id": "model-scalar-field:7CWVH5PctvBD", + "isOptional": false, + "name": "updatedAt", + "options": { "defaultToNow": true, "updatedAt": true }, + "type": "dateTime" + } + ], + "primaryKeyFieldRefs": ["id"], + "relations": [ + { + "foreignId": "model-foreign-relation:wI3FBas_VNa2", + "foreignRelationName": "billingSubscriptions", + "id": "model-local-relation:7_cnkNOxb0fZ", + "modelRef": "BillingAccount", + "name": "billingAccount", + "onDelete": "Cascade", + "onUpdate": "Restrict", + "references": [ + { "foreignRef": "id", "localRef": "billingAccountId" } + ] + } + ], + "uniqueConstraints": [ + { + "fields": [{ "fieldRef": "stripeSubscriptionId" }], + "id": "model-unique-constraint:rVWMdLZ9W36b" + } + ] + }, + "name": "BillingSubscription", + "service": { "transformers": [] } + }, { "featureRef": "accounts/users", "graphql": { @@ -1727,196 +1873,9 @@ }, "name": "Verification", "service": { "transformers": [] } - }, - { - "featureRef": "billing", - "id": "model:yQ8vn0mn0sG0", - "model": { - "fields": [ - { - "id": "model-scalar-field:sLwgpRj1rBQu", - "isOptional": false, - "name": "id", - "options": { "default": "", "genUuid": true }, - "type": "uuid" - }, - { - "id": "model-scalar-field:__Cd5sxmnA0J", - "isOptional": false, - "name": "stripeCustomerId", - "options": { "default": "" }, - "type": "string" - }, - { - "id": "model-scalar-field:MGYyEd8mvzn0", - "isOptional": false, - "name": "createdAt", - "options": { "defaultToNow": true }, - "type": "dateTime" - }, - { - "id": "model-scalar-field:onoGYXPFAG1R", - "isOptional": false, - "name": "updatedAt", - "options": { "defaultToNow": true, "updatedAt": true }, - "type": "dateTime" - } - ], - "primaryKeyFieldRefs": ["id"], - "uniqueConstraints": [ - { - "fields": [{ "fieldRef": "stripeCustomerId" }], - "id": "model-unique-constraint:9HIKDJ4uJBRz" - } - ] - }, - "name": "BillingAccount", - "service": { "transformers": [] } - }, - { - "featureRef": "billing", - "id": "model:rQOnZgMWGHWP", - "model": { - "fields": [ - { - "id": "model-scalar-field:5SAuLT4bAV5_", - "isOptional": false, - "name": "id", - "options": { "default": "", "genUuid": true }, - "type": "uuid" - }, - { - "id": "model-scalar-field:0j9aW2_oVZi7", - "isOptional": false, - "name": "billingAccountId", - "options": { "default": "" }, - "type": "uuid" - }, - { - "id": "model-scalar-field:iFOTvtC8ySj_", - "isOptional": false, - "name": "planKey", - "options": { "default": "" }, - "type": "string" - }, - { - "id": "model-scalar-field:vDEdhOpEBcgh", - "isOptional": false, - "name": "status", - "options": { "enumRef": "BillingSubscriptionStatus" }, - "type": "enum" - }, - { - "id": "model-scalar-field:jLzXegb1IqTo", - "isOptional": false, - "name": "stripeSubscriptionId", - "options": { "default": "" }, - "type": "string" - }, - { - "id": "model-scalar-field:XR_8ZVHS7aB7", - "isOptional": false, - "name": "currentPeriodStart", - "options": {}, - "type": "dateTime" - }, - { - "id": "model-scalar-field:EucV0WREtHCq", - "isOptional": false, - "name": "currentPeriodEnd", - "options": {}, - "type": "dateTime" - }, - { - "id": "model-scalar-field:_gldE8Uqnq4q", - "isOptional": false, - "name": "cancelAtPeriodEnd", - "options": { "default": "false" }, - "type": "boolean" - }, - { - "id": "model-scalar-field:8TD82_zwouGj", - "isOptional": false, - "name": "createdAt", - "options": { "defaultToNow": true }, - "type": "dateTime" - }, - { - "id": "model-scalar-field:7CWVH5PctvBD", - "isOptional": false, - "name": "updatedAt", - "options": { "defaultToNow": true, "updatedAt": true }, - "type": "dateTime" - } - ], - "primaryKeyFieldRefs": ["id"], - "relations": [ - { - "foreignId": "model-foreign-relation:wI3FBas_VNa2", - "foreignRelationName": "billingSubscriptions", - "id": "model-local-relation:7_cnkNOxb0fZ", - "modelRef": "BillingAccount", - "name": "billingAccount", - "onDelete": "Cascade", - "onUpdate": "Restrict", - "references": [ - { "foreignRef": "id", "localRef": "billingAccountId" } - ] - } - ], - "uniqueConstraints": [ - { - "fields": [{ "fieldRef": "stripeSubscriptionId" }], - "id": "model-unique-constraint:rVWMdLZ9W36b" - } - ] - }, - "name": "BillingSubscription", - "service": { "transformers": [] } } ], "plugins": [ - { - "config": { - "fileCategories": [ - { - "adapterRef": "uploads", - "authorize": { "uploadRoles": ["user"] }, - "id": "file-category:v1kFZCasEzf3", - "maxFileSizeMb": 10, - "name": "TODO_LIST_COVER_PHOTO" - }, - { - "adapterRef": "uploads", - "authorize": { "uploadRoles": ["user"] }, - "id": "file-category:n07NDlZNz8u3", - "maxFileSizeMb": 10, - "name": "USER_IMAGE_FILE" - }, - { - "adapterRef": "uploads", - "authorize": { "uploadRoles": ["user"] }, - "id": "file-category:QRE6e-sl7CV-", - "maxFileSizeMb": 10, - "name": "USER_PROFILE_AVATAR" - } - ], - "s3Adapters": [ - { - "bucketConfigVar": "AWS_UPLOADS_BUCKET", - "hostedUrlConfigVar": "AWS_UPLOADS_URL", - "id": "storage-adapter:sG0zrvD_Uak9", - "name": "uploads" - } - ], - "storageFeatureRef": "storage" - }, - "configSchemaVersion": 3, - "id": "plugin:baseplate-dev_plugin-storage_storage", - "name": "storage", - "packageName": "@baseplate-dev/plugin-storage", - "version": "0.1.0" - }, { "config": { "accountsFeatureRef": "accounts/users", @@ -1967,13 +1926,11 @@ "version": "1.0.0" }, { - "config": { - "implementationPluginKey": "baseplate-dev_plugin-queue_bullmq" - }, - "id": "plugin:baseplate-dev_plugin-queue_queue", - "name": "queue", - "packageName": "@baseplate-dev/plugin-queue", - "version": "1.0.0" + "config": {}, + "id": "plugin:baseplate-dev_plugin-auth_better-auth", + "name": "better-auth", + "packageName": "@baseplate-dev/plugin-auth", + "version": "0.1.0" }, { "config": { "bullmqOptions": { "deleteAfterDays": 7 } }, @@ -1983,12 +1940,30 @@ "version": "1.0.0" }, { - "config": {}, - "id": "plugin:baseplate-dev_plugin-auth_better-auth", - "name": "better-auth", - "packageName": "@baseplate-dev/plugin-auth", + "config": { + "implementationPluginKey": "baseplate-dev_plugin-email_postmark" + }, + "id": "plugin:baseplate-dev_plugin-email_email", + "name": "email", + "packageName": "@baseplate-dev/plugin-email", + "version": "0.1.0" + }, + { + "config": { "postmarkOptions": {} }, + "id": "plugin:baseplate-dev_plugin-email_postmark", + "name": "postmark", + "packageName": "@baseplate-dev/plugin-email", "version": "0.1.0" }, + { + "config": { + "implementationPluginKey": "baseplate-dev_plugin-queue_bullmq" + }, + "id": "plugin:baseplate-dev_plugin-queue_queue", + "name": "queue", + "packageName": "@baseplate-dev/plugin-queue", + "version": "1.0.0" + }, { "config": { "sentryOptions": {} }, "id": "plugin:baseplate-dev_plugin-observability_sentry", @@ -1996,6 +1971,47 @@ "packageName": "@baseplate-dev/plugin-observability", "version": "0.1.0" }, + { + "config": { + "fileCategories": [ + { + "adapterRef": "uploads", + "authorize": { "uploadRoles": ["user"] }, + "id": "file-category:v1kFZCasEzf3", + "maxFileSizeMb": 10, + "name": "TODO_LIST_COVER_PHOTO" + }, + { + "adapterRef": "uploads", + "authorize": { "uploadRoles": ["user"] }, + "id": "file-category:n07NDlZNz8u3", + "maxFileSizeMb": 10, + "name": "USER_IMAGE_FILE" + }, + { + "adapterRef": "uploads", + "authorize": { "uploadRoles": ["user"] }, + "id": "file-category:QRE6e-sl7CV-", + "maxFileSizeMb": 10, + "name": "USER_PROFILE_AVATAR" + } + ], + "s3Adapters": [ + { + "bucketConfigVar": "AWS_UPLOADS_BUCKET", + "hostedUrlConfigVar": "AWS_UPLOADS_URL", + "id": "storage-adapter:sG0zrvD_Uak9", + "name": "uploads" + } + ], + "storageFeatureRef": "storage" + }, + "configSchemaVersion": 3, + "id": "plugin:baseplate-dev_plugin-storage_storage", + "name": "storage", + "packageName": "@baseplate-dev/plugin-storage", + "version": "0.1.0" + }, { "config": { "billing": { @@ -2016,22 +2032,6 @@ "name": "stripe", "packageName": "@baseplate-dev/plugin-payments", "version": "0.1.0" - }, - { - "config": { - "implementationPluginKey": "baseplate-dev_plugin-email_postmark" - }, - "id": "plugin:baseplate-dev_plugin-email_email", - "name": "email", - "packageName": "@baseplate-dev/plugin-email", - "version": "0.1.0" - }, - { - "config": { "postmarkOptions": {} }, - "id": "plugin:baseplate-dev_plugin-email_postmark", - "name": "postmark", - "packageName": "@baseplate-dev/plugin-email", - "version": "0.1.0" } ], "schemaVersion": 30, diff --git a/packages/project-builder-lib/src/definition/project-definition-container.ts b/packages/project-builder-lib/src/definition/project-definition-container.ts index b20111df9..e368fb985 100644 --- a/packages/project-builder-lib/src/definition/project-definition-container.ts +++ b/packages/project-builder-lib/src/definition/project-definition-container.ts @@ -30,6 +30,7 @@ import { createProjectDefinitionSchema, } from '#src/schema/index.js'; import { collectEntityMetadata } from '#src/tools/entity-service/entity-type-map.js'; +import { sortEntityArrays } from '#src/tools/sort-entity-arrays.js'; /** * Container for a project definition that includes references and entities. @@ -150,7 +151,8 @@ export class ProjectDefinitionContainer { */ toSerializedContents(): string { const serializedContents = serializeSchema(this.schema, this.definition); - return stringifyPrettyStable(serializedContents); + const sortedContents = sortEntityArrays(this.schema, serializedContents); + return stringifyPrettyStable(sortedContents); } /** diff --git a/packages/project-builder-lib/src/references/definition-ref-builder.ts b/packages/project-builder-lib/src/references/definition-ref-builder.ts index 3f5c22549..316c1474f 100644 --- a/packages/project-builder-lib/src/references/definition-ref-builder.ts +++ b/packages/project-builder-lib/src/references/definition-ref-builder.ts @@ -68,6 +68,8 @@ interface DefinitionEntityInputBase< ) => DefinitionEntityNameResolver | string; /** Optional ref context slot that this entity provides. Registers this entity's path in a shared context. */ provides?: RefContextSlot; + /** When true, arrays of this entity type are sorted by name during serialization for deterministic output. */ + sortByName?: boolean; } /** diff --git a/packages/project-builder-lib/src/references/definition-ref-registry.ts b/packages/project-builder-lib/src/references/definition-ref-registry.ts index 462045210..0c1eb9581 100644 --- a/packages/project-builder-lib/src/references/definition-ref-registry.ts +++ b/packages/project-builder-lib/src/references/definition-ref-registry.ts @@ -23,6 +23,8 @@ export interface EntitySchemaMeta { ) => DefinitionEntityNameResolver | string; readonly parentSlot?: RefContextSlot; readonly provides?: RefContextSlot; + /** When true, arrays of this entity type are sorted by name during serialization for deterministic output. */ + readonly sortByName?: boolean; } export interface ReferenceSchemaMeta { diff --git a/packages/project-builder-lib/src/references/extend-parser-context-with-refs.ts b/packages/project-builder-lib/src/references/extend-parser-context-with-refs.ts index b42cdf3d9..8214872f2 100644 --- a/packages/project-builder-lib/src/references/extend-parser-context-with-refs.ts +++ b/packages/project-builder-lib/src/references/extend-parser-context-with-refs.ts @@ -138,6 +138,7 @@ export function withEnt( >['getNameResolver'], parentSlot: entity.parentSlot, provides: entity.provides, + sortByName: entity.sortByName, }); return schema as unknown as ZodTypeWithOptional; }; diff --git a/packages/project-builder-lib/src/schema/features/feature.ts b/packages/project-builder-lib/src/schema/features/feature.ts index 14ab50ba4..081a30284 100644 --- a/packages/project-builder-lib/src/schema/features/feature.ts +++ b/packages/project-builder-lib/src/schema/features/feature.ts @@ -34,7 +34,7 @@ export const createFeatureSchema = definitionSchema((ctx) => }) .optional(), }), - { type: featureEntityType }, + { type: featureEntityType, sortByName: true }, ), ); diff --git a/packages/project-builder-lib/src/schema/libraries/library.ts b/packages/project-builder-lib/src/schema/libraries/library.ts index be1e5f99d..f8c42bba2 100644 --- a/packages/project-builder-lib/src/schema/libraries/library.ts +++ b/packages/project-builder-lib/src/schema/libraries/library.ts @@ -21,6 +21,7 @@ export const createLibrarySchema = definitionSchema((ctx) => { type: libraryEntityType, provides: librarySlot, + sortByName: true, }, ); }), diff --git a/packages/project-builder-lib/src/schema/models/enums.ts b/packages/project-builder-lib/src/schema/models/enums.ts index 4b997ffad..b8ee1c8cd 100644 --- a/packages/project-builder-lib/src/schema/models/enums.ts +++ b/packages/project-builder-lib/src/schema/models/enums.ts @@ -50,6 +50,7 @@ export const createEnumSchema = definitionSchema((ctx) => ctx.withEnt(createEnumBaseSchema(ctx, { enumSlot }), { type: modelEnumEntityType, provides: enumSlot, + sortByName: true, }), ), ); diff --git a/packages/project-builder-lib/src/schema/models/models.ts b/packages/project-builder-lib/src/schema/models/models.ts index 33d9fcc43..0c072699a 100644 --- a/packages/project-builder-lib/src/schema/models/models.ts +++ b/packages/project-builder-lib/src/schema/models/models.ts @@ -572,6 +572,7 @@ export const createModelSchema = definitionSchema((ctx) => ctx.withEnt(createModelBaseSchema(ctx, { modelSlot }), { type: modelEntityType, provides: modelSlot, + sortByName: true, }), ), ); diff --git a/packages/project-builder-lib/src/schema/plugins/definition.ts b/packages/project-builder-lib/src/schema/plugins/definition.ts index 9fe1e3df3..71c961328 100644 --- a/packages/project-builder-lib/src/schema/plugins/definition.ts +++ b/packages/project-builder-lib/src/schema/plugins/definition.ts @@ -29,6 +29,7 @@ export const createPluginWithConfigSchema = definitionSchema((ctx) => { basePluginDefinitionSchema.refine(() => true), { type: pluginEntityType, + sortByName: true, }, ); } @@ -52,6 +53,7 @@ export const createPluginWithConfigSchema = definitionSchema((ctx) => { ), { type: pluginEntityType, + sortByName: true, }, ); }); diff --git a/packages/project-builder-lib/src/schema/project-definition.ts b/packages/project-builder-lib/src/schema/project-definition.ts index 4381bd192..c4bda36d8 100644 --- a/packages/project-builder-lib/src/schema/project-definition.ts +++ b/packages/project-builder-lib/src/schema/project-definition.ts @@ -30,6 +30,7 @@ export const createAppSchema = definitionSchema((ctx) => { type: appEntityType, provides: appSlot, + sortByName: true, }, ), ), diff --git a/packages/project-builder-lib/src/tools/index.ts b/packages/project-builder-lib/src/tools/index.ts index b113a4498..57fbd9e10 100644 --- a/packages/project-builder-lib/src/tools/index.ts +++ b/packages/project-builder-lib/src/tools/index.ts @@ -1,2 +1,3 @@ export * from './entity-service/index.js'; export * from './merge-schema/index.js'; +export { sortEntityArrays } from './sort-entity-arrays.js'; diff --git a/packages/project-builder-lib/src/tools/sort-entity-arrays.ts b/packages/project-builder-lib/src/tools/sort-entity-arrays.ts new file mode 100644 index 000000000..bbd178c33 --- /dev/null +++ b/packages/project-builder-lib/src/tools/sort-entity-arrays.ts @@ -0,0 +1,47 @@ +import type { z } from 'zod'; + +import { compareStrings } from '@baseplate-dev/utils'; + +import { getSchemaChildren } from '#src/parser/schema-structure.js'; +import { transformDataWithSchema } from '#src/parser/transform-data-with-schema.js'; + +import { getEntityMeta, getEntityName } from './merge-schema/entity-utils.js'; + +type PlainObject = Record; + +/** + * Sorts all entity arrays in a serialized definition by their resolved name. + * + * Uses `transformDataWithSchema` to walk the schema+data tree bottom-up. + * Arrays whose element schema has entity metadata (via `withEnt`) are sorted + * using `compareStrings` unless the entity has `disableSort: true`. + * All other arrays are left in their original order. + * + * @param schema - The Zod schema describing the data structure + * @param data - The serialized data (entity IDs already replaced with names) + * @returns A new data tree with entity arrays sorted by name + */ +export function sortEntityArrays(schema: z.ZodType, data: T): T { + return transformDataWithSchema( + schema, + data, + (value, { schema: nodeSchema }) => { + if (!Array.isArray(value)) return value; + + const items = value as PlainObject[]; + + const children = getSchemaChildren(nodeSchema, items, []); + if (children.kind !== 'array') return items; + + const entityMeta = getEntityMeta(children.elementSchema); + if (!entityMeta?.sortByName) return items; + + return items.toSorted((a, b) => + compareStrings( + getEntityName(entityMeta, a), + getEntityName(entityMeta, b), + ), + ); + }, + ); +} diff --git a/packages/project-builder-lib/src/tools/sort-entity-arrays.unit.test.ts b/packages/project-builder-lib/src/tools/sort-entity-arrays.unit.test.ts new file mode 100644 index 000000000..1d00d98f2 --- /dev/null +++ b/packages/project-builder-lib/src/tools/sort-entity-arrays.unit.test.ts @@ -0,0 +1,147 @@ +import { describe, expect, it } from 'vitest'; + +import { serializeSchema } from '#src/references/serialize-schema.js'; +import { + createTestFeature, + createTestModel, + createTestScalarField, +} from '#src/testing/definition-helpers.test-helper.js'; +import { createTestProjectDefinitionContainer } from '#src/testing/project-definition-container.test-helper.js'; + +import { sortEntityArrays } from './sort-entity-arrays.js'; + +describe('sortEntityArrays', () => { + const testFeature = createTestFeature({ name: 'testfeature' }); + + it('sorts entity arrays with sortByName: true', () => { + const container = createTestProjectDefinitionContainer({ + features: [testFeature], + models: [ + createTestModel({ + name: 'Zebra', + featureRef: testFeature.name, + }), + createTestModel({ + name: 'Apple', + featureRef: testFeature.name, + }), + createTestModel({ + name: 'Mango', + featureRef: testFeature.name, + }), + ], + }); + + const serialized = serializeSchema( + container.schema, + container.definition, + ) as Record; + const sorted = sortEntityArrays(container.schema, serialized); + + const modelNames = (sorted.models as { name: string }[]).map((m) => m.name); + expect(modelNames).toEqual(['Apple', 'Mango', 'Zebra']); + }); + + it('preserves order of entity arrays without sortByName', () => { + const container = createTestProjectDefinitionContainer({ + features: [testFeature], + models: [ + createTestModel({ + name: 'MyModel', + featureRef: testFeature.name, + model: { + fields: [ + createTestScalarField({ name: 'id', type: 'uuid' }), + createTestScalarField({ name: 'zebra', type: 'string' }), + createTestScalarField({ name: 'alpha', type: 'string' }), + ], + primaryKeyFieldRefs: ['id'], + }, + }), + ], + }); + + const serialized = serializeSchema( + container.schema, + container.definition, + ) as Record; + const sorted = sortEntityArrays(container.schema, serialized); + + const model = ( + sorted.models as { model: { fields: { name: string }[] } }[] + )[0]; + const fieldNames = model.model.fields.map((f) => f.name); + // Fields don't have sortByName, so they keep original order + expect(fieldNames).toEqual(['id', 'zebra', 'alpha']); + }); + + it('does not mutate the original data', () => { + const container = createTestProjectDefinitionContainer({ + features: [testFeature], + models: [ + createTestModel({ + name: 'Zebra', + featureRef: testFeature.name, + }), + createTestModel({ + name: 'Apple', + featureRef: testFeature.name, + }), + ], + }); + + const serialized = serializeSchema( + container.schema, + container.definition, + ) as Record; + + const originalModelNames = (serialized.models as { name: string }[]).map( + (m) => m.name, + ); + + sortEntityArrays(container.schema, serialized); + + const afterModelNames = (serialized.models as { name: string }[]).map( + (m) => m.name, + ); + expect(afterModelNames).toEqual(originalModelNames); + }); + + it('handles empty entity arrays', () => { + const container = createTestProjectDefinitionContainer({ + features: [testFeature], + models: [], + }); + + const serialized = serializeSchema( + container.schema, + container.definition, + ) as Record; + const sorted = sortEntityArrays(container.schema, serialized); + + expect(sorted.models).toEqual([]); + }); + + it('produces deterministic output regardless of insertion order', () => { + const modelsABC = [ + createTestModel({ name: 'Alpha', featureRef: testFeature.name }), + createTestModel({ name: 'Bravo', featureRef: testFeature.name }), + createTestModel({ name: 'Charlie', featureRef: testFeature.name }), + ]; + const modelsCBA = [modelsABC[2], modelsABC[1], modelsABC[0]]; + + const container1 = createTestProjectDefinitionContainer({ + features: [testFeature], + models: modelsABC, + }); + const container2 = createTestProjectDefinitionContainer({ + features: [testFeature], + models: modelsCBA, + }); + + const result1 = container1.toSerializedContents(); + const result2 = container2.toSerializedContents(); + + expect(result1).toBe(result2); + }); +}); diff --git a/plugins/plugin-payments/src/stripe/core/schema/plugin-definition.ts b/plugins/plugin-payments/src/stripe/core/schema/plugin-definition.ts index 45cb2fd7c..2add5f7bf 100644 --- a/plugins/plugin-payments/src/stripe/core/schema/plugin-definition.ts +++ b/plugins/plugin-payments/src/stripe/core/schema/plugin-definition.ts @@ -27,7 +27,10 @@ export const createBillingPlanSchema = definitionSchema((ctx) => ) .default([]), }), - { type: billingPlanEntityType, getNameResolver: (data) => data.key }, + { + type: billingPlanEntityType, + getNameResolver: (data) => data.key, + }, ), ); diff --git a/plugins/plugin-payments/src/stripe/core/schema/schema-issue-checker.ts b/plugins/plugin-payments/src/stripe/core/schema/schema-issue-checker.ts index a0d4092bd..ddeba2daf 100644 --- a/plugins/plugin-payments/src/stripe/core/schema/schema-issue-checker.ts +++ b/plugins/plugin-payments/src/stripe/core/schema/schema-issue-checker.ts @@ -18,12 +18,12 @@ export function createStripeSchemaChecker( pluginKey, pluginLabel: 'Stripe', buildPartialDef: (container) => { - const config = PluginUtils.configByKey( + const stripeConfig = PluginUtils.configByKey( container.definition, pluginKey, ) as StripePluginDefinition | undefined; - if (!config?.billing.enabled || !config.billing.featureRef) { + if (!stripeConfig?.billing.enabled || !stripeConfig.billing.featureRef) { return undefined; } @@ -35,7 +35,7 @@ export function createStripeSchemaChecker( const featureName = FeatureUtils.getFeaturePathByIdOrThrow( container.definition, - config.billing.featureRef, + stripeConfig.billing.featureRef, ); return createBillingPartialDefinition(featureName, userModelName); diff --git a/scripts/format-and-lint.ts b/scripts/format-and-lint.ts index 412f82bcb..5a61bdf10 100755 --- a/scripts/format-and-lint.ts +++ b/scripts/format-and-lint.ts @@ -1,20 +1,30 @@ #!/usr/bin/env node +/* eslint-disable import-x/no-extraneous-dependencies */ /** * A monorepo-aware formatting and linting hook script. * - * Groups all input files by their nearest package.json, then for each group: - * - Runs oxlint --fix (batch) - * - Runs prettier --write (batch) - * - Runs ESLint --fix for example projects only (they have their own tooling) + * Two code paths: + * - Monorepo files (packages/*, plugins/*, etc.): runs oxlint --fix then oxfmt on all + * files in one batch from the repo root. + * - Example project files (examples/*): uses per-project grouping with prettier + eslint + * since examples are standalone monorepos with their own tooling. * - * Required root dependencies: typescript, ts-node, oxlint, prettier, @types/node + * Required root dependencies: typescript, ts-node, oxfmt, oxlint, prettier, @types/node, @types/prettier */ +import type { Options } from 'prettier'; + import { execFile } from 'node:child_process'; import * as fs from 'node:fs'; import path from 'node:path'; import { promisify } from 'node:util'; +import { + format, + getFileInfo, + resolveConfig, + resolveConfigFile, +} from 'prettier'; const execFilePromise = promisify(execFile); @@ -22,6 +32,25 @@ const VALID_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx']); const ROOT_DIR = path.resolve(import.meta.dirname, '..'); +/** + * Collects all .prettierignore files from the starting directory up to the repo root. + */ +function findPrettierIgnorePaths(startDir: string): string[] { + const ignorePaths: string[] = []; + let currentDir = startDir; + while (true) { + const ignorePath = path.join(currentDir, '.prettierignore'); + if (fs.existsSync(ignorePath)) { + ignorePaths.push(ignorePath); + } + if (currentDir === ROOT_DIR || currentDir === path.dirname(currentDir)) { + break; + } + currentDir = path.dirname(currentDir); + } + return ignorePaths; +} + /** * Finds the nearest package.json file by traversing up from a starting directory. */ @@ -42,35 +71,40 @@ function findNearestPackageJson( } } -// --- Tool runners --- +// --- Prettier (for examples) --- -async function runPrettier(filePaths: string[], cwd: string): Promise { - console.info( - `Running prettier on ${filePaths.length} file(s) in ${path.basename(cwd)}...`, - ); - try { - await execFilePromise( - 'npx', - ['prettier', '--write', '--ignore-unknown', ...filePaths], - { cwd }, - ); - console.info(`✅ prettier completed for ${filePaths.length} file(s).`); - } catch (error) { - console.error(`❌ prettier failed for files in ${cwd}.`); - console.error(error instanceof Error ? error.message : String(error)); +const prettierConfigCache = new Map(); + +async function getPrettierConfig(filePath: string): Promise { + const configFile = await resolveConfigFile(filePath); + if (!configFile) return null; + if (prettierConfigCache.has(configFile)) { + return prettierConfigCache.get(configFile) ?? null; } + const config = await resolveConfig(filePath, { config: configFile }); + prettierConfigCache.set(configFile, config); + return config; } -async function runOxlint(filePaths: string[], cwd: string): Promise { - console.info( - `Running oxlint on ${filePaths.length} file(s) in ${path.basename(cwd)}...`, - ); +async function runPrettier(filePath: string): Promise { try { - await execFilePromise('npx', ['oxlint', '--fix', ...filePaths], { cwd }); - console.info(`✅ oxlint completed for ${filePaths.length} file(s).`); + const ignorePaths = findPrettierIgnorePaths(path.dirname(filePath)); + const fileInfo = await getFileInfo(filePath, { ignorePath: ignorePaths }); + if (fileInfo.ignored || !fileInfo.inferredParser) return; + const config = await getPrettierConfig(filePath); + if (!config) return; + const content = fs.readFileSync(filePath, 'utf8'); + const formattedContent = await format(content, { + ...config, + filepath: filePath, + }); + if (content !== formattedContent) { + fs.writeFileSync(filePath, formattedContent); + console.info(`✅ Prettier formatted ${path.basename(filePath)}.`); + } } catch (error) { - console.error(`❌ oxlint failed for files in ${cwd}.`); - console.error(error instanceof Error ? error.message : String(error)); + console.error(`❌ Prettier failed for ${filePath}.`); + if (error instanceof Error) console.error(error.message); } } @@ -90,6 +124,32 @@ async function runEslint(filePaths: string[], cwd: string): Promise { } } +// --- oxfmt + oxlint (for monorepo) --- + +async function runOxfmt(filePaths: string[]): Promise { + console.info(`Running oxfmt on ${filePaths.length} file(s)...`); + try { + await execFilePromise('npx', ['oxfmt', ...filePaths], { cwd: ROOT_DIR }); + console.info(`✅ oxfmt completed for ${filePaths.length} file(s).`); + } catch (error) { + console.error('❌ oxfmt failed.'); + console.error(error instanceof Error ? error.message : String(error)); + } +} + +async function runOxlint(filePaths: string[]): Promise { + console.info(`Running oxlint on ${filePaths.length} file(s)...`); + try { + await execFilePromise('npx', ['oxlint', '--fix', ...filePaths], { + cwd: ROOT_DIR, + }); + console.info(`✅ oxlint completed for ${filePaths.length} file(s).`); + } catch (error) { + console.error('❌ oxlint failed.'); + console.error(error instanceof Error ? error.message : String(error)); + } +} + // --- Main --- const skipEslint = process.argv.includes('--no-eslint'); @@ -102,41 +162,54 @@ if (inputPaths.length === 0) { const examplesDir = path.join(ROOT_DIR, 'examples'); -// Group all files by nearest package.json -const filesByProject = new Map< - string, - { pkgPath: string; files: string[]; isExample: boolean } ->(); +const monorepoFiles: string[] = []; +const exampleFiles: string[] = []; for (const filePath of inputPaths) { const absoluteFilePath = path.resolve(filePath); if (!VALID_EXTENSIONS.has(path.extname(absoluteFilePath))) continue; - - const packageInfo = findNearestPackageJson(path.dirname(absoluteFilePath)); - if (!packageInfo) { - console.info(`No package.json found for ${absoluteFilePath}. Skipping.`); - continue; - } - - const { pkgDir, pkgPath } = packageInfo; - if (!filesByProject.has(pkgDir)) { - const isExample = absoluteFilePath.startsWith(examplesDir + path.sep); - filesByProject.set(pkgDir, { pkgPath, files: [], isExample }); + if (absoluteFilePath.startsWith(examplesDir + path.sep)) { + exampleFiles.push(absoluteFilePath); + } else { + monorepoFiles.push(absoluteFilePath); } - // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion - filesByProject.get(pkgDir)!.files.push(absoluteFilePath); } -// Process each project group -for (const [ - pkgDir, - { pkgPath, files, isExample }, -] of filesByProject.entries()) { +// Process monorepo files: run oxlint then oxfmt from root. +if (monorepoFiles.length > 0) { console.info( - `\nProcessing ${files.length} file(s) in ${isExample ? 'example' : 'monorepo'} project: ${path.basename(pkgDir)}`, + `\nProcessing ${monorepoFiles.length} monorepo file(s) with oxlint + oxfmt...`, ); + await runOxlint(monorepoFiles); + await runOxfmt(monorepoFiles); +} + +// Process example files: per-project grouping with prettier + eslint. +if (exampleFiles.length > 0) { + console.info(`\nProcessing ${exampleFiles.length} example file(s)...`); + + const filesByProject = new Map< + string, + { pkgPath: string; files: string[] } + >(); + for (const filePath of exampleFiles) { + const packageInfo = findNearestPackageJson(path.dirname(filePath)); + if (!packageInfo) { + console.info(`No package.json found for ${filePath}. Skipping.`); + continue; + } + const { pkgDir, pkgPath } = packageInfo; + if (!filesByProject.has(pkgDir)) { + filesByProject.set(pkgDir, { pkgPath, files: [] }); + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + filesByProject.get(pkgDir)!.files.push(filePath); + } - if (isExample) { + for (const [pkgDir, { pkgPath, files }] of filesByProject.entries()) { + console.info( + `Processing ${files.length} file(s) in example project: ${pkgDir}`, + ); const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as { dependencies?: Record; devDependencies?: Record; @@ -144,10 +217,10 @@ for (const [ const deps = { ...pkg.dependencies, ...pkg.devDependencies }; if (deps.eslint && !skipEslint) await runEslint(files, pkgDir); - if (deps.prettier) await runPrettier(files, pkgDir); - } else { - await runOxlint(files, pkgDir); - await runPrettier(files, pkgDir); + + if (deps.prettier) { + await Promise.all(files.map((f) => runPrettier(f))); + } } } diff --git a/tests/simple/project-definition.json b/tests/simple/project-definition.json index ca2bc085f..4adb3a7a9 100644 --- a/tests/simple/project-definition.json +++ b/tests/simple/project-definition.json @@ -17,8 +17,8 @@ ], "cliVersion": "0.6.4", "features": [ - { "id": "feature:EHdFHsD17AHI", "name": "blog" }, - { "id": "feature:Hxp86n0StHww", "name": "auth" } + { "id": "feature:Hxp86n0StHww", "name": "auth" }, + { "id": "feature:EHdFHsD17AHI", "name": "blog" } ], "isInitialized": true, "libraries": [], @@ -86,7 +86,7 @@ "version": "0.1.0" } ], - "schemaVersion": 28, + "schemaVersion": 30, "settings": { "general": { "name": "simple", "packageScope": "", "portOffset": 3000 }, "infrastructure": { "redis": { "enabled": false } } From 8b40ecc290c14c64c49939a5ebf7105b0e1f912f Mon Sep 17 00:00:00 2001 From: Kingston Date: Sun, 22 Mar 2026 08:31:18 +0100 Subject: [PATCH 2/2] Switch eslint to oclint --- scripts/format-and-lint.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/format-and-lint.ts b/scripts/format-and-lint.ts index 5a61bdf10..02c6f1d0e 100755 --- a/scripts/format-and-lint.ts +++ b/scripts/format-and-lint.ts @@ -202,7 +202,7 @@ if (exampleFiles.length > 0) { if (!filesByProject.has(pkgDir)) { filesByProject.set(pkgDir, { pkgPath, files: [] }); } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion filesByProject.get(pkgDir)!.files.push(filePath); }