From f7cf3eefab576a19e14529ed32ce3315670883a0 Mon Sep 17 00:00:00 2001 From: overlookmotel Date: Mon, 22 Jun 2020 18:48:54 +0100 Subject: [PATCH] WIP --- .eslintrc.js | 17 ++++ .gitignore | 7 ++ NOTES2.md | 14 +++ NOTES3.md | 231 ++++++++++++++++++++++++++++++++++++++++++++++++++ TODO.md | 16 ++++ lib/debug.js | 47 ++++++++++ lib/locate.js | 28 +++++- 7 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 NOTES2.md create mode 100644 NOTES3.md create mode 100644 TODO.md create mode 100644 lib/debug.js diff --git a/.eslintrc.js b/.eslintrc.js index d0fb015..11b1b31 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -22,5 +22,22 @@ module.exports = { 'node/no-unsupported-features/es-syntax': ['error', {ignores: ['modules']}], 'node/no-unpublished-import': ['off'] } + }, { + files: ['build/**/*.js'], + rules: { + 'import/no-extraneous-dependencies': 'off', + 'node/no-extraneous-require': 'off', + 'global-require': 'off', + 'import/order': 'off', + 'import/newline-after-import': 'off', + 'object-shorthand': 'off', + 'prefer-destructuring': 'off', + 'prefer-arrow-callback': 'off', + 'symbol-description': 'off', + 'block-spacing': 'off', + 'space-before-blocks': 'off', + 'class-methods-use-this': 'off', + strict: 'off' + } }] }; diff --git a/.gitignore b/.gitignore index 032441d..e34d911 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ .DS_Store node_modules coverage + +build +src +src2 +testModule +foo.js +foo*.js diff --git a/NOTES2.md b/NOTES2.md new file mode 100644 index 0000000..761f22d --- /dev/null +++ b/NOTES2.md @@ -0,0 +1,14 @@ +Another question: + +If a plugin wants to provide non-JS resources as files, how does it do this? + +1. Reference them as `overlook@plugin-whatever/resources/file.css`? +2. Reference them with absolute path? +3. Reference them with relative path? +4. Use `@overlook/plugin-build` / `@overlook/plugin-virtual-fs` to write them into the app? + +(1) doesn't work because the plugin might be deeper in `node_modules`. +(2) would cause problems when app is built as absolute path would be different in deployment. +(3) would cause problems when app is built as relative path to the file from build dir could be different from what it is from src dir - e.g. if routes src dir is `/path/to/app/routes` and routes build dir is `/path/to/app/build/routes`. +(4) means many common things require a virtual file system to work. + diff --git a/NOTES3.md b/NOTES3.md new file mode 100644 index 0000000..ecdc5cf --- /dev/null +++ b/NOTES3.md @@ -0,0 +1,231 @@ +# Tracing process + +## 1. Trace globals + +Create record for global object: + +```js +{ + path: '', + packageDepth: 0, + pathDepth: 0, + keyPath: [], + keyPathMapSetDepth: 0, + js: 'global' +} +``` + +and for each property (and descendent property) of global object: + +```js +{ + path: '', + packageDepth: 0, + pathDepth: 0, + keyPath: [nameOfProp], // e.g. 'Object' + keyPathMapSetDepth: 0, + js: nameOfProp +} +``` + +## 2. Trace built-in packages + +Create record for each built-in package: + +```js +{ + path: nameOfPackage, // e.g. 'path' + packageDepth: 0, + pathDepth: 1, + keyPath: [], + keyPathMapSetDepth: 0 +} +``` + +and for each property (and descendent property) of built-in packages: + +```js +{ + path: nameOfPackage, + packageDepth: 0, + pathDepth: 1, + keyPath: [nameOfProp], + keyPathMapSetDepth: /* dependent on type of prop - see below */ +} +``` + +## 3. Traverse down module tree to root + +Then start with root and... + +## 4. Trace modules + +### 4a. If value is primitive + +Skip tracing for this module. Go on to child modules (step 5). + +### 4b. Get module path + +from `module.filename`. + +### 4c. If is REPL pseudo-module + +`module.filename` will be `null`. Skip tracing for this module. Go on to children. + +### 4d. Determine record details + +#### If path is below app root dir + +e.g. app root `/path/to/app/`, this module path `/path/to/foo.js` + +```js +{ + path: path, // Absolute path + packageDepth: Infinity, + pathDepth: Infinity, + keyPath: [], + keyPathMapSetDepth: 0 +} +``` + +**Or ignore it** + +#### Otherwise, if path contains `/node_modules/`: + +Parse package name and package path from path. + +e.g. path = `/path/to/node_modules/package-name/index.js` => `package-name`, `index.js` +e.g. path = `/path/to/node_modules/@package/name/index.js` => `@package/name`, `index.js` + +Locate any `package.json` files in dirs between first `node_modules` dir, and dir containing module file. Start at lowest level and work up. For each, `require()` `package.json`. + +If it has `main` field, check if it matches remaining file path. If so, stop searching. Strip off path up to and including first `node_modules/` + the part of path which is `main` field value. i.e. `/path/to/node_modules/@package/name/index.js` -> `@package/name`. + +If it has `exports` field, check if it matches remaining file path. If field exists but doesn't match, stop searching. Path remains absolute. + +If it does, continue searching upward for next `package.json`. + +If gets to the end without being stopped prematurely, strip off path up to and including first `node_modules/`. i.e. `/path/to/node_modules/@package/name/index.js` -> `@package/name/index.js`. + +Calculate package depth = Number of occurences of `/node_modules/` in path + 1. i.e. 1 or more. + +NB The +1 ensures built-in packages get priority. + +Calculate path depth = Number of slashes in path (what path remains at this stage). + +Record: + +```js +{ + path: path, // (after any stripping off, so may be absolute or package path) + packageDepth: packageDepth, // Number of occurences of `/node_modules/` in path + pathDepth: pathDepth, + keyPath: [], + keyPathMapSetDepth: 0 +} +``` + +#### Otherwise (file is not in a package) + +```js +{ + path: path, // Absolute path + packageDepth: Infinity, + pathDepth: pathDepth, // Number of slashes in path + keyPath: [], + keyPathMapSetDepth: 0 +} +``` + +### 4e. If already recorded + +Compare created record to existing: + +1. Lower `packageDepth` wins +2. Lower `keyPathMapSetDepth` wins +3. Lower `keyPath.length` wins +4. Lower `pathDepth` wins +5. Lowest path by alphabetic sorting wins +6. Lowest keyPath by alphabetic sorting wins + +**TODO Does a property of a route lose over property of another object?** + +```js +// NB A special object is used to represent prototype +const TYPE_SCORES = {number: 1, string: 2, symbol: 4, object: 5}; +function newRecordWins(oldRecord, newRecord) { + if (oldRecord.packageDepth < newRecord.packageDepth) return false; + if (oldRecord.packageDepth > newRecord.packageDepth) return true; + if (oldRecord.keyPathMapSetDepth < newRecord.keyPathMapSetDepth) return false; + if (oldRecord.keyPathMapSetDepth > newRecord.keyPathMapSetDepth) return true; + if (oldRecord.keyPath.length < newRecord.keyPath.length) return false; + if (oldRecord.keyPath.length > newRecord.keyPath.length) return true; + if (oldRecord.pathDepth < newRecord.pathDepth) return false; + if (oldRecord.pathDepth > newRecord.pathDepth) return true; + if (oldRecord.path < newRecord.path) return false; + if (oldRecord.path > newRecord.path) return true; + + for (const i = 0; i < oldRecord.keyPath.length; i++) { + const oldKey = oldRecord.keyPath[i]; + const newKey = newRecord.keyPath[i]; + const oldType = TYPE_SCORES[typeof oldKey]; + const newType = TYPE_SCORES[typeof newKey]; + if (oldType < newType) return false; + if (oldType > newType) return true; + if (oldKey < newKey) return false; + if (oldKey > newKey) return true; + } +} +``` + +If new record wins, replace old record with new. + +End tracing for this object. + +### 4f. Otherwise... + +Create record for object. + +### 4g. If is Route + +??? + +### 4h. Trace properties + +For each non-primitive property of the object, determine record. + +Properties keys include: + +* strings +* numbers (for Arrays, Sets, Maps) +* symbols +* `Object.getPrototypeOf(...)` (represented by a unique object `PROTO`) + +```js +{ + path: parentRecord.path, + packageDepth: parentRecord.packageDepth, + pathDepth: parentRecord.pathDepth, + keyPath: [...parentRecord.keyPath, key], + keyPathMapSetDepth: isMapOrSetEntry() + ? parentRecord.keyPathMapSetDepth + 1 + : parentrecord.keyPathMapSetDepth +} +``` +or: + +```js +{ + ...parentRecord, + keyPath: [...parentRecord.keyPath, key], + keyPathMapSetDepth: isMapOrSetEntry() + ? parentRecord.keyPathMapSetDepth + 1 + : parentrecord.keyPathMapSetDepth +} +``` + +For each property, pass record to step 4e. i.e. iteratively cover whole object. + +### 5. Trace child modules + +Go back to start of (4) with each child module. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..02265ae --- /dev/null +++ b/TODO.md @@ -0,0 +1,16 @@ +# TODO + +* Serialize plugins +* Serialize classes + functions using Babel +* Serialize function prototype + +* Ensure existing shared files + route files included in build +* Deal with circular references in object serialization +* Make a way for any object to be serialized to multiple lines +* Prefer `SRC_PATH` if defined for route file path +* Improve var naming e.g. for array items - currently not naming by parent + +* Handle property descriptors +* Serialize boxed primitives (`new String()` etc) - lave handles this +* Ensure serialization works for extending built-in classes e.g. RegExp +* Ensure serialization works for `new Function()` diff --git a/lib/debug.js b/lib/debug.js new file mode 100644 index 0000000..7c074ec --- /dev/null +++ b/lib/debug.js @@ -0,0 +1,47 @@ +/* -------------------- + * @overlook/plugin-build module + * Debugging + * ------------------*/ + +'use strict'; + +// Modules +const {isNumber, isSymbol} = require('is-it-type'); + +// Imports +const {PROTO, SYMBOL_KEYS, SET_OR_MAP_ENTRIES} = require('./trace.js'); + +// Exports + +module.exports = function getPath(val, records) { + let path; + const keyPath = []; + let node = val; + while (true) { // eslint-disable-line no-constant-condition + const nodeRecord = records.get(node); + path = nodeRecord.path; + + if (path === '') { + keyPath.unshift(nodeRecord.js); + break; + } + + if (path) break; + + node = nodeRecord.parent; + if (!node) break; + keyPath.unshift(serializeKey(nodeRecord.key)); + } + + const keyPathStr = keyPath.join(''); + return path ? `${path} ${keyPathStr}` : keyPathStr || ''; +}; + +function serializeKey(key) { + if (key === PROTO) return '[PROTO]'; + if (key === SYMBOL_KEYS) return '[SYMBOL_KEYS]'; + if (key === SET_OR_MAP_ENTRIES) return '[SET_OR_MAP_ENTRIES]'; + if (isSymbol(key)) return `[${key.toString()}]`; + if (isNumber(key)) return `[${key}]`; + return `.${key}`; +} diff --git a/lib/locate.js b/lib/locate.js index 17b9cfc..71622da 100644 --- a/lib/locate.js +++ b/lib/locate.js @@ -8,7 +8,7 @@ // Modules const {join: pathJoin, sep: pathSep} = require('path'), {isRoute} = require('@overlook/route'), - {isString} = require('is-it-type'); + {isString, isFunction} = require('is-it-type'); // Imports const {SYMBOL_KEYS, SET_OR_MAP_ENTRIES} = require('./trace.js'); @@ -20,6 +20,10 @@ const NODE_MODULES_PATH_SEGMENT = `${pathSep}node_modules${pathSep}`; module.exports = locate; +function isItemToDebug(val) { // eeslint-disable-line no-unused-vars + return isFunction(val) && val.toString() === 'function(x) { return x; }'; +} + /** * Determine best location for a value. * Can be (in order of preference): @@ -47,6 +51,17 @@ function locate(val, records) { const {locations} = record; if (!locations) return record; + const isDebugFn = false; // isItemToDebug(val); + const isDebugProto = false; // isItemToDebug(val.constructor); + // eslint-disable-next-line no-nested-ternary + const debugPrefix = isDebugFn ? 'fn' : isDebugProto ? 'prototype' : null; + const debug = debugPrefix + // eslint-disable-next-line no-console + ? (name, ...vals) => console.log(`${debugPrefix} ${name}:`, ...vals) + : () => {}; + + debug('record', record); + // Clear possible locations - to indicate this object is being worked on record.locations = undefined; @@ -66,10 +81,14 @@ function locate(val, records) { } else { // Get parent location const {parent} = thisLocation; + debug('thisLocation', thisLocation); const parentLocation = locate(parent, records); + debug('parentLocation', parentLocation); + // If in props of a route, note which route and skip const parentInDynamic = parentLocation.inDynamic; + if (parentInDynamic === true) continue; if (parentInDynamic) { if (!inDynamic) { inDynamic = parentInDynamic; @@ -101,6 +120,8 @@ function locate(val, records) { if (!location || isBetterLocation(thisLocation, location)) location = thisLocation; } + debug('location before', {location, inDynamic, isDynamic, hasCircular}); + if (location) { if (valIsRoute) location.inDynamic = val; } else { @@ -117,6 +138,7 @@ function locate(val, records) { } else { // Object only in 1 route - no location path = null; + if (hasCircular) inDynamic = true; } location = { @@ -130,6 +152,10 @@ function locate(val, records) { }; } + debug('location after', location, hasCircular); + + // if (isDebugFn) process.exit(); // eslint-disable-line no-process-exit + // Location is not definitively determined - restore possible locations and exit if (hasCircular) { record.locations = locations;