├── test ├── error.svelte ├── template │ ├── 404.svelte │ ├── error.svelte │ └── base.svelte ├── view │ ├── page1.svelte │ ├── class-constructor.svelte │ ├── factory-constructor.svelte │ ├── param.svelte │ ├── user-view.svelte │ └── layout-prop-view.svelte ├── application │ ├── foo.ts │ ├── FrameworkMeta.ts │ ├── LayoutPropController.ts │ ├── FactoryController.ts │ ├── test.app.ts │ └── BasicController.ts ├── dom.ts └── framework.spec.ts ├── .vscode ├── settings.json └── launch.json ├── src ├── helpers │ ├── history.ts │ ├── promise-any.ts │ ├── generate_id.ts │ ├── spa.ts │ └── url-join.ts ├── container │ ├── builder │ │ ├── IConstructor.ts │ │ ├── IContainer.ts │ │ ├── design.ts │ │ └── Container.ts │ ├── config │ │ ├── IContainerBuilder.ts │ │ ├── ProviderFromValue.ts │ │ ├── ProviderFromConstructor.ts │ │ └── ContainerBuilder.ts │ ├── scope.ts │ └── Provider.ts ├── types │ ├── IModuleConfig.ts │ ├── ViewDetails.ts │ ├── RouteDetails.ts │ ├── ModuleInitOptions.ts │ ├── ControllerOptions.ts │ └── constants.ts ├── stores │ ├── history.ts │ ├── main.ts │ └── storeupdator.ts ├── provider │ ├── providerName.ts │ ├── defaultProviders.ts │ └── check.ts ├── decorators │ ├── index.ts │ ├── allow.ts │ ├── layout.ts │ ├── Injectable.ts │ ├── layout-props.ts │ ├── View.ts │ ├── InitModule.ts │ ├── Controller.ts │ ├── inject.ts │ └── Module.ts ├── framework │ ├── tock.ts │ ├── CallInjectedController.ts │ ├── InitModule.ts │ └── CallInjectedView.ts ├── slick-for-svelte-factory.ts └── application.ts ├── assets └── logo.psd ├── jest.config.js ├── .npmignore ├── .gitignore ├── package.json ├── tsconfig.json └── README.md /test/error.svelte: -------------------------------------------------------------------------------- 1 |

Error

-------------------------------------------------------------------------------- /test/template/404.svelte: -------------------------------------------------------------------------------- 1 |

Error 404

-------------------------------------------------------------------------------- /test/view/page1.svelte: -------------------------------------------------------------------------------- 1 |
2 | Hello World 3 |
-------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true 3 | } -------------------------------------------------------------------------------- /src/helpers/history.ts: -------------------------------------------------------------------------------- 1 | export const HistoryContext = Symbol.for("history"); -------------------------------------------------------------------------------- /assets/logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shavyg2/slick-for-svelte/HEAD/assets/logo.psd -------------------------------------------------------------------------------- /src/container/builder/IConstructor.ts: -------------------------------------------------------------------------------- 1 | export interface IConstructor { 2 | new(...args: any[]): T; 3 | } 4 | -------------------------------------------------------------------------------- /test/view/class-constructor.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | {name} 7 |
-------------------------------------------------------------------------------- /test/view/factory-constructor.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 |
{message}
-------------------------------------------------------------------------------- /src/container/config/IContainerBuilder.ts: -------------------------------------------------------------------------------- 1 | export interface IContainerBuilder { 2 | add(identifier: any): this; 3 | } 4 | -------------------------------------------------------------------------------- /src/container/builder/IContainer.ts: -------------------------------------------------------------------------------- 1 | export interface IContainer { 2 | get(identifier: any): T | PromiseLike; 3 | } 4 | -------------------------------------------------------------------------------- /src/types/IModuleConfig.ts: -------------------------------------------------------------------------------- 1 | export interface IModuleConfig { 2 | import?: any[]; 3 | provider?: any[]; 4 | controllers?: any[]; 5 | } 6 | -------------------------------------------------------------------------------- /test/application/foo.ts: -------------------------------------------------------------------------------- 1 | export const foo = "foo-service"; 2 | export const FooProvider = { 3 | provide: foo, 4 | useValue: "foo" 5 | }; 6 | -------------------------------------------------------------------------------- /src/stores/history.ts: -------------------------------------------------------------------------------- 1 | 2 | import {writable} from "svelte/store"; 3 | import {History} from "history"; 4 | 5 | export const historyStore = writable(null as History) -------------------------------------------------------------------------------- /test/view/param.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 |
10 | Hello, {name} {page} 11 |
-------------------------------------------------------------------------------- /src/container/scope.ts: -------------------------------------------------------------------------------- 1 | export const SCOPE = { 2 | Request: "Request" as const, 3 | Singleton: "Singleton" as const, 4 | Transient: "Transient" as const 5 | }; 6 | -------------------------------------------------------------------------------- /src/types/ViewDetails.ts: -------------------------------------------------------------------------------- 1 | export interface ViewDetails { 2 | controller: any; 3 | method: string; 4 | route: string; 5 | path: string; 6 | url: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/provider/providerName.ts: -------------------------------------------------------------------------------- 1 | export const ParamService = Symbol.for("param"); 2 | export const QueryService = Symbol.for("query"); 3 | export const HistoryService = Symbol.for("history"); -------------------------------------------------------------------------------- /test/view/user-view.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 |
8 | {user.username} 9 |
10 |
{date}
-------------------------------------------------------------------------------- /src/types/RouteDetails.ts: -------------------------------------------------------------------------------- 1 | import { ViewDetails } from "./ViewDetails"; 2 | export interface RouteDetails { 3 | controller: any; 4 | controller_path: string; 5 | view_detail: ViewDetails[]; 6 | } 7 | -------------------------------------------------------------------------------- /src/container/builder/design.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export const Design = { 4 | Parameters:"design:paramtypes" as const, 5 | Constructor:Symbol.for("design:constructor") 6 | } 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/application/FrameworkMeta.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "../../src/slick-for-svelte-factory"; 2 | @Injectable() 3 | export class FrameworkMeta { 4 | getName() { 5 | return "@slick-for/svelte"; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Controller"; 2 | export * from "./Injectable"; 3 | export * from "./Module"; 4 | export * from "./View"; 5 | export * from "./inject"; 6 | export * from "./layout" 7 | export * from "./layout-props"; -------------------------------------------------------------------------------- /src/framework/tock.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | let tock = Promise.resolve(); 4 | export function setTock(promise:Promise){ 5 | tock = Promise.resolve(promise); 6 | } 7 | 8 | 9 | export async function Tock(){ 10 | await tock; 11 | } -------------------------------------------------------------------------------- /src/types/ModuleInitOptions.ts: -------------------------------------------------------------------------------- 1 | import history from "history"; 2 | export interface ModuleInitOptions { 3 | base?:any 4 | target: HTMLElement; 5 | component404: any; 6 | error?:any 7 | history: history.History; 8 | } 9 | -------------------------------------------------------------------------------- /src/decorators/allow.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export interface Guard{ 4 | allow(...any:any[]):string|undefined|Promise 5 | } 6 | 7 | export function Allow(...guards:Guard[]){ 8 | return ()=>{ 9 | 10 | } 11 | } -------------------------------------------------------------------------------- /src/helpers/promise-any.ts: -------------------------------------------------------------------------------- 1 | function reverse(promise) { 2 | return new Promise((resolve, reject) => Promise.resolve(promise).then(reject, resolve)); 3 | } 4 | 5 | export function promiseAny(iterable) { 6 | return reverse(Promise.all([...iterable].map(reverse))); 7 | }; -------------------------------------------------------------------------------- /test/view/layout-prop-view.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /src/types/ControllerOptions.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ScopeOptions{ 3 | scope?:"Request"|"Singleton"|"Transient" 4 | } 5 | 6 | export interface ControllerOptions extends ScopeOptions{ 7 | layout?: any; 8 | loading?:any 9 | error?:any; 10 | pause?:number 11 | } 12 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | 5 | "transform": { 6 | "\\.svelte$": "jest-transform-svelte" 7 | }, 8 | "moduleFileExtensions": [ 9 | "js", 10 | "ts", 11 | "json", 12 | "svelte" 13 | ] 14 | 15 | 16 | }; -------------------------------------------------------------------------------- /src/decorators/layout.ts: -------------------------------------------------------------------------------- 1 | import { VIEW_LAYOUT } from "../types/constants"; 2 | export function layout(layout: T) { 3 | return (target: any, key: string, descriptor: PropertyDescriptor) => { 4 | Reflect.defineMetadata(VIEW_LAYOUT, layout, target, key); 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /src/framework/CallInjectedController.ts: -------------------------------------------------------------------------------- 1 | import { MODULE } from "../types/constants"; 2 | import { Container } from "../container/builder/Container"; 3 | export function CallInjectedController(constructor) { 4 | 5 | let container: Container = Reflect.getMetadata(MODULE, constructor); 6 | return container.get(constructor); 7 | } 8 | -------------------------------------------------------------------------------- /src/container/config/ProviderFromValue.ts: -------------------------------------------------------------------------------- 1 | import { ValueProvider } from "../Provider"; 2 | import { SCOPE } from "../SCOPE"; 3 | export function ProviderFromValue(provider: ValueProvider) { 4 | return { 5 | scope:SCOPE.Singleton, 6 | provide:provider.provide, 7 | inject: [], 8 | useFactory: () => provider.useValue 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/container/Provider.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ValueProvider { 3 | scope?:"Request" | "Singleton" | "Transient" 4 | provide:any; 5 | useValue:any; 6 | inject?:any[] 7 | } 8 | 9 | 10 | 11 | export interface FactoryProvider{ 12 | 13 | scope?:"Request" | "Singleton" | "Transient" 14 | provide:any 15 | useFactory?:(...args:any)=>any|Promise 16 | inject?:any[] 17 | } 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/helpers/generate_id.ts: -------------------------------------------------------------------------------- 1 | export function makeid(length:number=32) { 2 | var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 3 | var charactersLength = characters.length; 4 | let array:string[] = new Array(length) 5 | for ( var i = 0; i < length; i++ ) { 6 | array[i] = characters.charAt(Math.floor(Math.random() * charactersLength)); 7 | } 8 | return array.join("")+"-"+Date.now(); 9 | } -------------------------------------------------------------------------------- /test/template/error.svelte: -------------------------------------------------------------------------------- 1 | 4 |
5 |

Error

6 | {#if !!error.message && !!error.stack} 7 |
 8 |           {error.message}
 9 |       
10 |
11 |           {error.stack}
12 |       
13 | {:else if typeof error==="string"} 14 |
15 |             {error}
16 |         
17 | {:else} 18 |
19 |             {JSON.stringify(error,null,2)}
20 |         
21 | {/if} 22 |
-------------------------------------------------------------------------------- /src/decorators/Injectable.ts: -------------------------------------------------------------------------------- 1 | import { INJECT_OPTIONS } from "../types/constants"; 2 | import { ScopeOptions } from "../types/ControllerOptions"; 3 | import { SCOPE } from "../container/SCOPE"; 4 | import { Design } from "../container/builder/design"; 5 | 6 | export function Injectable(options:ScopeOptions = {scope:SCOPE.Singleton}){ 7 | return constructor=>{ 8 | Reflect.defineMetadata(INJECT_OPTIONS,options,constructor); 9 | Reflect.defineMetadata(Design.Constructor,Reflect.getMetadata(Design.Parameters,constructor),constructor); 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "type": "node", 10 | "request": "launch", 11 | "name": "Launch Program", 12 | "args": ["${workspaceFolder}/example/module.eg.ts"], 13 | "runtimeArgs": ["-r","ts-node/register"] 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /test/dom.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import {JSDOM} from "jsdom" 3 | 4 | 5 | 6 | 7 | const {window} = new JSDOM(` 8 | 9 | 10 | 11 | `,{ 12 | url:"http://localhost/" 13 | }) 14 | 15 | 16 | export const document = window.document 17 | 18 | global["window"] = window; 19 | 20 | let {setTimeout,...browserApi} = window; 21 | Object.assign(global,browserApi); 22 | 23 | 24 | export function select(querySelector:string,element:any=document){ 25 | const root:typeof document = element as any; 26 | return root.querySelector(querySelector) 27 | } -------------------------------------------------------------------------------- /src/decorators/layout-props.ts: -------------------------------------------------------------------------------- 1 | import { LAYOUT_PROP } from "../types/constants"; 2 | import is from "@sindresorhus/is"; 3 | 4 | 5 | export function LayoutProps(target, method:string, decorator){ 6 | if(!method){ 7 | throw new Error("LayoutProps can only be used on a method") 8 | } 9 | target = method ? target.constructor: target; 10 | 11 | const method_name:string = Reflect.getMetadata(LAYOUT_PROP,target) 12 | if(!is.nullOrUndefined(method_name)){ 13 | throw new Error("LayoutProps can only be used once per a class") 14 | } 15 | 16 | Reflect.defineMetadata(LAYOUT_PROP,method,target) 17 | } -------------------------------------------------------------------------------- /src/decorators/View.ts: -------------------------------------------------------------------------------- 1 | import { VIEW, VIEW_PATH, VIEW_COMPONENT } from "../types/constants"; 2 | 3 | export function View(path: string,component:T) { 4 | return (target: any, key: string, descriptor: PropertyDescriptor) => { 5 | let inject = Reflect.getMetadata("design:paramtypes", target, key); 6 | 7 | Reflect.defineMetadata(VIEW, inject, target.constructor, key); 8 | Reflect.defineMetadata(VIEW_PATH, path, target.constructor, key); 9 | Reflect.defineMetadata(VIEW_COMPONENT, component, target.constructor, key); 10 | return descriptor; 11 | }; 12 | } 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/slick-for-svelte-factory.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { InitModule } from "./decorators/InitModule"; 3 | import { ModuleInitOptions } from "./types/ModuleInitOptions"; 4 | 5 | export * from "./decorators"; 6 | export * from "./stores/main"; 7 | export * from "./helpers/spa"; 8 | export * from "./helpers/history" 9 | export {Tock} from "./framework/tock"; 10 | export class SlickForSvelteFactory{ 11 | static create(app:any,options:ModuleInitOptions){ 12 | 13 | //options.base = options.base || defaultBase 14 | const App = InitModule( 15 | app, 16 | options 17 | ) 18 | return App; 19 | } 20 | } -------------------------------------------------------------------------------- /src/stores/main.ts: -------------------------------------------------------------------------------- 1 | import {writable} from "svelte/store" 2 | 3 | 4 | export const PARAMSTORE = writable({} as any); 5 | export const URLSTORE = writable(""); 6 | export const QUERYSTORE = writable({} as any); 7 | 8 | 9 | export const CurrentParameter = { 10 | value:null as any 11 | } 12 | 13 | PARAMSTORE.subscribe(value=>{ 14 | CurrentParameter.value=value; 15 | }) 16 | 17 | 18 | export const CurrentURL = { 19 | value:"" 20 | } 21 | 22 | URLSTORE.subscribe(value=>{ 23 | CurrentURL.value=value; 24 | }) 25 | 26 | export const CurrentQuery = { 27 | value:{} as any 28 | } 29 | 30 | 31 | QUERYSTORE.subscribe(value=>{ 32 | CurrentQuery.value=value; 33 | }) -------------------------------------------------------------------------------- /src/framework/InitModule.ts: -------------------------------------------------------------------------------- 1 | import { SlickApp } from "../application"; 2 | import history from "history"; 3 | import { MODULE, MODULE_OPTIONS } from "../types/constants"; 4 | import { ModuleInitOptions } from "../types/ModuleInitOptions"; 5 | export function InitModule(ModuleClass: any, options: ModuleInitOptions) { 6 | const container = Reflect.getMetadata(MODULE, ModuleClass); 7 | const moduleConfig = Reflect.getMetadata(MODULE_OPTIONS, ModuleClass); 8 | const h = options.history || history.createBrowserHistory(); 9 | const framework = new SlickApp(container, moduleConfig, options.target || document.body, options.base, options.component404, h); 10 | return framework; 11 | } 12 | -------------------------------------------------------------------------------- /src/helpers/spa.ts: -------------------------------------------------------------------------------- 1 | import {get} from "svelte/store"; 2 | 3 | import { historyStore } from "../stores/history"; 4 | 5 | export function spa(node:HTMLElement){ 6 | node.addEventListener("click",(e)=>{ 7 | let history = get(historyStore) 8 | e.stopImmediatePropagation(); 9 | e.stopPropagation(); 10 | e.preventDefault(); 11 | 12 | let event:any = e; 13 | let target = event.target.nodeName==="A"? event.target:event.currentTarget.nodeName==="A" ? event.currentTarget:null; 14 | if(target){ 15 | const link = target.getAttribute("href") 16 | if(link){ 17 | history.push(link); 18 | } 19 | } 20 | }) 21 | } -------------------------------------------------------------------------------- /test/application/LayoutPropController.ts: -------------------------------------------------------------------------------- 1 | import { Controller, View, Inject } from "../../src/decorators"; 2 | const Layout = require("../view/layout-prop-view.svelte"); 3 | const Page = require("../view/user-view.svelte"); 4 | import { LayoutProps } from "../../src/decorators/layout-props"; 5 | import { foo } from "./foo"; 6 | 7 | @Controller("/layout",{ 8 | layout:Layout 9 | }) 10 | export class LayoutPropController{ 11 | 12 | 13 | @LayoutProps 14 | getUser(@Inject(foo) foo:string){ 15 | return { 16 | user:{ 17 | username:foo 18 | } 19 | } 20 | } 21 | 22 | @View("/props",Page) 23 | getView(){ 24 | return { 25 | date:"Good Day" 26 | } 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/stores/storeupdator.ts: -------------------------------------------------------------------------------- 1 | import { QUERYSTORE, PARAMSTORE, URLSTORE } from "./main"; 2 | 3 | export class StoreUpdater{ 4 | 5 | private queryStore = QUERYSTORE 6 | 7 | private paramStore = PARAMSTORE 8 | 9 | 10 | private urlStore = URLSTORE 11 | 12 | 13 | 14 | updateQuery(query){ 15 | this.queryStore.update(()=>query); 16 | } 17 | 18 | updateUrl(url){ 19 | this.urlStore.update(()=>url) 20 | } 21 | 22 | updateParam(param){ 23 | this.paramStore.update(()=>param) 24 | } 25 | 26 | 27 | all({param,url,query}){ 28 | this.updateParam(param); 29 | this.updateQuery(query); 30 | this.updateUrl(url); 31 | } 32 | 33 | static set(){ 34 | return new StoreUpdater(); 35 | } 36 | 37 | 38 | } -------------------------------------------------------------------------------- /src/decorators/InitModule.ts: -------------------------------------------------------------------------------- 1 | import { SlickApp } from "../application"; 2 | import history from "history"; 3 | import { MODULE, MODULE_OPTIONS } from "../types/constants"; 4 | import { ModuleInitOptions } from "../types/ModuleInitOptions"; 5 | 6 | export function InitModule(ModuleClass: any, options: ModuleInitOptions) { 7 | const container = Reflect.getMetadata(MODULE, ModuleClass); 8 | const moduleConfig = Reflect.getMetadata(MODULE_OPTIONS, ModuleClass); 9 | const h = options.history || history.createBrowserHistory(); 10 | const framework = new SlickApp( 11 | container, 12 | moduleConfig, 13 | options.target || document.body, 14 | options.base, 15 | options.component404, h, 16 | options.error 17 | ); 18 | return framework; 19 | } 20 | -------------------------------------------------------------------------------- /src/types/constants.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | export const SERVICE = Symbol.for("service"); 5 | 6 | export const CONTROLLER = Symbol.for("controller"); 7 | 8 | export const CONTROLLER_PATH = Symbol.for("controller_path"); 9 | 10 | export const INJECT_OPTIONS = Symbol.for("controller_options") 11 | 12 | export const INJECT_CONSTRUCT = Symbol.for("inject"); 13 | 14 | export const MODULE_OPTIONS = Symbol.for("module-options"); 15 | 16 | export const MODULE = Symbol.for("module"); 17 | 18 | export const PARAMETER = "design:slick-for:parameter" 19 | 20 | export const LAYOUT_PROP = "design:slick-for:layout-props" 21 | 22 | export const VIEW = Symbol.for("view"); 23 | 24 | export const VIEW_PATH = Symbol.for("view_path"); 25 | 26 | export const VIEW_COMPONENT = Symbol.for("view-component"); 27 | 28 | export const VIEW_LAYOUT = Symbol.for("view-layout") 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/provider/defaultProviders.ts: -------------------------------------------------------------------------------- 1 | import { ParamService, QueryService, HistoryService } from "./providerName"; 2 | import {get} from "svelte/store"; 3 | import { PARAMSTORE, QUERYSTORE } from "../stores/main"; 4 | import { historyStore } from "../stores/history"; 5 | 6 | 7 | 8 | export const ParamProvider = { 9 | 10 | provide:ParamService, 11 | 12 | useFactory(){ 13 | 14 | return get(PARAMSTORE) 15 | }, 16 | scope:"Request" 17 | } 18 | 19 | 20 | export const QueryProvider = { 21 | provide:QueryService, 22 | useFactory(){ 23 | return get(QUERYSTORE) 24 | }, 25 | scope:"Request" 26 | } 27 | 28 | 29 | export const HistoryProvider = { 30 | provide:HistoryService, 31 | useFactory(){ 32 | return get(historyStore) 33 | }, 34 | scope:"Request" 35 | 36 | } 37 | 38 | 39 | export const ApplicationProviders = [ParamProvider,QueryProvider,HistoryProvider] -------------------------------------------------------------------------------- /src/container/config/ProviderFromConstructor.ts: -------------------------------------------------------------------------------- 1 | import { Design } from "../builder/design"; 2 | import { IConstructor } from "../builder/IConstructor"; 3 | import { PARAMETER, INJECT_OPTIONS } from "../../types/constants"; 4 | import { SCOPE } from "../SCOPE"; 5 | export function ProviderFromConstructor(constructor: IConstructor) { 6 | 7 | const inject = Reflect.getMetadata(Design.Parameters, constructor) || []; 8 | const alter = Reflect.getMetadata(PARAMETER, constructor) || []; 9 | 10 | 11 | 12 | 13 | if (alter) { 14 | alter.forEach(({ index, identifier }) => { 15 | inject.splice(index, 1, identifier); 16 | }); 17 | } 18 | 19 | return { 20 | provide: constructor, 21 | inject, 22 | useFactory: (...args: any[]) => { 23 | return Reflect.construct(constructor, args); 24 | }, 25 | scope: (Reflect.getMetadata(INJECT_OPTIONS, constructor) || {}).scope || "Singleton" 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/decorators/Controller.ts: -------------------------------------------------------------------------------- 1 | 2 | import { CONTROLLER_PATH, INJECT_OPTIONS, PARAMETER } from "../types/constants"; 3 | import { ControllerOptions } from "../types/ControllerOptions"; 4 | import { Design } from "../container/builder/design"; 5 | 6 | 7 | export function Controller(path: string = "/",options:ControllerOptions={}) { 8 | path = path === ""? "/":path 9 | return (constructor: any) => { 10 | Reflect.defineMetadata(CONTROLLER_PATH, path, constructor); 11 | if(options){ 12 | Reflect.defineMetadata(INJECT_OPTIONS,options,constructor); 13 | } 14 | 15 | let inject = Reflect.getMetadata(Design.Parameters,constructor) || []; 16 | 17 | 18 | inject = inject.map((x,index)=>{ 19 | return { 20 | index, 21 | identifier:x 22 | } 23 | }) 24 | 25 | //Reflect.defineMetadata(PARAMETER,inject,constructor); 26 | return constructor; 27 | }; 28 | } 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/application/FactoryController.ts: -------------------------------------------------------------------------------- 1 | import { Controller, View, Inject } from "../../src/decorators"; 2 | const FactoryView = require("../view/factory-constructor.svelte"); 3 | 4 | 5 | export const FactoryService = "factory"; 6 | 7 | export const FactoryProvider = { 8 | provide:FactoryService, 9 | useFactory(){ 10 | return { 11 | message:"New Factory" 12 | } 13 | } 14 | } 15 | 16 | 17 | 18 | 19 | export const AsyncFactoryService = "factory"; 20 | 21 | export const AsyncFactoryProvider = { 22 | provide:FactoryService, 23 | useFactory(){ 24 | return { 25 | message:"New Factory" 26 | } 27 | } 28 | } 29 | 30 | 31 | @Controller("/controller/factory") 32 | export class FactoryController{ 33 | 34 | constructor(@Inject(FactoryService) private factory, @Inject(AsyncFactoryService) private async_factory){ 35 | 36 | } 37 | 38 | 39 | @View("/inject",FactoryView) 40 | view(){ 41 | return this.factory 42 | } 43 | 44 | 45 | @View("/async",FactoryView) 46 | asyncView(){ 47 | return this.async_factory 48 | } 49 | } -------------------------------------------------------------------------------- /test/application/test.app.ts: -------------------------------------------------------------------------------- 1 | const Template = require("../template/base.svelte"); 2 | const NotFound = require("../template/404.svelte"); 3 | const ErrorPage = require("../template/error.svelte"); 4 | import "../dom"; 5 | import * as History from "history"; 6 | 7 | import { BasicController } from "./BasicController"; 8 | import { FooProvider } from "./foo"; 9 | import { Module, SlickForSvelteFactory } from "../../src/slick-for-svelte-factory"; 10 | import { FrameworkMeta } from "./FrameworkMeta"; 11 | import { FactoryController, FactoryProvider } from "./FactoryController"; 12 | import { LayoutPropController } from "./LayoutPropController"; 13 | 14 | 15 | @Module({ 16 | controllers: [BasicController, FactoryController,LayoutPropController], 17 | provider: [FooProvider, FrameworkMeta, FactoryProvider] 18 | }) 19 | class Application { 20 | 21 | } 22 | 23 | 24 | export const history = History.createMemoryHistory() 25 | 26 | 27 | 28 | const app = SlickForSvelteFactory.create(Application, { 29 | component404: NotFound, 30 | target: document.body, 31 | error: ErrorPage, 32 | history 33 | }) 34 | 35 | 36 | 37 | app.Initialize(); -------------------------------------------------------------------------------- /src/container/config/ContainerBuilder.ts: -------------------------------------------------------------------------------- 1 | import { FactoryProvider } from "../Provider"; 2 | import { IContainerBuilder } from "./IContainerBuilder"; 3 | import * as check from "../../provider/check"; 4 | import { ProviderFromConstructor } from "./ProviderFromConstructor"; 5 | import { ProviderFromValue } from "./ProviderFromValue"; 6 | import { Container } from "../builder/Container"; 7 | export class ContainerBuilder implements IContainerBuilder { 8 | private providers: FactoryProvider[] = []; 9 | 10 | 11 | bind(provider){ 12 | return this.add(provider); 13 | } 14 | add(provider: any): this { 15 | if (check.isConstructor(provider)) { 16 | this.providers.push(ProviderFromConstructor(provider)); 17 | } 18 | else if (check.IsUseValue(provider)) { 19 | this.providers.push(ProviderFromValue(provider)); 20 | } 21 | else if (check.IsUseFactory(provider)) { 22 | provider.inject = provider.inject || [] 23 | this.providers.push(provider); 24 | } 25 | else { 26 | throw new Error("not a valid provider"); 27 | } 28 | return this; 29 | } 30 | static getContainer(builder: ContainerBuilder) { 31 | return new Container(builder.providers); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/template/base.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | {#if viewProps} 12 | {#if viewProps.NotFound} 13 | 14 | {:else} 15 | {#await layout_props} 16 | 17 | {:then layout_props} 18 | {#await viewProps} 19 | 20 | {:then props} 21 | {#if viewProps.NotFound} 22 | 23 | {:else if layout} 24 | {#if props} 25 | 26 | 27 | 28 | {:else} 29 | 30 | 31 | 32 | {/if} 33 | {:else if props} 34 | 35 | {:else} 36 | 37 | {/if} 38 | {:catch error$} 39 | 40 | {/await} 41 | {/await} 42 | {/if} 43 | {/if} 44 | -------------------------------------------------------------------------------- /src/provider/check.ts: -------------------------------------------------------------------------------- 1 | import is from "@sindresorhus/is" 2 | 3 | export function IsProvider(provider:any){ 4 | return (is.object(provider) && isObjectProvider(provider)) || IsConstructor(provider); 5 | } 6 | 7 | export function IsConstructor(provider:any){ 8 | return isConstructor(provider); 9 | } 10 | export function isObjectProvider(provider:any){ 11 | return [IsUseValue,IsUseFactory,IsUseClass].reduce((f,g)=>{ 12 | return f || g(provider) 13 | },false) && 'provide' in provider; 14 | } 15 | 16 | export function IsUseValue(provider:any){ 17 | return 'useValue' in provider; 18 | } 19 | 20 | 21 | export function IsUseFactory(provider:any){ 22 | return 'useFactory' in provider; 23 | } 24 | 25 | 26 | export function IsUseClass(provider:any){ 27 | return 'useClass' in provider; 28 | } 29 | 30 | 31 | export function IsDefined(value:any){ 32 | return value!==undefined || value!==null || value!==void 0; 33 | } 34 | 35 | 36 | export function isConstructor(symbol:any) { 37 | return notUndefined(symbol) && 38 | symbol instanceof Function && 39 | symbol.constructor && 40 | symbol.constructor instanceof Function && 41 | // notUndefined(new symbol) && 42 | Object.getPrototypeOf(symbol) !== Object.prototype && 43 | symbol.constructor !== Object && 44 | symbol.prototype.hasOwnProperty('constructor'); 45 | } 46 | export function notUndefined(item:any) { 47 | return item != undefined && item != 'undefined'; 48 | } -------------------------------------------------------------------------------- /test/application/BasicController.ts: -------------------------------------------------------------------------------- 1 | const Page1 = require("../view/page1.svelte"); 2 | const ParamPage = require("../view/param.svelte") 3 | const Framename = require("../view/class-constructor.svelte") 4 | import { Controller, Inject, View, Param, Query } from "../../src/slick-for-svelte-factory"; 5 | 6 | import { foo } from "./foo"; 7 | import { FrameworkMeta } from "./FrameworkMeta"; 8 | 9 | 10 | 11 | @Controller("/") 12 | export class BasicController { 13 | constructor( 14 | @Inject(foo) 15 | public foo: string, 16 | private framework:FrameworkMeta) { 17 | } 18 | @View("/", Page1) 19 | page() { 20 | 21 | } 22 | 23 | @View("/user/:name",ParamPage) 24 | paramTest(@Param('name') name:string,@Query('page',x=>x||"1",parseInt) page:any){ 25 | 26 | return { 27 | name, 28 | page 29 | } 30 | } 31 | @View("/async/user/:name",ParamPage) 32 | async paramAsyncTest(@Param('name') name:string,@Query('page',x=>x||"1",parseInt) page:any){ 33 | await new Promise(r=>setTimeout(r,10)) 34 | return { 35 | name, 36 | page 37 | } 38 | } 39 | 40 | 41 | @View("/meta",Framename) 42 | withClassConstructor(){ 43 | return { 44 | name:this.framework.getName(), 45 | } 46 | } 47 | 48 | 49 | @View("/error",ParamPage) 50 | errorTest(){ 51 | throw new Error("This is intentional") 52 | } 53 | 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/decorators/inject.ts: -------------------------------------------------------------------------------- 1 | import { PARAMETER } from "../types/constants"; 2 | import { ParamService, QueryService, HistoryService } from "../provider/providerName"; 3 | 4 | export function Inject(identifier:any,action=x=>x){ 5 | return (target,method,parameterIndex)=>{ 6 | target = method? target.constructor: target; 7 | let params = (method ? Reflect.getMetadata(PARAMETER,target,method):Reflect.getMetadata(PARAMETER,target)) || [] 8 | params.push({ 9 | index:parameterIndex, 10 | identifier, 11 | action 12 | }) 13 | if(method){ 14 | Reflect.defineMetadata(PARAMETER,params,target,method); 15 | }else{ 16 | Reflect.defineMetadata(PARAMETER,params,target); 17 | } 18 | } 19 | } 20 | 21 | export function Param(name?:string,...transform:any[]){ 22 | let shaper = [(x)=>name?x[name]:x,...transform] 23 | function shape(x){ 24 | return shaper.reduce(((x,method)=>{ 25 | return method(x) 26 | }),x) 27 | } 28 | return Inject(ParamService,shape) 29 | } 30 | 31 | 32 | export function Query(name?:string,...transform:any[]){ 33 | let shaper = [(x)=>name?x[name]:x,...transform] 34 | function shape(x){ 35 | return shaper.reduce(((x,method)=>{ 36 | return method(x) 37 | }),x) 38 | } 39 | return Inject(QueryService,shape) 40 | } 41 | 42 | 43 | export function History(name?:string){ 44 | return Inject(HistoryService) 45 | } 46 | -------------------------------------------------------------------------------- /src/framework/CallInjectedView.ts: -------------------------------------------------------------------------------- 1 | 2 | import is from "@sindresorhus/is" 3 | import { MODULE, VIEW, PARAMETER } from "../types/constants"; 4 | 5 | 6 | export function CallInjectedView(target: any, key: string) { 7 | 8 | let method = target[key]; 9 | let constructor = Object.getPrototypeOf(target).constructor; 10 | 11 | let container = Reflect.getMetadata(MODULE, constructor); 12 | let inject: any[] = Reflect.getMetadata(VIEW, constructor, key) || []; 13 | 14 | const params = Reflect.getMetadata(PARAMETER,constructor,key); 15 | if(params){ 16 | params.forEach(({index,identifier})=>{ 17 | inject.splice(index,1,identifier) 18 | }) 19 | } 20 | 21 | 22 | let dependencies = inject.map((i, index) => { 23 | if (container.isBound(i)) { 24 | return container.get(i); 25 | } 26 | else { 27 | throw new Error(`Can't resolve parameter [${index}] of ${constructor.name}:${key}`); 28 | } 29 | }); 30 | if (dependencies.some(is.promise)) { 31 | return Promise.all(dependencies).then(args => { 32 | 33 | return method.apply(target, PerformParameterAction(args,params)); 34 | }); 35 | } 36 | else { 37 | 38 | return method.apply(target, PerformParameterAction(dependencies,params)); 39 | } 40 | } 41 | 42 | 43 | 44 | export function PerformParameterAction(args:any[],actions:any[]){ 45 | if(!actions){ 46 | return args; 47 | } 48 | actions.forEach((transform)=>{ 49 | let action = transform.action 50 | args.splice(transform.index,1,action(args[transform.index])) 51 | }) 52 | 53 | return args; 54 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Optional REPL history 59 | .node_repl_history 60 | 61 | # Output of 'npm pack' 62 | *.tgz 63 | 64 | # Yarn Integrity file 65 | .yarn-integrity 66 | 67 | # dotenv environment variables file 68 | .env 69 | .env.test 70 | 71 | # parcel-bundler cache (https://parceljs.org/) 72 | .cache 73 | 74 | # next.js build output 75 | .next 76 | 77 | # nuxt.js build output 78 | .nuxt 79 | 80 | # vuepress build output 81 | .vuepress/dist 82 | 83 | # Serverless directories 84 | .serverless/ 85 | 86 | # FuseBox cache 87 | .fusebox/ 88 | 89 | # DynamoDB Local files 90 | .dynamodb/ 91 | 92 | *.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Optional REPL history 59 | .node_repl_history 60 | 61 | # Output of 'npm pack' 62 | *.tgz 63 | 64 | # Yarn Integrity file 65 | .yarn-integrity 66 | 67 | # dotenv environment variables file 68 | .env 69 | .env.test 70 | 71 | # parcel-bundler cache (https://parceljs.org/) 72 | .cache 73 | 74 | # next.js build output 75 | .next 76 | 77 | # nuxt.js build output 78 | .nuxt 79 | 80 | # vuepress build output 81 | .vuepress/dist 82 | 83 | # Serverless directories 84 | .serverless/ 85 | 86 | # FuseBox cache 87 | .fusebox/ 88 | 89 | # DynamoDB Local files 90 | .dynamodb/ 91 | 92 | *.DS_Store -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@slick-for/svelte", 3 | "version": "1.2.3", 4 | "description": "A Framework for Stitching together sick svelte Applications", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/shavyg2/slick-for-svelte.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/shavyg2/slick-for-svelte/issues" 12 | }, 13 | "homepage": "https://github.com/shavyg2/slick-for-svelte#readme", 14 | "author": "Shavauhn Gabay", 15 | "keywords": [ 16 | "Svelte", 17 | "Framework", 18 | "BootStrap", 19 | "decorators", 20 | "typescript", 21 | "route", 22 | "routing" 23 | ], 24 | "main": "dist/src/slick-for-svelte-factory.js", 25 | "scripts": { 26 | "test": "echo \"No Test\"", 27 | "prepublish": "tsc" 28 | }, 29 | "dependencies": { 30 | "@sindresorhus/is": "^1.0.0", 31 | "query-string": "^6.8.1", 32 | "route-parser": "^0.0.5" 33 | }, 34 | "peerDependencies": { 35 | "history": "^4.9.0", 36 | "reflect-metadata": "^0.1.13", 37 | "svelte": "^3.6.7" 38 | }, 39 | "devDependencies": { 40 | "@slick-for/di": "^1.0.2", 41 | "@types/event-emitter": "^0.3.3", 42 | "@types/history": "^4.7.2", 43 | "@types/is-promise": "^2.1.0", 44 | "@types/jest": "^24.0.15", 45 | "@types/puppeteer": "^1.19.0", 46 | "@types/route-parser": "^0.1.3", 47 | "@types/shell-quote": "^1.6.1", 48 | "@types/url-join": "^4.0.0", 49 | "@types/uuid": "^3.4.5", 50 | "cheerio": "^1.0.0-rc.3", 51 | "get-port": "^5.0.0", 52 | "history": "^4.9.0", 53 | "jest": "^24.8.0", 54 | "jest-transform-svelte": "^2.0.2", 55 | "parcel": "^1.12.3", 56 | "puppeteer": "^1.19.0", 57 | "reflect-metadata": "^0.1.13", 58 | "shell-quote": "^1.6.1", 59 | "svelte": "^3.6.8", 60 | "svelte-jest": "^0.3.0", 61 | "ts-jest": "^24.0.2", 62 | "ts-node": "^8.3.0", 63 | "typescript": "^3.5.3" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/decorators/Module.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IModuleConfig } from "../types/IModuleConfig"; 3 | import { MODULE_OPTIONS, MODULE } from "../types/constants"; 4 | import * as check from "../provider/check"; 5 | import { ContainerBuilder } from "../container/config/ContainerBuilder"; 6 | import { ApplicationProviders } from "../provider/defaultProviders"; 7 | export function Module(config: IModuleConfig) { 8 | return (constructor: any) => { 9 | 10 | var builder = new ContainerBuilder() 11 | const container = ContainerBuilder.getContainer(builder); 12 | Reflect.defineMetadata(MODULE_OPTIONS, config, constructor); 13 | Reflect.defineMetadata(MODULE, container, constructor); 14 | if (config.controllers) { 15 | config.controllers.forEach(controller => { 16 | Reflect.defineMetadata(MODULE, container, controller); 17 | builder.add(controller) 18 | }); 19 | } 20 | if (config.provider) { 21 | config.provider.forEach(provider => { 22 | if (check.IsProvider(provider)) { 23 | if (check.isObjectProvider(provider)) { 24 | if (check.IsUseClass(provider)) { 25 | builder.bind(provider) 26 | } 27 | else if (check.IsUseValue(provider)) { 28 | builder.bind(provider) 29 | } 30 | else if (check.IsUseFactory(provider)) { 31 | builder.bind(provider) 32 | } 33 | } 34 | else if (check.IsConstructor(provider)) { 35 | builder.bind(provider) 36 | } 37 | else { 38 | throw new Error("incorrectly provided provider"); 39 | } 40 | } 41 | }); 42 | 43 | ApplicationProviders.forEach(provider=>{ 44 | builder.add(provider) 45 | }) 46 | return constructor; 47 | } 48 | 49 | 50 | 51 | 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/helpers/url-join.ts: -------------------------------------------------------------------------------- 1 | function UrlJoin() { 2 | 3 | function normalize (strArray) { 4 | var resultArray = []; 5 | if (strArray.length === 0) { return ''; } 6 | 7 | if (typeof strArray[0] !== 'string') { 8 | throw new TypeError('Url must be a string. Received ' + strArray[0]); 9 | } 10 | 11 | // If the first part is a plain protocol, we combine it with the next part. 12 | if (strArray[0].match(/^[^/:]+:\/*$/) && strArray.length > 1) { 13 | var first = strArray.shift(); 14 | strArray[0] = first + strArray[0]; 15 | } 16 | 17 | // There must be two or three slashes in the file protocol, two slashes in anything else. 18 | if (strArray[0].match(/^file:\/\/\//)) { 19 | strArray[0] = strArray[0].replace(/^([^/:]+):\/*/, '$1:///'); 20 | } else { 21 | strArray[0] = strArray[0].replace(/^([^/:]+):\/*/, '$1://'); 22 | } 23 | 24 | for (var i = 0; i < strArray.length; i++) { 25 | var component = strArray[i]; 26 | 27 | if (typeof component !== 'string') { 28 | throw new TypeError('Url must be a string. Received ' + component); 29 | } 30 | 31 | if (component === '') { continue; } 32 | 33 | if (i > 0) { 34 | // Removing the starting slashes for each component but the first. 35 | component = component.replace(/^[\/]+/, ''); 36 | } 37 | if (i < strArray.length - 1) { 38 | // Removing the ending slashes for each component but the last. 39 | component = component.replace(/[\/]+$/, ''); 40 | } else { 41 | // For the last component we will combine multiple slashes to a single one. 42 | component = component.replace(/[\/]+$/, '/'); 43 | } 44 | 45 | resultArray.push(component); 46 | 47 | } 48 | 49 | var str = resultArray.join('/'); 50 | // Each input component is now separated by a single slash except the possible first plain protocol part. 51 | 52 | // remove trailing slash before parameters or hash 53 | str = str.replace(/\/(\?|&|#[^!])/g, '$1'); 54 | 55 | // replace ? in parameters with & 56 | var parts = str.split('?'); 57 | str = parts.shift() + (parts.length > 0 ? '?': '') + parts.join('&'); 58 | 59 | return str; 60 | } 61 | 62 | return function (...args:string[]) { 63 | var input; 64 | 65 | if (typeof args[0] === 'object') { 66 | input = args[0]; 67 | } else { 68 | input = [].slice.call(args); 69 | } 70 | 71 | return normalize(input); 72 | } 73 | } 74 | 75 | 76 | export const urlJoin = UrlJoin(); -------------------------------------------------------------------------------- /test/framework.spec.ts: -------------------------------------------------------------------------------- 1 | require("svelte/register"); 2 | import { select } from "./dom"; 3 | import "./application/test.app"; 4 | import { history } from "./application/test.app"; 5 | import { Tock } from "../src/framework/tock"; 6 | 7 | 8 | 9 | describe("Framework Test",()=>{ 10 | 11 | it("should be able to render the document",()=>{ 12 | history.push("/") 13 | let div = select("div") 14 | expect(div.innerHTML).toBe("Hello World"); 15 | }) 16 | it("Should Render the Error Page for unknown url",async()=>{ 17 | history.push("/error/404") 18 | await Tock(); 19 | let div = select("h1") 20 | expect(div.innerHTML).toBe("Error 404"); 21 | }) 22 | 23 | 24 | it("it should be able to get the param from the url",async()=>{ 25 | history.push("/user/slick") 26 | await Tock(); 27 | let div = select("div") 28 | expect(div.innerHTML).toBe("Hello, slick 1"); 29 | }) 30 | 31 | 32 | it("it should be able to get the param from an async controller method",async()=>{ 33 | history.push("/async/user/slick") 34 | await Tock(); 35 | let div = select("div") 36 | expect(div.innerHTML).toBe("Hello, slick 1"); 37 | }) 38 | 39 | 40 | it("it should be able to get the param from a constructor class",async()=>{ 41 | history.push("/meta") 42 | await Tock(); 43 | let div = select("div") 44 | expect(div.innerHTML).toBe("@slick-for/svelte"); 45 | }) 46 | 47 | 48 | it("Throw an Error on purpose",async()=>{ 49 | history.push("/error") 50 | await Tock(); 51 | let div = select("h1") 52 | expect(div.innerHTML).toBe("Error"); 53 | let pre = select("pre") 54 | expect(/This is intentional/.test(pre.innerHTML)).toBeTruthy(); 55 | }) 56 | 57 | 58 | it("should load factory providers",async ()=>{ 59 | history.push("/controller/factory/inject") 60 | await Tock(); 61 | const div = select("div") 62 | expect(div.innerHTML).toBe("New Factory"); 63 | }) 64 | 65 | 66 | it("should load async factory providers",async ()=>{ 67 | history.push("/controller/factory/async") 68 | await Tock(); 69 | const div = select("div") 70 | expect(div.innerHTML).toBe("New Factory"); 71 | }) 72 | 73 | 74 | it("should load layout props using the layout props decorator",async ()=>{ 75 | history.push("/layout/props") 76 | await Tock(); 77 | 78 | debugger; 79 | const user = select("#user-menu") 80 | const date = select("#date") 81 | 82 | 83 | 84 | expect(user.innerHTML).toBe("foo"); 85 | expect(date.innerHTML).toBe("Good Day"); 86 | }) 87 | }) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | "lib": ["esnext","dom"], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | "declarationMap": false, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | "sourceMap": false, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./dist", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | "tsBuildInfoFile": "./dist/info", /* Specify file to store incremental compilation information */ 19 | "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": false, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": ["./node_modules"], /* List of folders to include type definitions from. */ 47 | "types": ["./node_modules/reflect-metadata","./node_modules/@types/jest"], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/container/builder/Container.ts: -------------------------------------------------------------------------------- 1 | import { FactoryProvider } from "../Provider"; 2 | import { SCOPE } from "../SCOPE"; 3 | // import uuid from "uuid/v4"; 4 | import is from "@sindresorhus/is" 5 | import { IContainer } from "./IContainer"; 6 | import { makeid } from "../../helpers/generate_id"; 7 | 8 | export type Scope = "Request" | "Transient" | "Singleton"; 9 | 10 | export class Container implements IContainer { 11 | cache = { 12 | Request: new Map>(), 13 | Singleton: new Map() 14 | }; 15 | constructor(private providers: FactoryProvider[]) {} 16 | 17 | isBound(identifier: any) { 18 | return !!this.providers.find(x => x.provide === identifier); 19 | } 20 | 21 | get(identifier: any): T | PromiseLike { 22 | let id = makeid(); 23 | this.cache.Request.set(id, new Map()); 24 | let build = this.build(identifier, id); 25 | 26 | if (is.promise(build)) { 27 | return build.then((instance:any) => { 28 | this.cache.Request.delete(id); 29 | return instance; 30 | }); 31 | } else { 32 | this.cache.Request.delete(id); 33 | return build; 34 | } 35 | } 36 | 37 | build(identifier: any, requestID: string) { 38 | let provider = this.providers.find(x => x.provide === identifier); 39 | if (!provider) { 40 | throw new Error( 41 | `No Provider for identifier ${this.printIdentifier(identifier)}` 42 | ); 43 | } 44 | 45 | if (provider.inject && provider.inject.length) { 46 | let injectScope = this.getScopeOfInjects(provider.inject); 47 | let scope = ScopePicker.pickList(provider.scope, ...injectScope); 48 | 49 | scope = provider.scope || "Singleton"; 50 | 51 | switch (scope) { 52 | case "Singleton": 53 | return this.buildAsSingleton(provider, requestID); 54 | case "Request": 55 | return this.buildAsRequest(provider, requestID); 56 | case "Transient": 57 | return this.buildAsTransient(provider, requestID); 58 | } 59 | } else { 60 | switch (provider.scope || "Singleton") { 61 | case "Singleton": 62 | return this.buildAsSingleton(provider, requestID); 63 | case "Request": 64 | return this.buildAsRequest(provider, requestID); 65 | case "Transient": 66 | return this.buildAsTransient(provider, requestID); 67 | default: 68 | throw new Error(`Invalid scope ${provider.scope}`); 69 | } 70 | } 71 | } 72 | 73 | private buildAsSingleton(provider: FactoryProvider, requestID: string) { 74 | let cache = this.cache.Singleton; 75 | if (cache.has(provider.provide)) { 76 | return cache.get(provider.provide); 77 | } else { 78 | 79 | let dependencies = provider.inject.map(x => { 80 | return this.build(x, requestID); 81 | }); 82 | 83 | if (dependencies.some(is.promise)) { 84 | return Promise.all(dependencies.map(x => Promise.resolve(x))) 85 | .then(args => { 86 | return provider.useFactory(...args); 87 | }) 88 | .then(instance => { 89 | cache.set(provider.provide, instance); 90 | return instance; 91 | }); 92 | } else { 93 | const instance = provider.useFactory(...dependencies); 94 | cache.set(provider.provide, instance); 95 | return instance; 96 | } 97 | } 98 | } 99 | 100 | buildAsRequest(provider: FactoryProvider, requestID: string) { 101 | let cache = this.cache.Request.get(requestID); 102 | 103 | if (cache.has(provider.provide)) { 104 | return cache.get(provider.provide); 105 | } else { 106 | let dependencies = (provider.inject || []).map(x => { 107 | return this.build(x, requestID); 108 | }); 109 | 110 | if (dependencies.some(is.promise)) { 111 | return Promise.all(dependencies.map(x => Promise.resolve(x))) 112 | .then(args => { 113 | return provider.useFactory(...args); 114 | }) 115 | .then(instance => { 116 | cache.set(provider.provide, instance); 117 | return instance; 118 | }); 119 | } else { 120 | const instance = provider.useFactory(...dependencies); 121 | cache.set(provider.provide, instance); 122 | return instance; 123 | } 124 | } 125 | } 126 | 127 | buildAsTransient(provider: FactoryProvider, requestID) { 128 | let dependencies = (provider.inject || []).map(x => { 129 | return this.build(x, requestID); 130 | }); 131 | 132 | if (dependencies.some(is.promise)) { 133 | return Promise.all(dependencies.map(x => Promise.resolve(x))) 134 | .then(args => { 135 | return provider.useFactory(...args); 136 | }) 137 | .then(instance => { 138 | return instance; 139 | }); 140 | } else { 141 | const instance = provider.useFactory(...dependencies); 142 | return instance; 143 | } 144 | } 145 | private printIdentifier(identifier: any) { 146 | return `${identifier}`; 147 | } 148 | 149 | private getScopeOfInjects(injects: any[]) { 150 | let scopes = injects.map(inject => { 151 | let dependency = this.providers.find(x => x.provide === inject); 152 | 153 | if (!dependency) { 154 | throw new Error(`Dependency not found for ${inject}`); 155 | } 156 | 157 | return dependency.scope; 158 | }); 159 | 160 | return scopes; 161 | } 162 | 163 | private scopePicker( 164 | scope1: Scope, 165 | scope2: Scope, 166 | options = ["Transient", "Request", "Singleton"] 167 | ) { 168 | return [scope1, scope2].sort((a, b) => { 169 | return options.indexOf(a) - options.indexOf(b); 170 | })[0]; 171 | } 172 | } 173 | 174 | export class ScopePicker { 175 | public static pickList(...scopes: Scope[]) { 176 | return scopes.reduce((a, b) => { 177 | return this.pick(a, b); 178 | }, "Singleton"); 179 | } 180 | 181 | public static pick( 182 | scope1: Scope, 183 | scope2: Scope, 184 | options = ["Transient", "Request", "Singleton"] 185 | ) { 186 | return [scope1, scope2].sort((a, b) => { 187 | return options.indexOf(a) - options.indexOf(b); 188 | })[0]; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/application.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CONTROLLER_PATH, 3 | VIEW_PATH, 4 | INJECT_OPTIONS, 5 | VIEW_COMPONENT, 6 | LAYOUT_PROP 7 | } from "./types/constants"; 8 | import { IModuleConfig } from "./types/IModuleConfig"; 9 | import { History } from "history"; 10 | import { URLSTORE, PARAMSTORE, QUERYSTORE } from "./stores/main"; 11 | import RouteParser from "route-parser"; 12 | import { RouteDetails } from "./types/RouteDetails"; 13 | import { ViewDetails } from "./types/ViewDetails"; 14 | import { CallInjectedView } from "./framework/CallInjectedView"; 15 | import { CallInjectedController } from "./framework/CallInjectedController"; 16 | import is from "@sindresorhus/is"; 17 | import queryString from "query-string"; 18 | 19 | import { Container } from "./container/builder/Container"; 20 | import { historyStore } from "./stores/history"; 21 | import { StoreUpdater } from "./stores/storeupdator"; 22 | import { setTock, Tock } from "./framework/tock"; 23 | import { promiseAny } from "./helpers/promise-any"; 24 | import { urlJoin } from "./helpers/url-join"; 25 | 26 | 27 | 28 | type UrlPathReference = (readonly [ 29 | RouteParser<{ 30 | [i: string]: any; 31 | }>, 32 | { 33 | controller: any; 34 | method: string; 35 | route: string; 36 | path: string; 37 | url: string; 38 | } 39 | ])[]; 40 | 41 | const getMetaData = Reflect.getMetadata.bind(Reflect); 42 | export class SlickApp { 43 | constructor( 44 | private container: Container, 45 | private options: IModuleConfig, 46 | private target: HTMLElement, 47 | private base: any, 48 | private Component404: any, 49 | private history: History, 50 | private errorPage?: any 51 | ) { 52 | historyStore.update(x => this.history); 53 | } 54 | 55 | Initialize() { 56 | this.compile(); 57 | this.start(); 58 | } 59 | compile() { 60 | let UrlConfiguration = this.GetAppConfiguration(); 61 | this.boot(UrlConfiguration); 62 | return this.container; 63 | } 64 | 65 | private start() { 66 | const init = urlJoin(window.location.pathname, window.location.search); 67 | this.history.push(init); 68 | } 69 | 70 | private GetAppConfiguration() { 71 | const routeDetail = this.controllers.map(Controller => { 72 | return this.getControllerRouteDetail(Controller); 73 | }); 74 | const RouteNavigation = this.getViewNavigation(routeDetail); 75 | 76 | let UrlCompass = RouteNavigation.map(navigation => { 77 | const search = new RouteParser(navigation.url); 78 | return [search, navigation] as const; 79 | }); 80 | return UrlCompass; 81 | } 82 | 83 | private boot(urlPathReference: UrlPathReference) { 84 | const CreateApplication = (viewProps:any = {})=>{ 85 | let view = new this.base({ 86 | target: this.target, 87 | props: { 88 | URLSTORE, 89 | PARAMSTORE, 90 | QUERYSTORE, 91 | viewProps 92 | } 93 | }); 94 | return view; 95 | } 96 | 97 | let Application = CreateApplication() 98 | 99 | this.history.listen(async (location, action) => { 100 | await Tock(); 101 | let tockTicker; 102 | const tocker = new Promise(async (r)=>{ 103 | tockTicker = r; 104 | }) 105 | 106 | setTock(tocker); 107 | 108 | //create page routes 109 | const pageRoute = urlJoin("/", location.pathname, location.search); 110 | 111 | const pageURL = urlJoin( 112 | "/", 113 | location.pathname, 114 | location.search, 115 | location.hash 116 | ); 117 | 118 | 119 | let param: any; 120 | const match = urlPathReference.find(([route]) => { 121 | param = route.match(pageRoute.trim() || "/"); 122 | return param; 123 | }); 124 | 125 | 126 | StoreUpdater.set().all({ 127 | url:pageURL, 128 | param, 129 | query:queryString.parse(location.search) 130 | }) 131 | 132 | 133 | if (!match) { 134 | Application.$set({ 135 | URLSTORE, 136 | PARAMSTORE, 137 | viewProps: { 138 | NotFound: this.Component404 139 | } 140 | }); 141 | tockTicker(); 142 | } else { 143 | let [, viewInfo] = match; 144 | 145 | const ControllerConstructor = viewInfo.controller; 146 | const controllerInstance = CallInjectedController( 147 | ControllerConstructor 148 | ); 149 | 150 | const layoutPropsMethod = Reflect.getMetadata(LAYOUT_PROP,ControllerConstructor) 151 | let layoutProps = is.string(layoutPropsMethod)? this.getLayoutProps(controllerInstance,layoutPropsMethod): {}; 152 | 153 | const viewProps = this.getViewProps(controllerInstance, viewInfo).then(x=>{ 154 | return x || {}; 155 | }); 156 | 157 | const viewComponent = this.getViewComponent(ControllerConstructor, viewInfo); 158 | 159 | const templateProps: any = {}; 160 | const viewOptions = this.getControllerOptions(viewInfo); 161 | 162 | if (viewOptions) { 163 | //Need to remove this 164 | if ("layout" in viewOptions) { 165 | Object.assign(templateProps, { layout: viewOptions.layout }); 166 | } 167 | //need to remove this 168 | if ("loading" in viewOptions) { 169 | Object.assign(templateProps, { loading: viewOptions.loading }); 170 | } 171 | } 172 | 173 | if ("error" in viewOptions) { 174 | Object.assign(templateProps, { error: viewOptions.error }); 175 | } else if (this.errorPage) { 176 | Object.assign(templateProps, { error: this.errorPage }); 177 | } 178 | 179 | Object.assign(templateProps, { 180 | URLSTORE, 181 | PARAMSTORE, 182 | layout_props:layoutProps, 183 | view: viewComponent 184 | }); 185 | 186 | Object.assign(templateProps, { viewProps }); 187 | const backup = Object.assign({}, templateProps); 188 | promiseAny([ 189 | viewProps, 190 | new Promise(r => 191 | setTimeout(r, viewOptions.pause != void 0 ? viewOptions.pause : 400) 192 | ) 193 | ]) 194 | .then(async () => { 195 | 196 | try{ 197 | await Application.$set(templateProps); 198 | tockTicker(); 199 | }catch(e){ 200 | try{ 201 | Application.$destroy(); 202 | Application = CreateApplication(); 203 | await Application.$set(templateProps); 204 | tockTicker(); 205 | }catch(e){ 206 | tockTicker(); 207 | 208 | } 209 | } 210 | }) 211 | .catch(e => { 212 | console.error(e); 213 | Object.assign(backup, { 214 | viewProps: Promise.resolve(Promise.reject(e)) 215 | }); 216 | Application.$set(backup); 217 | tockTicker(); 218 | }); 219 | } 220 | }); 221 | } 222 | 223 | private getControllerOptions(ViewActionDetail: { 224 | controller: any; 225 | method: string; 226 | route: string; 227 | path: string; 228 | url: string; 229 | }) { 230 | return getMetaData(INJECT_OPTIONS, ViewActionDetail.controller) || {}; 231 | } 232 | 233 | private getViewComponent( 234 | controller: any, 235 | ViewActionDetail: { 236 | controller: any; 237 | method: string; 238 | route: string; 239 | path: string; 240 | url: string; 241 | } 242 | ) { 243 | let view = Reflect.getMetadata( 244 | VIEW_COMPONENT, 245 | controller, 246 | ViewActionDetail.method 247 | ); 248 | this.ensureViewExist(view); 249 | return view; 250 | } 251 | 252 | private getLayoutProps( 253 | Controller: any, 254 | method 255 | ) { 256 | try { 257 | return Promise.resolve( 258 | CallInjectedView(Controller, method) 259 | ); 260 | } catch (e) { 261 | return Promise.resolve(Promise.reject(e)); 262 | } 263 | } 264 | private getViewProps( 265 | Controller: any, 266 | ViewActionDetail: { 267 | controller: any; 268 | method: string; 269 | route: string; 270 | path: string; 271 | url: string; 272 | } 273 | ) { 274 | try { 275 | return Promise.resolve( 276 | CallInjectedView(Controller, ViewActionDetail.method) 277 | ); 278 | } catch (e) { 279 | return Promise.resolve(Promise.reject(e)); 280 | } 281 | } 282 | 283 | protected get controllers() { 284 | return (this.options.controllers || []).map(x => x); 285 | } 286 | 287 | private getViewNavigation(routeDetail: RouteDetails[]) { 288 | const controllerNavigation = routeDetail.map(controllerSettings => { 289 | let controller = controllerSettings.controller; 290 | let route = controllerSettings.controller_path; 291 | 292 | let routes = controllerSettings.view_detail.map(viewSettings => { 293 | if (!viewSettings) { 294 | throw new Error(`Invalid view settings on ${controller}`); 295 | } 296 | 297 | const path = viewSettings.path; 298 | const method = viewSettings.method; 299 | const fullUrl = urlJoin(route, path).replace(/\/$/, "") || "/"; 300 | 301 | return { 302 | controller, 303 | method: method, 304 | route: route, 305 | path: path, 306 | url: fullUrl 307 | }; 308 | }); 309 | 310 | return routes; 311 | }); 312 | 313 | const RouteNavigation = this.flattenRoutes(controllerNavigation); 314 | 315 | return RouteNavigation; 316 | } 317 | private flattenRoutes( 318 | controllerNavigation: { 319 | controller: any; 320 | method: string; 321 | route: string; 322 | path: string; 323 | url: string; 324 | }[][] 325 | ) { 326 | return controllerNavigation.reduce( 327 | (a, b) => { 328 | return b ? [...a, ...b] : a; 329 | }, 330 | [] as ViewDetails[] 331 | ); 332 | } 333 | 334 | private getControllerRouteDetail(controller: any) { 335 | let controller_path = Reflect.getMetadata(CONTROLLER_PATH, controller); 336 | 337 | let view_detail = Object.entries(controller.prototype) 338 | .map(([method, value]) => { 339 | if (Reflect.hasMetadata(VIEW_PATH, controller, method)) { 340 | let path: string = Reflect.getMetadata(VIEW_PATH, controller, method); 341 | return { 342 | path, 343 | method: method as string 344 | } as ViewDetails; 345 | } else { 346 | return (null as any) as ViewDetails; 347 | } 348 | }) 349 | .filter(x => !!x); 350 | 351 | const routeDetail: RouteDetails = { 352 | controller, 353 | controller_path, 354 | view_detail 355 | }; 356 | 357 | return routeDetail; 358 | } 359 | 360 | private ensureViewExist(view) { 361 | if (!view) { 362 | throw new Error(`View doesn't exist`); 363 | } 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slick For Svelte 2 | 3 | 4 | ## What does this library do for you. 5 | 6 | Manage your views and routing following a practical and easy to follow approach for your svelte components. 7 | 8 | 9 | ```ts 10 | 11 | 12 | @Controller("/user/") 13 | export class UserController { 14 | 15 | 16 | 17 | @View("/home", HomePage) 18 | async homepage(userapi: GithubApi, @Query("page") page = 1) { 19 | let users = await userapi.getPage(page); 20 | 21 | return { 22 | users: users 23 | }; 24 | } 25 | 26 | 27 | 28 | @View("/:username", UserPage) 29 | async getUserPage(api: GithubApi, @Param("username") username: string) { 30 | let user = await api.getUserByName(username); 31 | return { 32 | user 33 | }; 34 | } 35 | 36 | } 37 | ``` 38 | 39 | 40 | This library is inspired by nest and uses dependency injection to gather all the requirements 41 | needed to call a route. 42 | 43 | 44 | ### Installation 45 | 46 | 47 | ##### Before I install I just want to play with the framework and see it work. 48 | 49 | [Click Here](https://codesandbox.io/s/lflyu) 50 | 51 | ##### I rather have is all done for me, Where do I clone. 52 | 53 | [Here](https://github.com/shavyg2/slick-app-basic-setup) 54 | 55 | ##### I want to set up it up from the official svelte repo, it's gonna be more work, but I am up for it. (Proceed) 56 | 57 | 58 | Starting from the Official repo. Notice I am using the WebPack template and **not the roll up template**. 59 | ``` 60 | npx degit sveltejs/template-webpack svelte-app 61 | cd svelte-app 62 | yarn or npm install 63 | ``` 64 | 65 | ### Dependencies (YOOOU need to install them, not me) 66 | - typescript 67 | - reflect-metadata 68 | - @slick-for/svelte 69 | - history 70 | 71 | 72 | 73 | **Confused?** 74 | Paste one of the following options in your terminal inside your project folder. 75 | 76 | 1. 77 | ``` 78 | npm install reflect-metadata @slick-for/svelte history 79 | npm install --save-dev typescript 80 | ``` 81 | or 82 | 83 | 2. 84 | ``` 85 | yarn add reflect-metadata @slick-for/svelte history 86 | yarn add --dev typescript 87 | 88 | ``` 89 | 90 | 91 | **Confused?** Do number 1 92 | 93 | 94 | ### You need Typescript (Not a nice to have, Required) 95 | 96 | #### I used the Webpack Template, Did you use Rollup? You will need to investigate how to get typescript working with Rollup. 97 | #### Here is what you do for webpack, something similar is needed for rollup. 98 | 99 | 1. 100 | ``` 101 | npm install --save-dev awesome-typescript-loader 102 | ``` 103 | 104 | 2. 105 | ``` 106 | yarn add --dev awesome-typescript-loader 107 | ``` 108 | 109 | 110 | Located ```webpack.config.js``` in your project root. 111 | 112 | 113 | Here is what you need to do **Exactly** 114 | 115 | 1. Easy Way 116 | ``` 117 | Copy and Paste the config here 118 | https://github.com/shavyg2/slick-for-svelte-test/blob/master/webpack.config.js 119 | ``` 120 | 121 | 122 | 123 | 2. Non Beginner (Webpack Config scares some people.) 124 | 125 | The original file contains the following: 126 | ```js 127 | resolve: { 128 | alias: { 129 | svelte: path.resolve('node_modules', 'svelte') 130 | }, 131 | extensions: ['.mjs', '.js', '.svelte'], 132 | mainFields: ['svelte', 'browser', 'module', 'main'] 133 | }, 134 | 135 | ``` 136 | 137 | You need to add support for typescript extension (typescript extension added) 138 | ```js 139 | resolve: { 140 | alias: { 141 | svelte: path.resolve('node_modules', 'svelte') 142 | }, 143 | extensions: ['.mjs', '.js', '.svelte','.ts'], //here 144 | mainFields: ['svelte', 'browser', 'module', 'main'] 145 | }, 146 | ``` 147 | 148 | 149 | Add ```awesome-typescript-loader``` and configure it for webpack, below is a reference. 150 | ```js 151 | { 152 | test: /\.ts$/, 153 | use: { 154 | loader: 'awesome-typescript-loader', 155 | options: { 156 | transpileOnly:true //make life easy 157 | } 158 | } 159 | }, 160 | ``` 161 | 162 | 163 | Configure webpack dev server to use the ```index.html``` file for your single page application. Below is a reference. 164 | ```js 165 | devServer: { 166 | port:process.env.PORT, 167 | historyApiFallback: { 168 | index: 'index.html' 169 | } 170 | }, 171 | ``` 172 | 173 | 174 | 175 | The entry file webpack compiles need to now be a typescript file. 176 | 177 | ```js 178 | entry: { 179 | bundle: ['./src/main.ts'] 180 | }, 181 | ``` 182 | 183 | ### Typescript config 184 | 185 | Generate tsconfig file 186 | 187 | ``` 188 | npx tsc --init 189 | ``` 190 | 191 | Add the following to your ```tsconfig.json``` 192 | 193 | ```json 194 | "experimentalDecorators": true, 195 | "emitDecoratorMetadata": true, 196 | ``` 197 | 198 | 199 | I use these settings. It works lovely for me ```tsconfig.json``` 200 | ```json 201 | "target": "es5", 202 | "module": "commonjs", 203 | "lib": ["dom","esnext"], 204 | ``` 205 | 206 | 207 | [Confused??? Click this and copy paste to tsconfig.json](https://github.com/shavyg2/slick-for-svelte-test/blob/master/tsconfig.json) 208 | 209 | 210 | ### Templates 211 | 212 | Honestly just copy this and place under ```src/Template.svelte```. 213 | You can check this template file out to understand how certain things work, however you don't need to understand it. 214 | 215 | ```xhtml 216 | 224 | {#await viewProps} 225 | {#if layout} 226 | 227 | 228 | 229 | {:else} 230 | 231 | {/if} 232 | {:then props} 233 | {#if viewProps.NotFound} 234 | 235 | {:else} 236 | {#if props.layout||layout} 237 | 238 | 239 | 240 | {:else} 241 | 242 | {/if} 243 | {/if} 244 | {:catch error$} 245 | 246 | {/await} 247 | ``` 248 | 249 | This is the global template engine for all views. You shouldn't need to change it unless it is to add something application wide. 250 | Please refrain from doing this. There are better places to add customizations. 251 | 252 | 253 | 254 | 255 | ### 404 Page Sample 404 or Create your own. 256 | 257 | ```xhtml 258 | 259 |

Error 404

260 | Not found 261 | ``` 262 | 263 | 264 | 265 | ### Imports 266 | ```ts 267 | import {Controller,View,Module,Injectable,Inject,Param,Query,History} from "@slick-for/svelte"; 268 | ``` 269 | 270 | These are some of the tools that come with this library and are very common to see/use. 271 | 272 | 273 | 274 | ### Main.ts 275 | 276 | ```ts 277 | 278 | import {Module,SlickForSvelteFactory} from "@slick-for/svelte"; 279 | import {createBrowserHistory} from "history" 280 | import { UserController } from "./controller/UserController"; 281 | import { GithubApi } from "./services/github-api"; 282 | import Template from "./Template.svelte"; 283 | import Error404 from "./404.svelte" 284 | import ErrorPage from "./Error.svelte"; 285 | 286 | 287 | const history = createBrowserHistory(); 288 | @Module({ 289 | controllers:[UserController], 290 | provider:[GithubApi] 291 | }) 292 | export class ApplicationModule{ 293 | 294 | } 295 | const app = SlickForSvelteFactory.create(ApplicationModule,{ 296 | base:Template, // Remember that template i told you to keep in your back pocket, take it out. 297 | history, //https://www.npmjs.com/package/history 298 | component404:Error404, //svelte 404 Page, make it up or copy from somewhere else, 299 | error:ErrorPage, 300 | target:document.body //Where to render to https://svelte.dev/docs#Creating_a_component 301 | }) 302 | 303 | app.Initialize(); 304 | 305 | ``` 306 | 307 | This will start your application listen for the URL changes using the history API. 308 | 309 | 310 | # Wheeeew (AMAZING!!!!!) 311 | 312 | Good Job, you have done great so far. Stretch you legs. Lemme know when you are ready. 313 | I will literally be waiting for you. 314 | 315 | 316 | 317 | 318 | ### Controller (Basic) 319 | Add the missing controller file. 320 | 321 | ```ts 322 | //src/controllers/UserController.ts 323 | import { Controller, View, Param, Query } from "@slick-for/svelte"; 324 | 325 | 326 | //Regular svelte components. Confused ? https://svelte.dev/tutorial/basics 327 | import HomePage from "./pages/home.svelte"; 328 | import UserPage from "./pages/user.svelte"; 329 | 330 | 331 | //Service Component 332 | import { GithubApi } from "../services/github-api"; 333 | 334 | @Controller("/user/") 335 | export class UserController { 336 | @View("/home", HomePage) 337 | async homepage(userapi: GithubApi, @Query("page") page = 1) { 338 | let users = await userapi.getPage(page); 339 | 340 | return { 341 | users: users 342 | }; 343 | } 344 | 345 | @View("/:username", UserPage) 346 | async getUserPage(api: GithubApi, @Param("username") username: string) { 347 | let user = await api.getUserByName(username); 348 | return { 349 | user 350 | }; 351 | } 352 | } 353 | 354 | 355 | ``` 356 | 357 | ### Services 358 | The case of the missing service file. You can add it. 359 | Note that services can't have method properties automatically injected. 360 | This can only be done in the controller. However the constructor can. 361 | 362 | ```ts 363 | 364 | import { 365 | Injectable 366 | } from "@slick-for/svelte"; 367 | 368 | @Injectable() 369 | export class GithubApi { 370 | private apiUrl = "https://api.github.com"; 371 | 372 | async getPage(page: number) { 373 | let res = await fetch(`${this.apiUrl}/users?since=${page}`); 374 | let users = await this.isGood(res); 375 | return users; 376 | } 377 | 378 | async getUserByName(username: string) { 379 | let res = await fetch(`${this.apiUrl}/users/${username}`); 380 | let users = await this.isGood(res); 381 | return users; 382 | } 383 | 384 | private async isGood(res: Response) { 385 | if (res.status - 299 > 0) { 386 | let text = await res.text(); 387 | let parsed; 388 | try { 389 | parsed = JSON.parse(text); 390 | } catch (e) { 391 | throw text; 392 | } 393 | 394 | throw parsed; 395 | } else { 396 | return res.json(); 397 | } 398 | } 399 | } 400 | 401 | ``` 402 | 403 | 404 | ## Serve over http and see the result 405 | 406 | ``` 407 | yarn run dev 408 | ``` 409 | 410 | 411 | You should be seeing the 404 page now 412 | 413 | ## Need Syntax highlighting for Svelte 414 | https://marketplace.visualstudio.com/items?itemName=JamesBirtles.svelte-vscode 415 | 416 | 417 | 418 | ## Advanced (wow super star. Keep going!!) 419 | 420 | 421 | ### Controller (Advance) 422 | ``` 423 | { 424 | layout:SvelteComponent 425 | loading:SvelteComponent 426 | error:SvelteComponent 427 | pause:400 428 | 429 | } 430 | ``` 431 | 432 | 433 | 434 | #### layout 435 | 436 | This is a custom view that you should give you a page, it allows you to customize the layout for that controller. 437 | Make sure that you have a slot in that layout. This is where your view from your page will go. 438 | 439 | #### loading 440 | 441 | This is the layout that will be shown while your page your data is loading. It will use the layout while it is loading. 442 | 443 | #### error 444 | If an error happens this is the component that will be shown, it will receieve one prop and it will be called error. 445 | 446 | ### pause 447 | 448 | When you are using a single page application the layout will be able to load much faster than the data will, This will delay the transition until the data is ready. 449 | However if the data is taking a really long time (over 400ms) by default then it will switch to that page and display the loading, component. 450 | 451 | This is useful because you don't want user on really fast connection to be seeing this loading screen if it takes less than 400 ms for the data to load. 452 | If you want to increase or decrease this time however you can. The default 400 ms is good however. 453 | 454 | 455 | ```ts 456 | 457 | 458 | 459 | @Controller("/admin",{ 460 | layout:AdminComponent, 461 | loading:AdminLoadingView, 462 | error:AdminErrorView, 463 | pause:400 464 | }) 465 | class AdminController{ 466 | 467 | constructor(private user:UserService,private account:AccountService){ 468 | 469 | } 470 | 471 | @View("/",AdminMainComponent) 472 | async getMainPage(){ 473 | 474 | //This can be using writable stores in the background. 475 | const user = await this.user.getAdminUserDetails(); 476 | 477 | 478 | //Notice that i didn't await for the settings and the Admin panel details 479 | const userSettings = this.account.getUserSettings(user.id); 480 | const userPanels = this.account.getAdminPanelDetails(user.id) 481 | 482 | return { 483 | user, 484 | userSettings, 485 | userPanels, 486 | } 487 | } 488 | } 489 | ``` 490 | 491 | ```xhtml 492 | 493 |
494 | Welcome, {user.username} 495 |
496 |
497 | {#await userPanels as panels} 498 |
499 | {#each panels as panel} 500 | 501 |
502 | 503 |
504 | {/each} 505 |
506 | 507 | {:catch error} 508 |
Failed to load panels
509 | {/await} 510 | 511 | {#await userSettings as settings} 512 | 513 |
514 | 515 |
516 | {:catch error} 517 |
Settings failed to load
518 | {/await} 519 |
520 | 521 | ``` 522 | 523 | 524 | #### Back to the method 525 | ```ts 526 | ... 527 | 528 | @View("/",AdminMainComponent) 529 | async getMainPage(){ 530 | 531 | //This can be using writable stores in the background. 532 | const user = await this.user.getAdminUserDetails(); 533 | 534 | 535 | //Notice that i didn't await for the settings and the Admin panel details 536 | const userSettings = this.account.getUserSettings(user.id); 537 | const userPanels = this.account.getAdminPanelDetails(user.id) 538 | 539 | return { 540 | user, 541 | userSettings, 542 | userPanels, 543 | } 544 | } 545 | 546 | ... 547 | ``` 548 | 549 | The reason why i waited for the view to load is that i don't want the page to be shown until that date is ready. 550 | This will keep the page until the pause amount is met, at which point this will show the loading page. 551 | 552 | However if the data is resolved in a decent enough time then it will show your page. 553 | There are somethings however we don't mind waiting for and we can start rendering the page. 554 | 555 | We will wait for the user panels and the user settings and can show a loading or 556 | a grayout version while the user is waiting for the page to load. 557 | 558 | This will make your page feel more responsive, You could have also loaded all of the information inside the controller method. 559 | This is completely upto you want you want to wait for in the component and what you want your page to load with. 560 | 561 | 562 | This saves you from having to do alot of templating in the the view layer since it's best for rendering views. 563 | 564 | but for the things that you would like to wait for you have that option. 565 | 566 | 567 | 568 | 569 | ## Injectable 570 | 571 | Injectable allows you to manage your application in a way where the classes and set up code just works and you don't have to string them all together. 572 | The Library will do all of the heavy lifting but does provide you with enough options so that you can get some things dont yourself. 573 | 574 | 575 | 576 | ### Providers 577 | 578 | Providers or Injectables come in different shapes and sizes 579 | 580 | 581 | ```ts 582 | //Singleton 583 | @Injectable() 584 | class Repo{ 585 | 586 | } 587 | 588 | // a new service is created for each request or page navigation 589 | @Injectable({ 590 | scope:"Request" 591 | }) 592 | class Service{ 593 | 594 | } 595 | 596 | /** 597 | * Since this depends on something that is refreshed for each navigation it will not default to being built with every request, you can't change this. 598 | */ 599 | @Injectable() 600 | class Operation{ 601 | constructor( 602 | public service:Service, 603 | @Inject("simple") public simple, 604 | public repo:Repo 605 | ){} 606 | } 607 | 608 | 609 | //This controller will be new everytime a user goes to a different page or reloads. 610 | @Controller("/thing",{ 611 | scope:"Request", 612 | }) 613 | class TestController{ 614 | 615 | } 616 | 617 | //Need to inject some text use the @Inject Parameter decorator 618 | const simpleProvider = { 619 | provide:"simple", 620 | useValue:"Simple" 621 | } 622 | 623 | /** 624 | * Need something resolved async but you still want the actual object. Use a factory. 625 | * Notice the inject is used. 626 | * 627 | * Transient a fresh copy for every page load. 628 | * 629 | */ 630 | const factory = { 631 | provide:"factory", 632 | async useFactory(operation:Operation){ 633 | 634 | await new Promise((r)=>{ 635 | setTimeout(r,500) 636 | }) 637 | 638 | return { 639 | operation 640 | } 641 | }, 642 | inject:[Operation], 643 | scope:"Transient" 644 | } 645 | 646 | ``` --------------------------------------------------------------------------------