├── .github
├── dependabot.yml
└── workflows
│ ├── ci.js.yml
│ ├── codeql-analysis.yml
│ └── npm-publish.yml
├── .gitignore
├── LICENSE
├── README.md
├── logo.png
├── package-lock.json
├── package.json
├── src
├── context.ts
├── index.ts
├── registry.ts
├── service.ts
└── utils.ts
├── tests
└── src
│ ├── context.test.ts
│ ├── registry.test.ts
│ └── service.test.ts
├── tsconfig.json
└── vite.config.ts
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "npm" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "daily"
12 | allow:
13 | - dependency-type: "production"
14 |
--------------------------------------------------------------------------------
/.github/workflows/ci.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: CI
5 |
6 | on:
7 | push:
8 | branches: [ main ]
9 | pull_request:
10 | branches: [ main ]
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - uses: actions/checkout@v2
18 | - uses: actions/setup-node@v2
19 | with:
20 | node-version: 18
21 | check-latest: true
22 | cache: 'npm'
23 | - run: npm ci
24 | - run: npm run build --if-present
25 | - run: npm test -- --run
26 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ main ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ main ]
20 | schedule:
21 | - cron: '21 9 * * 1'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'javascript' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v2
42 |
43 | # Initializes the CodeQL tools for scanning.
44 | - name: Initialize CodeQL
45 | uses: github/codeql-action/init@v1
46 | with:
47 | languages: ${{ matrix.language }}
48 | # If you wish to specify custom queries, you can do so here or in a config file.
49 | # By default, queries listed here will override any specified in a config file.
50 | # Prefix the list here with "+" to use these queries and those in the config file.
51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
52 |
53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
54 | # If this step fails, then you should remove it and run the build manually (see below)
55 | - name: Autobuild
56 | uses: github/codeql-action/autobuild@v1
57 |
58 | # ℹ️ Command-line programs to run using the OS shell.
59 | # 📚 https://git.io/JvXDl
60 |
61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
62 | # and modify them (or add more) to build your code if your project
63 | # uses a compiled language
64 |
65 | #- run: |
66 | # make bootstrap
67 | # make release
68 |
69 | - name: Perform CodeQL Analysis
70 | uses: github/codeql-action/analyze@v1
71 |
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
3 |
4 | name: Node.js Package
5 |
6 | on:
7 | release:
8 | types: [created]
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v2
15 | - uses: actions/setup-node@v2
16 | with:
17 | node-version: 18
18 | check-latest: true
19 | cache: 'npm'
20 | - run: npm ci
21 | - run: npm test
22 |
23 | publish-npm:
24 | needs: build
25 | runs-on: ubuntu-latest
26 | steps:
27 | - uses: actions/checkout@v2
28 | - uses: actions/setup-node@v2
29 | with:
30 | node-version: 18
31 | check-latest: true
32 | cache: 'npm'
33 | registry-url: https://registry.npmjs.org/
34 | - run: npm ci
35 | - run: npm publish
36 | env:
37 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
6 | coverage
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Maciej Kwaśniak
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Solid Services
6 |
7 | Solid Services is a package that provides a way to manage shared state and persistent connections in Solid.js applications. It consists of a `ServiceRegistry` component, which creates a context around your components and allows you to scope the services to specific parts of the application, and the `useService` hook, which registers a service or returns an existing object if it has already been used in the past.
8 |
9 | ## Installation
10 |
11 | ```sh
12 | npm install solid-services
13 | ```
14 |
15 | ## Compatibility
16 |
17 | - Solid.js ^1.0
18 |
19 | ## Demo
20 |
21 | [Open on CodeSandbox](https://codesandbox.io/s/solid-services-uqlnw)
22 |
23 | ## Services
24 |
25 | Service is an object that provides a particular function or set of functions. Services are designed to be "global" objects that can be accessed and used throughout an application, and are often used for features that require shared state or persistent connections.
26 |
27 | Some common examples of services include user authentication, geolocation, WebSockets, server-sent events or notifications, server-backed API call libraries, third-party APIs, and logging. Services can be implemented as either a class or a plain object (POJO) and are usually defined as a function that returns an instance of the class or object. For example:
28 |
29 | ```js
30 | import { createSignal } from 'solid-js';
31 |
32 | export function AuthService() {
33 | const [getUser, setUser] = createSignal();
34 |
35 | return {
36 | get user() {
37 | return getUser();
38 | },
39 |
40 | login(user) {
41 | setUser(user);
42 | },
43 |
44 | logout() {
45 | setUser(undefined);
46 | },
47 | };
48 | }
49 | ```
50 |
51 | You can also define a service using a class:
52 |
53 | ```js
54 | import { Service } from 'solid-services';
55 |
56 | export class AuthService extends Service {
57 | @signal #user; // Decorators not included in solid-services
58 |
59 | get user() {
60 | return this.#user;
61 | }
62 |
63 | @action
64 | login(user) {
65 | this.#user = user;
66 | }
67 |
68 | @action
69 | logout() {
70 | this.#user = undefined;
71 | }
72 | }
73 | ```
74 |
75 | To access a service in your components or other services, you can use the `useService` hook. This hook registers the service or returns an existing object if it has already been used in the past. For example:
76 |
77 | ```jsx
78 | import { useService } from "solid-services";
79 | import AuthService from "./services/auth";
80 |
81 | export default function LogoutComponent() {
82 | const getAuthService = useService(AuthService);
83 |
84 | function logout() {
85 | getAuthService().logout();
86 | }
87 |
88 | return ;
89 | }
90 | ```
91 |
92 | ### How does services differ from using just a context?
93 |
94 | Solid Services is built on top of the Solid.js context API, but it provides the opposite of how context works. While context gives you access to the closest data provider, Solid Services gives you access to the furthest available provider. This means you opt-in to use a "global" shared service, instead of nearest encapsulated provider, while maintaining full access control of downstream data.
95 |
96 | ## Service Registry
97 |
98 | The `ServiceRegistry` is a component that creates a context around the components within an application, allowing developers to scope the services to specific parts of the application.
99 |
100 | To use the `ServiceRegistry`, you can simply wrap the components that you want to be within the context of the registry in a element. For example:
101 |
102 | ```jsx
103 | import { ServiceRegistry } from 'solid-services';
104 |
105 | export default function App() {
106 | return (
107 |
108 | {/* All components within this element will have access to the services defined in this registry */}
109 |
110 |
111 |
112 | );
113 | }
114 | ```
115 |
116 | > ## **Remember!**
117 | >
118 | > It is important to wrap your application with a top-level `` before using services in components. Otherwise, services won't be able to register and their usage will throw an error.
119 |
120 | By default, the ServiceRegistry does not expose any services to sub-registries. This means that the components within a sub-registry will not have access to the services defined in the parent registry. However, you can configure this behavior using the `expose` property of the ServiceRegistry.
121 |
122 | For example, to expose a specific service to a sub-registry, you can set the `expose` property to an array containing the service(s) that you want to expose:
123 |
124 | ```jsx
125 | import { ServiceRegistry } from 'solid-services';
126 | import AuthService from './services/auth';
127 |
128 | export default function App() {
129 | return (
130 |
131 | {/* All components within this element will have access
132 | to all the services defined in this registry */}
133 |
134 |
135 | {/* All components within this element will have access
136 | to the AuthService from the parent registry,
137 | as well as any services defined in this registry */}
138 |
139 |
140 | );
141 | }
142 | ```
143 |
144 | You can also set the `expose` property to `true` to expose all services to sub-registries. This can be useful if you want to granularly control the availability of services in different parts of your application.
145 |
146 | ```jsx
147 | import { ServiceRegistry } from 'solid-services';
148 |
149 | export default function App() {
150 | return (
151 |
152 | {/* All components within this element will have access to the services defined in this registry */}
153 |
154 |
155 | {/* All components within this element will have access to all services from the parent registry,
156 | as well as any services defined in this registry */}
157 |
158 |
159 | );
160 | }
161 | ```
162 |
163 | By using the ServiceRegistry and the expose property, you can control which services are available to different parts of your application, and manage the shared state and persistent connections within your Solid.js application.
164 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Exelord/solid-services/7da46da5122f45037f0a72138e6dfec9c3215e08/logo.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "solid-services",
3 | "description": "Solid.js library adding a services layer for global shared state.",
4 | "version": "4.3.2",
5 | "license": "MIT",
6 | "homepage": "https://github.com/Exelord/solid-services",
7 | "repository": "github:exelord/solid-services",
8 | "info": "Services are global objects useful for features that require shared state or persistent connections. They are lazy evaluated, only when used, solving an issue of cross dependencies and contexts tree.",
9 | "keywords": [
10 | "solidhack",
11 | "best_ecosystem",
12 | "solidjs"
13 | ],
14 | "contributors": [
15 | {
16 | "name": "Maciej Kwaśniak",
17 | "email": "contact@exelord.com",
18 | "url": "https://exelord.com"
19 | }
20 | ],
21 | "files": [
22 | "dist"
23 | ],
24 | "main": "./dist/solid-services.cjs",
25 | "module": "./dist/solid-services.js",
26 | "types": "./dist/types/index.d.ts",
27 | "source": "./dist/src/index.js",
28 | "sideEffects": false,
29 | "type": "module",
30 | "exports": {
31 | ".": {
32 | "types": "./dist/types/index.d.ts",
33 | "import": "./dist/solid-services.js",
34 | "require": "./dist/solid-services.cjs"
35 | }
36 | },
37 | "scripts": {
38 | "test": "vitest run",
39 | "test:watch": "vitest",
40 | "coverage": "vitest run --coverage",
41 | "prepublishOnly": "npm run build",
42 | "dev": "npm-run-all --parallel 'build:** -- --watch'",
43 | "build": "npm-run-all --parallel build:** && npm run types",
44 | "build:js": "vite build",
45 | "build:source": "tsc",
46 | "types": "tsc --emitDeclarationOnly --declaration --outDir ./dist/types",
47 | "release": "release-it"
48 | },
49 | "release-it": {
50 | "git": {
51 | "commitMessage": "v${version}",
52 | "tagAnnotation": "v${version}"
53 | },
54 | "npm": {
55 | "publish": false
56 | },
57 | "github": {
58 | "release": true,
59 | "releaseName": "v${version}"
60 | },
61 | "hooks": {
62 | "before:init": [
63 | "npm test -- --run"
64 | ]
65 | }
66 | },
67 | "devDependencies": {
68 | "@vitest/ui": "^2.0.5",
69 | "c8": "^10.1.2",
70 | "npm-run-all": "^4.1.5",
71 | "release-it": "^17.6.0",
72 | "solid-js": "^1.8.22",
73 | "typescript": "^5.5.4",
74 | "vite": "^5.4.3",
75 | "vitest": "^2.0.5"
76 | },
77 | "peerDependencies": {
78 | "solid-js": "^1.0.0"
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/context.ts:
--------------------------------------------------------------------------------
1 | import { createRegistry, ServiceInitializer } from "./registry";
2 | import type { Registry } from "./registry";
3 |
4 | import {
5 | createContext,
6 | createComponent,
7 | useContext,
8 | FlowComponent,
9 | } from "solid-js";
10 |
11 | export type RegistryProviderProps = {
12 | expose?: ServiceInitializer[] | boolean;
13 | };
14 |
15 | export const ServiceRegistryContext = createContext();
16 |
17 | export const ServiceRegistry: FlowComponent = (props) => {
18 | let registry: Registry | undefined;
19 |
20 | return createComponent(ServiceRegistryContext.Provider, {
21 | get value() {
22 | return (registry ??= createRegistry({
23 | get expose() {
24 | return props.expose;
25 | },
26 | }));
27 | },
28 |
29 | get children() {
30 | return props.children;
31 | },
32 | });
33 | };
34 |
35 | /**
36 | * Returns the current registry.
37 | *
38 | * If no registry is found, it will throw an error.
39 | */
40 | export function useRegistry(): Registry {
41 | const registry = useContext(ServiceRegistryContext);
42 |
43 | if (!registry) {
44 | throw new Error(
45 | "Your app needs to be wrapped with context in order to use services."
46 | );
47 | }
48 |
49 | return registry;
50 | }
51 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./registry";
2 | export * from "./context";
3 | export * from "./service";
4 |
--------------------------------------------------------------------------------
/src/registry.ts:
--------------------------------------------------------------------------------
1 | import { Owner, getOwner, useContext } from "solid-js";
2 | import { ServiceRegistryContext } from "./context";
3 | import { runInSubRoot } from "./utils";
4 |
5 | export class Service {}
6 |
7 | type ServiceFunction = () => T;
8 | type ServiceConstructor = new () => T;
9 |
10 | export type ServiceInitializer =
11 | | ServiceFunction
12 | | ServiceConstructor;
13 |
14 | export interface RegistryConfig {
15 | expose?: ServiceInitializer[] | boolean;
16 | }
17 |
18 | /**
19 | * Registry is a container for services.
20 | * It is used to register and retrieve services.
21 | */
22 | export class Registry {
23 | #owner: Owner | null;
24 | #config: RegistryConfig;
25 | #cache: Map, any>;
26 |
27 | constructor(config: RegistryConfig = {}) {
28 | this.#config = config;
29 | this.#owner = getOwner();
30 | this.#cache = new Map, any>();
31 | }
32 |
33 | /**
34 | * Checks weather the registry has a service registered for the given initializer.
35 | *
36 | * First it will check in the parent registry and if no service is registered there,
37 | * it will check in the current registry.
38 | */
39 | has(initializer: ServiceInitializer): boolean {
40 | const parentRegistry = this.getParentRegistry();
41 |
42 | if (parentRegistry?.isExposing(initializer)) {
43 | return parentRegistry.has(initializer);
44 | }
45 |
46 | return this.#cache.has(initializer);
47 | }
48 |
49 | /**
50 | * Returns the service registered for the given initializer.
51 | *
52 | * First it will try to find it in the parent registry and if no service is registered there,
53 | * it will check in the current registry.
54 | */
55 | get(initializer: ServiceInitializer): T | undefined {
56 | const parentRegistry = this.getParentRegistry();
57 |
58 | if (parentRegistry?.isExposing(initializer)) {
59 | return parentRegistry.get(initializer);
60 | }
61 |
62 | return this.#cache.get(initializer);
63 | }
64 |
65 | /**
66 | * Clears the registry.
67 | */
68 | clear(): void {
69 | this.#cache.clear();
70 | }
71 |
72 | /**
73 | * Deletes registered service for the given initializer.
74 | */
75 | delete(initializer: ServiceInitializer): void {
76 | this.#cache.delete(initializer);
77 | }
78 |
79 | /**
80 | * Registers a service for the given initializer.
81 | *
82 | * If the registry has a parent registry, it will check if the service is exposed there.
83 | *
84 | * If it is exposed, it will register the service in the parent registry.
85 | *
86 | * If it is not exposed, it will register the service in the current registry.
87 | *
88 | * If the registry does not have a parent registry, it will register the service in the current registry.
89 | */
90 | register(initializer: ServiceInitializer): T {
91 | const parentRegistry = this.getParentRegistry();
92 |
93 | if (parentRegistry?.isExposing(initializer)) {
94 | return parentRegistry.register(initializer);
95 | }
96 |
97 | const registration = runInSubRoot(
98 | () => this.initializeService(initializer),
99 | this.#owner
100 | );
101 |
102 | this.#cache.set(initializer, registration);
103 |
104 | return registration;
105 | }
106 |
107 | protected isExposing(
108 | initializer: ServiceInitializer
109 | ): boolean {
110 | return (
111 | this.#config.expose === true ||
112 | (Array.isArray(this.#config.expose) &&
113 | this.#config.expose?.includes(initializer))
114 | );
115 | }
116 |
117 | private getParentRegistry(): Registry | undefined {
118 | return this.#owner?.owner
119 | ? runInSubRoot((dispose) => {
120 | const context = useContext(ServiceRegistryContext);
121 | dispose();
122 | return context;
123 | }, this.#owner.owner)
124 | : undefined;
125 | }
126 |
127 | private initializeService(
128 | initializer: ServiceInitializer
129 | ): T {
130 | return initializer.prototype?.constructor
131 | ? Reflect.construct(initializer, [])
132 | : (initializer as ServiceFunction)();
133 | }
134 | }
135 |
136 | /**
137 | * Creates a new registry for services.
138 | */
139 | export function createRegistry(config?: RegistryConfig): Registry {
140 | return new Registry(config);
141 | }
142 |
--------------------------------------------------------------------------------
/src/service.ts:
--------------------------------------------------------------------------------
1 | import { useRegistry } from "./context";
2 | import type { ServiceInitializer, Service } from "./registry";
3 |
4 | export type ServiceGetter = () => T;
5 |
6 | /**
7 | * Returns a getter that returns the service registered for the given initializer.
8 | *
9 | * If no service is registered, it will first register the service using the given initializer.
10 | */
11 | export function useService(
12 | initializer: ServiceInitializer
13 | ): ServiceGetter {
14 | const registry = useRegistry();
15 | return () => registry.get(initializer) || registry.register(initializer);
16 | }
17 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { createRoot, Owner, runWithOwner, onCleanup, getOwner } from "solid-js";
2 |
3 | function createSubRoot(
4 | fn: Parameters>[0],
5 | owner: typeof Owner = getOwner()
6 | ): T {
7 | return createRoot((dispose) => {
8 | owner && runWithOwner(owner, onCleanup.bind(void 0, dispose));
9 | return fn(dispose);
10 | }, owner!);
11 | }
12 |
13 | export function runInSubRoot(
14 | fn: Parameters>[0],
15 | owner?: typeof Owner
16 | ): T {
17 | let error: unknown;
18 | let hasError = false;
19 |
20 | const result = createSubRoot((dispose) => {
21 | try {
22 | return fn(dispose);
23 | } catch (e) {
24 | hasError = true;
25 | error = e;
26 | dispose();
27 | throw e;
28 | }
29 | }, owner);
30 |
31 | if (hasError) {
32 | throw error;
33 | }
34 |
35 | return result;
36 | }
37 |
--------------------------------------------------------------------------------
/tests/src/context.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from "vitest";
2 | import { createComponent } from "solid-js";
3 | import { ServiceRegistry, useRegistry } from "../../src/context";
4 |
5 | describe("ServiceRegistry", () => {
6 | test("allows to expose selected services", () => {
7 | const GlobalService = () => {
8 | return { service: "global" };
9 | };
10 |
11 | const DelegatedService = () => {
12 | return { service: "global" };
13 | };
14 |
15 | const LocalService = () => {
16 | return { service: "local" };
17 | };
18 |
19 | createComponent(ServiceRegistry, {
20 | get expose() {
21 | return [DelegatedService];
22 | },
23 |
24 | get children() {
25 | return [
26 | createComponent(() => {
27 | const registry = useRegistry();
28 | registry.register(GlobalService);
29 | registry.register(DelegatedService);
30 | return undefined;
31 | }, {}),
32 | createComponent(ServiceRegistry, {
33 | get expose() {
34 | return [DelegatedService];
35 | },
36 |
37 | get children() {
38 | return [
39 | createComponent(() => {
40 | const registry = useRegistry();
41 | expect(registry.has(DelegatedService)).toBe(true);
42 | expect(registry.has(GlobalService)).toBe(false);
43 | return undefined;
44 | }, {}),
45 | createComponent(ServiceRegistry, {
46 | get children() {
47 | return createComponent(() => {
48 | const registry = useRegistry();
49 | registry.register(LocalService);
50 |
51 | expect(registry.has(LocalService)).toBe(true);
52 | expect(registry.has(DelegatedService)).toBe(true);
53 | expect(registry.has(GlobalService)).toBe(false);
54 |
55 | return undefined;
56 | }, {});
57 | },
58 | }),
59 | ];
60 | },
61 | }),
62 | ];
63 | },
64 | });
65 | });
66 |
67 | test("allows to expose all services", () => {
68 | const GlobalService = () => {
69 | return { service: "global" };
70 | };
71 |
72 | const DelegatedService = () => {
73 | return { service: "global" };
74 | };
75 |
76 | const LocalService = () => {
77 | return { service: "local" };
78 | };
79 |
80 | createComponent(ServiceRegistry, {
81 | get expose() {
82 | return true;
83 | },
84 |
85 | get children() {
86 | return [
87 | createComponent(() => {
88 | const registry = useRegistry();
89 | registry.register(GlobalService);
90 | registry.register(DelegatedService);
91 | return undefined;
92 | }, {}),
93 | createComponent(ServiceRegistry, {
94 | get expose() {
95 | return [DelegatedService];
96 | },
97 |
98 | get children() {
99 | return [
100 | createComponent(() => {
101 | const registry = useRegistry();
102 | expect(registry.has(DelegatedService)).toBe(true);
103 | expect(registry.has(GlobalService)).toBe(true);
104 | return undefined;
105 | }, {}),
106 | createComponent(ServiceRegistry, {
107 | get children() {
108 | return createComponent(() => {
109 | const registry = useRegistry();
110 | registry.register(LocalService);
111 |
112 | expect(registry.has(LocalService)).toBe(true);
113 | expect(registry.has(DelegatedService)).toBe(true);
114 | expect(registry.has(GlobalService)).toBe(false);
115 |
116 | return undefined;
117 | }, {});
118 | },
119 | }),
120 | ];
121 | },
122 | }),
123 | ];
124 | },
125 | });
126 | });
127 |
128 | test("throws error when used without the registry provider", () => {
129 | const MyComponent = () => {
130 | expect(() => useRegistry()).toThrowError(
131 | "Your app needs to be wrapped with context in order to use services."
132 | );
133 |
134 | return undefined;
135 | };
136 |
137 | createComponent(MyComponent, {});
138 | });
139 | });
140 |
--------------------------------------------------------------------------------
/tests/src/registry.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect, vi } from "vitest";
2 | import { createRegistry } from "../../src/registry";
3 |
4 | describe("createRegistry", () => {
5 | test("registers a service", () => {
6 | const MyService = () => {
7 | return { my: "service" };
8 | };
9 |
10 | const registry = createRegistry();
11 | const myService = registry.register(MyService);
12 |
13 | expect(myService).toMatchObject({ my: "service" });
14 | });
15 |
16 | test("registers a class service", () => {
17 | class AuthService {
18 | loggedIn = true;
19 | }
20 |
21 | const registry = createRegistry();
22 | const myService = registry.register(AuthService);
23 |
24 | expect(myService).toBeInstanceOf(AuthService);
25 | expect(myService.loggedIn).toBe(true);
26 | });
27 |
28 | test("gets already registered service", () => {
29 | const spy = vi.fn();
30 |
31 | const MyService = () => {
32 | spy();
33 | return { my: "service" };
34 | };
35 |
36 | const registry = createRegistry();
37 | registry.register(MyService);
38 |
39 | const myService = registry.get(MyService);
40 |
41 | expect(myService).toMatchObject({ my: "service" });
42 | expect(spy).toBeCalledTimes(1);
43 | });
44 |
45 | test("deletes a registered service", () => {
46 | const MyService = () => {
47 | return { my: "service" };
48 | };
49 |
50 | const registry = createRegistry();
51 |
52 | registry.register(MyService);
53 |
54 | expect(registry.has(MyService)).toBe(true);
55 |
56 | registry.delete(MyService);
57 |
58 | expect(registry.has(MyService)).toBe(false);
59 | });
60 |
61 | test("check if service is already registered", () => {
62 | const MyService = () => {
63 | return { my: "service" };
64 | };
65 |
66 | const registry = createRegistry();
67 |
68 | expect(registry.has(MyService)).toBe(false);
69 | registry.register(MyService);
70 | expect(registry.has(MyService)).toBe(true);
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/tests/src/service.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect, vi } from "vitest";
2 | import { createComponent } from "solid-js";
3 | import { useService } from "../../src/service";
4 | import { ServiceRegistry } from "../../src/context";
5 |
6 | describe("useService", () => {
7 | test("registers a service if it does not exist", () => {
8 | const spy = vi.fn();
9 |
10 | const MyService = () => {
11 | spy();
12 | return { service: "my" };
13 | };
14 |
15 | const MyComponent = () => {
16 | const myService = useService(MyService);
17 |
18 | expect(myService()).toMatchObject({ service: "my" });
19 | expect(spy).toBeCalledTimes(1);
20 |
21 | const myServiceAgain = useService(MyService);
22 |
23 | expect(myServiceAgain()).toBe(myService());
24 | expect(spy).toBeCalledTimes(1);
25 |
26 | return undefined;
27 | };
28 |
29 | createComponent(ServiceRegistry, {
30 | get children() {
31 | return createComponent(MyComponent, {});
32 | },
33 | });
34 | });
35 |
36 | test("works with nested services", () => {
37 | const spy = vi.fn();
38 |
39 | function Service2() {
40 | return {
41 | get value() {
42 | return "Hello";
43 | },
44 | };
45 | }
46 |
47 | function Service1() {
48 | const service2 = useService(Service2);
49 |
50 | return {
51 | get value() {
52 | return service2().value;
53 | },
54 | };
55 | }
56 |
57 | function MyComponent() {
58 | const service = useService(Service1);
59 | spy(service().value);
60 | return undefined;
61 | }
62 |
63 | createComponent(ServiceRegistry, {
64 | get children() {
65 | return createComponent(MyComponent, {});
66 | },
67 | });
68 |
69 | expect(spy).toBeCalledWith("Hello");
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "useDefineForClassFields": true,
6 | "lib": ["ESNext", "DOM"],
7 | "moduleResolution": "Node",
8 | "outDir": "./dist/src",
9 | "strict": true,
10 | "alwaysStrict": true,
11 | "resolveJsonModule": true,
12 | "esModuleInterop": true,
13 | "noUnusedLocals": true,
14 | "noUnusedParameters": true,
15 | "noImplicitReturns": true,
16 | "isolatedModules": true,
17 | "noImplicitAny": true,
18 | "strictFunctionTypes": true,
19 | "strictPropertyInitialization": true,
20 | "noImplicitThis": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "noUncheckedIndexedAccess": true,
23 | "strictNullChecks": true,
24 | "inlineSourceMap": true,
25 | "inlineSources": true,
26 | "forceConsistentCasingInFileNames": true,
27 | "baseUrl": ".",
28 | },
29 | "include": ["./src"]
30 | }
31 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import { defineConfig } from "vite";
3 |
4 | export default defineConfig({
5 | build: {
6 | target: "esnext",
7 | minify: false,
8 | lib: {
9 | entry: path.resolve(__dirname, "src/index.ts"),
10 | formats: ["cjs", "es"],
11 | },
12 | rollupOptions: {
13 | external: ["solid-js"],
14 | },
15 | },
16 | resolve: {
17 | conditions: ["browser"],
18 | },
19 | });
20 |
--------------------------------------------------------------------------------