fix: add cli

This commit is contained in:
master 2025-01-31 02:02:07 +08:00
parent ca5f4984a4
commit 733b697ee2
11 changed files with 818 additions and 54 deletions

View File

@ -32,7 +32,8 @@
"publish": "npm run build && npm publish ./dist",
"coverage": "vitest run --coverage",
"lint": "ultracite lint",
"format": "ultracite format"
"format": "ultracite format",
"cli": "tsx scripts/cli.ts"
},
"dependencies": {
"@ngify/http": "^2.0.4",
@ -43,18 +44,24 @@
"rxjs": "^7.4.0||>=8.0.0"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@biomejs/js-api": "0.7.1",
"@biomejs/wasm-nodejs": "^1.9.4",
"@evilmartians/lefthook": "^1.0.3",
"@playwright/test": "^1.49.1",
"@rslib/core": "^0.3.1",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.10.1",
"@vitest/coverage-v8": "^3.0.1",
"commander": "^13.1.0",
"lodash-es": "^4.17.21",
"oxc-parser": "^0.48.1",
"oxc-walker": "^0.2.2",
"rfc4648": "^1.5.0",
"rxjs": "^7.4.0",
"tsx": "^4.19.2",
"typescript": "^5.7.3",
"ultracite": "^4.1.15",
"vitest": "^3.0.1",
"rxjs": "^7.4.0"
"vitest": "^3.0.1"
},
"keywords": [
"rxjs",

614
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

19
scripts/cli.ts Normal file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env node
import { Command } from 'commander';
import { rewriteAllObservableSubscribeToLastValueFrom } from './code-transform';
const program = new Command();
program
.version('1.0.0')
.description('A CLI tool to help develop oidc-client-rx');
program
.command('rewrite <pattern>')
.description('Rewrite files matching the given glob pattern')
.action(async (pattern: string) => {
await rewriteAllObservableSubscribeToLastValueFrom(pattern);
});
program.parse(process.argv);

View File

@ -0,0 +1,35 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { Biome, Distribution } from '@biomejs/js-api';
import { rewriteObservableSubscribeToLastValueFrom } from './code-transform';
describe('writeAllSpecObservableSubscribeToLastValueFrom', () => {
it('should transform valid string', async () => {
const actual = await rewriteObservableSubscribeToLastValueFrom(
'index.ts',
`refreshSessionIframeService
.refreshSessionWithIframe(allConfigs[0]!, allConfigs)
.subscribe((result) => {
expect(
result
).toHaveBeenCalledExactlyOnceWith(
'a-url',
allConfigs[0]!,
allConfigs
);
});`
);
const expect = `const result = await lastValueFrom(refreshSessionIframeService.refreshSessionWithIframe(allConfigs[0]!, allConfigs));
expect(result).toHaveBeenCalledExactlyOnceWith('a-url',allConfigs[0]!,allConfigs);`;
const biome = await Biome.create({
distribution: Distribution.NODE,
});
assert.equal(
biome.formatContent(actual, { filePath: 'index.ts' }).content,
biome.formatContent(expect, { filePath: 'index.ts' }).content
);
});
});

113
scripts/code-transform.ts Normal file
View File

@ -0,0 +1,113 @@
import assert from 'node:assert/strict';
import fsp from 'node:fs/promises';
import {
type ClassElement,
type MagicString,
type Statement,
parseSync,
} from 'oxc-parser';
import { type Node, walk } from 'oxc-walker';
function sourceTextFromNode(
context: { magicString?: MagicString },
node: Node
): string {
const magicString = context.magicString;
assert(magicString, 'magicString should be defined');
const start = node.start;
const end = node.end;
return magicString.getSourceText(start, end);
}
export async function rewriteObservableSubscribeToLastValueFrom(
filename: string,
content?: string
) {
const code = content ?? (await fsp.readFile(filename, 'utf-8'));
const parsedResult = parseSync('index.ts', code);
const magicString = parsedResult.magicString;
walk(parsedResult, {
leave(node, _, context) {
const transformExprs = <T extends Statement[]>(
children: T
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation>
): T => {
const newChildren: T = [] as any as T;
for (const child of children) {
if (
child.type === 'ExpressionStatement' &&
child.expression.type === 'CallExpression' &&
child.expression.callee.type === 'StaticMemberExpression' &&
child.expression.callee.property.name === 'subscribe' &&
child.expression.arguments[0]?.type === 'ArrowFunctionExpression' &&
child.expression.arguments[0].body.type === 'FunctionBody'
) {
const awaited =
child.expression.arguments[0].params.kind ===
'ArrowFormalParameters' &&
child.expression.arguments[0].params.items[0]?.type ===
'FormalParameter' &&
child.expression.arguments[0].params.items[0].pattern.type ===
'Identifier'
? child.expression.arguments[0].params.items[0].pattern.name
: undefined;
const newContent =
(awaited
? `const ${awaited} = await lastValueFrom(${sourceTextFromNode(
context,
child.expression.callee.object
)});\n`
: `await lastValueFrom(${sourceTextFromNode(context, child.expression.callee.object)});\n`) +
child.expression.arguments[0].body.statements
.map((s) => sourceTextFromNode(context, s))
.join(';\n');
const newStatements = parseSync('index.ts', newContent).program
.body as any[];
magicString.remove(child.start, child.end);
magicString.appendRight(child.start, newContent);
newChildren.push(...newStatements);
} else {
newChildren.push(child as any);
}
return newChildren;
}
return newChildren;
};
if ('body' in node && Array.isArray(node.body) && node.body.length > 0) {
const children = node.body;
node.body = transformExprs(children as any)!;
} else if (
'body' in node &&
node.body &&
'type' in node.body &&
node.body.type === 'FunctionBody'
) {
console.error('xxx', node.body.type);
const children = node.body.statements;
node.body.statements = transformExprs(children)!;
}
},
});
const result = magicString.toString();
return result;
}
export async function rewriteAllObservableSubscribeToLastValueFrom(
pattern: string | string[]
) {
const files = fsp.glob(pattern);
for await (const file of files) {
const source = await fsp.readFile(file, 'utf-8');
const result = await rewriteObservableSubscribeToLastValueFrom(file);
if (source !== result) {
console.error('not equal');
}
await fsp.writeFile(file, result, 'utf-8');
}
}

View File

@ -346,11 +346,9 @@ describe('CheckSessionService', () => {
describe('checkSessionChanged$', () => {
it('emits when internal event is thrown', async () => {
checkSessionService.checkSessionChanged$
.pipe(skip(1))
.subscribe((result) => {
expect(result).toBe(true);
});
const result = await lastValueFrom(checkSessionService.checkSessionChanged$
.pipe(skip(1)));
expect(result).toBe(true);
const serviceAsAny = checkSessionService as any;
@ -358,9 +356,8 @@ describe('CheckSessionService', () => {
});
it('emits false initially', async () => {
checkSessionService.checkSessionChanged$.subscribe((result) => {
expect(result).toBe(false);
});
const result = await lastValueFrom(checkSessionService.checkSessionChanged$);
expect(result).toBe(false);
});
it('emits false then true when emitted', async () => {

View File

@ -1,8 +1,8 @@
import { DOCUMENT } from '../dom';
import { Injectable, NgZone, OnDestroy, inject } from 'injection-js';
import { Injectable, NgZone, type OnDestroy, inject } from 'injection-js';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { take } from 'rxjs/operators';
import { OpenIdConfiguration } from '../config/openid-configuration';
import type { OpenIdConfiguration } from '../config/openid-configuration';
import { DOCUMENT } from '../dom';
import { LoggerService } from '../logging/logger.service';
import { EventTypes } from '../public-events/event-types';
import { PublicEventsService } from '../public-events/public-events.service';
@ -74,7 +74,7 @@ export class CheckSessionService implements OnDestroy {
}
start(configuration: OpenIdConfiguration): void {
if (!!this.scheduledHeartBeatRunning) {
if (this.scheduledHeartBeatRunning) {
return;
}
@ -141,13 +141,13 @@ export class CheckSessionService implements OnDestroy {
return of();
}
if (!contentWindow) {
if (contentWindow) {
contentWindow.location.replace(checkSessionIframe);
} else {
this.loggerService.logWarning(
configuration,
'CheckSession - init check session: IFrame contentWindow does not exist'
);
} else {
contentWindow.location.replace(checkSessionIframe);
}
return new Observable((observer) => {
@ -197,7 +197,7 @@ export class CheckSessionService implements OnDestroy {
this.outstandingMessages++;
contentWindow.postMessage(
clientId + ' ' + sessionState,
`${clientId} ${sessionState}`,
iframeOrigin
);
} else {

View File

@ -1,5 +1,5 @@
import { TestBed } from '@/testing';
import { of } from 'rxjs';
import { lastValueFrom, of } from 'rxjs';
import { vi } from 'vitest';
import { LoggerService } from '../logging/logger.service';
import { mockProvider } from '../testing/mock';
@ -20,9 +20,6 @@ describe('RefreshSessionIframeService ', () => {
mockProvider(UrlService),
],
});
});
beforeEach(() => {
refreshSessionIframeService = TestBed.inject(RefreshSessionIframeService);
urlService = TestBed.inject(UrlService);
});
@ -62,7 +59,9 @@ describe('RefreshSessionIframeService ', () => {
it('dispatches customevent to window object', async () => {
const dispatchEventSpy = vi.spyOn(window, 'dispatchEvent');
(refreshSessionIframeService as any).initSilentRenewRequest();
await lastValueFrom(
(refreshSessionIframeService as any).initSilentRenewRequest()
);
expect(dispatchEventSpy).toHaveBeenCalledExactlyOnceWith(
new CustomEvent('oidc-silent-renew-init', {

View File

@ -1,5 +1,5 @@
import { TestBed } from '@/testing';
import { of } from 'rxjs';
import { TestBed, mockImplementationWhenArgsEqual } from '@/testing';
import { lastValueFrom, of } from 'rxjs';
import { vi } from 'vitest';
import type { OpenIdConfiguration } from '../../config/openid-configuration';
import { FlowsDataService } from '../../flows/flows-data.service';
@ -1044,9 +1044,8 @@ describe('UrlService Tests', () => {
describe('getAuthorizeUrl', () => {
it('returns null if no config is given', async () => {
service.getAuthorizeUrl(null).subscribe((url) => {
expect(url).toBeNull();
});
const url = await lastValueFrom(service.getAuthorizeUrl(null));
expect(url).toBeNull();
});
it('returns null if current flow is code flow and no redirect url is defined', async () => {
@ -1383,7 +1382,7 @@ describe('UrlService Tests', () => {
resultObs$.subscribe((result) => {
expect(result).toBe(
`client_id=testClientId&redirect_uri=testRedirectUrl&response_type=testResponseType&scope=testScope&nonce=testNonce&state=testState&code_challenge=testCodeChallenge&code_challenge_method=S256`
'client_id=testClientId&redirect_uri=testRedirectUrl&response_type=testResponseType&scope=testScope&nonce=testNonce&state=testState&code_challenge=testCodeChallenge&code_challenge_method=S256'
);
});
});
@ -1414,7 +1413,7 @@ describe('UrlService Tests', () => {
resultObs$.subscribe((result) => {
expect(result).toBe(
`client_id=testClientId&redirect_uri=testRedirectUrl&response_type=testResponseType&scope=testScope&nonce=testNonce&state=testState&code_challenge=testCodeChallenge&code_challenge_method=S256&hd=testHdParam`
'client_id=testClientId&redirect_uri=testRedirectUrl&response_type=testResponseType&scope=testScope&nonce=testNonce&state=testState&code_challenge=testCodeChallenge&code_challenge_method=S256&hd=testHdParam'
);
});
});
@ -1445,7 +1444,7 @@ describe('UrlService Tests', () => {
resultObs$.subscribe((result) => {
expect(result).toBe(
`client_id=testClientId&redirect_uri=testRedirectUrl&response_type=testResponseType&scope=testScope&nonce=testNonce&state=testState&code_challenge=testCodeChallenge&code_challenge_method=S256&hd=testHdParam&any=thing`
'client_id=testClientId&redirect_uri=testRedirectUrl&response_type=testResponseType&scope=testScope&nonce=testNonce&state=testState&code_challenge=testCodeChallenge&code_challenge_method=S256&hd=testHdParam&any=thing'
);
});
});
@ -1480,7 +1479,7 @@ describe('UrlService Tests', () => {
resultObs$.subscribe((result) => {
expect(result).toBe(
`client_id=testClientId&redirect_uri=testRedirectUrl&response_type=testResponseType&scope=testScope&nonce=testNonce&state=testState&code_challenge=testCodeChallenge&code_challenge_method=S256&hd=testHdParam&any=thing&any=otherThing`
'client_id=testClientId&redirect_uri=testRedirectUrl&response_type=testResponseType&scope=testScope&nonce=testNonce&state=testState&code_challenge=testCodeChallenge&code_challenge_method=S256&hd=testHdParam&any=thing&any=otherThing'
);
});
});
@ -1904,8 +1903,7 @@ describe('UrlService Tests', () => {
resultObs$.subscribe((result: any) => {
expect(result).toBe(
`authorizationEndpoint?client_id=clientId&redirect_uri=http%3A%2F%2Fany-url.com` +
`&response_type=${responseType}&scope=${scope}&nonce=${nonce}&state=${state}&to=add&as=well`
`authorizationEndpoint?client_id=clientId&redirect_uri=http%3A%2F%2Fany-url.com&response_type=${responseType}&scope=${scope}&nonce=${nonce}&state=${state}&to=add&as=well`
);
});
});
@ -2121,7 +2119,8 @@ describe('UrlService Tests', () => {
const value = service.getEndSessionUrl(config);
// Assert
const expectValue = `something.auth0.com/v2/logout?client_id=someClientId&returnTo=https://localhost:1234/unauthorized`;
const expectValue =
'something.auth0.com/v2/logout?client_id=someClientId&returnTo=https://localhost:1234/unauthorized';
expect(value).toEqual(expectValue);
});

View File

@ -25,6 +25,9 @@
},
{
"path": "./tsconfig.spec.json"
},
{
"path": "./tsconfig.scripts.json"
}
]
}

12
tsconfig.scripts.json Normal file
View File

@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"composite": true,
"noUncheckedIndexedAccess": true,
"noEmit": true,
"types": ["node"]
},
"files": [],
"include": ["scripts/"]
}