UNPKG

node-sitefile

Version:

Bootstrap Express webserver for misc. resources using Sitefile

355 lines (273 loc) 9.77 kB
path = require 'path' fs = require 'fs' minimatch = require 'minimatch' glob = require 'glob' _ = require 'lodash' nodelib = require 'nodelib' Context = nodelib.Context expand_path = ( src, ctx ) -> base = ctx.sfdir+'/' if src.startsWith 'sitefile:' return src.replace 'sitefile:', base libdir = base+'lib/sitefile/' if src.startsWith 'sitefile-lib:' return src.replace 'sitefile-lib:', libdir if src.startsWith 'sitefile-client:' return src.replace 'sitefile-client:', libdir+'client/' src expand_paths_spec_to_route = ( rctx ) -> spec = rctx.route.spec if spec and '#' != spec # Expand non-glob from spec to paths srcs = minimatch.braceExpand spec else # Or fall back to verbatim route name as path srcs = [ rctx.name ] for src, idx in srcs srcs[idx] = expand_path src, rctx return srcs # Load default options from Sitefile resolve_route_options = ( ctx, route, router_name ) -> opts = {} if 'options' of ctx.sitefile # Global sitefile options (per router) if ctx.sitefile.options.global and router_name if router_name of ctx.sitefile.options.global global_opts = null #ctx.get "sitefile.options.global.#{router_name}" try global_opts = ctx.resolve "sitefile.options.global.#{router_name}" catch error null if global_opts opts = _.defaults {}, global_opts, opts # Local (per route) sitefile options if ctx.sitefile.options.local and route if route of ctx.sitefile.options.local local_opts = null esc = route.replace '.', '\\.' try local_opts = ctx.resolve "sitefile.options.local.#{esc}" catch error null if local_opts opts = _.defaultsDeep {}, local_opts, opts opts builtin = # TODO: extend redir spec for status code redir: ( rctx, url=null, status=302 ) -> if not url url = rctx.site.base + rctx.name p = rctx.site.base + rctx.route.spec # 301: Moved (Permanently) # 302: Found # 303: See Other rctx.context.log 'redir', url, p if rctx.route.handler == 'temp' rctx.context.redir 302, url, p else if rctx.route.handler == 'perm' rctx.context.redir 301, url, p else #rctx.context.redir status, url, p rctx.context.app.all url, ( req, res ) -> res.redirect p rctx.context.log ' ', url: url, '->', url: p static: ( rctx ) -> url = rctx.res.ref srcs = expand_paths_spec_to_route rctx rctx.context.app.use url, [ rctx.context.static_proto src for src in srcs ] rctx.context.log 'Static', url: url, '=', path: rctx.route.spec # Take care of rendering from a rctx with data, for a (data) handler that does # not care too itself since it is a very common task. data: ( rctx ) -> ( req, res ) -> res.type 'json' res.write JSON.stringify \ if "function" is typeof rctx.res.data then rctx.res.data() else rctx.res.data res.end() Base = name: 'Express' label: 'Express-Sitefile resource publisher' usage: """ Static path /path: <router[.handler-generator]>:<handler-spec> Leading underscore (accepts file glob handler-spec) _route_id: <router[.handler-generator]>:<handler-spec> TODO: maybe, later: Dollar path. /data/$record: <router[.handler-generator]>:<handler-spec> Prefix path /prefix$: <router[.handler-generator]>:<handler-spec> Also: handler-spec vs. glob-spec. Routers should parse own args. Router.Base provide some standard resolvers: - path/to/file-name.ext1.ext2 - path/to/dir """ # TODO: resource types? to use as 'backend' for fe route handlers.. resources: 'core.sitefile': null # Sitefile instance? 'core.sitefilerc': null # Subset mapped to ctx 'core.sitefile.extension': null # JS or coffee file 'core.sitefile.route.autocomplete': null # jQuery autcomplete API 'core.sitefile.route': null # Dynamic routes API? # XXX: clean me up, route spec parsing #parse_spec: ( route_spec, handler_spec, ctx ) -> # process parametrized rule #else if '$' in route # url = ctx.site.base + route.replace('$', ':') # log route, url: url # app.all url, handler.generator '.'+url, ctx # Return handler for path generate: ( ctx ) -> # XXX: register: ( app, ctx ) -> # Return resource sub-context for local file resource file_res_ctx: ( ctx, init, file_path ) -> init.res = { ref: ctx.site.base + file_path path: file_path extname: path.extname file_path dirname: path.dirname file_path } init.res.basename = path.basename file_path, init.res.extname # Create resolver sub-context ctx.getSub init prepare_dyn_path_res: ( rctx, ctx ) -> if rctx.res.dirname == '.' rctx.res.ref = ctx.site.base + rctx.res.basename else rctx.res.ref = \ "#{ctx.site.base}#{rctx.res.dirname}/#{rctx.res.basename}" dirurl = ctx.site.base + rctx.res.dirname # XXX: dir tracking if not ctx.routes.directories.hasOwnProperty dirurl ctx.routes.directories[ dirurl ] = [ rctx.res.basename ] else ctx.routes.directories[ dirurl ].push rctx.res.basename #rctx.path = dirurl default_resource_options: ( rctx, ctx, updateRef=false ) -> router = rctx.route.name if updateRef if 'string' is typeof updateRef rctx.res.ref = updateRef else Base.prepare_dyn_path_res rctx, ctx # Now parse dynamic path back from ref and look for defaults route = rctx.res.ref.substr ctx.site.base.length rctx.route.options = resolve_route_options( ctx, route, router ) # Return resource paths resolve: ( route, router_name, handler_name, handler_spec, ctx ) -> # List for resource contexts rs = [] rsctxinit = name: route route: name: router_name handler: handler_name spec: handler_spec # Route is RegEx if route.startsWith 'r:' init = res: ref: ctx.site.base + route.substr 2 match: new RegExp route.substr 2 _.defaultsDeep init, rsctxinit rctx = ctx.getSub init Base.default_resource_options rctx, ctx rs.push rctx # Use exact route as fs path else if fs.existsSync route rctx = Base.file_res_ctx ctx, rsctxinit, route Base.default_resource_options rctx, ctx rs.push rctx # Use route as ID for glob spec (a set of existing fs paths) else if route.startsWith '_' ctx.log 'Dynamic', url: route, '', path: handler_spec for name in glob.sync handler_spec rctx = Base.file_res_ctx ctx, rsctxinit, name Base.default_resource_options rctx, ctx, true rs.push rctx else if fs.existsSync handler_spec rctx = Base.file_res_ctx ctx, rsctxinit, handler_spec Base.default_resource_options rctx, ctx, ctx.site.base + route rs.push rctx # Use route as is else init = res: ref: ctx.site.base + route _.defaultsDeep init, rsctxinit rctx = ctx.getSub init Base.default_resource_options rctx, ctx rs.push rctx return rs initialize: ( router_type, rctx ) -> ctx = rctx.context # routes.resources: Track all paths to router instances ctx.routes.resources[rctx.res.ref] = rctx # generate: let router_type return handlers for given resource if rctx.route.handler g = ctx._routers.generator '.'+rctx.route.handler, rctx else g = ctx._routers.generator rctx.route.name # invoke routers selected generate function, expect a route handler object h = g rctx ref = if rctx.res.match then rctx.res.match else rctx.res.ref if 'function' is typeof h # Add as regular Express route handler rctx.context.app.all ref, ( req, res ) -> _.defaultsDeep rctx.route.options, req.query h req, res ctx.log rctx.name, url: rctx.res.ref, '=', ( if 'path' of rctx.res \ then path: rctx.res.path \ else res: rctx.route.name ), id: rctx.route.spec else if h and 'object' is typeof h # XXX: The object could have data, meta etc. attr and play as JSON API doc # For now just extend the resource context with the object it returned. rctx.prepare_from_obj h _.merge rctx._data, h if h.res and 'data' of h.res rctx.context.app.all ref, builtin.data rctx ctx.log "Extension at ", url: rctx.res.ref, \ "from", ( name: rctx.route.name+'.'+rctx.route.handler ), \ id: rctx.route.spec, "at", path: rctx.name else if not h module.exports.warn "Router not recognized", "Router #{rctx.route.name} returned nothing recognizable for #{rctx.name}, ignored" module.exports = builtin: builtin Base: Base # Current way of 'instantiating' router define: ( mixin ) -> _.assign {}, Base, mixin resolve_route_options: resolve_route_options # XXX: spec parse helper expand_paths_spec_to_route: expand_paths_spec_to_route expand_path: expand_path # XXX: spec parse helper parse_kw_spec: ( rctx ) -> kw = {} specs = rctx.route.spec.split ';' for spec in specs x = spec.indexOf '=' k = spec.substr(0, x) kw[k] = spec.substr x+1 kw # XXX: Read JSON + jspath read_xref: ( ctx, spec ) -> if '#' not in spec throw new Error spec [ jsonf, spec ] = spec.split '#' jsonf = expand_path jsonf, ctx if not jsonf.startsWith path.sep jsonf = path.join ctx.cwd, jsonf p = spec.split '/' if not p[0] p.shift() o = require jsonf c = o while p.length e = p.shift() c = c[e] return c