Skip to content

Commit

Permalink
✨ Allows passing encoding callbacks (#20)
Browse files Browse the repository at this point in the history
* ✨ Allows passing encoding callbacks

and base64, base64url encoders

* 🐛 Fixes js sourcemaps
  • Loading branch information
ekwoka authored Jan 15, 2024
1 parent 51fa4bd commit 02cf6b0
Show file tree
Hide file tree
Showing 12 changed files with 742 additions and 755 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"vitest.commandLine": "pnpm exec vitest",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
Expand Down
36 changes: 18 additions & 18 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,26 @@
},
"devDependencies": {
"@milahu/patch-package": "6.4.14",
"@trivago/prettier-plugin-sort-imports": "4.2.1",
"@types/alpinejs": "3.13.3",
"@types/node": "20.8.10",
"@typescript-eslint/eslint-plugin": "6.9.1",
"@typescript-eslint/parser": "6.9.1",
"@vitest/ui": "0.34.6",
"alpinejs": "3.13.2",
"esbuild": "0.19.5",
"eslint": "8.52.0",
"happy-dom": "9.1.9",
"@trivago/prettier-plugin-sort-imports": "4.3.0",
"@types/alpinejs": "3.13.6",
"@types/node": "20.11.1",
"@typescript-eslint/eslint-plugin": "6.18.1",
"@typescript-eslint/parser": "6.18.1",
"@vitest/ui": "1.2.0",
"alpinejs": "3.13.3",
"esbuild": "0.19.11",
"eslint": "8.56.0",
"happy-dom": "13.1.4",
"husky": "8.0.3",
"lint-staged": "15.0.2",
"lint-staged": "15.2.0",
"npm-run-all": "4.1.5",
"prettier": "3.0.3",
"prettier": "3.2.2",
"pretty-bytes": "6.1.1",
"typescript": "5.2.2",
"vite": "4.5.0",
"vite-plugin-dts": "3.6.3",
"vite-tsconfig-paths": "4.2.1",
"vitest": "0.34.6",
"typescript": "5.3.3",
"vite": "5.0.11",
"vite-plugin-dts": "3.7.0",
"vite-tsconfig-paths": "4.2.3",
"vitest": "1.2.0",
"vitest-dom": "0.1.1"
},
"lint-staged": {
Expand Down Expand Up @@ -73,7 +73,7 @@
}
},
"dependencies": {
"@vue/reactivity": "^3.3.8",
"@vue/reactivity": "^3.4.13",
"alpinets": "link:../alpinets/packages/alpinets"
}
}
12 changes: 6 additions & 6 deletions packages/params/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,12 @@ type Transformer<T> = (val: T | PrimitivesToStrings<T>) => T;
type PrimitivesToStrings<T> = T extends string | number | boolean | null
? `${T}`
: T extends Array<infer U>
? Array<PrimitivesToStrings<U>>
: T extends object
? {
[K in keyof T]: PrimitivesToStrings<T[K]>;
}
: T;
? Array<PrimitivesToStrings<U>>
: T extends object
? {
[K in keyof T]: PrimitivesToStrings<T[K]>;
}
: T;
```

Note, the transformer will need to be able to handle being called with the type of the value or a simply parsed structure that equates to all primitives being strings. This is because the transformer will be called with the value when initializing, which can be the provided value, or the one determined from the query string.
Expand Down
45 changes: 45 additions & 0 deletions packages/params/src/encoding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
export type Encoding<T> = {
to(value: T): PrimitivesToStrings<T>;
from(value: PrimitivesToStrings<T>): T;
};

export type PrimitivesToStrings<T> = T extends string | number | boolean | null
? `${T}`
: T extends Array<infer U>
? Array<PrimitivesToStrings<U>>
: T extends object
? {
[K in keyof T]: PrimitivesToStrings<T[K]>;
}
: T;

globalThis.btoa ??= (str: string) => Buffer.from(str).toString('base64');
globalThis.atob ??= (str: string) => Buffer.from(str, 'base64').toString();

export const base64: Encoding<string> = {
to: (value) => btoa(value),
from: (value) => atob(value),
};

export const base64URL: Encoding<string> = {
to: (value) =>
btoa(value).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', ''),
from: (value) => atob(value.replaceAll('-', '+').replaceAll('_', '/')),
};

if (import.meta.vitest) {
describe('Encoding', () => {
it('should encode and decode base64', () => {
expect(base64.to('hello world')).toBe('aGVsbG8gd29ybGQ=');
expect(base64.to('<<???>>')).toBe('PDw/Pz8+Pg==');
expect(base64.from('aGVsbG8gd29ybGQ=')).toBe('hello world');
expect(base64.from('PDw/Pz8+Pg==')).toBe('<<???>>');
});
it('should encode and decode base64URL', () => {
expect(base64URL.to('hello world')).toBe('aGVsbG8gd29ybGQ');
expect(base64URL.to('<<???>>')).toBe('PDw_Pz8-Pg');
expect(base64URL.from('aGVsbG8gd29ybGQ')).toBe('hello world');
expect(base64URL.from('PDw_Pz8-Pg')).toBe('<<???>>');
});
});
}
102 changes: 66 additions & 36 deletions packages/params/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,67 @@
import type { PluginCallback, InterceptorObject, Alpine } from 'alpinejs';
import { fromQueryString, toQueryString } from './querystring';
import { fromQueryString, toQueryString } from './querystring.js';
import {
retrieveDotNotatedValueFromData,
objectAtPath,
deleteDotNotatedValueFromData,
insertDotNotatedValueIntoData,
} from './pathresolve';
import { UpdateMethod, onURLChange, untrack } from './history';
} from './pathresolve.js';
import { UpdateMethod, onURLChange, untrack } from './history.js';
import { Encoding, PrimitivesToStrings } from './encoding.js';
export { base64, base64URL } from './encoding.js';
export type { Encoding, PrimitivesToStrings } from './encoding.js';

type InnerType<T, S> = T extends PrimitivesToStrings<T>
? T
: S extends Transformer<T>
? T
: T | PrimitivesToStrings<T>;
type _InnerType<T, S> =
T extends PrimitivesToStrings<T>
? T
: S extends Transformer<T>
? T
: T | PrimitivesToStrings<T>;

/**
* This is the InterceptorObject that is returned from the `query` function.
* When inside an Alpine Component or Store, these interceptors are initialized.
* This hooks up setter/getter methods to to replace the object itself
* and sync the query string params
*/
class QueryInterceptor<T, S extends Transformer<T> | undefined = undefined>
implements InterceptorObject<InnerType<T, S>>
{
class QueryInterceptor<T> implements InterceptorObject<T> {
_x_interceptor = true as const;
private alias: string | undefined = undefined;
private transformer?: S;
private encoder: Encoding<T> = {
to: (v) => v as PrimitivesToStrings<T>,
from: (v) => v as T,
};
private method: UpdateMethod = UpdateMethod.replace;
private show: boolean = false;
public initialValue: InnerType<T, S>;
public initialValue: T;
constructor(
initialValue: T,
private Alpine: Pick<Alpine, 'effect'>,
private reactiveParams: Record<string, unknown>,
) {
this.initialValue = initialValue as InnerType<T, S>;
this.initialValue = initialValue;
}
/**
* Self Initializing interceptor called by Alpine during component initialization
* @param {object} data The Alpine Data Object (component or store)
* @param {string} path dot notated path from the data root to the interceptor
* @returns {T} The value of the interceptor after initialization
*/
initialize(data: Record<string, unknown>, path: string): InnerType<T, S> {
initialize(data: Record<string, unknown>, path: string): T {
const {
alias = path,
Alpine,
initialValue,
method,
reactiveParams,
show,
transformer,
encoder,
} = this;
const initial = (retrieveDotNotatedValueFromData(alias, reactiveParams) ??
initialValue) as InnerType<T, S>;
const existing = retrieveDotNotatedValueFromData(
alias,
reactiveParams,
) as PrimitivesToStrings<T> | null;
const initial = existing ? encoder.from(existing) : initialValue;

const keys = path.split('.');
const final = keys.pop()!;
Expand All @@ -63,19 +71,26 @@ class QueryInterceptor<T, S extends Transformer<T> | undefined = undefined>
set: (value: T) => {
!show && value === initialValue
? deleteDotNotatedValueFromData(alias, reactiveParams)
: insertDotNotatedValueIntoData(alias, value, reactiveParams);
: insertDotNotatedValueIntoData(
alias,
encoder.to(value),
reactiveParams,
);
},
get: () => {
const value = (retrieveDotNotatedValueFromData(alias, reactiveParams) ??
initialValue) as T;
const existing = retrieveDotNotatedValueFromData(
alias,
reactiveParams,
) as PrimitivesToStrings<T> | null;
const value = existing ? encoder.from(existing) : initialValue;
return value;
},
enumerable: true,
});

Alpine.effect(paramEffect(alias, reactiveParams, method));

return (transformer?.(initial) ?? initial) as InnerType<T, S>;
return initial;
}
/**
* Changes the keyname for using in the query string
Expand All @@ -90,10 +105,9 @@ class QueryInterceptor<T, S extends Transformer<T> | undefined = undefined>
* Transforms the value of the query param before it is set on the data
* @param {function} fn Transformer function
*/
into(fn: Transformer<T>): QueryInterceptor<T, Transformer<T>> {
const self = this as QueryInterceptor<T, Transformer<T>>;
self.transformer = fn;
return self;
into(fn: Transformer<T>): QueryInterceptor<T> {
this.encoder.from = fn;
return this;
}
/**
* Always show the initial value in the query string
Expand All @@ -109,6 +123,14 @@ class QueryInterceptor<T, S extends Transformer<T> | undefined = undefined>
this.method = UpdateMethod.push;
return this;
}
/**
* Registers encoding and decoding functions to transform the value
* before it is set on the query string
*/
encoding(encoder: Encoding<T>): QueryInterceptor<T> {
this.encoder = encoder;
return this;
}
}

export const query: PluginCallback = (Alpine) => {
Expand Down Expand Up @@ -195,6 +217,7 @@ const setParams = (params: Record<string, unknown>, method: UpdateMethod) => {
if (import.meta.vitest) {
describe('QueryInterceptor', async () => {
const Alpine = await import('alpinejs').then((m) => m.default);
const { base64URL } = await import('./encoding');
afterEach(() => {
vi.restoreAllMocks();
});
Expand Down Expand Up @@ -351,17 +374,24 @@ if (import.meta.vitest) {
);
expect(history.replaceState).not.toHaveBeenCalled();
});
it('can have a defined encoding', async () => {
vi.spyOn(history, UpdateMethod.replace);
const paramObject = Alpine.reactive({});
const data = { foo: '' };
new QueryInterceptor(data.foo, Alpine, paramObject)
.encoding(base64URL)
.initialize(data, 'foo');
data.foo = '<<???>>';
await Alpine.nextTick();
expect(data).toEqual({ foo: '<<???>>' });
expect(paramObject).toEqual({ foo: 'PDw_Pz8-Pg' });
expect(history.replaceState).toHaveBeenCalledWith(
{ query: { foo: 'PDw_Pz8-Pg' } },
'',
'?foo=PDw_Pz8-Pg',
);
});
});
}

type PrimitivesToStrings<T> = T extends string | number | boolean | null
? `${T}`
: T extends Array<infer U>
? Array<PrimitivesToStrings<U>>
: T extends object
? {
[K in keyof T]: PrimitivesToStrings<T[K]>;
}
: T;

export { observeHistory } from './history';
2 changes: 1 addition & 1 deletion packages/params/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ export default defineConfig({
entryFileNames: ({ name: fileName }) => {
return `${fileName}.js`;
},
sourcemap: true,
},
},
sourcemap: true,
},
test: {
globals: true,
Expand Down
2 changes: 1 addition & 1 deletion packages/xajax/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ export default defineConfig({
entryFileNames: ({ name: fileName }) => {
return `${fileName}.js`;
},
sourcemap: true,
},
},
sourcemap: true,
},
test: {
globals: true,
Expand Down
2 changes: 1 addition & 1 deletion packages/xrias/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ export default defineConfig({
entryFileNames: ({ name: fileName }) => {
return `${fileName}.js`;
},
sourcemap: true,
},
},
sourcemap: true,
},
test: {
globals: true,
Expand Down
2 changes: 1 addition & 1 deletion packages/xrouter/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ export default defineConfig({
entryFileNames: ({ name: fileName }) => {
return `${fileName}.js`;
},
sourcemap: true,
},
},
sourcemap: true,
},
test: {
globals: true,
Expand Down
Loading

0 comments on commit 02cf6b0

Please sign in to comment.