diff --git a/package.json b/package.json index 921df0220..21873ea3e 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,15 @@ { "name": "derby", "description": "MVC framework making it easy to write realtime, collaborative applications that run in both Node.js and browsers.", - "version": "3.0.2", + "version": "4.0.0-beta.18", "homepage": "http://derbyjs.com/", "repository": { "type": "git", "url": "git://github.com/derbyjs/derby.git" }, + "publishConfig": { + "access": "public" + }, "main": "dist/index.js", "exports": { ".": "./dist/index.js", @@ -17,8 +20,9 @@ "./AppForServer": "./dist/AppForServer.js", "./server": "./dist/server.js", "./Page": "./dist/Page.js", - "./test-utils": "./test-utils/index.js", - "./test-utils/*": "./test-utils/*.js" + "./test-utils": "./dist/test-utils/index.js", + "./test-utils/*": "./dist/test-utils/*.js", + "./file-utils": "./dist/files.js" }, "files": [ "dist/", @@ -27,12 +31,11 @@ "scripts": { "build": "node_modules/.bin/tsc", "checks": "npm run lint && npm test", - "lint": "npx eslint src/**/*.js test/**/*.js test-utils/**/*.js", + "lint": "npx eslint src/**/*.ts test/**/*.js", "lint:ts": "npx eslint src/**/*.ts", "lint:fix": "npm run lint:ts -- --fix", "prepare": "npm run build", - "pretest": "npm run build", - "test": "npx mocha 'test/all/**/*.mocha.js' 'test/dom/**/*.mocha.js' 'test/server/**/*.mocha.js'", + "test": "npx mocha -r ts-node/register 'test/all/**/*.mocha.*' 'test/dom/**/*.mocha.*' 'test/server/**/*.mocha.*'", "test-browser": "node test/server.js" }, "dependencies": { @@ -40,16 +43,20 @@ "esprima-derby": "^0.1.0", "html-util": "^0.2.3", "qs": "^6.11.0", - "racer": "^1.0.3", "resolve": "^1.22.1", "serialize-object": "^1.0.0", "tracks": "^0.5.8" }, "devDependencies": { + "@types/chai": "^4.3.11", "@types/esprima-derby": "npm:@types/esprima@^4.0.3", "@types/estree": "^1.0.1", "@types/express": "^4.17.18", + "@types/mocha": "^10.0.6", "@types/node": "^20.3.1", + "@types/qs": "^6.9.11", + "@types/resolve": "^1.20.6", + "@types/sharedb": "^3.3.10", "@typescript-eslint/eslint-plugin": "^6.2.1", "@typescript-eslint/parser": "^6.2.1", "async": "^3.2.4", @@ -63,8 +70,13 @@ "jsdom": "^20.0.1", "mocha": "^10.0.0", "prettier": "^3.0.1", + "racer": "^v2.0.0-beta.11", + "ts-node": "^10.9.2", "typescript": "~5.1.3" }, + "peerDependencies": { + "racer": "^v2.0.0-beta.8" + }, "optionalDependencies": {}, "bugs": { "url": "https://github.com/derbyjs/derby/issues" diff --git a/src/App.ts b/src/App.ts index 1564bf34f..98b5504b1 100644 --- a/src/App.ts +++ b/src/App.ts @@ -8,89 +8,112 @@ import { EventEmitter } from 'events'; import { basename } from 'path'; -import { type Model } from 'racer'; -import * as util from 'racer/lib/util'; +import { type Model, type RootModel, createModel } from 'racer'; +import { util } from 'racer'; -import components = require('./components'); +import * as components from './components'; import { type ComponentConstructor, type SingletonComponentConstructor } from './components'; import { type Derby } from './Derby'; -import { Page, type PageBase } from './Page'; +import { PageForClient, type Page } from './Page'; import { PageParams, routes } from './routes'; import * as derbyTemplates from './templates'; import { type Views } from './templates/templates'; import { checkKeyIsSafe } from './templates/util'; +declare module 'racer/lib/util' { + export let isProduction: boolean; +} + const { templates } = derbyTemplates; // TODO: Change to Map once we officially drop support for ES5. global.APPS = global.APPS || {}; -export function createAppPage(derby): typeof PageBase { - const pageCtor = ((derby && derby.Page) || Page) as typeof PageBase; +export function createAppPage(derby): typeof Page { + const pageCtor = ((derby && derby.Page) || PageForClient) as typeof Page; // Inherit from Page/PageForServer so that we can add controller functions as prototype // methods on this app's pages class AppPage extends pageCtor { } return AppPage; } -interface AppOptions { +export interface AppOptions { appMetadata?: Record, scriptHash?: string, } -type OnRouteCallback = (arg0: Page, arg1: Page, model: Model, params: PageParams, done?: () => void) => void; +type OnRouteCallback = (this: Page, page: Page, model: Model, params: PageParams, done?: () => void) => void; type Routes = [string, string, any][]; -export abstract class AppBase extends EventEmitter { + +/* + * APP EVENTS + * + 'error', Error + 'pageRendered', Page + 'destroy' + 'model', Model + 'route', Page + 'routeDone', Page, transition: boolean + 'ready', Page + 'load', Page + 'destroyPage', Page + */ + +export abstract class App extends EventEmitter { derby: Derby; name: string; filename: string; scriptHash: string; // bundledAt: string; appMetadata: Record; - Page: typeof PageBase; + Page: typeof Page; proto: any; views: Views; tracksRoutes: Routes; - model: Model; - page: PageBase; - protected _pendingComponentMap: Record; + model: RootModel; + page: Page; + protected _pendingComponentMap: Record; protected _waitForAttach: boolean; protected _cancelAttach: boolean; use = util.use; serverUse = util.serverUse; - constructor(derby, name, filename, options: AppOptions = {}) { + constructor(derby, name?: string, filename?: string, options?: AppOptions) { super(); + if (options == null) { + options = {}; + } this.derby = derby; this.name = name; this.filename = filename; this.scriptHash = options.scriptHash ?? ''; - this.appMetadata = options.appMetadata; + this.appMetadata = options.appMetadata ?? {}; this.Page = createAppPage(derby); this.proto = this.Page.prototype; this.views = new templates.Views(); this.tracksRoutes = routes(this); - this.model = null; - this.page = null; this._pendingComponentMap = {}; } abstract _init(options?: AppOptions); - loadViews(_viewFilename, _viewName) { } - loadStyles(_filename, _options) { } + loadViews(_viewFilename, _viewName?) { } + loadStyles(_filename, _options?) { } component(constructor: ComponentConstructor | SingletonComponentConstructor): this; component(name: string, constructor: ComponentConstructor | SingletonComponentConstructor, isDependency?: boolean): this; - component(name: string | ComponentConstructor | SingletonComponentConstructor, constructor?: ComponentConstructor | SingletonComponentConstructor, isDependency?: boolean): this { + component(name: string | ComponentConstructor | SingletonComponentConstructor | null, constructor?: ComponentConstructor | SingletonComponentConstructor, isDependency?: boolean): this { if (typeof name === 'function') { constructor = name; name = null; } if (typeof constructor !== 'function') { - throw new Error('Missing component constructor argument'); + if (typeof name === 'string') { + throw new Error(`Missing component constructor argument for ${name} with constructor of ${JSON.stringify(constructor)}`); + } + throw new Error(`Missing component constructor argument. Cannot use passed constructor of ${JSON.stringify(constructor)}`); } const viewProp = constructor.view; @@ -224,12 +247,12 @@ export abstract class AppBase extends EventEmitter { } } -export class App extends AppBase { - page: Page; +export class AppForClient extends App { + page: PageForClient; history: { - refresh(): void, - push(): void, - replace(): void, + push: (url: string, render?: boolean, state?: object, e?: any) => void, + replace: (url: string, render?: boolean, state?: object, e?: any) => void, + refresh: () => void, }; constructor(derby, name, filename, options: AppOptions) { @@ -241,7 +264,7 @@ export class App extends AppBase { _init(_options) { this._waitForAttach = true; this._cancelAttach = false; - this.model = new this.derby.Model(); + this.model = createModel(); const serializedViews = this._views(); serializedViews(derbyTemplates, this.views); // Must init async so that app.on('model') listeners can be added. @@ -277,7 +300,8 @@ export class App extends AppBase { this.model.unbundle(data); const page = this.createPage(); - page.params = this.model.get('$render.params'); + // @ts-expect-error TODO resolve type error + page.params = this.model.get>('$render.params'); this.emit('ready', page); this._waitForAttach = false; @@ -306,7 +330,7 @@ export class App extends AppBase { private _getAppData() { const script = this._getAppStateScript(); if (script) { - return App._parseInitialData(script.textContent); + return AppForClient._parseInitialData(script.textContent); } else { return global.APPS[this.name].initialState; } @@ -400,7 +424,7 @@ export class App extends AppBase { createPage() { this._destroyCurrentPage(); - const ClientPage = this.Page as unknown as typeof Page; + const ClientPage = this.Page as unknown as typeof PageForClient; const page = new ClientPage(this, this.model); this.page = page; return page; @@ -435,7 +459,7 @@ export class App extends AppBase { if (action === 'refreshViews') { const fn = new Function('return ' + message.views)(); // jshint ignore:line fn(derbyTemplates, this.views); - const ns = this.model.get('$render.ns'); + const ns = this.model.get('$render.ns'); this.page.render(ns); } else if (action === 'refreshStyles') { diff --git a/src/AppForServer.ts b/src/AppForServer.ts index 3d652027d..91bdc2843 100644 --- a/src/AppForServer.ts +++ b/src/AppForServer.ts @@ -6,14 +6,17 @@ * */ -import racer = require('racer'); +import * as racer from 'racer'; -const util = racer.util; -import { AppBase } from './App'; +import { App } from './App'; +import { type Derby } from './Derby'; +import { type StyleCompilerOptions } from './files'; import { PageForServer } from './PageForServer'; import parsing = require('./parsing'); import * as derbyTemplates from './templates'; +const util = racer.util; + interface Agent { send(message: Record): void; } @@ -42,7 +45,7 @@ function htmlCompiler(file) { return file; } -type CompilerFunciton = (file: string, filename?: string, options?: unknown) => unknown; +type CompilerFunction = (file: string, filename?: string, options?: unknown) => string; function watchOnce(filenames, callback) { const watcher = chokidar.watch(filenames); @@ -59,9 +62,9 @@ function watchOnce(filenames, callback) { }); } -export class AppForServer extends AppBase { +export class AppForServer extends App { agents: Record; - compilers: Record; + compilers: Record; scriptBaseUrl: string; scriptCrossOrigin: boolean; scriptFilename: string; @@ -76,7 +79,7 @@ export class AppForServer extends AppBase { watchFiles: boolean; router: any; - constructor(derby, name: string, filename: string, options) { + constructor(derby: Derby, name: string, filename: string, options) { super(derby, name, filename, options); this._init(options); } @@ -103,7 +106,7 @@ export class AppForServer extends AppBase { this.agents = null; } - private _initLoad() { + _initLoad() { this.styleExtensions = STYLE_EXTENSIONS.slice(); this.viewExtensions = VIEW_EXTENSIONS.slice(); this.compilers = util.copyObject(COMPILERS); @@ -144,7 +147,7 @@ export class AppForServer extends AppBase { // overload w different signatures, but different use cases createPage(req, res, next) { - const model = req.model || new racer.Model(); + const model = req.model || racer.createModel(); this.emit('model', model); const Page = this.Page as unknown as typeof PageForServer; @@ -159,12 +162,14 @@ export class AppForServer extends AppBase { return page; } + // @DEPRECATED bundle(_backend, _options, _cb) { throw new Error( 'bundle implementation missing; use racer-bundler for implementation, or remove call to this method and use another bundler', ); } + // @DEPRECATED writeScripts(_backend, _dir, _options, _cb) { throw new Error( 'writeScripts implementation missing; use racer-bundler for implementation, or remove call to this method and use another bundler', @@ -208,7 +213,7 @@ export class AppForServer extends AppBase { this.scriptMapUrl = (this.scriptMapBaseUrl || serialized.scriptMapBaseUrl) + serialized.scriptMapUrl; } - loadViews(filename, namespace) { + loadViews(filename: string, namespace?: string) { const data = files.loadViewsSync(this, filename, namespace); parsing.registerParsedViews(this, data.views); if (this.watchFiles) this._watchViews(data.files, filename, namespace); @@ -216,7 +221,7 @@ export class AppForServer extends AppBase { return this; } - loadStyles(filename, options) { + loadStyles(filename: string, options?: StyleCompilerOptions) { this._loadStyles(filename, options); const stylesView = this.views.find('Styles'); stylesView.source += ''; @@ -224,7 +229,7 @@ export class AppForServer extends AppBase { return this; } - private _loadStyles(filename, options) { + private _loadStyles(filename: string, options?: StyleCompilerOptions) { const styles = files.loadStylesSync(this, filename, options); let filepath = ''; diff --git a/src/Controller.ts b/src/Controller.ts index 4447af93c..5ac22d0c3 100644 --- a/src/Controller.ts +++ b/src/Controller.ts @@ -1,25 +1,26 @@ import { EventEmitter } from 'events'; -import { type Model } from 'racer'; +import { DefualtType, type ChildModel } from 'racer'; -import { type AppBase } from './App'; +import { type App } from './App'; +import { type ComponentModelData } from './components'; import { Dom } from './Dom'; -import { PageBase } from './Page'; +import { Page } from './Page'; -export class Controller extends EventEmitter { +export class Controller extends EventEmitter { dom: Dom; - app: AppBase; - page: PageBase; - model: Model; + app: App; + page: Page; + model: ChildModel; markerNode: Node; - constructor(app: AppBase, page: PageBase, model: Model) { + constructor(app: App, page: Page, model: ChildModel) { super(); this.dom = new Dom(this); this.app = app; this.model = model; this.page = page; - model.data.$controller = this; + (model.data as ComponentModelData).$controller = this; } emitCancellable(...args: unknown[]) { diff --git a/src/Derby.ts b/src/Derby.ts index 8abfc6735..a9b9d4e35 100644 --- a/src/Derby.ts +++ b/src/Derby.ts @@ -3,39 +3,27 @@ * Meant to be the entry point for the framework. * */ +import { Racer, util } from 'racer'; -import { App, type AppBase } from './App'; +import { AppForClient, type App, type AppOptions } from './App'; import { Component } from './components'; -import { Page } from './Page'; +import { PageForClient } from './Page'; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const racer = require('racer'); -// eslint-disable-next-line @typescript-eslint/no-var-requires -const Racer = require('racer/lib/Racer'); - -export abstract class DerbyBase extends Racer { +export abstract class Derby extends Racer { Component = Component; - abstract createApp(name: string, filename: string, options): AppBase + + abstract createApp(name?: string, filename?: string, options?: AppOptions): App } -export class Derby extends DerbyBase { - App = App; - Page = Page; - Model: typeof racer.Model; +export class DerbyForClient extends Derby { + App = AppForClient; + Page = PageForClient; - createApp(name: string, filename: string, options) { + createApp(name?: string, filename?: string, options?: AppOptions) { return new this.App(this, name, filename, options); } - - use(plugin, options) { - return racer.util.use.call(this, plugin, options); - } - - serverUse(plugin, options) { - return racer.util.serverUse.call(this, plugin, options); - } } -if (!racer.util.isServer) { +if (!util.isServer) { module.require('./documentListeners').add(document); } diff --git a/src/DerbyForServer.ts b/src/DerbyForServer.ts index 417a37891..f28ac15ce 100644 --- a/src/DerbyForServer.ts +++ b/src/DerbyForServer.ts @@ -1,13 +1,17 @@ -import { AppBase } from './App'; +import { util } from 'racer'; + +import { App } from './App'; import { AppForServer } from './AppForServer'; -import { DerbyBase } from './Derby'; +import { Derby } from './Derby'; import { PageForServer } from './PageForServer'; -export class DerbyForServer extends DerbyBase { - App: typeof AppForServer = AppForServer; - Page: typeof PageForServer = PageForServer; +util.isProduction = process.env.NODE_ENV === 'production'; + +export class DerbyForServer extends Derby { + App = AppForServer; + Page = PageForServer; - createApp(name: string, filename: string, options: any): AppBase { + createApp(name: string, filename: string, options: any): App { return new this.App(this, name, filename, options); } } diff --git a/src/DerbyStandalone.ts b/src/DerbyStandalone.ts deleted file mode 100644 index 82a044985..000000000 --- a/src/DerbyStandalone.ts +++ /dev/null @@ -1,33 +0,0 @@ -import Model = require('racer/lib/Model/ModelStandalone'); -import util = require('racer/lib/util'); - -import { App } from './App'; -import * as components from './components'; -import { DerbyBase } from './Derby'; -import { Page } from './Page'; - - -// eslint-disable-next-line @typescript-eslint/no-var-requires -require('./documentListeners').add(document); - -// Standard Derby inherits from Racer, but we only set up the event emitter and -// expose the Model and util here instead -export class DerbyStandalone extends DerbyBase { - Model = Model; - util = util; - - App = AppStandalone; - Page = Page; - Component = components.Component; - - createApp() { - return new this.App(this, null, null, null); - } -} - -export class AppStandalone extends App { - _init() { - this.model = new this.derby.Model(); - this.createPage(); - } -} diff --git a/src/Page.ts b/src/Page.ts index 9d6c963a3..0b7069b7e 100644 --- a/src/Page.ts +++ b/src/Page.ts @@ -1,7 +1,10 @@ -import { type Model } from 'racer'; -import util = require('racer/lib/util'); +import { + type Model, + type RootModel, + util, +} from 'racer'; -import { type AppBase, type App } from './App'; +import { type App } from './App'; import components = require('./components'); import { Controller } from './Controller'; import documentListeners = require('./documentListeners'); @@ -19,17 +22,18 @@ const { templates, } = derbyTemplates; -export abstract class PageBase extends Controller { +export abstract class Page extends Controller { + model: RootModel; params: Readonly; context: Context; - create: (model: Model, dom: any) => void; - init?: (model: Model) => void; + create: (model: Model, dom: any) => void; + init?: (model: Model) => void; _components: Record _eventModel: any; _removeModelListeners: () => void = () => {}; - page: PageBase; + page: Page; - constructor(app: AppBase, model: Model) { + constructor(app: App, model: Model) { super(app, null, model); this.params = null; this._eventModel = null; @@ -51,19 +55,19 @@ export abstract class PageBase extends Controller { return classNames.join(' '); } - get(viewName: string, ns: string, unescaped?) { + get(viewName: string, ns?: string, unescaped?) { this._setRenderPrefix(ns); const view = this.getView(viewName, ns); return view.get(this.context, unescaped); } - getFragment(viewName: string, ns: string) { + getFragment(viewName: string, ns?: string) { this._setRenderPrefix(ns); const view = this.getView(viewName, ns); return view.getFragment(this.context); } - getView(viewName: string, ns: string) { + getView(viewName: string, ns?: string) { return this.app.views.find(viewName, ns); } @@ -101,7 +105,7 @@ export abstract class PageBase extends Controller { this.model.set('$render.prefix', prefix); } - _setRenderParams(ns) { + _setRenderParams(ns?: string) { this.model.set('$render.ns', ns); this.model.set('$render.params', this.params); this.model.set('$render.url', this.params && this.params.url); @@ -109,8 +113,8 @@ export abstract class PageBase extends Controller { } } -export class Page extends PageBase { - constructor(app: App, model: Model) { +export class PageForClient extends Page { + constructor(app: App, model: Model) { super(app, model); this._addListeners(); } @@ -125,7 +129,7 @@ export class Page extends PageBase { attach() { this.context.pause(); - const ns = this.model.get('$render.ns'); + const ns = this.model.get('$render.ns'); const titleView = this.getView('TitleElement', ns); const bodyView = this.getView('BodyElement', ns); const titleElement = document.getElementsByTagName('title')[0]; @@ -166,89 +170,33 @@ export class Page extends PageBase { // a bug with binding updates where a model listener causes a change to the // path being listened on, directly or indirectly. - // TODO: Remove this when upgrading Racer to the next major version. Feature - // detect which type of event listener to register by emitting a test event - if (useLegacyListeners(model)) { - return this._addModelListenersLegacy(eventModel); - } - // `util.castSegments(segments)` is needed to cast string segments into // numbers, since EventModel#child does typeof checks against segments. This // could be done once in Racer's Model#emit, instead of in every listener. const changeListener = model.on('changeImmediate', function onChange(segments, event) { // The pass parameter is passed in for special handling of updates // resulting from stringInsert or stringRemove - segments = util.castSegments(segments.slice()); - eventModel.set(segments, event.previous, event.passed); + eventModel.set(util.castSegments(segments), event.previous, event.passed); }); + const loadListener = model.on('loadImmediate', function onLoad(segments) { - segments = util.castSegments(segments.slice()); - eventModel.set(segments); + eventModel.set(util.castSegments(segments)); }); + const unloadListener = model.on('unloadImmediate', function onUnload(segments, event) { - segments = util.castSegments(segments.slice()); - eventModel.set(segments, event.previous); + eventModel.set(util.castSegments(segments), event.previous); }); + const insertListener = model.on('insertImmediate', function onInsert(segments, event) { - segments = util.castSegments(segments.slice()); - eventModel.insert(segments, event.index, event.values.length); + eventModel.insert(util.castSegments(segments), event.index, event.values.length); }); + const removeListener = model.on('removeImmediate', function onRemove(segments, event) { - segments = util.castSegments(segments.slice()); - eventModel.remove(segments, event.index, event.values.length); - }); - const moveListener = model.on('moveImmediate', function onMove(segments, event) { - segments = util.castSegments(segments.slice()); - eventModel.move(segments, event.from, event.to, event.howMany); + eventModel.remove(util.castSegments(segments), event.index, event.values.length); }); - this._removeModelListeners = function() { - model.removeListener('changeImmediate', changeListener); - model.removeListener('loadImmediate', loadListener); - model.removeListener('unloadImmediate', unloadListener); - model.removeListener('insertImmediate', insertListener); - model.removeListener('removeImmediate', removeListener); - model.removeListener('moveImmediate', moveListener); - }; - } - - private _addModelListenersLegacy(eventModel) { - const model = this.model; - if (!model) return; - - // `util.castSegments(segments)` is needed to cast string segments into - // numbers, since EventModel#child does typeof checks against segments. This - // could be done once in Racer's Model#emit, instead of in every listener. - const changeListener = model.on('changeImmediate', function onChange(segments, eventArgs) { - // eventArgs[0] is the new value, which Derby bindings don't use directly. - // The pass parameter is passed in for special handling of updates - // resulting from stringInsert or stringRemove - const [ previous, pass ] = eventArgs; - segments = util.castSegments(segments.slice()); - eventModel.set(segments, previous, pass); - }); - const loadListener = model.on('loadImmediate', function onLoad(segments) { - segments = util.castSegments(segments.slice()); - eventModel.set(segments); - }); - const unloadListener = model.on('unloadImmediate', function onUnload(segments) { - segments = util.castSegments(segments.slice()); - eventModel.set(segments); - }); - const insertListener = model.on('insertImmediate', function onInsert(segments, eventArgs) { - const [index, values] = eventArgs; - segments = util.castSegments(segments.slice()); - eventModel.insert(segments, index, values.length); - }); - const removeListener = model.on('removeImmediate', function onRemove(segments, eventArgs) { - const [index, values] = eventArgs; - segments = util.castSegments(segments.slice()); - eventModel.remove(segments, index, values.length); - }); - const moveListener = model.on('moveImmediate', function onMove(segments, eventArgs) { - const [from, to, howMany] = eventArgs; - segments = util.castSegments(segments.slice()); - eventModel.move(segments, from, to, howMany); + const moveListener = model.on('moveImmediate', function onMove(segments, event) { + eventModel.move(util.castSegments(segments), event.from, event.to, event.howMany); }); this._removeModelListeners = function() { @@ -307,18 +255,6 @@ export class Page extends PageBase { } } -function useLegacyListeners(model) { - let useLegacy = true; - // model.once is broken in older racer, so manually remove event - const listener = model.on('changeImmediate', function(_segments, event) { - model.removeListener('changeImmediate', listener); - // Older Racer emits an array of eventArgs, whereas newer racer emits an event object - useLegacy = Array.isArray(event); - }); - model.set('$derby.testEvent', true); - return useLegacy; -} - function addDependencies(eventModel, expression, binding) { const bindingWrapper = new BindingWrapper(eventModel, expression, binding); bindingWrapper.updateDependencies(); diff --git a/src/PageForServer.ts b/src/PageForServer.ts index 719e3facb..85895ebbd 100644 --- a/src/PageForServer.ts +++ b/src/PageForServer.ts @@ -2,15 +2,21 @@ import type { Request, Response } from 'express'; import { type Model } from 'racer'; import { type AppForServer } from './AppForServer'; -import { PageBase } from './Page'; +import { Page } from './Page'; import { type PageParams } from './routes'; -export class PageForServer extends PageBase { +declare module 'racer' { + interface Model { + hasErrored?: boolean; + } +} + +export class PageForServer extends Page { req: Request; res: Response; page: PageForServer; - constructor(app: AppForServer, model: Model, req: Request, res: Response) { + constructor(app: AppForServer, model: Model, req: Request, res: Response) { super(app, model); this.req = req; this.res = res; diff --git a/src/components.ts b/src/components.ts index dfd4631c3..1b4c79703 100644 --- a/src/components.ts +++ b/src/components.ts @@ -7,11 +7,10 @@ * */ -import { type ChildModel, type ModelData } from 'racer'; -import util = require('racer/lib/util'); +import { type ChildModel, util } from 'racer'; import { Controller } from './Controller'; -import { PageBase } from './Page'; +import { Page } from './Page'; import derbyTemplates = require('./templates'); import { Context } from './templates/contexts'; import { Expression } from './templates/expressions'; @@ -26,58 +25,50 @@ export interface DataConstructor extends Record { type AnyVoidFunction = (...args: any[]) => void; -export interface ComponentConstructor { - new(context: Context, data: ModelData): Component; +export interface ComponentConstructor { + new(context: Context, data: Record): Component; DataConstructor?: DataConstructor; - singleton?: boolean, - view?: { - dependencies?: any[], - file?: string, - is: string, - source?: string, - viewPartialDependencies?: string[], - } + singleton?: undefined, + view?: ComponentViewDefinition, } export interface SingletonComponentConstructor { - new(): Component + new(): object; singleton: true; - view?: { - is: string, - dependencies?: ComponentConstructor[], - source?: string, - file?: string, - } + view?: ComponentViewDefinition +} + +export interface ComponentViewDefinition { + dependencies?: Array, + file?: string, + is?: string, + source?: string, + viewPartialDependencies?: Array, } -export abstract class Component extends Controller { +export abstract class Component extends Controller { context: Context; id: string; isDestroyed: boolean; - page: PageBase; + page: Page; parent: Controller; singleton?: true; _scope: string[]; // new style view prop - view?: { - dependencies: ComponentConstructor[], - file: string, - is: string, - source: string, - } + view?: ComponentViewDefinition; static DataConstructor?: DataConstructor; - constructor(context: Context, data: ModelData) { + constructor(context: Context, data: Record) { const parent = context.controller; const id = context.id(); const scope = ['$components', id]; - const model = parent.model.root.eventContext(id); + const model = parent.model.root.eventContext(id) as ChildModel; model._at = scope.join('.'); data.id = id; model._set(scope, data); // Store a reference to the component's scope such that the expression // getters are relative to the component - model.data = data; + model.data = data as T; // IMPORTANT: call super _after_ model created super(context.controller.app, context.controller.page, model); @@ -92,7 +83,7 @@ export abstract class Component extends Controller { this.isDestroyed = false; } - init(_model: ChildModel): void {} + init(_model: ChildModel): void {} destroy() { this.emit('destroy'); @@ -344,19 +335,19 @@ export abstract class Component extends Controller { } } -function _safeWrap(component: Component, callback: () => void) { +function _safeWrap(component: Component, callback: () => void) { return function() { if (component.isDestroyed) return; callback.call(component); }; } -export class ComponentAttribute { +export class ComponentAttribute { expression: Expression; - model: ChildModel; + model: ChildModel; key: string; - constructor(expression: Expression, model: ChildModel, key: string) { + constructor(expression: Expression, model: ChildModel, key: string) { checkKeyIsSafe(key); this.expression = expression; this.model = model; @@ -384,7 +375,7 @@ export class ComponentAttributeBinding extends Binding { } } -function setModelAttributes(context: Context, model: ChildModel) { +function setModelAttributes(context: Context, model: ChildModel) { const attributes = context.parent.attributes; if (!attributes) return; // Set attribute values on component model @@ -394,7 +385,7 @@ function setModelAttributes(context: Context, model: ChildModel) } } -function setModelAttribute(context: Context, model: ChildModel, key: string, value: unknown) { +function setModelAttribute(context: Context, model: ChildModel, key: string, value: unknown) { // If an attribute is an Expression, set its current value in the model // and keep it up to date. When it is a resolvable path, use a Racer ref, // which makes it a two-way binding. Otherwise, set to the current value @@ -435,10 +426,10 @@ function setModelAttribute(context: Context, model: ChildModel, k model.set(key, value); } -export function createFactory(constructor: ComponentConstructor) { +export function createFactory(constructor: ComponentConstructor | SingletonComponentConstructor) { // DEPRECATED: constructor.prototype.singleton is deprecated. "singleton" // static property on the constructor is preferred - return (constructor.singleton || constructor.prototype.singleton) ? + return (constructor.singleton === true) ? new SingletonComponentFactory(constructor) : new ComponentFactory(constructor); } @@ -451,12 +442,15 @@ function emitInitHooks(context, component) { } } -class ComponentModelData { - id = null; - $controller = null; +export class ComponentModelData { + id: string; + $controller: Controller; + $element: any; + $event: any; + [key: string]: unknown; } -export class ComponentFactory{ +export class ComponentFactory { constructorFn: ComponentConstructor; constructor(constructorFn: ComponentConstructor) { @@ -513,7 +507,7 @@ class SingletonComponentFactory{ init(context) { // eslint-disable-next-line new-cap - if (!this.component) this.component = new this.constructorFn(); + if (!this.component) this.component = new this.constructorFn() as Component; return context.componentChild(this.component); } diff --git a/src/files.js b/src/files.js deleted file mode 100644 index 8055d0150..000000000 --- a/src/files.js +++ /dev/null @@ -1,82 +0,0 @@ -/* - * files.js - * load templates and styles from disk - * - */ - -var fs = require('fs'); -var path = require('path'); -var util = require('racer/lib/util'); -var resolve = require('resolve'); -var parsing = require('./parsing'); - -exports.loadViewsSync = loadViewsSync; -exports.loadStylesSync = loadStylesSync; - -function loadViewsSync(app, sourceFilename, namespace) { - var views = []; - var files = []; - var filename = resolve.sync(sourceFilename, { - extensions: app.viewExtensions, - packageFilter: deleteMain} - ); - if (!filename) { - throw new Error('View template file not found: ' + sourceFilename); - } - - var file = fs.readFileSync(filename, 'utf8'); - - var extension = path.extname(filename); - var compiler = app.compilers[extension]; - if (!compiler) { - throw new Error('Unable to find compiler for: ' + extension); - } - - function onImport(attrs) { - var dir = path.dirname(filename); - var importFilename = resolve.sync(attrs.src, { - basedir: dir, - extensions: app.viewExtensions, - packageFilter: deleteMain - }); - var importNamespace = parsing.getImportNamespace(namespace, attrs, importFilename); - var imported = loadViewsSync(app, importFilename, importNamespace); - views = views.concat(imported.views); - files = files.concat(imported.files); - } - - var htmlFile = compiler(file, filename); - var parsedViews = parsing.parseViews(htmlFile, namespace, filename, onImport); - return { - views: views.concat(parsedViews), - files: files.concat(filename) - }; -} - -function loadStylesSync(app, sourceFilename, options) { - if (options == null) { - options = { compress: util.isProduction }; - } - var resolved = resolve.sync(sourceFilename, { - extensions: app.styleExtensions, - packageFilter: deleteMain} - ); - if (!resolved) { - throw new Error('Style file not found: ' + sourceFilename); - } - var extension = path.extname(resolved); - var compiler = app.compilers[extension]; - if (!compiler) { - throw new Error('Unable to find compiler for: ' + extension); - } - var file = fs.readFileSync(resolved, 'utf8'); - return compiler(file, resolved, options); -} - -// Resolve will use a main path from a package.json if found. Main is the -// entry point for javascript in a module, so this will mistakenly cause us to -// load the JS file instead of a view or style file in some cases. This package -// filter deletes the main property so that the normal file name lookup happens -function deleteMain() { - return {}; -} diff --git a/src/files.ts b/src/files.ts new file mode 100644 index 000000000..d812df69e --- /dev/null +++ b/src/files.ts @@ -0,0 +1,85 @@ +/* + * files.ts + * load templates and styles from disk + * + */ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import * as racer from 'racer'; +import * as resolve from 'resolve'; + +import { type AppForServer } from './AppForServer'; +import * as parsing from './parsing'; + +export function loadViewsSync(app: AppForServer, sourceFilename: string, namespace: string) { + let views = []; + let files = []; + const filename = resolve.sync(sourceFilename, { + extensions: app.viewExtensions, + packageFilter: deleteMain} + ); + if (!filename) { + throw new Error('View template file not found: ' + sourceFilename); + } + + const file = fs.readFileSync(filename, 'utf8'); + + const extension = path.extname(filename); + const compiler = app.compilers[extension]; + if (!compiler) { + throw new Error('Unable to find compiler for: ' + extension); + } + + function onImport(attrs) { + const dir = path.dirname(filename); + const importFilename = resolve.sync(attrs.src, { + basedir: dir, + extensions: app.viewExtensions, + packageFilter: deleteMain + }); + const importNamespace = parsing.getImportNamespace(namespace, attrs, importFilename); + const imported = loadViewsSync(app, importFilename, importNamespace); + views = views.concat(imported.views); + files = files.concat(imported.files); + } + + const htmlFile = compiler(file, filename) as string; + const parsedViews = parsing.parseViews(htmlFile, namespace, filename, onImport); + return { + views: views.concat(parsedViews), + files: files.concat(filename) + }; +} + +export interface StyleCompilerOptions extends Record { + compress?: boolean; +} + +export function loadStylesSync(app: AppForServer, sourceFilename: string, options?: StyleCompilerOptions) { + if (options == null) { + options = { compress: racer.util.isProduction }; + } + const resolved = resolve.sync(sourceFilename, { + extensions: app.styleExtensions, + packageFilter: deleteMain} + ); + if (!resolved) { + throw new Error('Style file not found: ' + sourceFilename); + } + const extension = path.extname(resolved); + const compiler = app.compilers[extension]; + if (!compiler) { + throw new Error('Unable to find compiler for: ' + extension); + } + const file = fs.readFileSync(resolved, 'utf8'); + return compiler(file, resolved, options); +} + +// Resolve will use a main path from a package.json if found. Main is the +// entry point for javascript in a module, so this will mistakenly cause us to +// load the JS file instead of a view or style file in some cases. This package +// filter deletes the main property so that the normal file name lookup happens +function deleteMain() { + return {}; +} diff --git a/src/index.ts b/src/index.ts index ca44af633..cdd786761 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,36 @@ -import { Derby } from './Derby'; +import { util } from 'racer'; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const util = require('racer').util; +import { type AppOptions } from './App'; +import { DerbyForClient, type Derby } from './Derby'; + +export { AppForClient, App } from './App'; +export type { AppForServer } from './AppForServer'; +export { Dom } from './Dom'; +export { Page, PageForClient } from './Page'; +export type { PageForServer } from './PageForServer'; +export { + Component, + ComponentModelData, + type ComponentConstructor, + type ComponentViewDefinition, +} from './components'; +export { type Context } from './templates/contexts'; +export { type PageParams, type QueryParams } from './routes'; const DerbyClass = util.isServer ? util.serverRequire(module, './DerbyForServer').DerbyForServer - : Derby; -export = new DerbyClass(); + : DerbyForClient; +const instance: Derby = new DerbyClass(); + +export function createApp(name?: string, file?: string, options?: AppOptions) { + return instance.createApp(name, file, options); +} + +export function use(plugin: (derby: Derby, options?: T) => Derby, options?: T) { + return instance.use(plugin, options); +} + +export { + DerbyForClient as Derby, + util, +} diff --git a/src/parsing/index.ts b/src/parsing/index.ts index cf1ee28d3..e21a66758 100644 --- a/src/parsing/index.ts +++ b/src/parsing/index.ts @@ -4,7 +4,7 @@ import htmlUtil = require('html-util'); import { createPathExpression } from './createPathExpression'; import { markup } from './markup'; -import { App, AppBase } from '../App'; +import { App } from '../App'; import { templates, expressions } from '../templates'; import { Expression } from '../templates/expressions'; import { MarkupHook, View } from '../templates/templates'; @@ -14,7 +14,7 @@ export { createPathExpression } from './createPathExpression'; export { markup } from './markup'; declare module '../App' { - interface App { + interface AppForClient { addViews(file: string, namespace: string): void; } } @@ -947,7 +947,7 @@ export function parseViews(file: string, namespace: string, filename?: string, o return views; } -export function registerParsedViews(app: AppBase, items: ParsedView[]) { +export function registerParsedViews(app: App, items: ParsedView[]) { for (let i = 0, len = items.length; i < len; i++) { const item = items[i]; app.views.register(item.name, item.source, item.options); diff --git a/src/routes.ts b/src/routes.ts index f76c3287a..8a6b5344b 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -1,10 +1,10 @@ -import { type Model } from 'racer'; +import { RootModel, type Model } from 'racer'; import tracks = require('tracks'); -import { type AppBase } from './App'; -import { type PageBase } from './Page'; +import { type App } from './App'; +import { type Page } from './Page'; -export function routes(app: AppBase) { +export function routes(app: App) { return tracks.setup(app); } @@ -39,13 +39,13 @@ export interface RouteMethod { } export interface RouteHandler { - (page: PageBase, model: Model, params: PageParams, next: (err?: Error) => void): void; + (page: Page, model: RootModel, params: PageParams, next: (err?: Error) => void): void; } export interface TransitionalRouteHandler { ( - page: PageBase, - model: Model, + page: Page, + model: RootModel, params: PageParams, next: (err?: Error) => void, done: () => void @@ -53,7 +53,7 @@ export interface TransitionalRouteHandler { } declare module './App' { - interface AppBase { + interface App { del: RouteMethod; get: RouteMethod; history: { @@ -67,7 +67,7 @@ declare module './App' { } declare module './Page' { - interface PageBase { + interface Page { redirect(url: string, status?: number): void; } } diff --git a/src/templates/expressions.ts b/src/templates/expressions.ts index 4622e1886..9124cd248 100644 --- a/src/templates/expressions.ts +++ b/src/templates/expressions.ts @@ -8,7 +8,7 @@ import { checkKeyIsSafe, concat } from './util'; import { Component } from '../components'; type SegmentOrContext = string | number | { item: number } | Context; -type Segment = string | number; +type Segment = string; type Value = any; // global | Page | ModelData export function lookup(segments: Segment[] | undefined, value: Value) { @@ -142,7 +142,7 @@ export class Expression { module = 'expressions'; type = 'Expression'; meta?: ExpressionMeta; - segments: Array; + segments: string[]; constructor(meta?: ExpressionMeta) { this.meta = meta; diff --git a/src/templates/templates.ts b/src/templates/templates.ts index 98270c76d..74274861e 100644 --- a/src/templates/templates.ts +++ b/src/templates/templates.ts @@ -7,7 +7,7 @@ import { type Context } from './contexts'; import { DependencyOptions } from './dependencyOptions'; import { type Expression } from './expressions'; import { checkKeyIsSafe, concat, hasKeys, traverseAndCreate } from './util'; -import { Component } from '../components'; +import { Component, ComponentModelData } from '../components'; import { Controller } from '../Controller'; export type Attributes = Record; @@ -2278,7 +2278,7 @@ export class ElementOn extends MarkupHook { } apply(context: Context, element: any, event?: any) { - const modelData = context.controller.model.data; + const modelData = context.controller.model.data as ComponentModelData; modelData.$event = event; modelData.$element = element; const out = this.expression.apply(context); diff --git a/src/test-utils/ComponentHarness.ts b/src/test-utils/ComponentHarness.ts new file mode 100644 index 000000000..7f71d9494 --- /dev/null +++ b/src/test-utils/ComponentHarness.ts @@ -0,0 +1,315 @@ +import { EventEmitter } from 'events'; +import { parse as urlParse } from 'url'; + +import * as qs from 'qs'; +import { RootModel } from 'racer'; + +import { AppForClient } from '../App'; +import { AppForServer } from '../AppForServer'; +import { Component, ComponentConstructor, extendComponent, createFactory } from '../components'; +import { DerbyForClient } from '../Derby'; +import { PageForClient } from '../Page'; + +export interface RenderOptions { + url?: string; +} + +export class PageForHarness extends PageForClient { + component?: Component; + fragment?: any; + html?: any; +} + +const derby = new DerbyForClient(); + +export class AppForHarness extends AppForClient { + _harness: any; + _pages: PageForHarness[]; + page: PageForHarness; + Page = PageForHarness; + + constructor(harness) { + super(derby, 'ComponentHarness_App', '', {}); + this._harness = harness; + this._pages = []; + // required for calling funcitons from views + this.proto = PageForHarness.prototype; + } + + createPage(): PageForHarness { + const page = new this.Page(this, this._harness.model); + this._pages.push(page); + return page; + } + + // Load views by filename. The client version of this method is a no-op + loadViews(...args) { + AppForServer.prototype.loadViews.call(this, ...args); + } + + // `_init()` does setup for loading views from files on the server and loading + // serialized views and data on the client + _init() { + this._initLoad(); + } + + // Register default compilers so that AppForHarness can load views & styles from + // the filesystem + _initLoad() { + AppForServer.prototype._initLoad.call(this); + } +} + +/** + * Creates a `ComponentHarness`. + * + * If arguments are provided, then `#setup` is called with the arguments. + */ +export class ComponentHarness extends EventEmitter { + app: AppForHarness; + document: Document; + model: RootModel; + page: PageForHarness; + + constructor() { + super(); + this.app = new AppForHarness(this); + this.model = new RootModel(); + + if (arguments.length > 0) { + // eslint-disable-next-line prefer-spread, prefer-rest-params + this.setup.apply(this, arguments); + } + } + + /** @typedef { {view: {is: string, source?: string}} } InlineComponent */ + /** + * Sets up the harness with a HTML template, which should contain a `` for the + * component under test, and the components to register for the test. + * + * @param {string} source - HTML template for the harness page + * @param {...(Component | InlineComponent} components - components to register for the test + * + * @example + * var harness = new ComponentHarness().setup('', Dialog); + */ + setup(source: string, ...components: ComponentConstructor[]) { + this.app.views.register('$harness', source); + // Remaining variable arguments are components + components.forEach(constructor => this.app.component(constructor)); + return this; + } + + /** + * Stubs out view names with empty view or the provided source. + * + * A view name is a colon-separated string of segments, as used in ``. + * + * @example + * var harness = new ComponentHarness('', Dialog).stub( + * 'icons:open-icon', + * 'icons:close-icon', + * {is: 'dialog:buttons', source: ''} + * ); + */ + stub(...args: Array) { + for (let i = 0; i < args.length; i++) { + // eslint-disable-next-line prefer-rest-params + const arg = arguments[i]; + if (typeof arg === 'string') { + this.app.views.register(arg, ''); + } else if (arg && arg.is) { + this.app.views.register(arg.is, arg.source || ''); + } else { + throw new Error('each argument must be the name of a view or an object with an `is` property'); + } + } + return this; + } + + /** + * Stubs out view names as components. + * + * This can be used to test the values being bound to ("passed into") child components. + * + * @example + * var harness = new ComponentHarness('', Dialog) + * .stubComponent('common:file-picker', {is: 'footer', as: 'stubFooter'}); + */ + stubComponent(...args: Array) { + for (let i = 0; i < args.length; i++) { + // eslint-disable-next-line prefer-rest-params + const arg = arguments[i]; + const options = (typeof arg === 'string') ? {is: arg} : arg; + const Stub = createStubComponent(options); + this.app.component(Stub); + } + return this; + } + + /** + * @typedef {Object} RenderOptions + * @property {string} [url] - Optional URL for the render, used to populate `page.params` + */ + /** + * Renders the harness into a HTML string, as server-side rendering would do. + * + * @param {RenderOptions} [options] + * @returns { Page & {html: string} } - a `Page` that has a `html` property with the rendered HTML + * string + */ + renderHtml(options) { + return this._get(function(page) { + page.html = page.get('$harness'); + }, options); + } + + /** + * Renders the harness into a `DocumentFragment`, as client-side rendering would do. + * + * @param {RenderOptions} [options] + * @returns { Page & {fragment: DocumentFragment} } a `Page` that has a `fragment` property with the + * rendered `DocumentFragment` + */ + renderDom(options) { + return this._get(function(page) { + page.fragment = page.getFragment('$harness'); + }, options); + } + + attachTo(parentNode, node) { + return this._get(function(page) { + const view = page.getView('$harness'); + const targetNode = node || parentNode.firstChild; + view.attachTo(parentNode, targetNode, page.context); + }); + } + + /** + * @param {(page: PageForHarness) => void} render + * @param {RenderOptions} [options] + */ + _get(renderFn: (page: PageForHarness) => void, options?): PageForHarness { + options = options || {}; + const url = options.url || ''; + + const page = this.app.createPage(); + // Set `page.params`, which is usually created in tracks during `Page#render`: + // https://github.com/derbyjs/tracks/blob/master/lib/index.js + function setPageUrl(url) { + page.params = { + url: url, + query: qs.parse(urlParse(url).query), + // @ts-expect-error 'body' does not exist in type 'Readonly' + body: {}, + }; + // Set "$render.params", "$render.query", "$render.url" based on `page.params`. + page._setRenderParams(); + } + setPageUrl(url); + // Fake some methods from tracks/lib/History.js. + // JSDOM doesn't really support updating the window URL, but this should work for Derby code that + // pulls URL info from the model or page. + this.app.history = { push: setPageUrl, replace: setPageUrl, refresh: () => {} }; + + // The `#render` assertion in assertions.js wants to compare the results of HTML and DOM + // rendering, to make sure they match. However, component `create()` methods can modify the DOM + // immediately after initial rendering, which can break assertions. + // + // To get around this, we trigger a "pageRendered" event on the harness before `create()` methods + // get called. This is done by pausing the context, which prevents create() methods from getting + // called until the pause-count drops to 0. + page.context.pause(); + renderFn(page); + this.emit('pageRendered', page); + page.context.unpause(); + + // HACK: Implement getting an instance as a side-effect of rendering. This + // code relies on the fact that while rendering, components are instantiated, + // and a reference is kept on page._components. Since we just created the + // page, we can reliably return the first component. + // + // The more standard means for getting a reference to a component controller + // would be to add a hooks in the view with `as=` or `on-init=`. However, we + // want the developer to pass this view in, so they can supply whatever + // harness context they like. + // + // This may need to be updated if the internal workings of Derby change. + page.component = page._components._1; + return page; + } + + /** + * Instantiates a component and calls its `init()` if present, without rendering it. + * + * This can be used in place of `new MyComponent()` in old tests that were written prior to the + * component test framework being developed. + * + * @param Ctor - class (constructor) for the component to instantiate + * @param rootModel - a root model + * @returns a newly instantiated component, with its `init()` already called if present + */ + createNonRenderedComponent(Ctor: ComponentConstructor, rootModel: RootModel) { + // If the component doesn't already extend Component, then do so. + // This normally happens when calling `app.component(Ctor)` to register a component: + // https://github.com/derbyjs/derby/blob/2ababe7c805c59e51ddef0153cb8c5f6b66dd4ce/lib/App.js#L278-L279 + extendComponent(Ctor); + + // Mimic Derby's component creation process: + // createFactory: https://github.com/derbyjs/derby/blob/57d28637e8489244cc8438041e6c61d9468cd344/lib/components.js#L346 + // Factory#init: https://github.com/derbyjs/derby/blob/57d28637e8489244cc8438041e6c61d9468cd344/lib/components.js#L370-L392 + // new Page: https://github.com/derbyjs/derby/blob/57d28637e8489244cc8438041e6c61d9468cd344/lib/Page.js#L15 + // Context#controller: https://github.com/derbyjs/derby-templates/blob/master/lib/contexts.js#L19-L25 + return createFactory(Ctor).init(new PageForClient(this.app, rootModel).context).controller; + } + + static createStubComponent(options) { + return createStubComponent(options); + } +} + +function createStubComponent(options) { + const as = options.as || options.is; + const asArray = options.asArray; + + class StubComponent extends Component { + static view = { + is: options.is, + file: options.file, + source: options.source, + dependencies: options.dependencies + }; + + init() { + if (asArray) { + pageArrayInit.call(this); + } else { + pageInit.call(this); + } + } + } + + function pageArrayInit() { + const page = this.page; + if (page[asArray]) { + page[asArray].push(this); + } else { + page[asArray] = [this]; + } + this.on('destroy', () => { + const index = page[asArray].indexOf(this); + if (index === -1) return; + page[asArray].splice(index, 1); + }); + } + + function pageInit() { + const page = this.page; + page[as] = this; + this.on('destroy', function() { + page[as] = undefined; + }); + } + + return (StubComponent as unknown) as ComponentConstructor; +} diff --git a/test-utils/assertions.js b/src/test-utils/assertions.ts similarity index 74% rename from test-utils/assertions.js rename to src/test-utils/assertions.ts index 6437e0cdf..c20a0a9a2 100644 --- a/test-utils/assertions.js +++ b/src/test-utils/assertions.ts @@ -1,4 +1,16 @@ -var ComponentHarness = require('./ComponentHarness'); +import { ComponentHarness } from './ComponentHarness'; + +import 'chai'; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace, @typescript-eslint/no-unused-vars + export namespace Chai { + interface Assertion { + html(expectedText: string | undefined, options): void; + render(expectedText: string | undefined, options): void; + } + } +} /** * @param { {window: Window } } [dom] - _optional_ - An object that will have a `window` property @@ -6,37 +18,37 @@ var ComponentHarness = require('./ComponentHarness'); * @param {Assertion} [chai.Assertion] - _optional_ - Chai's Assertion class. If provided, the * chainable expect methods `#html(expected)` and `#render(expected)` will be added to Chai. */ -module.exports = function(dom, Assertion) { - var getWindow = dom ? +export function assertions(dom, Assertion) { + const getWindow = dom ? function() { return dom.window; } : function() { return window; }; function removeComments(node) { - var domDocument = getWindow().document; - var clone = domDocument.importNode(node, true); + const domDocument = getWindow().document; + const clone = domDocument.importNode(node, true); // last two arguments for createTreeWalker are required in IE // NodeFilter.SHOW_COMMENT === 128 - var treeWalker = domDocument.createTreeWalker(clone, 128, null, false); - var toRemove = []; - for (var item = treeWalker.nextNode(); item != null; item = treeWalker.nextNode()) { + const treeWalker = domDocument.createTreeWalker(clone, 128, null, false); + const toRemove = []; + for (let item = treeWalker.nextNode(); item != null; item = treeWalker.nextNode()) { toRemove.push(item); } - for (var i = toRemove.length; i--;) { + for (let i = toRemove.length; i--;) { toRemove[i].parentNode.removeChild(toRemove[i]); } return clone; } function getHtml(node, parentTag) { - var domDocument = getWindow().document; + const domDocument = getWindow().document; // We use the element, because it has a transparent content model: // https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories#Transparent_content_model // // In practice, DOM validity isn't enforced by browsers when using // appendChild and innerHTML, so specifying a valid parentTag for the node // should not be necessary - var el = domDocument.createElement(parentTag || 'ins'); - var clone = domDocument.importNode(node, true); + const el = domDocument.createElement(parentTag || 'ins'); + const clone = domDocument.importNode(node, true); el.appendChild(clone); return el.innerHTML; } @@ -48,8 +60,8 @@ module.exports = function(dom, Assertion) { // model's state between the rendering passes. function resetPageModel(page) { page._removeModelListeners(); - for (var componentId in page._components) { - var component = page._components[componentId]; + for (const componentId in page._components) { + const component = page._components[componentId]; component.destroy(); } page.model.silent().destroy('$components'); @@ -57,16 +69,16 @@ module.exports = function(dom, Assertion) { if (Assertion) { Assertion.addMethod('html', function(expected, options) { - var obj = this._obj; - var includeComments = options && options.includeComments; - var parentTag = options && options.parentTag; - var domNode = getWindow().Node; + const obj = this._obj; + const includeComments = options && options.includeComments; + const parentTag = options && options.parentTag; + const domNode = getWindow().Node; new Assertion(obj).instanceOf(domNode); new Assertion(expected).is.a('string'); - var fragment = (includeComments) ? obj : removeComments(obj); - var html = getHtml(fragment, parentTag); + const fragment = (includeComments) ? obj : removeComments(obj); + const html = getHtml(fragment, parentTag); this.assert( html === expected, @@ -78,20 +90,20 @@ module.exports = function(dom, Assertion) { }); Assertion.addMethod('render', function(expected, options) { - var harness = this._obj; + const harness = this._obj; if (expected && typeof expected === 'object') { options = expected; expected = null; } - var domDocument = getWindow().document; - var parentTag = (options && options.parentTag) || 'ins'; - var firstFailureMessage, actual; + const domDocument = getWindow().document; + const parentTag = (options && options.parentTag) || 'ins'; + let firstFailureMessage, actual; new Assertion(harness).instanceOf(ComponentHarness); // Render to a HTML string. - var renderResult = harness.renderHtml(options); - var htmlString = renderResult.html; + let renderResult = harness.renderHtml(options); + const htmlString = renderResult.html; // Normalize `htmlString` into the same form as the DOM would give for `element.innerHTML`. // @@ -99,9 +111,9 @@ module.exports = function(dom, Assertion) { // HTML entities like ' ' to their corresponding Unicode characters. However, for this // assertion, if the `expected` string is provided, it will not have that same transformation. // To make the assertion work properly, normalize the actual `htmlString`. - var html = normalizeHtml(htmlString); + const html = normalizeHtml(htmlString); - var htmlRenderingOk; + let htmlRenderingOk; if (expected == null) { // If `expected` is not provided, then we skip this check. // Set `expected` as the normalized HTML string for subsequent checks. @@ -125,7 +137,7 @@ module.exports = function(dom, Assertion) { // Check DOM rendering is also equivalent. // This uses the harness "pageRendered" event to grab the rendered DOM *before* any component // `create()` methods are called, as `create()` methods can do DOM mutations. - var domRenderingOk; + let domRenderingOk; harness.once('pageRendered', function(page) { try { new Assertion(page.fragment).html(expected, options); @@ -142,10 +154,10 @@ module.exports = function(dom, Assertion) { resetPageModel(renderResult.page); // Try attaching. Attachment will throw an error if HTML doesn't match - var el = domDocument.createElement(parentTag); + const el = domDocument.createElement(parentTag); el.innerHTML = htmlString; - var innerHTML = el.innerHTML; - var attachError; + const innerHTML = el.innerHTML; + let attachError; try { harness.attachTo(el); } catch (err) { @@ -156,7 +168,7 @@ module.exports = function(dom, Assertion) { actual = innerHTML; } } - var attachOk = !attachError; + const attachOk = !attachError; // TODO: Would be nice to add a diff of the expected and actual HTML this.assert( @@ -180,8 +192,8 @@ module.exports = function(dom, Assertion) { * - Certain single characters, e.g. ">" or a non-breaking space, are converted * into their escaped HTML entity forms, e.g. ">" or " ". */ - var normalizeHtml = function(html) { - var normalizerElement = window.document.createElement('ins'); + const normalizeHtml = function(html) { + const normalizerElement = window.document.createElement('ins'); normalizerElement.innerHTML = html; return normalizerElement.innerHTML; }; @@ -191,4 +203,4 @@ module.exports = function(dom, Assertion) { removeComments: removeComments, getHtml: getHtml }; -}; +} diff --git a/test-utils/domTestRunner.js b/src/test-utils/domTestRunner.ts similarity index 54% rename from test-utils/domTestRunner.js rename to src/test-utils/domTestRunner.ts index 5065fa777..c53ea0176 100644 --- a/test-utils/domTestRunner.js +++ b/src/test-utils/domTestRunner.ts @@ -1,55 +1,53 @@ -var util = require('racer').util; -var registerAssertions = require('./assertions'); -var ComponentHarness = require('./ComponentHarness'); +import { util } from 'racer'; -var runner = new DomTestRunner(); -// Set up Chai assertion chain methods: `#html` and `#render` -registerAssertions(runner, require('chai').Assertion); +import { assertions as registerAssertions } from './assertions'; +import { ComponentHarness } from './ComponentHarness'; -exports.install = function(options) { - runner.installMochaHooks(options); - return runner; -}; +export class DomTestRunner{ + window?: any; + document?: any; + harnesses: ComponentHarness[]; -exports.DomTestRunner = DomTestRunner; -function DomTestRunner() { - this.window = null; - this.document = null; - this.harnesses = []; -} + constructor() { + this.window = null; + this.document = null; + this.harnesses = []; + } -DomTestRunner.prototype.installMochaHooks = function(options) { - options = options || {}; - var jsdomOptions = options.jsdomOptions; + installMochaHooks(options) { + options = options || {}; + const jsdomOptions = options.jsdomOptions; - // Set up runner's `window` and `document`. - if (util.isServer) { - mochaHooksForNode(this, { - jsdomOptions: jsdomOptions - }); - } else { - mochaHooksForBrowser(this); + // Set up runner's `window` and `document`. + if (util.isServer) { + mochaHooksForNode(this, { + jsdomOptions: jsdomOptions + }); + } else { + mochaHooksForBrowser(this); + } } -}; -DomTestRunner.prototype.createHarness = function() { - var harness = new ComponentHarness(); - if (arguments.length > 0) { - harness.setup.apply(harness, arguments); + createHarness() { + const harness = new ComponentHarness(); + if (arguments.length > 0) { + // eslint-disable-next-line prefer-spread, prefer-rest-params + harness.setup.apply(harness, arguments); + } + this.harnesses.push(harness); + return harness; } - this.harnesses.push(harness); - return harness; -}; +} function mochaHooksForNode(runner, options) { - var jsdomOptions = options.jsdomOptions; + const jsdomOptions = options.jsdomOptions; // Use an indirect require so that Browserify doesn't try to bundle JSDOM. - var JSDOM = util.serverRequire(module, 'jsdom').JSDOM; + const JSDOM = util.serverRequire(module, 'jsdom').JSDOM; - var nodeGlobal = global; + const nodeGlobal = global; // Keep a direct reference so that we're absolutely sure we clean up our own JSDOM. - var jsdom; + let jsdom; global.beforeEach(function() { jsdom = new JSDOM('', jsdomOptions); @@ -59,7 +57,7 @@ function mochaHooksForNode(runner, options) { nodeGlobal.window = runner.window; nodeGlobal.document = runner.document; // Initialize "input" and "change" listeners on the document. - require('../dist/documentListeners').add(runner.document); + module.require('../documentListeners').add(runner.document); }); global.afterEach(function() { @@ -91,3 +89,12 @@ function mochaHooksForBrowser(runner) { runner.document = null; }); } + +const runner = new DomTestRunner(); +// Set up Chai assertion chain methods: `#html` and `#render` +registerAssertions(runner, module.require('chai').Assertion); + +export function install(options) { + runner.installMochaHooks(options); + return runner; +} diff --git a/src/test-utils/index.ts b/src/test-utils/index.ts new file mode 100644 index 000000000..b149f92c1 --- /dev/null +++ b/src/test-utils/index.ts @@ -0,0 +1,3 @@ +export { assertions } from './assertions'; +export { ComponentHarness, type RenderOptions, PageForHarness } from './ComponentHarness'; +export { install as domTestRunner, DomTestRunner } from './domTestRunner'; diff --git a/test-utils/ComponentHarness.js b/test-utils/ComponentHarness.js deleted file mode 100644 index 63109065d..000000000 --- a/test-utils/ComponentHarness.js +++ /dev/null @@ -1,245 +0,0 @@ -var EventEmitter = require('events').EventEmitter; -var qs = require('qs'); -var urlParse = require('url').parse; -var Model = require('racer').Model; -var racerUtil = require('racer/lib/util'); -var App = require('../dist/App').App; -var AppForServer = require('../dist/AppForServer').AppForServer; - -function AppForHarness(harness) { - App.call(this); - this._harness = harness; - this._pages = []; -} -AppForHarness.prototype = Object.create(App.prototype); -AppForHarness.prototype.constructor = AppForHarness; - -AppForHarness.prototype.createPage = function() { - var page = new this.Page(this, this._harness.model); - this._pages.push(page); - return page; -}; - -// Load views by filename. The client version of this method is a no-op -AppForHarness.prototype.loadViews = AppForServer.prototype.loadViews; - -// `_init()` does setup for loading views from files on the server and loading -// serialized views and data on the client -AppForHarness.prototype._init = function() { - this._initLoad(); -}; -// Register default compilers so that AppForHarness can load views & styles from -// the filesystem -AppForHarness.prototype._initLoad = AppForServer.prototype._initLoad; - - -module.exports = ComponentHarness; -/** - * Creates a `ComponentHarness`. - * - * If arguments are provided, then `#setup` is called with the arguments. - */ -function ComponentHarness() { - EventEmitter.call(this); - this.app = new AppForHarness(this); - this.model = new Model(); - - if (arguments.length > 0) { - this.setup.apply(this, arguments); - } -} - -racerUtil.mergeInto(ComponentHarness.prototype, EventEmitter.prototype); - -/** @typedef { {view: {is: string, source?: string}} } InlineComponent */ -/** - * Sets up the harness with a HTML template, which should contain a `` for the - * component under test, and the components to register for the test. - * - * @param {string} source - HTML template for the harness page - * @param {...(Component | InlineComponent} components - components to register for the test - * - * @example - * var harness = new ComponentHarness().setup('', Dialog); - */ -ComponentHarness.prototype.setup = function(source) { - this.app.views.register('$harness', source); - // Remaining variable arguments are components - for (var i = 1; i < arguments.length; i++) { - var constructor = arguments[i]; - this.app.component(constructor); - } - return this; -}; - -/** - * Stubs out view names with empty view or the provided source. - * - * A view name is a colon-separated string of segments, as used in ``. - * - * @example - * var harness = new ComponentHarness('', Dialog).stub( - * 'icons:open-icon', - * 'icons:close-icon', - * {is: 'dialog:buttons', source: ''} - * ); - */ -ComponentHarness.prototype.stub = function() { - for (var i = 0; i < arguments.length; i++) { - var arg = arguments[i]; - if (typeof arg === 'string') { - this.app.views.register(arg, ''); - } else if (arg && arg.is) { - this.app.views.register(arg.is, arg.source || ''); - } else { - throw new Error('each argument must be the name of a view or an object with an `is` property'); - } - } - return this; -}; - -/** - * Stubs out view names as components. - * - * This can be used to test the values being bound to ("passed into") child components. - * - * @example - * var harness = new ComponentHarness('', Dialog) - * .stubComponent('common:file-picker', {is: 'footer', as: 'stubFooter'}); - */ -ComponentHarness.prototype.stubComponent = function() { - for (var i = 0; i < arguments.length; i++) { - var arg = arguments[i]; - var options = (typeof arg === 'string') ? {is: arg} : arg; - var Stub = createStubComponent(options); - this.app.component(Stub); - } - return this; -}; - -/** - * @typedef {Object} RenderOptions - * @property {string} [url] - Optional URL for the render, used to populate `page.params` - */ -/** - * Renders the harness into a HTML string, as server-side rendering would do. - * - * @param {RenderOptions} [options] - * @returns { Page & {html: string} } - a `Page` that has a `html` property with the rendered HTML - * string - */ -ComponentHarness.prototype.renderHtml = function(options) { - return this._get(function(page) { - page.html = page.get('$harness'); - }, options); -}; -/** - * Renders the harness into a `DocumentFragment`, as client-side rendering would do. - * - * @param {RenderOptions} [options] - * @returns { Page & {fragment: DocumentFragment} } a `Page` that has a `fragment` property with the - * rendered `DocumentFragment` - */ -ComponentHarness.prototype.renderDom = function(options) { - return this._get(function(page) { - page.fragment = page.getFragment('$harness'); - }, options); -}; - -ComponentHarness.prototype.attachTo = function(parentNode, node) { - return this._get(function(page) { - var view = page.getView('$harness'); - var targetNode = node || parentNode.firstChild; - view.attachTo(parentNode, targetNode, page.context); - }); -}; - -/** - * @param {(page: Page) => void} render - * @param {RenderOptions} [options] - */ -ComponentHarness.prototype._get = function(render, options) { - options = options || {}; - var url = options.url || ''; - - var page = this.app.createPage(); - // Set `page.params`, which is usually created in tracks during `Page#render`: - // https://github.com/derbyjs/tracks/blob/master/lib/index.js - function setPageUrl(url) { - page.params = { - url: url, - query: qs.parse(urlParse(url).query), - body: {}, - }; - // Set "$render.params", "$render.query", "$render.url" based on `page.params`. - page._setRenderParams(); - } - setPageUrl(url); - // Fake some methods from tracks/lib/History.js. - // JSDOM doesn't really support updating the window URL, but this should work for Derby code that - // pulls URL info from the model or page. - this.app.history = { push: setPageUrl, replace: setPageUrl }; - - // The `#render` assertion in assertions.js wants to compare the results of HTML and DOM - // rendering, to make sure they match. However, component `create()` methods can modify the DOM - // immediately after initial rendering, which can break assertions. - // - // To get around this, we trigger a "pageRendered" event on the harness before `create()` methods - // get called. This is done by pausing the context, which prevents create() methods from getting - // called until the pause-count drops to 0. - page.context.pause(); - render(page); - this.emit('pageRendered', page); - page.context.unpause(); - - // HACK: Implement getting an instance as a side-effect of rendering. This - // code relies on the fact that while rendering, components are instantiated, - // and a reference is kept on page._components. Since we just created the - // page, we can reliably return the first component. - // - // The more standard means for getting a reference to a component controller - // would be to add a hooks in the view with `as=` or `on-init=`. However, we - // want the developer to pass this view in, so they can supply whatever - // harness context they like. - // - // This may need to be updated if the internal workings of Derby change. - page.component = page._components._1; - return page; -}; -ComponentHarness.createStubComponent = createStubComponent; - -function createStubComponent(options) { - var as = options.as || options.is; - var asArray = options.asArray; - - function StubComponent() {} - StubComponent.view = { - is: options.is, - file: options.file, - source: options.source, - dependencies: options.dependencies - }; - StubComponent.prototype.init = (asArray) ? - function() { - var page = this.page; - var component = this; - if (page[asArray]) { - page[asArray].push(this); - } else { - page[asArray] = [this]; - } - this.on('destroy', function() { - var index = page[asArray].indexOf(component); - if (index === -1) return; - page[asArray].splice(index, 1); - }); - } : - function() { - var page = this.page; - page[as] = this; - this.on('destroy', function() { - page[as] = undefined; - }); - }; - return StubComponent; -} diff --git a/test-utils/index.js b/test-utils/index.js deleted file mode 100644 index 63e8ee23b..000000000 --- a/test-utils/index.js +++ /dev/null @@ -1,2 +0,0 @@ -exports.assertions = require('./assertions'); -exports.ComponentHarness = require('./ComponentHarness'); diff --git a/test/all/App.mocha.js b/test/all/App.mocha.js index 37b56cb7e..9c877a8fb 100644 --- a/test/all/App.mocha.js +++ b/test/all/App.mocha.js @@ -1,19 +1,19 @@ var expect = require('chai').expect; -var App = require('../../dist/App').App; +var AppForClient = require('../../src/App').AppForClient; describe('App._parseInitialData', () => { it('parses simple json', () => { - var actual = App._parseInitialData('{"foo": "bar"}'); + var actual = AppForClient._parseInitialData('{"foo": "bar"}'); expect(actual).to.deep.equal({ foo: 'bar' }); }); it('parses escaped json', () => { - var actual = App._parseInitialData('{"foo": "<\\u0021bar><\\/bar>"}'); + var actual = AppForClient._parseInitialData('{"foo": "<\\u0021bar><\\/bar>"}'); expect(actual).to.deep.equal({ foo: '' }); }); it('thorws error with context for unexpected tokens', () => { - expect(() => App._parseInitialData('{"foo": b}')).to.throw( + expect(() => AppForClient._parseInitialData('{"foo": b}')).to.throw( /^Parse failure: Unexpected token/ ); }); diff --git a/test/all/ComponentHarness.mocha.js b/test/all/ComponentHarness.mocha.js index cf9254c7d..6ad7c66cb 100644 --- a/test/all/ComponentHarness.mocha.js +++ b/test/all/ComponentHarness.mocha.js @@ -1,6 +1,6 @@ var expect = require('chai').expect; -var ComponentHarness = require('../../test-utils').ComponentHarness; -var derbyTemplates = require('../../dist/templates'); +var ComponentHarness = require('../../src/test-utils').ComponentHarness; +var derbyTemplates = require('../../src/templates'); describe('ComponentHarness', function() { describe('renderHtml', function() { diff --git a/test/all/eventmodel.mocha.js b/test/all/eventmodel.mocha.js index 396517d9d..a2991c3a2 100644 --- a/test/all/eventmodel.mocha.js +++ b/test/all/eventmodel.mocha.js @@ -1,5 +1,5 @@ var expect = require('chai').expect; -var EventModel = require('../../dist/eventmodel'); +var EventModel = require('../../src/eventmodel'); describe('eventmodel', function() { beforeEach(function() { @@ -56,7 +56,6 @@ describe('eventmodel', function() { expect(binding.eventModels).to.eq(undefined); em.addBinding(['x'], binding); expect(binding.eventModels).to.be.instanceOf(Object); - // console.log(em.at(['x'])); expect(em.at(['x'])).has.ownProperty('bindings').instanceOf(Object); }); }); diff --git a/test/all/parsing/dependencies.mocha.js b/test/all/parsing/dependencies.mocha.js index 188f84973..12d9ea84e 100644 --- a/test/all/parsing/dependencies.mocha.js +++ b/test/all/parsing/dependencies.mocha.js @@ -1,8 +1,8 @@ var expect = require('chai').expect; -var derbyTemplates = require('../../../dist/templates'); +var derbyTemplates = require('../../../src/templates'); var contexts = derbyTemplates.contexts; var templates = derbyTemplates.templates; -var parsing = require('../../../dist/parsing'); +var parsing = require('../../../src/parsing'); var createExpression = parsing.createExpression; var createTemplate = parsing.createTemplate; diff --git a/test/all/parsing/expressions.mocha.js b/test/all/parsing/expressions.mocha.js index 412f4720e..e12391e87 100644 --- a/test/all/parsing/expressions.mocha.js +++ b/test/all/parsing/expressions.mocha.js @@ -1,8 +1,8 @@ var expect = require('chai').expect; -var derbyTemplates = require('../../../dist/templates'); +var derbyTemplates = require('../../../src/templates'); var contexts = derbyTemplates.contexts; var expressions = derbyTemplates.expressions; -var create = require('../../../dist/parsing/createPathExpression').createPathExpression; +var create = require('../../../src/parsing/createPathExpression').createPathExpression; var controller = { plus: function(a, b) { diff --git a/test/all/parsing/templates.mocha.js b/test/all/parsing/templates.mocha.js index 5b40d6b05..6322fa89d 100644 --- a/test/all/parsing/templates.mocha.js +++ b/test/all/parsing/templates.mocha.js @@ -1,8 +1,8 @@ var expect = require('chai').expect; -var derbyTemplates = require('../../../dist/templates'); +var derbyTemplates = require('../../../src/templates'); var contexts = derbyTemplates.contexts; var templates = derbyTemplates.templates; -var parsing = require('../../../dist/parsing'); +var parsing = require('../../../src/parsing'); var model = { data: { @@ -107,15 +107,14 @@ describe('Parse and render dynamic text and blocks', function() { it('no pound sign at start of alias', function() { var source = '{{with _page.greeting as greeting}}{{/with}}'; expect(function() { - var template = parsing.createTemplate(source); - // console.log(template.content[0]); + parsing.createTemplate(source); }).to.throw(/Alias must be an identifier starting with "#"/); }); it('trailing parenthesis in alias', function() { var source = '{{with _page.greeting as #greeting)}}{{/with}}'; expect(function() { - var template = parsing.createTemplate(source); + parsing.createTemplate(source); }).to.throw(/Unexpected token \)/); }); diff --git a/test/all/parsing/truthy.mocha.js b/test/all/parsing/truthy.mocha.js index 1c4174880..f4ac1a067 100644 --- a/test/all/parsing/truthy.mocha.js +++ b/test/all/parsing/truthy.mocha.js @@ -1,5 +1,5 @@ var expect = require('chai').expect; -var parsing = require('../../../dist/parsing'); +var parsing = require('../../../src/parsing'); describe('template truthy', function() { diff --git a/test/all/templates/templates.mocha.js b/test/all/templates/templates.mocha.js index 98145b767..81e10c305 100644 --- a/test/all/templates/templates.mocha.js +++ b/test/all/templates/templates.mocha.js @@ -1,5 +1,5 @@ var expect = require('chai').expect; -var templates = require('../../../dist/templates/templates'); +var templates = require('../../../src/templates/templates'); describe('Views', function() { diff --git a/test/browser/components.js b/test/browser/components.js index e042641d0..e361e1764 100644 --- a/test/browser/components.js +++ b/test/browser/components.js @@ -1,5 +1,5 @@ var expect = require('chai').expect; -var templates = require('../../dist/templates').templates; +var templates = require('../../src/templates').templates; var derby = require('./util').derby; describe('components', function() { diff --git a/test/browser/util.js b/test/browser/util.js index 4d17e98da..3b57025c8 100644 --- a/test/browser/util.js +++ b/test/browser/util.js @@ -1,6 +1,6 @@ var chai = require('chai'); -var DerbyStandalone = require('../../dist/DerbyStandalone'); -require('../../dist/parsing'); +var DerbyStandalone = require('../../src/DerbyStandalone'); +require('../../src/parsing'); require('../../test-utils').assertions(window, chai.Assertion); exports.derby = new DerbyStandalone(); diff --git a/test/dom/ComponentHarness.mocha.js b/test/dom/ComponentHarness.mocha.js index ae8163400..3273e7bd6 100644 --- a/test/dom/ComponentHarness.mocha.js +++ b/test/dom/ComponentHarness.mocha.js @@ -1,6 +1,6 @@ var expect = require('chai').expect; -var Component = require('../../dist/components').Component; -var domTestRunner = require('../../test-utils/domTestRunner'); +var Component = require('../../src/components').Component; +var domTestRunner = require('../../src/test-utils/domTestRunner'); describe('ComponentHarness', function() { var runner = domTestRunner.install(); diff --git a/test/dom/bindings.mocha.js b/test/dom/bindings.mocha.js index db46e42e9..d9eb918e6 100644 --- a/test/dom/bindings.mocha.js +++ b/test/dom/bindings.mocha.js @@ -1,5 +1,5 @@ var expect = require('chai').expect; -var domTestRunner = require('../../test-utils/domTestRunner'); +var domTestRunner = require('../../src/test-utils/domTestRunner'); describe('bindings', function() { var runner = domTestRunner.install(); diff --git a/test/dom/components.mocha.js b/test/dom/components.mocha.js index 94913db57..746553ed6 100644 --- a/test/dom/components.mocha.js +++ b/test/dom/components.mocha.js @@ -1,6 +1,6 @@ var expect = require('chai').expect; var pathLib = require('node:path'); -var domTestRunner = require('../../test-utils/domTestRunner'); +var domTestRunner = require('../../src/test-utils/domTestRunner'); describe('components', function() { var runner = domTestRunner.install(); diff --git a/test/dom/domTestRunner.mocha.js b/test/dom/domTestRunner.mocha.js index ceb258356..22e581a93 100644 --- a/test/dom/domTestRunner.mocha.js +++ b/test/dom/domTestRunner.mocha.js @@ -1,4 +1,4 @@ -var domTestRunner = require('../../test-utils/domTestRunner'); +var domTestRunner = require('../../src/test-utils/domTestRunner'); describe('domTestRunner', function() { describe('with JSDOM option pretendToBeVisual', function() { diff --git a/test/dom/templates/templates.dom.mocha.js b/test/dom/templates/templates.dom.mocha.js index 085972824..c1e467c63 100644 --- a/test/dom/templates/templates.dom.mocha.js +++ b/test/dom/templates/templates.dom.mocha.js @@ -1,7 +1,7 @@ var chai = require('chai'); var expect = chai.expect; -var saddle = require('../../../dist/templates/templates'); -var domTestRunner = require('../../../test-utils/domTestRunner'); +var saddle = require('../../../src/templates/templates'); +var domTestRunner = require('../../../src/test-utils/domTestRunner'); describe('templates rendering', function() { domTestRunner.install({jsdomOptions: {pretendToBeVisual: true}}); diff --git a/test/server/ComponentHarness.mocha.js b/test/server/ComponentHarness.mocha.js index a2baae6d8..7b8cbf616 100644 --- a/test/server/ComponentHarness.mocha.js +++ b/test/server/ComponentHarness.mocha.js @@ -1,5 +1,5 @@ var expect = require('chai').expect; -var ComponentHarness = require('../../test-utils').ComponentHarness; +var ComponentHarness = require('../../src/test-utils').ComponentHarness; describe('ComponentHarness', function() { describe('file loading', function() { diff --git a/test/server/templates/templates.server.mocha.js b/test/server/templates/templates.server.mocha.js index 018e9f707..ccdf0cbf2 100644 --- a/test/server/templates/templates.server.mocha.js +++ b/test/server/templates/templates.server.mocha.js @@ -1,6 +1,6 @@ var expect = require('chai').expect; -var templates = require('../../../dist/templates/templates'); -var expressions = require('../../../dist/templates/expressions'); +var templates = require('../../../src/templates/templates'); +var expressions = require('../../../src/templates/expressions'); function test(createTemplate) { return function() { diff --git a/tsconfig.json b/tsconfig.json index ba98083a0..867045932 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,8 +8,8 @@ "outDir": "dist", "target": "ES5", "sourceMap": false, - "declaration": false, - "declarationMap": false, + "declaration": true, + "declarationMap": false }, "include": [ "src/**/*"