import {SystemjsConfigFile} from "@gongt/ts-stl-client/jspm/defines"; import {JspmPackagePassing} from "@gongt/ts-stl-client/jspm/jspm-package-passing"; import {createLogger} from "@gongt/ts-stl-library/debug/create-logger"; import {LOG_LEVEL} from "@gongt/ts-stl-library/debug/levels"; import DI, {DDNames} from "@gongt/ts-stl-library/DI"; import {hideGlobal} from "@gongt/ts-stl-library/pattern/hide-global"; import {objectPath} from "@gongt/ts-stl-library/pattern/object-path"; import {HTTP} from "@gongt/ts-stl-library/request/request"; import {Application} from "express-serve-static-core"; import * as extend from "extend"; import {readFileSync, statSync} from "fs-extra"; import {dirname, resolve} from "path"; import {constructor as SystemJS} from "systemjs"; import {resolve as resolveUrl} from "url"; import {createServeStatic} from "../inject/serve-static"; import {HtmlContainer, IContainerPlugin} from "../middlewares/html-render"; import {IExpressProvide} from "../middlewares/well-known-provider"; import {InternetExplorerProviders} from "./jspm.ie"; const debug = createLogger(LOG_LEVEL.DEBUG, 'jspm-plug'); const silly = createLogger(LOG_LEVEL.SILLY, 'jspm-plug'); export interface JspmConstructOptions { packageName: string; packageJsonFile: string; debug: boolean; } export class JspmPackagePlugin implements IContainerPlugin, IExpressProvide { protected _opts: Partial; protected pacakgeJson: any; protected jspmCfg: SystemjsConfigFile; protected clientSource: string; readonly packageName: string; protected extraConfig: JspmPackagePassing; public readonly rootPath: string; protected alertTimeout: any = null; protected alertChecked: boolean = false; protected node_modules: string[] = ['node_modules']; private ieCompatible: boolean = true; constructor(options: Partial = {}) { debug('serve-static[jspm]: init'); this._opts = options; const packageJsonFile = options.packageJsonFile || process.cwd() + '/package.json'; const pkg: object = this.pacakgeJson = require(packageJsonFile); debug('\tbasePath= %s', this.basePath); debug('\tpackageJsonFile= %s', packageJsonFile); this.rootPath = dirname(packageJsonFile) + '/'; debug('\tconfigFile= %s', this.configFile); this.jspmCfg = loadSystemjsConfigFile(this.configFile); const config = this.extraConfig = new JspmPackagePassing; this.packageName = options.packageName || objectPath(pkg, 'jspm.name') || objectPath(pkg, 'name') || 'client'; debug('\trootPath= %s', this.rootPath); debug('\tabsBasePath= %s', this.absBasePath); debug('\tpackageFolder= %s', this.packageFolder); debug('\tconfigUrl= %s', this.configUrl); debug('\tbaseUrl= %s', this.baseUrl); debug('\tclientUrl= %s', this.clientUrl); debug('\tpackageUrl= %s', this.packageUrl); debug('\tpackageName= %s', this.packageName); } ignoreIE() { this.ieCompatible = false; } public get baseUrl(): string { const jspmCfg = this.jspmCfg; let baseUrl = objectPath(jspmCfg, 'browserConfig.baseURL') || objectPath(jspmCfg, 'baseURL') || '/'; baseUrl = resolveUrl('/', baseUrl); if (!/\/$/.test(baseUrl)) { baseUrl = baseUrl + '/'; } return baseUrl; } public get basePath(): string { let basePath = objectPath(this.pacakgeJson, 'jspm.directories.baseURL') || '.'; if (!/\/$/.test(basePath)) { basePath = basePath + '/'; } return basePath; } public get absBasePath(): string { return resolve(this.rootPath, this.basePath) + '/'; } public get configFile(): string { let configFile = objectPath(this.pacakgeJson, 'jspm.configFiles.jspm') || objectPath(this.pacakgeJson, 'configFiles.jspm') || `${this.basePath}jspm.config.js`; configFile = resolve(this.rootPath, configFile.replace(/^\//g, '')); return configFile; } public get configUrl(): string { return resolveUrl(this.baseUrl, this.configFile.replace(this.absBasePath, '')); } public get packageFolder(): string { let packageFolder = objectPath(this.pacakgeJson, 'jspm.directories.packages') || `${this.basePath}jspm_packages`; packageFolder = packageFolder.replace(/^\//g, ''); packageFolder = resolve(this.rootPath, packageFolder); return packageFolder; } private _clientPathOverwrite; public get clientPath(): string { if (this._clientPathOverwrite) { return this._clientPathOverwrite; } let clientUrl = objectPath(this.jspmCfg, `browserConfig.paths.${this.packageName}/`) || objectPath(this.jspmCfg, `paths.${this.packageName}/`) || this.packageName; return resolve(this.absBasePath, clientUrl); } public get clientUrl(): string { let clientUrl = objectPath(this.jspmCfg, `browserConfig.paths.${this.packageName}/`) || objectPath(this.jspmCfg, `paths.${this.packageName}/`) || this.packageName; return resolveUrl(this.baseUrl, clientUrl); // return resolve(this.basePath, clientUrl.replace(/^\//, '')); } public get packageUrl(): string { let packageUrl = objectPath(this.jspmCfg, 'browserConfig.paths.npm:') || objectPath(this.jspmCfg, 'browserConfig.paths.jspm:') || objectPath(this.jspmCfg, 'paths.npm:') || objectPath(this.jspmCfg, 'paths.jspm:') || 'jspm_packages'; packageUrl = packageUrl.replace(/^\//g, ''); packageUrl = resolveUrl(this.baseUrl, packageUrl.replace(/\/(jspm|npm)\/?$/, '/')); if (!/\/$/.test(packageUrl)) { packageUrl += '/'; } return packageUrl; } jspmConfig(): JspmPackagePassing { return this.extraConfig; } clientCodeLocation(path: string, sourcePath: string = '') { this._clientPathOverwrite = resolve(this.absBasePath, path); this.clientSource = resolve(this.absBasePath, sourcePath); } protected ensureStaticUnique(reg: any, url: string, path: string) { if (!reg.hasOwnProperty(url)) { reg[url] = path; return true; } if (reg[url] === path) { return false; } throw new Error(`[jspm plugin] url conflict. "${reg[url]}" and "${path}" both mount at same url "${url}"`); } __express_provide(app: Application) { this.expressPrepend(app); this.mountJspmSpecial(app); this.mountCurrentClient(app); this.mountBasic(app); } protected expressPrepend(app: Application) { if (!this.alertChecked) { clearTimeout(this.alertTimeout); this.alertTimeout = null; this.alertChecked = true; } if (!app['_jspm_reg']) { app['_jspm_reg'] = {}; } } protected mountCurrentClient(app: Application) { const reg: any = app['_jspm_reg']; const opts = { // fallthrough: true, }; if (!this.ensureStaticUnique(reg, this.clientUrl, this.clientPath)) { return; } const clientProvider = []; if (this.clientSource) { debug('client: \n\t\tpath = %s\n\t\tlocal= %s\n\t\tlocal= %s', this.clientUrl, this.clientPath, this.clientSource); clientProvider.push( createServeStatic(this.clientPath, { fallthrough: true, }), createServeStatic(this.clientSource), ); } else { debug('client: \n\t\tpath = %s\n\t\tlocal= %s', this.clientUrl, this.clientPath); clientProvider.push( createServeStatic(this.clientPath), ); } app.use(this.clientUrl, clientProvider); if (this.ieCompatible) { app.use('/_IE_' + this.clientUrl, InternetExplorerProviders(this.clientPath, this.clientSource)); } } protected mountJspmSpecial(app: Application) { const reg: any = app['_jspm_reg']; const opts = { fallthrough: false, }; if (this.ensureStaticUnique(reg, this.packageUrl, this.packageFolder)) { debug('jspm package: \n\t\tpath = %s\n\t\tlocal= %s', this.packageUrl, this.packageFolder); app.use(this.packageUrl, createServeStatic(this.packageFolder, opts)); } if (this.ensureStaticUnique(reg, this.configUrl, this.configFile)) { debug('config file: \n\t\tpath = %s\n\t\tlocal= %s', this.configUrl, this.configFile); const middleware = createServeStatic(this.configFile, opts); const configFileForIE = readFileSync(this.configFile, {encoding: 'utf8'}).replace(/(["']?baseURL["']?: ["'])/, '$1/_IE_'); const isIE = /Internet Explorer|Trident/i; app.use(this.configUrl, (req, res, next) => { if (isIE.test(req.header('user-agent'))) { next(); } else { middleware(req, res, next); } }, (req, res) => { // IE config file res.header('Cache-Control', 'private'); res.header('Vary', 'user-agent'); res.header('Content-Type', 'text/javascript; charset=utf-8'); res.send(configFileForIE); }); } } addNodeModulesLayer(path: string) { this.node_modules.push(path); } protected mountBasic(app: Application) { const reg: any = app['_jspm_reg']; const opts = { // fallthrough: true, }; // node_modules folder const pathList = this.node_modules.map((location) => { return resolve(this.rootPath, location); }); const nmUrl = resolveUrl(this.baseUrl, 'node_modules'); if (this.ensureStaticUnique(reg, nmUrl, pathList.join(':'))) { debug('node_modules: \n\t\tpath = %s\n\t\tlocal= %s', nmUrl, pathList.join('\n\t\tlocal= ')); const handlers = pathList.map((path, index) => { return createServeStatic(path, { fallthrough: true, etag: true, lastModified: true, maxAge: 3600, }); }); app.use(nmUrl, handlers, (req, res, next) => { res.status(HTTP.NOT_FOUND).header('Content-Type', 'text/plain; charset=utf-8') .send(`Cannot GET ${req.originalUrl}: [${req.url}] - ${DI.get(DDNames.isDebugMode)? 'debug mode' : 'production mode'} \ttried: \t\t ${pathList.join('\n\t\t ')}`); }); } // the root public folder if (this.ensureStaticUnique(reg, this.baseUrl, this.absBasePath)) { debug('base public folder:\n\t\tpath = %s\n\t\tlocal= %s', this.baseUrl, this.absBasePath); app.use(this.baseUrl, createServeStatic(this.absBasePath, opts)); } } __modify_html(html: HtmlContainer, options: Partial): void { html.addJavascript(this.getScriptContent(options)); debug('\tthis.packageUrl= %s', this.packageUrl); html.script(this.systemJsUrl, 'jspm-loader'); html.script(this.configUrl, 'jspm-config'); if (!this.alertTimeout && !this.alertChecked) { this.alertTimeout = setTimeout(() => { this.alertTimeout = null; throw new Error('JspmPackagePlugin: must call provideWithExpress(app, jspm)'); }, 500); } html.addHead(JspmPackagePassing.getHeader()); } public getHtmlResult(options: Partial = {}) { const scTag = (url: string, debug_tag: string) => { `` }; let scripts = ''; debug('\tthis.packageUrl= %s', this.packageUrl); scripts += scTag(this.systemJsUrl, 'jspm-loader'); scripts += scTag(this.configUrl, 'jspm-config'); //language=HTML scripts += ``; return scripts; } public get systemJsUrl() { return resolveUrl(this.packageUrl, this._opts.debug? 'system.src.js' : 'system.js') } private getScriptContent(options: Partial) { //language=JavaScript return `(function (window) { /* extra passing config */ ${this.extraConfig.toString().replace(/^/mg, '\t')} /* extra passing config end */; var mainPackage = '<%- locals.packageName || "${options.packageName || this.packageName}" %>'; console.log('boot application: %s', mainPackage); SystemJS.import(mainPackage) .then(function (){ console.info('application load success.'); }, function (e){ console.warn('application load failed'); console.error(e); }); })(window);` } } export interface JspmHtmlConfig { packageName: string; } export function loadSystemjsConfigFile(configFile: string): SystemjsConfigFile { const target = require.resolve(configFile); silly('load systemjs config file: %s', target); if (require.cache[target]) { if (require.cache[target].time !== statSync(target).mtime) { delete require.cache[target]; silly(' file changed, reloading.'); } } let config: any; const sys = { config(data) { if (!config) { config = {}; } extend(true, config, data); }, }; const resetG1 = hideGlobal('SystemJS', sys); const resetG2 = hideGlobal('System', sys); const resetG3 = hideGlobal('window', global); require(configFile); require.cache[target].time = statSync(target).mtime; resetG1(); resetG2(); resetG3(); if (config) { return config; } throw new Error(`[jspm-plugin] systemjs config fail, no SystemJS.config() call, file: ${configFile}`); } export function loadSystemjsConfigFileMultiParts(configFile: string): SystemjsConfigFile[] { const config: any[] = []; const sys = { config(data) { config.push(data); }, }; const resetG1 = hideGlobal('SystemJS', sys); const resetG2 = hideGlobal('System', sys); const resetG3 = hideGlobal('window', global); delete require.cache[require.resolve(configFile)]; require(configFile); delete require.cache[require.resolve(configFile)]; resetG1(); resetG2(); resetG3(); if (config.length) { return config; } throw new Error(`[jspm-plugin] systemjs config fail, no SystemJS.config() call, file: ${configFile}`); }