feat: disable tracking request history (#4607)

<|original_code|>
  removeDuplicateRestHistoryEntry,
  removeDuplicateGraphqlHistoryEntry,
  restHistoryStore,
} from "@hoppscotch/common/newstore/history"
import {
  getSettingSubject,
  settingsStore,
} from "@hoppscotch/common/newstore/settings"

import { getSyncInitFunction } from "../../lib/sync"

import * as E from "fp-ts/Either"

import { StoreSyncDefinitionOf } from "../../lib/sync"
import {
  createUserHistory,
  deleteAllUserHistory,
  removeRequestFromHistory,
  toggleHistoryStarStatus,
} from "./history.api"
import { ReqType } from "../../api/generated/graphql"

export const restHistoryStoreSyncDefinition: StoreSyncDefinitionOf<
  typeof restHistoryStore
> = {
  async addEntry({ entry }) {
    const res = await createUserHistory(
      JSON.stringify(entry.request),
      JSON.stringify(entry.responseMeta),
      ReqType.Rest
    )

    if (E.isRight(res)) {
      entry.id = res.right.createUserHistory.id

      // preventing double insertion from here and subscription
      removeDuplicateRestHistoryEntry(entry.id)
    }
  },
  deleteEntry({ entry }) {
    if (entry.id) {
      removeRequestFromHistory(entry.id)
    }
  },
  toggleStar({ entry }) {
    if (entry.id) {
      toggleHistoryStarStatus(entry.id)
    }
  },
  clearHistory() {
    deleteAllUserHistory(ReqType.Rest)
  },
}

export const gqlHistoryStoreSyncDefinition: StoreSyncDefinitionOf<
  typeof graphqlHistoryStore
> = {
  async addEntry({ entry }) {
    const res = await createUserHistory(
      JSON.stringify(entry.request),
      JSON.stringify(entry.response),
      ReqType.Gql
    )

    if (E.isRight(res)) {
      entry.id = res.right.createUserHistory.id

      // preventing double insertion from here and subscription
      removeDuplicateGraphqlHistoryEntry(entry.id)
    }
  },
  deleteEntry({ entry }) {
    if (entry.id) {
      removeRequestFromHistory(entry.id)
    }
  },
  toggleStar({ entry }) {
    if (entry.id) {
      toggleHistoryStarStatus(entry.id)
    }
  },
  clearHistory() {
<|edits_diff|>
--- packages/hoppscotch-selfhost-web/src/platform/history/history.sync.ts
+++ packages/hoppscotch-selfhost-web/src/platform/history/history.sync.ts
@@ -20,10 +20,16 @@
 } from "./history.api"
 import { ReqType } from "../../api/generated/graphql"

+import { isHistoryStoreEnabled } from "./history.platform"
+
 export const restHistoryStoreSyncDefinition: StoreSyncDefinitionOf<
   typeof restHistoryStore
 > = {
   async addEntry({ entry }) {
+    if (!isHistoryStoreEnabled.value) {
+      return
+    }
+
     const res = await createUserHistory(
       JSON.stringify(entry.request),
       JSON.stringify(entry.responseMeta),
<|current_version|>
  removeDuplicateRestHistoryEntry,
  removeDuplicateGraphqlHistoryEntry,
  restHistoryStore,
} from "@hoppscotch/common/newstore/history"
import {
  getSettingSubject,
  settingsStore,
} from "@hoppscotch/common/newstore/settings"

import { getSyncInitFunction } from "../../lib/sync"

import * as E from "fp-ts/Either"

import { StoreSyncDefinitionOf } from "../../lib/sync"
import {
  createUserHistory,
  deleteAllUserHistory,
  removeRequestFromHistory,
  toggleHistoryStarStatus,
} from "./history.api"
import { ReqType } from "../../api/generated/graphql"

import { isHistoryStoreEnabled } from "./history.platform"

export const restHistoryStoreSyncDefinition: StoreSyncDefinitionOf<
  typeof restHistoryStore
> = {
  async addEntry({ entry }) {
    if (!isHistoryStoreEnabled.value) {
      return
    }

    const res = await createUserHistory(
      JSON.stringify(entry.request),
      JSON.stringify(entry.responseMeta),
      ReqType.Rest
    )

    if (E.isRight(res)) {
      entry.id = res.right.createUserHistory.id

      // preventing double insertion from here and subscription
      removeDuplicateRestHistoryEntry(entry.id)
    }
  },
  deleteEntry({ entry }) {
    if (entry.id) {
      removeRequestFromHistory(entry.id)
    }
  },
  toggleStar({ entry }) {
    if (entry.id) {
      toggleHistoryStarStatus(entry.id)
    }
  },
  clearHistory() {
    deleteAllUserHistory(ReqType.Rest)
  },
}

export const gqlHistoryStoreSyncDefinition: StoreSyncDefinitionOf<
  typeof graphqlHistoryStore
> = {
  async addEntry({ entry }) {
    const res = await createUserHistory(
      JSON.stringify(entry.request),
      JSON.stringify(entry.response),
      ReqType.Gql
    )

    if (E.isRight(res)) {
      entry.id = res.right.createUserHistory.id

      // preventing double insertion from here and subscription
      removeDuplicateGraphqlHistoryEntry(entry.id)
    }
  },
  deleteEntry({ entry }) {
    if (entry.id) {
      removeRequestFromHistory(entry.id)
    }
  },
  toggleStar({ entry }) {
    if (entry.id) {
      toggleHistoryStarStatus(entry.id)
    }
  },
  clearHistory() {
<|next_version|>
  removeDuplicateRestHistoryEntry,
  removeDuplicateGraphqlHistoryEntry,
  restHistoryStore,
} from "@hoppscotch/common/newstore/history"
import {
  getSettingSubject,
  settingsStore,
} from "@hoppscotch/common/newstore/settings"

import { getSyncInitFunction } from "../../lib/sync"

import * as E from "fp-ts/Either"

import { StoreSyncDefinitionOf } from "../../lib/sync"
import {
  createUserHistory,
  deleteAllUserHistory,
  removeRequestFromHistory,
  toggleHistoryStarStatus,
} from "./history.api"
import { ReqType } from "../../api/generated/graphql"

import { isHistoryStoreEnabled } from "./history.platform"

export const restHistoryStoreSyncDefinition: StoreSyncDefinitionOf<
  typeof restHistoryStore
> = {
  async addEntry({ entry }) {
    if (!isHistoryStoreEnabled.value) {
      return
    }

    const res = await createUserHistory(
      JSON.stringify(entry.request),
      JSON.stringify(entry.responseMeta),
      ReqType.Rest
    )

    if (E.isRight(res)) {
      entry.id = res.right.createUserHistory.id

      // preventing double insertion from here and subscription
      removeDuplicateRestHistoryEntry(entry.id)
    }
  },
  deleteEntry({ entry }) {
    if (entry.id) {
      removeRequestFromHistory(entry.id)
    }
  },
  toggleStar({ entry }) {
    if (entry.id) {
      toggleHistoryStarStatus(entry.id)
    }
  },
  clearHistory() {
    deleteAllUserHistory(ReqType.Rest)
  },
}

export const gqlHistoryStoreSyncDefinition: StoreSyncDefinitionOf<
  typeof graphqlHistoryStore
> = {
  async addEntry({ entry }) {
    if (!isHistoryStoreEnabled.value) {
      return
    }

    const res = await createUserHistory(
      JSON.stringify(entry.request),
      JSON.stringify(entry.response),
      ReqType.Gql
    )

    if (E.isRight(res)) {
      entry.id = res.right.createUserHistory.id

      // preventing double insertion from here and subscription
      removeDuplicateGraphqlHistoryEntry(entry.id)
    }
  },
  deleteEntry({ entry }) {
    if (entry.id) {
      removeRequestFromHistory(entry.id)
    }
  },
  toggleStar({ entry }) {
    if (entry.id) {
      toggleHistoryStarStatus(entry.id)
    }
  },
  clearHistory() {


feat(backend): add the ability to disable tracking request history (#4594)

<|original_code|>
import { Module } from '@nestjs/common';
import { AdminResolver } from './admin.resolver';
import { AdminService } from './admin.service';
import { PrismaModule } from '../prisma/prisma.module';
import { PubSubModule } from '../pubsub/pubsub.module';
import { UserModule } from '../user/user.module';
import { TeamModule } from '../team/team.module';
import { TeamInvitationModule } from '../team-invitation/team-invitation.module';
import { TeamEnvironmentsModule } from '../team-environments/team-environments.module';
import { TeamCollectionModule } from '../team-collection/team-collection.module';
import { TeamRequestModule } from '../team-request/team-request.module';
import { InfraResolver } from './infra.resolver';
import { ShortcodeModule } from 'src/shortcode/shortcode.module';
import { InfraConfigModule } from 'src/infra-config/infra-config.module';

@Module({
  imports: [
    PrismaModule,
    PubSubModule,
    UserModule,
    TeamModule,
    TeamInvitationModule,
    TeamEnvironmentsModule,
    TeamCollectionModule,
    TeamRequestModule,
    ShortcodeModule,
    InfraConfigModule,
  ],
  providers: [InfraResolver, AdminResolver, AdminService],
  exports: [AdminService],
})
export class AdminModule {}

<|edits_diff|>
--- packages/hoppscotch-backend/src/admin/admin.module.ts
+++ packages/hoppscotch-backend/src/admin/admin.module.ts
@@ -12,6 +12,7 @@
 import { InfraResolver } from './infra.resolver';
 import { ShortcodeModule } from 'src/shortcode/shortcode.module';
 import { InfraConfigModule } from 'src/infra-config/infra-config.module';
+import { UserHistoryModule } from 'src/user-history/user-history.module';

 @Module({
   imports: [
<|current_version|>
import { Module } from '@nestjs/common';
import { AdminResolver } from './admin.resolver';
import { AdminService } from './admin.service';
import { PrismaModule } from '../prisma/prisma.module';
import { PubSubModule } from '../pubsub/pubsub.module';
import { UserModule } from '../user/user.module';
import { TeamModule } from '../team/team.module';
import { TeamInvitationModule } from '../team-invitation/team-invitation.module';
import { TeamEnvironmentsModule } from '../team-environments/team-environments.module';
import { TeamCollectionModule } from '../team-collection/team-collection.module';
import { TeamRequestModule } from '../team-request/team-request.module';
import { InfraResolver } from './infra.resolver';
import { ShortcodeModule } from 'src/shortcode/shortcode.module';
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
import { UserHistoryModule } from 'src/user-history/user-history.module';

@Module({
  imports: [
    PrismaModule,
    PubSubModule,
    UserModule,
    TeamModule,
    TeamInvitationModule,
    TeamEnvironmentsModule,
    TeamCollectionModule,
    TeamRequestModule,
    ShortcodeModule,
    InfraConfigModule,
  ],
  providers: [InfraResolver, AdminResolver, AdminService],
  exports: [AdminService],
})
export class AdminModule {}

<|next_version|>
import { Module } from '@nestjs/common';
import { AdminResolver } from './admin.resolver';
import { AdminService } from './admin.service';
import { PrismaModule } from '../prisma/prisma.module';
import { PubSubModule } from '../pubsub/pubsub.module';
import { UserModule } from '../user/user.module';
import { TeamModule } from '../team/team.module';
import { TeamInvitationModule } from '../team-invitation/team-invitation.module';
import { TeamEnvironmentsModule } from '../team-environments/team-environments.module';
import { TeamCollectionModule } from '../team-collection/team-collection.module';
import { TeamRequestModule } from '../team-request/team-request.module';
import { InfraResolver } from './infra.resolver';
import { ShortcodeModule } from 'src/shortcode/shortcode.module';
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
import { UserHistoryModule } from 'src/user-history/user-history.module';

@Module({
  imports: [
    PrismaModule,
    PubSubModule,
    UserModule,
    TeamModule,
    TeamInvitationModule,
    TeamEnvironmentsModule,
    TeamCollectionModule,
    TeamRequestModule,
    ShortcodeModule,
    InfraConfigModule,
    UserHistoryModule,
  ],
  providers: [InfraResolver, AdminResolver, AdminService],
  exports: [AdminService],
})
export class AdminModule {}


feat: collection import summaries (#4489)

<|original_code|>
import FileImportVue from "~/components/importExport/ImportExportSteps/FileImport.vue"
import { defineStep } from "~/composables/step-components"

import { v4 as uuidv4 } from "uuid"
import { Ref } from "vue"

export function FileSource(metadata: {
  acceptedFileTypes: string
  caption: string
  onImportFromFile: (content: string[]) => any | Promise<any>
  isLoading?: Ref<boolean>
}) {
  const stepID = uuidv4()

  return defineStep(stepID, FileImportVue, () => ({
    acceptedFileTypes: metadata.acceptedFileTypes,
    caption: metadata.caption,
    onImportFromFile: metadata.onImportFromFile,
    loading: metadata.isLoading?.value,
  }))
}

<|edits_diff|>
--- packages/hoppscotch-common/src/helpers/import-export/import/import-sources/FileSource.ts
+++ packages/hoppscotch-common/src/helpers/import-export/import/import-sources/FileSource.ts
@@ -9,6 +9,7 @@
   caption: string
   onImportFromFile: (content: string[]) => any | Promise<any>
   isLoading?: Ref<boolean>
+  description?: string
 }) {
   const stepID = uuidv4()

<|current_version|>
import FileImportVue from "~/components/importExport/ImportExportSteps/FileImport.vue"
import { defineStep } from "~/composables/step-components"

import { v4 as uuidv4 } from "uuid"
import { Ref } from "vue"

export function FileSource(metadata: {
  acceptedFileTypes: string
  caption: string
  onImportFromFile: (content: string[]) => any | Promise<any>
  isLoading?: Ref<boolean>
  description?: string
}) {
  const stepID = uuidv4()

  return defineStep(stepID, FileImportVue, () => ({
    acceptedFileTypes: metadata.acceptedFileTypes,
    caption: metadata.caption,
    onImportFromFile: metadata.onImportFromFile,
    loading: metadata.isLoading?.value,
  }))
}

<|next_version|>
import FileImportVue from "~/components/importExport/ImportExportSteps/FileImport.vue"
import { defineStep } from "~/composables/step-components"

import { v4 as uuidv4 } from "uuid"
import { Ref } from "vue"

export function FileSource(metadata: {
  acceptedFileTypes: string
  caption: string
  onImportFromFile: (content: string[]) => any | Promise<any>
  isLoading?: Ref<boolean>
  description?: string
}) {
  const stepID = uuidv4()

  return defineStep(stepID, FileImportVue, () => ({
    acceptedFileTypes: metadata.acceptedFileTypes,
    caption: metadata.caption,
    onImportFromFile: metadata.onImportFromFile,
    loading: metadata.isLoading?.value,
    description: metadata.description,
  }))
}


feat: add loading state for import actions (#4217)

<|original_code|>
import FileImportVue from "~/components/importExport/ImportExportSteps/FileImport.vue"
import { defineStep } from "~/composables/step-components"

import { v4 as uuidv4 } from "uuid"

export function FileSource(metadata: {
  acceptedFileTypes: string
  caption: string
  onImportFromFile: (content: string[]) => any | Promise<any>
}) {
  const stepID = uuidv4()

  return defineStep(stepID, FileImportVue, () => ({
    acceptedFileTypes: metadata.acceptedFileTypes,
    caption: metadata.caption,
    onImportFromFile: metadata.onImportFromFile,
  }))
}

<|edits_diff|>
--- packages/hoppscotch-common/src/helpers/import-export/import/import-sources/FileSource.ts
+++ packages/hoppscotch-common/src/helpers/import-export/import/import-sources/FileSource.ts
@@ -2,11 +2,13 @@
 import { defineStep } from "~/composables/step-components"

 import { v4 as uuidv4 } from "uuid"
+import { Ref } from "vue"

 export function FileSource(metadata: {
   acceptedFileTypes: string
   caption: string
   onImportFromFile: (content: string[]) => any | Promise<any>
+  isLoading?: Ref<boolean>
 }) {
   const stepID = uuidv4()

<|current_version|>
import FileImportVue from "~/components/importExport/ImportExportSteps/FileImport.vue"
import { defineStep } from "~/composables/step-components"

import { v4 as uuidv4 } from "uuid"
import { Ref } from "vue"

export function FileSource(metadata: {
  acceptedFileTypes: string
  caption: string
  onImportFromFile: (content: string[]) => any | Promise<any>
  isLoading?: Ref<boolean>
}) {
  const stepID = uuidv4()

  return defineStep(stepID, FileImportVue, () => ({
    acceptedFileTypes: metadata.acceptedFileTypes,
    caption: metadata.caption,
    onImportFromFile: metadata.onImportFromFile,
  }))
}

<|next_version|>
import FileImportVue from "~/components/importExport/ImportExportSteps/FileImport.vue"
import { defineStep } from "~/composables/step-components"

import { v4 as uuidv4 } from "uuid"
import { Ref } from "vue"

export function FileSource(metadata: {
  acceptedFileTypes: string
  caption: string
  onImportFromFile: (content: string[]) => any | Promise<any>
  isLoading?: Ref<boolean>
}) {
  const stepID = uuidv4()

  return defineStep(stepID, FileImportVue, () => ({
    acceptedFileTypes: metadata.acceptedFileTypes,
    caption: metadata.caption,
    onImportFromFile: metadata.onImportFromFile,
    loading: metadata.isLoading?.value,
  }))
}


HSB-462 feat: infra token module and sh apis (#4191)

<|original_code|>
import { ArgsType, Field, ID, InputType } from '@nestjs/graphql';

@ArgsType()
@InputType()
export class PaginationArgs {
  @Field(() => ID, {
    nullable: true,
    defaultValue: undefined,
    description: 'Cursor for pagination, ID of the last item in the list',
  })
  cursor: string;

  @Field({
    nullable: true,
    defaultValue: 10,
    description: 'Number of items to fetch',
  })
  take: number;
}

@ArgsType()
@InputType()
export class OffsetPaginationArgs {
  @Field({
    nullable: true,
    defaultValue: 0,
    description: 'Number of items to skip',
  })
  skip: number;

  @Field({
    nullable: true,
    defaultValue: 10,
    description: 'Number of items to fetch',
  })
  take: number;
}

<|edits_diff|>
--- packages/hoppscotch-backend/src/types/input-types.args.ts
+++ packages/hoppscotch-backend/src/types/input-types.args.ts
@@ -1,4 +1,7 @@
 import { ArgsType, Field, ID, InputType } from '@nestjs/graphql';
+import { ApiPropertyOptional } from '@nestjs/swagger';
+import { Type } from 'class-transformer';
+import { IsNotEmpty, IsOptional } from 'class-validator';

 @ArgsType()
 @InputType()
@@ -21,6 +24,10 @@
 @ArgsType()
 @InputType()
 export class OffsetPaginationArgs {
+  @IsOptional()
+  @IsNotEmpty()
+  @Type(() => Number)
+  @ApiPropertyOptional()
   @Field({
     nullable: true,
     defaultValue: 0,
<|current_version|>
import { ArgsType, Field, ID, InputType } from '@nestjs/graphql';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsNotEmpty, IsOptional } from 'class-validator';

@ArgsType()
@InputType()
export class PaginationArgs {
  @Field(() => ID, {
    nullable: true,
    defaultValue: undefined,
    description: 'Cursor for pagination, ID of the last item in the list',
  })
  cursor: string;

  @Field({
    nullable: true,
    defaultValue: 10,
    description: 'Number of items to fetch',
  })
  take: number;
}

@ArgsType()
@InputType()
export class OffsetPaginationArgs {
  @IsOptional()
  @IsNotEmpty()
  @Type(() => Number)
  @ApiPropertyOptional()
  @Field({
    nullable: true,
    defaultValue: 0,
    description: 'Number of items to skip',
  })
  skip: number;

  @Field({
    nullable: true,
    defaultValue: 10,
    description: 'Number of items to fetch',
  })
  take: number;
}

<|next_version|>
import { ArgsType, Field, ID, InputType } from '@nestjs/graphql';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsNotEmpty, IsOptional } from 'class-validator';

@ArgsType()
@InputType()
export class PaginationArgs {
  @Field(() => ID, {
    nullable: true,
    defaultValue: undefined,
    description: 'Cursor for pagination, ID of the last item in the list',
  })
  cursor: string;

  @Field({
    nullable: true,
    defaultValue: 10,
    description: 'Number of items to fetch',
  })
  take: number;
}

@ArgsType()
@InputType()
export class OffsetPaginationArgs {
  @IsOptional()
  @IsNotEmpty()
  @Type(() => Number)
  @ApiPropertyOptional()
  @Field({
    nullable: true,
    defaultValue: 0,
    description: 'Number of items to skip',
  })
  skip: number;

  @IsOptional()
  @IsNotEmpty()
  @Type(() => Number)
  @ApiPropertyOptional()
  @Field({
    nullable: true,
    defaultValue: 10,
    description: 'Number of items to fetch',
  })
  take: number;
}


feat: full text search for `TeamCollections` and `TeamRequests` (#3857)

<|original_code|>
import { Module } from '@nestjs/common';
import { PrismaModule } from '../prisma/prisma.module';
import { TeamCollectionService } from './team-collection.service';
import { TeamCollectionResolver } from './team-collection.resolver';
import { GqlCollectionTeamMemberGuard } from './guards/gql-collection-team-member.guard';
import { TeamModule } from '../team/team.module';
import { UserModule } from '../user/user.module';
import { PubSubModule } from '../pubsub/pubsub.module';

@Module({
  imports: [PrismaModule, TeamModule, UserModule, PubSubModule],
  providers: [
    TeamCollectionService,
    TeamCollectionResolver,
    GqlCollectionTeamMemberGuard,
  ],
  exports: [TeamCollectionService, GqlCollectionTeamMemberGuard],
})
export class TeamCollectionModule {}

<|edits_diff|>
--- packages/hoppscotch-backend/src/team-collection/team-collection.module.ts
+++ packages/hoppscotch-backend/src/team-collection/team-collection.module.ts
@@ -6,6 +6,7 @@
 import { TeamModule } from '../team/team.module';
 import { UserModule } from '../user/user.module';
 import { PubSubModule } from '../pubsub/pubsub.module';
+import { TeamCollectionController } from './team-collection.controller';

 @Module({
   imports: [PrismaModule, TeamModule, UserModule, PubSubModule],
<|current_version|>
import { Module } from '@nestjs/common';
import { PrismaModule } from '../prisma/prisma.module';
import { TeamCollectionService } from './team-collection.service';
import { TeamCollectionResolver } from './team-collection.resolver';
import { GqlCollectionTeamMemberGuard } from './guards/gql-collection-team-member.guard';
import { TeamModule } from '../team/team.module';
import { UserModule } from '../user/user.module';
import { PubSubModule } from '../pubsub/pubsub.module';
import { TeamCollectionController } from './team-collection.controller';

@Module({
  imports: [PrismaModule, TeamModule, UserModule, PubSubModule],
  providers: [
    TeamCollectionService,
    TeamCollectionResolver,
    GqlCollectionTeamMemberGuard,
  ],
  exports: [TeamCollectionService, GqlCollectionTeamMemberGuard],
})
export class TeamCollectionModule {}

<|next_version|>
import { Module } from '@nestjs/common';
import { PrismaModule } from '../prisma/prisma.module';
import { TeamCollectionService } from './team-collection.service';
import { TeamCollectionResolver } from './team-collection.resolver';
import { GqlCollectionTeamMemberGuard } from './guards/gql-collection-team-member.guard';
import { TeamModule } from '../team/team.module';
import { UserModule } from '../user/user.module';
import { PubSubModule } from '../pubsub/pubsub.module';
import { TeamCollectionController } from './team-collection.controller';

@Module({
  imports: [PrismaModule, TeamModule, UserModule, PubSubModule],
  providers: [
    TeamCollectionService,
    TeamCollectionResolver,
    GqlCollectionTeamMemberGuard,
  ],
  exports: [TeamCollectionService, GqlCollectionTeamMemberGuard],
  controllers: [TeamCollectionController],
})
export class TeamCollectionModule {}


feat: rest revamp (#2918)

<|original_code|>
import { createHoppApp } from "@hoppscotch/common"
import { def as authDef } from "./firebase/auth"
import { def as envDef } from "./environments"
import { def as collectionsDef } from "./collections"
import { def as settingsDef } from "./settings"
import { def as historyDef } from "./history"

createHoppApp("#app", {
  auth: authDef,
  sync: {
    environments: envDef,
    collections: collectionsDef,
    settings: settingsDef,
    history: historyDef,
  },
})

<|edits_diff|>
--- packages/hoppscotch-web/src/main.ts
+++ packages/hoppscotch-web/src/main.ts
@@ -4,6 +4,7 @@
 import { def as collectionsDef } from "./collections"
 import { def as settingsDef } from "./settings"
 import { def as historyDef } from "./history"
+import { def as tabStateDef } from "./tab"

 createHoppApp("#app", {
   auth: authDef,
<|current_version|>
import { createHoppApp } from "@hoppscotch/common"
import { def as authDef } from "./firebase/auth"
import { def as envDef } from "./environments"
import { def as collectionsDef } from "./collections"
import { def as settingsDef } from "./settings"
import { def as historyDef } from "./history"
import { def as tabStateDef } from "./tab"

createHoppApp("#app", {
  auth: authDef,
  sync: {
    environments: envDef,
    collections: collectionsDef,
    settings: settingsDef,
    history: historyDef,
  },
})

<|next_version|>
import { createHoppApp } from "@hoppscotch/common"
import { def as authDef } from "./firebase/auth"
import { def as envDef } from "./environments"
import { def as collectionsDef } from "./collections"
import { def as settingsDef } from "./settings"
import { def as historyDef } from "./history"
import { def as tabStateDef } from "./tab"

createHoppApp("#app", {
  auth: authDef,
  sync: {
    environments: envDef,
    collections: collectionsDef,
    settings: settingsDef,
    history: historyDef,
    tabState: tabStateDef,
  },
})


feat: global workspace selector (#2922)

<|original_code|>
import * as E from "fp-ts/Either"
import { BehaviorSubject } from "rxjs"
import { GQLError, runGQLQuery } from "../backend/GQLClient"
import { GetMyTeamsDocument, GetMyTeamsQuery } from "../backend/graphql"
import { platform } from "~/platform"

const BACKEND_PAGE_SIZE = 10
const POLL_DURATION = 10000

export default class TeamListAdapter {
  error$: BehaviorSubject<GQLError<string> | null>
  loading$: BehaviorSubject<boolean>
  teamList$: BehaviorSubject<GetMyTeamsQuery["myTeams"]>

  private timeoutHandle: ReturnType<typeof setTimeout> | null
  private isDispose: boolean

  constructor(deferInit = false) {
    this.error$ = new BehaviorSubject<GQLError<string> | null>(null)
    this.loading$ = new BehaviorSubject<boolean>(false)
    this.teamList$ = new BehaviorSubject<GetMyTeamsQuery["myTeams"]>([])
    this.timeoutHandle = null
    this.isDispose = false

    if (!deferInit) this.initialize()
  }

  initialize() {
    if (this.timeoutHandle) throw new Error(`Adapter already initialized`)
    if (this.isDispose) throw new Error(`Adapter has been disposed`)

    const func = async () => {
      await this.fetchList()

      if (!this.isDispose) {
        this.timeoutHandle = setTimeout(() => func(), POLL_DURATION)
      }
    }

    func()
  }

  public dispose() {
    this.isDispose = true
    clearTimeout(this.timeoutHandle as any)
    this.timeoutHandle = null
  }

  async fetchList() {
    const currentUser = platform.auth.getCurrentUser()

    // if the authIdToken is not present, don't fetch the teams list, as it will fail anyway
    if (!currentUser) return

    this.loading$.next(true)

    const results: GetMyTeamsQuery["myTeams"] = []

    while (true) {
      const result = await runGQLQuery({
        query: GetMyTeamsDocument,
        variables: {
          cursor:
            results.length > 0 ? results[results.length - 1].id : undefined,
        },
      })

      if (E.isLeft(result)) {
        this.error$.next(result.left)
        throw new Error(
<|edits_diff|>
--- packages/hoppscotch-common/src/helpers/teams/TeamListAdapter.ts
+++ packages/hoppscotch-common/src/helpers/teams/TeamListAdapter.ts
@@ -15,6 +15,8 @@
   private timeoutHandle: ReturnType<typeof setTimeout> | null
   private isDispose: boolean

+  public isInitialized: boolean
+
   constructor(deferInit = false) {
     this.error$ = new BehaviorSubject<GQLError<string> | null>(null)
     this.loading$ = new BehaviorSubject<boolean>(false)
@@ -22,12 +24,16 @@
     this.timeoutHandle = null
     this.isDispose = false

+    this.isInitialized = false
+
     if (!deferInit) this.initialize()
   }

   initialize() {
     if (this.timeoutHandle) throw new Error(`Adapter already initialized`)
     if (this.isDispose) throw new Error(`Adapter has been disposed`)
+
+    this.isInitialized = true

     const func = async () => {
       await this.fetchList()
<|current_version|>
import * as E from "fp-ts/Either"
import { BehaviorSubject } from "rxjs"
import { GQLError, runGQLQuery } from "../backend/GQLClient"
import { GetMyTeamsDocument, GetMyTeamsQuery } from "../backend/graphql"
import { platform } from "~/platform"

const BACKEND_PAGE_SIZE = 10
const POLL_DURATION = 10000

export default class TeamListAdapter {
  error$: BehaviorSubject<GQLError<string> | null>
  loading$: BehaviorSubject<boolean>
  teamList$: BehaviorSubject<GetMyTeamsQuery["myTeams"]>

  private timeoutHandle: ReturnType<typeof setTimeout> | null
  private isDispose: boolean

  public isInitialized: boolean

  constructor(deferInit = false) {
    this.error$ = new BehaviorSubject<GQLError<string> | null>(null)
    this.loading$ = new BehaviorSubject<boolean>(false)
    this.teamList$ = new BehaviorSubject<GetMyTeamsQuery["myTeams"]>([])
    this.timeoutHandle = null
    this.isDispose = false

    this.isInitialized = false

    if (!deferInit) this.initialize()
  }

  initialize() {
    if (this.timeoutHandle) throw new Error(`Adapter already initialized`)
    if (this.isDispose) throw new Error(`Adapter has been disposed`)

    this.isInitialized = true

    const func = async () => {
      await this.fetchList()

      if (!this.isDispose) {
        this.timeoutHandle = setTimeout(() => func(), POLL_DURATION)
      }
    }

    func()
  }

  public dispose() {
    this.isDispose = true
    clearTimeout(this.timeoutHandle as any)
    this.timeoutHandle = null
  }

  async fetchList() {
    const currentUser = platform.auth.getCurrentUser()

    // if the authIdToken is not present, don't fetch the teams list, as it will fail anyway
    if (!currentUser) return

    this.loading$.next(true)

    const results: GetMyTeamsQuery["myTeams"] = []

    while (true) {
      const result = await runGQLQuery({
        query: GetMyTeamsDocument,
        variables: {
          cursor:
            results.length > 0 ? results[results.length - 1].id : undefined,
        },
      })

      if (E.isLeft(result)) {
        this.error$.next(result.left)
        throw new Error(
<|next_version|>
import * as E from "fp-ts/Either"
import { BehaviorSubject } from "rxjs"
import { GQLError, runGQLQuery } from "../backend/GQLClient"
import { GetMyTeamsDocument, GetMyTeamsQuery } from "../backend/graphql"
import { platform } from "~/platform"

const BACKEND_PAGE_SIZE = 10
const POLL_DURATION = 10000

export default class TeamListAdapter {
  error$: BehaviorSubject<GQLError<string> | null>
  loading$: BehaviorSubject<boolean>
  teamList$: BehaviorSubject<GetMyTeamsQuery["myTeams"]>

  private timeoutHandle: ReturnType<typeof setTimeout> | null
  private isDispose: boolean

  public isInitialized: boolean

  constructor(deferInit = false) {
    this.error$ = new BehaviorSubject<GQLError<string> | null>(null)
    this.loading$ = new BehaviorSubject<boolean>(false)
    this.teamList$ = new BehaviorSubject<GetMyTeamsQuery["myTeams"]>([])
    this.timeoutHandle = null
    this.isDispose = false

    this.isInitialized = false

    if (!deferInit) this.initialize()
  }

  initialize() {
    if (this.timeoutHandle) throw new Error(`Adapter already initialized`)
    if (this.isDispose) throw new Error(`Adapter has been disposed`)

    this.isInitialized = true

    const func = async () => {
      await this.fetchList()

      if (!this.isDispose) {
        this.timeoutHandle = setTimeout(() => func(), POLL_DURATION)
      }
    }

    func()
  }

  public dispose() {
    this.isDispose = true
    clearTimeout(this.timeoutHandle as any)
    this.timeoutHandle = null
    this.isInitialized = false
  }

  async fetchList() {
    const currentUser = platform.auth.getCurrentUser()

    // if the authIdToken is not present, don't fetch the teams list, as it will fail anyway
    if (!currentUser) return

    this.loading$.next(true)

    const results: GetMyTeamsQuery["myTeams"] = []

    while (true) {
      const result = await runGQLQuery({
        query: GetMyTeamsDocument,
        variables: {
          cursor:
            results.length > 0 ? results[results.length - 1].id : undefined,
        },
      })

      if (E.isLeft(result)) {
        this.error$.next(result.left)
        throw new Error(


refactor: changed auth module to work with signed cookies

<|original_code|>
export const authCookieHandler = (
  res: Response,
  authTokens: AuthTokens,
  redirect: boolean,
) => {
  const currentTime = DateTime.now();
  const accessTokenValidity = currentTime
    .plus({
      milliseconds: parseInt(process.env.ACCESS_TOKEN_VALIDITY),
    })
    .toMillis();
  const refreshTokenValidity = currentTime
    .plus({
      milliseconds: parseInt(process.env.REFRESH_TOKEN_VALIDITY),
    })
    .toMillis();

  res.cookie('access_token', authTokens.access_token, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: accessTokenValidity,
  });
  res.cookie('refresh_token', authTokens.refresh_token, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: refreshTokenValidity,
  });
  if (redirect) {
    res.status(HttpStatus.OK).redirect(process.env.REDIRECT_URL);
  } else res.status(HttpStatus.OK).send();
};

/*
 * String to JSON parser
 * @param {str} str The string to parse
 * @returns {E.Right<T> | E.Left<"json_invalid">} An Either of the parsed JSON
 */
export function stringToJson<T>(
  str: string,
): E.Right<T | any> | E.Left<string> {
  try {
    return E.right(JSON.parse(str));
  } catch (err) {
    return E.left(JSON_INVALID);
  }
}

<|edits_diff|>
--- packages/hoppscotch-backend/src/utils.ts
+++ packages/hoppscotch-backend/src/utils.ts
@@ -20,6 +20,7 @@
     secure: true,
     sameSite: 'lax',
     maxAge: accessTokenValidity,
+    signed: true,
   });
   res.cookie('refresh_token', authTokens.refresh_token, {
     httpOnly: true,
<|current_version|>
export const authCookieHandler = (
  res: Response,
  authTokens: AuthTokens,
  redirect: boolean,
) => {
  const currentTime = DateTime.now();
  const accessTokenValidity = currentTime
    .plus({
      milliseconds: parseInt(process.env.ACCESS_TOKEN_VALIDITY),
    })
    .toMillis();
  const refreshTokenValidity = currentTime
    .plus({
      milliseconds: parseInt(process.env.REFRESH_TOKEN_VALIDITY),
    })
    .toMillis();

  res.cookie('access_token', authTokens.access_token, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: accessTokenValidity,
    signed: true,
  });
  res.cookie('refresh_token', authTokens.refresh_token, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: refreshTokenValidity,
  });
  if (redirect) {
    res.status(HttpStatus.OK).redirect(process.env.REDIRECT_URL);
  } else res.status(HttpStatus.OK).send();
};

/*
 * String to JSON parser
 * @param {str} str The string to parse
 * @returns {E.Right<T> | E.Left<"json_invalid">} An Either of the parsed JSON
 */
export function stringToJson<T>(
  str: string,
): E.Right<T | any> | E.Left<string> {
  try {
    return E.right(JSON.parse(str));
  } catch (err) {
    return E.left(JSON_INVALID);
  }
}

<|next_version|>
export const authCookieHandler = (
  res: Response,
  authTokens: AuthTokens,
  redirect: boolean,
) => {
  const currentTime = DateTime.now();
  const accessTokenValidity = currentTime
    .plus({
      milliseconds: parseInt(process.env.ACCESS_TOKEN_VALIDITY),
    })
    .toMillis();
  const refreshTokenValidity = currentTime
    .plus({
      milliseconds: parseInt(process.env.REFRESH_TOKEN_VALIDITY),
    })
    .toMillis();

  res.cookie('access_token', authTokens.access_token, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: accessTokenValidity,
    signed: true,
  });
  res.cookie('refresh_token', authTokens.refresh_token, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: refreshTokenValidity,
    signed: true,
  });
  if (redirect) {
    res.status(HttpStatus.OK).redirect(process.env.REDIRECT_URL);
  } else res.status(HttpStatus.OK).send();
};

/*
 * String to JSON parser
 * @param {str} str The string to parse
 * @returns {E.Right<T> | E.Left<"json_invalid">} An Either of the parsed JSON
 */
export function stringToJson<T>(
  str: string,
): E.Right<T | any> | E.Left<string> {
  try {
    return E.right(JSON.parse(str));
  } catch (err) {
    return E.left(JSON_INVALID);
  }
}


chore: pulled from main

<|original_code|>
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { UserModule } from './user/user.module';
import { GQLComplexityPlugin } from './plugins/GQLComplexityPlugin';
import { UserEnvironmentsModule } from './user-environment/user-environments.module';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      playground: process.env.PRODUCTION !== 'true',
      debug: process.env.PRODUCTION !== 'true',
      autoSchemaFile: true,
      installSubscriptionHandlers: true,
      subscriptions: {
        'subscriptions-transport-ws': {
          path: '/graphql',
          onConnect: (connectionParams: any) => {
            return {
              reqHeaders: Object.fromEntries(
                Object.entries(connectionParams).map(([k, v]) => [
                  k.toLowerCase(),
                  v,
                ]),
              ),
            };
          },
        },
      },
      context: async ({ req, connection }) => {
        if (req) {
          return { reqHeaders: req.headers };
        } else {
          return {
            // Lowercase the keys
            reqHeaders: Object.fromEntries(
              Object.entries(connection.context).map(([k, v]) => [
                k.toLowerCase(),
                v,
              ]),
            ),
          };
        }
      },
      driver: ApolloDriver,
    }),
    UserModule,
    UserEnvironmentsModule,
  ],
  providers: [GQLComplexityPlugin],
})
export class AppModule {}

<|edits_diff|>
--- packages/hoppscotch-backend/src/app.module.ts
+++ packages/hoppscotch-backend/src/app.module.ts
@@ -3,7 +3,9 @@
 import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
 import { UserModule } from './user/user.module';
 import { GQLComplexityPlugin } from './plugins/GQLComplexityPlugin';
+import { UserSettingsModule } from './user-settings/user-settings.module';
 import { UserEnvironmentsModule } from './user-environment/user-environments.module';
+import { UserHistoryModule } from './user-history/user-history.module';

 @Module({
   imports: [
@@ -45,6 +47,7 @@
       driver: ApolloDriver,
     }),
     UserModule,
+    UserSettingsModule,
     UserEnvironmentsModule,
   ],
   providers: [GQLComplexityPlugin],
<|current_version|>
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { UserModule } from './user/user.module';
import { GQLComplexityPlugin } from './plugins/GQLComplexityPlugin';
import { UserSettingsModule } from './user-settings/user-settings.module';
import { UserEnvironmentsModule } from './user-environment/user-environments.module';
import { UserHistoryModule } from './user-history/user-history.module';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      playground: process.env.PRODUCTION !== 'true',
      debug: process.env.PRODUCTION !== 'true',
      autoSchemaFile: true,
      installSubscriptionHandlers: true,
      subscriptions: {
        'subscriptions-transport-ws': {
          path: '/graphql',
          onConnect: (connectionParams: any) => {
            return {
              reqHeaders: Object.fromEntries(
                Object.entries(connectionParams).map(([k, v]) => [
                  k.toLowerCase(),
                  v,
                ]),
              ),
            };
          },
        },
      },
      context: async ({ req, connection }) => {
        if (req) {
          return { reqHeaders: req.headers };
        } else {
          return {
            // Lowercase the keys
            reqHeaders: Object.fromEntries(
              Object.entries(connection.context).map(([k, v]) => [
                k.toLowerCase(),
                v,
              ]),
            ),
          };
        }
      },
      driver: ApolloDriver,
    }),
    UserModule,
    UserSettingsModule,
    UserEnvironmentsModule,
  ],
  providers: [GQLComplexityPlugin],
})
export class AppModule {}

<|next_version|>
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { UserModule } from './user/user.module';
import { GQLComplexityPlugin } from './plugins/GQLComplexityPlugin';
import { UserSettingsModule } from './user-settings/user-settings.module';
import { UserEnvironmentsModule } from './user-environment/user-environments.module';
import { UserHistoryModule } from './user-history/user-history.module';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      playground: process.env.PRODUCTION !== 'true',
      debug: process.env.PRODUCTION !== 'true',
      autoSchemaFile: true,
      installSubscriptionHandlers: true,
      subscriptions: {
        'subscriptions-transport-ws': {
          path: '/graphql',
          onConnect: (connectionParams: any) => {
            return {
              reqHeaders: Object.fromEntries(
                Object.entries(connectionParams).map(([k, v]) => [
                  k.toLowerCase(),
                  v,
                ]),
              ),
            };
          },
        },
      },
      context: async ({ req, connection }) => {
        if (req) {
          return { reqHeaders: req.headers };
        } else {
          return {
            // Lowercase the keys
            reqHeaders: Object.fromEntries(
              Object.entries(connection.context).map(([k, v]) => [
                k.toLowerCase(),
                v,
              ]),
            ),
          };
        }
      },
      driver: ApolloDriver,
    }),
    UserModule,
    UserSettingsModule,
    UserEnvironmentsModule,
    UserHistoryModule,
  ],
  providers: [GQLComplexityPlugin],
})
export class AppModule {}


fix: added scope for github strategy in auth module

<|original_code|>
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UserModule } from 'src/user/user.module';
import { MailerModule } from 'src/mailer/mailer.module';
import { PrismaModule } from 'src/prisma/prisma.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt/dist';
import { JwtStrategy } from './strategies/jwt.strategy';
import { RTJwtStrategy } from './strategies/rt-jwt.strategy';
import { GoogleStrategy } from './strategies/google.strategy';
import { GithubStrategy } from './strategies/github.strategy';

@Module({
  imports: [
    PrismaModule,
    UserModule,
    MailerModule,
    PassportModule,
    JwtModule.register({
      secret: process.env.JWT_SECRET,
    }),
  ],
  providers: [
    AuthService,
    JwtStrategy,
    RTJwtStrategy,
    GoogleStrategy,
    GithubStrategy,
  ],
  controllers: [AuthController],
})
export class AuthModule {}

<|edits_diff|>
--- packages/hoppscotch-backend/src/auth/auth.module.ts
+++ packages/hoppscotch-backend/src/auth/auth.module.ts
@@ -10,6 +10,7 @@
 import { RTJwtStrategy } from './strategies/rt-jwt.strategy';
 import { GoogleStrategy } from './strategies/google.strategy';
 import { GithubStrategy } from './strategies/github.strategy';
+import { MicrosoftStrategy } from './strategies/microsoft.strategy';

 @Module({
   imports: [
<|current_version|>
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UserModule } from 'src/user/user.module';
import { MailerModule } from 'src/mailer/mailer.module';
import { PrismaModule } from 'src/prisma/prisma.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt/dist';
import { JwtStrategy } from './strategies/jwt.strategy';
import { RTJwtStrategy } from './strategies/rt-jwt.strategy';
import { GoogleStrategy } from './strategies/google.strategy';
import { GithubStrategy } from './strategies/github.strategy';
import { MicrosoftStrategy } from './strategies/microsoft.strategy';

@Module({
  imports: [
    PrismaModule,
    UserModule,
    MailerModule,
    PassportModule,
    JwtModule.register({
      secret: process.env.JWT_SECRET,
    }),
  ],
  providers: [
    AuthService,
    JwtStrategy,
    RTJwtStrategy,
    GoogleStrategy,
    GithubStrategy,
  ],
  controllers: [AuthController],
})
export class AuthModule {}

<|next_version|>
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UserModule } from 'src/user/user.module';
import { MailerModule } from 'src/mailer/mailer.module';
import { PrismaModule } from 'src/prisma/prisma.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt/dist';
import { JwtStrategy } from './strategies/jwt.strategy';
import { RTJwtStrategy } from './strategies/rt-jwt.strategy';
import { GoogleStrategy } from './strategies/google.strategy';
import { GithubStrategy } from './strategies/github.strategy';
import { MicrosoftStrategy } from './strategies/microsoft.strategy';

@Module({
  imports: [
    PrismaModule,
    UserModule,
    MailerModule,
    PassportModule,
    JwtModule.register({
      secret: process.env.JWT_SECRET,
    }),
  ],
  providers: [
    AuthService,
    JwtStrategy,
    RTJwtStrategy,
    GoogleStrategy,
    GithubStrategy,
    MicrosoftStrategy,
  ],
  controllers: [AuthController],
})
export class AuthModule {}


fix: null value checked on user_settings.properties

<|original_code|>
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { PubSubService } from 'src/pubsub/pubsub.service';
import { User } from 'src/user/user.model';
import * as E from 'fp-ts/Either';
import { stringToJson } from 'src/utils';
import { UserSettings } from './user-settings.model';
import {
  USER_SETTINGS_NOT_FOUND,
  USER_SETTINGS_UPDATE_FAILED,
} from 'src/errors';

@Injectable()
export class UserSettingsService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly pubsub: PubSubService,
  ) {}

  async fetchUserSettings(user: User) {
    try {
      const dbUserSettings = await this.prisma.userSettings.findUnique({
        where: { userUid: user.uid },
        rejectOnNotFound: true,
      });

      const userSettings: UserSettings = {
        id: dbUserSettings.id,
        userUid: dbUserSettings.userUid,
        properties: JSON.stringify(dbUserSettings.properties),
        updatedOn: dbUserSettings.updatedOn,
      };

      return E.right(userSettings);
    } catch (e) {
      return E.left(USER_SETTINGS_NOT_FOUND);
    }
  }

  async createUserSettings(user: User, properties: string) {
    const jsonProperties = stringToJson(properties);
    if (E.isLeft(jsonProperties)) return E.left(jsonProperties.left);

    const dbUserSettings = await this.prisma.userSettings.create({
      data: {
        properties: jsonProperties.right,
        userUid: user.uid,
      },
    });

    const userSettings: UserSettings = {
      id: dbUserSettings.id,
      userUid: dbUserSettings.userUid,
      properties,
      updatedOn: dbUserSettings.updatedOn,
    };

    return E.right(userSettings);
  }

  async updateUserSettings(user: User, properties: string) {
    const jsonProperties = stringToJson(properties);
    if (E.isLeft(jsonProperties)) return E.left(jsonProperties.left);

    try {
      const dbUpdatedUserSettings = await this.prisma.userSettings.update({
        where: { userUid: user.uid },
        data: {
          properties: jsonProperties.right,
        },
      });

      const updatedUserSettings: UserSettings = {
        id: dbUpdatedUserSettings.id,
        userUid: dbUpdatedUserSettings.userUid,
        properties,
        updatedOn: dbUpdatedUserSettings.updatedOn,
      };

      // Publish subscription for environment creation
      await this.pubsub.publish(
        `user_settings/${user.uid}/updated`,
        updatedUserSettings,
      );

<|edits_diff|>
--- packages/hoppscotch-backend/src/user-settings/user-settings.service.ts
+++ packages/hoppscotch-backend/src/user-settings/user-settings.service.ts
@@ -6,6 +6,7 @@
 import { stringToJson } from 'src/utils';
 import { UserSettings } from './user-settings.model';
 import {
+  USER_SETTINGS_INVALID_PROPERTIES,
   USER_SETTINGS_NOT_FOUND,
   USER_SETTINGS_UPDATE_FAILED,
 } from 'src/errors';
@@ -38,6 +39,8 @@
   }

   async createUserSettings(user: User, properties: string) {
+    if (!properties) return E.left(USER_SETTINGS_INVALID_PROPERTIES);
+
     const jsonProperties = stringToJson(properties);
     if (E.isLeft(jsonProperties)) return E.left(jsonProperties.left);

<|current_version|>
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { PubSubService } from 'src/pubsub/pubsub.service';
import { User } from 'src/user/user.model';
import * as E from 'fp-ts/Either';
import { stringToJson } from 'src/utils';
import { UserSettings } from './user-settings.model';
import {
  USER_SETTINGS_INVALID_PROPERTIES,
  USER_SETTINGS_NOT_FOUND,
  USER_SETTINGS_UPDATE_FAILED,
} from 'src/errors';

@Injectable()
export class UserSettingsService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly pubsub: PubSubService,
  ) {}

  async fetchUserSettings(user: User) {
    try {
      const dbUserSettings = await this.prisma.userSettings.findUnique({
        where: { userUid: user.uid },
        rejectOnNotFound: true,
      });

      const userSettings: UserSettings = {
        id: dbUserSettings.id,
        userUid: dbUserSettings.userUid,
        properties: JSON.stringify(dbUserSettings.properties),
        updatedOn: dbUserSettings.updatedOn,
      };

      return E.right(userSettings);
    } catch (e) {
      return E.left(USER_SETTINGS_NOT_FOUND);
    }
  }

  async createUserSettings(user: User, properties: string) {
    if (!properties) return E.left(USER_SETTINGS_INVALID_PROPERTIES);

    const jsonProperties = stringToJson(properties);
    if (E.isLeft(jsonProperties)) return E.left(jsonProperties.left);

    const dbUserSettings = await this.prisma.userSettings.create({
      data: {
        properties: jsonProperties.right,
        userUid: user.uid,
      },
    });

    const userSettings: UserSettings = {
      id: dbUserSettings.id,
      userUid: dbUserSettings.userUid,
      properties,
      updatedOn: dbUserSettings.updatedOn,
    };

    return E.right(userSettings);
  }

  async updateUserSettings(user: User, properties: string) {
    const jsonProperties = stringToJson(properties);
    if (E.isLeft(jsonProperties)) return E.left(jsonProperties.left);

    try {
      const dbUpdatedUserSettings = await this.prisma.userSettings.update({
        where: { userUid: user.uid },
        data: {
          properties: jsonProperties.right,
        },
      });

      const updatedUserSettings: UserSettings = {
        id: dbUpdatedUserSettings.id,
        userUid: dbUpdatedUserSettings.userUid,
        properties,
        updatedOn: dbUpdatedUserSettings.updatedOn,
      };

      // Publish subscription for environment creation
      await this.pubsub.publish(
        `user_settings/${user.uid}/updated`,
        updatedUserSettings,
      );

<|next_version|>
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { PubSubService } from 'src/pubsub/pubsub.service';
import { User } from 'src/user/user.model';
import * as E from 'fp-ts/Either';
import { stringToJson } from 'src/utils';
import { UserSettings } from './user-settings.model';
import {
  USER_SETTINGS_INVALID_PROPERTIES,
  USER_SETTINGS_NOT_FOUND,
  USER_SETTINGS_UPDATE_FAILED,
} from 'src/errors';

@Injectable()
export class UserSettingsService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly pubsub: PubSubService,
  ) {}

  async fetchUserSettings(user: User) {
    try {
      const dbUserSettings = await this.prisma.userSettings.findUnique({
        where: { userUid: user.uid },
        rejectOnNotFound: true,
      });

      const userSettings: UserSettings = {
        id: dbUserSettings.id,
        userUid: dbUserSettings.userUid,
        properties: JSON.stringify(dbUserSettings.properties),
        updatedOn: dbUserSettings.updatedOn,
      };

      return E.right(userSettings);
    } catch (e) {
      return E.left(USER_SETTINGS_NOT_FOUND);
    }
  }

  async createUserSettings(user: User, properties: string) {
    if (!properties) return E.left(USER_SETTINGS_INVALID_PROPERTIES);

    const jsonProperties = stringToJson(properties);
    if (E.isLeft(jsonProperties)) return E.left(jsonProperties.left);

    const dbUserSettings = await this.prisma.userSettings.create({
      data: {
        properties: jsonProperties.right,
        userUid: user.uid,
      },
    });

    const userSettings: UserSettings = {
      id: dbUserSettings.id,
      userUid: dbUserSettings.userUid,
      properties,
      updatedOn: dbUserSettings.updatedOn,
    };

    return E.right(userSettings);
  }

  async updateUserSettings(user: User, properties: string) {
    if (!properties) return E.left(USER_SETTINGS_INVALID_PROPERTIES);

    const jsonProperties = stringToJson(properties);
    if (E.isLeft(jsonProperties)) return E.left(jsonProperties.left);

    try {
      const dbUpdatedUserSettings = await this.prisma.userSettings.update({
        where: { userUid: user.uid },
        data: {
          properties: jsonProperties.right,
        },
      });

      const updatedUserSettings: UserSettings = {
        id: dbUpdatedUserSettings.id,
        userUid: dbUpdatedUserSettings.userUid,
        properties,
        updatedOn: dbUpdatedUserSettings.updatedOn,
      };

      // Publish subscription for environment creation
      await this.pubsub.publish(
        `user_settings/${user.uid}/updated`,
        updatedUserSettings,
      );


Add support for source maps (#17775)

<|original_code|>

  lastCss = css

  I.start('Compile CSS')
  try {
    compiler = await tailwindcss.compile(css, {
      base: '/',
      loadStylesheet,
      loadModule,
    })
  } finally {
    I.end('Compile CSS')
    I.end(`Create compiler`)
  }

  classes.clear()
}

async function loadStylesheet(id: string, base: string) {
  function load() {
    if (id === 'tailwindcss') {
      return {
        base,
        content: assets.css.index,
      }
    } else if (
      id === 'tailwindcss/preflight' ||
      id === 'tailwindcss/preflight.css' ||
      id === './preflight.css'
    ) {
      return {
        base,
        content: assets.css.preflight,
      }
    } else if (
      id === 'tailwindcss/theme' ||
      id === 'tailwindcss/theme.css' ||
      id === './theme.css'
    ) {
      return {
        base,
        content: assets.css.theme,
      }
    } else if (
      id === 'tailwindcss/utilities' ||
      id === 'tailwindcss/utilities.css' ||
      id === './utilities.css'
    ) {
      return {
        base,
        content: assets.css.utilities,
      }
    }

    throw new Error(`The browser build does not support @import for "${id}"`)
  }

  try {
    let sheet = load()

    I.hit(`Loaded stylesheet`, {
      id,
      base,
      size: sheet.content.length,
    })

    return sheet
  } catch (err) {
    I.hit(`Failed to load stylesheet`, {
      id,
      base,
      error: (err as Error).message ?? err,
    })
<|edits_diff|>
--- packages/@tailwindcss-browser/src/index.ts
+++ packages/@tailwindcss-browser/src/index.ts
@@ -20,6 +20,7 @@
   function load() {
     if (id === 'tailwindcss') {
       return {
+        path: 'virtual:tailwindcss/index.css',
         base,
         content: assets.css.index,
       }
@@ -29,6 +30,7 @@
       id === './preflight.css'
     ) {
       return {
+        path: 'virtual:tailwindcss/preflight.css',
         base,
         content: assets.css.preflight,
       }
@@ -38,6 +40,7 @@
       id === './theme.css'
     ) {
       return {
+        path: 'virtual:tailwindcss/theme.css',
         base,
         content: assets.css.theme,
       }
<|current_version|>

  lastCss = css

  I.start('Compile CSS')
  try {
    compiler = await tailwindcss.compile(css, {
      base: '/',
      loadStylesheet,
      loadModule,
    })
  } finally {
    I.end('Compile CSS')
    I.end(`Create compiler`)
  }

  classes.clear()
}

async function loadStylesheet(id: string, base: string) {
  function load() {
    if (id === 'tailwindcss') {
      return {
        path: 'virtual:tailwindcss/index.css',
        base,
        content: assets.css.index,
      }
    } else if (
      id === 'tailwindcss/preflight' ||
      id === 'tailwindcss/preflight.css' ||
      id === './preflight.css'
    ) {
      return {
        path: 'virtual:tailwindcss/preflight.css',
        base,
        content: assets.css.preflight,
      }
    } else if (
      id === 'tailwindcss/theme' ||
      id === 'tailwindcss/theme.css' ||
      id === './theme.css'
    ) {
      return {
        path: 'virtual:tailwindcss/theme.css',
        base,
        content: assets.css.theme,
      }
    } else if (
      id === 'tailwindcss/utilities' ||
      id === 'tailwindcss/utilities.css' ||
      id === './utilities.css'
    ) {
      return {
        base,
        content: assets.css.utilities,
      }
    }

    throw new Error(`The browser build does not support @import for "${id}"`)
  }

  try {
    let sheet = load()

    I.hit(`Loaded stylesheet`, {
      id,
      base,
      size: sheet.content.length,
    })

    return sheet
  } catch (err) {
    I.hit(`Failed to load stylesheet`, {
      id,
      base,
      error: (err as Error).message ?? err,
    })
<|next_version|>

  lastCss = css

  I.start('Compile CSS')
  try {
    compiler = await tailwindcss.compile(css, {
      base: '/',
      loadStylesheet,
      loadModule,
    })
  } finally {
    I.end('Compile CSS')
    I.end(`Create compiler`)
  }

  classes.clear()
}

async function loadStylesheet(id: string, base: string) {
  function load() {
    if (id === 'tailwindcss') {
      return {
        path: 'virtual:tailwindcss/index.css',
        base,
        content: assets.css.index,
      }
    } else if (
      id === 'tailwindcss/preflight' ||
      id === 'tailwindcss/preflight.css' ||
      id === './preflight.css'
    ) {
      return {
        path: 'virtual:tailwindcss/preflight.css',
        base,
        content: assets.css.preflight,
      }
    } else if (
      id === 'tailwindcss/theme' ||
      id === 'tailwindcss/theme.css' ||
      id === './theme.css'
    ) {
      return {
        path: 'virtual:tailwindcss/theme.css',
        base,
        content: assets.css.theme,
      }
    } else if (
      id === 'tailwindcss/utilities' ||
      id === 'tailwindcss/utilities.css' ||
      id === './utilities.css'
    ) {
      return {
        path: 'virtual:tailwindcss/utilities.css',
        base,
        content: assets.css.utilities,
      }
    }

    throw new Error(`The browser build does not support @import for "${id}"`)
  }

  try {
    let sheet = load()

    I.hit(`Loaded stylesheet`, {
      id,
      base,
      size: sheet.content.length,
    })

    return sheet
  } catch (err) {
    I.hit(`Failed to load stylesheet`, {
      id,
      base,
      error: (err as Error).message ?? err,
    })


Add `self-baseline-last` (#17476)

<|original_code|>
      '-place-self-center',
      '-place-self-stretch',
      'place-self-auto/foo',
      'place-self-start/foo',
      'place-self-end/foo',
      'place-self-center/foo',
      'place-self-stretch/foo',
    ]),
  ).toEqual('')
})

test('self', async () => {
  expect(
    await run([
      'self-auto',
      'self-start',
      'self-end',
      'self-end-safe',
      'self-center',
      'self-center-safe',
      'self-stretch',
      'self-baseline',
    ]),
  ).toMatchInlineSnapshot(`
    ".self-auto {
      align-self: auto;
    }

    .self-baseline {
      align-self: baseline;
    }

    .self-center {
      align-self: center;
    }

    .self-center-safe {
      align-self: safe center;
    }

    .self-end {
      align-self: flex-end;
    }

    .self-end-safe {
      align-self: safe flex-end;
    }

    .self-start {
      align-self: flex-start;
    }

    .self-stretch {
      align-self: stretch;
<|edits_diff|>
--- packages/tailwindcss/src/utilities.test.ts
+++ packages/tailwindcss/src/utilities.test.ts
@@ -20,6 +20,7 @@
       'self-center-safe',
       'self-stretch',
       'self-baseline',
+      'self-baseline-last',
     ]),
   ).toMatchInlineSnapshot(`
     ".self-auto {
<|current_version|>
      '-place-self-center',
      '-place-self-stretch',
      'place-self-auto/foo',
      'place-self-start/foo',
      'place-self-end/foo',
      'place-self-center/foo',
      'place-self-stretch/foo',
    ]),
  ).toEqual('')
})

test('self', async () => {
  expect(
    await run([
      'self-auto',
      'self-start',
      'self-end',
      'self-end-safe',
      'self-center',
      'self-center-safe',
      'self-stretch',
      'self-baseline',
      'self-baseline-last',
    ]),
  ).toMatchInlineSnapshot(`
    ".self-auto {
      align-self: auto;
    }

    .self-baseline {
      align-self: baseline;
    }

    .self-center {
      align-self: center;
    }

    .self-center-safe {
      align-self: safe center;
    }

    .self-end {
      align-self: flex-end;
    }

    .self-end-safe {
      align-self: safe flex-end;
    }

    .self-start {
      align-self: flex-start;
    }

    .self-stretch {
      align-self: stretch;
<|next_version|>
      '-place-self-center',
      '-place-self-stretch',
      'place-self-auto/foo',
      'place-self-start/foo',
      'place-self-end/foo',
      'place-self-center/foo',
      'place-self-stretch/foo',
    ]),
  ).toEqual('')
})

test('self', async () => {
  expect(
    await run([
      'self-auto',
      'self-start',
      'self-end',
      'self-end-safe',
      'self-center',
      'self-center-safe',
      'self-stretch',
      'self-baseline',
      'self-baseline-last',
    ]),
  ).toMatchInlineSnapshot(`
    ".self-auto {
      align-self: auto;
    }

    .self-baseline {
      align-self: baseline;
    }

    .self-baseline-last {
      align-self: last baseline;
    }

    .self-center {
      align-self: center;
    }

    .self-center-safe {
      align-self: safe center;
    }

    .self-end {
      align-self: flex-end;
    }

    .self-end-safe {
      align-self: safe flex-end;
    }

    .self-start {
      align-self: flex-start;
    }

    .self-stretch {
      align-self: stretch;


Revert: Only expose used CSS variables (#16403)

<|original_code|>
import { defineConfig } from 'tsup'

export default defineConfig([
  {
    format: ['esm'],
    minify: true,
    dts: true,
    entry: {
      lib: 'src/index.ts',
      plugin: 'src/plugin.ts',
      colors: 'src/compat/colors.ts',
      'default-theme': 'src/compat/default-theme.ts',
      'flatten-color-palette': 'src/compat/flatten-color-palette.ts',
    },
  },
  {
    format: ['cjs'],
    minify: true,
    dts: true,
    entry: {
      plugin: 'src/plugin.cts',
      lib: 'src/index.cts',
      colors: 'src/compat/colors.cts',
      'default-theme': 'src/compat/default-theme.cts',
      'flatten-color-palette': 'src/compat/flatten-color-palette.cts',
    },
  },
])

<|edits_diff|>
--- packages/tailwindcss/tsup.config.ts
+++ packages/tailwindcss/tsup.config.ts
@@ -11,6 +11,9 @@
       colors: 'src/compat/colors.ts',
       'default-theme': 'src/compat/default-theme.ts',
       'flatten-color-palette': 'src/compat/flatten-color-palette.ts',
+    },
+    define: {
+      'process.env.FEATURES_ENV': JSON.stringify(process.env.FEATURES_ENV ?? 'insiders'),
     },
   },
   {
<|current_version|>
import { defineConfig } from 'tsup'

export default defineConfig([
  {
    format: ['esm'],
    minify: true,
    dts: true,
    entry: {
      lib: 'src/index.ts',
      plugin: 'src/plugin.ts',
      colors: 'src/compat/colors.ts',
      'default-theme': 'src/compat/default-theme.ts',
      'flatten-color-palette': 'src/compat/flatten-color-palette.ts',
    },
    define: {
      'process.env.FEATURES_ENV': JSON.stringify(process.env.FEATURES_ENV ?? 'insiders'),
    },
  },
  {
    format: ['cjs'],
    minify: true,
    dts: true,
    entry: {
      plugin: 'src/plugin.cts',
      lib: 'src/index.cts',
      colors: 'src/compat/colors.cts',
      'default-theme': 'src/compat/default-theme.cts',
      'flatten-color-palette': 'src/compat/flatten-color-palette.cts',
    },
  },
])

<|next_version|>
import { defineConfig } from 'tsup'

export default defineConfig([
  {
    format: ['esm'],
    minify: true,
    dts: true,
    entry: {
      lib: 'src/index.ts',
      plugin: 'src/plugin.ts',
      colors: 'src/compat/colors.ts',
      'default-theme': 'src/compat/default-theme.ts',
      'flatten-color-palette': 'src/compat/flatten-color-palette.ts',
    },
    define: {
      'process.env.FEATURES_ENV': JSON.stringify(process.env.FEATURES_ENV ?? 'insiders'),
    },
  },
  {
    format: ['cjs'],
    minify: true,
    dts: true,
    entry: {
      plugin: 'src/plugin.cts',
      lib: 'src/index.cts',
      colors: 'src/compat/colors.cts',
      'default-theme': 'src/compat/default-theme.cts',
      'flatten-color-palette': 'src/compat/flatten-color-palette.cts',
    },
    define: {
      'process.env.FEATURES_ENV': JSON.stringify(process.env.FEATURES_ENV ?? 'insiders'),
    },
  },
])


Take `NODE_PATH` into account when resolving modules (#16274)

<|original_code|>
}

// Attempts to import the module using the native `import()` function. If this
// fails, it sets up `jiti` and attempts to import this way so that `.ts` files
// can be resolved properly.
let jiti: null | Jiti = null
async function importModule(path: string): Promise<any> {
  if (typeof globalThis.__tw_load === 'function') {
    let module = await globalThis.__tw_load(path)
    if (module) {
      return module
    }
  }

  try {
    return await import(path)
  } catch (error) {
    jiti ??= createJiti(import.meta.url, { moduleCache: false, fsCache: false })
    return await jiti.import(path)
  }
}

const cssResolver = EnhancedResolve.ResolverFactory.createResolver({
  fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000),
  useSyncFileSystemCalls: true,
  extensions: ['.css'],
  mainFields: ['style'],
  conditionNames: ['style'],
})
async function resolveCssId(
  id: string,
  base: string,
  customCssResolver?: Resolver,
): Promise<string | false | undefined> {
  if (typeof globalThis.__tw_resolve === 'function') {
    let resolved = globalThis.__tw_resolve(id, base)
    if (resolved) {
      return Promise.resolve(resolved)
    }
  }

  if (customCssResolver) {
    let customResolution = await customCssResolver(id, base)
    if (customResolution) {
      return customResolution
    }
  }

  return runResolver(cssResolver, id, base)
}

const esmResolver = EnhancedResolve.ResolverFactory.createResolver({
  fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000),
  useSyncFileSystemCalls: true,
  extensions: ['.js', '.json', '.node', '.ts'],
  conditionNames: ['node', 'import'],
})

const cjsResolver = EnhancedResolve.ResolverFactory.createResolver({
  fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000),
  useSyncFileSystemCalls: true,
  extensions: ['.js', '.json', '.node', '.ts'],
  conditionNames: ['node', 'require'],
})

async function resolveJsId(
  id: string,
  base: string,
  customJsResolver?: Resolver,
): Promise<string | false | undefined> {
  if (typeof globalThis.__tw_resolve === 'function') {
    let resolved = globalThis.__tw_resolve(id, base)
    if (resolved) {
      return Promise.resolve(resolved)
    }
  }

  if (customJsResolver) {
    let customResolution = await customJsResolver(id, base)
    if (customResolution) {
      return customResolution
    }
  }

  return runResolver(esmResolver, id, base).catch(() => runResolver(cjsResolver, id, base))
}

<|edits_diff|>
--- packages/@tailwindcss-node/src/compile.ts
+++ packages/@tailwindcss-node/src/compile.ts
@@ -20,12 +20,15 @@
   }
 }

+const modules = ['node_modules', ...(process.env.NODE_PATH ? [process.env.NODE_PATH] : [])]
+
 const cssResolver = EnhancedResolve.ResolverFactory.createResolver({
   fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000),
   useSyncFileSystemCalls: true,
   extensions: ['.css'],
   mainFields: ['style'],
   conditionNames: ['style'],
+  modules,
 })
 async function resolveCssId(
   id: string,
@@ -54,6 +57,7 @@
   useSyncFileSystemCalls: true,
   extensions: ['.js', '.json', '.node', '.ts'],
   conditionNames: ['node', 'import'],
+  modules,
 })

 const cjsResolver = EnhancedResolve.ResolverFactory.createResolver({
<|current_version|>
}

// Attempts to import the module using the native `import()` function. If this
// fails, it sets up `jiti` and attempts to import this way so that `.ts` files
// can be resolved properly.
let jiti: null | Jiti = null
async function importModule(path: string): Promise<any> {
  if (typeof globalThis.__tw_load === 'function') {
    let module = await globalThis.__tw_load(path)
    if (module) {
      return module
    }
  }

  try {
    return await import(path)
  } catch (error) {
    jiti ??= createJiti(import.meta.url, { moduleCache: false, fsCache: false })
    return await jiti.import(path)
  }
}

const modules = ['node_modules', ...(process.env.NODE_PATH ? [process.env.NODE_PATH] : [])]

const cssResolver = EnhancedResolve.ResolverFactory.createResolver({
  fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000),
  useSyncFileSystemCalls: true,
  extensions: ['.css'],
  mainFields: ['style'],
  conditionNames: ['style'],
  modules,
})
async function resolveCssId(
  id: string,
  base: string,
  customCssResolver?: Resolver,
): Promise<string | false | undefined> {
  if (typeof globalThis.__tw_resolve === 'function') {
    let resolved = globalThis.__tw_resolve(id, base)
    if (resolved) {
      return Promise.resolve(resolved)
    }
  }

  if (customCssResolver) {
    let customResolution = await customCssResolver(id, base)
    if (customResolution) {
      return customResolution
    }
  }

  return runResolver(cssResolver, id, base)
}

const esmResolver = EnhancedResolve.ResolverFactory.createResolver({
  fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000),
  useSyncFileSystemCalls: true,
  extensions: ['.js', '.json', '.node', '.ts'],
  conditionNames: ['node', 'import'],
  modules,
})

const cjsResolver = EnhancedResolve.ResolverFactory.createResolver({
  fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000),
  useSyncFileSystemCalls: true,
  extensions: ['.js', '.json', '.node', '.ts'],
  conditionNames: ['node', 'require'],
})

async function resolveJsId(
  id: string,
  base: string,
  customJsResolver?: Resolver,
): Promise<string | false | undefined> {
  if (typeof globalThis.__tw_resolve === 'function') {
    let resolved = globalThis.__tw_resolve(id, base)
    if (resolved) {
      return Promise.resolve(resolved)
    }
  }

  if (customJsResolver) {
    let customResolution = await customJsResolver(id, base)
    if (customResolution) {
      return customResolution
    }
  }

  return runResolver(esmResolver, id, base).catch(() => runResolver(cjsResolver, id, base))
}

<|next_version|>
}

// Attempts to import the module using the native `import()` function. If this
// fails, it sets up `jiti` and attempts to import this way so that `.ts` files
// can be resolved properly.
let jiti: null | Jiti = null
async function importModule(path: string): Promise<any> {
  if (typeof globalThis.__tw_load === 'function') {
    let module = await globalThis.__tw_load(path)
    if (module) {
      return module
    }
  }

  try {
    return await import(path)
  } catch (error) {
    jiti ??= createJiti(import.meta.url, { moduleCache: false, fsCache: false })
    return await jiti.import(path)
  }
}

const modules = ['node_modules', ...(process.env.NODE_PATH ? [process.env.NODE_PATH] : [])]

const cssResolver = EnhancedResolve.ResolverFactory.createResolver({
  fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000),
  useSyncFileSystemCalls: true,
  extensions: ['.css'],
  mainFields: ['style'],
  conditionNames: ['style'],
  modules,
})
async function resolveCssId(
  id: string,
  base: string,
  customCssResolver?: Resolver,
): Promise<string | false | undefined> {
  if (typeof globalThis.__tw_resolve === 'function') {
    let resolved = globalThis.__tw_resolve(id, base)
    if (resolved) {
      return Promise.resolve(resolved)
    }
  }

  if (customCssResolver) {
    let customResolution = await customCssResolver(id, base)
    if (customResolution) {
      return customResolution
    }
  }

  return runResolver(cssResolver, id, base)
}

const esmResolver = EnhancedResolve.ResolverFactory.createResolver({
  fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000),
  useSyncFileSystemCalls: true,
  extensions: ['.js', '.json', '.node', '.ts'],
  conditionNames: ['node', 'import'],
  modules,
})

const cjsResolver = EnhancedResolve.ResolverFactory.createResolver({
  fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000),
  useSyncFileSystemCalls: true,
  extensions: ['.js', '.json', '.node', '.ts'],
  conditionNames: ['node', 'require'],
  modules,
})

async function resolveJsId(
  id: string,
  base: string,
  customJsResolver?: Resolver,
): Promise<string | false | undefined> {
  if (typeof globalThis.__tw_resolve === 'function') {
    let resolved = globalThis.__tw_resolve(id, base)
    if (resolved) {
      return Promise.resolve(resolved)
    }
  }

  if (customJsResolver) {
    let customResolution = await customJsResolver(id, base)
    if (customResolution) {
      return customResolution
    }
  }

  return runResolver(esmResolver, id, base).catch(() => runResolver(cjsResolver, id, base))
}


Add support for `important` in v4 (#14448)

<|original_code|>
  type UserConfig,
} from './types'

export interface ConfigFile {
  path?: string
  base: string
  config: UserConfig
}

interface ResolutionContext {
  design: DesignSystem
  configs: UserConfig[]
  plugins: PluginWithConfig[]
  content: ResolvedContentConfig
  theme: Record<string, ThemeValue>
  extend: Record<string, ThemeValue[]>
  result: ResolvedConfig
}

let minimal: ResolvedConfig = {
  blocklist: [],
  prefix: '',
  darkMode: null,
  theme: {},
  plugins: [],
  content: {
    files: [],
  },
}

export function resolveConfig(design: DesignSystem, files: ConfigFile[]): ResolvedConfig {
  let ctx: ResolutionContext = {
    design,
    configs: [],
    plugins: [],
    content: {
      files: [],
    },
    theme: {},
    extend: {},

    // Start with a minimal valid, but empty config
    result: structuredClone(minimal),
  }

  for (let file of files) {
    extractConfigs(ctx, file)
  }

  // Merge top level keys
  for (let config of ctx.configs) {
    if ('darkMode' in config && config.darkMode !== undefined) {
      ctx.result.darkMode = config.darkMode ?? null
    }

    if ('prefix' in config && config.prefix !== undefined) {
      ctx.result.prefix = config.prefix ?? ''
    }

    if ('blocklist' in config && config.blocklist !== undefined) {
      ctx.result.blocklist = config.blocklist ?? []
    }
  }

  // Merge themes
  mergeTheme(ctx)

  return {
    ...ctx.result,
    content: ctx.content,
    theme: ctx.theme as ResolvedConfig['theme'],
    plugins: ctx.plugins,
  }
}

function mergeThemeExtension(
  themeValue: ThemeValue | ThemeValue[],
  extensionValue: ThemeValue | ThemeValue[],
) {
  // When we have an array of objects, we do want to merge it
  if (Array.isArray(themeValue) && isPlainObject(themeValue[0])) {
    return themeValue.concat(extensionValue)
  }

  // When the incoming value is an array, and the existing config is an object,
<|edits_diff|>
--- packages/tailwindcss/src/compat/config/resolve-config.ts
+++ packages/tailwindcss/src/compat/config/resolve-config.ts
@@ -20,6 +20,7 @@
 let minimal: ResolvedConfig = {
   blocklist: [],
   prefix: '',
+  important: false,
   darkMode: null,
   theme: {},
   plugins: [],
<|current_version|>
  type UserConfig,
} from './types'

export interface ConfigFile {
  path?: string
  base: string
  config: UserConfig
}

interface ResolutionContext {
  design: DesignSystem
  configs: UserConfig[]
  plugins: PluginWithConfig[]
  content: ResolvedContentConfig
  theme: Record<string, ThemeValue>
  extend: Record<string, ThemeValue[]>
  result: ResolvedConfig
}

let minimal: ResolvedConfig = {
  blocklist: [],
  prefix: '',
  important: false,
  darkMode: null,
  theme: {},
  plugins: [],
  content: {
    files: [],
  },
}

export function resolveConfig(design: DesignSystem, files: ConfigFile[]): ResolvedConfig {
  let ctx: ResolutionContext = {
    design,
    configs: [],
    plugins: [],
    content: {
      files: [],
    },
    theme: {},
    extend: {},

    // Start with a minimal valid, but empty config
    result: structuredClone(minimal),
  }

  for (let file of files) {
    extractConfigs(ctx, file)
  }

  // Merge top level keys
  for (let config of ctx.configs) {
    if ('darkMode' in config && config.darkMode !== undefined) {
      ctx.result.darkMode = config.darkMode ?? null
    }

    if ('prefix' in config && config.prefix !== undefined) {
      ctx.result.prefix = config.prefix ?? ''
    }

    if ('blocklist' in config && config.blocklist !== undefined) {
      ctx.result.blocklist = config.blocklist ?? []
    }
  }

  // Merge themes
  mergeTheme(ctx)

  return {
    ...ctx.result,
    content: ctx.content,
    theme: ctx.theme as ResolvedConfig['theme'],
    plugins: ctx.plugins,
  }
}

function mergeThemeExtension(
  themeValue: ThemeValue | ThemeValue[],
  extensionValue: ThemeValue | ThemeValue[],
) {
  // When we have an array of objects, we do want to merge it
  if (Array.isArray(themeValue) && isPlainObject(themeValue[0])) {
    return themeValue.concat(extensionValue)
  }

  // When the incoming value is an array, and the existing config is an object,
<|next_version|>
  type UserConfig,
} from './types'

export interface ConfigFile {
  path?: string
  base: string
  config: UserConfig
}

interface ResolutionContext {
  design: DesignSystem
  configs: UserConfig[]
  plugins: PluginWithConfig[]
  content: ResolvedContentConfig
  theme: Record<string, ThemeValue>
  extend: Record<string, ThemeValue[]>
  result: ResolvedConfig
}

let minimal: ResolvedConfig = {
  blocklist: [],
  prefix: '',
  important: false,
  darkMode: null,
  theme: {},
  plugins: [],
  content: {
    files: [],
  },
}

export function resolveConfig(design: DesignSystem, files: ConfigFile[]): ResolvedConfig {
  let ctx: ResolutionContext = {
    design,
    configs: [],
    plugins: [],
    content: {
      files: [],
    },
    theme: {},
    extend: {},

    // Start with a minimal valid, but empty config
    result: structuredClone(minimal),
  }

  for (let file of files) {
    extractConfigs(ctx, file)
  }

  // Merge top level keys
  for (let config of ctx.configs) {
    if ('darkMode' in config && config.darkMode !== undefined) {
      ctx.result.darkMode = config.darkMode ?? null
    }

    if ('prefix' in config && config.prefix !== undefined) {
      ctx.result.prefix = config.prefix ?? ''
    }

    if ('blocklist' in config && config.blocklist !== undefined) {
      ctx.result.blocklist = config.blocklist ?? []
    }

    if ('important' in config && config.important !== undefined) {
      ctx.result.important = config.important ?? false
    }
  }

  // Merge themes
  mergeTheme(ctx)

  return {
    ...ctx.result,
    content: ctx.content,
    theme: ctx.theme as ResolvedConfig['theme'],
    plugins: ctx.plugins,
  }
}

function mergeThemeExtension(
  themeValue: ThemeValue | ThemeValue[],
  extensionValue: ThemeValue | ThemeValue[],
) {
  // When we have an array of objects, we do want to merge it
  if (Array.isArray(themeValue) && isPlainObject(themeValue[0])) {
    return themeValue.concat(extensionValue)
  }

  // When the incoming value is an array, and the existing config is an object,


Add support for the CSS `theme()` function  (#14177)

<|original_code|>
    let themeKey = this.#resolveKey(candidateValue, themeKeys)

    if (!themeKey) return null

    let extra = {} as Record<string, string>
    for (let name of nestedKeys) {
      let nestedValue = this.#var(`${themeKey}${name}`)
      if (nestedValue) {
        extra[name] = nestedValue
      }
    }

    return [this.#var(themeKey)!, extra]
  }

  namespace(namespace: string) {
    let values = new Map<string | null, string>()
    let prefix = `${namespace}-`

    for (let [key, value] of this.values) {
      if (key === namespace) {
        values.set(null, value.value)
      } else if (key.startsWith(prefix)) {
        values.set(key.slice(prefix.length), value.value)
      }
    }

    return values
  }

  resolveNamespace(namespace: string) {
    let values = new Map<string | null, string>()
    let prefix = `${namespace}-`

    for (let [key, value] of this.values) {
      if (key === namespace) {
        values.set(null, value.isInline ? value.value : this.#var(key)!)
      } else if (key.startsWith(prefix)) {
        values.set(key.slice(prefix.length), value.isInline ? value.value : this.#var(key)!)
      }
    }

    return values
  }
}

export type ThemeKey =
  | '--accent-color'
  | '--animate'
  | '--aspect-ratio'
  | '--backdrop-blur'
  | '--backdrop-brightness'
  | '--backdrop-contrast'
  | '--backdrop-grayscale'
  | '--backdrop-hue-rotate'
  | '--backdrop-invert'
  | '--backdrop-opacity'
  | '--backdrop-saturate'
  | '--backdrop-sepia'
  | '--background-color'
  | '--background-image'
<|edits_diff|>
--- packages/tailwindcss/src/theme.ts
+++ packages/tailwindcss/src/theme.ts
@@ -20,6 +20,10 @@
     for (let [key, value] of this.values) {
       if (key === namespace) {
         values.set(null, value.value)
+      } else if (key.startsWith(`${prefix}-`)) {
+        // Preserve `--` prefix for sub-variables
+        // e.g. `--font-size-sm--line-height`
+        values.set(key.slice(namespace.length), value.value)
       } else if (key.startsWith(prefix)) {
         values.set(key.slice(prefix.length), value.value)
       }
<|current_version|>
    let themeKey = this.#resolveKey(candidateValue, themeKeys)

    if (!themeKey) return null

    let extra = {} as Record<string, string>
    for (let name of nestedKeys) {
      let nestedValue = this.#var(`${themeKey}${name}`)
      if (nestedValue) {
        extra[name] = nestedValue
      }
    }

    return [this.#var(themeKey)!, extra]
  }

  namespace(namespace: string) {
    let values = new Map<string | null, string>()
    let prefix = `${namespace}-`

    for (let [key, value] of this.values) {
      if (key === namespace) {
        values.set(null, value.value)
      } else if (key.startsWith(`${prefix}-`)) {
        // Preserve `--` prefix for sub-variables
        // e.g. `--font-size-sm--line-height`
        values.set(key.slice(namespace.length), value.value)
      } else if (key.startsWith(prefix)) {
        values.set(key.slice(prefix.length), value.value)
      }
    }

    return values
  }

  resolveNamespace(namespace: string) {
    let values = new Map<string | null, string>()
    let prefix = `${namespace}-`

    for (let [key, value] of this.values) {
      if (key === namespace) {
        values.set(null, value.isInline ? value.value : this.#var(key)!)
      } else if (key.startsWith(prefix)) {
        values.set(key.slice(prefix.length), value.isInline ? value.value : this.#var(key)!)
      }
    }

    return values
  }
}

export type ThemeKey =
  | '--accent-color'
  | '--animate'
  | '--aspect-ratio'
  | '--backdrop-blur'
  | '--backdrop-brightness'
  | '--backdrop-contrast'
  | '--backdrop-grayscale'
  | '--backdrop-hue-rotate'
  | '--backdrop-invert'
  | '--backdrop-opacity'
  | '--backdrop-saturate'
  | '--backdrop-sepia'
  | '--background-color'
  | '--background-image'
<|next_version|>
    let themeKey = this.#resolveKey(candidateValue, themeKeys)

    if (!themeKey) return null

    let extra = {} as Record<string, string>
    for (let name of nestedKeys) {
      let nestedValue = this.#var(`${themeKey}${name}`)
      if (nestedValue) {
        extra[name] = nestedValue
      }
    }

    return [this.#var(themeKey)!, extra]
  }

  namespace(namespace: string) {
    let values = new Map<string | null, string>()
    let prefix = `${namespace}-`

    for (let [key, value] of this.values) {
      if (key === namespace) {
        values.set(null, value.value)
      } else if (key.startsWith(`${prefix}-`)) {
        // Preserve `--` prefix for sub-variables
        // e.g. `--font-size-sm--line-height`
        values.set(key.slice(namespace.length), value.value)
      } else if (key.startsWith(prefix)) {
        values.set(key.slice(prefix.length), value.value)
      }
    }

    return values
  }

  resolveNamespace(namespace: string) {
    let values = new Map<string | null, string>()
    let prefix = `${namespace}-`

    for (let [key, value] of this.values) {
      if (key === namespace) {
        values.set(null, value.isInline ? value.value : this.#var(key)!)
      } else if (key.startsWith(`${prefix}-`)) {
        // Preserve `--` prefix for sub-variables
        // e.g. `--font-size-sm--line-height`
        values.set(key.slice(namespace.length), value.value)
      } else if (key.startsWith(prefix)) {
        values.set(key.slice(prefix.length), value.isInline ? value.value : this.#var(key)!)
      }
    }

    return values
  }
}

export type ThemeKey =
  | '--accent-color'
  | '--animate'
  | '--aspect-ratio'
  | '--backdrop-blur'
  | '--backdrop-brightness'
  | '--backdrop-contrast'
  | '--backdrop-grayscale'
  | '--backdrop-hue-rotate'
  | '--backdrop-invert'
  | '--backdrop-opacity'
  | '--backdrop-saturate'
  | '--backdrop-sepia'
  | '--background-color'
  | '--background-image'