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"; | |
f59ac416 | 36 | /* eslint-env node */ |
c53f86d4 | 37 | |
f59ac416 | 38 | module.exports = ({template, types}) => { |
c53f86d4 | 39 | const fs = require('fs'); |
ac1a91bf | 40 | const path = require('path'); |
c53f86d4 | 41 | const cwd = process.cwd(); |
a8109e75 | 42 | const ComponentList = require(path.resolve('GruntfileComponents.js')); |
c53f86d4 RW |
43 | |
44 | /** | |
45 | * Search the list of components that match the given file name | |
46 | * and return the Moodle component for that file, if found. | |
47 | * | |
48 | * Throw an exception if no matching component is found. | |
49 | * | |
50 | * @throws {Error} | |
51 | * @param {string} searchFileName The file name to look for. | |
52 | * @return {string} Moodle component | |
53 | */ | |
54 | function getModuleNameFromFileName(searchFileName) { | |
55 | searchFileName = fs.realpathSync(searchFileName); | |
ac1a91bf | 56 | const relativeFileName = searchFileName.replace(`${cwd}${path.sep}`, '').replace(/\\/g, '/'); |
c53f86d4 RW |
57 | const [componentPath, file] = relativeFileName.split('/amd/src/'); |
58 | const fileName = file.replace('.js', ''); | |
59 | ||
60 | // Check subsystems first which require an exact match. | |
a8109e75 AN |
61 | const componentName = ComponentList.getComponentFromPath(componentPath); |
62 | if (componentName) { | |
63 | return `${componentName}/${fileName}`; | |
c53f86d4 RW |
64 | } |
65 | ||
66 | // This matches the previous PHP behaviour that would throw an exception | |
67 | // if it couldn't parse an AMD file. | |
a8109e75 | 68 | throw new Error(`Unable to find module name for ${searchFileName} (${componentPath}::${file}}`); |
c53f86d4 RW |
69 | } |
70 | ||
f59ac416 AN |
71 | /** |
72 | * This is heavily inspired by the babel-plugin-add-module-exports plugin. | |
73 | * See: https://github.com/59naga/babel-plugin-add-module-exports | |
74 | * | |
75 | * This is used when we detect a module using "export default Foo;" to make | |
76 | * sure the transpiled code just returns Foo directly rather than an object | |
77 | * with the default property (i.e. {default: Foo}). | |
78 | * | |
79 | * Note: This means that we can't support modules that combine named exports | |
80 | * with a default export. | |
81 | * | |
82 | * @param {String} path | |
83 | * @param {String} exportObjectName | |
84 | */ | |
c53f86d4 RW |
85 | function addModuleExportsDefaults(path, exportObjectName) { |
86 | const rootPath = path.findParent(path => { | |
87 | return path.key === 'body' || !path.parentPath; | |
88 | }); | |
89 | ||
90 | // HACK: `path.node.body.push` instead of path.pushContainer(due doesn't work in Plugin.post). | |
91 | // This is hardcoded to work specifically with AMD. | |
f59ac416 | 92 | rootPath.node.body.push(template(`return ${exportObjectName}.default`)()); |
c53f86d4 RW |
93 | } |
94 | ||
95 | return { | |
96 | pre() { | |
97 | this.seenDefine = false; | |
98 | this.addedReturnForDefaultExport = false; | |
c53f86d4 RW |
99 | }, |
100 | visitor: { | |
101 | // Plugin ordering is only respected if we visit the "Program" node. | |
102 | // See: https://babeljs.io/docs/en/plugins.html#plugin-preset-ordering | |
103 | // | |
104 | // We require this to run after the other AMD module transformation so | |
105 | // let's visit the "Program" node. | |
106 | Program: { | |
107 | exit(path) { | |
108 | path.traverse({ | |
109 | CallExpression(path) { | |
110 | // If we find a "define" function call. | |
111 | if (!this.seenDefine && path.get('callee').isIdentifier({name: 'define'})) { | |
112 | // We only want to modify the first instance of define that we find. | |
113 | this.seenDefine = true; | |
114 | // Get the Moodle component for the file being processed. | |
115 | var moduleName = getModuleNameFromFileName(this.file.opts.filename); | |
116 | // Add the module name as the first argument to the define function. | |
117 | path.node.arguments.unshift(types.stringLiteral(moduleName)); | |
118 | // Add a space after the define function in the built file so that previous versions | |
119 | // of Moodle will not try to add the module name to the file when it's being served | |
120 | // by PHP. This forces the regex in PHP to not match for this file. | |
121 | path.node.callee.name = 'define '; | |
122 | } | |
123 | ||
124 | // Check for any Object.defineProperty('exports', 'default') calls. | |
125 | if (!this.addedReturnForDefaultExport && path.get('callee').matchesPattern('Object.defineProperty')) { | |
f59ac416 AN |
126 | const [identifier, prop] = path.get('arguments'); |
127 | const objectName = identifier.get('name').node; | |
128 | const propertyName = prop.get('value').node; | |
c53f86d4 RW |
129 | |
130 | if ((objectName === 'exports' || objectName === '_exports') && propertyName === 'default') { | |
131 | addModuleExportsDefaults(path, objectName); | |
132 | this.addedReturnForDefaultExport = true; | |
133 | } | |
134 | } | |
135 | }, | |
136 | AssignmentExpression(path) { | |
137 | // Check for an exports.default assignments. | |
138 | if ( | |
139 | !this.addedReturnForDefaultExport && | |
140 | ( | |
141 | path.get('left').matchesPattern('exports.default') || | |
142 | path.get('left').matchesPattern('_exports.default') | |
143 | ) | |
144 | ) { | |
145 | const objectName = path.get('left.object.name').node; | |
146 | addModuleExportsDefaults(path, objectName); | |
147 | this.addedReturnForDefaultExport = true; | |
148 | } | |
149 | } | |
150 | }, this); | |
151 | } | |
152 | } | |
153 | } | |
154 | }; | |
2c28ba88 | 155 | }; |