├── lib ├── require-argv2.js ├── compile.js ├── check-python.js ├── check.js ├── execWithMeasured.js ├── solutions.js ├── packagejson.js ├── execWith.js ├── copy.js ├── napiExercise.js └── gyp.js ├── exercises ├── objectification │ ├── exercise.js │ ├── solution │ │ └── solution.js │ └── problem.md ├── team_grunt_work │ ├── exercise.js │ ├── solution │ │ └── solution.js │ └── problem.md ├── its_all_about_scope │ ├── exercise.js │ ├── solution │ │ └── solution.js │ └── problem.md ├── objectify_all_the_things │ ├── exercise.js │ ├── solution │ │ └── solution.js │ └── problem.md ├── mission_possible_part_two │ ├── solution │ │ ├── solution.js │ │ ├── index.js │ │ ├── binding.gyp │ │ └── package.json │ ├── faux │ │ ├── index.js │ │ ├── package.json │ │ ├── binding.gyp │ │ └── myaddon.cc │ ├── problem.md │ └── exercise.js ├── mission_possible_part_three │ ├── faux │ │ ├── index.js │ │ ├── package.json │ │ ├── binding.gyp │ │ └── myaddon.cc │ ├── solution │ │ ├── index.js │ │ ├── binding.gyp │ │ ├── package.json │ │ └── myaddon.cc │ ├── boilerplate │ │ └── myaddon.cc │ ├── more.md │ ├── exercise.js │ └── problem.md ├── its_a_twoway_street │ ├── solution │ │ ├── index.js │ │ ├── binding.gyp │ │ ├── package.json │ │ └── myaddon.cc │ ├── problem.md │ └── exercise.js ├── hello_napi │ ├── exercise.js │ ├── solution │ │ ├── index.js │ │ ├── package.json │ │ ├── binding.gyp │ │ └── myaddon.cc │ └── problem.md ├── call_me_maybe │ ├── faux │ │ ├── index.js │ │ └── myaddon.cc │ ├── solution │ │ ├── index.js │ │ ├── binding.gyp │ │ ├── package.json │ │ └── myaddon.cc │ ├── problem.md │ └── exercise.js ├── for_the_sake_of_argument │ ├── solution │ │ ├── index.js │ │ ├── binding.gyp │ │ ├── package.json │ │ └── myaddon.cc │ ├── more.md │ ├── problem.md │ └── exercise.js ├── going_deep_into_napi │ ├── exercise.js │ ├── solution │ │ ├── binding.gyp │ │ ├── index.js │ │ ├── package.json │ │ └── myaddon.cc │ └── problem.md ├── mission_possible_part_one │ ├── boilerplate │ │ └── myaddon │ │ │ ├── binding.gyp │ │ │ └── package.json │ ├── solution │ │ ├── binding.gyp │ │ └── package.json │ ├── exercise.js │ ├── problem.md │ └── more.md ├── offloading_the_work │ ├── faux │ │ ├── binding.gyp │ │ ├── package.json │ │ ├── index.js │ │ └── myaddon.cc │ ├── solution │ │ ├── binding.gyp │ │ ├── package.json │ │ ├── index.js │ │ └── myaddon.cc │ ├── boilerplate │ │ ├── index.js │ │ └── myaddon.cc │ ├── more.md │ ├── problem.md │ └── exercise.js ├── menu.json └── am_i_ready │ ├── child.js │ ├── problem.md.tmpl │ ├── vars.json │ └── exercise.js ├── .travis.yml ├── packages └── test-addon │ ├── test.js │ ├── binding.gyp │ ├── package.json │ └── test.cc ├── goingnative.png ├── .gitignore ├── bin └── goingnative ├── i18n └── en.json ├── credits.js ├── credits.txt ├── help.txt ├── postinstall.js ├── goingnative.js ├── LICENSE.md ├── README.md ├── .jshintrc ├── test ├── check.js └── lib │ └── check-python.test.js ├── package.json └── CHANGELOG.md /lib/require-argv2.js: -------------------------------------------------------------------------------- 1 | require(process.argv[2]) 2 | -------------------------------------------------------------------------------- /exercises/objectification/exercise.js: -------------------------------------------------------------------------------- 1 | // code stuff here 2 | -------------------------------------------------------------------------------- /exercises/team_grunt_work/exercise.js: -------------------------------------------------------------------------------- 1 | // code stuff here 2 | -------------------------------------------------------------------------------- /exercises/its_all_about_scope/exercise.js: -------------------------------------------------------------------------------- 1 | // code stuff here 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | - lts/* 5 | -------------------------------------------------------------------------------- /exercises/its_all_about_scope/solution/solution.js: -------------------------------------------------------------------------------- 1 | // code stuff here 2 | -------------------------------------------------------------------------------- /exercises/objectification/solution/solution.js: -------------------------------------------------------------------------------- 1 | // code stuff here 2 | -------------------------------------------------------------------------------- /exercises/objectify_all_the_things/exercise.js: -------------------------------------------------------------------------------- 1 | // code stuff here 2 | -------------------------------------------------------------------------------- /exercises/team_grunt_work/solution/solution.js: -------------------------------------------------------------------------------- 1 | // code stuff here 2 | -------------------------------------------------------------------------------- /exercises/objectification/problem.md: -------------------------------------------------------------------------------- 1 | # Write stuff about OBJECTIFICATION here -------------------------------------------------------------------------------- /exercises/objectify_all_the_things/solution/solution.js: -------------------------------------------------------------------------------- 1 | // code stuff here 2 | -------------------------------------------------------------------------------- /exercises/team_grunt_work/problem.md: -------------------------------------------------------------------------------- 1 | # Write stuff about TEAM GRUNT WORK here -------------------------------------------------------------------------------- /exercises/mission_possible_part_two/solution/solution.js: -------------------------------------------------------------------------------- 1 | // code stuff here 2 | -------------------------------------------------------------------------------- /packages/test-addon/test.js: -------------------------------------------------------------------------------- 1 | module.exports = require('bindings')('test').test 2 | -------------------------------------------------------------------------------- /exercises/its_all_about_scope/problem.md: -------------------------------------------------------------------------------- 1 | # Write stuff about IT'S ALL ABOUT SCOPE here -------------------------------------------------------------------------------- /exercises/mission_possible_part_two/faux/index.js: -------------------------------------------------------------------------------- 1 | require('bindings')('myaddon').print() 2 | -------------------------------------------------------------------------------- /goingnative.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workshopper/goingnative/HEAD/goingnative.png -------------------------------------------------------------------------------- /exercises/mission_possible_part_three/faux/index.js: -------------------------------------------------------------------------------- 1 | require('bindings')('myaddon').print() 2 | -------------------------------------------------------------------------------- /exercises/objectify_all_the_things/problem.md: -------------------------------------------------------------------------------- 1 | # Write stuff about OBJECTIFY ALL THE THINGS here -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build/ 3 | exercises/am_i_ready/problem.md 4 | package-lock.json 5 | ~test-addon* 6 | -------------------------------------------------------------------------------- /bin/goingnative: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const workshopper = require('..') 3 | workshopper.execute(process.argv.slice(2)) 4 | -------------------------------------------------------------------------------- /exercises/its_a_twoway_street/solution/index.js: -------------------------------------------------------------------------------- 1 | const addon = require('bindings')('myaddon') 2 | 3 | console.log(addon.length(process.argv[2])) 4 | -------------------------------------------------------------------------------- /exercises/mission_possible_part_three/solution/index.js: -------------------------------------------------------------------------------- 1 | const bindings = require('bindings') 2 | const myaddon = bindings('myaddon') 3 | 4 | myaddon.print() 5 | -------------------------------------------------------------------------------- /exercises/mission_possible_part_two/solution/index.js: -------------------------------------------------------------------------------- 1 | const bindings = require('bindings') 2 | const myaddon = bindings('myaddon') 3 | 4 | myaddon.print() 5 | -------------------------------------------------------------------------------- /exercises/hello_napi/exercise.js: -------------------------------------------------------------------------------- 1 | const exercise = require('../../lib/napiExercise') 2 | 3 | exercise.expected = 'hello NAPI!' 4 | 5 | module.exports = exercise 6 | -------------------------------------------------------------------------------- /exercises/call_me_maybe/faux/index.js: -------------------------------------------------------------------------------- 1 | const addon = require('bindings')('myaddon') 2 | 3 | addon.delay(process.argv[2], function () { 4 | console.log('Done!') 5 | }) 6 | -------------------------------------------------------------------------------- /exercises/for_the_sake_of_argument/solution/index.js: -------------------------------------------------------------------------------- 1 | const bindings = require('bindings') 2 | const myaddon = bindings('myaddon') 3 | 4 | myaddon.print(process.argv[2]) 5 | -------------------------------------------------------------------------------- /exercises/call_me_maybe/solution/index.js: -------------------------------------------------------------------------------- 1 | const addon = require('bindings')('myaddon') 2 | 3 | addon.delay(process.argv[2], function () { 4 | console.log('Done!') 5 | }) 6 | -------------------------------------------------------------------------------- /exercises/going_deep_into_napi/exercise.js: -------------------------------------------------------------------------------- 1 | const exercise = require('../../lib/napiExercise') 2 | 3 | exercise.expected = 'hello N-API!' 4 | 5 | module.exports = exercise 6 | -------------------------------------------------------------------------------- /exercises/mission_possible_part_two/faux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myaddon", 3 | "version": "0.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "gypfile": true 7 | } -------------------------------------------------------------------------------- /exercises/going_deep_into_napi/solution/binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "myaddon", 5 | "sources": [ "myaddon.cc" ] 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /exercises/hello_napi/solution/index.js: -------------------------------------------------------------------------------- 1 | const bindings = require('bindings') 2 | const myaddon = bindings('myaddon') 3 | 4 | const greeting = myaddon.hello() 5 | console.log(greeting) 6 | -------------------------------------------------------------------------------- /exercises/mission_possible_part_three/faux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myaddon", 3 | "version": "0.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "gypfile": true 7 | } -------------------------------------------------------------------------------- /exercises/going_deep_into_napi/solution/index.js: -------------------------------------------------------------------------------- 1 | const bindings = require('bindings') 2 | const myaddon = bindings('myaddon') 3 | 4 | const greeting = myaddon.hello() 5 | console.log(greeting) 6 | -------------------------------------------------------------------------------- /exercises/mission_possible_part_one/boilerplate/myaddon/binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "derp", 5 | "sources": [ "derp.cc" ], 6 | "include_dirs": [ ] 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /exercises/call_me_maybe/solution/binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "myaddon", 5 | "sources": [ "myaddon.cc" ], 6 | "include_dirs": [ " 2 | 3 | using namespace v8; 4 | 5 | NAN_METHOD(Print) { 6 | 7 | } 8 | 9 | NAN_MODULE_INIT(Init) { 10 | Nan::Set(target, Nan::New("print").ToLocalChecked(), 11 | Nan::GetFunction(Nan::New(Print)).ToLocalChecked()); 12 | } 13 | -------------------------------------------------------------------------------- /packages/test-addon/test.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void Init(v8::Local exports) { 4 | v8::Local context = exports->CreationContext(); 5 | exports->Set(context, 6 | Nan::New("test").ToLocalChecked(), 7 | Nan::New("OK").ToLocalChecked()); 8 | } 9 | 10 | NODE_MODULE(test, Init) 11 | -------------------------------------------------------------------------------- /exercises/offloading_the_work/faux/index.js: -------------------------------------------------------------------------------- 1 | const addon = require('bindings')('myaddon') 2 | 3 | const interval = setInterval(function () { 4 | process.stdout.write('.') 5 | }, 50) 6 | 7 | addon.delay(process.argv[2], function () { 8 | clearInterval(interval) 9 | console.log('Done!') 10 | }) 11 | 12 | process.stdout.write('Waiting') 13 | -------------------------------------------------------------------------------- /exercises/offloading_the_work/solution/index.js: -------------------------------------------------------------------------------- 1 | const addon = require('bindings')('myaddon') 2 | 3 | const interval = setInterval(function () { 4 | process.stdout.write('.') 5 | }, 50) 6 | 7 | addon.delay(process.argv[2], function () { 8 | clearInterval(interval) 9 | console.log('Done!') 10 | }) 11 | 12 | process.stdout.write('Waiting') 13 | -------------------------------------------------------------------------------- /exercises/offloading_the_work/boilerplate/index.js: -------------------------------------------------------------------------------- 1 | const addon = require('bindings')('myaddon') 2 | 3 | const interval = setInterval(function () { 4 | process.stdout.write('.') 5 | }, 50) 6 | 7 | addon.delay(process.argv[2], function () { 8 | clearInterval(interval) 9 | console.log('Done!') 10 | }) 11 | 12 | process.stdout.write('Waiting') 13 | -------------------------------------------------------------------------------- /credits.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const colorsTmpl = require('colors-tmpl') 4 | 5 | function credits () { 6 | fs.readFile(path.join(__dirname, './credits.txt'), 'utf8', function (err, data) { 7 | if (err) { throw err } 8 | 9 | console.log(colorsTmpl(data)) 10 | }) 11 | } 12 | 13 | module.exports = credits 14 | -------------------------------------------------------------------------------- /exercises/mission_possible_part_one/boilerplate/myaddon/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myaddon", 3 | "version": "1.0.0", 4 | "description": "My Awesome Addon", 5 | "main": "index.js", 6 | "scripts": { 7 | "install": "node-gyp rebuild" 8 | }, 9 | "license": "MIT", 10 | "dependencies": { 11 | "bindings": "^1.2.1", 12 | "nan": "^2.2.9" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /exercises/mission_possible_part_three/faux/myaddon.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | using namespace v8; 4 | 5 | NAN_METHOD(Print) { 6 | printf("FAUX\n"); 7 | } 8 | 9 | NAN_MODULE_INIT(Init) { 10 | Nan::Set(target, Nan::New("print").ToLocalChecked(), 11 | Nan::GetFunction(Nan::New(Print)).ToLocalChecked()); 12 | } 13 | 14 | NODE_MODULE(myaddon, Init) 15 | -------------------------------------------------------------------------------- /exercises/mission_possible_part_two/faux/myaddon.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | using namespace v8; 4 | 5 | NAN_METHOD(Print) { 6 | printf("FAUX\n"); 7 | } 8 | 9 | NAN_MODULE_INIT(Init) { 10 | Nan::Set(target, Nan::New("print").ToLocalChecked(), 11 | Nan::GetFunction(Nan::New(Print)).ToLocalChecked()); 12 | } 13 | 14 | NODE_MODULE(myaddon, Init) 15 | -------------------------------------------------------------------------------- /credits.txt: -------------------------------------------------------------------------------- 1 | {yellow}{bold}goingnative is brought to you by the following dedicated hackers:{/bold}{/yellow} 2 | 3 | {bold}Name GitHub Username{/bold} 4 | ----------------------------------- 5 | Rod Vagg @rvagg 6 | Will Blankenship @wblankenship 7 | Benjamin Byholm @kkoopa 8 | Trevor Norris @trevnorris 9 | Adam Brady @SomeoneWeird 10 | -------------------------------------------------------------------------------- /exercises/mission_possible_part_three/solution/myaddon.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | using namespace v8; 4 | 5 | NAN_METHOD(Print) { 6 | printf("I am a native addon and I AM ALIVE!\n"); 7 | } 8 | 9 | NAN_MODULE_INIT(Init) { 10 | Nan::Set(target, Nan::New("print").ToLocalChecked(), 11 | Nan::GetFunction(Nan::New(Print)).ToLocalChecked()); 12 | } 13 | 14 | NODE_MODULE(myaddon, Init) 15 | -------------------------------------------------------------------------------- /exercises/hello_napi/solution/myaddon.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | Napi::Value Method(const Napi::CallbackInfo& info) { 4 | Napi::Env env = info.Env(); 5 | return Napi::String::New(env, "hello NAPI!"); 6 | } 7 | 8 | Napi::Object Init(Napi::Env env, Napi::Object exports) { 9 | exports.Set(Napi::String::New(env, "hello"), 10 | Napi::Function::New(env, Method)); 11 | return exports; 12 | } 13 | 14 | NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init) 15 | -------------------------------------------------------------------------------- /help.txt: -------------------------------------------------------------------------------- 1 | {yellow}{bold}Having trouble with a {appname} exercise?{/bold}{/yellow} 2 | 3 | Simply go to: 4 | https://github.com/nodeschool/discussions/issues 5 | and add a {bold}New Issue{/bold} and let us know what you're having trouble 6 | with. There are no {italic}dumb{/italic} questions! 7 | 8 | {yellow}{bold}Found a bug with {appname} or just want to contribute?{/bold}{/yellow} 9 | 10 | The official repository for {appname} is: 11 | https://github.com/rvagg/{appname}/ 12 | Feel free to file a bug report or (preferably) a pull request. -------------------------------------------------------------------------------- /exercises/am_i_ready/child.js: -------------------------------------------------------------------------------- 1 | const bindings = require('bindings') 2 | 3 | let binding 4 | 5 | try { 6 | binding = bindings({ module_root: process.argv[2], bindings: 'test' }) 7 | } catch (e) { 8 | console.error('Could not properly compile test addon, error finding binding: ' + e.message) 9 | } 10 | 11 | console.log('Found compiled test binding file') 12 | 13 | if (!binding) { 14 | console.error('Could not properly compile test addon, did not load binding') 15 | } 16 | 17 | if (binding.test !== 'OK') { 18 | console.error('Could not properly compile test addon, binding did not behave properly') 19 | } 20 | -------------------------------------------------------------------------------- /exercises/for_the_sake_of_argument/solution/myaddon.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | using namespace v8; 4 | 5 | NAN_METHOD(Print) { 6 | Nan::MaybeLocal maybeStr = Nan::To(info[0]); 7 | v8::Local str; 8 | if (maybeStr.ToLocal(&str) == false) { 9 | Nan::ThrowError("Error converting first argument to string"); 10 | } 11 | printf("%s\n", *Nan::Utf8String(str)); 12 | } 13 | 14 | NAN_MODULE_INIT(Init) { 15 | Nan::Set(target, Nan::New("print").ToLocalChecked(), 16 | Nan::GetFunction(Nan::New(Print)).ToLocalChecked()); 17 | } 18 | 19 | NODE_MODULE(myaddon, Init) 20 | -------------------------------------------------------------------------------- /lib/compile.js: -------------------------------------------------------------------------------- 1 | const gyp = require('./gyp') 2 | 3 | // run a `node-gyp rebuild` on their unmolested code in our copy 4 | function checkCompile (dir) { 5 | return function (mode, callback) { 6 | const exercise = this 7 | 8 | if (!exercise.passed) { return callback(null, true) } // shortcut if we've already had a failure 9 | 10 | gyp.rebuild(dir, function (err) { 11 | if (err) { 12 | exercise.emit('fail', err.message) 13 | return callback(null, false) 14 | } 15 | 16 | callback(null, true) 17 | }) 18 | } 19 | } 20 | 21 | module.exports.checkCompile = checkCompile 22 | -------------------------------------------------------------------------------- /exercises/its_a_twoway_street/solution/myaddon.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | using namespace v8; 4 | 5 | NAN_METHOD(Length) { 6 | Nan::MaybeLocal maybeStr = Nan::To(info[0]); 7 | v8::Local str; 8 | 9 | if (maybeStr.ToLocal(&str) == false) { 10 | Nan::ThrowError("Error converting first argument to string"); 11 | } 12 | 13 | int len = strlen(*Nan::Utf8String(str)); 14 | 15 | info.GetReturnValue().Set(len); 16 | } 17 | 18 | NAN_MODULE_INIT(Init) { 19 | Nan::Set(target, Nan::New("length").ToLocalChecked(), 20 | Nan::GetFunction(Nan::New(Length)).ToLocalChecked()); 21 | } 22 | 23 | NODE_MODULE(myaddon, Init) 24 | -------------------------------------------------------------------------------- /postinstall.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const varstring = require('varstring') 3 | const getos = require('getos') 4 | const fs = require('fs') 5 | const instructions = require('./exercises/am_i_ready/vars.json').instructions 6 | 7 | const tmpl = path.join(__dirname, 'exercises/am_i_ready/problem.md.tmpl') 8 | const out = path.join(__dirname, 'exercises/am_i_ready/problem.md') 9 | const problem = fs.readFileSync(tmpl, 'utf-8') 10 | 11 | getos(function (err, os) { 12 | if (err) { throw err } 13 | 14 | const lookup = os.dist ? os.dist : os.os 15 | const markdown = varstring(problem, instructions[lookup] || instructions.Other) 16 | 17 | fs.writeFileSync(out, markdown, 'utf8') 18 | }) 19 | -------------------------------------------------------------------------------- /lib/check-python.js: -------------------------------------------------------------------------------- 1 | const { promisify } = require('util') 2 | const execFile = promisify(require('child_process').execFile) 3 | const findPython = require('node-gyp/lib/find-python') 4 | 5 | const checkPython = function (mode, callback) { 6 | const exercise = this 7 | 8 | findPython({}, async (err, python) => { 9 | if (err) { 10 | exercise.emit('fail', exercise.__('fail.python', { message: err.message })) 11 | return callback(null, false) 12 | } 13 | 14 | const { stdout } = await execFile(python, ['-V']).catch(e => console.log(e)) 15 | const version = stdout.split(' ')[1] 16 | exercise.emit('pass', exercise.__('pass.python', { version })) 17 | callback(null, true) 18 | }) 19 | } 20 | 21 | module.exports = checkPython 22 | -------------------------------------------------------------------------------- /exercises/going_deep_into_napi/solution/myaddon.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | napi_value Method(napi_env env, napi_callback_info info) { 4 | napi_value greeting; 5 | napi_status status; 6 | 7 | status = napi_create_string_utf8(env, "hello N-API!", NAPI_AUTO_LENGTH, &greeting); 8 | if (status != napi_ok) return nullptr; 9 | return greeting; 10 | } 11 | 12 | napi_value Init(napi_env env, napi_value exports) { 13 | napi_status status; 14 | napi_value fn; 15 | 16 | status = napi_create_function(env, nullptr, 0, Method, nullptr, &fn); 17 | if (status != napi_ok) return nullptr; 18 | 19 | status = napi_set_named_property(env, exports, "hello", fn); 20 | if (status != napi_ok) return nullptr; 21 | return exports; 22 | } 23 | 24 | NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) 25 | -------------------------------------------------------------------------------- /lib/check.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | // simple check to see they are running a verify or run with an actual directory 4 | function checkSubmissionDir (mode, callback) { 5 | const exercise = this 6 | 7 | exercise.submission = this.args[0] // submission first arg obviously 8 | 9 | function failBadPath () { 10 | exercise.emit('fail', 'Submitted a readable directory path (please supply a path to your solution)') 11 | callback(null, false) 12 | } 13 | 14 | if (!exercise.submission) { return failBadPath() } 15 | 16 | fs.stat(exercise.submission, function (err, stat) { 17 | if (err) { return failBadPath() } 18 | 19 | if (!stat.isDirectory()) { return failBadPath() } 20 | 21 | callback(null, true) 22 | }) 23 | } 24 | 25 | module.exports.checkSubmissionDir = checkSubmissionDir 26 | -------------------------------------------------------------------------------- /lib/execWithMeasured.js: -------------------------------------------------------------------------------- 1 | const execWith = require('./execWith') 2 | 3 | /** 4 | * Same as ./execWith but executes twice to make sure that the vm is prewarmed. 5 | * and returns a delay to the result 6 | */ 7 | module.exports = function execWithMeasured (dir, arg, expect, options, callback) { 8 | if (!callback) { 9 | callback = options 10 | } 11 | 12 | // Run the code the first time to "warm"-up the binary loading vm 13 | execWith(dir, arg, expect, options, function onColdResult (err) { 14 | if (err) { return callback(err) } 15 | 16 | // Execute and measure the now warm code 17 | const start = Date.now() 18 | execWith(dir, arg, expect, options, function onWarmResult (err, pass) { 19 | if (err) { return callback(err) } 20 | callback(null, { 21 | duration: Date.now() - start, 22 | pass: pass 23 | }) 24 | }) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /exercises/mission_possible_part_three/more.md: -------------------------------------------------------------------------------- 1 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 2 | 3 | # MORE 4 | 5 | ## NAN_METHOD() 6 | 7 | The version of V8 shipping with Node.js 0.10 and prior uses a signature for methods that are exposed into JavaScript that is *completely* different to that of the V8 in 0.11 and later. This complexity is hidden from you by NAN via the `NAN_METHOD` macro. We will see later how this provides you with a consistent `info` array and how you can return values into JavaScript from your C++. It may be slightly opaque to read, but hey, this is C++ so opacity is mandatory! 8 | 9 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 10 | 11 | __»__ To print the instructions again, run: `{appname} print` 12 | __»__ To compile and test your solution, run: `{appname} verify myaddon/` 13 | __»__ For help run: `{appname} help` 14 | -------------------------------------------------------------------------------- /lib/solutions.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | function solutions (exercise, files) { 5 | exercise.getSolutionFiles = function (callback) { 6 | const solutionDir = path.join(this.dir, './solution/') 7 | 8 | fs.readdir(solutionDir, function (err, list) { 9 | if (err) return callback(err) 10 | 11 | try { 12 | callback( 13 | null, 14 | list 15 | .filter(function (f) { 16 | return !files || files.indexOf(f) > -1 17 | }) 18 | .map(function (f) { 19 | return path.join(solutionDir, f) 20 | }) 21 | .filter(function (f) { 22 | return fs.statSync(f).isFile() 23 | }) 24 | ) 25 | } catch (err) { 26 | callback(err) 27 | } 28 | }) 29 | } 30 | 31 | return exercise 32 | } 33 | 34 | module.exports = solutions 35 | -------------------------------------------------------------------------------- /lib/packagejson.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | // inspect package.json, make sure it's parsable and check that it has 5 | // "gyp":true 6 | function checkPackageJson (mode, callback) { 7 | const exercise = this 8 | 9 | function fail (msg) { 10 | exercise.emit('fail', msg) 11 | return callback(null, false) 12 | } 13 | 14 | fs.readFile(path.join(exercise.submission, 'package.json'), 'utf8', function (err, data) { 15 | if (err) return fail('Read package.json (' + err.message + ')') 16 | 17 | let doc 18 | 19 | try { 20 | doc = JSON.parse(data) 21 | } catch (e) { 22 | return fail('Parse package.json (' + e.message + ')') 23 | } 24 | 25 | const gypfile = doc.gypfile === true 26 | 27 | exercise.emit(gypfile ? 'pass' : 'fail', 'package.json contains `"gypfile": true`') 28 | 29 | callback(null, gypfile) 30 | }) 31 | } 32 | 33 | module.exports.checkPackageJson = checkPackageJson 34 | -------------------------------------------------------------------------------- /exercises/call_me_maybe/solution/myaddon.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #ifndef _WIN32 3 | # include 4 | #endif 5 | 6 | using namespace v8; 7 | 8 | NAN_METHOD(Delay) { 9 | Nan::Maybe maybeDelay = Nan::To(info[0]); 10 | 11 | if (maybeDelay.IsNothing() == true) { 12 | Nan::ThrowError("Error converting first argument to integer"); 13 | } 14 | 15 | int delay = maybeDelay.FromJust(); 16 | 17 | if (info[1]->IsFunction() == false) { 18 | Nan::ThrowError("Error converting second argument to function"); 19 | } 20 | 21 | #ifdef _WIN32 22 | Sleep(delay); 23 | #else 24 | usleep(delay * 1000); 25 | #endif 26 | 27 | v8::Local callback = info[1].As(); 28 | Nan::MakeCallback(Nan::GetCurrentContext()->Global(), callback, 0, NULL); 29 | } 30 | 31 | NAN_MODULE_INIT(Init) { 32 | Nan::Set(target, Nan::New("delay").ToLocalChecked(), 33 | Nan::GetFunction(Nan::New(Delay)).ToLocalChecked()); 34 | } 35 | 36 | NODE_MODULE(myaddon, Init) 37 | -------------------------------------------------------------------------------- /exercises/call_me_maybe/faux/myaddon.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #ifndef _WIN32 3 | # include 4 | #endif 5 | 6 | using namespace v8; 7 | 8 | NAN_METHOD(Delay) { 9 | Nan::Maybe maybeDelay = Nan::To(info[0]); 10 | 11 | if (maybeDelay.IsNothing() == true) { 12 | Nan::ThrowError("Error converting first argument to integer"); 13 | } 14 | 15 | int delay = maybeDelay.FromJust(); 16 | 17 | if (info[1]->IsFunction() == false) { 18 | Nan::ThrowError("Error converting second argument to function"); 19 | } 20 | 21 | printf("FAUX %d\n", delay); 22 | fflush(stdout); 23 | 24 | #ifdef _WIN32 25 | Sleep(delay); 26 | #else 27 | usleep(delay * 1000); 28 | #endif 29 | 30 | v8::Local callback = info[1].As(); 31 | Nan::MakeCallback(Nan::GetCurrentContext()->Global(), callback, 0, NULL); 32 | } 33 | 34 | NAN_MODULE_INIT(Init) { 35 | Nan::Set(target, Nan::New("delay").ToLocalChecked(), 36 | Nan::GetFunction(Nan::New(Delay)).ToLocalChecked()); 37 | } 38 | 39 | NODE_MODULE(myaddon, Init) 40 | -------------------------------------------------------------------------------- /lib/execWith.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process') 2 | 3 | function defaultProcessPass (pass, stdout, stderr) { 4 | if (!pass) { 5 | process.stderr.write(stderr) 6 | process.stdout.write(stdout) 7 | } 8 | } 9 | 10 | function defaultResolvePass (expected, stdout) { 11 | return stdout.toString().replace('\r', '') === expected 12 | } 13 | 14 | function execWith (dir, arg, expect, options, callback) { 15 | if (!callback) { 16 | callback = options 17 | } 18 | 19 | exec(`'${process.execPath}' '${dir}' '${arg}'`, function (err, stdout, stderr) { 20 | if (err) { 21 | process.stderr.write(stderr) 22 | process.stdout.write(stdout) 23 | return callback(err) 24 | } 25 | 26 | const expected = typeof (expect) === 'function' ? expect(arg) : expect 27 | const resolvePass = options.resolvePass || defaultResolvePass 28 | const processPass = options.processPass || defaultProcessPass 29 | 30 | const pass = resolvePass(expected, stdout) 31 | 32 | processPass(pass, stdout, stderr) 33 | 34 | callback(null, pass) 35 | }) 36 | } 37 | 38 | module.exports = execWith 39 | -------------------------------------------------------------------------------- /goingnative.js: -------------------------------------------------------------------------------- 1 | const Workshopper = require('workshopper-adventure') 2 | const path = require('path') 3 | const credits = require('./credits') 4 | const menu = require('./exercises/menu') 5 | const hooray = require('workshopper-hooray') 6 | const more = require('workshopper-more') 7 | 8 | const appname = 'goingnative' 9 | const title = 'Going Native' 10 | const subtitle = '\x1b[23mSelect an exercise and hit \x1b[3mEnter\x1b[23m to begin' 11 | 12 | function fpath (f) { 13 | return path.join(__dirname, f) 14 | } 15 | 16 | const workshopper = Workshopper({ 17 | name: appname, 18 | title: title, 19 | subtitle: subtitle, 20 | exerciseDir: fpath('./exercises/'), 21 | appDir: __dirname, 22 | helpFile: fpath('help.txt'), 23 | footerFile: false, 24 | menu: { 25 | fs: 'white', 26 | bg: 'black' 27 | }, 28 | menuItems: [ 29 | { 30 | name: 'credits', 31 | handler: credits 32 | }, 33 | { 34 | name: 'more', 35 | menu: false, 36 | short: 'm', 37 | handler: more 38 | } 39 | ], 40 | onComplete: hooray 41 | }) 42 | 43 | workshopper.addAll(menu) 44 | module.exports = workshopper 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright (c) 2014 Rod Vagg 5 | --------------------------- 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # goingnative 3 | **A NodeSchool style workshopper for learning how to write native Node.js addons** 4 | 5 | [![NPM](https://nodei.co/npm/goingnative.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/goingnative/) [![NPM](https://nodei.co/npm-dl/goingnative.png?months=3&height=3)](https://nodei.co/npm/goingnative/) 6 | 7 | ![goingnative](https://github.com/rvagg/goingnative/raw/master/goingnative.png) 8 | 9 | ***Please note this is a work in progress and is far from complete but will serve as an interesting introduction*** 10 | 11 | ```sh 12 | sudo npm install goingnative -g 13 | goingnative 14 | ``` 15 | 16 | ## Contributors 17 | 18 | * [Will Blankenship](https://github.com/wblankenship) 19 | * [Benjamin Byholm](https://github.com/kkoopa) 20 | * [Trevor Norris](https://github.com/trevnorris) 21 | * [Adam Brady](https://github.com/someoneweird) 22 | 23 | ## License 24 | 25 | **goingnative** is Copyright (c) 2014 Rod Vagg [@rvagg](https://twitter.com/rvagg) and contributors licensed under the MIT License. All rights not explicitly granted in the MIT License are reserved. See the included [LICENSE.md](./LICENSE.md) file for more details. 26 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ ] 3 | , "bitwise": false 4 | , "camelcase": false 5 | , "curly": false 6 | , "eqeqeq": false 7 | , "forin": false 8 | , "immed": false 9 | , "latedef": false 10 | , "noarg": true 11 | , "noempty": true 12 | , "nonew": true 13 | , "plusplus": false 14 | , "quotmark": true 15 | , "regexp": false 16 | , "undef": true 17 | , "unused": true 18 | , "strict": false 19 | , "trailing": true 20 | , "maxlen": 120 21 | , "asi": true 22 | , "boss": true 23 | , "debug": true 24 | , "eqnull": true 25 | , "evil": true 26 | , "expr": true 27 | , "funcscope": false 28 | , "globalstrict": false 29 | , "iterator": false 30 | , "lastsemic": true 31 | , "laxbreak": true 32 | , "laxcomma": true 33 | , "loopfunc": true 34 | , "multistr": false 35 | , "onecase": false 36 | , "proto": false 37 | , "regexdash": false 38 | , "scripturl": true 39 | , "smarttabs": false 40 | , "shadow": false 41 | , "sub": true 42 | , "supernew": false 43 | , "validthis": true 44 | , "browser": true 45 | , "couch": false 46 | , "devel": false 47 | , "dojo": false 48 | , "mootools": false 49 | , "node": true 50 | , "nonstandard": true 51 | , "prototypejs": false 52 | , "rhino": false 53 | , "worker": true 54 | , "wsh": false 55 | , "nomen": false 56 | , "onevar": false 57 | , "passfail": false 58 | , "esversion": 9 59 | } 60 | -------------------------------------------------------------------------------- /test/check.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process') 2 | const path = require('path') 3 | const fsp = require('fs').promises 4 | const test = require('tape') 5 | const { idFromName } = require('workshopper-adventure/util') 6 | 7 | const cleanFile = async (path, callback) => { 8 | fsp.access(path) 9 | .then(() => callback(path)) 10 | .catch(() => {}) 11 | } 12 | 13 | const exercises = require('../exercises/menu.json') 14 | exercises.forEach(function (name) { 15 | test(name, function (t) { 16 | t.plan(2) 17 | const nameId = idFromName(name) 18 | const solution = path.join(__dirname, '../exercises', nameId, 'solution') 19 | 20 | const ps = run(['select', name]) 21 | ps.on('exit', selected) 22 | ps.stderr.pipe(process.stderr) 23 | 24 | function selected (code) { 25 | t.equal(code, 0) 26 | const ps = run(['verify', solution]) 27 | ps.on('exit', verified) 28 | ps.stderr.pipe(process.stderr) 29 | test.onFailure(() => { 30 | ps.stdout.pipe(process.stdout) 31 | }) 32 | } 33 | 34 | async function verified (code) { 35 | t.equal(code, 0) 36 | 37 | cleanFile('myaddon', async (path) => fsp.rmdir(path, { recursive: true })) 38 | cleanFile('myaddon.cc', async (path) => fsp.unlink(path)) 39 | cleanFile('index.js', async (path) => fsp.unlink(path)) 40 | } 41 | }) 42 | }) 43 | 44 | function run (args) { 45 | args.unshift(path.join(__dirname, '../goingnative.js')) 46 | return spawn(process.execPath, args) 47 | } 48 | -------------------------------------------------------------------------------- /exercises/am_i_ready/problem.md.tmpl: -------------------------------------------------------------------------------- 1 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 2 | 3 | ## Task 4 | 5 | Prepare your development environment for native Node.js add-ons 6 | 7 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 8 | 9 | ## Description 10 | 11 | To pass this exercise you simply need to have a development environment with all the required tools installed. 12 | 13 | You need: 14 | 15 | A recent and **stable version of Node.js**: unstable releases are not tested with this workshop and should be avoided. Visit http://nodejs.org/ to download a new version or use your local package manager to upgrade. 16 | 17 | A **compiler**: specifically, a (non-ancient) C++ compiler, we will require **gcc** or LLVM gcc on OS X. {gcc} 18 | 19 | **Python**: version 2.7.x is preferred for use by GYP, a build generation tool used for configuring and compiling Node.js add-ons. {python} 20 | 21 | **node-gyp**: a recent version installed globally with `sudo npm install node-gyp -g`. node-gyp wraps GYP and produces builds specifically targeted for Node.js. Install with `sudo npm install -g node-gyp`. 22 | 23 | If you are unsure, simply run `{appname} verify` and we'll tell you what versions you have. 24 | 25 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 26 | 27 | __»__ To print these instructions again, run: `{appname} print` 28 | __»__ To verify you have the correct environment, run: `{appname} verify` 29 | __»__ For help run: `{appname} help` 30 | -------------------------------------------------------------------------------- /exercises/for_the_sake_of_argument/more.md: -------------------------------------------------------------------------------- 1 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 2 | 3 | # MORE 4 | 5 | ## About V8 types 6 | 7 | In general, the C++ interface to V8 gives you access to the same types (and more) that you know from within JavaScript. Including `Object`, `String`, `Boolean`, `Function`, etc. Each of these have different methods and properties but they all form a hierarchy and share many methods on common. When you are using one of these types you are using an object that may be exposed into JavaScript. 8 | 9 | To explore the documentation about the various V8 types, visit https://v8docs.nodesource.com/ and click on *Data Structures* and you will see some familiar names. 10 | 11 | ## About V8 handles 12 | 13 | The `Local` construct is required to wrap up the raw type object in a *handle* that can safely interact with the V8 runtime. The handle is used to attach to the garbage collector and automatically clean up the object when we fall out of *scope*. We can also use the handle to perform conversions and comparisons. In general we only interact with V8 data types when they are wrapped in a handle. 14 | 15 | ```c++ 16 | Local arg1 = info[0]; 17 | Local str1 = arg1.As(); 18 | ``` 19 | 20 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 21 | 22 | __»__ To print the instructions again, run: `{appname} print` 23 | __»__ To compile and test your solution, run: `{appname} verify myaddon/` 24 | __»__ For help run: `{appname} help` 25 | -------------------------------------------------------------------------------- /exercises/mission_possible_part_one/exercise.js: -------------------------------------------------------------------------------- 1 | const boilerplate = require('workshopper-boilerplate') 2 | const path = require('path') 3 | const copy = require('../../lib/copy') 4 | const solutions = require('../../lib/solutions') 5 | const check = require('../../lib/check') 6 | const gyp = require('../../lib/gyp') 7 | const packagejson = require('../../lib/packagejson') 8 | 9 | // name of the module required in binding.gyp 10 | const boilerplateName = 'myaddon' 11 | // what we should get on stdout for this to pass 12 | const solutionFiles = ['package.json', 'binding.gyp'] 13 | 14 | let exercise = require('workshopper-exercise')() 15 | 16 | // add solutions file listing from solutions/ directory 17 | exercise = solutions(exercise, solutionFiles) 18 | // add boilerplate functionality 19 | exercise = boilerplate(exercise) 20 | 21 | // boilerplate directory to copy into CWD to give them a base to start from 22 | exercise.addBoilerplate(path.join(__dirname, 'boilerplate/' + boilerplateName)) 23 | // need to add the two deps (bindings & nan) into node_modules so they don't *need* to `npm install` 24 | exercise.addPrepare(boilerplateSetup) 25 | 26 | // the steps towards verification 27 | exercise.addProcessor(check.checkSubmissionDir) 28 | exercise.addProcessor(packagejson.checkPackageJson) 29 | exercise.addProcessor(gyp.checkBinding) 30 | 31 | // complete the copied boilerplate dir by adding node_modules/bindings/ 32 | // so they don't need to `npm install bindings` 33 | function boilerplateSetup (callback) { 34 | const target = path.join(process.cwd(), exercise.boilerplateOut[boilerplateName]) 35 | copy.copyDeps(target, callback) 36 | } 37 | 38 | module.exports = exercise 39 | -------------------------------------------------------------------------------- /exercises/offloading_the_work/boilerplate/myaddon.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | using namespace v8; 4 | 5 | // A worker class extending the NanAsyncWorker helper 6 | // class, a simple encapsulation of worker-thread 7 | // logic to make simple tasks easier 8 | 9 | class MyWorker : public Nan::AsyncWorker { 10 | public: 11 | // Constructor 12 | MyWorker(Nan::Callback *callback, int delay) 13 | : Nan::AsyncWorker(callback), delay(delay) {} 14 | // Destructor 15 | ~MyWorker() {} 16 | 17 | // Executed inside the worker-thread. 18 | // It is not safe to access V8, or V8 data structures 19 | // here, so everything we need for input and output 20 | // should go on `this`. 21 | void Execute () { 22 | // Asynchronous, non-V8 work goes here 23 | } 24 | 25 | // Executed when the async work is complete 26 | // this function will be run inside the main event loop 27 | // so it is safe to use V8 again 28 | void HandleOKCallback () { 29 | Nan::HandleScope scope; 30 | Nan::AsyncResource resource("Nan::Callback"); 31 | 32 | // Nan::Callback#Call() does a Nan::MakeCallback() for us 33 | callback->Call(0, NULL, &resource); 34 | } 35 | 36 | private: 37 | int delay; 38 | }; 39 | 40 | NAN_METHOD(Delay) { 41 | // get delay and callback 42 | // create NanCallback instance wrapping the callback 43 | // create a MyWorker instance, passing the callback and delay 44 | // queue the worker instance onto the thread-pool 45 | } 46 | 47 | NAN_MODULE_INIT(Init) { 48 | Nan::Set(target, Nan::New("delay").ToLocalChecked(), 49 | Nan::GetFunction(Nan::New(Delay)).ToLocalChecked()); 50 | } 51 | 52 | NODE_MODULE(myaddon, Init) 53 | -------------------------------------------------------------------------------- /test/lib/check-python.test.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const sinon = require('sinon') 3 | const proxyquire = require('proxyquire') 4 | const Exercise = require('workshopper-exercise') 5 | let exercise 6 | 7 | const setup = () => { 8 | exercise = Exercise() 9 | exercise.__ = sinon.fake() 10 | } 11 | 12 | const run = (exercise) => { 13 | return new Promise(function (resolve) { 14 | exercise.run([], () => resolve()) 15 | }) 16 | } 17 | 18 | test('with valid python version', async (t) => { 19 | t.plan(2) 20 | setup() 21 | const version = '3.8.0' 22 | const stdout = `Python ${version}` 23 | const checkPython = proxyquire('../../lib/check-python', { 24 | 'node-gyp/lib/find-python': function (config, callback) { 25 | return callback(null, '/path/python') 26 | }, 27 | child_process: { 28 | execFile: (file, args, cb) => { 29 | return cb(null, { stdout }) 30 | } 31 | } 32 | }) 33 | exercise.addProcessor(checkPython) 34 | await run(exercise) 35 | t.equal(exercise.__.callCount, 1) 36 | t.ok(exercise.__.calledWith('pass.python', { version })) 37 | }) 38 | 39 | test('with invalid python version', async (t) => { 40 | t.plan(2) 41 | setup() 42 | const message = 'Could not find any Python installation to use' 43 | const error = new Error(message) 44 | const checkPython = proxyquire('../../lib/check-python', { 45 | 'node-gyp/lib/find-python': function (config, callback) { 46 | return callback(error) 47 | } 48 | }) 49 | exercise.addProcessor(checkPython) 50 | await run(exercise) 51 | t.equal(exercise.__.callCount, 1) 52 | t.ok(exercise.__.calledWith('fail.python', { message })) 53 | }) 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "goingnative", 3 | "version": "2.1.0", 4 | "description": "A NodeSchool style workshopper for learning how to write native Node.js addons", 5 | "main": "goingnative.js", 6 | "bin": "./bin/goingnative", 7 | "preferGlobal": true, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/rvagg/goingnative.git" 11 | }, 12 | "scripts": { 13 | "lint": "standard && jshint --exclude=node_modules .", 14 | "test": "npm run lint && tape test/**/**/*.js", 15 | "postinstall": "node postinstall.js", 16 | "release": "standard-version" 17 | }, 18 | "author": "Rod Vagg (https://github.com/rvagg)", 19 | "license": "MIT", 20 | "dependencies": { 21 | "after": "~0.8.1", 22 | "bindings": "^1.5.0", 23 | "chalk": "^4.1.0", 24 | "colors-tmpl": "~1.0.0", 25 | "core-util-is": "~1.0.2", 26 | "cpr": "^1.1.1", 27 | "getos": "^3.2.1", 28 | "js-yaml": "^4.1.0", 29 | "mkdirp": "^1.0.4", 30 | "nan": "^2.14.0", 31 | "node-addon-api": "^4.0.0", 32 | "node-gyp": "^7.0.0", 33 | "rimraf": "^3.0.2", 34 | "semver": "^7.3.2", 35 | "varstring": "~0.2.0", 36 | "workshopper-adventure": "^7.0.0", 37 | "workshopper-boilerplate": "~1.1.2", 38 | "workshopper-exercise": "^3.0.1", 39 | "workshopper-hooray": "~1.1.0", 40 | "workshopper-more": "~1.0.1" 41 | }, 42 | "devDependencies": { 43 | "jshint": "^2.11.1", 44 | "proxyquire": "^2.1.3", 45 | "sinon": "^11.1.2", 46 | "standard": "^16.0.3", 47 | "standard-version": "^9.3.1", 48 | "tape": "^5.0.1" 49 | }, 50 | "standard-version": { 51 | "bumpFiles": [ 52 | "package.json" 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /exercises/am_i_ready/vars.json: -------------------------------------------------------------------------------- 1 | { 2 | "versions" : { 3 | "node": { 4 | "min": "10.x", 5 | "max": "14.x" 6 | }, 7 | "gcc" : "4.4.0", 8 | "llvm" : "4.2", 9 | "gyp" : "0.12.0" 10 | }, 11 | "instructions" : { 12 | "Debian" : { 13 | "gcc" : "Install with `sudo apt-get install build-essential`.", 14 | "python" : "Install with `sudo apt-get install python3.8`." 15 | }, 16 | "Ubuntu Linux" : { 17 | "gcc" : "Install with `apt-get install build-essential`.", 18 | "python" : "Install with `sudo apt-get install python3.8`." 19 | }, 20 | "Arch Linux" : { 21 | "gcc" : "Install with `sudo pacman -S base-devel`.", 22 | "python" : "Install with `sudo pacman -S python`." 23 | }, 24 | "Darwin" : { 25 | "gcc" : "Visit .", 26 | "python" : "Visit ." 27 | }, 28 | "win32" : { 29 | "gcc" : "On **Windows**, you should install Visual Studio Community Edition 2013 http://go.microsoft.com/fwlink/?LinkId=517284.", 30 | "python" : "Python must be in your PATH: on Mac and Linux this should be handled by default. On **Windows 8**, you should search 'environment' from the search menu, and click 'edit environment variable for your account', then you should edit 'PATH', and add your Python's path (Ex: ';C:\\Python38'). On **Windows 7**, you can access a similar menu at My Computer > Properties > Advanced System Settings > Environment Variables'." 31 | }, 32 | "Other" : { 33 | "gcc" : "Use your native package manager to install build tools including gcc.", 34 | "python" : "Download from or use your package manager." 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/copy.js: -------------------------------------------------------------------------------- 1 | const cpr = require('cpr') 2 | const path = require('path') 3 | const after = require('after') 4 | const rimraf = require('rimraf') 5 | 6 | // where node_modules/bindings is so it can be copied to make a submission compilable 7 | const bindingsDir = path.dirname(require.resolve('bindings')) 8 | // where node_modules/nan is so it can be copied to make a submission compilable 9 | const nanDir = path.dirname(require.resolve('nan')) 10 | 11 | // copy their submission into two tmp directories that we can mess with and test without 12 | // touching their original 13 | function copyTemp (toDirs) { 14 | return function (mode, callback) { 15 | const exercise = this 16 | let done = after(toDirs.length, function (err) { 17 | if (err) { return callback(err) } 18 | 19 | done = after(toDirs.length, function (err) { 20 | if (err) { return callback(err) } 21 | 22 | callback(null, true) 23 | }) 24 | 25 | toDirs.forEach(function (dir) { 26 | copyDeps(dir, done) 27 | }) 28 | }) 29 | 30 | toDirs.forEach(function (dir) { 31 | cpr(exercise.submission, dir, { overwrite: true }, done) 32 | }) 33 | } 34 | } 35 | 36 | function copyDeps (dir, callback) { 37 | const done = after(2, callback) 38 | 39 | cpr(bindingsDir, path.join(dir, 'node_modules/bindings/'), { overwrite: true }, done) 40 | cpr(nanDir, path.join(dir, 'node_modules/nan/'), { overwrite: true }, done) 41 | } 42 | 43 | // don't leave the tmp dirs 44 | function cleanup (dirs) { 45 | return function (mode, pass, callback) { 46 | const done = after(dirs.length, callback) 47 | 48 | dirs.forEach(function (dir) { 49 | rimraf(dir, done) 50 | }) 51 | } 52 | } 53 | 54 | module.exports = cpr 55 | module.exports.copyTemp = copyTemp 56 | module.exports.cleanup = cleanup 57 | module.exports.copyDeps = copyDeps 58 | -------------------------------------------------------------------------------- /exercises/offloading_the_work/solution/myaddon.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #ifndef _WIN32 3 | # include 4 | #endif 5 | 6 | using namespace v8; 7 | 8 | class MyWorker : public Nan::AsyncWorker { 9 | public: 10 | MyWorker(Nan::Callback *callback, int delay) 11 | : Nan::AsyncWorker(callback), delay(delay) {} 12 | ~MyWorker() {} 13 | 14 | // Executed inside the worker-thread. 15 | // It is not safe to access V8, or V8 data structures 16 | // here, so everything we need for input and output 17 | // should go on `this`. 18 | void Execute () { 19 | #ifdef _WIN32 20 | Sleep(delay); 21 | #else 22 | usleep(delay * 1000); 23 | #endif 24 | } 25 | 26 | // Executed when the async work is complete 27 | // this function will be run inside the main event loop 28 | // so it is safe to use V8 again 29 | void HandleOKCallback () { 30 | Nan::HandleScope scope; 31 | Nan::AsyncResource resource("Nan::Callback"); 32 | 33 | callback->Call(0, NULL, &resource); 34 | } 35 | 36 | private: 37 | int delay; 38 | }; 39 | 40 | NAN_METHOD(Delay) { 41 | Nan::Maybe maybeDelay = Nan::To(info[0]); 42 | 43 | if (maybeDelay.IsNothing() == true) { 44 | Nan::ThrowError("Error converting first argument to integer"); 45 | } 46 | 47 | if (info[1]->IsFunction() == false) { 48 | Nan::ThrowError("Error converting second argument to function"); 49 | } 50 | 51 | int delay = maybeDelay.FromJust(); 52 | 53 | v8::Local callback = info[1].As(); 54 | 55 | Nan::Callback* nanCallback = new Nan::Callback(callback); 56 | MyWorker* worker = new MyWorker(nanCallback, delay); 57 | Nan::AsyncQueueWorker(worker); 58 | } 59 | 60 | NAN_MODULE_INIT(Init) { 61 | Nan::Set(target, Nan::New("delay").ToLocalChecked(), 62 | Nan::GetFunction(Nan::New(Delay)).ToLocalChecked()); 63 | } 64 | 65 | NODE_MODULE(myaddon, Init) 66 | -------------------------------------------------------------------------------- /exercises/mission_possible_part_two/problem.md: -------------------------------------------------------------------------------- 1 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 2 | 3 | ## Task 4 | 5 | Create a JavaScript file for your native add-on package that loads and executes the compiled module. 6 | 7 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 8 | 9 | ## Description 10 | 11 | In this exercise, you simply need to create an *index.js* file inside the add-on package directory you submitted in the previous exercise. 12 | 13 | ### Mission: write an *index.js* 14 | 15 | You will always need a small amount of JavaScript glue for a native add-on to tell Node.js where the add-on binary is located for loading (via `require()`) and to then either expose the add-on via `exports` or perform some internal work with the add-on. 16 | 17 | For this exercise, the add-on C++ code we will be building will expose a method named `print()`. We are using the *bindings* library to simplify the locating and loading of the compiled binary. Load your add-on module with: 18 | 19 | ```js 20 | var bindings = require('bindings') 21 | var addon = bindings('modulename') 22 | ``` 23 | 24 | Where `modulename` is the name in your *binding.gyp*. 25 | 26 | The loaded `addon` will behave like any normal Node.js module, so you can fetch properties from it, call methods on it, and anything else you are used to doing with a Node.js module. 27 | 28 | Aside from loading the addon, you also need to call its `addon.print()` method. 29 | 30 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 31 | 32 | ## Conditions 33 | 34 | Your submission will be tested against a working native add-on to determine if your JavaScript works. You must call the `print()` function exposed by the working add-on. 35 | 36 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 37 | 38 | __»__ To print these instructions again, run: `{appname} print` 39 | __»__ To compile and test your solution, run: `{appname} verify myaddon` 40 | __»__ For help run: `{appname} help` 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [2.1.0](https://github.com/rvagg/goingnative/compare/v2.0.8...v2.1.0) (2021-08-19) 6 | 7 | 8 | ### Features 9 | 10 | * add basic N-API exercises ([c48d2c7](https://github.com/rvagg/goingnative/commit/c48d2c74bc1fde30d9a68a61afc6f042cce0bad4)) 11 | * allowing access to goingnative as library ([3f427b4](https://github.com/rvagg/goingnative/commit/3f427b4ac3182a39dab9d4c9107bf7cda56c2b1f)) 12 | 13 | 14 | ### Bug Fixes 15 | 16 | * **timing:** pre-warming the the vm with the binary code to make sure the execution can be measured. ([81fc044](https://github.com/rvagg/goingnative/commit/81fc0446cf012bcc83fcc881368611b54d5bb07a)) 17 | * **timing:** pre-warming the the vm with the binary code to make sure the execution can be measured. ([0d72080](https://github.com/rvagg/goingnative/commit/0d720802afc9481bfb9365a2e6af386f77a37cf2)), closes [#98](https://github.com/rvagg/goingnative/issues/98) 18 | * **ui:** fixing display of solution files ([5ff1ca8](https://github.com/rvagg/goingnative/commit/5ff1ca85b83a4be756659f9b4053ce01a4f4ee90)) 19 | 20 | ### [2.0.8](https://github.com/rvagg/goingnative/compare/v2.0.7...v2.0.8) (2020-05-14) 21 | 22 | 23 | ### Bug Fixes 24 | 25 | * **offloading_the_work:** fix Nan::Callback#Call deprecation warning ([5e098e4](https://github.com/rvagg/goingnative/commit/5e098e4bf99180bc49025a6270bf167fdc3e4b98)) 26 | * use `Nan::Utf8String` instead `v8::String::Utf8Value` ([a9d58b9](https://github.com/rvagg/goingnative/commit/a9d58b9aa76feda60b08c77d127225ad282eb798)) 27 | * **am_i_ready:** update node versions ([8e9a028](https://github.com/rvagg/goingnative/commit/8e9a0284b2e582b86248a8c45ae22aa41efbb5fc)) 28 | * **am_i_ready:** update test package ([8e01af6](https://github.com/rvagg/goingnative/commit/8e01af629906a26b64cd81550964a5b63d4d04b0)) 29 | * **lib/solution:** remove map-async dependency ([b098e8a](https://github.com/rvagg/goingnative/commit/b098e8ae6733b98e53ed2d3566dc5f423be7f59e)) 30 | -------------------------------------------------------------------------------- /exercises/offloading_the_work/faux/myaddon.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #ifndef _WIN32 3 | # include 4 | #endif 5 | 6 | using namespace v8; 7 | 8 | class MyWorker : public Nan::AsyncWorker { 9 | public: 10 | MyWorker(Nan::Callback *callback, int delay) 11 | : Nan::AsyncWorker(callback), delay(delay) {} 12 | ~MyWorker() {} 13 | 14 | // Executed inside the worker-thread. 15 | // It is not safe to access V8, or V8 data structures 16 | // here, so everything we need for input and output 17 | // should go on `this`. 18 | void Execute () { 19 | // tiny sleep prior to ensure proper print order of 2 & 3 20 | #ifdef _WIN32 21 | Sleep(50); 22 | #else 23 | usleep(50 * 1000); 24 | #endif 25 | 26 | printf("FAUX 3\n"); 27 | fflush(stdout); 28 | 29 | #ifdef _WIN32 30 | Sleep(delay); 31 | #else 32 | usleep(delay * 1000); 33 | #endif 34 | 35 | printf("FAUX 4\n"); 36 | fflush(stdout); 37 | } 38 | 39 | // Executed when the async work is complete 40 | // this function will be run inside the main event loop 41 | // so it is safe to use V8 again 42 | void HandleOKCallback () { 43 | Nan::HandleScope scope; 44 | Nan::AsyncResource resource("Nan::Callback"); 45 | 46 | callback->Call(0, NULL, &resource); 47 | } 48 | 49 | private: 50 | int delay; 51 | }; 52 | 53 | NAN_METHOD(Delay) { 54 | Nan::Maybe maybeDelay = Nan::To(info[0]); 55 | 56 | if (maybeDelay.IsNothing() == true) { 57 | Nan::ThrowError("Error converting first argument to integer"); 58 | } 59 | 60 | if (info[1]->IsFunction() == false) { 61 | Nan::ThrowError("Error converting second argument to Function"); 62 | } 63 | 64 | int delay = maybeDelay.FromJust(); 65 | 66 | v8::Local callback = info[1].As(); 67 | 68 | printf("FAUX 1\n"); 69 | fflush(stdout); 70 | 71 | Nan::Callback* nanCallback = new Nan::Callback(callback); 72 | MyWorker* worker = new MyWorker(nanCallback, delay); 73 | Nan::AsyncQueueWorker(worker); 74 | 75 | printf("FAUX 2\n"); 76 | fflush(stdout); 77 | } 78 | 79 | NAN_MODULE_INIT(Init) { 80 | Nan::Set(target, Nan::New("delay").ToLocalChecked(), 81 | Nan::GetFunction(Nan::New(Delay)).ToLocalChecked()); 82 | } 83 | 84 | NODE_MODULE(myaddon, Init) 85 | -------------------------------------------------------------------------------- /lib/napiExercise.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { exec } = require('child_process') 3 | const copy = require('./copy') 4 | const compile = require('./compile') 5 | const solutions = require('./solutions') 6 | const check = require('./check') 7 | const gyp = require('./gyp') 8 | const packagejson = require('./packagejson') 9 | const argv2 = require.resolve('./require-argv2') 10 | 11 | const copyTempDir = path.join(process.cwd(), '~test-addon.' + Math.floor(Math.random() * 10000)) 12 | const solutionFiles = ['myaddon.cc'] 13 | 14 | let exercise = require('workshopper-exercise')() 15 | exercise = solutions(exercise, solutionFiles) 16 | 17 | exercise.addProcessor(check.checkSubmissionDir) 18 | exercise.addProcessor(copy.copyTemp([copyTempDir])) 19 | exercise.addProcessor(packagejson.checkPackageJson) 20 | exercise.addProcessor(gyp.checkBinding) 21 | exercise.addProcessor(compile.checkCompile(copyTempDir)) 22 | 23 | function checkExec (mode, callback) { 24 | const exercise = this 25 | const expected = exercise.expected 26 | 27 | if (!exercise.passed) { 28 | return callback(null, true) // shortcut if we've already had a failure 29 | } 30 | 31 | exec(`${process.execPath} ${argv2} ${copyTempDir}`, function (err, stdout, stderr) { 32 | if (err) { 33 | process.stderr.write(stderr) 34 | process.stdout.write(stdout) 35 | return callback(err) 36 | } 37 | 38 | const pass = stdout.toString().replace('\r', '') === expected + '\n' 39 | const seminl = !pass && stdout.toString() === expected 40 | const semicase = !pass && !seminl && new RegExp(expected, 'i').test(stdout.toString()) 41 | 42 | if (!seminl && !semicase && !pass) { 43 | process.stderr.write(stderr) 44 | process.stdout.write(stdout) 45 | } 46 | 47 | if (seminl) { 48 | exercise.emit('fail', 'Addon prints out expected string (missing newline)') 49 | } else if (semicase) { 50 | exercise.emit('fail', 'Addon prints out expected string (printed with wrong character case)') 51 | } else { 52 | exercise.emit(pass ? 'pass' : 'fail', 'Addon prints out expected string') 53 | } 54 | 55 | callback(null, pass) 56 | }) 57 | } 58 | 59 | exercise.addProcessor(checkExec) 60 | exercise.addCleanup(copy.cleanup([copyTempDir])) 61 | 62 | exercise.skipBindingIncludeDirs = true 63 | 64 | module.exports = exercise 65 | -------------------------------------------------------------------------------- /exercises/mission_possible_part_two/exercise.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const copy = require('../../lib/copy') 3 | const solutions = require('../../lib/solutions') 4 | const check = require('../../lib/check') 5 | const gyp = require('../../lib/gyp') 6 | const packagejson = require('../../lib/packagejson') 7 | const execWith = require('../../lib/execWith') 8 | 9 | // a place to make a full copy to replace myaddon.cc with a mock to do a mocked run to test JS 10 | const copyFauxTempDir = path.join(process.cwd(), '~test-addon-faux.' + Math.floor(Math.random() * 10000)) 11 | const solutionFiles = ['index.js'] 12 | 13 | let exercise = require('workshopper-exercise')() 14 | 15 | // add solutions file listing from solutions/ directory 16 | exercise = solutions(exercise, solutionFiles) 17 | 18 | // the steps towards verification 19 | exercise.addProcessor(check.checkSubmissionDir) 20 | exercise.addProcessor(copy.copyTemp([copyFauxTempDir])) 21 | exercise.addProcessor(copyFauxAddon) 22 | exercise.addProcessor(packagejson.checkPackageJson) 23 | exercise.addProcessor(gyp.checkBinding) 24 | exercise.addProcessor(checkJs) 25 | 26 | // always clean up the temp directories 27 | exercise.addCleanup(copy.cleanup([copyFauxTempDir])) 28 | 29 | function copyFauxAddon (mode, callback) { 30 | copy(path.join(__dirname, 'faux', 'myaddon.cc'), copyFauxTempDir, { overwrite: true }, function (err) { 31 | if (err) { return callback(err) } 32 | 33 | callback(null, true) 34 | }) 35 | } 36 | 37 | // run `node-gyp rebuild` on a mocked version of the addon that prints what we want 38 | // so we can test that their JS is doing what it is supposed to be doing and there 39 | // is no cheating! (e.g. console.log(...)) 40 | function checkJs (mode, callback) { 41 | const exercise = this 42 | 43 | if (!exercise.passed) { return callback(null, true) } // shortcut if we've already had a failure 44 | 45 | gyp.rebuild(copyFauxTempDir, function (err) { 46 | if (err) { 47 | exercise.emit('fail', 'Compile mock C++ to test JavaScript: ' + err.message) 48 | return callback(null, false) 49 | } 50 | 51 | execWith( 52 | require.resolve('../../lib/require-argv2'), 53 | copyFauxTempDir, 54 | 'FAUX\n', 55 | function (err, pass) { 56 | if (err) { 57 | return callback(err) 58 | } 59 | 60 | exercise.emit(pass ? 'pass' : 'fail', 'JavaScript code loads addon and invokes `print()` method') 61 | callback(null, pass) 62 | } 63 | ) 64 | }) 65 | } 66 | 67 | module.exports = exercise 68 | -------------------------------------------------------------------------------- /exercises/offloading_the_work/more.md: -------------------------------------------------------------------------------- 1 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 2 | 3 | # MORE 4 | 5 | ## About Node.js worker threads 6 | 7 | Node.js performs asynchronous I/O using two main mechanisms. Socket I/O is performed using non-blocking system calls. In this way, a significant number of sockets can be handled by a single thread and it never needs to block. Instead it polls existing connections for available data and moves on if there is none. For file system I/O this isn't as fast so Node.js spins up a thread-pool to offload discrete chunks of file system work to perform the I/O. A `fs.readFile()` will end up spanning multiple `fs.read()` operations that may end up distributed across multiple threads during the course of the full read. These reads can obviously be interleaved with other file system operations simultaneously. 8 | 9 | Node.js and libuv give us the ability to interact with the thread-pool via C++. We don't have to limit the worker threads to just file system I/O although the more work we put on them, the less time they have for file system I/O. 10 | 11 | Heavy threading work may be best to opt for separate threads for particular jobs, defined outside of the thread-pool. libuv makes this easy to achieve in a cross-platform way, but this is beyond the scope of this workshop! 12 | 13 | While the default number of worker threads is 4, this can be modified all the way up to a maximum of 128 threads by setting the `UV_THREADPOOL_SIZE` environment variable. However, you must be sure to measure the efficacy of changing this value before using it in production. 14 | 15 | ## About the JavaScript thread 16 | 17 | Due to the V8 architecture and the single-threaded nature of JavaScript. It is *vital* that you not *touch* any V8 objects from code that is not running in the same thread as JavaScript. 18 | 19 | Code running in a worker thread can perform any other function, as long as it doesn't interact with V8. Values passed to the thread and values passed back from the thread should be stored in a data structure of some form so they can be retrieved when execution passes back to the JavaScript thread. Values coming *from* V8 objects must be converted out of their original form into a non-V8 form. Values returning *to* V8 must be converted back in to V8 objects from their non-V8 forms. 20 | 21 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 22 | 23 | __»__ To print the instructions again, run: `{appname} print` 24 | __»__ To compile and test your solution, run: `{appname} verify myaddon/` 25 | __»__ For help run: `{appname} help` 26 | -------------------------------------------------------------------------------- /exercises/mission_possible_part_one/problem.md: -------------------------------------------------------------------------------- 1 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 2 | 3 | ## Task 4 | 5 | Prepare a *package.json* and a *binding.gyp* for a native add-on package. 6 | 7 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 8 | 9 | ## Description 10 | 11 | Some components of a Node.js native add-on have been created for you in a directory named ***{boilerplate:myaddon}*** in your current working directory. 12 | 13 | We are starting with the configuration files and will add the other components in the next two exercises. 14 | 15 | ### Mission: finish *package.json* 16 | 17 | Look at your _package.json_. It has two dependencies that assist with add-on development: 18 | 19 | * **NAN**: a standardized C++ interface to Node and V8 20 | * **bindings**: a tool to help find the location of the *compiled* version of your add-on during runtime 21 | 22 | *Type `{appname} more` for more information on these dependencies.* 23 | 24 | Your mission: tell node to look for your _.gyp_ file. To do this, add `"gypfile": true` to your _package.json_ 25 | 26 | ### Mission: finish *binding.gyp* 27 | 28 | _binding.gyp_ is a JSON-esq file that tells `node-gyp` how to build your project. Look inside it now. A basic structure has been provided for you, but it needs more work. 29 | 30 | 31 | * `"target_name"` of the single target listed in the file must be the exact name of your add-on. It needs to match the name you use for it in your JavaScript file *and* the name you use in the `NODE_MODULE()` macro in your C++ file. *Set it to `"myaddon"`*. 32 | * The `"sources"` array must include the file name of the C++ file used by your add-on, you need to change it to `"myaddon.cc"`. 33 | * The `"include_dirs"` array tells the compiler where to find the *nan.h* header file used by NAN. *Set it to this text:* 34 | 35 | 36 | ".node`*. Every target also needs a `"sources"` array to tell GYP what files to compile. They can be any format that GYP knows how to compile which is generally just C++ or C. Finally, for native add-on development, we generally want an `"include_dirs"` array telling node-gyp what directories to include in the search path for files that can be referenced via `#include` in your C++. The recommendation is to use NAN to make add-on development a more stable experience against multiple Node.js versions and this needs to be loaded via `#include `. We use `" 59 | ``` 60 | 61 | Then you can use all `node-addon-api` APIs. 62 | 63 | Let's learn some basic APIs that you are going to use to solve this exercise. 64 | 65 | * For create you can use `Napi::String::New`: 66 | 67 | ```c++ 68 | Napi::Env env; 69 | Napi::String::New(env, "my string") 70 | ``` 71 | 72 | * Create a function that return a string: 73 | 74 | ```c++ 75 | Napi::Value SayHi(const Napi::CallbackInfo& info) { 76 | Napi::Env env = info.Env(); 77 | return Napi::String::New(env, "Hi Nodeschool"); 78 | } 79 | ``` 80 | 81 | * Create an init function: 82 | Like with `nan` we have to create a function to expose our methods to Node.js. 83 | 84 | ```c++ 85 | Napi::Object Init(Napi::Env env, Napi::Object exports) { 86 | // set a key on `exports` object 87 | exports.Set( 88 | Napi::String::New(env, "tell"), // property name 89 | Napi::Function::New(env, MyTellFunc) // property value 90 | ) 91 | 92 | return exports 93 | } 94 | ``` 95 | 96 | ### Module registration 97 | 98 | You can expose your init function to Node.js in similar way than with `nan`. 99 | Just you have to use `NODE_API_MODULE` macro. 100 | 101 | ```c++ 102 | NODE_API_MODULE(moduleName, InitFunction) 103 | ``` 104 | 105 | Note: You can use `NODE_GYP_MODULE_NAME` macro for your moduleName as long as 106 | you use `node-gyp` for build your addon. 107 | 108 | ### Mission: 109 | 110 | Write an addon that export a function `hello`, which should return the string: 111 | 112 | ``` 113 | hello NAPI! 114 | ``` 115 | 116 | Then in your javascript file print the returned value from your addon in console 117 | 118 | Docs: 119 | * `node-addon-api` docs: https://github.com/nodejs/node-addon-api#api-documentation 120 | -------------------------------------------------------------------------------- /exercises/for_the_sake_of_argument/problem.md: -------------------------------------------------------------------------------- 1 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 2 | 3 | ## Task 4 | 5 | Create a Node.js program that takes a `String` command-line argument, passes it to a native Node.js add-on which prints it to standard output with `printf()` (or similar). 6 | 7 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 8 | 9 | ## Description 10 | 11 | This exercise starts with your code from the previous exercise, but adds two new features: 12 | 13 | 1. Read a command-line argument from within *index.js* and pass it to the add-on. 14 | 2. Read an argument from the method defined in your C++ add-on code and send it to the `printf()` function. 15 | 16 | Feature #1 is implemented entirely in JavaScript. All you need to do is read `process.argv[2]`, the first user-supplied argument, and pass that as an argument to the add-on method. 17 | 18 | Feature #2 is a C++ change. To implement it, you'll need to learn about argument handling and V8 data types. 19 | 20 | ### I wanted an argument! 21 | 22 | When you use the macro `NAN_METHOD()`, you automatically have access to an `info` array inside the method even though you don't see it declared. The elements of this array correspond to the arguments passed in, so `info[0]` is the first argument. 23 | 24 | NAN contains a special function `To()` that lets you *convert* a `v8::Value` to another `V8` type. This value returns either a `MaybeLocal` or `Maybe` - this is because some values cannot be converted to other types. You must check if the response value is empty before using `::IsEmpty()`, like so: 25 | 26 | ```c++ 27 | Nan::MaybeLocal maybeNum = Nan::To(info[2]); 28 | if (maybeNum.IsEmpty() == false) { 29 | Local num = maybeNum.ToLocalChecked(); 30 | } 31 | ``` 32 | 33 | ### Handles 34 | 35 | `Local` is a V8 object *handle*. A handle is a special wrapper for a C++ object that allows it to behave properly inside V8. Most of the time a handle will be a `Local`. A `String` handle would be defined as `Local`. In this case, we've extracted the third argument as a `Number` type, so we can use it as a number instead of as a generic object. 36 | 37 | For this exercise, you'll need to extract the function argument as a `String` type for printing. But because JavaScript lives in UTF-8-land you can't just print the string handle! To get a C-compatible string to give to `printf()` you need to get a decoded UTF-8 version of the raw data inside the object. To do this, create a `String::Utf8Value()` object using the string handle. Use the `*` *operator* of the Utf8Value object to pass it to `printf` like this: 38 | 39 | ```c++ 40 | printf("%s\n", *String::Utf8Value(str)); 41 | ``` 42 | 43 | `str` must be a V8 `String` handle. 44 | 45 | Note how we are using `printf("format string", arg1, arg2, ...);`. `"format string"` is a string that can contain argument specifiers, similar to the ones you use with node's `util.format()`. 46 | 47 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 48 | 49 | ## Conditions 50 | 51 | Your submission will be compiled using `node-gyp rebuild` and executed with `node . "some string"`. Standard output will be checked for that string. Your code will be checked to ensure that the C++ code is performing the print. 52 | 53 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 54 | 55 | __»__ To print these instructions again, run: `{appname} print` 56 | __»__ To print additional learning material relating to these instructions, run: `{appname} more` 57 | __»__ To compile and test your solution, run: `{appname} verify myaddon` 58 | __»__ For help run: `{appname} help` 59 | -------------------------------------------------------------------------------- /exercises/going_deep_into_napi/problem.md: -------------------------------------------------------------------------------- 1 | # Going deep into N-API 2 | 3 | Now you will learn how to use the `N-API` C interface directly, without the 4 | support of the `node-addon-api` C++ wrapper. 5 | 6 | Which approach you take in your own projects will depend on personal preference 7 | and the nature of the project, but it's worth understanding the difference. 8 | 9 | `N-API` APIs are generally used to create and manipulate JavaScript values, this 10 | APIs use an opaque type named `napi_value` to abstract this values. 11 | 12 | All `N-API` calls return a status code of type `napi_status` that indicates if 13 | the API call success or failed. 14 | 15 | ### Anatomy of a Node.js Addon with N-API 16 | 17 | The files used for an addon with `N-API` will be the same as before. The only 18 | difference will be the API's used in your C++ file. 19 | 20 | ### Include header 21 | 22 | You need to include `N-API` header at top of your file. 23 | 24 | ```cpp 25 | #include 26 | ``` 27 | 28 | ### Do your stuff 29 | 30 | Use `N-API` APIs according your needs. 31 | 32 | For example for define a variable `foo` with value the string `bar`: 33 | 34 | ```cpp 35 | napi_value foo; 36 | napi_status status; 37 | 38 | status = napi_create_string_utf8(env, "bar", NAPI_AUTO_LENGTH, &foo); 39 | if (status != napi_ok) return nullptr; 40 | ``` 41 | 42 | A simple function that return an array with numbers from 0 to 4: 43 | 44 | ```cpp 45 | napi_value MyFunction(napi_env env, napi_callback_info info) { 46 | napi_status status; 47 | napi_value array, num; 48 | 49 | status = napi_create_array(env, &array); 50 | if (status != napi_ok) return nullptr; 51 | 52 | for (int i = 0; i < 5; i++) { 53 | status = napi_create_uint32(env, i, &num); 54 | if (status != napi_ok) return nullptr; 55 | 56 | status = napi_set_element(env, array, i, num); 57 | if (status != napi_ok) return nullptr; 58 | } 59 | 60 | return array; 61 | } 62 | ``` 63 | 64 | Or a initialization function `Init` that exports a function `Bark` as `bark`: 65 | 66 | ```cpp 67 | napi_value Init(napi_env env, napi_value exports) { 68 | napi_status status; 69 | napi_value fn; 70 | 71 | status = napi_create_function(env, nullptr, 0, Bark, nullptr, &fn); 72 | if (status != napi_ok) return nullptr; 73 | 74 | status = napi_set_named_property(env, exports, 'bark', fn); 75 | if (status != napi_ok) return nullptr; 76 | 77 | return exports; 78 | } 79 | ``` 80 | 81 | ### Register the Module 82 | 83 | `N-API` modules are registered in similar way to modules you built before, but 84 | now use the `NAPI_MODULE` macro. 85 | 86 | ``` 87 | NAPI_MODULE(moduleName, InitFunction) 88 | ``` 89 | 90 | or (as long as you use `node-gyp`) 91 | 92 | ``` 93 | NAPI_MODULE(NODE_GYP_MODULE_NAME, InitFunction) 94 | ``` 95 | 96 | ## Mission: 97 | 98 | Write an addon that export a function `hello`, which should return the string: 99 | 100 | ``` 101 | hello N-API! 102 | ``` 103 | 104 | Then in your javascript file print the returned value from your addon in console 105 | 106 | ## Docs 107 | 108 | * `N-API` docs: https://nodejs.org/api/n-api.html 109 | * `napi_status`: https://nodejs.org/api/n-api.html#n_api_napi_status 110 | * `napi_env`: https://nodejs.org/api/n-api.html#n_api_napi_env 111 | * `napi_value`: https://nodejs.org/api/n-api.html#n_api_napi_value 112 | * `napi_callback_info`: https://nodejs.org/api/n-api.html#n_api_napi_callback_info 113 | * `napi_create_string_utf8`: https://nodejs.org/api/n-api.html#n_api_napi_create_string_utf8 114 | * `napi_create_function`: https://nodejs.org/api/n-api.html#n_api_napi_create_function 115 | -------------------------------------------------------------------------------- /exercises/its_a_twoway_street/problem.md: -------------------------------------------------------------------------------- 1 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 2 | 3 | ## Task 4 | 5 | Create a native Node.js add-on that can read a `String` argument and returns a integer indicating the length of that string. 6 | 7 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 8 | 9 | ## Description 10 | 11 | Instead of a `print()` method, your native add-on now needs to have a `length()` method. This function accepts a string, like the last exercise did, and returns the number of characters in that string. 12 | 13 | We need the 8-byte character length of the string which will be different than `String#length` in JavaScript for strings that include multi-byte UTF-8 characters (such as "♥"). 14 | 15 | To calculate the length, use the standard C function `strlen()`: 16 | 17 | int len = strlen("a string"); 18 | 19 | will result in a `len` value of `8`. You can pass in the `*String::Utf8Value(str)` construct to calculate the length of the V8 `String`. 20 | 21 | We could use `*String::AsciiValue(str)` but this would squash the multi-byte characters into single bytes and give us the wrong length. 22 | 23 | Try and print out the length with `printf("length: %d\n", len)` and see that you are calculating the length properly. 24 | 25 | Next we need to tackle the tricky concept of V8 **scopes**. 26 | 27 | C++ doesn't have automatic garbage collection, but JavaScript does. V8 tries to bridge the gap by providing an environment in C++ where your objects interact directly with the garbage collector, even if they are created in C++. 28 | 29 | It's not a fully automatic process unfortunately. To replicate the concept of function scoping of variables, V8 introduces a `HandleScope`. When you declare a `HandleScope` at the top of a C++ function, all V8 objects *created* within that function will be *attached* to that scope in the same way that a `function` in JavaScript will capture new variables declared within it. 30 | 31 | To declare one of these scopes in your code, use `Nan::HandleScope scope;` call at the top of your function. When your function ends, the scope can then pass on the objects to the garbage collector if they have not been passed outside of the scope. 32 | 33 | We then need to create a new V8 object representing our string length to pass back in to JavaScript. To create a new V8 type, use `Nan::New(value)`, where `Type` is the V8 type (such as `Number` or `String`) and `value` is the initial C++ value compatible with that type. A `"string"` can be passed to `Nan::New()` and a number value can be passed to a `Nan::New(101)`. If you want to assign the result to a variable, then use a construct such as: 34 | 35 | ```c++ 36 | Local str = Nan::New("a string"); 37 | ``` 38 | 39 | *Hint: you want to create a `Number` handle, not a `String`.* 40 | 41 | In the previous exercise, we returned `undefined` from our function by just returning. This time, as we are returning a value, we want to pass that value to `info.GetReturnValue().Set(value);`. 42 | 43 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 44 | 45 | ## Conditions 46 | 47 | Your submission will be compiled using `node-gyp rebuild` and executed with `node . "some string"`. Standard output will be checked for an integer representing the byte-length of the string passed in. Your code will be checked to ensure that the C++ code is returning the length of the string. 48 | 49 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 50 | 51 | __»__ To print these instructions again, run: `{appname} print` 52 | __»__ To compile and test your solution, run: `{appname} verify myaddon` 53 |  __»__ For help run: `{appname} help` 54 | -------------------------------------------------------------------------------- /lib/gyp.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const gyp = require('node-gyp') 4 | const yaml = require('js-yaml') 5 | const is = require('core-util-is') 6 | 7 | // invoke `node-gyp rebuild` programatically, runs clean;configure;build 8 | function rebuild (dir, _callback) { 9 | const cwd = process.cwd() 10 | const gypInst = gyp() 11 | 12 | const callback = function (err) { 13 | _callback(err) 14 | } 15 | 16 | gypInst.parseArgv([null, null, 'rebuild', '--loglevel', 'silent']) 17 | process.chdir(dir) 18 | gypInst.commands.clean([], function (err) { 19 | if (err) return callback(new Error('node-gyp clean: ' + err.message)) 20 | gypInst.commands.configure([], function (err) { 21 | if (err) return callback(new Error('node-gyp configure: ' + err.message)) 22 | gypInst.commands.build([], function (err) { 23 | if (err) return callback(new Error('node-gyp build: ' + err.message)) 24 | 25 | process.chdir(cwd) 26 | return callback() 27 | }) 28 | }) 29 | }) 30 | } 31 | 32 | // check binding.gyp to see if it's parsable YAML and contains the 33 | // basic structure that we need for this to work 34 | function checkBinding (mode, callback) { 35 | const exercise = this 36 | 37 | function fail (msg) { 38 | exercise.emit('fail', msg) 39 | return callback(null, false) 40 | } 41 | 42 | fs.readFile(path.join(exercise.submission, 'binding.gyp'), 'utf8', function (err, data) { 43 | if (err) return fail('Read binding.gyp (' + err.message + ')') 44 | 45 | let doc 46 | 47 | try { 48 | doc = yaml.load(data) 49 | } catch (e) { 50 | return fail('Parse binding.gyp (' + e.message + ')') 51 | } 52 | 53 | if (!is.isObject(doc)) { 54 | return fail('binding.gyp does not contain a parent object ({ ... })') 55 | } 56 | 57 | if (!is.isArray(doc.targets)) { 58 | return fail('binding.gyp does not contain a targets array ({ targets: [ ... ] })') 59 | } 60 | 61 | if (!is.isString(doc.targets[0].target_name)) { 62 | return fail('binding.gyp does not contain a target_name for the first target') 63 | } 64 | 65 | if (doc.targets[0].target_name !== 'myaddon') { 66 | return fail('binding.gyp does not name the first target "myaddon"') 67 | } 68 | 69 | exercise.emit('pass', 'binding.gyp includes a "myaddon" target') 70 | 71 | if (!is.isArray(doc.targets[0].sources)) { 72 | return fail('binding.gyp does not contain a sources array for the first target (sources: [ ... ])') 73 | } 74 | 75 | if (!doc.targets[0].sources.some(function (s) { return s === 'myaddon.cc' })) { 76 | return fail('binding.gyp does not list "myaddon.cc" in the sources array for the first target') 77 | } 78 | 79 | exercise.emit('pass', 'binding.gyp includes "myaddon.cc" as a source file') 80 | 81 | if (!exercise.skipBindingIncludeDirs) { 82 | if (!is.isArray(doc.targets[0].include_dirs)) { 83 | return fail('binding.gyp does not contain a include_dirs array for the first target (include_dirs: [ ... ])') 84 | } 85 | 86 | const nanConstruct = '` by using the converter `Nan::To(info[0])`. Now you have a convertable native 64-bit (if you need) `int` value from a `Number` _if it has been passed_. Check if it's been passed appropriately by checking the `Maybe` with `IsNothing()`, then if you have a value you can finally get a proper `int` using `FromJust()` on it. 20 | 2. Make a **sleep** happen. Unfortunately you achieve this differently in C++ depending on your platform, and what's more, you *should* write your native add-ons to be cross-platform compatible. On Windows, you call the built-in function `Sleep(time)` (where `time` is in milliseconds). On "POSIX-compliant" systems like Linux and OS X, you call `usleep(time)` (where `time` is in **microseconds**). To call `usleep()`, you must `#include` the *unistd.h* system header. 21 | 22 | To make a cross-platform sleep, we can use C++ macros to determine whether we are compiling on Windows or not. 23 | 24 | ```c++ 25 | // at the top of your file 26 | #ifndef _WIN32 27 | #include 28 | #endif 29 | 30 | ... 31 | 32 | // (in function) 33 | #ifdef _WIN32 34 | Sleep(x); 35 | #else 36 | usleep(x * 1000); 37 | #endif 38 | ``` 39 | 40 | 3. Use `Nan::MakeCallback()` to call the callback function, which will be of type `Function`. Recall that to convert to a `Local` you can use `info[1].As()`. V8 `Function` objects have a `Call()` method on them that you can use. `Nan::MakeCallback()` improves on this by wiring up domains and other debugging support, so that's what we'll use here. 41 | 42 | Use `Nan::MakeCallback()`` like this: 43 | 44 | ```c++ 45 | Nan::MakeCallback(Nan::GetCurrentContext()->Global(), callback, 0, NULL); 46 | ``` 47 | 48 | The first argument specifies what to use as `this` in JavaScript. Here it's just be `global`. The second argument is the function you wish to use as a callback. The third argument is the number of arguments to apply to the function. The fourth argument is an array of `Local` handles that supply the arguments. In this case, we're not passing any arguments to the callback, so we specify `0` arguments and a `NULL` array. 49 | 50 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 51 | 52 | ## Conditions 53 | 54 | Your submission will be compiled using `node-gyp rebuild` and executed with `node . x`, where `x` is an integer representing the number of milliseconds to sleep. Your code will be timed to ensure an appropriate amount of time has delayed before the program exits. Standard output will be checked for `"Done!"`. Your code will be checked to ensure that the C++ code is performing the sleep. 55 | 56 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 57 | 58 | __»__ To print these instructions again, run: `{appname} print` 59 | __»__ To compile and test your solution, run: `{appname} verify myaddon` 60 | __»__ For help run: `{appname} help` 61 | -------------------------------------------------------------------------------- /exercises/its_a_twoway_street/exercise.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const copy = require('../../lib/copy') 3 | const gyp = require('../../lib/gyp') 4 | const solutions = require('../../lib/solutions') 5 | const check = require('../../lib/check') 6 | const compile = require('../../lib/compile') 7 | const packagejson = require('../../lib/packagejson') 8 | const execWith = require('../../lib/execWith') 9 | 10 | const solutionFiles = ['myaddon.cc', 'index.js'] 11 | // a place to make a full copy to run a test compile 12 | const copyTempDir = path.join(process.cwd(), '~test-addon.' + Math.floor(Math.random() * 10000)) 13 | // a place to make a full copy to replace myaddon.cc with a mock to do a mocked run to test JS 14 | const copyFauxTempDir = path.join(process.cwd(), '~test-addon-faux.' + Math.floor(Math.random() * 10000)) 15 | 16 | let exercise = require('workshopper-exercise')() 17 | 18 | // add solutions file listing from solutions/ directory 19 | exercise = solutions(exercise, solutionFiles) 20 | 21 | // the steps towards verification 22 | exercise.addProcessor(check.checkSubmissionDir) 23 | exercise.addProcessor(copy.copyTemp([copyTempDir, copyFauxTempDir])) 24 | exercise.addProcessor(copyFauxAddon) 25 | exercise.addProcessor(packagejson.checkPackageJson) 26 | exercise.addProcessor(gyp.checkBinding) 27 | exercise.addProcessor(compile.checkCompile(copyTempDir)) 28 | exercise.addProcessor(checkJs) 29 | exercise.addProcessor(checkExec) 30 | 31 | // always clean up the temp directories 32 | exercise.addCleanup(copy.cleanup([copyTempDir, copyFauxTempDir])) 33 | 34 | function copyFauxAddon (mode, callback) { 35 | copy(path.join(__dirname, 'solution', 'myaddon.cc'), copyFauxTempDir, { overwrite: true }, function (err) { 36 | if (err) { return callback(err) } 37 | 38 | callback(null, true) 39 | }) 40 | } 41 | 42 | const expectFn = (arg) => (Buffer.from(arg).length + '\n') 43 | 44 | // run `node-gyp rebuild` on a mocked version of the addon that prints what we want 45 | // so we can test that their JS is doing what it is supposed to be doing and there 46 | // is no cheating! (e.g. console.log(...)) 47 | function checkJs (mode, callback) { 48 | const exercise = this 49 | 50 | if (!exercise.passed) { return callback(null, true) } // shortcut if we've already had a failure 51 | 52 | gyp.rebuild(copyFauxTempDir, function (err) { 53 | if (err) { 54 | exercise.emit('fail', 'Compile mock C++ to test JavaScript: ' + err.message) 55 | return callback(null, false) 56 | } 57 | 58 | execWith(copyFauxTempDir, '♥ FAUX', expectFn, function (err, pass) { 59 | if (err) { return callback(err) } 60 | if (!pass) { 61 | exercise.emit('fail', 'JavaScript code loads addon, invokes `length(str)` method and prints the return value') 62 | return callback(null, false) 63 | } 64 | 65 | execWith(copyFauxTempDir, '♥ FAUX FAUX FAUX FAUX ♥', expectFn, function (err, pass) { 66 | if (err) { return callback(err) } 67 | 68 | exercise.emit(pass ? 'pass' : 'fail' 69 | , 'JavaScript code loads addon, invokes `length(str)` method and prints the return value') 70 | callback(null, pass) 71 | }) 72 | }) 73 | }) 74 | } 75 | 76 | // run a full execution of their code & addon, uses a `require()` in a child process 77 | // and check the stdout for expected 78 | function checkExec (mode, callback) { 79 | if (!exercise.passed) { return callback(null, true) } // shortcut if we've already had a failure 80 | 81 | execWith(copyTempDir, 'testing', expectFn, function (err, pass) { 82 | if (err) { return callback(err) } 83 | if (!pass) { 84 | exercise.emit('fail', 'JavaScript code loads addon, invokes `length(str)` method and prints the return value') 85 | return callback(null, false) 86 | } 87 | 88 | execWith(copyTempDir, 'this is a longer test string, with spaces in it', expectFn, function (err, pass) { 89 | if (err) { return callback(err) } 90 | 91 | exercise.emit(pass ? 'pass' : 'fail', 'Add-on receives string, calculates length and returns value') 92 | callback(null, pass) 93 | }) 94 | }) 95 | } 96 | 97 | module.exports = exercise 98 | -------------------------------------------------------------------------------- /exercises/for_the_sake_of_argument/exercise.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const childProcess = require('child_process') 3 | const copy = require('../../lib/copy') 4 | const gyp = require('../../lib/gyp') 5 | const solutions = require('../../lib/solutions') 6 | const check = require('../../lib/check') 7 | const compile = require('../../lib/compile') 8 | const packagejson = require('../../lib/packagejson') 9 | const execWith = require('../../lib/execWith') 10 | 11 | const solutionFiles = ['myaddon.cc', 'index.js'] 12 | // a place to make a full copy to run a test compile 13 | const copyTempDir = path.join(process.cwd(), '~test-addon.' + Math.floor(Math.random() * 10000)) 14 | // a place to make a full copy to replace myaddon.cc with a mock to do a mocked run to test JS 15 | const copyFauxTempDir = path.join(process.cwd(), '~test-addon-faux.' + Math.floor(Math.random() * 10000)) 16 | // what we should get on stdout for this to pass 17 | const expected = 'this is a test, I repeat, this is a test' 18 | 19 | let exercise = require('workshopper-exercise')() 20 | 21 | // add solutions file listing from solutions/ directory 22 | exercise = solutions(exercise, solutionFiles) 23 | 24 | // the steps towards verification 25 | exercise.addProcessor(check.checkSubmissionDir) 26 | exercise.addProcessor(copy.copyTemp([copyTempDir, copyFauxTempDir])) 27 | exercise.addProcessor(copyFauxAddon) 28 | exercise.addProcessor(packagejson.checkPackageJson) 29 | exercise.addProcessor(gyp.checkBinding) 30 | exercise.addProcessor(compile.checkCompile(copyTempDir)) 31 | exercise.addProcessor(checkJs) 32 | exercise.addProcessor(checkExec) 33 | 34 | // always clean up the temp directories 35 | exercise.addCleanup(copy.cleanup([copyTempDir, copyFauxTempDir])) 36 | 37 | function copyFauxAddon (mode, callback) { 38 | copy(path.join(__dirname, 'solution', 'myaddon.cc'), copyFauxTempDir, { overwrite: true }, function (err) { 39 | if (err) { return callback(err) } 40 | 41 | callback(null, true) 42 | }) 43 | } 44 | 45 | // run `node-gyp rebuild` on a mocked version of the addon that prints what we want 46 | // so we can test that their JS is doing what it is supposed to be doing and there 47 | // is no cheating! (e.g. console.log(...)) 48 | function checkJs (mode, callback) { 49 | const exercise = this 50 | 51 | if (!exercise.passed) { return callback(null, true) } // shortcut if we've already had a failure 52 | 53 | gyp.rebuild(copyFauxTempDir, function (err) { 54 | if (err) { 55 | exercise.emit('fail', 'Compile mock C++ to test JavaScript: ' + err.message) 56 | return callback(null, false) 57 | } 58 | 59 | execWith(copyFauxTempDir, 'FAUX', 'FAUX\n', function (err, pass) { 60 | if (err) { 61 | return callback(err) 62 | } 63 | 64 | exercise.emit(pass ? 'pass' : 'fail', 'JavaScript code loads addon and invokes `print(str)` method') 65 | 66 | callback(null, pass) 67 | }) 68 | }) 69 | } 70 | 71 | // run a full execution of their code & addon, uses a `require()` in a child process 72 | // and check the stdout for expected 73 | function checkExec (mode, callback) { 74 | if (!exercise.passed) { return callback(null, true) } // shortcut if we've already had a failure 75 | 76 | childProcess.exec( 77 | '"' + 78 | process.execPath + 79 | '" "' + 80 | copyTempDir + 81 | '" "' + 82 | expected + 83 | '"' 84 | , function (err, stdout, stderr) { 85 | if (err) { 86 | process.stderr.write(stderr) 87 | process.stdout.write(stdout) 88 | return callback(err) 89 | } 90 | 91 | const pass = stdout.toString().replace('\r', '') === expected + '\n' 92 | const seminl = !pass && stdout.toString() === expected 93 | 94 | if (!seminl && !pass) { 95 | process.stderr.write(stderr) 96 | process.stdout.write(stdout) 97 | } 98 | 99 | if (seminl) { 100 | exercise.emit('fail', 'Addon prints out expected string (missing newline)') 101 | } else { 102 | exercise.emit(pass ? 'pass' : 'fail', 'Addon prints out expected string') 103 | } 104 | 105 | callback(null, pass) 106 | } 107 | ) 108 | } 109 | 110 | module.exports = exercise 111 | -------------------------------------------------------------------------------- /exercises/offloading_the_work/problem.md: -------------------------------------------------------------------------------- 1 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 2 | 3 | ## Task 4 | 5 | Your effort on passing the previous exercise is to be congratulated. However, you've created a bug! You're blocking the JavaScript thread so nothing else can be done in your application. 6 | 7 | Your task now is to move the *sleep* off onto a *worker thread* so that the JavaScript thread doesn't block and can continue with its work. You still need to delay the callback for the correct amount of time! 8 | 9 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 10 | 11 | ## Description 12 | 13 | The `usleep()` and `Sleep()` functions put the current thread to sleep so nothing else executes. For your add-on, this also includes the whole JavaScript/V8 and Node.js execution environment. This isn't acceptable for a Node.js application that should *never* block. 14 | 15 | In your working directory we have given you a new file named {boilerplate:index.js} that you can use to replace the *index.js* file from your previous solution. 16 | 17 | This new JavaScript code will create an interval timer to print a `.` to standard out every 50ms. It will also print out `Waiting` at the start but the code to print this is called *after* the call to your add-on. This will demonstrate just how broken your code is. 18 | 19 | Run your code and you will likely see it print this: 20 | 21 | ``` 22 | Done! 23 | Waiting 24 | ``` 25 | 26 | What the code *should* be doing, and what you need to achieve, is this, without changing any JavaScript: 27 | 28 | ``` 29 | Waiting..........................Done! 30 | ``` 31 | 32 | ## Worker threads 33 | 34 | Node.js spins up 4 worker threads (by default) in a thread-pool for handling file system I/O. In our C++ code we can easily make use of this thread-pool to offload work from the JavaScript thread. Your task is to get the `usleep()` or `Sleep()` to run on a worker thread and then have the callback fire from within the JavaScript thread. 35 | 36 | Fortunately, NAN makes this a little easier otherwise it would be difficult to achieve. 37 | 38 | We have also given you a new file {boilerplate:myaddon.cc} in your current working directory that has a basic structure you can use. It defines a `MyWorker` C++ *class* that extends the `Nan::AsyncWorker` class that NAN uses to define a discrete chunk of asynchronous work. 39 | 40 | To use your worker class, wrap up a standard V8 `Local` in a `Nan::Callback` object. This protects the callback from garbage collection and exposes a simple `Call()` method that replaces the need to use `Nan::MakeCallback()`. 41 | 42 | To use `MyWorker` and `Nan::Callback` you need to allocate memory on the *"heap"* for them by using the `new` operator. NAN will perform clean-up of both objects for you so you don't need a matching `delete` in this case as you normally would in C++. 43 | 44 | Things you need to do: 45 | 46 | 47 | 1. Wrap your `Local` in a `Nan::Callback` with: `Nan::Callback* nanCallback = new Nan::Callback(callback);` 48 | 49 | 50 | 2. Create a `MyWorker` and pass it the `nanCallback` and your amount of timer `delay` with `MyWorker* worker = new MyWorker(nanCallback, delay);` 51 | 52 | 53 | 3. Submit `worker` to the thread-pool with `Nan::AsyncQueueWorker(worker);` After this you can return as normal and the asynchronous work will be performed when there is a spare thread for it. 54 | 55 | 56 | 4. Put your `usleep()` / `Sleep()` logic into the `MyWorker`s `Execute()` method. 57 | 58 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 59 | 60 | ## Conditions 61 | 62 | Your submission will be compiled using `node-gyp rebuild` and executed with `node . x`, where `x` is an integer representing the number of milliseconds to sleep. Your code will be timed to ensure an appropriate amount of time has delayed before the program exits. Standard output will be checked for `"Waiting......Done!"` (with an appropriate number of `.` characters. Your code will be checked to ensure that the C++ code is performing the sleep and that it is performed asynchronously. 63 | 64 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 65 | 66 | __»__ To print these instructions again, run: `{appname} print` 67 | __»__ To print additional learning material relating to these instructions, run: `{appname} more` 68 | __»__ To compile and test your solution, run: `{appname} verify myaddon` 69 | __»__ For help run: `{appname} help` 70 | -------------------------------------------------------------------------------- /exercises/mission_possible_part_three/exercise.js: -------------------------------------------------------------------------------- 1 | const boilerplate = require('workshopper-boilerplate') 2 | const path = require('path') 3 | const copy = require('../../lib/copy') 4 | const compile = require('../../lib/compile') 5 | const solutions = require('../../lib/solutions') 6 | const check = require('../../lib/check') 7 | const gyp = require('../../lib/gyp') 8 | const packagejson = require('../../lib/packagejson') 9 | const execWith = require('../../lib/execWith') 10 | 11 | // a place to make a full copy to run a test compile 12 | const copyTempDir = path.join(process.cwd(), '~test-addon.' + Math.floor(Math.random() * 10000)) 13 | // a place to make a full copy to replace myaddon.cc with a mock to do a mocked run to test JS 14 | const copyFauxTempDir = path.join(process.cwd(), '~test-addon-faux.' + Math.floor(Math.random() * 10000)) 15 | // what we should get on stdout for this to pass 16 | const expected = 'I am a native addon and I AM ALIVE!' 17 | const solutionFiles = ['myaddon.cc'] 18 | 19 | let exercise = require('workshopper-exercise')() 20 | 21 | // add solutions file listing from solutions/ directory 22 | exercise = solutions(exercise, solutionFiles) 23 | // add boilerplate functionality 24 | exercise = boilerplate(exercise) 25 | 26 | // boilerplate directory to copy into CWD to give them a base to start from 27 | exercise.addBoilerplate(path.join(__dirname, 'boilerplate/myaddon.cc')) 28 | 29 | // the steps towards verification 30 | exercise.addProcessor(check.checkSubmissionDir) 31 | exercise.addProcessor(copy.copyTemp([copyTempDir, copyFauxTempDir])) 32 | exercise.addProcessor(copyFauxAddon) 33 | exercise.addProcessor(packagejson.checkPackageJson) 34 | exercise.addProcessor(gyp.checkBinding) 35 | exercise.addProcessor(compile.checkCompile(copyTempDir)) 36 | exercise.addProcessor(checkJs) 37 | exercise.addProcessor(checkExec) 38 | 39 | // always clean up the temp directories 40 | exercise.addCleanup(copy.cleanup([copyTempDir, copyFauxTempDir])) 41 | 42 | function copyFauxAddon (mode, callback) { 43 | copy(path.join(__dirname, 'faux', 'myaddon.cc'), copyFauxTempDir, { overwrite: true }, function (err) { 44 | if (err) { return callback(err) } 45 | 46 | callback(null, true) 47 | }) 48 | } 49 | 50 | // run `node-gyp rebuild` on a mocked version of the addon that prints what we want 51 | // so we can test that their JS is doing what it is supposed to be doing and there 52 | // is no cheating! (e.g. console.log(...)) 53 | function checkJs (mode, callback) { 54 | const exercise = this 55 | 56 | if (!exercise.passed) { return callback(null, true) } // shortcut if we've already had a failure 57 | 58 | gyp.rebuild(copyFauxTempDir, function (err) { 59 | if (err) { 60 | exercise.emit('fail', 'Compile mock C++ to test JavaScript: ' + err.message) 61 | return callback(null, false) 62 | } 63 | 64 | execWith( 65 | require.resolve('../../lib/require-argv2'), 66 | copyFauxTempDir, 67 | 'FAUX\n', 68 | function (err, pass) { 69 | if (err) { 70 | return callback(err) 71 | } 72 | 73 | exercise.emit(pass ? 'pass' : 'fail', 'JavaScript code loads addon and invokes `print()` method') 74 | 75 | callback(null, pass) 76 | } 77 | ) 78 | }) 79 | } 80 | 81 | // run a full execution of their code & addon, uses a `require()` in a child process 82 | // and check the stdout for expected 83 | function checkExec (mode, callback) { 84 | if (!exercise.passed) { return callback(null, true) } // shortcut if we've already had a failure 85 | 86 | execWith( 87 | require.resolve('../../lib/require-argv2'), 88 | copyTempDir, 89 | `${expected}\n`, 90 | { 91 | processPass: function (pass, stdout, stderr) { 92 | const seminl = !pass && stdout.toString() === expected 93 | const semicase = !pass && !seminl && new RegExp(expected, 'i').test(stdout.toString()) 94 | 95 | if (!seminl && !semicase && !pass) { 96 | process.stderr.write(stderr) 97 | process.stdout.write(stdout) 98 | } 99 | 100 | if (seminl) { 101 | exercise.emit('fail', 'Addon prints out expected string (missing newline)') 102 | } else if (semicase) { 103 | exercise.emit('fail', 'Addon prints out expected string (printed with wrong character case)') 104 | } else { 105 | exercise.emit(pass ? 'pass' : 'fail', 'Addon prints out expected string') 106 | } 107 | } 108 | }, 109 | function (err, pass) { 110 | if (err) { 111 | return callback(err) 112 | } 113 | 114 | callback(null, pass) 115 | } 116 | ) 117 | } 118 | 119 | module.exports = exercise 120 | -------------------------------------------------------------------------------- /exercises/call_me_maybe/exercise.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const copy = require('../../lib/copy') 3 | const gyp = require('../../lib/gyp') 4 | const solutions = require('../../lib/solutions') 5 | const check = require('../../lib/check') 6 | const compile = require('../../lib/compile') 7 | const packagejson = require('../../lib/packagejson') 8 | const execWith = require('../../lib/execWith') 9 | const execWithMeasured = require('../../lib/execWithMeasured') 10 | 11 | const solutionFiles = ['myaddon.cc', 'index.js'] 12 | // a place to make a full copy to run a test compile 13 | const copyTempDir = path.join(process.cwd(), '~test-addon.' + Math.floor(Math.random() * 10000)) 14 | // a place to make a full copy to replace myaddon.cc with a mock to do a mocked run to test JS 15 | const copyFauxTempDir = path.join(process.cwd(), '~test-addon-faux.' + Math.floor(Math.random() * 10000)) 16 | // what we should get on stdout for this to pass 17 | 18 | let exercise = require('workshopper-exercise')() 19 | 20 | // add solutions file listing from solutions/ directory 21 | exercise = solutions(exercise, solutionFiles) 22 | 23 | // the steps towards verification 24 | exercise.addProcessor(check.checkSubmissionDir) 25 | exercise.addProcessor(copy.copyTemp([copyTempDir, copyFauxTempDir])) 26 | exercise.addProcessor(copyFauxAddon) 27 | exercise.addProcessor(packagejson.checkPackageJson) 28 | exercise.addProcessor(gyp.checkBinding) 29 | exercise.addProcessor(compile.checkCompile(copyTempDir)) 30 | exercise.addProcessor(checkJs) 31 | exercise.addProcessor(checkExec) 32 | 33 | // always clean up the temp directories 34 | exercise.addCleanup(copy.cleanup([copyTempDir, copyFauxTempDir])) 35 | 36 | function copyFauxAddon (mode, callback) { 37 | copy(path.join(__dirname, 'faux', 'myaddon.cc'), copyFauxTempDir, { overwrite: true }, function (err) { 38 | if (err) { return callback(err) } 39 | 40 | callback(null, true) 41 | }) 42 | } 43 | 44 | // run `node-gyp rebuild` on a mocked version of the addon that prints what we want 45 | // so we can test that their JS is doing what it is supposed to be doing and there 46 | // is no cheating! (e.g. console.log(...)) 47 | function checkJs (mode, callback) { 48 | const exercise = this 49 | 50 | if (!exercise.passed) { return callback(null, true) } // shortcut if we've already had a failure 51 | 52 | gyp.rebuild(copyFauxTempDir, function (err) { 53 | if (err) { 54 | exercise.emit('fail', 'Compile mock C++ to test JavaScript: ' + err.message) 55 | return callback(null, false) 56 | } 57 | 58 | execWith(copyFauxTempDir, 111, 'FAUX 111\nDone!\n', function (err, pass) { 59 | if (err) { return callback(err) } 60 | if (!pass) { 61 | exercise.emit('fail', 'JavaScript code loads addon and invokes `delay(x, cb)` method') 62 | return callback(null, false) 63 | } 64 | 65 | execWith(copyFauxTempDir, 1111, 'FAUX 1111\nDone!\n', function (err, pass) { 66 | if (err) { return callback(err) } 67 | 68 | exercise.emit(pass ? 'pass' : 'fail' 69 | , 'JavaScript code loads addon and invokes `delay(x, cb)` method') 70 | callback(null, pass) 71 | }) 72 | }) 73 | }) 74 | } 75 | 76 | // run a full execution of their code & addon, uses a `require()` in a child process 77 | // and check the stdout for expected 78 | function checkExec (mode, callback) { 79 | if (!exercise.passed) { return callback(null, true) } // shortcut if we've already had a failure 80 | 81 | execWithMeasured(copyTempDir, 111, 'Done!\n', function (err, result) { 82 | if (err) { return callback(err) } 83 | 84 | if (!result.pass) { 85 | exercise.emit('fail', 'JavaScript code loads addon, invokes `delay(x, cb)` method and sleeps for x milliseconds') 86 | return callback(null, false) 87 | } 88 | 89 | let delay = result.duration 90 | if (delay < 100 || delay > 300) { 91 | if (retry) { 92 | return runOrRetry(false) 93 | } else { 94 | exercise.emit('fail', 'Slept for the right amount of time (asked for 111ms, slept for ' + delay + ')') 95 | return callback(null, false) 96 | } 97 | } 98 | 99 | execWithMeasured(copyTempDir, 1111, 'Done!\n', function (err, result) { 100 | if (err) { return callback(err) } 101 | 102 | delay = result.duration 103 | 104 | if (delay < 1000 || delay > 1300) { 105 | exercise.emit('fail', 'Slept for the right amount of time (asked for 1111ms, slept for ' + delay + 'ms)') 106 | return callback(null, false) 107 | } 108 | 109 | const pass = result.pass 110 | exercise.emit(pass ? 'pass' : 'fail' 111 | , 'JavaScript code loads addon, invokes `delay(x, cb)` method and sleeps for x milliseconds') 112 | callback(null, pass) 113 | }) 114 | }) 115 | } 116 | 117 | module.exports = exercise 118 | -------------------------------------------------------------------------------- /exercises/mission_possible_part_three/problem.md: -------------------------------------------------------------------------------- 1 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 2 | 3 | ## Task 4 | 5 | Finish your add-on by adding a C++ function that prints a message to standard output when invoked by JavaScript. 6 | 7 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 8 | 9 | ## Description 10 | 11 | Because C++ is no simple matter, you have been provided with a skeleton of an add-on file. Move the file named ***{boilerplate:myaddon.cc}*** in your current working directory, *to* the directory containing your add-on, calling it *myaddon.cc*. 12 | 13 | ### Mission: write *myaddon.cc* 14 | 15 | Let's write C++! In *myaddon.cc* you have some things to fill in. 16 | 17 | 18 | At the top of your file you must include the NAN macros and helper code into your build. You can think of this as *similar* to `require()` in Node.js code. Here's the include line you need: 19 | 20 | 21 | ```cpp 22 | #include 23 | ``` 24 | 25 | Now you must use the v8 namespace: 26 | 27 | ```cpp 28 | using namespace v8; 29 | ``` 30 | 31 | This line imports everything in the "v8" C++ *namespace* into your code so you don't have to explicitly name everything. A `v8::String` is the `String` class inside the "v8" namespace. `using namespace v8` lets us just write `String` to refer to this object. 32 | 33 | ### Mission: implement Print() 34 | 35 | ```cpp 36 | NAN_METHOD(Print) { 37 | // ... 38 | } 39 | ``` 40 | 41 | This construct uses a C++ macro from NAN to expose a public *method* that is accessible via V8 to your JavaScript. The method is called `Print` on the C++ side. It's missing a body! You need to add code to make something happen when it's called. 42 | 43 | Your method body should use the standard C `printf()` function to print to standard out. (You can also use `cout` for more idiomatic C++ if you dare.) Provide text surrounded by double-quotes: `printf("a string");`. You need to print the following line: 44 | 45 | "I am a native addon and I AM ALIVE!" 46 | 47 | Remember to add an explicit new-line character at the end: `\n`. 48 | 49 | ### Mission: return a value 50 | 51 | If you need to return a value from your function, you can do so by passing a variable to `info.GetReturnValue().Set()` 52 | 53 | ### Mission: export `Print` to JavaScript 54 | 55 | Now we export this method to JavaScript! Copy and paste this line: 56 | 57 | ```cpp 58 | NAN_MODULE_INIT(Init) { 59 | Nan::Set(target, Nan::New("print").ToLocalChecked(), 60 | Nan::GetFunction(Nan::New(Print)).ToLocalChecked()); 61 | } 62 | ``` 63 | 64 | Take a deep breath! This line is secretly the equivalent of `module.exports.print = Print` in JavaScript. Let's step through it. 65 | 66 | 67 | * `NAN_MODULE_INIT(Init) { .. }` defines a function that receives a `target` object from V8. This is the same object you would receive in a JavaScript module as `module.exports` but it's now exposed as a C++ type. 68 | * `Nan::New()` creates a new V8 object, in this case a `String`. This object is used as the property name of the `exports` object. 69 | * `Nan::GetFunction(Nan::New(Print)).ToLocalChecked())` is how we get a reference to the method we declared earlier so that we can expose it to JavaScript. 70 | 71 | 72 | Notice how we are declaring `print` as an idiomatic JavaScript lower-case name while `Print` is idiomatic C++ title-case. Get used to the verbosity; this is C++ after all! 73 | 74 | ### Mission: expose the init function to Node.js. 75 | 76 | The `NAN_MODULE_INIT(Init)` function needs to be given to Node.js because it's the entry-point to the module. Node.js is responsible for passing in the `exports` object. Exposing it to Node.js requires a native Node.js *macro* that does the heavy-lifting of registering your initialization function: 77 | 78 | ```c++ 79 | NODE_MODULE(modulename, InitFunction) 80 | ``` 81 | 82 | Where `modulename` matches the name in your *binding.gyp* and your *index.js* and `InitFunction` is the name of your C++ initialization function that accepts the `exports` object. 83 | 84 | ### Mission: build it! 85 | 86 | When you have these tasks complete, type: 87 | 88 | node-gyp rebuild 89 | 90 | and watch your add-on compile. Watch for errors and fix anything that prevents a successful compilation. 91 | 92 | Running the add-on should print the following text: 93 | 94 | ``` 95 | I am a native addon and I AM ALIVE! 96 | ``` 97 | 98 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 99 | 100 | ## Conditions 101 | 102 | Your submission will be compiled using `node-gyp rebuild` and executed with `node .`, the standard output must be "I am a native addon and I AM ALIVE!" and this must be printed by the compiled (C++) component of your solution. 103 | 104 | {cyan}──────────────────────────────────────────────────────────────────────{/cyan} 105 | 106 | __»__ To print these instructions again, run: `{appname} print` 107 | __»__ To print additional learning material relating to these instructions, run: `{appname} more` 108 | __»__ To compile and test your solution, run: `{appname} verify myaddon` 109 | __»__ For help run: `{appname} help` 110 | -------------------------------------------------------------------------------- /exercises/offloading_the_work/exercise.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const boilerplate = require('workshopper-boilerplate') 3 | const copy = require('../../lib/copy') 4 | const gyp = require('../../lib/gyp') 5 | const solutions = require('../../lib/solutions') 6 | const check = require('../../lib/check') 7 | const compile = require('../../lib/compile') 8 | const packagejson = require('../../lib/packagejson') 9 | const execWith = require('../../lib/execWith') 10 | const execWithMeasured = require('../../lib/execWithMeasured') 11 | 12 | const solutionFiles = ['myaddon.cc'] 13 | // a place to make a full copy to run a test compile 14 | const copyTempDir = path.join(process.cwd(), '~test-addon.' + Math.floor(Math.random() * 10000)) 15 | // a place to make a full copy to replace myaddon.cc with a mock to do a mocked run to test JS 16 | const copyFauxTempDir = path.join(process.cwd(), '~test-addon-faux.' + Math.floor(Math.random() * 10000)) 17 | // what we should get on stdout for this to pass 18 | 19 | let exercise = require('workshopper-exercise')() 20 | 21 | // add solutions file listing from solutions/ directory 22 | exercise = solutions(exercise, solutionFiles) 23 | // add boilerplate functionality 24 | exercise = boilerplate(exercise) 25 | 26 | // boilerplate files for them to start from 27 | exercise.addBoilerplate(path.join(__dirname, 'boilerplate/index.js')) 28 | exercise.addBoilerplate(path.join(__dirname, 'boilerplate/myaddon.cc')) 29 | 30 | // the steps towards verification 31 | exercise.addProcessor(check.checkSubmissionDir) 32 | exercise.addProcessor(copy.copyTemp([copyTempDir, copyFauxTempDir])) 33 | exercise.addProcessor(copyFauxAddon) 34 | exercise.addProcessor(packagejson.checkPackageJson) 35 | exercise.addProcessor(gyp.checkBinding) 36 | exercise.addProcessor(compile.checkCompile(copyTempDir)) 37 | exercise.addProcessor(checkJs) 38 | exercise.addProcessor(checkExec) 39 | 40 | // always clean up the temp directories 41 | exercise.addCleanup(copy.cleanup([copyTempDir, copyFauxTempDir])) 42 | 43 | function copyFauxAddon (mode, callback) { 44 | copy(path.join(__dirname, 'faux', 'myaddon.cc'), copyFauxTempDir, { overwrite: true }, function (err) { 45 | if (err) { return callback(err) } 46 | 47 | callback(null, true) 48 | }) 49 | } 50 | 51 | const resolvePass = (expected, stdout) => expected.test(stdout.toString().replace(/\r/g, '')) 52 | 53 | // run `node-gyp rebuild` on a mocked version of the addon that prints what we want 54 | // so we can test that their JS is doing what it is supposed to be doing and there 55 | // is no cheating! (e.g. console.log(...)) 56 | function checkJs (mode, callback) { 57 | const exercise = this 58 | const expect = /FAUX 1\nFAUX 2\nWaiting\.*FAUX 3\n\.+FAUX 4\n\.*Done!\n/m 59 | 60 | if (!exercise.passed) { return callback(null, true) } // shortcut if we've already had a failure 61 | 62 | gyp.rebuild(copyFauxTempDir, function (err) { 63 | if (err) { 64 | exercise.emit('fail', 'Compile mock C++ to test JavaScript: ' + err.message) 65 | return callback(null, false) 66 | } 67 | 68 | execWith(copyFauxTempDir, 111, expect, { resolvePass }, function (err, pass) { 69 | if (err) { return callback(err) } 70 | 71 | if (!pass) { 72 | exercise.emit('fail', 'JavaScript code loads addon and invokes `delay(x, cb)` method') 73 | return callback(null, false) 74 | } 75 | 76 | execWith(copyFauxTempDir, 1111, expect, { resolvePass }, function (err, pass) { 77 | if (err) { return callback(err) } 78 | 79 | exercise.emit(pass ? 'pass' : 'fail' 80 | , 'JavaScript code loads addon and invokes `delay(x, cb)` method') 81 | callback(null, pass) 82 | }) 83 | }) 84 | }) 85 | } 86 | 87 | // run a full execution of their code & addon, uses a `require()` in a child process 88 | // and check the stdout for expected 89 | function checkExec (mode, callback) { 90 | if (!exercise.passed) { return callback(null, true) } // shortcut if we've already had a failure 91 | 92 | const expect = /Waiting\.\.*Done!\n/m 93 | 94 | execWithMeasured(copyTempDir, 111, expect, { resolvePass }, function (err, result) { 95 | if (err) { return callback(err) } 96 | 97 | if (!result.pass) { 98 | exercise.emit('fail', 'JavaScript code loads addon, invokes `delay(x, cb)` method and sleeps for x seconds') 99 | return callback(null, false) 100 | } 101 | 102 | let delay = result.duration 103 | 104 | if (delay < 100 || delay > 600) { 105 | if (retry) { 106 | return runOrRetry(false) 107 | } else { 108 | exercise.emit('fail', 'Slept for the right amount of time (asked for 111ms, slept for ' + delay + 'ms)') 109 | return callback(null, false) 110 | } 111 | } 112 | 113 | execWithMeasured(copyTempDir, 1111, expect, { resolvePass }, function (err, result) { 114 | if (err) { return callback(err) } 115 | 116 | delay = result.duration 117 | 118 | if (delay < 1000 || delay > 1500) { 119 | exercise.emit('fail', 'Slept for the right amount of time (asked for 1111ms, slept for ' + delay + 'ms)') 120 | return callback(null, false) 121 | } 122 | 123 | const pass = result.pass 124 | exercise.emit(pass ? 'pass' : 'fail' 125 | , 'JavaScript code loads addon, invokes `delay(x, cb)` method and sleeps for x seconds') 126 | callback(null, pass) 127 | }) 128 | }) 129 | } 130 | 131 | module.exports = exercise 132 | -------------------------------------------------------------------------------- /exercises/am_i_ready/exercise.js: -------------------------------------------------------------------------------- 1 | const versions = require('./vars.json').versions 2 | const MIN_GCC_VERSION = versions.gcc 3 | const MIN_LLVM_VERSION = versions.llvm 4 | const MIN_NODE_GYP_VERSION = versions.gyp 5 | const MIN_NODE_VERSION = versions.node.min 6 | const MAX_NODE_VERSION = versions.node.max 7 | 8 | const { exec } = require('child_process') 9 | const path = require('path') 10 | const fs = require('fs') 11 | const semver = require('semver') 12 | const chalk = require('chalk') 13 | const rimraf = require('rimraf') 14 | const copy = require('../../lib/copy') 15 | const win = process.platform === 'win32' 16 | const checkPython = require('../../lib/check-python') 17 | 18 | const testPackageSrc = path.join(__dirname, '../../packages/test-addon/') 19 | // a place to make a full copy to run a test compile 20 | const testPackageRnd = path.join(process.cwd(), '~test-addon.' + Math.floor(Math.random() * 10000)) 21 | 22 | const exercise = require('workshopper-exercise')() 23 | 24 | exercise.requireSubmission = false // don't need a submission arg 25 | exercise.addSetup(setup) 26 | exercise.addProcessor(checkPython) 27 | exercise.addProcessor(processor) 28 | exercise.addCleanup(cleanup) 29 | 30 | // copy test package to a temporary location, populate it with bindings and nan 31 | function setup (mode, callback) { 32 | copy(testPackageSrc, testPackageRnd, { overwrite: true }, function (err) { 33 | if (err) { return callback(err) } 34 | 35 | copy.copyDeps(testPackageRnd, callback) 36 | }) 37 | } 38 | 39 | function cleanup (mode, pass, callback) { 40 | setTimeout(function () { 41 | rimraf(testPackageRnd, callback) 42 | }, 1000) 43 | } 44 | 45 | function processor (mode, callback) { 46 | const checks = [checkNode, win ? checkMsvc : checkGcc, checkNodeGyp, checkBuild] 47 | let pass = true 48 | 49 | ;(function checkNext (curr) { 50 | if (!checks[curr]) { return callback(null, pass) } 51 | 52 | checks[curr](pass, function (err, _pass) { 53 | if (err) { return callback(err) } 54 | 55 | if (!_pass) { pass = false } 56 | 57 | process.nextTick(checkNext.bind(null, curr + 1)) 58 | }) 59 | })(0) 60 | } 61 | 62 | function checkNode (pass, callback) { 63 | if (!semver.satisfies(process.versions.node, '>=' + MIN_NODE_VERSION)) { 64 | exercise.emit('fail', 65 | '`' + 66 | chalk.bold('node') + 67 | '` version is too old: ' + 68 | chalk.bold('v' + process.versions.node) + 69 | ', please upgrade to a version >= ' + 70 | chalk.bold('v' + MIN_NODE_VERSION) + 71 | ' and <= ' + 72 | chalk.bold('v' + MAX_NODE_VERSION) 73 | ) 74 | return callback(null, false) 75 | } 76 | 77 | if (!semver.satisfies(process.versions.node, '<=' + MAX_NODE_VERSION)) { 78 | exercise.emit('fail', 79 | '`' + 80 | chalk.bold('node') + 81 | '` version is too new, you are likely using an unstable version: ' + 82 | chalk.bold('v' + process.versions.node) + 83 | ', please upgrade to a version >= ' + 84 | chalk.bold('v' + MIN_NODE_VERSION) + 85 | ' and <= ' + 86 | chalk.bold('v' + MAX_NODE_VERSION) 87 | ) 88 | return callback(null, false) 89 | } 90 | 91 | exercise.emit('pass', 'Found usable `' + chalk.bold('node') + '` version: ' + 92 | chalk.bold('v' + process.versions.node)) 93 | 94 | callback(null, true) 95 | } 96 | 97 | function checkGcc (pass, callback) { 98 | exec('gcc -v', { env: process.env }, function (err, stdout, stderr) { 99 | if (err) { 100 | exercise.emit('fail', '`' + chalk.bold('gcc') + '` not found in $PATH') 101 | return callback(null, false) 102 | } 103 | 104 | let versionMatch = stderr.toString().split('\n').filter(Boolean).pop() 105 | .match(/gcc version (\d+\.\d+\.\d+) /) 106 | let versionString 107 | 108 | if (versionMatch) { 109 | versionString = versionMatch && versionMatch[1] 110 | 111 | if (!semver.satisfies(versionString, '>=' + MIN_GCC_VERSION)) { 112 | exercise.emit('fail', 113 | '`' + 114 | chalk.bold('gcc') + 115 | '` version is too old: ' + 116 | chalk.bold('v' + versionString) + 117 | ', please upgrade to a version >= ' + 118 | chalk.bold('v' + MIN_GCC_VERSION) 119 | ) 120 | } 121 | } else if (stderr.toString().match(/Apple (?:LLVM|clang) version (\d+\.\d+)/)) { 122 | versionMatch = stderr.toString().match(/Apple (?:LLVM|clang) version (\d+\.\d+)/) 123 | versionString = versionMatch && versionMatch[1] + '.0' 124 | 125 | if (!semver.satisfies(versionString, '>=' + MIN_LLVM_VERSION)) { 126 | exercise.emit('fail', 127 | '`' + 128 | chalk.bold('gcc/llvm') + 129 | '` version is too old: ' + 130 | chalk.bold('v' + versionString) + 131 | ', please upgrade to a version >= ' + 132 | chalk.bold('v' + MIN_LLVM_VERSION) 133 | ) 134 | } 135 | } 136 | 137 | if (!versionMatch) { 138 | exercise.emit('fail', 'Unknown `' + chalk.bold('gcc') + '` found in $PATH') 139 | return callback(null, false) 140 | } 141 | 142 | exercise.emit('pass', 'Found usable `' + chalk.bold('gcc') + '` in $PATH: ' + chalk.bold('v' + versionString)) 143 | 144 | callback(null, true) 145 | }) 146 | } 147 | 148 | function checkMsvc (pass, callback) { 149 | const msvsVars = { 150 | 2015: 'VS140COMNTOOLS', 151 | 2013: 'VS130COMNTOOLS', 152 | 2012: 'VS120COMNTOOLS', 153 | 2011: 'VS110COMNTOOLS' 154 | } 155 | const msvsVersion = Object.keys(msvsVars).reverse().filter(function (k) { 156 | return !!process.env[msvsVars[k]] 157 | })[0] 158 | 159 | if (!msvsVersion) { 160 | exercise.emit('fail', 161 | 'Check for ' + 162 | chalk.bold('Microsoft Visual Studio') + 163 | ' version 2011, 2012 or 2013: not found on system' 164 | ) 165 | return callback(null, false) 166 | } 167 | 168 | if (!fs.existsSync(path.join((process.env[msvsVars[msvsVersion]]), 'vsvars32.bat'))) { 169 | exercise.emit('fail', 170 | 'Check for ' + 171 | chalk.bold('Microsoft Visual Studio') + 172 | ' version 2011, 2012 or 2013: not found on system' 173 | ) 174 | return callback(null, false) 175 | } 176 | 177 | exercise.emit('pass', 178 | 'Found usable `' + 179 | chalk.bold('Microsoft Visual Studio') + 180 | '`: ' + 181 | chalk.bold(msvsVersion) 182 | ) 183 | 184 | callback(null, true) 185 | } 186 | 187 | function checkNodeGyp (pass, callback) { 188 | function checkVersionString (print, versionString) { 189 | if (!versionString) { 190 | if (print) { exercise.emit('fail', 'Unknown `' + chalk.bold('node-gyp') + '` found in $PATH') } 191 | return false 192 | } 193 | 194 | if (!semver.satisfies(versionString, '>=' + MIN_NODE_GYP_VERSION)) { 195 | exercise.emit('fail', 196 | '`' + 197 | chalk.bold('node-gyp') + 198 | '` version is too old: ' + 199 | chalk.bold('v' + versionString) + 200 | ', please install a version >= ' + 201 | chalk.bold('v' + MIN_NODE_GYP_VERSION) 202 | ) 203 | return false 204 | } 205 | 206 | return true 207 | } 208 | 209 | function npmLsG (callback) { 210 | // note we can't reliably trap stdout on Windows for `node-gyp -v`, perhaps because of the 211 | // immediate `process.exit(0)` after a `console.log(version)`? 212 | exec('npm ls -g --depth 0', { env: process.env }, function (err, stdout, stderr) { 213 | if (err) { 214 | // Added some debugging to give insight into why things are failing. 215 | exercise.emit('fail', '`' + chalk.bold('node-gyp') + '` not found by `npm ls -g`') 216 | process.stdout.write(stdout) 217 | process.stderr.write(stderr) 218 | return callback(null, false) 219 | } 220 | 221 | const versionMatch = stdout.toString().match(/node-gyp@(\d+\.\d+\.\d+)/) 222 | const versionString = versionMatch && versionMatch[1] 223 | 224 | callback(null, checkVersionString(true, versionString), versionString) 225 | }) 226 | } 227 | 228 | function nodeGypV (print, callback) { 229 | // note we can't reliably trap stdout on Windows for `node-gyp -v`, perhaps because of the 230 | // immediate `process.exit(0)` after a `console.log(version)`? 231 | exec('node-gyp -v', { env: process.env }, function (err, stdout, stderr) { 232 | if (err) { 233 | if (print) { 234 | // Added some debugging to give insight into why things are failing. 235 | exercise.emit('fail', '`' + chalk.bold('node-gyp') + '` not found by `npm ls -g`') 236 | process.stdout.write(stdout) 237 | process.stderr.write(stderr) 238 | } 239 | return callback(null, false) 240 | } 241 | 242 | const versionMatch = stdout.toString().match(/v(\d+\.\d+\.\d+)/) 243 | const versionString = versionMatch && versionMatch[1] 244 | 245 | callback(null, checkVersionString(false, versionString), versionString) 246 | }) 247 | } 248 | 249 | function passFail (pass, versionString) { 250 | exercise.emit( 251 | pass ? 'pass' : 'fail' 252 | , 'Found usable `' + chalk.bold('node-gyp') + '` in $PATH' + 253 | (pass && versionString ? ': ' + chalk.bold('v' + versionString) : '') 254 | ) 255 | } 256 | 257 | // this pyramid of nasty is for the following reasons: 258 | // * `npm ls -g` is frail and will break if you have anything even partially broken 259 | // installed globally, so we can't rely on it for a first-run 260 | // * `node-gyp -v` is semi-busted on Windows because of the way Node handles 261 | // stdin, 50% of the time it gets lost and not passed up through child_process 262 | // Solution is to try `node-gyp -v` twice and then resort to `npm ls -g` 263 | // to play the odds... 264 | // Need a better solution, idea from @visnup is to `node-gyp -v > out` 265 | 266 | nodeGypV(false, function (_err, pass, versionString) { 267 | if (pass) { 268 | passFail(pass, versionString) 269 | return callback(null, true) 270 | } 271 | 272 | nodeGypV(true, function (_err, pass, versionString) { 273 | if (pass) { 274 | passFail(pass, versionString) 275 | return callback(null, true) 276 | } 277 | 278 | npmLsG(function (_err, pass, versionString) { 279 | passFail(pass, versionString) 280 | callback(null, pass) 281 | }) 282 | }) 283 | }) 284 | } 285 | 286 | function checkBuild (pass, callback) { 287 | if (!pass) { return callback() } 288 | 289 | console.log('Running `node-gyp`, this may take a few minutes if it hasn\'t been run before...') 290 | 291 | exec('node-gyp rebuild', { cwd: testPackageRnd, env: process.env }, function (err, stdout, stderr) { 292 | if (err) { 293 | if (stdout) { process.stdout.write(stdout) } 294 | if (stderr) { process.stderr.write(stderr) } 295 | if (!stdout && !stderr) { console.error(err.stack) } 296 | exercise.emit('fail', 'Could not compile test addon') 297 | return callback(null, false) 298 | } 299 | 300 | process.stdout.write(stdout) 301 | // ignore stderr, gyp cruft 302 | 303 | exercise.emit('pass', 'Compiled test package') 304 | 305 | exec( 306 | '"' + 307 | process.execPath + 308 | '" "' + 309 | require.resolve('./child') + 310 | '" ' + 311 | testPackageRnd 312 | , { env: process.env } 313 | , function (_err, stdout, stderr) { 314 | stdout.toString().split(/\n/).filter(Boolean).forEach(function (s) { 315 | exercise.emit('pass', s) 316 | }) 317 | 318 | stderr.toString().split(/\n/).filter(Boolean).forEach(function (s) { 319 | exercise.emit('fail', s) 320 | }) 321 | 322 | exercise.emit(stderr.length ? 'fail' : 'pass', 'Test binding file works as expected') 323 | 324 | callback(null, !stderr.length) 325 | }) 326 | }) 327 | } 328 | 329 | module.exports = exercise 330 | --------------------------------------------------------------------------------