Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
OliverJAsh committed Dec 14, 2018
0 parents commit 42c843b
Show file tree
Hide file tree
Showing 15 changed files with 672 additions and 0 deletions.
15 changes: 15 additions & 0 deletions .editorconfig
@@ -0,0 +1,15 @@
# Note: prettier inherits from `indent_style`, `indent_size`/`tab_width`, and `max_line_length`
# https://github.com/prettier/prettier/blob/cecf0657a521fa265b713274ed67ca39be4142cf/docs/api.md#prettierresolveconfigfilepath--options

[*]
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true

[*.{js,ts}]
# https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties#max_line_length
max_line_length = 100

[package.json]
indent_size = 2
2 changes: 2 additions & 0 deletions .gitignore
@@ -0,0 +1,2 @@
/target/
node_modules/
8 changes: 8 additions & 0 deletions .prettierignore
@@ -0,0 +1,8 @@
# https://prettier.io/docs/en/ignore.html

/target/

# CLI ignores Node modules by default https://github.com/prettier/prettier/pull/1683, VSCode
# extension does not https://github.com/prettier/prettier-vscode/issues/198,
# https://github.com/prettier/prettier-vscode/issues/548.
node_modules/
4 changes: 4 additions & 0 deletions .prettierrc
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}
40 changes: 40 additions & 0 deletions README.md
@@ -0,0 +1,40 @@
# ts-imgix

Strongly-typed imgix URL builder function, `buildImgixUrl`.

```ts
import * as assert from 'assert';
import { buildImgixUrl } from 'ts-imgix';

assert.strictEqual(
buildImgixUrl('https://foo.com')({
auto: {
format: true,
},
w: 300,
}),
'https://foo.com/?auto=format&w=300',
);
```

![](./demo1.png)
![](./demo2.png)
![](./demo3.png)

## Installation

```sh
yarn add ts-imgix
npm install ts-imgix
```

## Development

```
yarn
npm run start
```

Help wanted! TODO:

- Remove dependencies: `lodash`, `funfix-core`
Binary file added demo1.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo2.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo3.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions package.json
@@ -0,0 +1,34 @@
{
"name": "ts-imgix",
"main": "./target/index.js",
"typings": "./target/index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/unsplash/ts-imgix.git"
},
"version": "0.0.0",
"scripts": {
"lint": "tslint --project ./tsconfig.json",
"compile": "rm -rf ./target/ && tsc",
"test": "npm run compile && node --require source-map-support/register ./target/tests.js",
"format": "prettier --write './**/*.{ts,tsx,js,json,md}' '.prettierrc'",
"prepublishOnly": "npm run compile"
},
"files": [
"target"
],
"dependencies": {
"@types/lodash": "^4.14.119",
"funfix-core": "^7.0.1",
"lodash": "^4.17.11",
"url-transformers": "^0.0.1"
},
"devDependencies": {
"prettier": "^1.15.3",
"source-map-support": "^0.5.9",
"tslint": "^5.11.0",
"tslint-language-service": "^0.9.9",
"tslint-no-unused": "^0.2.0-alpha.1",
"typescript": "^3.2.2"
}
}
15 changes: 15 additions & 0 deletions src/helpers/maybe.ts
@@ -0,0 +1,15 @@
import { Option } from 'funfix-core';
import pickBy from 'lodash/pickBy';

type Maybe<T> = undefined | T;

const isMaybeDefined = <T>(maybeT: Maybe<T>): maybeT is T => maybeT !== undefined;

export const catMaybesDictionary = <T>(maybesDictionary: {
[index: string]: Maybe<T>;
}): { [index: string]: T } => pickBy(maybesDictionary, isMaybeDefined);

export const mapValueIfDefined = <V, V2>(fn: (v: V) => V2) => (maybe: Maybe<V>) =>
Option.of(maybe)
.map(fn)
.getOrElse(undefined);
104 changes: 104 additions & 0 deletions src/index.ts
@@ -0,0 +1,104 @@
// tslint:disable-next-line match-default-export-name
import pipe from 'lodash/flow';
import pickBy from 'lodash/pickBy';
import { ParsedUrlQuery } from 'querystring';
import { addQueryToUrl } from 'url-transformers';
import { catMaybesDictionary, mapValueIfDefined } from './helpers/maybe';

// https://docs.imgix.com/apis/url/size/fit
export enum ImgixFit {
crop = 'crop',
max = 'max',
}

// https://docs.imgix.com/apis/url/size/crop
export type ImgixCrop = Partial<
Record<
'top' | 'bottom' | 'left' | 'right' | 'faces' | 'focalpoint' | 'edges' | 'entropy',
boolean
>
>;

// https://docs.imgix.com/apis/url/format/cs
export enum ImgixColorSpace {
srgb = 'srgb',
adobergb1998 = 'adobergb1998',
tinysrgb = 'tinysrgb',
strip = 'strip',
}

// https://docs.imgix.com/apis/url/auto
export type ImgixAuto = Partial<Record<'compress' | 'format', boolean>>;

// https://docs.imgix.com/apis/url/format/ch
export type ImgixClientHints = Partial<Record<'width' | 'dpr' | 'saveData', boolean>>;

// https://docs.imgix.com/apis/url
export type ImgixUrlQueryParams = {
auto?: ImgixAuto;
q?: number;
h?: number;
w?: number;
fit?: ImgixFit;
dpr?: number;
crop?: ImgixCrop;
bg?: string;
ch?: ImgixClientHints;
blur?: number;
cs?: ImgixColorSpace;
};

const pickTrueInObject = (obj: {}) => pickBy(obj, value => value === true);
const pickTrueObjectKeys = pipe(
pickTrueInObject,
// tslint:disable-next-line no-unbound-method
Object.keys,
);
const undefinedIfEmptyString = (str: string): string | undefined => (str === '' ? undefined : str);
const joinWithComma = (strs: string[]) => strs.join(',');
const serializeImgixUrlQueryParamListValue = pipe(
pickTrueObjectKeys,
joinWithComma,
undefinedIfEmptyString,
);

const throwErrorIfNotFinite = (n: number) => {
if (!Number.isFinite(n)) {
const error = new Error('Expected number to be finite');
throw error;
} else {
return n;
}
};

const mapFiniteNumberToStringIfDefined = mapValueIfDefined(
pipe(
throwErrorIfNotFinite,
String,
),
);
const mapToSerializedListValueIfDefined = mapValueIfDefined(serializeImgixUrlQueryParamListValue);

const serializeImgixUrlQueryParamValues = (query: ImgixUrlQueryParams): ParsedUrlQuery =>
pipe(
(): Record<keyof ImgixUrlQueryParams, string | undefined> => ({
dpr: mapFiniteNumberToStringIfDefined(query.dpr),
auto: mapToSerializedListValueIfDefined(query.auto),
fit: query.fit,
w: mapFiniteNumberToStringIfDefined(query.w),
h: mapFiniteNumberToStringIfDefined(query.h),
q: mapFiniteNumberToStringIfDefined(query.q),
cs: query.cs,
crop: mapToSerializedListValueIfDefined(query.crop),
bg: query.bg,
ch: mapToSerializedListValueIfDefined(query.ch),
blur: mapFiniteNumberToStringIfDefined(query.blur),
}),
catMaybesDictionary,
)();

export const buildImgixUrl = (url: string) =>
pipe(
serializeImgixUrlQueryParamValues,
query => addQueryToUrl({ url })({ queryToAppend: query }),
);
12 changes: 12 additions & 0 deletions src/tests.ts
@@ -0,0 +1,12 @@
import * as assert from 'assert';
import { buildImgixUrl } from './index';

assert.strictEqual(
buildImgixUrl('https://foo.com')({
auto: {
format: true,
},
w: 300,
}),
'https://foo.com/?auto=format&w=300',
);
16 changes: 16 additions & 0 deletions tsconfig.json
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"module": "commonjs",
// Needed for Lodash
"esModuleInterop": true,
"target": "es2015",
"strict": true,
"noImplicitReturns": true,
"sourceMap": true,
"outDir": "./target/",
"rootDir": "./src/",
"plugins": [{ "name": "tslint-language-service" }],
"declaration": true
},
"files": ["./src/index.ts", "./src/tests.ts"]
}
61 changes: 61 additions & 0 deletions tslint.json
@@ -0,0 +1,61 @@
{
"extends": ["tslint-no-unused"],
"linterOptions": {
"exclude": ["**/node_modules/**/*"]
},
"rules": {
"no-unused": true,
"no-any": true,
"no-unsafe-any": true,
"strict-boolean-expressions": true,
"ordered-imports": [true],
"no-inferrable-types": [true, "ignore-properties"],
"no-switch-case-fall-through": true,
"arrow-return-shorthand": [true, "multiline"],
"no-use-before-declare": true,
"no-inferred-empty-object-type": true,
"unified-signatures": true,
"no-conditional-assignment": true,
"no-floating-promises": true,
"no-object-literal-type-assertion": true,
"no-shadowed-variable": true,
"no-unbound-method": [true, "ignore-static"],
"no-unused-expression": true,
"no-var-keyword": true,
"no-void-expression": true,
"prefer-object-spread": true,
"radix": true,
"restrict-plus-operands": true,
"triple-equals": true,
"use-default-type-parameter": true,
"use-isnan": true,
"deprecation": true,
"max-file-line-count": [true, 400],
// Rationale: https://github.com/palantir/tslint/issues/1182#issue-151780453
"no-default-export": true,
"prefer-const": true,
"class-name": true,
"match-default-export-name": true,
"no-boolean-literal-compare": true,
"no-consecutive-blank-lines": true,
"no-irregular-whitespace": true,
"no-unnecessary-callback-wrapper": true,
"object-literal-shorthand": true,
"prefer-switch": true,
"prefer-template": true,
"quotemark": [true, "single", "avoid-escape"],
"variable-name": [
true,
"ban-keywords",
"check-format",
// e.g. for whitelisting unused function parameters
"allow-leading-underscore",
// e.g. for io-ts types
"allow-pascal-case"
],
"no-import-side-effect": true,
"no-duplicate-imports": true,
"no-implicit-dependencies": [true, "dev"],
"array-type": [true, "array-simple"]
}
}

0 comments on commit 42c843b

Please sign in to comment.