Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .babelrc
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"env": {
"test": {
"presets": ["@babel/preset-typescript"],
"plugins": ["@babel/plugin-transform-modules-commonjs"]
}
}
}
}
10 changes: 6 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ jobs:
qa:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
- run: yarn install
- name: Lint
run: yarn lint
- name: Typecheck
run: yarn typecheck
- name: Build
run: yarn build
- name: Test
run: yarn test
6 changes: 4 additions & 2 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
rollup.config.js
rollup.config.mjs
__tests__/
.github/
.github/
tsconfig.json
.babelrc
13 changes: 10 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"version": "0.0.12",
"description": "A tiny Fetch API wrapper that allows you to make http requests without need to handle to send the CSRF Token on every request",
"main": "./dist/requestjs.js",
"module": "./src/index.js",
"module": "./src/index.ts",
"types": "./dist/index.d.ts",
"repository": "https://github.com/rails/request.js",
"author": "Marcelo Lauxen <marcelolauxen16@gmail.com>",
"license": "MIT",
Expand All @@ -18,21 +19,27 @@
"url": "https://github.com/rails/request.js"
},
"scripts": {
"lint": "standard src",
"build": "rollup -c",
"typecheck": "tsc --noEmit",
"test": "jest"
},
"jest": {
"moduleFileExtensions": ["ts", "js"]
},
"devDependencies": {
"@babel/core": "^7.26.10",
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
"@babel/preset-typescript": "^7.28.5",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.3.0",
"babel-jest": "^29.7.0",
"isomorphic-fetch": "^3.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"rollup": "^4.40.1",
"standard": "^17.1.2"
"tslib": "^2.8.1",
"typescript": "^5.9.3"
},
"dependencies": {}
}
9 changes: 5 additions & 4 deletions rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import resolve from "@rollup/plugin-node-resolve"
import terser from "@rollup/plugin-terser"
import pkg from "./package.json" assert { type: 'json' };
import typescript from "@rollup/plugin-typescript"

export default {
input: pkg.module,
input: "src/index.ts",
output: {
file: pkg.main,
file: "dist/requestjs.js",
format: "es",
inlineDynamicImports: true
},
plugins: [
resolve(),
typescript(),
terser({
mangle: false,
compress: false,
Expand All @@ -20,4 +21,4 @@ export default {
}
})
]
}
}
78 changes: 48 additions & 30 deletions src/fetch_request.js → src/fetch_request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,32 @@ import { FetchResponse } from './fetch_response'
import { RequestInterceptor } from './request_interceptor'
import { getCookie, compact, metaContent, stringEntriesFromFormData, mergeEntries } from './lib/utils'

export type ResponseKind = 'html' | 'json' | 'turbo-stream' | 'script'

export interface RequestOptions {
body?: BodyInit | Record<string, unknown> | null
contentType?: string
query?: Record<string, string> | FormData | URLSearchParams
responseKind?: ResponseKind | (string & {})
signal?: AbortSignal
headers?: Record<string, string>
credentials?: RequestCredentials
redirect?: RequestRedirect
keepalive?: boolean
}

export class FetchRequest {
constructor (method, url, options = {}) {
method: string
options: RequestOptions
originalUrl: string

constructor (method: string, url: string | URL, options: RequestOptions = {}) {
this.method = method
this.options = options
this.originalUrl = url.toString()
}

async perform () {
async perform (): Promise<FetchResponse> {
try {
const requestInterceptor = RequestInterceptor.get()
if (requestInterceptor) {
Expand All @@ -19,8 +37,8 @@ export class FetchRequest {
console.error(error)
}

const fetch = window.Turbo ? window.Turbo.fetch : window.fetch
const response = new FetchResponse(await fetch(this.url, this.fetchOptions))
const fetchFunction = window.Turbo ? window.Turbo.fetch : window.fetch
const response = new FetchResponse(await fetchFunction(this.url, this.fetchOptions))

if (response.unauthenticated && response.authenticationURL) {
return Promise.reject(window.location.href = response.authenticationURL)
Expand All @@ -39,13 +57,13 @@ export class FetchRequest {
return response
}

addHeader (key, value) {
addHeader (key: string, value: string): void {
const headers = this.additionalHeaders
headers[key] = value
this.options.headers = headers
}

sameHostname () {
sameHostname (): boolean {
if (!this.originalUrl.startsWith('http:') && !this.originalUrl.startsWith('https:')) {
return true
}
Expand All @@ -57,7 +75,7 @@ export class FetchRequest {
}
}

get fetchOptions () {
get fetchOptions (): RequestInit {
return {
method: this.method.toUpperCase(),
headers: this.headers,
Expand All @@ -69,8 +87,8 @@ export class FetchRequest {
}
}

get headers () {
const baseHeaders = {
get headers (): Record<string, string> {
const baseHeaders: Record<string, string | undefined> = {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': this.contentType,
Accept: this.accept
Expand All @@ -85,11 +103,11 @@ export class FetchRequest {
)
}

get csrfToken () {
return getCookie(metaContent('csrf-param')) || metaContent('csrf-token')
get csrfToken (): string | undefined {
return getCookie(metaContent('csrf-param') || '') || metaContent('csrf-token') || undefined
}

get contentType () {
get contentType (): string | undefined {
if (this.options.contentType) {
return this.options.contentType
} else if (this.body == null || this.body instanceof window.FormData) {
Expand All @@ -101,7 +119,7 @@ export class FetchRequest {
return 'application/json'
}

get accept () {
get accept (): string {
switch (this.responseKind) {
case 'html':
return 'text/html, application/xhtml+xml'
Expand All @@ -116,21 +134,21 @@ export class FetchRequest {
}
}

get body () {
get body (): RequestOptions['body'] {
return this.options.body
}

get query () {
get query (): string {
const originalQuery = (this.originalUrl.split('?')[1] || '').split('#')[0]
const params = new URLSearchParams(originalQuery)

let requestQuery = this.options.query
if (requestQuery instanceof window.FormData) {
requestQuery = stringEntriesFromFormData(requestQuery)
} else if (requestQuery instanceof window.URLSearchParams) {
requestQuery = requestQuery.entries()
let requestQuery: Iterable<[string, string | File]>
if (this.options.query instanceof window.FormData) {
requestQuery = stringEntriesFromFormData(this.options.query)
} else if (this.options.query instanceof window.URLSearchParams) {
requestQuery = this.options.query.entries()
} else {
requestQuery = Object.entries(requestQuery || {})
requestQuery = Object.entries(this.options.query || {})
}

mergeEntries(params, requestQuery)
Expand All @@ -139,42 +157,42 @@ export class FetchRequest {
return (query.length > 0 ? `?${query}` : '')
}

get url () {
get url (): string {
return (this.originalUrl.split('?')[0]).split('#')[0] + this.query
}

get responseKind () {
get responseKind (): string {
return this.options.responseKind || 'html'
}

get signal () {
get signal (): AbortSignal | undefined {
return this.options.signal
}

get redirect () {
get redirect (): RequestRedirect {
return this.options.redirect || 'follow'
}

get credentials () {
get credentials (): RequestCredentials {
return this.options.credentials || 'same-origin'
}

get keepalive () {
get keepalive (): boolean {
return this.options.keepalive || false
}

get additionalHeaders () {
get additionalHeaders (): Record<string, string> {
return this.options.headers || {}
}

get formattedBody () {
get formattedBody (): BodyInit | null | undefined {
const bodyIsAString = Object.prototype.toString.call(this.body) === '[object String]'
const contentTypeIsJson = this.headers['Content-Type'] === 'application/json'

if (contentTypeIsJson && !bodyIsAString) {
return JSON.stringify(this.body)
}

return this.body
return this.body as BodyInit | null | undefined
}
}
Loading