node-sitefile
Version:
Bootstrap Express webserver for misc. resources using Sitefile
355 lines (273 loc) • 9.77 kB
text/coffeescript
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