Commit | Line | Data |
---|---|---|
c53f86d4 RW |
1 | // This file is part of Moodle - http://moodle.org/ |
2 | // | |
3 | // Moodle is free software: you can redistribute it and/or modify | |
4 | // it under the terms of the GNU General Public License as published by | |
5 | // the Free Software Foundation, either version 3 of the License, or | |
6 | // (at your option) any later version. | |
7 | // | |
8 | // Moodle is distributed in the hope that it will be useful, | |
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of | |
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
11 | // GNU General Public License for more details. | |
12 | // | |
13 | // You should have received a copy of the GNU General Public License | |
14 | // along with Moodle. If not, see <http://www.gnu.org/licenses/>. | |
15 | ||
16 | /** | |
17 | * This is a babel plugin to add the Moodle module names to the AMD modules | |
18 | * as part of the transpiling process. | |
19 | * | |
20 | * In addition it will also add a return statement for the default export if the | |
21 | * module is using default exports. This is a highly specific Moodle thing because | |
22 | * we're transpiling to AMD and none of the existing Babel 7 plugins work correctly. | |
23 | * | |
24 | * This will fix the issue where an ES6 module using "export default Foo" will be | |
25 | * transpiled into an AMD module that returns {default: Foo}; Instead it will now | |
26 | * just simply return Foo. | |
27 | * | |
28 | * Note: This means all other named exports in that module are ignored and won't be | |
29 | * exported. | |
30 | * | |
31 | * @copyright 2018 Ryan Wyllie <ryan@moodle.com> | |
32 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
33 | */ | |
34 | ||
35 | "use strict"; | |
36 | ||
37 | module.exports = ({ template, types }) => { | |
38 | const fs = require('fs'); | |
39 | const glob = require('glob'); | |
40 | const cwd = process.cwd(); | |
41 | ||
42 | // Static variable to hold the modules. | |
43 | let moodleSubsystems = null; | |
44 | let moodlePlugins = null; | |
45 | ||
46 | /** | |
47 | * Parse Moodle's JSON files containing the lists of components. | |
48 | * | |
49 | * The values are stored in the static variables because we | |
50 | * only need to load them once per transpiling run. | |
51 | */ | |
52 | function loadMoodleModules() { | |
53 | moodleSubsystems = {'lib': 'core'}; | |
54 | moodlePlugins = {}; | |
55 | let components = fs.readFileSync('lib/components.json'); | |
56 | components = JSON.parse(components); | |
57 | ||
58 | for (const [component, path] of Object.entries(components.subsystems)) { | |
59 | if (path) { | |
60 | // Prefix "core_" to the front of the subsystems. | |
61 | moodleSubsystems[path] = `core_${component}`; | |
62 | } | |
63 | } | |
64 | ||
65 | for (const [component, path] of Object.entries(components.plugintypes)) { | |
66 | if (path) { | |
67 | moodlePlugins[path] = component; | |
68 | } | |
69 | } | |
70 | ||
71 | for (const file of glob.sync('**/db/subplugins.json')) { | |
72 | var rawContents = fs.readFileSync(file); | |
73 | var subplugins = JSON.parse(rawContents); | |
74 | ||
75 | for (const [component, path] of Object.entries(subplugins)) { | |
76 | if (path) { | |
77 | moodlePlugins[path] = component; | |
78 | } | |
79 | } | |
80 | } | |
81 | } | |
82 | ||
83 | /** | |
84 | * Search the list of components that match the given file name | |
85 | * and return the Moodle component for that file, if found. | |
86 | * | |
87 | * Throw an exception if no matching component is found. | |
88 | * | |
89 | * @throws {Error} | |
90 | * @param {string} searchFileName The file name to look for. | |
91 | * @return {string} Moodle component | |
92 | */ | |
93 | function getModuleNameFromFileName(searchFileName) { | |
94 | searchFileName = fs.realpathSync(searchFileName); | |
95 | const relativeFileName = searchFileName.replace(`${cwd}/`, ''); | |
96 | const [componentPath, file] = relativeFileName.split('/amd/src/'); | |
97 | const fileName = file.replace('.js', ''); | |
98 | ||
99 | // Check subsystems first which require an exact match. | |
100 | if (moodleSubsystems.hasOwnProperty(componentPath)) { | |
101 | return `${moodleSubsystems[componentPath]}/${fileName}`; | |
102 | } | |
103 | ||
104 | // It's not a subsystem so it must be a plugin. Moodle defines root folders | |
105 | // where plugins can be installed so our path with be <plugin_root>/<plugin_name>. | |
106 | // Let's separate the two. | |
107 | let pathParts = componentPath.split('/'); | |
108 | const pluginName = pathParts.pop(); | |
109 | const pluginPath = pathParts.join('/'); | |
110 | ||
111 | // The plugin path mutch match exactly because some plugins are subplugins of | |
112 | // other plugins which means their paths would partially match. | |
113 | if (moodlePlugins.hasOwnProperty(pluginPath)) { | |
114 | return `${moodlePlugins[pluginPath]}_${pluginName}/${fileName}`; | |
115 | } | |
116 | ||
117 | // This matches the previous PHP behaviour that would throw an exception | |
118 | // if it couldn't parse an AMD file. | |
119 | throw new Error('Unable to find module name for ' + searchFileName); | |
120 | } | |
121 | ||
122 | // This is heavily inspired by the babel-plugin-add-module-exports plugin. | |
123 | // See: https://github.com/59naga/babel-plugin-add-module-exports | |
124 | // | |
125 | // This is used when we detect a module using "export default Foo;" to make | |
126 | // sure the transpiled code just returns Foo directly rather than an object | |
127 | // with the default property (i.e. {default: Foo}). | |
128 | // | |
129 | // Note: This means that we can't support modules that combine named exports | |
130 | // with a default export. | |
131 | function addModuleExportsDefaults(path, exportObjectName) { | |
132 | const rootPath = path.findParent(path => { | |
133 | return path.key === 'body' || !path.parentPath; | |
134 | }); | |
135 | ||
136 | // HACK: `path.node.body.push` instead of path.pushContainer(due doesn't work in Plugin.post). | |
137 | // This is hardcoded to work specifically with AMD. | |
138 | rootPath.node.body.push(template(`return ${exportObjectName}.default`)()) | |
139 | } | |
140 | ||
141 | return { | |
142 | pre() { | |
143 | this.seenDefine = false; | |
144 | this.addedReturnForDefaultExport = false; | |
145 | ||
146 | if (moodleSubsystems === null) { | |
147 | loadMoodleModules(); | |
148 | } | |
149 | }, | |
150 | visitor: { | |
151 | // Plugin ordering is only respected if we visit the "Program" node. | |
152 | // See: https://babeljs.io/docs/en/plugins.html#plugin-preset-ordering | |
153 | // | |
154 | // We require this to run after the other AMD module transformation so | |
155 | // let's visit the "Program" node. | |
156 | Program: { | |
157 | exit(path) { | |
158 | path.traverse({ | |
159 | CallExpression(path) { | |
160 | // If we find a "define" function call. | |
161 | if (!this.seenDefine && path.get('callee').isIdentifier({name: 'define'})) { | |
162 | // We only want to modify the first instance of define that we find. | |
163 | this.seenDefine = true; | |
164 | // Get the Moodle component for the file being processed. | |
165 | var moduleName = getModuleNameFromFileName(this.file.opts.filename); | |
166 | // Add the module name as the first argument to the define function. | |
167 | path.node.arguments.unshift(types.stringLiteral(moduleName)); | |
168 | // Add a space after the define function in the built file so that previous versions | |
169 | // of Moodle will not try to add the module name to the file when it's being served | |
170 | // by PHP. This forces the regex in PHP to not match for this file. | |
171 | path.node.callee.name = 'define '; | |
172 | } | |
173 | ||
174 | // Check for any Object.defineProperty('exports', 'default') calls. | |
175 | if (!this.addedReturnForDefaultExport && path.get('callee').matchesPattern('Object.defineProperty')) { | |
176 | const [identifier, prop] = path.get('arguments') | |
177 | const objectName = identifier.get('name').node | |
178 | const propertyName = prop.get('value').node | |
179 | ||
180 | if ((objectName === 'exports' || objectName === '_exports') && propertyName === 'default') { | |
181 | addModuleExportsDefaults(path, objectName); | |
182 | this.addedReturnForDefaultExport = true; | |
183 | } | |
184 | } | |
185 | }, | |
186 | AssignmentExpression(path) { | |
187 | // Check for an exports.default assignments. | |
188 | if ( | |
189 | !this.addedReturnForDefaultExport && | |
190 | ( | |
191 | path.get('left').matchesPattern('exports.default') || | |
192 | path.get('left').matchesPattern('_exports.default') | |
193 | ) | |
194 | ) { | |
195 | const objectName = path.get('left.object.name').node; | |
196 | addModuleExportsDefaults(path, objectName); | |
197 | this.addedReturnForDefaultExport = true; | |
198 | } | |
199 | } | |
200 | }, this); | |
201 | } | |
202 | } | |
203 | } | |
204 | }; | |
205 | }; |