ProjectBasedExtensionLoader.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. //
  2. // Copyright (c) 2014-2016 THUNDERBEAST GAMES LLC
  3. //
  4. // Permission is hereby granted, free of charge, to any person obtaining a copy
  5. // of this software and associated documentation files (the "Software"), to deal
  6. // in the Software without restriction, including without limitation the rights
  7. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. // copies of the Software, and to permit persons to whom the Software is
  9. // furnished to do so, subject to the following conditions:
  10. //
  11. // The above copyright notice and this permission notice shall be included in
  12. // all copies or substantial portions of the Software.
  13. //
  14. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  20. // THE SOFTWARE.
  21. //
  22. import * as EditorEvents from "../../editor/EditorEvents";
  23. // Duktape require isn't recognized as a function, but can be used as one
  24. declare function require(filename: string): any;
  25. /**
  26. * Resource extension that supports the web view typescript extension
  27. */
  28. export default class ProjectBasedExtensionLoader implements Editor.HostExtensions.ProjectService {
  29. name: string = "ProjectBasedExtensionLoader";
  30. description: string = "This service supports loading extensions that reside in the project under {ProjectRoot}/Editor and named '*.Service.js'.";
  31. private serviceRegistry: Editor.HostExtensions.HostServiceLocator = null;
  32. private modSearchRewritten = false;
  33. /**
  34. * Prefix to use to detect "special" require paths
  35. * @type {String}
  36. */
  37. private static duktapeRequirePrefix = "project:";
  38. /**
  39. * Inject this language service into the registry
  40. * @return {[type]} True if successful
  41. */
  42. initialize(serviceRegistry: Editor.HostExtensions.HostServiceLocator) {
  43. // Let's rewrite the mod search
  44. this.rewriteModSearch();
  45. // We care project events
  46. serviceRegistry.projectServices.register(this);
  47. this.serviceRegistry = serviceRegistry;
  48. }
  49. /**
  50. * Rewrite the duktape modSearch routine so that we can intercept any
  51. * require calls with a "project:" prefix. Duktape will fail if it receives
  52. * a require call with a fully qualified path starting with a "/" (at least on OSX and Linux),
  53. * so we will need to detect any of these project level requires and allow Atomic to go to the
  54. * file system and manually pull these in to provide to duktape
  55. */
  56. private rewriteModSearch() {
  57. Duktape.modSearch = (function(origModSearch) {
  58. return function(id: string, require, exports, module) {
  59. let system = ToolCore.getToolSystem();
  60. if (id.indexOf(ProjectBasedExtensionLoader.duktapeRequirePrefix) == 0) {
  61. let path = id.substr(ProjectBasedExtensionLoader.duktapeRequirePrefix.length) + ".js";
  62. // For safety, only allow bringing modules in from the project directory. This could be
  63. // extended to look for some global extension directory to pull extensions from such as
  64. // ~/.atomicExtensions/...
  65. if (system.project && path.indexOf(system.project.projectPath) == 0) {
  66. console.log(`Searching for project based include: ${path}`);
  67. // we have a project based require
  68. if (Atomic.fileSystem.fileExists(path)) {
  69. let include = new Atomic.File(path, Atomic.FILE_READ);
  70. try {
  71. return include.readText();
  72. } finally {
  73. include.close();
  74. }
  75. } else {
  76. throw new Error(`Cannot find project module: ${path}`);
  77. }
  78. } else {
  79. throw new Error(`Extension at ${path} does not reside in the project directory ${system.project.projectPath}`);
  80. }
  81. } else {
  82. return origModSearch(id, require, exports, module);
  83. }
  84. };
  85. })(Duktape.modSearch);
  86. }
  87. /**
  88. * Called when the project is being loaded to allow the typscript language service to reset and
  89. * possibly compile
  90. */
  91. projectLoaded(ev: Editor.EditorEvents.LoadProjectEvent) {
  92. // got a load, we need to reset the language service
  93. console.log(`${this.name}: received a project loaded event for project at ${ev.path}`);
  94. let system = ToolCore.getToolSystem();
  95. if (system.project) {
  96. let fileSystem = Atomic.getFileSystem();
  97. let editorScriptsPath = Atomic.addTrailingSlash(system.project.projectPath) + "Editor/";
  98. if (fileSystem.dirExists(editorScriptsPath)) {
  99. let filenames = fileSystem.scanDir(editorScriptsPath, "*.js", Atomic.SCAN_FILES, true);
  100. filenames.forEach((filename) => {
  101. // Filtered search in Atomic doesn't due true wildcarding, only handles extension filters
  102. // in the future this may be better handled with some kind of manifest file
  103. if (filename.toLowerCase().lastIndexOf(".service.js") >= 0) {
  104. var extensionPath = editorScriptsPath + filename;
  105. extensionPath = extensionPath.substring(0, extensionPath.length - 3);
  106. console.log(`Detected project extension at: ${extensionPath} `);
  107. // Note: duktape does not yet support unloading modules,
  108. // but will return the same object when passed a path the second time.
  109. let resourceServiceModule = require(ProjectBasedExtensionLoader.duktapeRequirePrefix + extensionPath);
  110. // Handle situation where the service is either exposed by a typescript default export
  111. // or as the module.export (depends on if it is being written in typescript, javascript, es6, etc.)
  112. let resourceService: Editor.HostExtensions.HostEditorService = null;
  113. if (resourceServiceModule.default) {
  114. resourceService = resourceServiceModule.default;
  115. } else {
  116. resourceService = resourceServiceModule;
  117. }
  118. this.serviceRegistry.loadService(resourceService);
  119. }
  120. });
  121. }
  122. }
  123. }
  124. }