Closes #14 Closes #29 Closes #18

sdl
Valerie 2021-05-25 00:08:36 -04:00
parent 54db1823f4
commit 75f55fccaa
19 changed files with 316 additions and 153 deletions

11
examples/test/a.v 100644
View File

@ -0,0 +1,11 @@
static a;
restore() {
if(typeof b !== 'object') {
throw new Error('Static instance A does not exist!');
} else b.fun();
}
fun() {
console.log('hello from A');
}

11
examples/test/b.v 100644
View File

@ -0,0 +1,11 @@
static b;
restore() {
if(typeof a !== 'object') {
throw new Error('Static instance A does not exist!');
} else a.fun();
}
fun() {
console.log('hello from B');
}

View File

@ -1,4 +1,3 @@
singleton;
link counter;
link window;

View File

@ -12,20 +12,17 @@
},
"scripts": {
"dev": "multiview [ yarn test:watch ] [ yarn compile:watch ] [ yarn debug:watch ]",
"test": "c8 --all mocha",
"test": "c8 --include src/**/*.ts --reporter lcov --reporter text mocha",
"test:watch": "cross-env FORCE_COLOR=true supervisor -t -w src,test,.mocharc.json -n exit --extensions js,ts,node --exec cross-env -- yarn test",
"debug": "cross-env DEBUG=vogue:* FORCE_COLOR=true DEBUG_COLORS=true DEBUG_HIDE_DATE=true node --enable-source-maps --unhandled-rejections=strict out/run.js examples/test",
"debug": "cross-env DEBUG=vogue:* FORCE_COLOR=true DEBUG_COLORS=true DEBUG_HIDE_DATE=true node --enable-source-maps out/run.js examples/test",
"debug:watch": "supervisor -t -w out,test/system/**/*.v,lib -n exit --exec yarn -- debug",
"compile": "tsc",
"compile:watch": "yarn compile --watch --preserveWatchOutput",
"postinstall": "yarn compile && cd examples/test && yarn"
},
"devDependencies": {
"c8": "^7.7.2",
"cross-env": "^7.0.3",
"multiview": "^3.0.1",
"yarn": "^1.22.10",
"@types/chai": "^4.2.18",
"@types/chai-as-promised": "^7.1.4",
"@types/debug": "^4.1.5",
"@types/fs-extra": "^9.0.11",
"@types/jest": "^26.0.23",
@ -35,20 +32,29 @@
"@types/sinon": "^10.0.0",
"@types/uglify-js": "^3.13.0",
"@types/uuid": "^8.3.0",
"c8": "^7.7.2",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"cross-env": "^7.0.3",
"mocha": "^8.4.0",
"mocha-lcov-reporter": "^1.3.0",
"supervisor": "^0.12.0",
"multiview": "^3.0.1",
"sinon": "^10.0.0",
"supervisor": "^0.12.0",
"ts-node": "^9.1.1",
"typescript": "^4.2.4"
"typescript": "^4.2.4",
"yarn": "^1.22.10"
},
"dependencies": {
"@types/rimraf": "^3.0.0",
"@types/tmp": "^0.2.0",
"debug": "^4.3.1",
"fs-extra": "^10.0.0",
"lodash": "^4.17.21",
"moo": "^0.5.1",
"nearley": "^2.20.1",
"rimraf": "^3.0.2",
"tmp": "^0.2.1",
"uglify-js": "^3.13.5",
"uuid": "^8.3.2"
}

View File

@ -3,7 +3,7 @@ import _ from 'lodash';
const log = debug('vogue:instance');
import vm from 'vm';
import Module, { LinkDescription, Variable } from './Module.js';
import System from './System.js';
import { System } from './System.js';
import * as uuid from 'uuid';
/**
* @typedef {import('./System.js').default} System
@ -54,9 +54,11 @@ export default class Instance {
// TODO request context from system...
initialContext.create = this.system.newLink.bind(this.system);
initialContext.process = process;
for(const name in this.system.staticLinks) {
// static links!
for(const [name, link] of this.system.staticLinks) {
log('creating context with static link: ' + name);
initialContext[name] = this.system.staticLinks[name];
initialContext[name] = this.system.staticLinks.get(name);
}
// local links!
@ -89,9 +91,11 @@ export default class Instance {
vm.runInContext(injectedScript, context, {});
}
log('context created! ' + Object.keys(context));
log('context created!');
log(Object.keys(context));
// log(context);
this.context = context;
return context;
};
@ -107,7 +111,6 @@ export default class Instance {
constructor(
module: Module,
location: string,
parameters: {[name: string]: any},
system: System,
options?: {
@ -115,7 +118,7 @@ export default class Instance {
}
) {
this.module = module;
this.location = location;
this.location = this.module.rootDir;
this.system = system;
// this.context = this.createContext();
this._id = options?.id ?? uuid.v4();
@ -124,9 +127,6 @@ export default class Instance {
get(target: Instance, prop: string, receiver) {
log(`getting ${target.module.name.full}.${prop.toString()}: (${target.module.identifiers[prop]}|${typeof target.context?.[prop]})`);
if(target.context === null)
target.restore();
const DNEText = `${target.module.name.full}.${prop.toString()} either does not exist, or is not accessible`;
if(prop === 'restore') throw new Error(DNEText);
if(prop === '__link__') return target._id;
@ -140,11 +140,9 @@ export default class Instance {
log('created ' + this);
}
restore() {
if(this.context === null)
this.context = this.createContext();
async restore() {
return this.context?.restore?.();
return await this.context?.restore?.();
}
get link () {

View File

@ -5,7 +5,8 @@ import debug from 'debug';
import { lstatSync, readdirSync, readFileSync, writeFileSync } from 'fs';
import { resolve } from 'path';
import { ensureDirSync } from 'fs-extra';
const log = debug('vogue:system')
import * as uuid from 'uuid';
const log = debug('vogue:system');
const {get, set} = _;
@ -15,70 +16,44 @@ type ModuleNamespaceMap = {
type ModuleName = string;
class System {
export class System {
instances: Map<string, Instance> = new Map();
bootInstances: Map<string, Instance> = new Map();
staticInstances: Map<string, Instance> = new Map();
staticLinks: Map<string, Link> = new Map();
modules: Module[];
namespace: ModuleNamespaceMap = {};
staticLinks: {
[key: string]: Link
} = {};
rootDir: string;
constructor(modules: Module[], rootDir: string) {
this.rootDir = rootDir;
this.modules = modules;
this.createNamespace();
static async create(modules: Module[], rootDir: string) {
const system = new System(modules, rootDir);
await system.start();
return system;
}
async start() {
ensureDirSync(resolve(this.rootDir, '.system'));
const vault = readdirSync(resolve(this.rootDir, '.system')).map(v => resolve(this.rootDir, '.system', v));
const serializedInstances: SerializedInstance[] = vault.map((v) => JSON.parse(readFileSync(v).toString()));
log('System startup, in 5 steps!');
log('injecting serialized instances...');
// step 1.1: load serialized instances...
log('Step 1.1: reading serialized instances...')
const serializedInstances: SerializedInstance[] =
readdirSync(resolve(this.rootDir, '.system'))
.map(v => resolve(this.rootDir, '.system', v))
.map(v => JSON.parse(readFileSync(v).toString()));
// step 1.2: create serialized instances
log('Step 1.2: creating serialized instances...');
for(const serializedInstance of serializedInstances)
this.injectSerializedInstance(serializedInstance);
log('linking serialized instances...');
for(const serializedInstance of serializedInstances)
this.linkSerializedInstance(serializedInstance);
this.createInstance(serializedInstance);
log('restoring static instances...');
for(const [,instance] of this.instances) {
if(!!instance.module.static) {
instance.restore();
}
}
log('restoring boot instances...');
for(const [,instance] of this.instances) {
if(!!instance.module.singleton) {
instance.restore();
}
}
// this.inject(serializedInstance);
if (vault.length !== 0) {
return this;
}
// TODO future workflow notes
// pull jsons into boots
// filter json boots
// create static / singletons into boots
// boot boots!
const bootModules = this.deriveBootModules();
this.createStaticInstances();
log('instantiating boot modules...');
for(const name of bootModules) {
log(' ' + name);
this.newLink(name);
}
}
linkSerializedInstance(serializedInstance: SerializedInstance): void {
// TODO again, make links automagically tranform from strings to links when needed at runtime, so this isnt needed...
// step 1.2 addendum:
log('Step 1.2+: linking serialized instances...');
for(const serializedInstance of serializedInstances) {
const instance = this.getInstanceById(serializedInstance.id);
for(const name in serializedInstance.links) {
const linkId = serializedInstance.links[name]
@ -88,20 +63,110 @@ class System {
}
}
injectSerializedInstance(serializedInstance: SerializedInstance): void {
const instance = new Instance(this.getModule(serializedInstance.type), this.rootDir, {}, this, {
// step 2: validate all modules decalred as static have a static module counterpart
log('Step 2: creating missing static instances...');
for(const module of this.modules.filter(module => !!module.static)) {
const staticIdentifier = module.static;
const moduleName = module.name.full;
if(!this.staticLinks.has(staticIdentifier)) {
this.createInstance({
type: moduleName,
links: {},
members: {},
id: uuid.v4()
})
}
}
// step 3: create missing singleton instances
log('Step 3: creating missing singleton instances...');
for(const module of this.modules.filter(module => !!module.singleton)) {
let instanceCount = 0;
for(const [,instance] of this.instances) {
if(instance.module.name.full === module.name.full) {
instanceCount ++;
}
}
if(instanceCount === 0) {
this.createInstance({
type: module.name.full,
links: {},
members: {},
id: uuid.v4()
})
}
}
// step 4: create context in all instances
log('Step 4: create context in instances...');
for(const [,instance] of this.instances) {
instance.createContext();
}
// step 5: restore all boot modules (static & singleton)
log('Step 5: restore all boot modules...');
for(const [,instance] of this.bootInstances) {
await instance.restore();
}
// log('restoring static instances...');
// for(const [,instance] of this.instances) {
// if(!!instance.module.static) {
// await instance.restore();
// }
// }
// log('restoring boot instances...');
// for(const [,instance] of this.instances) {
// if(!!instance.module.singleton) {
// await instance.restore();
// }
// }
// const bootModules = this.deriveBootModules();
// this.createStaticInstances();
// log('instantiating boot modules...');
// for(const name of bootModules) {
// log(' ' + name);
// await this.newLink(name);
// }
}
private constructor(modules: Module[], rootDir: string) {
this.rootDir = rootDir;
this.modules = modules;
this.createNamespace();
}
createInstance(serializedInstance: SerializedInstance) {
// create instance
const instance = new Instance(this.getModule(serializedInstance.type), {}, this, {
id: serializedInstance.id
});
this.instances.set(instance._id, instance);
// preset members
for(const name in serializedInstance.members) {
instance.setMember(name, serializedInstance.members[name]);
}
// TODO preset links
// TODO consolidate links/members into args
// add to all isntances
this.instances.set(instance._id, instance);
// if its static, add it to both static instance data structures.
if(instance.module.static) {
log('injected static instance ' + instance.module.static + ': ' + instance.module.name.full);
this.staticLinks[instance.module.static] = instance.link;
this.staticLinks.set(instance.module.static, instance.link);
this.staticInstances.set(instance._id, instance);
}
// if it static, or just a singleton, add it to boot modules!
if(instance.module.singleton || instance.module.static) {
this.bootInstances.set(instance._id, instance);
}
return instance;
}
getInstanceById(id: string): Instance {
@ -111,23 +176,6 @@ class System {
return this.instances.get(id) as Instance;
}
createStaticInstances() {
log('deriving static modules...');
const staticModules = this.modules.filter((module) => {
return !!module.static;
}).map((module) => {
log(' ' + module.name.full);
return module;
});
log('instantiating static modules...');
for(const module of staticModules) {
log(' ' + module.static + ': ' + module.name.full);
this.staticLinks[module.static] =
this.newLink(module.name.full, {});
}
}
deriveBootModules() {
log('deriving boot modules...');
const bootModules = this.modules.filter((module) => {
@ -143,12 +191,10 @@ class System {
}
createNamespace() {
log('creating namespace map...');
this.namespace = this.modules.reduce((acc, val) => {
if(get(acc, val.name.full) instanceof Module)
throw new Error('Duplicate module "' + val.name.full + '"');
set(acc, val.name.full, val);
log(' ' + val.name.full);
return acc;
}, {});
}
@ -164,27 +210,25 @@ class System {
}
getModule(name: ModuleName): Module {
console.log('GETTING MODULE', name)
const module = get(this.namespace, name);
if(module instanceof Module) return module;
else throw Error(`${name} is not a module`);
else throw Error(`unknown module ${name}`);
}
createInstance(name: ModuleName, args = {}) {
const instance = new Instance(this.getModule(name), this.rootDir, args, this);
this.saveInstance(instance);
return instance;
}
newLink(name: ModuleName, args = {}) {
const instance = this.createInstance(name, args);
const link = instance.link;
instance.restore();
return link;
async newLink(name: ModuleName, args = {}) {
const instance = this.createInstance({
type: name,
links: args,
members: {},
id: uuid.v4()
});
instance.createContext();
await instance.restore();
return instance.link;
}
}
export default System;
// class SerializedInstanceInjector {
// system: System;
// serializedInstances: SerializedInstance[];

View File

@ -5,7 +5,10 @@ Object.defineProperty(Array.prototype, 'empty', {
}
});
// in theory we dont need this anymore... with strict promise rejections...
// process.on('unhandledRejection', (reason: Error, p) => {
// console.log(reason.stack ?? reason.name + '\n\nStack trace unavailable...');
// });
// ignored because there seems to be no good way to test this.
// would love to find one though...
/* c8 ignore start */
process.on('unhandledRejection', (reason: Error, p) => {
console.log(reason.stack ?? reason.name + '\n\nStack trace unavailable...');
});
/* c8 ignore end */

View File

@ -1,16 +0,0 @@
// import uglify from 'uglify-js';
// export default (code: string): string => {
// return uglify.minify(code, {
// compress: {
// dead_code: true,
// global_defs: {
// DEBUG: false
// }
// },
// sourceMap: {
// content: 'inline'
// }
// }).code;
// }

View File

@ -1,4 +1,4 @@
#!/usr/bin/env node --enable-source-maps --unhandled-rejections=strict
#!/usr/bin/env node --enable-source-maps
import debug from 'debug';
const log = debug('vogue:cli');
const systemLocation = resolve(process.argv[2]);
@ -8,7 +8,7 @@ import { readdirSync, lstatSync } from 'fs';
import _ from 'lodash';
import Module from './Module.js';
import System from './System.js';
import {System} from './System.js';
import './extensions.js';
import { fileURLToPath } from 'url';
@ -31,7 +31,7 @@ const standardLibrary = resolve(fileURLToPath(dirname(import.meta.url)), '..', '
log('parsing modules...');
const modules = await Promise.all(fullpaths.map(loc => Module.create(loc, systemLocation)));
const sys = new System(modules, systemLocation);
const sys = await System.create(modules, systemLocation);
})();
function walkdirSync(root: string, filter: ((path: string) => boolean) = () => true): string[] {

View File

@ -1,4 +1,4 @@
import { expect } from 'chai';
import { expect } from './lib/expect.js';
import '../src/extensions.ts';
describe('extensions', () => {

View File

@ -1,5 +1,5 @@
import { createAst } from '../src/createAst.js';
import { expect } from 'chai';
import { expect } from './lib/expect.js';
import * as ModuleFiles from './lib/ModuleFiles.js'
describe('Lexer', () => {

22
test/lib/System.ts 100644
View File

@ -0,0 +1,22 @@
import {System} from '../../src/System.js';
import * as tmp from 'tmp';
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
import { createAst } from '../../src/createAst.js';
import { readdirSync } from 'fs';
import Module from '../../src/Module.js';
const systemsPath = resolve(fileURLToPath(dirname(import.meta.url)), '..', 'systems');
export async function createSystem(systemName: string) {
const { name: tmpDir } = tmp.dirSync();
const modulesPath = resolve(systemsPath, systemName);
const modules = await Promise.all(readdirSync(modulesPath)
.map(v => resolve(modulesPath, v))
.map(v => Module.create(v, modulesPath)));
const system = await System.create(modules, tmpDir);
return system;
}

View File

@ -0,0 +1,5 @@
import * as chai from 'chai';
import cap from 'chai-as-promised';
chai.use(cap);
export const {expect} = chai;

View File

@ -0,0 +1,12 @@
import { createSystem } from './lib/System.js';
import { expect } from './lib/expect.js'
describe('system', () => {
it('cross linked static instances', (done) => {
expect(createSystem('crossStaticLinking')).to.eventually.be.fulfilled.notify(done);
});
it('non-existant module retrieval throws', (done) => {
expect(createSystem('unknownModule')).to.eventually.be.rejected.notify(done);
})
});

View File

@ -0,0 +1,11 @@
static a;
restore() {
if(typeof b !== 'object') {
throw new Error('Static instance A does not exist!');
} else b.fun();
}
fun() {
}

View File

@ -0,0 +1,11 @@
static b;
restore() {
if(typeof a !== 'object') {
throw new Error('Static instance A does not exist!');
} else a.fun();
}
fun() {
}

View File

@ -0,0 +1,5 @@
singleton;
async restore {
await create('this.module.doesnt.exist');
}

View File

@ -1,8 +1,9 @@
{
"compilerOptions": {
"module": "ES6",
"module": "esnext",
"moduleResolution": "node",
"target": "es2020"
"target": "es2020",
"allowSyntheticDefaultImports": true
},
"include": [
"**/*.ts"

View File

@ -50,7 +50,13 @@
version "0.7.1"
resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5"
"@types/chai@^4.2.18":
"@types/chai-as-promised@^7.1.4":
version "7.1.4"
resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.4.tgz#caf64e76fb056b8c8ced4b761ed499272b737601"
dependencies:
"@types/chai" "*"
"@types/chai@*", "@types/chai@^4.2.18":
version "4.2.18"
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.18.tgz#0c8e298dbff8205e2266606c1ea5fbdba29b46e4"
@ -64,6 +70,13 @@
dependencies:
"@types/node" "*"
"@types/glob@*":
version "7.1.3"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183"
dependencies:
"@types/minimatch" "*"
"@types/node" "*"
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
@ -91,6 +104,10 @@
version "4.14.170"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6"
"@types/minimatch@*":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.4.tgz#f0ec25dbf2f0e4b18647313ac031134ca5b24b21"
"@types/nearley@^2.11.1":
version "2.11.1"
resolved "https://registry.yarnpkg.com/@types/nearley/-/nearley-2.11.1.tgz#6ac3f57c00ca28071a1774ec72d2e45750f21420"
@ -99,12 +116,23 @@
version "15.6.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-15.6.0.tgz#f0ddca5a61e52627c9dcb771a6039d44694597bc"
"@types/rimraf@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-3.0.0.tgz#b9d03f090ece263671898d57bb7bb007023ac19f"
dependencies:
"@types/glob" "*"
"@types/node" "*"
"@types/sinon@^10.0.0":
version "10.0.0"
resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.0.tgz#eecc3847af03d45ffe53d55aaaaf6ecb28b5e584"
dependencies:
"@sinonjs/fake-timers" "^7.0.4"
"@types/tmp@^0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.2.0.tgz#e3f52b4d7397eaa9193592ef3fdd44dc0af4298c"
"@types/uglify-js@^3.13.0":
version "3.13.0"
resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.13.0.tgz#1cad8df1fb0b143c5aba08de5712ea9d1ff71124"
@ -228,6 +256,12 @@ camelcase@^6.0.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809"
chai-as-promised@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/chai-as-promised/-/chai-as-promised-7.1.1.tgz#08645d825deb8696ee61725dbf590c012eb00ca0"
dependencies:
check-error "^1.0.2"
chai@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.4.tgz#b55e655b31e1eac7099be4c08c21964fce2e6c49"
@ -859,7 +893,7 @@ ret@~0.1.10:
version "0.1.15"
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
rimraf@^3.0.0:
rimraf@^3.0.0, rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
dependencies:
@ -1015,6 +1049,12 @@ through@2:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
tmp@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
dependencies:
rimraf "^3.0.0"
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"