├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── circle.yml ├── docs ├── compilation.json └── extensibility.md ├── example ├── app │ ├── entry-a.js │ ├── entry-b.js │ └── shared │ │ ├── lib-a.js │ │ ├── lib-b.js │ │ ├── lib-c.js │ │ └── required-but-not-assigned.js ├── build.js ├── example-plugin.js ├── ilk-config.js ├── package.json └── preset.js ├── index.js ├── package.json ├── scripts ├── generate-docs.js └── validate-docs.sh ├── spec ├── .eslintrc ├── run.js ├── src │ ├── compile │ │ └── construct.spec.js │ ├── index.spec.js │ └── resolve.spec.js └── util.js └── src ├── cli ├── ilk-build.js ├── ilk-server.js ├── ilk-watch.js └── ilk.js ├── compile ├── bundles │ ├── dedupe-explicit.js │ ├── dedupe-implicit.js │ ├── generate-raw.js │ ├── generate.js │ ├── get-seeds.js │ ├── hash.js │ ├── init.js │ └── interpolate-filename.js ├── construct │ ├── index.js │ └── templates │ │ ├── .eslintrc │ │ ├── common-module.jst │ │ ├── iife.jst │ │ ├── module-set.jst │ │ ├── register-urls.jst │ │ └── runtime.jst ├── index.js └── modules │ ├── compile.js │ ├── generate-id.js │ ├── generate-maps.js │ ├── get-seeds.js │ ├── hash.js │ ├── load.js │ ├── parse.js │ ├── resolve.js │ ├── transform-amd.js │ ├── transform.js │ └── update-references.js ├── index.js ├── optimizations └── file-cache │ └── index.js ├── options ├── compile.js ├── index.js ├── server.js └── shared.js ├── profiler.js ├── resolve.js ├── server └── server.js └── util ├── ast.js ├── file.js ├── object.js └── template.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["nodejs-lts"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | example/ 2 | lib/ 3 | docs/ 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | parser: "babel-eslint" 3 | 4 | extends: 5 | - "formidable/configurations/es6-node" 6 | 7 | parserOptions: 8 | ecmaVersion: 6 9 | sourceType: "module" 10 | 11 | rules: 12 | no-invalid-this: "off" 13 | func-style: "off" 14 | arrow-parens: "off" 15 | consistent-return: "off" 16 | space-before-function-paren: 17 | - "error" 18 | - anonymous: "always" 19 | named: "always" 20 | generator-star-spacing: 21 | - "error" 22 | - before: false 23 | after: true 24 | no-magic-numbers: "off" 25 | max-params: 26 | - "warn" 27 | - 5 28 | no-return-assign: "off" 29 | quotes: 30 | - "error" 31 | - "double" 32 | - "avoid-escape" 33 | no-unused-expressions: "off" 34 | prefer-arrow-callback: "off" 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # -------------------- 2 | # OSX Files 3 | # -------------------- 4 | .DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | Icon 8 | ._* 9 | .Spotlight-V100 10 | .Trashes 11 | 12 | # -------------------- 13 | # Sublime Text Files 14 | # -------------------- 15 | *.sublime-project 16 | *.sublime-workspace 17 | 18 | # -------------------- 19 | # IntelliJ Files 20 | # -------------------- 21 | *.iml 22 | *.ipr 23 | *.iws 24 | .idea/ 25 | out/ 26 | 27 | # -------------------- 28 | # Eclipse Files 29 | # -------------------- 30 | .project 31 | .metadata 32 | *.bak 33 | .classpath 34 | .settings/ 35 | 36 | # -------------------- 37 | # App Files 38 | # -------------------- 39 | npm-debug.log 40 | node_modules/ 41 | dist/ 42 | lib/ 43 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | example/ 3 | scripts/ 4 | spec/ 5 | src/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dale Bustad 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # interlock 2 | 3 | npm version 4 | 5 | *** 6 | 7 | For information about Interlock and how it works, check out our [website](http://interlockjs.com). To learn how to extend Interlock through plugins, we recommend you [read the docs](http://interlockjs.com/docs/extensibility). 8 | 9 | ## License 10 | 11 | [MIT License](http://opensource.org/licenses/MIT) 12 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: v6.3.0 4 | 5 | test: 6 | override: 7 | - npm run check 8 | 9 | notify: 10 | webhooks: 11 | - url: https://webhooks.gitter.im/e/ec8fbe5ef76183d4a766 12 | -------------------------------------------------------------------------------- /docs/extensibility.md: -------------------------------------------------------------------------------- 1 | # Extensibility 2 | 3 | Bacon ipsum dolor amet jerky jowl meatloaf ribeye beef. Doner chicken bacon tongue picanha 4 | landjaeger pork chop brisket leberkas fatback ball tip meatball corned beef. Drumstick turkey 5 | salami fatback ham hock venison tenderloin pork chop short ribs t-bone beef ribs hamburger 6 | shankle. 7 | 8 | Chuck pastrami bresaola salami, pork flank porchetta ground round filet mignon tongue corned 9 | beef. Pork belly spare ribs kielbasa chicken ribeye turducken, jerky pig doner flank. 10 | 11 | Hamburger tail landjaeger ball tip, porchetta fatback drumstick kielbasa shankle frankfurter. 12 | 13 | Something about Pluggable.CONTINUE... 14 | 15 | ## buildOutput 16 | 17 | Reduces an array of compiled bundles into a compilation object. This compilation 18 | object will have three key/value pairs: 19 | 20 | - **cache:** populated from the compilation process 21 | - **bundles:** a mapping of destination paths to `raw` code 22 | - **opts:** the original options passed to the compilation 23 | 24 | 25 | | | Name | Type | Description | 26 | | --- | ---- | ---- | ----------- | 27 | | Parameter | **bundles** | Array | Compiled bundles, generated by generateBundles. | 28 | | Return value | | Promise | Compilation object. | 29 | 30 | 31 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/index.js#L88-L100). 32 | 33 | ## compile 34 | 35 | Loads, transforms, and bundles an application using the provided options. 36 | Modules are collected and transformed, bundles are formed from those modules, 37 | and those bundles are finally converted into a format that can be written 38 | to disk or served over HTTP. 39 | 40 | 41 | | | Name | Type | Description | 42 | | --- | ---- | ---- | ----------- | 43 | | Return value | | Promise | Resolves to an object with three properties: `bundles`, `opts`, and `cache`. | 44 | 45 | 46 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/index.js#L111-L119). 47 | 48 | ## compileModule 49 | 50 | Given an unprocess module that has been loaded from disk, return a promise 51 | that resolves to the same module in a processed/compiled state, and whose 52 | dependencies have also been processed/compiled. 53 | 54 | 55 | | | Name | Type | Description | 56 | | --- | ---- | ---- | ----------- | 57 | | Parameter | **module** | Object | Seed module. | 58 | | Return value | | Promise | Resolves to compiled module. | 59 | 60 | 61 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/modules/compile.js#L85-L107). 62 | 63 | ## compileModuleR 64 | 65 | Because the `compileModule` and `generateDependencies` functions interact 66 | recursively, defining a stand-in pluggable for `compileModule` allows for 67 | plugins to utilize `compileModule` from within an overridden `generateDependencies`. 68 | 69 | For true behavior, please see documentation for `compileModule`. 70 | 71 | 72 | | | Name | Type | Description | 73 | | --- | ---- | ---- | ----------- | 74 | | Return value | | Promise | Resolves to compiled module. | 75 | 76 | 77 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/modules/compile.js#L26-L28). 78 | 79 | ## compileModules 80 | 81 | Given one or more module seeds, traverse their dependency graph, collecting any and 82 | all dependency modules, and then parse, transform, and hash those modules. Return 83 | a promise that resolves to the full set of modules, once they have been correctly 84 | gathered and compiled. 85 | 86 | 87 | | | Name | Type | Description | 88 | | --- | ---- | ---- | ----------- | 89 | | Parameter | **moduleSeeds** | Array | Module seeds, i.e. modules that have not yet been populated with properties such as ast, `dependencies`, etc. Module objects _should_ have path, rawSource, and namespace values. | 90 | | Return value | | Promise | Resolves to array of all compiled modules. | 91 | 92 | 93 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/modules/compile.js#L122-L130). 94 | 95 | ## constructBundle 96 | 97 | Given a compiled bundle and moduleHash-to-URL lookup object, output 98 | the same bundle with generated AST. 99 | 100 | 101 | | | Name | Type | Description | 102 | | --- | ---- | ---- | ----------- | 103 | | Parameter | **bundle** | Object | Fully compiled bundle, ready to be outputed. | 104 | | Parameter | **urls** | Object | moduleHash-to-URL lookup dictionary. | 105 | | Return value | | Object | Bundle with new `ast` property. | 106 | 107 | 108 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/index.js#L40-L48). 109 | 110 | ## constructBundleAst 111 | 112 | Construct the AST for an output bundle. A number of optional options-args are 113 | allowed, to give flexibility to the compiler for what sort of bundle should be 114 | constructed. 115 | 116 | For example, in the case of a bundle with an entry module, you'll want everything 117 | to be included. The run-time is needed, because there is no guarantee another 118 | bundle has already loaded the run-time. The module-hash-to-bundle-URLs object 119 | should be included, as again there is no guarantee another bundle has already 120 | set those values. The modules of the bundle itself need to be included, etc. 121 | 122 | However, you might instead generate a specialized bundle that only contains the 123 | run-time and URLs. This bundle might be inlined into the page, or guaranteed 124 | to be loaded first, so that redundant copies of the run-time be included in 125 | every other bundle generated. 126 | 127 | The output for this function should be a root AST node, ready to be transformed 128 | back into JavaScript code. 129 | 130 | 131 | | | Name | Type | Description | 132 | | --- | ---- | ---- | ----------- | 133 | | Parameter | **opts** | Object | Options. | 134 | | Parameter | **opts.includeRuntime** | Boolean | Indicates whether Interlock run-time should be emitted. | 135 | | Parameter | **opts.urls** | Object | Optional. If included, map of module hashes to URLs will be emitted. | 136 | | Parameter | **opts.modules** | Array | Optional. If included, the module objects will be transformed into output module AST and emitted. | 137 | | Parameter | **opts.entryModuleId** | String | Optional. If included, a statement will be rendered to invoke the specified module on load. | 138 | | Return value | | ASTnode | Single program AST node. | 139 | 140 | 141 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/construct/index.js#L165-L170). 142 | 143 | ## constructBundleBody 144 | 145 | Builds body of output bundle, to be inserted into the IIFE. 146 | 147 | 148 | | | Name | Type | Description | 149 | | --- | ---- | ---- | ----------- | 150 | | Parameter | **opts** | Object | Same options object passed to constructBundleBody. | 151 | | Return value | | Array | Body of bundle. | 152 | 153 | 154 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/construct/index.js#L121-L133). 155 | 156 | ## constructCommonModule 157 | 158 | Given an array of AST nodes from a module's body along with that module's 159 | dependencies, construct an AST object expression that represents its run-time 160 | equivalent. 161 | 162 | 163 | | | Name | Type | Description | 164 | | --- | ---- | ---- | ----------- | 165 | | Parameter | **moduleBody** | Array | Array of AST nodes. | 166 | | Parameter | **deps** | Array | Array of modules upon which module is dependent. | 167 | | Return value | | ASTnode | Object expression AST node. | 168 | 169 | 170 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/construct/index.js#L39-L46). 171 | 172 | ## constructModuleSet 173 | 174 | Given an array of compiled modules, construct the AST for JavaScript that would 175 | register those modules for consumption by the Interlock run-time. 176 | 177 | 178 | | | Name | Type | Description | 179 | | --- | ---- | ---- | ----------- | 180 | | Parameter | **modules** | Array | Array of compiled modules. | 181 | | Parameter | **globalName** | String | Global variable name of the Interlock run-time. | 182 | | Parameter | **entryModuleId** | String | Module-hash of the entry module. | 183 | | Return value | | Array | Array of AST nodes to be emitted as JavaScript. | 184 | 185 | 186 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/construct/index.js#L66-L82). 187 | 188 | ## constructRegisterUrls 189 | 190 | Transforms a map of module-hashes-to-URLs to the AST equivalent. 191 | 192 | 193 | | | Name | Type | Description | 194 | | --- | ---- | ---- | ----------- | 195 | | Parameter | **urls** | Object | Keys are module hashes, values are URL strings. | 196 | | Parameter | **globalName** | String | Global variable name of Interlock run-time. | 197 | | Return value | | ASTnode | Single AST node. | 198 | 199 | 200 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/construct/index.js#L105-L112). 201 | 202 | ## constructRuntime 203 | 204 | Construct the guts of the Interlock run-time for inclusion in file output. 205 | 206 | 207 | | | Name | Type | Description | 208 | | --- | ---- | ---- | ----------- | 209 | | Parameter | **globalName** | String | Global variable name of Interlock run-time. | 210 | | Return value | | Array | Array of AST nodes. | 211 | 212 | 213 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/construct/index.js#L91-L95). 214 | 215 | ## dedupeExplicit 216 | 217 | First, update the bundle's `module` property to refer to the compiled 218 | version of the module. Then generate a moduleHashes property for each 219 | of the bundles, containing all hashes for all modules in the bundle's 220 | dependency branch. 221 | 222 | Then, identify bundles that include the entry module from another bundle. 223 | When found, remove all of the second module's bundles from the first. 224 | 225 | This will ensure that for any explicitly-defined bundle, other bundles 226 | will not include its module or module-dependencies. This avoids copies 227 | of a module from appearing in multiple bundles. 228 | 229 | 230 | | | Name | Type | Description | 231 | | --- | ---- | ---- | ----------- | 232 | | Parameter | **bundleSeeds** | Array | Early-stage bundle objects without module or moduleHashes properties. | 233 | | Parameter | **modulesByAbsPath** | Object | Map of absolute paths to compiled modules. | 234 | | Return value | | Array | Bundle objects with explicit intersections removed and new module and moduleHashes properties. | 235 | 236 | 237 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/bundles/dedupe-explicit.js#L53). 238 | The function that it wraps can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/bundles/dedupe-explicit.js#L27-L51). 239 | 240 | ## dedupeImplicit 241 | 242 | Given an array of explicitly defined bundles, generate a new array of bundles 243 | including new implicit bundles. These implicit bundles will be generated from 244 | the intersections of two (or more) bundles' module hashes. 245 | 246 | This ensures that no module is included in more than one bundle. It further 247 | ensures that any module that is depended upon by more than one bundle will be 248 | split off into its own new bundle. 249 | 250 | 251 | | | Name | Type | Description | 252 | | --- | ---- | ---- | ----------- | 253 | | Parameter | **explicitBundles** | Array | Bundles with module and moduleHashes properties. | 254 | | Return value | | Array | Explicit bundles plus new implicit bundles. | 255 | 256 | 257 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/bundles/dedupe-implicit.js#L50-L52). 258 | 259 | ## emitRawBundles 260 | 261 | Given an array of compiled bundles and a moduleHash-to-URL lookup dictionary, 262 | generate a new array of bundles with new `ast` and `raw` properties. 263 | 264 | Some compiled bundles (as internally represented) will result in more than 265 | one output file. The canonical example of this is a JS file and its source-map. 266 | Plugins may also implement mechanisms to output multiple files per bundle. 267 | 268 | This one-to-many relationship is defined by the generateRawBundles method, which 269 | may output an array of raw bundles. 270 | 271 | 272 | | | Name | Type | Description | 273 | | --- | ---- | ---- | ----------- | 274 | | Parameter | **bundlesArr** | Array | Compiled bundles. | 275 | | Parameter | **urls** | Object | moduleHash-to-URL lookup dictionary. | 276 | | Return value | | Array | Bundles with new `raw` properties. | 277 | 278 | 279 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/index.js#L66-L74). 280 | 281 | ## generateBundles 282 | 283 | Given a set of module seeds - originally generated from the bundle definitions 284 | passed into the Interlock constructor - and the set of fully generated modules, 285 | generate the full set of bundles that should be emitted, populate them with 286 | module objects, hash them, and interpolate any output filenames. 287 | 288 | Bundles outputted from this function should be ready to be transformed into 289 | strings using AST->source transformation, and then written to disk. 290 | 291 | 292 | | | Name | Type | Description | 293 | | --- | ---- | ---- | ----------- | 294 | | Parameter | **moduleSeeds** | Object | Early-stage module objects, indexed by their path relative to the compilation context. | 295 | | Parameter | **moduleMaps** | Object | Maps of fully compiled modules, indexed by both absolute path and hash. | 296 | | Return value | | Array | Fully compiled bundles. | 297 | 298 | 299 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/bundles/generate.js#L64-L71). 300 | 301 | ## generateDependencies 302 | 303 | Given a module whose dependency references (like require strings) have been 304 | determined, recursively compile all dependencies and return the module with 305 | new dependency properties. 306 | 307 | 308 | | | Name | Type | Description | 309 | | --- | ---- | ---- | ----------- | 310 | | Parameter | **module** | Object | Module for whom dependencies should be compiled. | 311 | | Return value | | Object | Module with new dependency properties. | 312 | 313 | 314 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/modules/compile.js#L39-L74). 315 | 316 | ## generateJsCode 317 | 318 | Given an AST and a set of options, generate the corresponding JavaScript 319 | source and optional sourcemap string. 320 | 321 | 322 | | | Name | Type | Description | 323 | | --- | ---- | ---- | ----------- | 324 | | Parameter | **opts** | Object | The generation options. | 325 | | Parameter | **opts.ast** | AST | The AST to render. | 326 | | Parameter | **opts.sourceMaps** | Boolean | Whether to render a source-map. | 327 | | Parameter | **opts.sourceMapTarget** | String | The output filename. | 328 | | Parameter | **opts.pretty** | Boolean | Whether to output formatted JS. | 329 | | Parameter | **opts.includeComments** | Boolean | Whether to include comments in the output. | 330 | | Parameter | **opts.sources** | Object | A hash of source filenames to source content. | 331 | | Return value | | Object | An object with `code` and `map` strings, where `map` can be null. | 332 | 333 | 334 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/bundles/generate-raw.js#L22-L44). 335 | 336 | ## generateModuleId 337 | 338 | Given a mostly-compiled module, generate an ID for that module 339 | and resolve the same module with an `id` property. 340 | 341 | 342 | | | Name | Type | Description | 343 | | --- | ---- | ---- | ----------- | 344 | | Parameter | **module** | Object | Module that needs an ID. | 345 | | Return value | | Object | Module that now has an `id` property. | 346 | 347 | 348 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/modules/generate-id.js#L13-L21). 349 | 350 | ## generateModuleMaps 351 | 352 | Given a set of fully compiled modules, generate and return two 353 | hashmaps of those modules, indexed by their hash and their 354 | absolute path. 355 | 356 | 357 | | | Name | Type | Description | 358 | | --- | ---- | ---- | ----------- | 359 | | Parameter | **modules** | Array | Fully compiles modules. | 360 | | Return value | | Object | Fully compiled modules, indexed by hash and absolute path. | 361 | 362 | 363 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/modules/generate-maps.js#L16-L25). 364 | 365 | ## generateRawBundles 366 | 367 | Given a compiled bundle object, return an array of one or more bundles with 368 | new `raw` property. This raw property should be generated from the bundle's 369 | AST or equivalent intermediate representation. 370 | 371 | This is a one-to-many transformation because it is quite possible for multiple 372 | output files to result from a single bundle object. The canonical example (and 373 | default behavior of this function, when sourcemaps are enabled) is for one 374 | bundle to result in a `.js` file and a `.map` file. 375 | 376 | 377 | | | Name | Type | Description | 378 | | --- | ---- | ---- | ----------- | 379 | | Parameter | **bundle** | Object | Bundle object without `raw` property. | 380 | | Return value | | Array | Array of bundle objects. At minimum, these bundle objects should have a `raw` property - a string representation of the file to be written to disk - and a `dest` property - the relative filepath of the file to be written to disk. | 381 | 382 | 383 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/bundles/generate-raw.js#L64-L92). 384 | 385 | ## getBundleSeeds 386 | 387 | Given the set of early-stage modules (originally generated from the bundle definitions) 388 | and the set of fully compiled modules (indexed by their absolute path), return an array 389 | of early-stage bundles. These bundles do not yet know about which modules they contain, 390 | but do hold a reference to the root module of their branch of the dependency graph. 391 | 392 | 393 | | | Name | Type | Description | 394 | | --- | ---- | ---- | ----------- | 395 | | Parameter | **moduleSeeds** | Object | Early-stage modules, indexed by path relative to the compilation context. | 396 | | Parameter | **modulesByPath** | Object | Fully compiled modules, indexed by absolute path. | 397 | | Return value | | Array | Early-stage bundles with `module` property. | 398 | 399 | 400 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/bundles/get-seeds.js#L20-L31). 401 | 402 | ## getModuleSeeds 403 | 404 | Inspect the compilation options for bundle definitions (provided as 405 | key/value pairs to options.entry and options.split), resolve references, 406 | and return an object of the early-stage modules indexed by their path 407 | relative to the compilation context. 408 | 409 | 410 | | | Name | Type | Description | 411 | | --- | ---- | ---- | ----------- | 412 | | Return value | | Object | Early-stage modules indexed by relative path. | 413 | 414 | 415 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/modules/get-seeds.js#L16-L23). 416 | 417 | ## getUrls 418 | 419 | Given an array of bundles, generate a lookup dictionary of module hashes 420 | to the destination path of the bundles that contains them. 421 | 422 | 423 | | | Name | Type | Description | 424 | | --- | ---- | ---- | ----------- | 425 | | Parameter | **bundles** | Array | Compiled bundles. | 426 | | Return value | | Object | moduleHash-to-URL lookup dictionary. | 427 | 428 | 429 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/index.js#L24-L29). 430 | 431 | ## hashBundle 432 | 433 | Given an otherwise prepared bundle, generate a hash for that bundle and resolve 434 | to that same bundle with a new `hash` property. 435 | 436 | 437 | | | Name | Type | Description | 438 | | --- | ---- | ---- | ----------- | 439 | | Parameter | **bundle** | Object | Unhashed bundle. | 440 | | Return value | | Object | Bundle plus new `hash` property, a 40-character SHA1 that uniquely identifies the bundle. | 441 | 442 | 443 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/bundles/hash.js#L47). 444 | The function that it wraps can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/bundles/hash.js#L29-L45). 445 | 446 | ## hashModule 447 | 448 | Given a mostly-compiled module, generate a hash for that module and resolve 449 | to that module with a new `hash` property. 450 | 451 | 452 | | | Name | Type | Description | 453 | | --- | ---- | ---- | ----------- | 454 | | Parameter | **module** | Object | Module that needs to be hashed hash. | 455 | | Return value | | Object | Module that now has a hash property. | 456 | 457 | 458 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/modules/hash.js#L36-L48). 459 | 460 | ## initBundle 461 | 462 | 463 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/bundles/init.js#L4-L30). 464 | 465 | ## interpolateFilename 466 | 467 | Given a bundle, determine its ultimate output filepath by replacing 468 | supported placeholders with their dynamic equivalents. 469 | 470 | 471 | | | Name | Type | Description | 472 | | --- | ---- | ---- | ----------- | 473 | | Parameter | **bundle** | Object | Late-stage bundle object. | 474 | | Return value | | Object | Bundle with interpolated `dest` property. | 475 | 476 | 477 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/bundles/interpolate-filename.js#L13-L23). 478 | 479 | ## loadModule 480 | 481 | Given a module seed, read the module from disk and determine its type. 482 | 483 | 484 | | | Name | Type | Description | 485 | | --- | ---- | ---- | ----------- | 486 | | Parameter | **module** | Object | Module seed. | 487 | | Return value | | Object | Module seed plus `rawSource` and `type` properties. | 488 | 489 | 490 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/modules/load.js#L55-L60). 491 | 492 | ## parseModule 493 | 494 | Parse the source of the provided early-stage module. Resolves to the same 495 | module object, with additional `ast` and `sourcePath` properties (or equivalent 496 | for non-JavaScript modules). 497 | 498 | 499 | | | Name | Type | Description | 500 | | --- | ---- | ---- | ----------- | 501 | | Parameter | **module** | Object | Unparsed module with rawSource property. | 502 | | Return value | | Object | Parsed module with new `ast` and `sourcePath` properties. | 503 | 504 | 505 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/modules/parse.js#L19-L52). 506 | 507 | ## partitionBundles 508 | 509 | Given a set of module seeds and the set of fully generated modules, generate 510 | a finalized array of bundles. These bundles will be early-stage and should 511 | not be populated with the actual modules. Instead, each bundle will be defined 512 | by the module hashes (unique IDs) of the modules that comprise the bundle. 513 | 514 | 515 | | | Name | Type | Description | 516 | | --- | ---- | ---- | ----------- | 517 | | Parameter | **moduleSeeds** | Object | Early-stage module objects, indexed by their path relative to the compilation context. | 518 | | Parameter | **moduleMaps** | Object | Maps of fully compiled modules, indexed by both absolute path and hash. | 519 | | Return value | | Array | Early-stage bundles. | 520 | 521 | 522 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/bundles/generate.js#L42-L46). 523 | 524 | ## populateBundleModules 525 | 526 | Define the canonical modules array for a bundle. This should occur after 527 | bundle module hashes are deduped. 528 | 529 | 530 | | | Name | Type | Description | 531 | | --- | ---- | ---- | ----------- | 532 | | Parameter | **bundle** | Object | The bundle object, with no modules property. | 533 | | Parameter | **moduleMaps** | Object | Has two properties - byAbsPath and byHash - where each of these map to the compiled module via the respective value. | 534 | | Return value | | Object | The bundle object, with modules property. | 535 | 536 | 537 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/bundles/generate.js#L23-L27). 538 | 539 | ## preresolve 540 | 541 | Transform the require string before it is resolved to a file on disk. 542 | No transformations occur by default - the output is the same as the input. 543 | 544 | 545 | | | Name | Type | Description | 546 | | --- | ---- | ---- | ----------- | 547 | | Parameter | **requireStr** | String | Require string or comparable value. | 548 | | Return value | | String | Transformed require string. | 549 | 550 | 551 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/modules/resolve.js#L13-L15). 552 | 553 | ## readSource 554 | 555 | This function is invoked whenever the compiler attempts to read a source-file 556 | from the disk. It takes an raw-module object as its only input. The properties 557 | available on that object are as follows: 558 | 559 | - `path` - the absolute path of the file 560 | - `ns` - the namespace of the module (either the default ns, or borrowed from its 561 | containing package) 562 | - `nsRoot` - the absolute path to the root of the namespace 563 | - `nsPath` - the file's path relative to the root of the namespace 564 | 565 | The function should output an object with the same properties, plus one additional 566 | property: `rawSource`. This property should be the string-value of the module 567 | source. 568 | 569 | 570 | | | Name | Type | Description | 571 | | --- | ---- | ---- | ----------- | 572 | | Parameter | **module** | Object | Module object. | 573 | | Return value | | Object | Module object + `rawSource`. | 574 | 575 | 576 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/modules/load.js#L30-L33). 577 | 578 | ## resolveModule 579 | 580 | Given a require string and some context, resolve that require string 581 | to a file on disk, returning a module seed. 582 | 583 | 584 | | | Name | Type | Description | 585 | | --- | ---- | ---- | ----------- | 586 | | Parameter | **requireStr** | String | Require string or comparable value. | 587 | | Parameter | **contextPath** | String | Absolute path from which to resolve any relative paths. | 588 | | Parameter | **ns** | String | Namespace to set on module seed if the resolved module is of the same namespace as its context. | 589 | | Parameter | **nsRoot** | String | Absolute path of default namespace. | 590 | | Parameter | **extensions** | Array | Array of file extension strings, including the leading dot. | 591 | | Return value | | Object | Module seed. | 592 | 593 | 594 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/modules/resolve.js#L53). 595 | The function that it wraps can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/modules/resolve.js#L32-L51). 596 | 597 | ## setModuleType 598 | 599 | Given the early-stage module (module seed + rawSource property), determine and set 600 | its type. This value defaults to "javascript" and is used to determine whether 601 | default behaviors for parsing and processing modules should be used on the module. 602 | 603 | 604 | | | Name | Type | Description | 605 | | --- | ---- | ---- | ----------- | 606 | | Parameter | **module** | Object | Early-stage module. | 607 | | Return value | | Object | Module with new `type` property. | 608 | 609 | 610 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/modules/load.js#L44-L46). 611 | 612 | ## transformModule 613 | 614 | Transforms the module's AST, returning a module object with transformed 615 | `ast` property as well as a new `synchronousRequires` property. If the 616 | module is not of type "javascript", transformations to type-specific 617 | intermediate representation should occur at this step. 618 | 619 | 620 | | | Name | Type | Description | 621 | | --- | ---- | ---- | ----------- | 622 | | Parameter | **module** | Object | Module object, with `ast` property. | 623 | | Return value | | Object | Module object with transformed `ast` property and new `synchronousRequires` property. | 624 | 625 | 626 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/modules/transform.js#L19-L58). 627 | 628 | ## updateBundleHash 629 | 630 | Calculate the bundle's hash by invoking `update` with data from the bundle. 631 | `update` should be called with string data only. 632 | 633 | 634 | | | Name | Type | Description | 635 | | --- | ---- | ---- | ----------- | 636 | | Parameter | **update** | Function | Updates the ongoing computation of bundle hash. | 637 | | Parameter | **bundle** | Object | The bundle object. | 638 | 639 | 640 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/bundles/hash.js#L14-L18). 641 | 642 | ## updateModuleHash 643 | 644 | Use data from the provided module to generate a hash, utilizing the provided 645 | update function. Only string values should be passed to the update function. 646 | The resulting hash should be deterministic for the same inputs in the same order. 647 | 648 | 649 | | | Name | Type | Description | 650 | | --- | ---- | ---- | ----------- | 651 | | Parameter | **module** | Object | Module that needs a hash property. | 652 | | Parameter | **update** | Function | Function to be invoked with data that uniquely identifies the module (or, more precisely, the run-time behavior of the module). | 653 | 654 | 655 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/modules/hash.js#L18-L26). 656 | 657 | ## updateReferences 658 | 659 | Given a module whose dependencies have been identified and compiled, 660 | replace all original references with run-time references. In the case 661 | of JavaScript, this will mean updating references like `path/to/dep` 662 | or `./sibling-dep` with each dependency's module ID. 663 | 664 | 665 | | | Name | Type | Description | 666 | | --- | ---- | ---- | ----------- | 667 | | Parameter | **module** | Object | Module with AST containing original require expressions. | 668 | | Return value | | Object | Module with AST containing require expressions whose arguments have been replaced with corresponding dependency module hashes. | 669 | 670 | 671 | This Pluggable's definition can be found [here](http://github.com/interlockjs/interlock/tree/master/src/compile/modules/update-references.js#L19-L33). 672 | 673 | -------------------------------------------------------------------------------- /example/app/entry-a.js: -------------------------------------------------------------------------------- 1 | var libA = require("./shared/lib-a"); 2 | var libB = require("./shared/lib-b"); 3 | require("lodash"); 4 | 5 | console.log("entry-a:libA", libA); 6 | console.log("entra-a:libB", libB); 7 | -------------------------------------------------------------------------------- /example/app/entry-b.js: -------------------------------------------------------------------------------- 1 | var libB = require("./shared/lib-b"); 2 | import libC from "./shared/lib-c"; 3 | 4 | console.log("entry-b:libB", libB); 5 | console.log("entry-b:libC", libC.thing); 6 | -------------------------------------------------------------------------------- /example/app/shared/lib-a.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "./lib-b", 3 | "./required-but-not-assigned" 4 | ], function (b) { 5 | return "A! " + b; 6 | }); 7 | 8 | /* 9 | Expected output: 10 | module.exports = (function () { 11 | var b = require("hash-for-lib-b"); 12 | require("hash-for-required-but-not-assigned"); 13 | return "A! " + b; 14 | })(); 15 | */ 16 | -------------------------------------------------------------------------------- /example/app/shared/lib-b.js: -------------------------------------------------------------------------------- 1 | function thing () { 2 | return "this should be transformed into a function expr!"; 3 | } 4 | thing(); 5 | 6 | module.exports = "B!"; 7 | -------------------------------------------------------------------------------- /example/app/shared/lib-c.js: -------------------------------------------------------------------------------- 1 | const jsxExpression =
some text
; 2 | export const thing = "C!"; 3 | -------------------------------------------------------------------------------- /example/app/shared/required-but-not-assigned.js: -------------------------------------------------------------------------------- 1 | module.exports = "this will never be used... :("; 2 | -------------------------------------------------------------------------------- /example/build.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | 3 | var Interlock = require(".."); 4 | 5 | var ilk = new Interlock(require("./ilk-config")); 6 | 7 | 8 | // ilk.watch(function (buildEvent) { 9 | // var patchModules = buildEvent.patchModules; 10 | // var compilation = buildEvent.compilation; 11 | 12 | // if (patchModules) { 13 | // const paths = patchModules.map(function (module) { return module.path; }); 14 | // console.log("the following modules have been updated:", paths); 15 | // } 16 | // if (compilation) { 17 | // console.log("a new compilation has completed"); 18 | // } 19 | // }, { save: true }); 20 | 21 | ilk.build(); 22 | -------------------------------------------------------------------------------- /example/example-plugin.js: -------------------------------------------------------------------------------- 1 | module.exports = function (babel) { 2 | var t = babel.types; 3 | return { 4 | visitor: { 5 | FunctionDeclaration: function (path) { 6 | var id = path.node.id; 7 | path.node.type = "FunctionExpression"; 8 | path.node.id = null; 9 | 10 | path.replaceWith(t.variableDeclaration("var", [ 11 | t.variableDeclarator(id, path.node) 12 | ])); 13 | } 14 | } 15 | }; 16 | } -------------------------------------------------------------------------------- /example/ilk-config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | 3 | module.exports = { 4 | srcRoot: __dirname, 5 | destRoot: path.join(__dirname, "dist"), 6 | 7 | entry: { 8 | "./app/entry-a.js": "entry-a.bundle.js", 9 | "./app/entry-b.js": { dest: "entry-b.bundle.js" } 10 | }, 11 | split: { 12 | "./app/shared/lib-a.js": "[setHash].js" 13 | }, 14 | 15 | includeComments: true, 16 | sourceMaps: true, 17 | 18 | plugins: [], 19 | 20 | presets: [require("./preset")], 21 | 22 | babelConfig: { 23 | plugins: [require.resolve("./example-plugin")] 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interlock-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "clean": "rm -fr ./dist/*.js ./dist/*.map", 7 | "build": "node build.js" 8 | }, 9 | "author": "Dale Bustad ", 10 | "license": "MIT", 11 | "dependencies": {} 12 | } 13 | -------------------------------------------------------------------------------- /example/preset.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | pretty: false, 3 | includeComments: false 4 | }; 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var useTranspiled = true; // eslint-disable-line no-var 2 | 3 | try { 4 | require.resolve("./lib"); 5 | } catch (e) { 6 | useTranspiled = false; 7 | } 8 | 9 | /* eslint-disable global-require */ 10 | if (useTranspiled) { 11 | require("babel-polyfill"); 12 | module.exports = require("./lib"); 13 | } else { 14 | require("babel-core/register"); 15 | module.exports = require("./src"); 16 | } 17 | /* eslint-enable global-require */ 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interlock", 3 | "version": "0.10.7", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint --ext .js --ext .jst .", 8 | "test": "mocha spec/run.js", 9 | "build": "babel -d lib/ src/ && npm run copy-templates && npm run prep-cli", 10 | "watch": "watch 'npm run build' src/ -d", 11 | "copy-templates": "cp -R src/compile/construct/templates lib/compile/construct/", 12 | "prep-cli": "chmod u+x lib/cli/ilk.js", 13 | "prepublish": "npm run build", 14 | "preversion": "npm run check && npm run build", 15 | "check": "npm run lint && npm run test && ./scripts/validate-docs.sh", 16 | "generate-docs": "babel-node ./scripts/generate-docs.js" 17 | }, 18 | "bin": { 19 | "ilk": "./lib/cli/ilk.js" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/interlockjs/interlock" 24 | }, 25 | "author": "Dale Bustad ", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/interlockjs/interlock/issues" 29 | }, 30 | "homepage": "https://github.com/interlockjs/interlock", 31 | "dependencies": { 32 | "babel-cli": "^6.14.0", 33 | "babel-core": "^6.14.0", 34 | "babel-eslint": "^6.1.2", 35 | "babel-generator": "^6.14.0", 36 | "babel-plugin-transform-es2015-modules-commonjs": "^6.14.0", 37 | "babel-polyfill": "^6.13.0", 38 | "babel-preset-nodejs-lts": "^2.0.1", 39 | "babel-traverse": "^6.15.0", 40 | "babel-types": "^6.15.0", 41 | "babylon": "^6.9.2", 42 | "bluebird": "^3.4.6", 43 | "chokidar": "^1.6.0", 44 | "eslint": "^3.4.0", 45 | "eslint-config-defaults": "^9.0.0", 46 | "eslint-plugin-filenames": "^1.1.0", 47 | "farmhash": "^1.2.1", 48 | "lodash": "^4.15.0", 49 | "mime": "*", 50 | "mkdirp": "*", 51 | "mocha": "^3.0.2", 52 | "pluggable": "^1.1.4", 53 | "sinon": "^1.17.5", 54 | "source-map": "*", 55 | "watch": "^0.19.2", 56 | "yargs": "^5.0.0" 57 | }, 58 | "devDependencies": { 59 | "babel-cli": "^6.4.5", 60 | "babel-eslint": "^6.1.2", 61 | "babel-preset-nodejs-lts": "^2.0.1", 62 | "chai": "^3.2.0", 63 | "eslint": "^3.5.0", 64 | "eslint-config-formidable": "^1.0.1", 65 | "eslint-plugin-filenames": "^1.1.0", 66 | "eslint-plugin-import": "^1.14.0", 67 | "mocha": "^3.0.2", 68 | "require-dir": "^0.3.0", 69 | "sinon": "^1.14.1", 70 | "sinon-chai": "^2.8.0", 71 | "watch": "^0.19.2" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /scripts/generate-docs.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { join } from "path"; 3 | import generateDocs from "pluggable/lib/generate-docs"; 4 | 5 | const PREAMBLE = `# Extensibility 6 | 7 | Bacon ipsum dolor amet jerky jowl meatloaf ribeye beef. Doner chicken bacon tongue picanha 8 | landjaeger pork chop brisket leberkas fatback ball tip meatball corned beef. Drumstick turkey 9 | salami fatback ham hock venison tenderloin pork chop short ribs t-bone beef ribs hamburger 10 | shankle. 11 | 12 | Chuck pastrami bresaola salami, pork flank porchetta ground round filet mignon tongue corned 13 | beef. Pork belly spare ribs kielbasa chicken ribeye turducken, jerky pig doner flank. 14 | 15 | Hamburger tail landjaeger ball tip, porchetta fatback drumstick kielbasa shankle frankfurter. 16 | 17 | Something about Pluggable.CONTINUE... 18 | 19 | `; 20 | 21 | generateDocs({ 22 | rootPath: join(__dirname, ".."), 23 | outputPath: "docs/extensibility.md", 24 | jsonOutputPath: "docs/compilation.json", 25 | sources: "src/**/*.js", 26 | preamble: PREAMBLE 27 | }); 28 | -------------------------------------------------------------------------------- /scripts/validate-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | npm run generate-docs || exit 1 3 | 4 | if [ -n "$(git status --short)" ]; then 5 | echo -e "\033[0;31m[error]\033[0m Changes found in auto-generated docs. Run 'npm run generate-docs', commit, and try again." 6 | exit 1 7 | else 8 | exit 0 9 | fi -------------------------------------------------------------------------------- /spec/.eslintrc: -------------------------------------------------------------------------------- 1 | extends: 2 | - "../.eslintrc" 3 | 4 | env: 5 | mocha: true 6 | 7 | globals: 8 | sinon: true 9 | assert: false 10 | expect: true 11 | 12 | rules: 13 | max-nested-callbacks: "off" 14 | -------------------------------------------------------------------------------- /spec/run.js: -------------------------------------------------------------------------------- 1 | Error.stackTraceLimit = Infinity; 2 | 3 | require("babel-core/register"); 4 | 5 | const requireDir = require("require-dir"); 6 | const chai = require("chai"); 7 | const sinonChai = require("sinon-chai"); 8 | global.sinon = require("sinon"); 9 | 10 | chai.config.includeStack = true; 11 | chai.use(sinonChai); 12 | 13 | global.expect = chai.expect; 14 | global.AssertionError = chai.AssertionError; 15 | global.Assertion = chai.Assertion; 16 | global.assert = chai.assert; 17 | 18 | requireDir("./src", { recurse: true }); 19 | -------------------------------------------------------------------------------- /spec/src/compile/construct.spec.js: -------------------------------------------------------------------------------- 1 | import { parse } from "babylon"; 2 | import { query } from "../../util"; 3 | import generate from "babel-generator"; 4 | import { constructCommonModule } from "../../../src/compile/construct"; 5 | 6 | function render (ast) { 7 | return generate(ast, { 8 | compact: false, 9 | comments: true, 10 | quotes: "double" 11 | }).code; 12 | } 13 | 14 | describe("src/compile/construct", () => { 15 | describe("common module", () => { 16 | function simpleModule () { 17 | const origModuleBody = parse("module.exports = 'hello';").program.body; 18 | 19 | const dependencies = [{ id: "ddb179" }, { id: "aa527f" }]; 20 | 21 | return Promise.all([ 22 | constructCommonModule(origModuleBody, dependencies), 23 | origModuleBody 24 | ]); 25 | } 26 | 27 | it("outputs an object literal with two properties", () => { 28 | return simpleModule() 29 | .then(([ast]) => { 30 | const objLiterals = query(ast, "ObjectExpression"); 31 | expect(objLiterals).to.have.length(1); 32 | expect(objLiterals[0].properties).to.have.length(2); 33 | }); 34 | }); 35 | 36 | it("includes dependency IDs", () => { 37 | return simpleModule() 38 | .then(([ast]) => { 39 | const depsArray = query(ast, "[key.name=deps]")[0]; 40 | expect(depsArray).to.have.deep.property("value.type", "ArrayExpression"); 41 | expect(depsArray.value.elements).to.have.length(2); 42 | expect(query(depsArray, "StringLiteral[value=ddb179]")).to.have.length(1); 43 | expect(query(depsArray, "StringLiteral[value=aa527f]")).to.have.length(1); 44 | }); 45 | }); 46 | 47 | it("includes the wrapped module body", () => { 48 | return simpleModule() 49 | .then(([ast, origBody]) => { 50 | const moduleFn = query(ast, "ObjectProperty[key.name=fn]")[0]; 51 | expect(moduleFn).to.have.deep.property("value.type", "FunctionExpression"); 52 | expect(moduleFn.value.params).to.have.length(3); 53 | 54 | const constructedModuleFnBody = query(moduleFn, "BlockStatement")[0].body; 55 | expect(constructedModuleFnBody).to.eql(origBody); 56 | }); 57 | }); 58 | 59 | it("outputs correct JS when rendered", () => { 60 | return simpleModule() 61 | .then(([ast]) => { 62 | expect(render(ast)).to.eql([ 63 | "{", 64 | " deps: [\"ddb179\", \"aa527f\"],", 65 | " fn: function (require, module, exports) {", 66 | " module.exports = 'hello';", 67 | " }", 68 | "}" 69 | ].join("\n")); 70 | }); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /spec/src/index.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-nested-callbacks,no-new */ 2 | 3 | import path from "path"; 4 | 5 | import _ from "lodash"; 6 | 7 | import Interlock from "../../src/index.js"; 8 | 9 | const minimalValidConfig = { 10 | entry: { "./index.js": "bundle.js" }, 11 | srcRoot: path.join(__dirname, "/../..") 12 | }; 13 | 14 | describe("src/index", () => { 15 | describe("constructor", function () { 16 | // TODO: Test for [] and undefined. _.merge ignores those values. 17 | it("throws an Error if not passed invalid options", function () { // eslint-disable-line max-statements,max-len 18 | // Missing or empty config 19 | expect(() => { new Interlock(); }).to.throw(Error); 20 | expect(() => { new Interlock({}); }).to.throw(Error); 21 | 22 | // Invalid options.entry 23 | expect(() => { new Interlock(_.merge({}, minimalValidConfig, { entry: true })); }) 24 | .to.throw(Error, "Received invalid value for option 'entry': true."); 25 | expect(() => { new Interlock(_.merge({}, minimalValidConfig, { entry: 1 })); }) 26 | .to.throw(Error, "Received invalid value for option 'entry': 1."); 27 | expect(() => { new Interlock(_.merge({}, minimalValidConfig, { entry: null })); }) 28 | .to.throw(Error, "Received invalid value for option 'entry': null."); 29 | expect(() => { 30 | const invalidConfig = _.merge({}, minimalValidConfig, 31 | { entry: null }, 32 | { entry: { "fakepath": {} } } 33 | ); 34 | new Interlock(invalidConfig); 35 | }).to.throw(Error, "Received invalid value for option 'entry': {\"fakepath\":{}}."); 36 | expect(() => { 37 | const invalidConfig = _.merge({}, minimalValidConfig, 38 | { entry: null }, 39 | { entry: { "fakepath": { dest: true } } } 40 | ); 41 | new Interlock(invalidConfig); 42 | }).to.throw( 43 | Error, 44 | "Received invalid value for option 'entry': {\"fakepath\":{\"dest\":true}}." 45 | ); 46 | 47 | // Invalid options.split 48 | expect(() => { new Interlock(_.merge({}, minimalValidConfig, { split: true })); }) 49 | .to.throw(Error, "Received invalid value for option 'split': true."); 50 | expect(() => { new Interlock(_.merge({}, minimalValidConfig, { split: 1 })); }) 51 | .to.throw(Error, "Received invalid value for option 'split': 1."); 52 | expect(() => { new Interlock(_.merge({}, minimalValidConfig, { split: null })); }) 53 | .to.throw(Error, "Received invalid value for option 'split': null."); 54 | expect(() => { 55 | const invalidConfig = _.merge({}, minimalValidConfig, { split: { "fakepath": {} } }); 56 | new Interlock(invalidConfig); 57 | }).to.throw(Error, "Received invalid value for option \'split\': {\"fakepath\":{}}."); 58 | expect(() => { 59 | const invalidConfig = _.merge({}, minimalValidConfig, 60 | { split: { "fakepath": { dest: true } } }); 61 | new Interlock(invalidConfig); 62 | }).to.throw( 63 | Error, 64 | "Received invalid value for option \'split\': {\"fakepath\":{\"dest\":true}}." 65 | ); 66 | 67 | // Conditional options.split || options.entry requirement 68 | expect(() => { 69 | new Interlock({ 70 | entry: { "./index.js": "bundle.js" }, 71 | srcRoot: path.join(__dirname, "/../..") 72 | }); 73 | }) 74 | .to.not.throw(Error); 75 | expect(() => { 76 | new Interlock({ 77 | split: { "./index.js": "bundle.js" }, 78 | srcRoot: path.join(__dirname, "/../..") 79 | }); 80 | }) 81 | .to.not.throw(Error); 82 | expect(() => { 83 | new Interlock({ 84 | split: { "./index.js": "bundle.js" }, 85 | entry: { "./index.js": "bundle.js" }, 86 | srcRoot: path.join(__dirname, "/../..") 87 | }); 88 | }) 89 | .to.not.throw(Error); 90 | expect(() => { 91 | new Interlock({ srcRoot: path.join(__dirname, "/../..") }); 92 | }).to.throw(Error); 93 | 94 | // Invalid options.srcRoot 95 | expect(() => { new Interlock(_.merge({}, minimalValidConfig, { srcRoot: true })); }) 96 | .to.throw(Error); 97 | expect(() => { new Interlock(_.merge({}, minimalValidConfig, { srcRoot: 1 })); }) 98 | .to.throw(Error); 99 | expect(() => { new Interlock(_.merge({}, minimalValidConfig, { srcRoot: null })); }) 100 | .to.throw(Error); 101 | }); 102 | 103 | it("fills in default values when not passed in", function () { 104 | const ilk = new Interlock(minimalValidConfig); 105 | 106 | // Can't do deep-equal comparison on function objects. 107 | delete ilk.options.log; 108 | 109 | expect(ilk.options).to.deep.equal({ 110 | entry: { "./index.js": { dest: "bundle.js" } }, 111 | split: {}, 112 | globalName: "__interlock__", 113 | srcRoot: path.join(__dirname, "/../.."), 114 | destRoot: path.join(__dirname, "../..", "dist"), 115 | extensions: [ ".js", ".jsx", ".es6" ], 116 | ns: "interlock", 117 | implicitBundleDest: "[setHash].js", 118 | includeComments: false, 119 | multiprocess: false, 120 | plugins: [], 121 | pretty: false, 122 | sourceMaps: false, 123 | fcache: false, 124 | presets: [], 125 | __defaults: { 126 | destRoot: true, 127 | extensions: true, 128 | fcache: true, 129 | implicitBundleDest: true, 130 | includeComments: true, 131 | log: true, 132 | multiprocess: true, 133 | ns: true, 134 | plugins: true, 135 | presets: true, 136 | pretty: true, 137 | sourceMaps: true 138 | } 139 | }); 140 | }); 141 | 142 | it("allows overrides to the default config", function () { 143 | const ilk = new Interlock({ 144 | entry: { "./index.js": "bundle.js" }, 145 | srcRoot: path.join(__dirname, "/../.."), 146 | context: "custom context", 147 | destRoot: "custom destRoot", 148 | extensions: [".custom"], 149 | ns: "custom-namespace", 150 | implicitBundleDest: "custom-dest" 151 | }); 152 | 153 | // Can't do deep-equal comparison on function objects. 154 | delete ilk.options.log; 155 | 156 | expect(ilk.options).to.deep.equal({ 157 | entry: { "./index.js": { "dest": "bundle.js" } }, 158 | split: {}, 159 | globalName: "__interlock__", 160 | srcRoot: path.join(__dirname, "/../.."), 161 | context: "custom context", 162 | destRoot: "custom destRoot", 163 | extensions: [".custom"], 164 | ns: "custom-namespace", 165 | implicitBundleDest: "custom-dest", 166 | includeComments: false, 167 | multiprocess: false, 168 | plugins: [], 169 | pretty: false, 170 | sourceMaps: false, 171 | fcache: false, 172 | presets: [], 173 | __defaults: { 174 | fcache: true, 175 | includeComments: true, 176 | log: true, 177 | multiprocess: true, 178 | plugins: true, 179 | presets: true, 180 | pretty: true, 181 | sourceMaps: true 182 | } 183 | }); 184 | }); 185 | }); 186 | 187 | describe("build", function () { 188 | it("return a Promise"); 189 | it("resolves to the compilation output"); 190 | }); 191 | describe("watch", function () { 192 | it("rebuilds on file change"); 193 | }); 194 | describe("_saveBundles", function () { 195 | it("saves output from compilation to destRoot"); 196 | it("prefixes bundles with namespace"); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /spec/src/resolve.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-nested-callbacks,no-new */ 2 | 3 | import path from "path"; 4 | 5 | import resolve from "../../src/resolve"; 6 | 7 | const baseDir = path.resolve(__dirname, "../.."); 8 | 9 | 10 | describe("src/resolve", function () { 11 | function attemptResolve (requireStr, extentions) { 12 | return resolve( 13 | requireStr, 14 | path.join(baseDir, "src"), 15 | "interlock", 16 | baseDir, 17 | extentions || [".js"] 18 | ); 19 | } 20 | 21 | it("resolves file in same directory", function () { 22 | const resolved = attemptResolve("./index"); 23 | expect(resolved).to.have.property("ns", "interlock"); 24 | expect(resolved).to.have.property("nsPath", "src/index.js"); 25 | expect(resolved.path).to.equal(path.join(baseDir, "src/index.js")); 26 | }); 27 | 28 | it("resolves file in sub-directory", function () { 29 | const resolved = attemptResolve("./compile/construct/index"); 30 | expect(resolved).to.have.property("ns", "interlock"); 31 | expect(resolved).to.have.property("nsPath", "src/compile/construct/index.js"); 32 | expect(resolved.path).to.equal(path.join(baseDir, "src/compile/construct/index.js")); 33 | }); 34 | 35 | it("resolves current directory", function () { 36 | const resolved = attemptResolve("./"); 37 | expect(resolved).to.have.property("ns", "interlock"); 38 | expect(resolved).to.have.property("nsPath", "src/index.js"); 39 | expect(resolved.path).to.equal(path.join(baseDir, "src/index.js")); 40 | }); 41 | 42 | it("resolves a file in a separate directory branch", function () { 43 | const resolved = attemptResolve("../example/build"); 44 | expect(resolved).to.have.property("ns", "interlock"); 45 | expect(resolved).to.have.property("nsPath", "example/build.js"); 46 | expect(resolved.path).to.equal(path.join(baseDir, "example/build.js")); 47 | }); 48 | 49 | it("resolves a node_modules package", function () { 50 | const resolved = attemptResolve("lodash"); 51 | expect(resolved).to.have.property("ns", "lodash"); 52 | expect(resolved).to.have.property("nsPath", "lodash.js"); 53 | expect(resolved.path) 54 | .to.equal(path.join(baseDir, "node_modules/lodash", "lodash.js")); 55 | }); 56 | 57 | it("resolves a file in node_modules package", function () { 58 | const resolved = attemptResolve("lodash/slice.js"); 59 | expect(resolved).to.have.property("ns", "lodash"); 60 | expect(resolved).to.have.property("nsPath", "slice.js"); 61 | expect(resolved.path) 62 | .to.equal(path.join(baseDir, "node_modules/lodash/slice.js")); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /spec/util.js: -------------------------------------------------------------------------------- 1 | import { get } from "lodash"; 2 | import traverse from "babel-traverse"; 3 | 4 | const QUERY_FORMAT = /(([A-Za-z]+)?)((\[[A-Za-z\.]+=[A-Za-z0-9+-=_\.]+\])*)/; 5 | const NUMBER_FORMAT = /[0-9\.]+/; 6 | const NOT_FOUND = Symbol(); 7 | 8 | export function query (ast, queryStr) { 9 | const queryMatch = QUERY_FORMAT.exec(queryStr); 10 | if (!queryMatch) { return null; } 11 | const [ , nodeType, , keyValsStr ] = queryMatch; 12 | const keyVals = !keyValsStr ? 13 | [] : 14 | keyValsStr 15 | .slice(1, keyValsStr.length - 1) 16 | .split("][") 17 | .map(keyValPair => keyValPair.split("=")) 18 | .map(([keypath, val]) => ({ 19 | keypath, 20 | val, 21 | altVal: NUMBER_FORMAT.test(val) ? Number(val) : undefined 22 | })); 23 | 24 | const matches = []; 25 | traverse.cheap(ast, node => { 26 | if (!nodeType || node.type === nodeType) { 27 | const keyValsMatch = keyVals.reduce((isMatch, { keypath, val, altVal }) => { 28 | const valAtPath = get(node, keypath, NOT_FOUND); 29 | return isMatch && valAtPath === val || valAtPath === altVal; 30 | }, true); 31 | if (keyValsMatch) { matches.push(node); } 32 | } 33 | }); 34 | return matches; 35 | } 36 | -------------------------------------------------------------------------------- /src/cli/ilk-build.js: -------------------------------------------------------------------------------- 1 | import Interlock from ".."; 2 | import * as options from "../options"; 3 | import { assign } from "lodash"; 4 | 5 | 6 | export const builder = yargs => { 7 | return options 8 | .buildArgs(yargs, options.compile) 9 | .epilogue("For more information, see http://www.interlockjs.com/docs/ilk-build."); 10 | }; 11 | 12 | export const handler = argv => { 13 | const config = argv.config ? options.loadConfig(argv.config) : {}; 14 | const logger = options.getLogger(argv.verbose); 15 | 16 | const compileOpts = options.getInterlockOpts( 17 | argv, 18 | options.compile, 19 | config 20 | ); 21 | const sharedOpts = options.getInterlockOpts( 22 | argv, 23 | options.shared, 24 | config 25 | ); 26 | 27 | const opts = assign({}, sharedOpts, compileOpts); 28 | 29 | let ilk; 30 | try { 31 | ilk = new Interlock(opts); 32 | ilk.build(); 33 | } catch (err) { 34 | logger.warn(err.stack) || logger.error("Error:", err.message); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/cli/ilk-server.js: -------------------------------------------------------------------------------- 1 | import Interlock from ".."; 2 | import * as options from "../options"; 3 | import { createServer } from "../server/server"; 4 | import { assign, keys } from "lodash"; 5 | 6 | 7 | export const builder = yargs => { 8 | return options 9 | .buildArgs(yargs, options.server, options.compile) 10 | .epilogue("For more information, see http://www.interlockjs.com/docs/ilk-build."); 11 | }; 12 | 13 | export const handler = argv => { 14 | const config = argv.config ? options.loadConfig(argv.config) : {}; 15 | const logger = options.getLogger(argv.verbose); 16 | 17 | const compileOpts = options.getInterlockOpts( 18 | argv, 19 | options.compile, 20 | config 21 | ); 22 | const sharedOpts = options.getInterlockOpts( 23 | argv, 24 | options.shared, 25 | config 26 | ); 27 | const opts = assign({}, sharedOpts, compileOpts); 28 | 29 | let serverOpts = options.getInterlockOpts(argv, options.server); 30 | serverOpts = options.validate(serverOpts, options.server); 31 | 32 | const { 33 | setDynamicAssets, 34 | notify, 35 | pause 36 | } = createServer(serverOpts); 37 | 38 | try { 39 | const ilk = new Interlock(opts); 40 | 41 | let resume = pause(); 42 | ilk.watch(buildEvent => { 43 | const { change, patchModules, compilation } = buildEvent; 44 | 45 | if (change) { 46 | resume = pause(); 47 | notify("recompiling", { filename: buildEvent.change }); 48 | return; 49 | } 50 | 51 | if (patchModules) { 52 | notify("update", { update: true }); 53 | } else if (compilation) { 54 | const newAssets = keys(compilation.bundles).reduce((assets, filename) => { 55 | assets[`/${filename}`] = compilation.bundles[filename].raw; 56 | return assets; 57 | }, {}); 58 | setDynamicAssets(newAssets); 59 | resume(); 60 | notify("compilation", { compilation: true }); 61 | } 62 | }); 63 | } catch (err) { 64 | logger.warn(err.stack) || logger.error("Error:", err.message); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /src/cli/ilk-watch.js: -------------------------------------------------------------------------------- 1 | // TODO 2 | -------------------------------------------------------------------------------- /src/cli/ilk.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import yargs from "yargs"; 4 | 5 | import * as options from "../options"; 6 | 7 | 8 | const y = yargs 9 | .strict() 10 | .usage(`Usage: $0 [options] 11 | 12 | This is where additional information will go.`); 13 | 14 | options.buildArgs(y, options.shared) 15 | .command( 16 | "build", 17 | "Transform source files to destination JS.", 18 | require("./ilk-build") 19 | ) 20 | .command( 21 | "server", 22 | "Run a development server with live-updated assets.", 23 | require("./ilk-server") 24 | ) 25 | 26 | .demand(1, "You must specify an Interlock command.") 27 | 28 | .epilogue("For more information, see http://www.interlockjs.com/docs/ilk.") 29 | .help() 30 | .parse(process.argv); 31 | -------------------------------------------------------------------------------- /src/compile/bundles/dedupe-explicit.js: -------------------------------------------------------------------------------- 1 | import { assign, includes, difference } from "lodash"; 2 | 3 | import { pluggable } from "pluggable"; 4 | 5 | 6 | /** 7 | * First, update the bundle's `module` property to refer to the compiled 8 | * version of the module. Then generate a moduleHashes property for each 9 | * of the bundles, containing all hashes for all modules in the bundle's 10 | * dependency branch. 11 | * 12 | * Then, identify bundles that include the entry module from another bundle. 13 | * When found, remove all of the second module's bundles from the first. 14 | * 15 | * This will ensure that for any explicitly-defined bundle, other bundles 16 | * will not include its module or module-dependencies. This avoids copies 17 | * of a module from appearing in multiple bundles. 18 | * 19 | * @param {Array} bundleSeeds Early-stage bundle objects without module 20 | * or moduleHashes properties. 21 | * @param {Object} modulesByAbsPath Map of absolute paths to compiled modules. 22 | * 23 | * @return {Array} Bundle objects with explicit intersections 24 | * removed and new module and moduleHashes 25 | * properties. 26 | */ 27 | function dedupeExplicit (bundleSeeds, modulesByAbsPath) { 28 | const bundles = bundleSeeds.map(bundle => { 29 | // Generate flat, naive dependency arrays. 30 | const module = modulesByAbsPath[bundle.module.path]; 31 | return assign({}, bundle, { 32 | moduleHashes: [module.hash, ...module.deepDependencies.map((dep) => dep.hash)], 33 | module 34 | }); 35 | }); 36 | 37 | // For each explicitly-defined bundle, remove that bundle's entry module 38 | // and other deep dependencies from other bundles' module arrays. 39 | return bundles.map(bundleA => { 40 | bundleA = assign({}, bundleA); 41 | 42 | bundles.forEach(bundleB => { 43 | if (bundleA.module.path !== bundleB.module.path && 44 | includes(bundleA.moduleHashes, bundleB.module.hash)) { 45 | bundleA.moduleHashes = difference(bundleA.moduleHashes, bundleB.moduleHashes); 46 | } 47 | }); 48 | 49 | return bundleA; 50 | }); 51 | } 52 | 53 | export default pluggable(dedupeExplicit); 54 | -------------------------------------------------------------------------------- /src/compile/bundles/dedupe-implicit.js: -------------------------------------------------------------------------------- 1 | import { assign, difference, intersection, filter } from "lodash"; 2 | import Promise from "bluebird"; 3 | 4 | import { pluggable } from "pluggable"; 5 | import initBundle from "./init"; 6 | 7 | 8 | const genBundlesWithImplicit = Promise.coroutine(function* (bundles) { 9 | bundles = bundles.slice(); 10 | 11 | for (let a = 0; a < bundles.length; a++) { 12 | const bundleA = bundles[a]; 13 | const bundleLengthAtIteration = bundles.length; 14 | 15 | for (let b = a + 1; b < bundleLengthAtIteration; b++) { 16 | const bundleB = bundles[b]; 17 | const commonHashes = intersection(bundleA.moduleHashes, bundleB.moduleHashes); 18 | 19 | if (commonHashes.length) { 20 | const moduleHashesA = difference(bundleA.moduleHashes, commonHashes); 21 | const moduleHashesB = difference(bundleB.moduleHashes, commonHashes); 22 | bundles[a] = assign({}, bundleA, { moduleHashes: moduleHashesA }); 23 | bundles[b] = assign({}, bundleB, { moduleHashes: moduleHashesB }); 24 | 25 | bundles.push(yield this.initBundle({ 26 | moduleHashes: commonHashes, 27 | type: bundles[a].type, 28 | excludeRuntime: true 29 | })); 30 | } 31 | } 32 | } 33 | 34 | return filter(bundles, bundle => bundle.moduleHashes.length); 35 | }); 36 | 37 | /** 38 | * Given an array of explicitly defined bundles, generate a new array of bundles 39 | * including new implicit bundles. These implicit bundles will be generated from 40 | * the intersections of two (or more) bundles' module hashes. 41 | * 42 | * This ensures that no module is included in more than one bundle. It further 43 | * ensures that any module that is depended upon by more than one bundle will be 44 | * split off into its own new bundle. 45 | * 46 | * @param {Array} explicitBundles Bundles with module and moduleHashes properties. 47 | * 48 | * @return {Array} Explicit bundles plus new implicit bundles. 49 | */ 50 | export default pluggable(function dedupeImplicit (explicitBundles) { 51 | return genBundlesWithImplicit.call(this, explicitBundles); 52 | }, { initBundle }); 53 | -------------------------------------------------------------------------------- /src/compile/bundles/generate-raw.js: -------------------------------------------------------------------------------- 1 | import generate from "babel-generator"; 2 | import { assign } from "lodash"; 3 | 4 | import { pluggable } from "pluggable"; 5 | 6 | 7 | /** 8 | * Given an AST and a set of options, generate the corresponding JavaScript 9 | * source and optional sourcemap string. 10 | * 11 | * @param {Object} opts The generation options. 12 | * @param {AST} opts.ast The AST to render. 13 | * @param {Boolean} opts.sourceMaps Whether to render a source-map. 14 | * @param {String} opts.sourceMapTarget The output filename. 15 | * @param {Boolean} opts.pretty Whether to output formatted JS. 16 | * @param {Boolean} opts.includeComments Whether to include comments in the output. 17 | * @param {Object} opts.sources A hash of source filenames to source content. 18 | * 19 | * @return {Object} An object with `code` and `map` strings, 20 | * where `map` can be null. 21 | */ 22 | const generateJsCode = pluggable(function generateJsCode (opts) { 23 | const { 24 | ast, 25 | sourceMaps, 26 | sourceMapTarget, 27 | pretty, 28 | includeComments, 29 | sources 30 | } = opts; 31 | 32 | const { code, map } = generate(ast, { 33 | sourceMaps, 34 | sourceMapTarget, 35 | comments: includeComments, 36 | compact: !pretty, 37 | quotes: "double" 38 | }, sources); 39 | 40 | return { 41 | code, 42 | map: sourceMaps ? JSON.stringify(map) : null 43 | }; 44 | }); 45 | 46 | /** 47 | * Given a compiled bundle object, return an array of one or more bundles with 48 | * new `raw` property. This raw property should be generated from the bundle's 49 | * AST or equivalent intermediate representation. 50 | * 51 | * This is a one-to-many transformation because it is quite possible for multiple 52 | * output files to result from a single bundle object. The canonical example (and 53 | * default behavior of this function, when sourcemaps are enabled) is for one 54 | * bundle to result in a `.js` file and a `.map` file. 55 | * 56 | * @param {Object} bundle Bundle object without `raw` property. 57 | * 58 | * @return {Array} Array of bundle objects. At minimum, these bundle 59 | * objects should have a `raw` property - a string 60 | * representation of the file to be written to disk - 61 | * and a `dest` property - the relative filepath 62 | * of the file to be written to disk. 63 | */ 64 | export default pluggable(function generateRawBundles (bundle) { 65 | if (bundle.type !== "javascript") { 66 | throw new Error("Cannot generate JS source for non-JavaScript bundle."); 67 | } 68 | 69 | const bundleSources = bundle.modules.reduce((hash, module) => { 70 | hash[module.sourcePath] = module.rawSource; 71 | return hash; 72 | }, {}); 73 | 74 | const ast = bundle.ast.type === "Program" || bundle.ast.type === "File" ? 75 | bundle.ast : 76 | { type: "Program", body: [].concat(bundle.ast) }; 77 | 78 | return this.generateJsCode({ 79 | ast, 80 | sourceMaps: !!this.opts.sourceMaps, 81 | sourceMapTarget: this.opts.sourceMaps && bundle.dest, 82 | pretty: !this.opts.pretty, 83 | includeComments: !!this.opts.includeComments, 84 | sources: bundleSources 85 | }).then(({ code, map }) => { 86 | const outputBundle = assign({}, bundle, { raw: code }); 87 | 88 | return this.opts.sourceMaps ? 89 | [ outputBundle, { raw: map, dest: `${bundle.dest}.map` } ] : 90 | [ outputBundle ]; 91 | }); 92 | }, { generateJsCode }); 93 | -------------------------------------------------------------------------------- /src/compile/bundles/generate.js: -------------------------------------------------------------------------------- 1 | import { assign } from "lodash"; 2 | import { pluggable } from "pluggable"; 3 | import Promise from "bluebird"; 4 | 5 | import getBundleSeeds from "./get-seeds"; 6 | import dedupeExplicit from "./dedupe-explicit"; 7 | import dedupeImplicit from "./dedupe-implicit"; 8 | import hashBundle from "./hash"; 9 | import interpolateFilename from "./interpolate-filename"; 10 | 11 | 12 | /** 13 | * Define the canonical modules array for a bundle. This should occur after 14 | * bundle module hashes are deduped. 15 | * 16 | * @param {Object} bundle The bundle object, with no modules property. 17 | * @param {Object} moduleMaps Has two properties - byAbsPath and byHash - 18 | * where each of these map to the compiled module 19 | * via the respective value. 20 | * 21 | * @return {Object} The bundle object, with modules property. 22 | */ 23 | const populateBundleModules = pluggable(function populateBundleModules (bundle, moduleMaps) { 24 | return assign({}, bundle, { 25 | modules: bundle.moduleHashes.map(hash => moduleMaps.byHash[hash]) 26 | }); 27 | }); 28 | 29 | /** 30 | * Given a set of module seeds and the set of fully generated modules, generate 31 | * a finalized array of bundles. These bundles will be early-stage and should 32 | * not be populated with the actual modules. Instead, each bundle will be defined 33 | * by the module hashes (unique IDs) of the modules that comprise the bundle. 34 | * 35 | * @param {Object} moduleSeeds Early-stage module objects, indexed by their 36 | * path relative to the compilation context. 37 | * @param {Object} moduleMaps Maps of fully compiled modules, indexed by both 38 | * absolute path and hash. 39 | * 40 | * @return {Array} Early-stage bundles. 41 | */ 42 | const partitionBundles = pluggable(function partitionBundles (moduleSeeds, moduleMaps) { 43 | return this.getBundleSeeds(moduleSeeds, moduleMaps.byAbsPath) 44 | .then(seedBundles => this.dedupeExplicit(seedBundles, moduleMaps.byAbsPath)) 45 | .then(this.dedupeImplicit); 46 | }, { getBundleSeeds, dedupeExplicit, dedupeImplicit }); 47 | 48 | /** 49 | * Given a set of module seeds - originally generated from the bundle definitions 50 | * passed into the Interlock constructor - and the set of fully generated modules, 51 | * generate the full set of bundles that should be emitted, populate them with 52 | * module objects, hash them, and interpolate any output filenames. 53 | * 54 | * Bundles outputted from this function should be ready to be transformed into 55 | * strings using AST->source transformation, and then written to disk. 56 | * 57 | * @param {Object} moduleSeeds Early-stage module objects, indexed by their 58 | * path relative to the compilation context. 59 | * @param {Object} moduleMaps Maps of fully compiled modules, indexed by both 60 | * absolute path and hash. 61 | * 62 | * @return {Array} Fully compiled bundles. 63 | */ 64 | export default pluggable(function generateBundles (moduleSeeds, moduleMaps) { 65 | return this.partitionBundles(moduleSeeds, moduleMaps) 66 | .then(bundles => Promise.all( 67 | bundles.map(bundle => this.populateBundleModules(bundle, moduleMaps))) 68 | ) 69 | .then(bundles => Promise.all(bundles.map(this.hashBundle))) 70 | .then(bundles => Promise.all(bundles.map(this.interpolateFilename))); 71 | }, { partitionBundles, hashBundle, interpolateFilename, populateBundleModules }); 72 | -------------------------------------------------------------------------------- /src/compile/bundles/get-seeds.js: -------------------------------------------------------------------------------- 1 | import { assign, map } from "lodash"; 2 | import Promise from "bluebird"; 3 | 4 | import { pluggable } from "pluggable"; 5 | import initBundle from "./init"; 6 | 7 | 8 | /** 9 | * Given the set of early-stage modules (originally generated from the bundle definitions) 10 | * and the set of fully compiled modules (indexed by their absolute path), return an array 11 | * of early-stage bundles. These bundles do not yet know about which modules they contain, 12 | * but do hold a reference to the root module of their branch of the dependency graph. 13 | * 14 | * @param {Object} moduleSeeds Early-stage modules, indexed by path relative to 15 | * the compilation context. 16 | * @param {Object} modulesByPath Fully compiled modules, indexed by absolute path. 17 | * 18 | * @return {Array} Early-stage bundles with `module` property. 19 | */ 20 | export default pluggable(function getBundleSeeds (moduleSeeds, modulesByPath) { 21 | return Promise.all([].concat( 22 | map(this.opts.entry, (bundleDef, relPath) => this.initBundle(assign({}, bundleDef, { 23 | module: modulesByPath[moduleSeeds[relPath].path], 24 | isEntryPt: true 25 | }))), 26 | map(this.opts.split, (bundleDef, relPath) => this.initBundle(assign({}, bundleDef, { 27 | module: modulesByPath[moduleSeeds[relPath].path], 28 | isEntryPt: false 29 | }))) 30 | )); 31 | }, { initBundle }); 32 | -------------------------------------------------------------------------------- /src/compile/bundles/hash.js: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | import { assign } from "lodash"; 3 | 4 | import { pluggable } from "pluggable"; 5 | 6 | 7 | /** 8 | * Calculate the bundle's hash by invoking `update` with data from the bundle. 9 | * `update` should be called with string data only. 10 | * 11 | * @param {Function} update Updates the ongoing computation of bundle hash. 12 | * @param {Object} bundle The bundle object. 13 | */ 14 | const updateBundleHash = pluggable(function updateBundleHash (update, bundle) { 15 | update(JSON.stringify(bundle.moduleHashes), "utf-8"); 16 | update(JSON.stringify(!!bundle.entry), "utf-8"); 17 | update(JSON.stringify(!!bundle.includeRuntime), "utf-8"); 18 | }); 19 | 20 | /** 21 | * Given an otherwise prepared bundle, generate a hash for that bundle and resolve 22 | * to that same bundle with a new `hash` property. 23 | * 24 | * @param {Object} bundle Unhashed bundle. 25 | * 26 | * @returns {Object} Bundle plus new `hash` property, a 40-character SHA1 27 | * that uniquely identifies the bundle. 28 | */ 29 | function hashBundle (bundle) { 30 | // Node v0.10.x cannot re-use crypto instances after digest is called. 31 | bundle.setHash = crypto.createHash("sha1") 32 | .update(JSON.stringify(bundle.moduleHashes), "utf-8") 33 | .digest("hex"); 34 | 35 | const shasum = crypto.createHash("sha1"); 36 | const update = shasum.update.bind(shasum); 37 | 38 | return this.updateBundleHash(update, bundle) 39 | .then(() => assign({}, bundle, { 40 | hash: shasum.digest("base64") 41 | .replace(/\//g, "_") 42 | .replace(/\+/g, "-") 43 | .replace(/=+$/, "") 44 | })); 45 | } 46 | 47 | export default pluggable(hashBundle, { updateBundleHash }); 48 | -------------------------------------------------------------------------------- /src/compile/bundles/init.js: -------------------------------------------------------------------------------- 1 | import { pluggable } from "pluggable"; 2 | 3 | 4 | export default pluggable(function initBundle (opts = {}) { 5 | const { 6 | dest = this.opts.implicitBundleDest, 7 | module, 8 | moduleHashes = [], 9 | modules = [], 10 | isEntryPt = false, 11 | type = "javascript", 12 | excludeRuntime = false 13 | } = opts; 14 | const includeRuntime = isEntryPt && !excludeRuntime; 15 | 16 | if (type !== "javascript") { 17 | throw new Error("Cannot create JS bundle for non-JavaScript module. " + 18 | "Please configure appropriate plugin."); 19 | } 20 | 21 | return { 22 | module, 23 | moduleHashes, 24 | modules, 25 | dest, 26 | type, 27 | isEntryPt, 28 | includeRuntime 29 | }; 30 | }); 31 | -------------------------------------------------------------------------------- /src/compile/bundles/interpolate-filename.js: -------------------------------------------------------------------------------- 1 | import { assign } from "lodash"; 2 | import { pluggable } from "pluggable"; 3 | 4 | 5 | /** 6 | * Given a bundle, determine its ultimate output filepath by replacing 7 | * supported placeholders with their dynamic equivalents. 8 | * 9 | * @param {Object} bundle Late-stage bundle object. 10 | * 11 | * @return {Object} Bundle with interpolated `dest` property. 12 | */ 13 | export default pluggable(function interpolateFilename (bundle) { 14 | let dest = bundle.dest 15 | .replace("[setHash]", bundle.setHash) 16 | .replace("[hash]", bundle.hash); 17 | if (bundle.module) { 18 | dest = dest.replace("[primaryModuleHash]", bundle.module.hash); 19 | dest = dest.replace("[primaryModuleId]", bundle.module.id); 20 | } 21 | 22 | return assign({}, bundle, { dest }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/compile/construct/index.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | 4 | import { assign } from "lodash"; 5 | import Promise from "bluebird"; 6 | import * as t from "babel-types"; 7 | import template from "../../util/template"; 8 | 9 | import { pluggable } from "pluggable"; 10 | import { fromObject } from "../../util/ast"; 11 | 12 | 13 | function getTemplate (templateName, transform) { 14 | transform = transform || (node => node); 15 | const absPath = path.join(__dirname, `templates/${templateName}.jst`); 16 | const templateStr = fs.readFileSync(absPath, "utf-8") 17 | // Remove ESlint rule exclusions from parsed templates. 18 | .replace(/\s*\/\/\s*eslint-disable-line.*/g, ""); 19 | const _template = template(templateStr); 20 | return opts => transform(_template(opts)); 21 | } 22 | 23 | const commonModuleTmpl = getTemplate("common-module", node => node.expression); 24 | const moduleSetTmpl = getTemplate("module-set"); 25 | const runtimeTmpl = getTemplate("runtime"); 26 | const registerUrlsTmpl = getTemplate("register-urls"); 27 | const iifeTmpl = getTemplate("iife"); 28 | 29 | /** 30 | * Given an array of AST nodes from a module's body along with that module's 31 | * dependencies, construct an AST object expression that represents its run-time 32 | * equivalent. 33 | * 34 | * @param {Array} moduleBody Array of AST nodes. 35 | * @param {Array} deps Array of modules upon which module is dependent. 36 | * 37 | * @return {ASTnode} Object expression AST node. 38 | */ 39 | export const constructCommonModule = pluggable( 40 | function constructCommonModule (moduleBody, deps) { 41 | return commonModuleTmpl({ 42 | MODULE_BODY: moduleBody, 43 | DEPS: t.arrayExpression(deps.map(dep => t.stringLiteral(dep.id))) 44 | }); 45 | } 46 | ); 47 | 48 | function markAsEntry (moduleAst) { 49 | return assign({}, moduleAst, { 50 | properties: moduleAst.properties.concat( 51 | t.objectProperty(t.identifier("entry"), t.booleanLiteral(true)) 52 | ) 53 | }); 54 | } 55 | 56 | /** 57 | * Given an array of compiled modules, construct the AST for JavaScript that would 58 | * register those modules for consumption by the Interlock run-time. 59 | * 60 | * @param {Array} modules Array of compiled modules. 61 | * @param {String} globalName Global variable name of the Interlock run-time. 62 | * @param {String} entryModuleId Module-hash of the entry module. 63 | * 64 | * @return {Array} Array of AST nodes to be emitted as JavaScript. 65 | */ 66 | export const constructModuleSet = pluggable( 67 | function constructModuleSet (modules, globalName, entryModuleId) { 68 | return Promise.all(modules.map(module => 69 | this.constructCommonModule(module.ast.body, module.dependencies) 70 | .then(moduleAst => module.id === entryModuleId ? 71 | markAsEntry(moduleAst) : 72 | moduleAst 73 | ) 74 | .then(moduleAst => t.objectProperty(t.stringLiteral(module.id), moduleAst)) 75 | )) 76 | .then(moduleProps => moduleSetTmpl({ 77 | GLOBAL_NAME: t.stringLiteral(globalName), 78 | MODULES_HASH: t.objectExpression(moduleProps) 79 | })); 80 | }, 81 | { constructCommonModule } 82 | ); 83 | 84 | /** 85 | * Construct the guts of the Interlock run-time for inclusion in file output. 86 | * 87 | * @param {String} globalName Global variable name of Interlock run-time. 88 | * 89 | * @return {Array} Array of AST nodes. 90 | */ 91 | export const constructRuntime = pluggable(function constructRuntime (globalName) { 92 | return runtimeTmpl({ 93 | GLOBAL_NAME: t.stringLiteral(globalName) 94 | }); 95 | }); 96 | 97 | /** 98 | * Transforms a map of module-hashes-to-URLs to the AST equivalent. 99 | * 100 | * @param {Object} urls Keys are module hashes, values are URL strings. 101 | * @param {String} globalName Global variable name of Interlock run-time. 102 | * 103 | * @return {ASTnode} Single AST node. 104 | */ 105 | export const constructRegisterUrls = pluggable( 106 | function constructRegisterUrls (urls, globalName) { 107 | return registerUrlsTmpl({ 108 | GLOBAL_NAME: t.stringLiteral(globalName), 109 | URLS: fromObject(urls) 110 | }); 111 | } 112 | ); 113 | 114 | /** 115 | * Builds body of output bundle, to be inserted into the IIFE. 116 | * 117 | * @param {Object} opts Same options object passed to constructBundleBody. 118 | * 119 | * @return {Array} Body of bundle. 120 | */ 121 | export const constructBundleBody = pluggable(function constructBundleBody (opts) { 122 | return Promise.all([ 123 | opts.includeRuntime && this.constructRuntime(this.opts.globalName), 124 | opts.urls && this.constructRegisterUrls(opts.urls, this.opts.globalName), 125 | opts.modules && this.constructModuleSet( 126 | opts.modules, 127 | this.opts.globalName, 128 | opts.entryModuleId 129 | ) 130 | ]) 131 | .then(([runtime, urls, moduleSet, loadEntry]) => 132 | [].concat(runtime, urls, moduleSet, loadEntry)); 133 | }, { constructModuleSet, constructRuntime, constructRegisterUrls }); 134 | 135 | /** 136 | * Construct the AST for an output bundle. A number of optional options-args are 137 | * allowed, to give flexibility to the compiler for what sort of bundle should be 138 | * constructed. 139 | * 140 | * For example, in the case of a bundle with an entry module, you'll want everything 141 | * to be included. The run-time is needed, because there is no guarantee another 142 | * bundle has already loaded the run-time. The module-hash-to-bundle-URLs object 143 | * should be included, as again there is no guarantee another bundle has already 144 | * set those values. The modules of the bundle itself need to be included, etc. 145 | * 146 | * However, you might instead generate a specialized bundle that only contains the 147 | * run-time and URLs. This bundle might be inlined into the page, or guaranteed 148 | * to be loaded first, so that redundant copies of the run-time be included in 149 | * every other bundle generated. 150 | * 151 | * The output for this function should be a root AST node, ready to be transformed 152 | * back into JavaScript code. 153 | * 154 | * @param {Object} opts Options. 155 | * @param {Boolean} opts.includeRuntime Indicates whether Interlock run-time should be emitted. 156 | * @param {Object} opts.urls Optional. If included, map of module hashes to URLs 157 | * will be emitted. 158 | * @param {Array} opts.modules Optional. If included, the module objects will be 159 | * transformed into output module AST and emitted. 160 | * @param {String} opts.entryModuleId Optional. If included, a statement will be rendered 161 | * to invoke the specified module on load. 162 | * 163 | * @return {ASTnode} Single program AST node. 164 | */ 165 | export const constructBundleAst = pluggable(function constructBundleAst (opts) { 166 | return this.constructBundleBody(opts) 167 | .then(body => iifeTmpl({ 168 | BODY: body.filter(x => x) 169 | })); 170 | }, { constructBundleBody }); 171 | -------------------------------------------------------------------------------- /src/compile/construct/templates/.eslintrc: -------------------------------------------------------------------------------- 1 | extends: 2 | - "formidable/configurations/es5-browser" 3 | 4 | rules: 5 | no-unused-expressions: "off" 6 | no-extra-parens: "off" 7 | no-unused-vars: "off" 8 | max-len: "off" 9 | valid-jsdoc: "off" 10 | no-return-assign: "off" 11 | func-style: "off" 12 | no-magic-numbers: "off" 13 | space-before-function-paren: 14 | - 2 15 | - anonymous: "always" 16 | named: "always" 17 | -------------------------------------------------------------------------------- /src/compile/construct/templates/common-module.jst: -------------------------------------------------------------------------------- 1 | ({ 2 | deps: DEPS, // eslint-disable-line no-undef 3 | fn: function (require, module, exports) { MODULE_BODY; } // eslint-disable-line no-undef 4 | }); 5 | -------------------------------------------------------------------------------- /src/compile/construct/templates/iife.jst: -------------------------------------------------------------------------------- 1 | (function () { BODY; })(); // eslint-disable-line no-undef 2 | -------------------------------------------------------------------------------- /src/compile/construct/templates/module-set.jst: -------------------------------------------------------------------------------- 1 | window[GLOBAL_NAME].load(MODULES_HASH); // eslint-disable-line no-undef 2 | -------------------------------------------------------------------------------- /src/compile/construct/templates/register-urls.jst: -------------------------------------------------------------------------------- 1 | window[GLOBAL_NAME].registerUrls(URLS); // eslint-disable-line no-undef 2 | -------------------------------------------------------------------------------- /src/compile/construct/templates/runtime.jst: -------------------------------------------------------------------------------- 1 | /** 2 | * Behavior identical to _.extend. Copies key/value pairs from source 3 | * objects to destination object. 4 | * 5 | * @param {Object} dest Destination for key/value pairs. 6 | * @param {Object} sources One or more sources for key/value pairs. 7 | * 8 | * @return {Object} Destination object. 9 | */ 10 | function copyProps (dest/*, sources... */) { 11 | var len = arguments.length; 12 | if (arguments.length < 2 || dest === null) { return dest; } 13 | Array.prototype.splice.call(arguments, 1).forEach(function (src) { 14 | Object.keys(src).forEach(function (key) { dest[key] = src[key]; }); 15 | }); 16 | return dest; 17 | } 18 | 19 | /** 20 | * This object is exposed to the browser environment and is accessible 21 | * to chunks that are loaded asynchronously. It represents the core 22 | * of the Interlock runtime. 23 | */ 24 | var r = window[GLOBAL_NAME] = window[GLOBAL_NAME] || { // eslint-disable-line no-undef 25 | modules: {}, 26 | urls: {}, 27 | providers: [ 28 | /** 29 | * Default module provider. Builds a