This commit is contained in:
parent
a1d04d652e
commit
3efbfecf9d
@ -32,6 +32,7 @@
|
||||
"mime-types": "^2.1.31",
|
||||
"mkdirp": "^1.0.4",
|
||||
"negotiator": "^0.6.2",
|
||||
"node-fetch": "^3",
|
||||
"pg": "^8.6.0",
|
||||
"pluralize": "^8.0.0",
|
||||
"pug": "^3.0.2",
|
||||
|
@ -28,6 +28,7 @@ specifiers:
|
||||
mime-types: ^2.1.31
|
||||
mkdirp: ^1.0.4
|
||||
negotiator: ^0.6.2
|
||||
node-fetch: ^3
|
||||
pg: ^8.6.0
|
||||
pluralize: ^8.0.0
|
||||
pug: ^3.0.2
|
||||
@ -66,6 +67,7 @@ dependencies:
|
||||
mime-types: 2.1.31
|
||||
mkdirp: 1.0.4
|
||||
negotiator: 0.6.2
|
||||
node-fetch: 3.0.0
|
||||
pg: 8.6.0
|
||||
pluralize: 8.0.0
|
||||
pug: 3.0.2
|
||||
@ -793,6 +795,11 @@ packages:
|
||||
which: 2.0.2
|
||||
dev: true
|
||||
|
||||
/data-uri-to-buffer/3.0.1:
|
||||
resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==}
|
||||
engines: {node: '>= 6'}
|
||||
dev: false
|
||||
|
||||
/debug/4.3.1:
|
||||
resolution: {integrity: sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
@ -1061,6 +1068,13 @@ packages:
|
||||
reusify: 1.0.4
|
||||
dev: true
|
||||
|
||||
/fetch-blob/3.1.2:
|
||||
resolution: {integrity: sha512-hunJbvy/6OLjCD0uuhLdp0mMPzP/yd2ssd1t2FCJsaA7wkWhpbp9xfuNVpv7Ll4jFhzp6T4LAupSiV9uOeg0VQ==}
|
||||
engines: {node: ^12.20 || >= 14.13}
|
||||
dependencies:
|
||||
web-streams-polyfill: 3.1.1
|
||||
dev: false
|
||||
|
||||
/figures/3.2.0:
|
||||
resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
|
||||
engines: {node: '>=8'}
|
||||
@ -1613,6 +1627,14 @@ packages:
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
dev: false
|
||||
|
||||
/node-fetch/3.0.0:
|
||||
resolution: {integrity: sha512-bKMI+C7/T/SPU1lKnbQbwxptpCrG9ashG+VkytmXCPZyuM9jB6VU+hY0oi4lC8LxTtAeWdckNCTa3nrGsAdA3Q==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
dependencies:
|
||||
data-uri-to-buffer: 3.0.1
|
||||
fetch-blob: 3.1.2
|
||||
dev: false
|
||||
|
||||
/nopt/5.0.0:
|
||||
resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==}
|
||||
engines: {node: '>=6'}
|
||||
@ -2456,6 +2478,11 @@ packages:
|
||||
defaults: 1.0.3
|
||||
dev: false
|
||||
|
||||
/web-streams-polyfill/3.1.1:
|
||||
resolution: {integrity: sha512-Czi3fG883e96T4DLEPRvufrF2ydhOOW1+1a6c3gNjH2aIh50DNFBdfwh2AKoOf1rXvpvavAoA11Qdq9+BKjE0Q==}
|
||||
engines: {node: '>= 8'}
|
||||
dev: false
|
||||
|
||||
/which/2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {Instantiable} from '../di'
|
||||
import {ORMUserRepository} from './orm/ORMUserRepository'
|
||||
import {OAuth2LoginConfig} from './external/oauth2/OAuth2LoginController'
|
||||
|
||||
/**
|
||||
* Inferface for type-checking the AuthenticatableRepositories values.
|
||||
@ -21,5 +22,8 @@ export const AuthenticatableRepositories: AuthenticatableRepositoryMapping = {
|
||||
export interface AuthConfig {
|
||||
repositories: {
|
||||
session: keyof AuthenticatableRepositoryMapping,
|
||||
}
|
||||
},
|
||||
sources?: {
|
||||
[key: string]: OAuth2LoginConfig,
|
||||
},
|
||||
}
|
||||
|
95
src/auth/external/oauth2/OAuth2LoginController.ts
vendored
Normal file
95
src/auth/external/oauth2/OAuth2LoginController.ts
vendored
Normal file
@ -0,0 +1,95 @@
|
||||
import {Controller} from '../../../http/Controller'
|
||||
import {Inject, Injectable} from '../../../di'
|
||||
import {Config} from '../../../service/Config'
|
||||
import {Request} from '../../../http/lifecycle/Request'
|
||||
import {ResponseObject, Route} from '../../../http/routing/Route'
|
||||
import {ErrorWithContext} from '../../../util'
|
||||
import {OAuth2Repository} from './OAuth2Repository'
|
||||
import {json} from '../../../http/response/JSONResponseFactory'
|
||||
|
||||
export interface OAuth2LoginConfig {
|
||||
name: string,
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
redirectUrl: string,
|
||||
authorizationCodeField: string,
|
||||
tokenEndpoint: string,
|
||||
tokenEndpointMapping?: {
|
||||
clientId?: string,
|
||||
clientSecret?: string,
|
||||
grantType?: string,
|
||||
codeKey?: string,
|
||||
},
|
||||
tokenEndpointResponseMapping?: {
|
||||
token?: string,
|
||||
expiresIn?: string,
|
||||
expiresAt?: string,
|
||||
},
|
||||
userEndpoint: string,
|
||||
userEndpointResponseMapping?: {
|
||||
identifier?: string,
|
||||
display?: string,
|
||||
},
|
||||
}
|
||||
|
||||
export function isOAuth2LoginConfig(what: unknown): what is OAuth2LoginConfig {
|
||||
return (
|
||||
Boolean(what)
|
||||
&& typeof (what as any).name === 'string'
|
||||
&& typeof (what as any).clientId === 'string'
|
||||
&& typeof (what as any).clientSecret === 'string'
|
||||
&& typeof (what as any).redirectUrl === 'string'
|
||||
&& typeof (what as any).authorizationCodeField === 'string'
|
||||
&& typeof (what as any).tokenEndpoint === 'string'
|
||||
&& typeof (what as any).userEndpoint === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class OAuth2LoginController extends Controller {
|
||||
public static routes(configName: string): void {
|
||||
Route.group(`/auth/${configName}`, () => {
|
||||
Route.get('login', (request: Request) => {
|
||||
const controller = <OAuth2LoginController> request.make(OAuth2LoginController, configName)
|
||||
return controller.getLogin()
|
||||
}).pre('@auth:guest')
|
||||
}).pre('@auth:web')
|
||||
}
|
||||
|
||||
@Inject()
|
||||
protected readonly config!: Config
|
||||
|
||||
constructor(
|
||||
protected readonly request: Request,
|
||||
protected readonly configName: string,
|
||||
) {
|
||||
super(request)
|
||||
}
|
||||
|
||||
public async getLogin(): Promise<ResponseObject> {
|
||||
const repo = this.getRepository()
|
||||
if ( repo.shouldRedirect() ) {
|
||||
return repo.redirect()
|
||||
}
|
||||
|
||||
// We were redirected from the auth source
|
||||
const user = await repo.redeem()
|
||||
return json(user)
|
||||
}
|
||||
|
||||
protected getRepository(): OAuth2Repository {
|
||||
return this.request.make(OAuth2Repository, this.getConfig())
|
||||
}
|
||||
|
||||
protected getConfig(): OAuth2LoginConfig {
|
||||
const config = this.config.get(`auth.sources.${this.configName}`)
|
||||
if ( !isOAuth2LoginConfig(config) ) {
|
||||
throw new ErrorWithContext('Invalid OAuth2 source config.', {
|
||||
configName: this.configName,
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
}
|
156
src/auth/external/oauth2/OAuth2Repository.ts
vendored
Normal file
156
src/auth/external/oauth2/OAuth2Repository.ts
vendored
Normal file
@ -0,0 +1,156 @@
|
||||
import {
|
||||
Authenticatable,
|
||||
AuthenticatableCredentials,
|
||||
AuthenticatableIdentifier,
|
||||
AuthenticatableRepository,
|
||||
} from '../../types'
|
||||
import {Inject, Injectable} from '../../../di'
|
||||
import {
|
||||
Awaitable,
|
||||
dataGetUnsafe,
|
||||
fetch,
|
||||
Maybe,
|
||||
MethodNotSupportedError,
|
||||
UniversalPath,
|
||||
universalPath,
|
||||
uuid4,
|
||||
} from '../../../util'
|
||||
import {OAuth2LoginConfig} from './OAuth2LoginController'
|
||||
import {Session} from '../../../http/session/Session'
|
||||
import {ResponseObject} from '../../../http/routing/Route'
|
||||
import {temporary} from '../../../http/response/TemporaryRedirectResponseFactory'
|
||||
import {Request} from '../../../http/lifecycle/Request'
|
||||
import {Logging} from '../../../service/Logging'
|
||||
import {OAuth2User} from './OAuth2User'
|
||||
|
||||
@Injectable()
|
||||
export class OAuth2Repository implements AuthenticatableRepository {
|
||||
@Inject()
|
||||
protected readonly session!: Session
|
||||
|
||||
@Inject()
|
||||
protected readonly request!: Request
|
||||
|
||||
@Inject()
|
||||
protected readonly logging!: Logging
|
||||
|
||||
constructor(
|
||||
protected readonly config: OAuth2LoginConfig,
|
||||
) { }
|
||||
|
||||
public createByCredentials(): Awaitable<Authenticatable> {
|
||||
throw new MethodNotSupportedError()
|
||||
}
|
||||
|
||||
getByCredentials(credentials: AuthenticatableCredentials): Awaitable<Maybe<Authenticatable>> {
|
||||
return this.getAuthenticatableFromBearer(credentials.credential)
|
||||
}
|
||||
|
||||
getByIdentifier(id: AuthenticatableIdentifier): Awaitable<Maybe<Authenticatable>> {
|
||||
return undefined
|
||||
}
|
||||
|
||||
public getRedirectUrl(state?: string): UniversalPath {
|
||||
const url = universalPath(this.config.redirectUrl)
|
||||
if ( state ) {
|
||||
url.query.append('state', state)
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
public getTokenEndpoint(): UniversalPath {
|
||||
return universalPath(this.config.tokenEndpoint)
|
||||
}
|
||||
|
||||
public getUserEndpoint(): UniversalPath {
|
||||
return universalPath(this.config.userEndpoint)
|
||||
}
|
||||
|
||||
public async redeem(): Promise<Maybe<OAuth2User>> {
|
||||
if ( !this.stateIsValid() ) {
|
||||
return // FIXME throw
|
||||
}
|
||||
|
||||
const body = new URLSearchParams()
|
||||
|
||||
if ( this.config.tokenEndpointMapping ) {
|
||||
if ( this.config.tokenEndpointMapping.clientId ) {
|
||||
body.append(this.config.tokenEndpointMapping.clientId, this.config.clientId)
|
||||
}
|
||||
|
||||
if ( this.config.tokenEndpointMapping.clientSecret ) {
|
||||
body.append(this.config.tokenEndpointMapping.clientSecret, this.config.clientSecret)
|
||||
}
|
||||
|
||||
if ( this.config.tokenEndpointMapping.codeKey ) {
|
||||
body.append(this.config.tokenEndpointMapping.codeKey, String(this.request.input(this.config.authorizationCodeField)))
|
||||
}
|
||||
|
||||
if ( this.config.tokenEndpointMapping.grantType ) {
|
||||
body.append(this.config.tokenEndpointMapping.grantType, 'authorization_code')
|
||||
}
|
||||
}
|
||||
|
||||
this.logging.debug(`Redeeming auth code: ${body.toString()}`)
|
||||
|
||||
const response = await fetch(this.getTokenEndpoint().toRemote, {
|
||||
method: 'post',
|
||||
body: body,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
if ( typeof data !== 'object' || data === null ) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
this.logging.debug(data)
|
||||
const bearer = String(dataGetUnsafe(data, this.config.tokenEndpointResponseMapping?.token ?? 'bearer'))
|
||||
|
||||
this.logging.debug(bearer)
|
||||
if ( !bearer || typeof bearer !== 'string' ) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
return this.getAuthenticatableFromBearer(bearer)
|
||||
}
|
||||
|
||||
public async getAuthenticatableFromBearer(bearer: string): Promise<Maybe<OAuth2User>> {
|
||||
const response = await fetch(this.getUserEndpoint().toRemote, {
|
||||
method: 'get',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${bearer}`,
|
||||
},
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
if ( typeof data !== 'object' || data === null ) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
return new OAuth2User(data, this.config)
|
||||
}
|
||||
|
||||
public stateIsValid(): boolean {
|
||||
const correctState = this.session.get('extollo.auth.oauth2.state', '')
|
||||
const inputState = this.request.input('state') || ''
|
||||
return correctState === inputState
|
||||
}
|
||||
|
||||
public shouldRedirect(): boolean {
|
||||
const codeField = this.config.authorizationCodeField
|
||||
const code = this.request.input(codeField)
|
||||
return !code
|
||||
}
|
||||
|
||||
public async redirect(): Promise<ResponseObject> {
|
||||
const state = uuid4()
|
||||
await this.session.set('extollo.auth.oauth2.state', state)
|
||||
return temporary(this.getRedirectUrl(state).toRemote)
|
||||
}
|
||||
}
|
50
src/auth/external/oauth2/OAuth2User.ts
vendored
Normal file
50
src/auth/external/oauth2/OAuth2User.ts
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
import {Authenticatable, AuthenticatableIdentifier} from '../../types'
|
||||
import {OAuth2LoginConfig} from './OAuth2LoginController'
|
||||
import {Awaitable, dataGetUnsafe, InvalidJSONStateError, JSONState} from '../../../util'
|
||||
|
||||
export class OAuth2User implements Authenticatable {
|
||||
protected displayField: string
|
||||
|
||||
protected identifierField: string
|
||||
|
||||
constructor(
|
||||
protected data: {[key: string]: any},
|
||||
config: OAuth2LoginConfig,
|
||||
) {
|
||||
this.displayField = config.userEndpointResponseMapping?.display || 'name'
|
||||
this.identifierField = config.userEndpointResponseMapping?.identifier || 'id'
|
||||
}
|
||||
|
||||
getDisplayIdentifier(): string {
|
||||
return String(dataGetUnsafe(this.data, this.displayField || 'name', ''))
|
||||
}
|
||||
|
||||
getIdentifier(): AuthenticatableIdentifier {
|
||||
return String(dataGetUnsafe(this.data, this.identifierField || 'id', ''))
|
||||
}
|
||||
|
||||
async dehydrate(): Promise<JSONState> {
|
||||
return {
|
||||
isOAuth2User: true,
|
||||
data: this.data,
|
||||
displayField: this.displayField,
|
||||
identifierField: this.identifierField,
|
||||
}
|
||||
}
|
||||
|
||||
rehydrate(state: JSONState): Awaitable<void> {
|
||||
if (
|
||||
!state.isOAuth2User
|
||||
|| typeof state.data !== 'object'
|
||||
|| state.data === null
|
||||
|| typeof state.displayField !== 'string'
|
||||
|| typeof state.identifierField !== 'string'
|
||||
) {
|
||||
throw new InvalidJSONStateError('OAuth2User state is invalid', { state })
|
||||
}
|
||||
|
||||
this.data = state.data
|
||||
this.identifierField = state.identifierField
|
||||
this.displayField = state.identifierField
|
||||
}
|
||||
}
|
@ -22,3 +22,5 @@ export * from './config'
|
||||
|
||||
export * from './basic-ui/BasicLoginFormRequest'
|
||||
export * from './basic-ui/BasicLoginController'
|
||||
|
||||
export * from './external/oauth2/OAuth2LoginController'
|
||||
|
@ -108,6 +108,7 @@ export class Request extends ScopedContainer implements DataContainer {
|
||||
protected serverResponse: ServerResponse,
|
||||
) {
|
||||
super(Container.getContainer())
|
||||
this.registerSingletonInstance(Request, this)
|
||||
|
||||
this.secure = Boolean((clientRequest.connection as TLSSocket).encrypted)
|
||||
|
||||
@ -124,12 +125,6 @@ export class Request extends ScopedContainer implements DataContainer {
|
||||
minor: clientRequest.httpVersionMinor,
|
||||
}
|
||||
|
||||
this.register(Request)
|
||||
this.instances.push({
|
||||
key: Request,
|
||||
value: this,
|
||||
})
|
||||
|
||||
const parts = url.parse(this.url, true)
|
||||
|
||||
this.path = parts.pathname ?? '/'
|
||||
|
@ -1,39 +0,0 @@
|
||||
import {Inject, Injectable} from '../di'
|
||||
import {ConstraintType, DatabaseService, FieldType, Migration, Schema} from '../orm'
|
||||
|
||||
/**
|
||||
* Migration that creates the sessions table used by the ORMSession backend.
|
||||
*/
|
||||
@Injectable()
|
||||
export default class CreateSessionsTableMigration extends Migration {
|
||||
@Inject()
|
||||
protected readonly db!: DatabaseService
|
||||
|
||||
async up(): Promise<void> {
|
||||
const schema: Schema = this.db.get().schema()
|
||||
const table = await schema.table('sessions')
|
||||
|
||||
table.primaryKey('session_uuid', FieldType.varchar)
|
||||
.required()
|
||||
|
||||
table.column('session_data')
|
||||
.type(FieldType.json)
|
||||
.required()
|
||||
.default('{}')
|
||||
|
||||
table.constraint('session_uuid_ck')
|
||||
.type(ConstraintType.Check)
|
||||
.expression('LENGTH(session_uuid) > 0')
|
||||
|
||||
await schema.commit(table)
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
const schema: Schema = this.db.get().schema()
|
||||
const table = await schema.table('sessions')
|
||||
|
||||
table.dropIfExists()
|
||||
|
||||
await schema.commit(table)
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
import {Inject, Injectable} from '../di'
|
||||
import {DatabaseService, FieldType, Migration, Schema} from '../orm'
|
||||
|
||||
/**
|
||||
* Migration that creates the users table used by @extollo/lib.auth.
|
||||
*/
|
||||
@Injectable()
|
||||
export default class CreateUsersTableMigration extends Migration {
|
||||
@Inject()
|
||||
protected readonly db!: DatabaseService
|
||||
|
||||
async up(): Promise<void> {
|
||||
const schema: Schema = this.db.get().schema()
|
||||
const table = await schema.table('users')
|
||||
|
||||
table.primaryKey('user_id')
|
||||
.required()
|
||||
|
||||
table.column('first_name')
|
||||
.type(FieldType.varchar)
|
||||
.nullable()
|
||||
|
||||
table.column('last_name')
|
||||
.type(FieldType.varchar)
|
||||
.nullable()
|
||||
|
||||
table.column('password_hash')
|
||||
.type(FieldType.text)
|
||||
.nullable()
|
||||
|
||||
table.column('username')
|
||||
.type(FieldType.varchar)
|
||||
.required()
|
||||
.unique()
|
||||
|
||||
await schema.commit(table)
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
const schema: Schema = this.db.get().schema()
|
||||
const table = await schema.table('users')
|
||||
|
||||
table.dropIfExists()
|
||||
|
||||
await schema.commit(table)
|
||||
}
|
||||
}
|
10
src/util/error/MethodNotSupportedError.ts
Normal file
10
src/util/error/MethodNotSupportedError.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import {ErrorWithContext} from './ErrorWithContext'
|
||||
|
||||
export class MethodNotSupportedError extends ErrorWithContext {
|
||||
constructor(
|
||||
message = 'Method not supported',
|
||||
context: {[key: string]: any} = {},
|
||||
) {
|
||||
super(message, context)
|
||||
}
|
||||
}
|
@ -1,3 +1,7 @@
|
||||
import {RequestInfo, RequestInit, Response} from 'node-fetch'
|
||||
import {unsafeESMImport} from './unsafe'
|
||||
export const fetch = (url: RequestInfo, init?: RequestInit): Promise<Response> => unsafeESMImport('node-fetch').then(({default: nodeFetch}) => nodeFetch(url, init))
|
||||
|
||||
export * from './cache/Cache'
|
||||
export * from './cache/InMemCache'
|
||||
|
||||
@ -10,6 +14,7 @@ export * from './collection/where'
|
||||
export * from './const/http'
|
||||
|
||||
export * from './error/ErrorWithContext'
|
||||
export * from './error/MethodNotSupportedError'
|
||||
|
||||
export * from './logging/Logger'
|
||||
export * from './logging/StandardLogger'
|
||||
|
@ -5,6 +5,7 @@ import * as mime from 'mime-types'
|
||||
import {FileNotFoundError, Filesystem} from './path/Filesystem'
|
||||
import {Collection} from '../collection/Collection'
|
||||
import {Readable, Writable} from 'stream'
|
||||
import {Pipe} from './Pipe'
|
||||
|
||||
/**
|
||||
* An item that could represent a path.
|
||||
@ -82,6 +83,8 @@ export class UniversalPath {
|
||||
|
||||
protected resourceLocalPath!: string
|
||||
|
||||
protected resourceQuery: URLSearchParams = new URLSearchParams()
|
||||
|
||||
constructor(
|
||||
/**
|
||||
* The path string this path refers to.
|
||||
@ -94,6 +97,10 @@ export class UniversalPath {
|
||||
) {
|
||||
this.setPrefix()
|
||||
this.setLocal()
|
||||
|
||||
if ( this.isRemote ) {
|
||||
this.resourceQuery = (new URL(this.toRemote)).searchParams
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -140,6 +147,13 @@ export class UniversalPath {
|
||||
return new UniversalPath(this.initial)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URLSearchParams for this resource.
|
||||
*/
|
||||
get query(): URLSearchParams {
|
||||
return this.resourceQuery
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string of this resource.
|
||||
*/
|
||||
@ -183,7 +197,8 @@ export class UniversalPath {
|
||||
* Get the fully-prefixed path to this resource.
|
||||
*/
|
||||
get toRemote(): string {
|
||||
return `${this.prefix}${this.resourceLocalPath}`
|
||||
const query = this.query.toString()
|
||||
return `${this.prefix}${this.resourceLocalPath}${query ? '?' + query : ''}`
|
||||
}
|
||||
|
||||
/**
|
||||
@ -517,4 +532,9 @@ export class UniversalPath {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/** Get a new Pipe instance wrapping this. */
|
||||
toPipe(): Pipe<UniversalPath> {
|
||||
return Pipe.wrap(this)
|
||||
}
|
||||
}
|
||||
|
24
src/util/unsafe.ts
Normal file
24
src/util/unsafe.ts
Normal file
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* UNSAFE
|
||||
*
|
||||
* Sometimes, we need to make a literal `import()` call from within commonJS
|
||||
* modules in order to pull in ES modules from commonJS.
|
||||
*
|
||||
* However, when tsc renders the modules to commonJS, it rewrites _all_ calls
|
||||
* to `import` as calls to `require`, which means we cannot actually use ES
|
||||
* modules from commonJS-transpiled TypeScript.
|
||||
*
|
||||
* To bypass this, we can eval the literal string. This is a stupid hack and
|
||||
* I hate it so much, but unfortunately it works.
|
||||
*
|
||||
* So, this is a wrapper function that results in a call to the literal
|
||||
* `import(...)` function in the transpiled code. It should be used VERY
|
||||
* sparingly.
|
||||
*
|
||||
* @see https://github.com/microsoft/TypeScript/issues/43329
|
||||
* @param path
|
||||
*/
|
||||
export function unsafeESMImport(path: string): Promise<any> {
|
||||
((p: string) => p)(path)
|
||||
return eval('import(path)') // eslint-disable-line no-eval
|
||||
}
|
Loading…
Reference in New Issue
Block a user