├── .npmignore
├── example
├── src
│ ├── fetcher.ts
│ ├── utils.css
│ ├── main.js
│ ├── index.html
│ ├── Switch.svelte
│ └── App.svelte
├── static
│ └── favicon.png
├── .prettierrc
├── tailwind.config.js
├── svelte.config.js
├── .prettierignore
├── tsconfig.json
├── .eslintrc.js
├── .gitignore
├── LICENSE
├── postcss.config.js
├── package.json
├── rollup.config.js
└── README.md
├── src
├── index.ts
├── browser_state.ts
└── fetcher.ts
├── dist
├── index.d.ts
├── index.js
├── index.js.map
├── index.d.ts.map
├── browser_state.d.ts.map
├── browser_state.d.ts
├── fetcher.d.ts.map
├── browser_state.js.map
├── browser_state.js
├── fetcher.d.ts
├── fetcher.js.map
└── fetcher.js
├── .gitignore
├── package.json
├── LICENSE
├── README.md
└── tsconfig.json
/.npmignore:
--------------------------------------------------------------------------------
1 | example
2 |
--------------------------------------------------------------------------------
/example/src/fetcher.ts:
--------------------------------------------------------------------------------
1 | import { fetcher } from 'swr-xstate';
2 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './browser_state';
2 | export * from './fetcher';
3 |
--------------------------------------------------------------------------------
/example/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimfeld/swr-xstate/HEAD/example/static/favicon.png
--------------------------------------------------------------------------------
/dist/index.d.ts:
--------------------------------------------------------------------------------
1 | export * from './browser_state';
2 | export * from './fetcher';
3 | //# sourceMappingURL=index.d.ts.map
--------------------------------------------------------------------------------
/dist/index.js:
--------------------------------------------------------------------------------
1 | export * from './browser_state';
2 | export * from './fetcher';
3 | //# sourceMappingURL=index.js.map
--------------------------------------------------------------------------------
/example/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "es5",
4 | "arrowParens": "always"
5 | }
6 |
--------------------------------------------------------------------------------
/dist/index.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC"}
--------------------------------------------------------------------------------
/dist/index.d.ts.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC"}
--------------------------------------------------------------------------------
/example/src/utils.css:
--------------------------------------------------------------------------------
1 | /* Import Tailwind as Global Utils */
2 | @import 'tailwindcss/base';
3 | @import 'tailwindcss/components';
4 | @import 'tailwindcss/utilities';
5 |
--------------------------------------------------------------------------------
/example/src/main.js:
--------------------------------------------------------------------------------
1 | import './utils.css';
2 | import App from './App.svelte';
3 |
4 | const app = new App({
5 | target: document.body,
6 | });
7 |
8 | export default app;
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /node_modules/
3 | yarn-error.log
4 | /cypress/screenshots/
5 | .vscode
6 | .env
7 | yarn.lock
8 | package-lock.json
9 | dist/tsconfig.tsbuildinfo
10 |
--------------------------------------------------------------------------------
/example/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | purge: false,
3 | theme: {
4 | extend: {},
5 | },
6 | variants: {},
7 | plugins: [require('@tailwindcss/ui')],
8 | };
9 |
--------------------------------------------------------------------------------
/example/svelte.config.js:
--------------------------------------------------------------------------------
1 | const sveltePreprocess = require('svelte-preprocess');
2 |
3 | const dev = process.env.NODE_ENV === 'development';
4 |
5 | module.exports = {
6 | preprocess: sveltePreprocess({
7 | postcss: require('./postcss.config'),
8 | aliases: [['ts', 'typescript']],
9 | }),
10 | };
11 |
--------------------------------------------------------------------------------
/example/.prettierignore:
--------------------------------------------------------------------------------
1 | .git/*
2 | .DS_Store
3 |
4 | license
5 | yarn.lock
6 | .travis.yml
7 |
8 | .yarnclean
9 | .eslintignore
10 | .prettierignore
11 | .npmignore
12 | .gitignore
13 | .dockerignore
14 |
15 | dist
16 | build
17 | packages/*/lib/app
18 | consoles/*/lib/app
19 |
20 | *.ico
21 | *.html
22 | *.log
23 | *.svg
24 | *.map
25 | *.png
26 | *.snap
27 | *.ttf
28 | *.sh
29 | *.txt
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "sourceMap": true,
4 | "module": "ESNext",
5 | "moduleResolution": "node",
6 | "target": "es2020",
7 | "noEmit": true,
8 | "lib": ["dom", "es5", "ES2015", "es2016", "es2017", "es2018", "ES2019"]
9 | },
10 | "paths": {
11 | "^/*": ["src/*"]
12 | },
13 | "include": ["src"],
14 | "compileOnSave": false
15 | }
16 |
--------------------------------------------------------------------------------
/dist/browser_state.d.ts.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"browser_state.d.ts","sourceRoot":"","sources":["../src/browser_state.ts"],"names":[],"mappings":"AACA,wBAAgB,iBAAiB,IAAI,OAAO,CAS3C;AAED,wBAAgB,QAAQ,IAAI,OAAO,CAMlC;AAED,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,eAAe,IAAK,YAAY,CAM/C;AAuCD,oBAAY,YAAY,GAAG,MAAM,IAAI,CAAC;AAEtC,wBAAgB,SAAS,CAAC,EAAE,EAAG,CAAC,KAAK,EAAG,YAAY,KAAK,GAAG,GAAI,YAAY,CAe3E"}
--------------------------------------------------------------------------------
/example/src/index.html:
--------------------------------------------------------------------------------
1 |
2 | >
3 |
4 |
5 |
6 |
7 | <%= title %>
8 |
9 |
10 | <%= links %>
11 |
12 |
13 |
14 |
15 | <%= scripts %>
16 |
17 |
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "swr-xstate",
3 | "version": "1.0.2",
4 | "description": "SWR Fetcher implemented using XState",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "scripts": {
8 | "build": "rimraf dist && tsc",
9 | "dev": "tsc --watch"
10 | },
11 | "author": "Daniel Imfeld ",
12 | "license": "MIT",
13 | "dependencies": {
14 | "xstate": "^4.11.0"
15 | },
16 | "devDependencies": {
17 | "rimraf": "^3.0.2",
18 | "typescript": "^3"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/dist/browser_state.d.ts:
--------------------------------------------------------------------------------
1 | export declare function isDocumentVisible(): boolean;
2 | export declare function isOnline(): boolean;
3 | export declare function isDocumentFocused(): boolean;
4 | export interface BrowserState {
5 | online: boolean;
6 | visible: boolean;
7 | focused: boolean;
8 | }
9 | export declare function getBrowserState(): BrowserState;
10 | export declare type Unsubscriber = () => void;
11 | export declare function subscribe(cb: (state: BrowserState) => any): Unsubscriber;
12 | //# sourceMappingURL=browser_state.d.ts.map
--------------------------------------------------------------------------------
/example/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const jsRules = {
2 | 'no-unused-vars': ['warn', { args: 'none' }],
3 | 'no-return-await': 'warn',
4 | 'no-use-before-define': 'error',
5 | 'no-mixed-spaces-and-tabs': 'error',
6 | 'no-trailing-spaces': 'warn',
7 | };
8 |
9 | module.exports = {
10 | env: {
11 | browser: true,
12 | es6: true,
13 | node: true,
14 | },
15 | parser: '@typescript-eslint/parser',
16 | parserOptions: {
17 | project: 'tsconfig.json',
18 | ecmaVersion: 2019,
19 | sourceType: 'module',
20 | },
21 | overrides: [
22 | {
23 | files: ['**/*.svelte'],
24 | parser: 'espree',
25 | processor: 'svelte3/svelte3',
26 | extends: ['eslint:recommended'],
27 | rules: jsRules,
28 | },
29 | {
30 | files: ['**/*.js'],
31 | extends: ['eslint:recommended'],
32 | rules: jsRules,
33 | },
34 | ],
35 | plugins: ['@typescript-eslint/eslint-plugin', 'svelte3'],
36 | extends: [
37 | 'plugin:@typescript-eslint/eslint-recommended',
38 | 'prettier/@typescript-eslint',
39 | ],
40 | rules: {},
41 | };
42 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | public
2 | public/bundle.*
3 | public/utils.*
4 | build
5 | ### Node ###
6 | # Logs
7 | logs
8 | *.log
9 | npm-debug.log*
10 |
11 | # Runtime data
12 | pids
13 | *.pid
14 | *.seed
15 | *.pid.lock
16 |
17 | # node-waf configuration
18 | .lock-wscript
19 |
20 | # Compiled binary addons (http://nodejs.org/api/addons.html)
21 | build/Release
22 |
23 | # Dependency directories
24 | node_modules
25 | jspm_packages
26 |
27 | # Optional npm cache directory
28 | .npm
29 |
30 | # Optional eslint cache
31 | .eslintcache
32 |
33 | # Optional REPL history
34 | .node_repl_history
35 |
36 | # Output of 'npm pack'
37 | *.tgz
38 |
39 | ### Git ###
40 | *.orig
41 |
42 | ### macOS ###
43 | .DS_Store
44 | .AppleDouble
45 | .LSOverride
46 |
47 | # Thumbnails
48 | ._*
49 | # Files that might appear in the root of a volume
50 | .DocumentRevisions-V100
51 | .fseventsd
52 | .Spotlight-V100
53 | .TemporaryItems
54 | .Trashes
55 | .VolumeIcon.icns
56 | .com.apple.timemachine.donotpresent
57 | # Directories potentially created on remote AFP share
58 | .AppleDB
59 | .AppleDesktop
60 | Network Trash Folder
61 | Temporary Items
62 | .apdisk
63 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 Daniel Imfeld
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/example/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 Daniel Imfeld
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/example/src/Switch.svelte:
--------------------------------------------------------------------------------
1 |
20 |
21 |
33 |
39 |
40 |
--------------------------------------------------------------------------------
/example/postcss.config.js:
--------------------------------------------------------------------------------
1 | const production = process.env.NODE_ENV !== 'development';
2 | const purgecss = require('@fullhuman/postcss-purgecss');
3 | const cssnano = require('cssnano');
4 |
5 | module.exports = {
6 | plugins: [
7 | require('postcss-import')(),
8 | require('tailwindcss'),
9 | require('autoprefixer'),
10 | production &&
11 | purgecss({
12 | content: ['./src/**/*.html', './static/**/*.html', './src/**/*.svelte'],
13 | whitelistPatterns: [/^svelte-/],
14 | defaultExtractor: (content) => {
15 | const regExp = new RegExp(/[A-Za-z0-9-_:/.]+/g);
16 |
17 | const matchedTokens = [];
18 |
19 | let match = regExp.exec(content);
20 | // To make sure that you do not lose any tailwind classes used in class directive.
21 | // https://github.com/tailwindcss/discuss/issues/254#issuecomment-517918397
22 | while (match) {
23 | if (match[0].startsWith('class:')) {
24 | matchedTokens.push(match[0].substring(6));
25 | } else {
26 | matchedTokens.push(match[0]);
27 | }
28 |
29 | match = regExp.exec(content);
30 | }
31 |
32 | return matchedTokens;
33 | },
34 | }),
35 | production && cssnano(),
36 | ].filter(Boolean),
37 | };
38 |
--------------------------------------------------------------------------------
/dist/fetcher.d.ts.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"fetcher.d.ts","sourceRoot":"","sources":["../src/fetcher.ts"],"names":[],"mappings":"AACA,OAAO,EAA8B,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAE3E,eAAO,MAAM,UAAU,eAAuB,CAAC;AAE/C,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,IAAI,CAAC,EAAE,CAAC,CAAC;IACT,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,iCAAiC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,KAAK,CAAC,EAAG,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,cAAc,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;IAChD,KAAK,EAAE,cAAc,CAAC;CACvB;AAED,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,IAAI,EAAE,CAAC,CAAC;IACR,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB,CACjC,CAAC;IAED;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,qDAAqD;IACrD,WAAW,CAAC,EAAE,CAAC,GAAG,EAAC,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,GAAC,IAAI,CAAC,CAAC;IAE3D,4DAA4D;IAC5D,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,GAAC,MAAM,CAAC,CAAC;IAC5C,qDAAqD;IACrD,OAAO,EAAE,CAAC,MAAM,EAAG,WAAW,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC;IAE1C,iFAAiF;IACjF,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B,8GAA8G;IAC9G,UAAU,CAAC,EAAG,MAAM,CAAC;IAErB,yIAAyI;IACzI,gBAAgB,CAAC,EAAG,OAAO,CAAC;IAE5B;;;OAGG;IACH,cAAc,CAAC,EAAG,OAAO,CAAC;IAE1B,wLAAwL;IACxL,KAAK,CAAC,EAAE,CAAC,GAAG,EAAG,YAAY,KAAK,GAAG,CAAC;CACrC;AAED,MAAM,WAAW,WAAW;IAC1B,8GAA8G;IAC9G,UAAU,EAAE,CAAC,OAAO,EAAG,OAAO,KAAK,IAAI,CAAC;IACxC;4DACwD;IACxD,YAAY,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3C,mGAAmG;IACnG,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,OAAO;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;IACtB,cAAc,EAAE,OAAO,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,OAAO,CACrB,CAAC,EAED,EACE,IAAI,EACJ,GAAG,EACH,iBAAkC,EAAE,iBAAiB;AACrD,UAAsB,EAAE,mBAAmB;AAC3C,OAAO,EACP,OAAO,EACP,gBAAgB,EAChB,cAAc,EACd,WAAW,EACX,KAAK,GACN,EAAE,kBAAkB,CAAC,CAAC,CAAC,GACtB,WAAW,CAiOd"}
--------------------------------------------------------------------------------
/dist/browser_state.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"browser_state.js","sourceRoot":"","sources":["../src/browser_state.ts"],"names":[],"mappings":"AAAA,uIAAuI;AACvI,MAAM,UAAU,iBAAiB;IAC/B,IACE,OAAO,QAAQ,KAAK,WAAW;QAC/B,OAAO,QAAQ,CAAC,eAAe,KAAK,WAAW,EAC/C;QACA,OAAO,QAAQ,CAAC,eAAe,KAAK,QAAQ,CAAC;KAC9C;IACD,iCAAiC;IACjC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,QAAQ;IACtB,IAAI,OAAO,SAAS,CAAC,MAAM,KAAK,WAAW,EAAE;QAC3C,OAAO,SAAS,CAAC,MAAM,CAAC;KACzB;IACD,8DAA8D;IAC9D,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,iBAAiB;IAC/B,OAAO,QAAQ,CAAC,QAAQ,EAAE,CAAC;AAC7B,CAAC;AAQD,MAAM,UAAU,eAAe;IAC7B,OAAO;QACL,MAAM,EAAE,QAAQ,EAAE;QAClB,OAAO,EAAE,iBAAiB,EAAE;QAC5B,OAAO,EAAE,iBAAiB,EAAE;KAC7B,CAAC;AACJ,CAAC;AAED,MAAM,SAAS,GAAG,EAAE,CAAC;AAErB,SAAS,OAAO;IACd,IAAI,KAAK,GAAG,eAAe,EAAE,CAAC;IAC9B,IAAI,KAAK,CAAC;IAEV,KAAI,IAAI,QAAQ,IAAI,SAAS,EAAE;QAC7B,IAAI;YACF,QAAQ,CAAC,KAAK,CAAC,CAAC;SACjB;QAAC,OAAM,CAAC,EAAE;YACT,2EAA2E;YAC3E,qBAAqB;YACrB,KAAK,GAAG,CAAC,CAAC;SACX;KACF;IAED,IAAG,KAAK,EAAE;QACR,qEAAqE;QACrE,MAAM,KAAK,CAAC;KACb;AACH,CAAC;AAED,SAAS,WAAW,CAAC,EAAE;IACrB,IAAI,KAAK,GAAG,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAClC,IAAG,KAAK,KAAK,CAAC,CAAC,EAAE;QACf,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;KAC5B;IAED,IAAG,CAAC,SAAS,CAAC,MAAM,EAAE;QACpB,MAAM,CAAC,mBAAmB,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC;QACxD,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC7C,MAAM,CAAC,mBAAmB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC5C,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC9C,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;KAChD;AACH,CAAC;AAID,MAAM,UAAU,SAAS,CAAC,EAAkC;IAC1D,IAAG,CAAC,SAAS,CAAC,MAAM,EAAE;QACpB,MAAM,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC;QACrD,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC1C,MAAM,CAAC,gBAAgB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACzC,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC3C,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;KAC7C;IAED,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEnB,uDAAuD;IACvD,EAAE,CAAC,eAAe,EAAE,CAAC,CAAC;IAEtB,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;AAC/B,CAAC"}
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "swr-xstate-example",
3 | "author": "Daniel Imfeld",
4 | "license": "MIT",
5 | "version": "0.0.2",
6 | "scripts": {
7 | "clean": "rimraf public",
8 | "rollup": "rollup -c",
9 | "rollup:watch": "rollup -c -w",
10 | "build": "NODE_ENV=production run-s clean rollup",
11 | "dev": "NODE_ENV=development npm-run-all clean --parallel start:dev rollup:watch",
12 | "start": "sirv public --single -H 0.0.0.0",
13 | "start:dev": "NODE_ENV=development sirv public --single --dev -H 0.0.0.0"
14 | },
15 | "dependencies": {
16 | "sirv-cli": "^0.4.4",
17 | "swr-xstate": "^1.0.1"
18 | },
19 | "devDependencies": {
20 | "@babel/core": "^7.9.0",
21 | "@babel/plugin-proposal-class-properties": "^7.8.3",
22 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
23 | "@babel/plugin-proposal-object-rest-spread": "^7.8.3",
24 | "@babel/plugin-proposal-optional-chaining": "^7.8.3",
25 | "@babel/plugin-syntax-dynamic-import": "^7.8.3",
26 | "@babel/plugin-transform-runtime": "^7.9.0",
27 | "@babel/preset-env": "^7.9.0",
28 | "@babel/preset-typescript": "^7.9.0",
29 | "@babel/runtime": "^7.9.0",
30 | "@fullhuman/postcss-purgecss": "^1.3.0",
31 | "@rollup/plugin-babel": "^5.0.3",
32 | "@rollup/plugin-commonjs": "^13.0.0",
33 | "@rollup/plugin-html": "^0.2.0",
34 | "@rollup/plugin-node-resolve": "^8.1.0",
35 | "@rollup/plugin-replace": "^2.3.3",
36 | "@tailwindcss/ui": "^0.1.3",
37 | "@typescript-eslint/eslint-plugin": "^2.26.0",
38 | "@typescript-eslint/parser": "^2.26.0",
39 | "autoprefixer": "^9.6.1",
40 | "babel-eslint": "^10.0.3",
41 | "cssnano": "^4.1.10",
42 | "eslint": "^6.8.0",
43 | "eslint-config-prettier": "^6.10.1",
44 | "eslint-plugin-svelte3": "^2.7.3",
45 | "lodash": "^4.17.21",
46 | "npm-run-all": "^4.1.5",
47 | "postcss": "^7.0.18",
48 | "postcss-cli": "^6.1.3",
49 | "postcss-import": "^12.0.1",
50 | "prettier": "^2",
51 | "prettier-plugin-svelte": "^1.1.0",
52 | "rollup": "^2",
53 | "rollup-plugin-copy": "^3.3.0",
54 | "rollup-plugin-livereload": "^1.0.0",
55 | "rollup-plugin-postcss": "^2.0.3",
56 | "rollup-plugin-svelte": "^5.2.2",
57 | "rollup-plugin-terser": "^6.1.0",
58 | "svelte": "^3.24",
59 | "svelte-preprocess": "^4",
60 | "tailwindcss": "^1.4.0",
61 | "typescript": "^3.9"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/dist/browser_state.js:
--------------------------------------------------------------------------------
1 | // The browser state functions here are mostly adapted from the related functions in https://github.com/vercel/swr/tree/master/src/libs
2 | export function isDocumentVisible() {
3 | if (typeof document !== 'undefined' &&
4 | typeof document.visibilityState !== 'undefined') {
5 | return document.visibilityState !== 'hidden';
6 | }
7 | // Otherwise assume it's visible.
8 | return true;
9 | }
10 | export function isOnline() {
11 | if (typeof navigator.onLine !== 'undefined') {
12 | return navigator.onLine;
13 | }
14 | // Assume it's online if onLine doesn't exist for some reason.
15 | return true;
16 | }
17 | export function isDocumentFocused() {
18 | return document.hasFocus();
19 | }
20 | export function getBrowserState() {
21 | return {
22 | online: isOnline(),
23 | visible: isDocumentVisible(),
24 | focused: isDocumentFocused(),
25 | };
26 | }
27 | const listeners = [];
28 | function refresh() {
29 | let state = getBrowserState();
30 | let error;
31 | for (let listener of listeners) {
32 | try {
33 | listener(state);
34 | }
35 | catch (e) {
36 | // Catch errors and store them away to ensure that we call all the handlers
37 | // even if one fails.
38 | error = e;
39 | }
40 | }
41 | if (error) {
42 | // Throw the error so that any global error handlers will pick it up.
43 | throw error;
44 | }
45 | }
46 | function unsubscribe(cb) {
47 | let index = listeners.indexOf(cb);
48 | if (index !== -1) {
49 | listeners.splice(index, 1);
50 | }
51 | if (!listeners.length) {
52 | window.removeEventListener('visibilitychange', refresh);
53 | window.removeEventListener('focus', refresh);
54 | window.removeEventListener('blur', refresh);
55 | window.removeEventListener('online', refresh);
56 | window.removeEventListener('offline', refresh);
57 | }
58 | }
59 | export function subscribe(cb) {
60 | if (!listeners.length) {
61 | window.addEventListener('visibilitychange', refresh);
62 | window.addEventListener('focus', refresh);
63 | window.addEventListener('blur', refresh);
64 | window.addEventListener('online', refresh);
65 | window.addEventListener('offline', refresh);
66 | }
67 | listeners.push(cb);
68 | // Call the callback right away with the current state.
69 | cb(getBrowserState());
70 | return () => unsubscribe(cb);
71 | }
72 | //# sourceMappingURL=browser_state.js.map
--------------------------------------------------------------------------------
/src/browser_state.ts:
--------------------------------------------------------------------------------
1 | // The browser state functions here are mostly adapted from the related functions in https://github.com/vercel/swr/tree/master/src/libs
2 | export function isDocumentVisible(): boolean {
3 | if (
4 | typeof document !== 'undefined' &&
5 | typeof document.visibilityState !== 'undefined'
6 | ) {
7 | return document.visibilityState !== 'hidden';
8 | }
9 | // Otherwise assume it's visible.
10 | return true;
11 | }
12 |
13 | export function isOnline(): boolean {
14 | if (typeof navigator.onLine !== 'undefined') {
15 | return navigator.onLine;
16 | }
17 | // Assume it's online if onLine doesn't exist for some reason.
18 | return true;
19 | }
20 |
21 | export function isDocumentFocused(): boolean {
22 | return document.hasFocus();
23 | }
24 |
25 | export interface BrowserState {
26 | online: boolean;
27 | visible: boolean;
28 | focused: boolean;
29 | }
30 |
31 | export function getBrowserState() : BrowserState {
32 | return {
33 | online: isOnline(),
34 | visible: isDocumentVisible(),
35 | focused: isDocumentFocused(),
36 | };
37 | }
38 |
39 | const listeners = [];
40 |
41 | function refresh() {
42 | let state = getBrowserState();
43 | let error;
44 |
45 | for(let listener of listeners) {
46 | try {
47 | listener(state);
48 | } catch(e) {
49 | // Catch errors and store them away to ensure that we call all the handlers
50 | // even if one fails.
51 | error = e;
52 | }
53 | }
54 |
55 | if(error) {
56 | // Throw the error so that any global error handlers will pick it up.
57 | throw error;
58 | }
59 | }
60 |
61 | function unsubscribe(cb) {
62 | let index = listeners.indexOf(cb);
63 | if(index !== -1) {
64 | listeners.splice(index, 1);
65 | }
66 |
67 | if(!listeners.length) {
68 | window.removeEventListener('visibilitychange', refresh);
69 | window.removeEventListener('focus', refresh);
70 | window.removeEventListener('blur', refresh);
71 | window.removeEventListener('online', refresh);
72 | window.removeEventListener('offline', refresh);
73 | }
74 | }
75 |
76 | export type Unsubscriber = () => void;
77 |
78 | export function subscribe(cb : (state : BrowserState) => any) : Unsubscriber {
79 | if(!listeners.length) {
80 | window.addEventListener('visibilitychange', refresh);
81 | window.addEventListener('focus', refresh);
82 | window.addEventListener('blur', refresh);
83 | window.addEventListener('online', refresh);
84 | window.addEventListener('offline', refresh);
85 | }
86 |
87 | listeners.push(cb);
88 |
89 | // Call the callback right away with the current state.
90 | cb(getBrowserState());
91 |
92 | return () => unsubscribe(cb);
93 | }
94 |
--------------------------------------------------------------------------------
/dist/fetcher.d.ts:
--------------------------------------------------------------------------------
1 | import { State, AnyEventObject } from 'xstate';
2 | export declare const UNMODIFIED: unique symbol;
3 | export interface FetchResult {
4 | data?: T;
5 | error?: Error;
6 | /** When this data was fetched */
7 | timestamp: number;
8 | /** True if this is the stale data. Absent otherwise. */
9 | stale?: boolean;
10 | }
11 | export interface DebugMessage {
12 | id: string;
13 | state: State;
14 | event: AnyEventObject;
15 | }
16 | export interface InitialData {
17 | data: T;
18 | timestamp?: number;
19 | }
20 | export interface AutoFetcherOptions {
21 | /** A string that can uniquely identify the resource to be fetched. This is
22 | * passed as an argument to the `fetch` and `initialData` functions.
23 | */
24 | key: string;
25 | /** A name for this fetcher. Will default to the vaue of `key` if not set. */
26 | name?: string;
27 | /** A function that should fetch the "stale" data. */
28 | initialData?: (key: string) => Promise | null>;
29 | /** `fetcher` is called periodically to retrieve new data */
30 | fetcher: (key: string) => Promise;
31 | /** `receive` is called when new data has arrived. */
32 | receive: (result: FetchResult) => any;
33 | /** Number of milliseconds between refresh attempts, unless refresh is forced. */
34 | autoRefreshPeriod?: number;
35 | /** Maximum number of milliseconds to wait between refresh attempts in case of error. Defaults to 1 minute. */
36 | maxBackoff?: number;
37 | /** True if the state machine should permit refreshes by default. False if it should wait for `setPerrmitted(true)`. Defaults to true. */
38 | initialPermitted?: boolean;
39 | /** True if the state machine should be enabled by default. False if it should wait for `setEnabled(true)`
40 | * with `true` as the data before it starts refreshing. Defaults to true.
41 | * Typically this is used to disable updates if nothing in the application is actually listening for changes.
42 | */
43 | initialEnabled?: boolean;
44 | /** Given an object, this function should print out debug information. This can be `console.log` if you want, or something like the `debug` module. Called on every state transition. */
45 | debug?: (msg: DebugMessage) => any;
46 | }
47 | export interface AutoFetcher {
48 | /** Set if fetching is enabled. It might be disabled if you know that nothing is using this data right now. */
49 | setEnabled: (enabled: boolean) => void;
50 | /** Set if fetching is permitted. Fetching might not be permitted if the user is not logged in or lacks
51 | * proper permissions for this endpoint, for example. */
52 | setPermitted: (permitted: boolean) => void;
53 | /** Force a refresh. This will not do anything if fetching has been disabled via `setPermitted`. */
54 | refresh: () => void;
55 | destroy: () => void;
56 | }
57 | export interface Context {
58 | lastRefresh: number;
59 | retries: number;
60 | reportedError: boolean;
61 | storeEnabled: boolean;
62 | browserEnabled: boolean;
63 | permitted: boolean;
64 | }
65 | export declare function fetcher({ name, key, autoRefreshPeriod, // default 1 hour
66 | maxBackoff, // default 1 minute
67 | fetcher, receive, initialPermitted, initialEnabled, initialData, debug, }: AutoFetcherOptions): AutoFetcher;
68 | //# sourceMappingURL=fetcher.d.ts.map
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is an implementation of a stale-while-revalidate data fetcher implemented with the [XState](https://xstate.js.org) library. It implements many of the features found in other popular SWR libraries, such as periodic updates and disabling the fetcher when the browser is not focused.
2 |
3 | I wrote about the design and implementation on [my website](https://imfeld.dev/writing/swr_with_xstate), and there's also a simple [example site](https://swr-xstate.imfeld.dev).
4 |
5 | ```typescript
6 | /** The fetcher function can return this if it tried to fetch and found the data was unchanged. */
7 | export const UNMODIFIED = Symbol('unmodified');
8 |
9 | export interface FetchResult {
10 | data?: T;
11 | error?: Error;
12 | /** When this data was fetched */
13 | timestamp: number;
14 | /** True if this is the stale data. Absent otherwise. */
15 | stale? : boolean;
16 | }
17 |
18 | export interface DebugMessage {
19 | id: string;
20 | state: State;
21 | event: AnyEventObject;
22 | }
23 |
24 | export interface InitialData {
25 | data: T;
26 | timestamp?: number;
27 | }
28 |
29 | export interface AutoFetcherOptions<
30 | T
31 | > {
32 | /** A string that can uniquely identify the resource to be fetched. This is
33 | * passed as an argument to the `fetch` and `initialData` functions.
34 | */
35 | key: string;
36 |
37 | /** A name for this fetcher. Will default to the vaue of `key` if not set. */
38 | name?: string;
39 |
40 | /** A function that should fetch the "stale" data. Called when the fetcher is created. */
41 | initialData?: (key:string) => Promise|null>;
42 |
43 | /** `fetcher` is called periodically to retrieve new data */
44 | fetcher: (key: string) => Promise,
45 | /** `receive` is called when new data has arrived. */
46 | receive: (result : FetchResult) => any;
47 |
48 | /** Number of milliseconds between refresh attempts, unless refresh is forced. */
49 | autoRefreshPeriod?: number;
50 |
51 | /** Maximum number of milliseconds to wait between refresh attempts in case of error. Defaults to 1 minute. */
52 | maxBackoff? : number;
53 |
54 | /** True if the state machine should permit refreshes by default. False if it should wait for `setPerrmitted(true)`. Defaults to true. */
55 | initialPermitted? : boolean;
56 |
57 | /** True if the state machine should be enabled by default. False if it should wait for `setEnabled(true)`
58 | * with `true` as the data before it starts refreshing. Defaults to true.
59 | * Typically this is used to disable updates if nothing in the application is actually listening for changes.
60 | */
61 | initialEnabled? : boolean;
62 |
63 | /** Given an object, this function should print out debug information. This can be `console.log` if you want, or something like the `debug` module. Called on every state transition. */
64 | debug?: (msg : DebugMessage) => any;
65 | }
66 |
67 | export interface AutoFetcher {
68 | /** Set if fetching is enabled. It might be disabled if you know that nothing is using this data right now. */
69 | setEnabled: (enabled : boolean) => void;
70 | /** Set if fetching is permitted. Fetching might not be permitted if the user is not logged in or lacks
71 | * proper permissions for this endpoint, for example. */
72 | setPermitted: (permitted: boolean) => void;
73 | /** Force a refresh. This will not do anything if fetching has been disabled via `setPermitted`. */
74 | refresh: () => void;
75 | destroy: () => void;
76 | }
77 |
78 | /** This function creates a fetcher */
79 | export function fetcher(options : AutoFetcherOptions) : AutoFetcher;
80 | ```
81 |
82 |
--------------------------------------------------------------------------------
/example/rollup.config.js:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import { template } from 'lodash';
3 | import svelte from 'rollup-plugin-svelte';
4 | import resolve from '@rollup/plugin-node-resolve';
5 | import replace from '@rollup/plugin-replace';
6 | import commonjs from '@rollup/plugin-commonjs';
7 | import livereload from 'rollup-plugin-livereload';
8 | import { terser } from 'rollup-plugin-terser';
9 | import postcss from 'rollup-plugin-postcss';
10 | import babel from '@rollup/plugin-babel';
11 | import html, { makeHtmlAttributes } from '@rollup/plugin-html';
12 | import copy from 'rollup-plugin-copy';
13 |
14 | const production = process.env.NODE_ENV !== 'development';
15 |
16 | const babelConfig = {
17 | extensions: ['.js', '.mjs', '.html', '.svelte', '.ts'],
18 | exclude: ['node_modules/@babel/**', 'static/**', 'build/**', 'public/**'],
19 | presets: [
20 | ['@babel/preset-env', { targets: { esmodules: true } }],
21 | '@babel/preset-typescript',
22 | ],
23 | plugins: [
24 | '@babel/plugin-syntax-dynamic-import',
25 | '@babel/proposal-class-properties',
26 | '@babel/plugin-proposal-object-rest-spread',
27 | '@babel/plugin-proposal-nullish-coalescing-operator',
28 | '@babel/plugin-proposal-optional-chaining',
29 | ],
30 | };
31 |
32 | export default {
33 | input: 'src/main.js',
34 | output: {
35 | dir: 'public',
36 | entryFileNames: '[name].[hash].js',
37 | chunkFileNames: '[name].[hash].js',
38 | assetFileNames: '[name].[hash][extname]',
39 | sourcemap: production ? true : 'inline',
40 | format: 'esm',
41 | },
42 | plugins: [
43 | svelte({
44 | preprocess: require('./svelte.config').preprocess,
45 | // enable run-time checks when not in production
46 | dev: !production,
47 | // extract any component CSS out into
48 | // a separate file — better for performance
49 | // css: css => {
50 | // css.write('public/bundle.css');
51 | // },
52 | // Instead, emit CSS as a file for processing through rollup
53 | emitCss: true,
54 | }),
55 | postcss({
56 | extract: true,
57 | }),
58 |
59 | resolve({
60 | mainFields: ['module', 'browser', 'main'],
61 | extensions: ['.mjs', '.js', '.json', '.ts', '.svelte'],
62 | dedupe: (importee) =>
63 | importee === 'svelte' || importee.startsWith('svelte/'),
64 | }),
65 | copy({
66 | targets: [{ src: 'static/**/*', dest: 'public/' }],
67 | }),
68 | commonjs(),
69 | babel(babelConfig),
70 |
71 | replace({
72 | 'process.env.NODE_ENV': JSON.stringify(
73 | process.env.NODE_ENV || 'development'
74 | ),
75 | }),
76 |
77 | // Watch the `public` directory and refresh the
78 | // browser on changes when not in production
79 | !production && livereload('public'),
80 |
81 | // If we're building for production (npm run build
82 | // instead of npm run dev), minify
83 | production && terser(),
84 |
85 | html({
86 | title: 'SWR-Xstate Example',
87 | template: ({ attributes, files, publicPath, title }) => {
88 | let templateFile = fs.readFileSync('src/index.html');
89 |
90 | // This is adapted from the default template function in the HTML plugin.
91 | const scripts = (files.js || [])
92 | .map(({ fileName }) => {
93 | const attrs = makeHtmlAttributes(attributes.script);
94 | return ``;
95 | })
96 | .join('\n');
97 |
98 | const links = (files.css || [])
99 | .map(({ fileName }) => {
100 | const attrs = makeHtmlAttributes(attributes.link);
101 | return ` `;
102 | })
103 | .join('\n');
104 |
105 | let exec = template(templateFile.toString());
106 | return exec({
107 | attributes,
108 | title,
109 | scripts,
110 | links,
111 | htmlAttributes: makeHtmlAttributes(attributes.html),
112 | });
113 | },
114 | }),
115 | ],
116 | watch: {
117 | clearScreen: false,
118 | },
119 | };
120 |
--------------------------------------------------------------------------------
/example/src/App.svelte:
--------------------------------------------------------------------------------
1 |
67 |
68 |
73 |
74 |
75 |
SWR Xstate
76 |
82 |
83 |
84 | This is a very simple example of automatic periodic fetching that gets a new
85 | random Unsplash image every five seconds. You can use the checkboxes to
86 | alter the behavior of the state machine.
87 |
88 |
89 |
90 |
91 |
92 |
93 | Enable Fetcher
94 |
95 |
96 |
97 | Permit Fetching
98 |
99 |
100 |
101 | Fetch Succeeds
102 |
103 |
104 | fetcher.refresh()}>
112 | Force Refresh
113 |
114 |
115 |
116 |
117 | Fetch Delay
118 | {fetchDelay}ms
119 |
120 |
121 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 | Fetch Result:
137 | {errorText || imageSrc}
138 | {#if receivedStale}
139 | (stale)
140 | {/if}
141 |
142 |
143 |
144 |
145 |
146 |
147 | Current State:
148 |
149 | {fetcherState && fetcherState.toStrings()}
150 |
151 |
152 |
Last Refresh: {new Date(latestTimestamp).toTimeString()}
153 |
Store Enabled: {fetcherState.context.storeEnabled}
154 |
Browser Active: {fetcherState.context.browserEnabled}
155 |
Fetching Permitted: {fetcherState.context.permitted}
156 |
157 |
158 |
159 |
160 |
--------------------------------------------------------------------------------
/dist/fetcher.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"fetcher.js","sourceRoot":"","sources":["../src/fetcher.ts"],"names":[],"mappings":"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"}
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src/**/*.ts"],
3 | "compilerOptions": {
4 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
5 |
6 | /* Basic Options */
7 | "incremental": true, /* Enable incremental compilation */
8 | "target": "ES2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
9 | "module": "ESNext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
10 | "lib": ["dom", "ES2019"], /* Specify library files to be included in the compilation. */
11 | // "allowJs": true, /* Allow javascript files to be compiled. */
12 | // "checkJs": true, /* Report errors in .js files. */
13 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
14 | "declaration": true, /* Generates corresponding '.d.ts' file. */
15 | "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
16 | "sourceMap": true, /* Generates corresponding '.map' file. */
17 | // "outFile": "./", /* Concatenate and emit output to single file. */
18 | "outDir": "dist", /* Redirect output structure to the directory. */
19 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
20 | // "composite": true, /* Enable project compilation */
21 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
22 | // "removeComments": true, /* Do not emit comments to output. */
23 | // "noEmit": true, /* Do not emit outputs. */
24 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
25 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
26 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
27 |
28 | /* Strict Type-Checking Options */
29 | // "strict": true, /* Enable all strict type-checking options. */
30 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
31 | // "strictNullChecks": true, /* Enable strict null checks. */
32 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
33 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
34 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
35 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
36 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
37 |
38 | /* Additional Checks */
39 | // "noUnusedLocals": true, /* Report errors on unused locals. */
40 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
41 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
42 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
43 |
44 | /* Module Resolution Options */
45 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
46 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
47 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
49 | // "typeRoots": [], /* List of folders to include type definitions from. */
50 | // "types": [], /* Type declaration files to be included in compilation. */
51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
52 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
54 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
55 |
56 | /* Source Map Options */
57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
61 |
62 | /* Experimental Options */
63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
65 |
66 | /* Advanced Options */
67 | "skipLibCheck": true, /* Skip type checking of declaration files. */
68 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/dist/fetcher.js:
--------------------------------------------------------------------------------
1 | import * as browserStateModule from './browser_state';
2 | import { interpret, assign, Machine } from 'xstate';
3 | export const UNMODIFIED = Symbol('unmodified');
4 | export function fetcher({ name, key, autoRefreshPeriod = 60 * 60 * 1000, // default 1 hour
5 | maxBackoff = 60 * 1000, // default 1 minute
6 | fetcher, receive, initialPermitted, initialEnabled, initialData, debug, }) {
7 | name = name || key;
8 | const id = `autofetcher-${name}`;
9 | const machineDef = Machine({
10 | id,
11 | initial: 'maybeStart',
12 | on: {
13 | FETCHER_ENABLED: { target: 'maybeStart', actions: 'updateStoreEnabled' },
14 | SET_PERMITTED: { target: 'maybeStart', actions: 'updatePermitted' },
15 | BROWSER_ENABLED: {
16 | target: 'maybeStart',
17 | actions: 'updateBrowserEnabled',
18 | },
19 | INITIAL_DATA: {
20 | actions: 'receiveInitialData',
21 | },
22 | },
23 | states: {
24 | // Not permitted to refresh, so ignore everything except the global events that might permit us to refresh.
25 | notPermitted: {
26 | entry: ['clearData', 'clearLastRefresh'],
27 | },
28 | // Store is disabled, but still permitted to refresh so we honor the FORCE_REFRESH event.
29 | disabled: {
30 | on: {
31 | FORCE_REFRESH: {
32 | target: 'refreshing',
33 | cond: 'permitted_to_refresh',
34 | },
35 | },
36 | },
37 | maybeStart: {
38 | always: [
39 | { cond: 'not_permitted_to_refresh', target: 'notPermitted' },
40 | { cond: 'can_enable', target: 'waitingForRefresh' },
41 | { target: 'disabled' },
42 | ],
43 | },
44 | waitingForRefresh: {
45 | on: {
46 | FORCE_REFRESH: 'refreshing',
47 | },
48 | after: {
49 | nextRefreshDelay: 'refreshing',
50 | },
51 | },
52 | refreshing: {
53 | on: {
54 | // Ignore the events while we're refreshing but still update the
55 | // context so we know where to go next.
56 | FETCHER_ENABLED: { target: undefined, actions: 'updateStoreEnabled' },
57 | SET_PERMITTED: { target: undefined, actions: 'updatePermitted' },
58 | BROWSER_ENABLED: {
59 | target: undefined,
60 | actions: 'updateBrowserEnabled',
61 | },
62 | },
63 | invoke: {
64 | id: 'refresh',
65 | src: 'refresh',
66 | onDone: {
67 | target: 'maybeStart',
68 | actions: 'refreshDone',
69 | },
70 | onError: {
71 | target: 'errorBackoff',
72 | actions: 'reportError',
73 | },
74 | },
75 | },
76 | errorBackoff: {
77 | entry: 'incrementRetry',
78 | after: {
79 | errorBackoffDelay: 'refreshing',
80 | },
81 | },
82 | },
83 | }, {
84 | delays: {
85 | errorBackoffDelay: (context, event) => {
86 | const baseDelay = 200;
87 | const delay = baseDelay * 2 ** Math.min(context.retries, 20);
88 | return Math.min(delay, maxBackoff);
89 | },
90 | nextRefreshDelay: (context) => {
91 | let timeSinceRefresh = Date.now() - context.lastRefresh;
92 | let remaining = autoRefreshPeriod - timeSinceRefresh;
93 | return Math.max(remaining, 0);
94 | },
95 | },
96 | services: {
97 | refresh: () => fetcher(key),
98 | },
99 | actions: {
100 | clearLastRefresh: assign({
101 | lastRefresh: () => 0,
102 | }),
103 | updatePermitted: assign({
104 | permitted: (ctx, event) => event.data,
105 | }),
106 | updateStoreEnabled: assign({
107 | storeEnabled: (ctx, event) => event.data,
108 | }),
109 | updateBrowserEnabled: assign({
110 | browserEnabled: (ctx, event) => event.data,
111 | }),
112 | receiveInitialData: assign((context, { data }) => {
113 | if (context.lastRefresh) {
114 | // We already got some new data, so don't send the stale data.
115 | return {};
116 | }
117 | let timestamp = data.timestamp || 0;
118 | receive({
119 | data: data.data,
120 | stale: true,
121 | timestamp,
122 | });
123 | return {
124 | // If the initial data included a timestamp, put it into lastRefresh.
125 | lastRefresh: timestamp,
126 | };
127 | }),
128 | incrementRetry: assign({ retries: (context) => context.retries + 1 }),
129 | refreshDone: assign((context, event) => {
130 | let lastRefresh = Date.now();
131 | let updated = {
132 | lastRefresh,
133 | retries: 0,
134 | reportedError: false,
135 | };
136 | if (event.data !== UNMODIFIED && context.permitted) {
137 | receive({ data: event.data, timestamp: lastRefresh });
138 | }
139 | return updated;
140 | }),
141 | reportError: assign((context, event) => {
142 | // Ignore the error if it happened because the browser went offline while fetching.
143 | // Otherwise report it.
144 | if (!context.reportedError &&
145 | browserStateModule.isOnline()) {
146 | receive({ error: event.data, timestamp: Date.now() });
147 | }
148 | return {
149 | reportedError: true,
150 | };
151 | }),
152 | clearData: () => receive({ data: null, timestamp: Date.now() }),
153 | },
154 | guards: {
155 | not_permitted_to_refresh: (ctx) => !ctx.permitted,
156 | permitted_to_refresh: (ctx) => ctx.permitted,
157 | can_enable: (ctx) => {
158 | if (!ctx.storeEnabled || !ctx.permitted) {
159 | return false;
160 | }
161 | if (!ctx.lastRefresh) {
162 | // Refresh if we haven't loaded any data yet.
163 | return true;
164 | }
165 | // Finally, we can enable if the browser tab is active.
166 | return ctx.browserEnabled;
167 | },
168 | },
169 | }, {
170 | lastRefresh: 0,
171 | retries: 0,
172 | reportedError: false,
173 | browserEnabled: true,
174 | storeEnabled: initialEnabled === false ? false : true,
175 | permitted: initialPermitted === false ? false : true,
176 | });
177 | let machine = interpret(machineDef).start();
178 | if (debug) {
179 | machine.onTransition((state, event) => {
180 | debug({ id, event, state });
181 | });
182 | }
183 | let browserStateUnsub = browserStateModule.subscribe((state) => {
184 | let enabled = state.focused && state.online && state.visible;
185 | machine.send({ type: 'BROWSER_ENABLED', data: enabled });
186 | });
187 | async function fetchInitial() {
188 | let data = await initialData(key);
189 | let d = data?.data;
190 | if (d !== null && d !== undefined) {
191 | machine.send('INITIAL_DATA', { data });
192 | }
193 | }
194 | if (initialData) {
195 | fetchInitial();
196 | }
197 | return {
198 | /** Enable or disable the fetcher. This is usually linked to whether there is anything that actually cares about this
199 | * data or not. */
200 | setEnabled: (enabled) => machine.send({ type: 'FETCHER_ENABLED', data: enabled }),
201 | /** Set if fetching is permitted. Fetching might not be permitted if the user is not logged in or lacks
202 | * proper permissions for this endpoint, for example. */
203 | setPermitted: (permitted) => machine.send({ type: 'SET_PERMITTED', data: permitted }),
204 | /** Force a refresh. This will not do anything if fetching has been disabled via `setPermitted`. */
205 | refresh: () => machine.send('FORCE_REFRESH'),
206 | destroy: () => {
207 | browserStateUnsub();
208 | machine.stop();
209 | },
210 | };
211 | }
212 | //# sourceMappingURL=fetcher.js.map
--------------------------------------------------------------------------------
/src/fetcher.ts:
--------------------------------------------------------------------------------
1 | import * as browserStateModule from './browser_state';
2 | import { interpret, assign, Machine, State, AnyEventObject } from 'xstate';
3 |
4 | export const UNMODIFIED = Symbol('unmodified');
5 |
6 | export interface FetchResult {
7 | data?: T;
8 | error?: Error;
9 | /** When this data was fetched */
10 | timestamp: number;
11 | /** True if this is the stale data. Absent otherwise. */
12 | stale? : boolean;
13 | }
14 |
15 | export interface DebugMessage {
16 | id: string;
17 | state: State;
18 | event: AnyEventObject;
19 | }
20 |
21 | export interface InitialData {
22 | data: T;
23 | timestamp?: number;
24 | }
25 |
26 | export interface AutoFetcherOptions<
27 | T
28 | > {
29 | /** A string that can uniquely identify the resource to be fetched. This is
30 | * passed as an argument to the `fetch` and `initialData` functions.
31 | */
32 | key: string;
33 |
34 | /** A name for this fetcher. Will default to the vaue of `key` if not set. */
35 | name?: string;
36 |
37 | /** A function that should fetch the "stale" data. */
38 | initialData?: (key:string) => Promise|null>;
39 |
40 | /** `fetcher` is called periodically to retrieve new data */
41 | fetcher: (key: string) => Promise,
42 | /** `receive` is called when new data has arrived. */
43 | receive: (result : FetchResult) => any;
44 |
45 | /** Number of milliseconds between refresh attempts, unless refresh is forced. */
46 | autoRefreshPeriod?: number;
47 |
48 | /** Maximum number of milliseconds to wait between refresh attempts in case of error. Defaults to 1 minute. */
49 | maxBackoff? : number;
50 |
51 | /** True if the state machine should permit refreshes by default. False if it should wait for `setPerrmitted(true)`. Defaults to true. */
52 | initialPermitted? : boolean;
53 |
54 | /** True if the state machine should be enabled by default. False if it should wait for `setEnabled(true)`
55 | * with `true` as the data before it starts refreshing. Defaults to true.
56 | * Typically this is used to disable updates if nothing in the application is actually listening for changes.
57 | */
58 | initialEnabled? : boolean;
59 |
60 | /** Given an object, this function should print out debug information. This can be `console.log` if you want, or something like the `debug` module. Called on every state transition. */
61 | debug?: (msg : DebugMessage) => any;
62 | }
63 |
64 | export interface AutoFetcher {
65 | /** Set if fetching is enabled. It might be disabled if you know that nothing is using this data right now. */
66 | setEnabled: (enabled : boolean) => void;
67 | /** Set if fetching is permitted. Fetching might not be permitted if the user is not logged in or lacks
68 | * proper permissions for this endpoint, for example. */
69 | setPermitted: (permitted: boolean) => void;
70 | /** Force a refresh. This will not do anything if fetching has been disabled via `setPermitted`. */
71 | refresh: () => void;
72 | destroy: () => void;
73 | }
74 |
75 | export interface Context {
76 | lastRefresh: number;
77 | retries: number;
78 | reportedError: boolean;
79 | storeEnabled: boolean;
80 | browserEnabled: boolean;
81 | permitted: boolean;
82 | }
83 |
84 | export function fetcher<
85 | T
86 | >(
87 | {
88 | name,
89 | key,
90 | autoRefreshPeriod = 60 * 60 * 1000, // default 1 hour
91 | maxBackoff = 60 * 1000, // default 1 minute
92 | fetcher,
93 | receive,
94 | initialPermitted,
95 | initialEnabled,
96 | initialData,
97 | debug,
98 | }: AutoFetcherOptions
99 | ) : AutoFetcher {
100 | name = name || key;
101 | const id = `autofetcher-${name}`;
102 |
103 | const machineDef = Machine(
104 | {
105 | id,
106 | initial: 'maybeStart',
107 | on: {
108 | FETCHER_ENABLED: { target: 'maybeStart', actions: 'updateStoreEnabled' },
109 | SET_PERMITTED: { target: 'maybeStart', actions: 'updatePermitted' },
110 | BROWSER_ENABLED: {
111 | target: 'maybeStart',
112 | actions: 'updateBrowserEnabled',
113 | },
114 | INITIAL_DATA: {
115 | actions: 'receiveInitialData',
116 | },
117 | },
118 | states: {
119 | // Not permitted to refresh, so ignore everything except the global events that might permit us to refresh.
120 | notPermitted: {
121 | entry: ['clearData', 'clearLastRefresh'],
122 | },
123 | // Store is disabled, but still permitted to refresh so we honor the FORCE_REFRESH event.
124 | disabled: {
125 | on: {
126 | FORCE_REFRESH: {
127 | target: 'refreshing',
128 | cond: 'permitted_to_refresh',
129 | },
130 | },
131 | },
132 | maybeStart: {
133 | always: [
134 | { cond: 'not_permitted_to_refresh', target: 'notPermitted' },
135 | { cond: 'can_enable', target: 'waitingForRefresh' },
136 | { target: 'disabled' },
137 | ],
138 | },
139 | waitingForRefresh: {
140 | on: {
141 | FORCE_REFRESH: 'refreshing',
142 | },
143 | after: {
144 | nextRefreshDelay: 'refreshing',
145 | },
146 | },
147 | refreshing: {
148 | on: {
149 | // Ignore the events while we're refreshing but still update the
150 | // context so we know where to go next.
151 | FETCHER_ENABLED: { target: undefined, actions: 'updateStoreEnabled' },
152 | SET_PERMITTED: { target: undefined, actions: 'updatePermitted' },
153 | BROWSER_ENABLED: {
154 | target: undefined,
155 | actions: 'updateBrowserEnabled',
156 | },
157 | },
158 | invoke: {
159 | id: 'refresh',
160 | src: 'refresh',
161 | onDone: {
162 | target: 'maybeStart',
163 | actions: 'refreshDone',
164 | },
165 | onError: {
166 | target: 'errorBackoff',
167 | actions: 'reportError',
168 | },
169 | },
170 | },
171 | errorBackoff: {
172 | entry: 'incrementRetry',
173 | after: {
174 | errorBackoffDelay: 'refreshing',
175 | },
176 | },
177 | },
178 | },
179 | {
180 | delays: {
181 | errorBackoffDelay: (context, event) => {
182 | const baseDelay = 200;
183 | const delay = baseDelay * 2 ** Math.min(context.retries, 20);
184 | return Math.min(delay, maxBackoff);
185 | },
186 | nextRefreshDelay: (context) => {
187 | let timeSinceRefresh = Date.now() - context.lastRefresh;
188 | let remaining = autoRefreshPeriod - timeSinceRefresh;
189 | return Math.max(remaining, 0);
190 | },
191 | },
192 | services: {
193 | refresh: () => fetcher(key),
194 | },
195 | actions: {
196 | clearLastRefresh: assign({
197 | lastRefresh: () => 0,
198 | }),
199 | updatePermitted: assign({
200 | permitted: (ctx, event) => event.data,
201 | }),
202 | updateStoreEnabled: assign({
203 | storeEnabled: (ctx, event) => event.data,
204 | }),
205 | updateBrowserEnabled: assign({
206 | browserEnabled: (ctx, event) => event.data,
207 | }),
208 | receiveInitialData: assign((context: Context, {data}) => {
209 | if(context.lastRefresh) {
210 | // We already got some new data, so don't send the stale data.
211 | return {};
212 | }
213 |
214 | let timestamp = data.timestamp || 0;
215 | receive({
216 | data: data.data,
217 | stale: true,
218 | timestamp,
219 | });
220 |
221 | return {
222 | // If the initial data included a timestamp, put it into lastRefresh.
223 | lastRefresh: timestamp,
224 | };
225 | }),
226 | incrementRetry: assign({ retries: (context) => context.retries + 1 }),
227 | refreshDone: assign((context: Context, event) => {
228 | let lastRefresh = Date.now();
229 | let updated = {
230 | lastRefresh,
231 | retries: 0,
232 | reportedError: false,
233 | };
234 |
235 | if(event.data !== UNMODIFIED && context.permitted) {
236 | receive({ data: event.data, timestamp: lastRefresh });
237 | }
238 |
239 | return updated;
240 | }),
241 | reportError: assign((context: Context, event) => {
242 | // Ignore the error if it happened because the browser went offline while fetching.
243 | // Otherwise report it.
244 | if (
245 | !context.reportedError &&
246 | browserStateModule.isOnline()
247 | ) {
248 | receive({ error: event.data, timestamp: Date.now() });
249 | }
250 |
251 | return {
252 | reportedError: true,
253 | };
254 | }),
255 | clearData: () => receive({ data: null, timestamp: Date.now() }),
256 | },
257 | guards: {
258 | not_permitted_to_refresh: (ctx) => !ctx.permitted,
259 | permitted_to_refresh: (ctx) => ctx.permitted,
260 | can_enable: (ctx) => {
261 | if (!ctx.storeEnabled || !ctx.permitted) {
262 | return false;
263 | }
264 |
265 | if (!ctx.lastRefresh) {
266 | // Refresh if we haven't loaded any data yet.
267 | return true;
268 | }
269 |
270 | // Finally, we can enable if the browser tab is active.
271 | return ctx.browserEnabled;
272 | },
273 | },
274 | },
275 | {
276 | lastRefresh: 0,
277 | retries: 0,
278 | reportedError: false,
279 | browserEnabled: true,
280 | storeEnabled: initialEnabled === false ? false : true,
281 | permitted: initialPermitted === false ? false : true,
282 | }
283 | );
284 |
285 | let machine = interpret(machineDef).start();
286 |
287 | if(debug) {
288 | machine.onTransition((state, event) => {
289 | debug({ id, event, state });
290 | });
291 | }
292 |
293 | let browserStateUnsub = browserStateModule.subscribe((state) => {
294 | let enabled = state.focused && state.online && state.visible;
295 | machine.send({ type: 'BROWSER_ENABLED', data: enabled });
296 | });
297 |
298 | async function fetchInitial() {
299 | let data = await initialData(key);
300 | let d = data?.data;
301 | if(d !== null && d !== undefined) {
302 | machine.send('INITIAL_DATA', {data});
303 | }
304 | }
305 |
306 | if(initialData) {
307 | fetchInitial();
308 | }
309 |
310 | return {
311 | /** Enable or disable the fetcher. This is usually linked to whether there is anything that actually cares about this
312 | * data or not. */
313 | setEnabled: (enabled : boolean) => machine.send({ type: 'FETCHER_ENABLED', data: enabled }),
314 | /** Set if fetching is permitted. Fetching might not be permitted if the user is not logged in or lacks
315 | * proper permissions for this endpoint, for example. */
316 | setPermitted: (permitted: boolean) => machine.send({ type: 'SET_PERMITTED', data: permitted }),
317 | /** Force a refresh. This will not do anything if fetching has been disabled via `setPermitted`. */
318 | refresh: () => machine.send('FORCE_REFRESH'),
319 | destroy: () => {
320 | browserStateUnsub();
321 | machine.stop();
322 | },
323 | };
324 | }
325 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | [](https://app.netlify.com/sites/svelte-tailwindcss-storybook/deploys) [](https://app.fossa.io/projects/git%2Bgithub.com%2Fjerriclynsjohn%2Fsvelte-storybook-tailwind?ref=badge_shield)
2 |
3 | # A starter template for Svelte, TailwindCSS and Storybook
4 |
5 | 
6 |
7 |
8 | > Visit this website to see the outcome: [Svelte + TailwindCSS + Storybook](https://svelte-tailwindcss-storybook.netlify.com)
9 |
10 | ```bash
11 | // Quickstart
12 |
13 | npx degit jerriclynsjohn/svelte-storybook-tailwind my-svelte-project
14 | cd my-svelte-project
15 |
16 | yarn
17 | yarn dev
18 | yarn stories
19 | ```
20 |
21 | Svelte and TailwindCSS is an awesome combination for Frontend development, but sometimes the setup seems a bit non intuitive, especially when trying to try out this awesome combination. When integrating Storybook, which is another awesome tool for UI Component development and documentation, there is no obvious place to get how it's done. This repo was made to address just that!
22 |
23 | > You can easily start your project with this template, instead of wasting time figuring out configurations for each integration.
24 |
25 | ## What do you get in this repo
26 |
27 | 
28 |
29 | 1. A fully functional Svelte + TailwindCSS integration with side-by-side implementation of independent Storybook
30 | 2. Storybook with 5 essential Addons
31 | 3. Storybook populated with basic examples of Svelte + TailwindCSS
32 |
33 | ### Addons
34 |
35 | - Accessibility Addon
36 |
37 | 
38 |
39 | - Accessibility Addon - Colorblindness Emulation
40 |
41 | 
42 |
43 | - Actions Addon
44 |
45 | 
46 |
47 | - Notes Addon
48 |
49 | 
50 |
51 | - Source Addon
52 |
53 | 
54 |
55 | - Viewport Addon
56 |
57 | 
58 |
59 | ## Svelte + TailwindCSS + Storybook
60 |
61 | [Storybook](https://storybook.js.org/) is an open source tool for developing JavaScript UI
62 | components in isolation
63 |
64 | [Svelte](https://svelte.dev/) is a component framework that allows you to write highly-efficient,
65 | imperative code, that surgically updates the DOM to maintain performance.
66 |
67 | [TailwindCSS](https://tailwindcss.com) is a highly customizable, low-level CSS framework that gives
68 | you all of the building blocks you need to build bespoke designs without any annoying opinionated
69 | styles you have to fight to override.
70 |
71 | ## Steps to build
72 |
73 | 1. Clone this repo `git clone https://github.com/jerriclynsjohn/svelte-storybook-tailwind.git`
74 | 2. Go to the directory `cd svelte-storybook-tailwind`
75 | 3. Install dependencies `yarn`
76 | 4. To develop your Svelte App: `yarn dev`
77 | 5. To develop UI components independent of your app: `yarn stories`
78 |
79 | ### Documentations
80 |
81 | 1. Svelte - [API](https://svelte.dev/docs) and [Tutorial](https://svelte.dev/tutorial/)
82 | 2. TailwindCSS - [Docs](https://tailwindcss.com/docs) and [Tutorial](https://tailwindcss.com/screencasts/)
83 | 3. Storybook - [Docs](https://storybook.js.org/docs/basics/introduction/) and [Tutorial (No Svelte Yet!)](https://www.learnstorybook.com/)
84 |
85 | ## Steps to build it all by yourself and some tips [Warning: It's lengthy]
86 |
87 | ### Instantiate Svelte App
88 |
89 | - Start the template file using `npx degit sveltejs/template svelte-storybook-tailwind`
90 | - Go to the directory `cd svelte-storybook-tailwind`
91 | - Install dependencies `yarn`
92 | - Try run the svelte app `yarn dev`
93 |
94 | ### Add Tailwind into the project
95 |
96 | - Install dependencies:
97 | `yarn add -D tailwindcss @fullhuman/postcss-purgecss autoprefixer postcss postcss-import svelte-preprocess`
98 | - Change the rollup config as shown:
99 |
100 | ```javascript
101 | import svelte from 'rollup-plugin-svelte';
102 | import resolve from 'rollup-plugin-node-resolve';
103 | import commonjs from 'rollup-plugin-commonjs';
104 | import livereload from 'rollup-plugin-livereload';
105 | import { terser } from 'rollup-plugin-terser';
106 | import postcss from 'rollup-plugin-postcss';
107 | import autoPreprocess from 'svelte-preprocess';
108 |
109 | const production = !process.env.ROLLUP_WATCH;
110 |
111 | export default {
112 | input: 'src/main.js',
113 | output: {
114 | sourcemap: true,
115 | format: 'iife',
116 | name: 'app',
117 | file: 'public/bundle.js',
118 | },
119 | plugins: [
120 | svelte({
121 | preprocess: autoPreprocess({
122 | postcss: true,
123 | }),
124 | // enable run-time checks when not in production
125 | dev: !production,
126 | // we'll extract any component CSS out into
127 | // a separate file — better for performance
128 | css: css => {
129 | css.write('public/bundle.css');
130 | },
131 | }),
132 | postcss({
133 | extract: 'public/utils.css',
134 | }),
135 |
136 | // If you have external dependencies installed from
137 | // npm, you'll most likely need these plugins. In
138 | // some cases you'll need additional configuration —
139 | // consult the documentation for details:
140 | // https://github.com/rollup/rollup-plugin-commonjs
141 | resolve({
142 | browser: true,
143 | dedupe: importee => importee === 'svelte' || importee.startsWith('svelte/'),
144 | }),
145 | commonjs(),
146 |
147 | // Watch the `public` directory and refresh the
148 | // browser on changes when not in production
149 | !production && livereload('public'),
150 |
151 | // If we're building for production (npm run build
152 | // instead of npm run dev), minify
153 | production && terser(),
154 | ],
155 | watch: {
156 | clearScreen: false,
157 | },
158 | };
159 | ```
160 |
161 | - Add tailwind config using the command `npx tailwind init`
162 |
163 | - Add PostCSS config `./postcss.config.js` as follows:
164 |
165 | ```javascript
166 | const production = !process.env.ROLLUP_WATCH;
167 | const purgecss = require('@fullhuman/postcss-purgecss');
168 |
169 | module.exports = {
170 | plugins: [
171 | require('postcss-import')(),
172 | require('tailwindcss'),
173 | require('autoprefixer'),
174 | production &&
175 | purgecss({
176 | content: ['./**/*.html', './**/*.svelte'],
177 | defaultExtractor: content => {
178 | const regExp = new RegExp(/[A-Za-z0-9-_:/]+/g);
179 |
180 | const matchedTokens = [];
181 |
182 | let match = regExp.exec(content);
183 | // To make sure that you do not lose any tailwind classes used in class directive.
184 | // https://github.com/tailwindcss/discuss/issues/254#issuecomment-517918397
185 | while (match) {
186 | if (match[0].startsWith('class:')) {
187 | matchedTokens.push(match[0].substring(6));
188 | } else {
189 | matchedTokens.push(match[0]);
190 | }
191 |
192 | match = regExp.exec(content);
193 | }
194 |
195 | return matchedTokens;
196 | },
197 | }),
198 | ],
199 | };
200 | ```
201 |
202 | - Build the project with some TailwindCSS utilities `yarn dev`
203 |
204 | ### Add Storybook into the Svelte Project
205 |
206 | - Add Storybook dependencies `yarn add -D @storybook/svelte`
207 | - Add 5 commonly used Storybook [Addons](https://storybook.js.org/addons/):
208 |
209 | - [Source](https://github.com/storybookjs/storybook/tree/master/addons/storysource):
210 | `yarn add -D @storybook/addon-storysource`
211 | - [Actions](https://github.com/storybookjs/storybook/tree/master/addons/actions):
212 | `yarn add -D @storybook/addon-actions`
213 | - [Notes](https://github.com/storybookjs/storybook/tree/master/addons/notes):
214 | `yarn add -D @storybook/addon-notes`
215 | - [Viewport](https://github.com/storybookjs/storybook/tree/master/addons/viewport):
216 | `yarn add -D @storybook/addon-viewport`
217 | - [Accessibility](https://github.com/storybookjs/storybook/tree/master/addons/a11y):
218 | `yarn add @storybook/addon-a11y --dev`
219 | - Create an addon file at the root `.storybook/addons.js` with the following content and keep
220 | adding additional addons in this file.
221 |
222 | ```javascript
223 | import '@storybook/addon-storysource/register';
224 | import '@storybook/addon-actions/register';
225 | import '@storybook/addon-notes/register';
226 | import '@storybook/addon-viewport/register';
227 | import '@storybook/addon-a11y/register';
228 | ```
229 |
230 | - Create a config file at the root `.storybook/config.js` with the following content:
231 |
232 | ```javascript
233 | import { configure, addParameters, addDecorator } from '@storybook/svelte';
234 | import { withA11y } from '@storybook/addon-a11y';
235 |
236 | // automatically import all files ending in *.stories.js
237 | const req = require.context('../storybook/stories', true, /\.stories\.js$/);
238 | function loadStories() {
239 | req.keys().forEach(filename => req(filename));
240 | }
241 |
242 | configure(loadStories, module);
243 | addDecorator(withA11y);
244 | addParameters({ viewport: { viewports: newViewports } });
245 | ```
246 |
247 | - Add tailwind configs in the `webpack.config.js` under `.storybook` and also accommodate for Source addon:
248 |
249 | ```javascript
250 | const path = require('path');
251 |
252 | module.exports = ({ config, mode }) => {
253 | config.module.rules.push(
254 | {
255 | test: /\.css$/,
256 | loaders: [
257 | {
258 | loader: 'postcss-loader',
259 | options: {
260 | sourceMap: true,
261 | config: {
262 | path: './.storybook/',
263 | },
264 | },
265 | },
266 | ],
267 |
268 | include: path.resolve(__dirname, '../storybook/'),
269 | },
270 | //This is the new block for the addon
271 | {
272 | test: /\.stories\.js?$/,
273 | loaders: [require.resolve('@storybook/addon-storysource/loader')],
274 | include: [path.resolve(__dirname, '../storybook')],
275 | enforce: 'pre',
276 | },
277 | );
278 |
279 | return config;
280 | };
281 | ```
282 |
283 | - Create the `postcss.config.js` under `.storybook`:
284 |
285 | ```javascript
286 | var tailwindcss = require('tailwindcss');
287 |
288 | module.exports = {
289 | plugins: [
290 | require('postcss-import')(),
291 | tailwindcss('./tailwind.config.js'),
292 | require('autoprefixer'),
293 | ],
294 | };
295 | ```
296 |
297 | - Make sure you have babel and svelte-loader dependencies
298 | `yarn add -D babel-loader @babel/core svelte-loader`
299 | - Add npm script in your `package.json`
300 |
301 | ```bash
302 | {
303 | "scripts": {
304 | // Rest of the scripts
305 | "stories": "start-storybook",
306 | "build-stories": "build-storybook"
307 | }
308 | }
309 | ```
310 |
311 | - Add a utils.css file under `storybook/css/` and make sure you `import 'utils.css'` in your
312 | `stories.js` files:
313 |
314 | ```css
315 | /* Import Tailwind as Global Utils */
316 |
317 | @import 'tailwindcss/base';
318 |
319 | @import 'tailwindcss/components';
320 |
321 | @import 'tailwindcss/utilities';
322 | ```
323 |
324 | - Write your Svelte component in `storybook\components` and yes you can use your regular `.svelte`
325 | file. The only thing is that you cant use templates in a story yet, not supported, but yes you
326 | can compose other components together. For the starter pack lets just create a clickable button.
327 |
328 | ```html
329 |
337 |
338 |
340 | {text}
341 |
342 | ```
343 |
344 | - Write your stories in `storybook/stories` and you can name any number of story file with
345 | `.stories.js`, for the starter package we can create stories of `Button` with the
346 | readme notes at `.stories.md`. Note: reference the css here to make sure that tailwind
347 | is called by postcss:
348 |
349 | ```javascript
350 | import '../../css/utils.css';
351 |
352 | import { storiesOf } from '@storybook/svelte';
353 | import ButtonSimple from '../../components/buttons/button-simple.svelte';
354 | import markdownNotes from './buttons.stories.md';
355 |
356 | storiesOf('Buttons | Buttons', module)
357 | //Simple Button
358 | .add(
359 | 'Simple',
360 | () => ({
361 | Component: ButtonSimple,
362 | props: { text: 'Button' },
363 | on: {
364 | click: action('I am logging in the actions tab too'),
365 | },
366 | }),
367 | { notes: { markdown: markdownNotes } },
368 | )
369 | ```
370 |
371 | - Write your own Documentation for the Component which will `.stories.md` :
372 |
373 | ```md
374 | # Buttons
375 |
376 | _Examples of building buttons with Tailwind CSS._
377 |
378 | ---
379 |
380 | Tailwind doesn't include pre-designed button styles out of the box, but they're easy to build using
381 | existing utilities.
382 |
383 | Here are a few examples to help you get an idea of how to build components like this using Tailwind.
384 | ```
385 |
386 | - Run your storyboard `yarn stories` and you'll see this:
387 |
388 | 
389 |
390 | You can add more addons and play around with them.
391 |
392 | That's a wrap!
393 |
394 |
395 | ## License
396 | [](https://app.fossa.io/projects/git%2Bgithub.com%2Fjerriclynsjohn%2Fsvelte-storybook-tailwind?ref=badge_large)
397 |
--------------------------------------------------------------------------------