Skip to content

FS cache #199

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 108 additions & 25 deletions src/core/getExports.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,20 @@ import parse from './parse'
import resolve from './resolve'
import isIgnored from './ignore'

// map from settings sha1 => path => export map objects
const exportCaches = new Map()

export default class ExportMap {
constructor(context) {
this.context = context
this.named = new Map()

constructor() {
this.namespace = new Map()
this.errors = []
}

get settings() { return this.context && this.context.settings }

get hasDefault() { return this.named.has('default') }
get hasNamed() { return this.named.size > (this.hasDefault ? 1 : 0) }
/**
* @deprecated use 'namespace'
* @return {Map}
*/
get named() { return this.namespace }
get hasDefault() { return this.namespace.has('default') }
get hasNamed() { return this.namespace.size > (this.hasDefault ? 1 : 0) }

static get(source, context) {

Expand Down Expand Up @@ -49,25 +48,29 @@ export default class ExportMap {
// return cached ignore
if (exportMap === null) return null

// todo: evict ENOENT cache entries
const stats = fs.statSync(path)
if (exportMap != null) {
// date equality check
if (exportMap.mtime - stats.mtime === 0) {
return exportMap
}
// future: check content equality?
// todo: check content equality?
}

exportMap = ExportMap.parse(path, context)
exportMap.mtime = stats.mtime

// ignore empties, optionally
if (exportMap.named.size === 0 && isIgnored(path, context)) {
if (exportMap.namespace.size === 0 && isIgnored(path, context)) {
exportMap = null
}

exportCache.set(path, exportMap)

// queues a save of the caches
queueCacheSave(CACHE_FILE, exportCaches)

return exportMap
}

Expand Down Expand Up @@ -100,8 +103,8 @@ export default class ExportMap {
function getNamespace(identifier) {
if (!namespaces.has(identifier.name)) return

let namespace = m.resolveReExport(namespaces.get(identifier.name), path)
if (namespace) return { namespace: namespace.named }
let namespace = m.resolveReExport(context, namespaces.get(identifier.name), path)
if (namespace) return { namespace: namespace.namespace }
}

ast.body.forEach(function (n) {
Expand All @@ -111,14 +114,14 @@ export default class ExportMap {
if (n.declaration.type === 'Identifier') {
Object.assign(exportMeta, getNamespace(n.declaration))
}
m.named.set('default', exportMeta)
m.namespace.set('default', exportMeta)
return
}

if (n.type === 'ExportAllDeclaration') {
let remoteMap = m.resolveReExport(n, path)
let remoteMap = m.resolveReExport(context, n, path)
if (remoteMap == null) return
remoteMap.named.forEach((value, name) => { m.named.set(name, value) })
remoteMap.namespace.forEach((value, name) => { m.namespace.set(name, value) })
return
}

Expand All @@ -138,18 +141,18 @@ export default class ExportMap {
case 'FunctionDeclaration':
case 'ClassDeclaration':
case 'TypeAlias': // flowtype with babel-eslint parser
m.named.set(n.declaration.id.name, captureDoc(n))
m.namespace.set(n.declaration.id.name, captureDoc(n))
break
case 'VariableDeclaration':
n.declaration.declarations.forEach((d) =>
recursivePatternCapture(d.id, id => m.named.set(id.name, captureDoc(d, n))))
recursivePatternCapture(d.id, id => m.namespace.set(id.name, captureDoc(d, n))))
break
}
}

// capture specifiers
let remoteMap
if (n.source) remoteMap = m.resolveReExport(n, path)
if (n.source) remoteMap = m.resolveReExport(context, n, path)

n.specifiers.forEach((s) => {
const exportMeta = {}
Expand All @@ -160,23 +163,23 @@ export default class ExportMap {
} else if (s.type === 'ExportSpecifier'){
Object.assign(exportMeta, getNamespace(s.local))
} else if (s.type === 'ExportNamespaceSpecifier') {
exportMeta.namespace = remoteMap.named
exportMeta.namespace = remoteMap.namespace
}

// todo: JSDoc
m.named.set(s.exported.name, exportMeta)
m.namespace.set(s.exported.name, exportMeta)
})
}
})

return m
}

resolveReExport(node, base) {
var remotePath = resolve.relative(node.source.value, base, this.settings)
resolveReExport(context, node, base) {
var remotePath = resolve.relative(node.source.value, base, context.settings)
if (remotePath == null) return null

return ExportMap.for(remotePath, this.context)
return ExportMap.for(remotePath, context)
}

reportErrors(context, declaration) {
Expand Down Expand Up @@ -251,3 +254,83 @@ function hashObject(object) {
settingsShasum.update(JSON.stringify(object))
return settingsShasum.digest('hex')
}

// map from settings sha1 => path => export map objects
const CACHE_FILE = "import.cache"
const exportCaches = loadFSCaches(CACHE_FILE)

import { readFileSync, writeFile } from 'fs'

// TODO: save as directory structure and load on demand?

function loadFSCaches(filename) {
function rehydrateExports([key, dry]) {
if (!dry) return [key, dry] // null map

const wet = new ExportMap()
wet.namespace = new Map(dry.namespace.map(rehydrateNamespace))
wet.mtime = new Date(dry.mtime)
return [key, wet]
}

try {
const caches = JSON.parse(readFileSync(filename))
, map = new Map()
for (let key in caches) {
map.set(key, new Map(caches[key].map(rehydrateExports)))
}
return map
} catch (err) {
/* ??? */
return new Map()
}
}

function saveFSCaches(filename, caches) {
const dry = {}
for (let [hash, maps] of caches) {
dry[hash] = Array.from(maps, dehydrate)
}
// fire and forget
writeFile(filename, JSON.stringify(dry), () => null)
}

let queued
/**
* only write the cache a maximum of
* @param {[type]} caches [description]
* @return {[type]} [description]
*/
function queueCacheSave(filename, caches) {
function S() {
saveFSCaches(filename, queued)
queued = null
}
if (!queued) process.nextTick(S)
queued = caches
}

function rehydrateNamespace(o) {
if (o.namespace) o.namespace = new Map(o.namespace.map(rehydrateNamespace))
return o
}

function dehydrate([key, map]) {
return [ key, map && {
mtime: map.mtime,
namespace: Array.from(map.namespace, dehydrateMapKeys),
} ]
}

function dehydrateMapKeys([k, o]) {
const dry = {}
for (let key in o) {
const val = o[key]
if (val instanceof Map) {
dry[key] = Array.from(val, dehydrateMapKeys)
} else {
dry[key] = val
}
}
return [k, dry]
}
1 change: 1 addition & 0 deletions src/core/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { dirname, basename, join } from 'path'

const CASE_INSENSITIVE = fs.existsSync(join(__dirname, 'reSOLVE.js'))

// todo: cheaper way to do this (or f'real async)
// http://stackoverflow.com/a/27382838
function fileExistsWithCaseSync(filepath) {
var dir = dirname(filepath)
Expand Down
2 changes: 2 additions & 0 deletions src/rules/no-unresolved.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
*/
import resolve from '../core/resolve'

// todo: check ExportMap for existence first?

module.exports = function (context) {

function checkSourceValue(source) {
Expand Down