@datalackey/gas-demodulify-plugin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +605 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin/GASDemodulifyPlugin.d.ts +33 -0
- package/dist/plugin/GASDemodulifyPlugin.js +125 -0
- package/dist/plugin/GASDemodulifyPlugin.js.map +1 -0
- package/dist/plugin/Logger.d.ts +22 -0
- package/dist/plugin/Logger.js +64 -0
- package/dist/plugin/Logger.js.map +1 -0
- package/dist/plugin/code-emission/CodeEmitter.d.ts +14 -0
- package/dist/plugin/code-emission/CodeEmitter.js +253 -0
- package/dist/plugin/code-emission/CodeEmitter.js.map +1 -0
- package/dist/plugin/code-emission/shared-types.d.ts +46 -0
- package/dist/plugin/code-emission/shared-types.js +3 -0
- package/dist/plugin/code-emission/shared-types.js.map +1 -0
- package/dist/plugin/code-emission/wildcards-resolution-helpers.d.ts +7 -0
- package/dist/plugin/code-emission/wildcards-resolution-helpers.js +100 -0
- package/dist/plugin/code-emission/wildcards-resolution-helpers.js.map +1 -0
- package/dist/plugin/invariants.d.ts +13 -0
- package/dist/plugin/invariants.js +23 -0
- package/dist/plugin/invariants.js.map +1 -0
- package/dist/plugin/plugin-configuration-options/options.schema.d.ts +41 -0
- package/dist/plugin/plugin-configuration-options/options.schema.js +67 -0
- package/dist/plugin/plugin-configuration-options/options.schema.js.map +1 -0
- package/dist/plugin/plugin-configuration-options/validateAndNormalizePluginOptions.d.ts +5 -0
- package/dist/plugin/plugin-configuration-options/validateAndNormalizePluginOptions.js +12 -0
- package/dist/plugin/plugin-configuration-options/validateAndNormalizePluginOptions.js.map +1 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
# gas-demodulify
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
<!-- TOC:START -->
|
|
6
|
+
- [gas-demodulify](#gas-demodulify)
|
|
7
|
+
- [Table of Contents](#table-of-contents)
|
|
8
|
+
- [Plugin Overview](#plugin-overview)
|
|
9
|
+
- [Support for Modern Architectures Comprised of Subsystems](#support-for-modern-architectures-comprised-of-subsystems)
|
|
10
|
+
- [UI subsystem](#ui-subsystem)
|
|
11
|
+
- [Backend (GAS) subsystem](#backend-gas-subsystem)
|
|
12
|
+
- [Common subsystem](#common-subsystem)
|
|
13
|
+
- [Example](#example)
|
|
14
|
+
- [Backend subsystem (`gas/`)](#backend-subsystem-gas)
|
|
15
|
+
- [Common subsystem (`common/`)](#common-subsystem-common)
|
|
16
|
+
- [UI subsystem (`ui/`)](#ui-subsystem-ui)
|
|
17
|
+
- [What the Plugin Generates](#what-the-plugin-generates)
|
|
18
|
+
- [1. Backend bundle (`backend.gs`)](#1-backend-bundle-backendgs)
|
|
19
|
+
- [2. Common subsystem bundles](#2-common-subsystem-bundles)
|
|
20
|
+
- [COMMON for backend (`common.gs`)](#common-for-backend-commongs)
|
|
21
|
+
- [COMMON for UI (`common.html`)](#common-for-ui-commonhtml)
|
|
22
|
+
- [3. UI bundle (`ui.html`)](#3-ui-bundle-uihtml)
|
|
23
|
+
- [Finer Points Regarding How Code Must Be Bundled for GAS](#finer-points-regarding-how-code-must-be-bundled-for-gas)
|
|
24
|
+
- [Why should client-side browser code be processed with Webpack at all?](#why-should-client-side-browser-code-be-processed-with-webpack-at-all)
|
|
25
|
+
- [How Load Order Can Be Leveraged to Manage Inter-Subsystem Dependencies -- OBSOLETE](#how-load-order-can-be-leveraged-to-manage-inter-subsystem-dependencies----obsolete)
|
|
26
|
+
- [GAS Load Order Constraints](#gas-load-order-constraints)
|
|
27
|
+
- [Restrictions](#restrictions)
|
|
28
|
+
- [Configuration](#configuration)
|
|
29
|
+
- [General Options](#general-options)
|
|
30
|
+
- [module.exports.entry](#moduleexportsentry)
|
|
31
|
+
- [Plugin Constructor Options](#plugin-constructor-options)
|
|
32
|
+
- [*namespaceRoot*](#namespaceroot)
|
|
33
|
+
- [*subsystem*](#subsystem)
|
|
34
|
+
- [*buildMode*](#buildmode)
|
|
35
|
+
- [*defaultExportName*](#defaultexportname)
|
|
36
|
+
- [Example](#example-1)
|
|
37
|
+
- [Log level](#log-level)
|
|
38
|
+
- [Of Interest to Contributors](#of-interest-to-contributors)
|
|
39
|
+
<!-- TOC:END -->
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## Plugin Overview
|
|
44
|
+
|
|
45
|
+
A Webpack plugin that flattens modular TypeScript codebases into
|
|
46
|
+
[Google Apps Script](https://workspace.google.com/products/apps-script/) (GAS)-safe
|
|
47
|
+
JavaScript with clean **hierarchical
|
|
48
|
+
namespaces** corresponding to the top-level subsystems of a complex [GAS
|
|
49
|
+
add-on extension](https://developers.google.com/apps-script/guides/sheets).
|
|
50
|
+
This plugin was originally intended to serve as the core of an opinionated build
|
|
51
|
+
system for such extensions. Most existing Webpack-based tooling and GAS starter repos deal with
|
|
52
|
+
simple codebases and flat scripts, but fail when applied to more
|
|
53
|
+
complex architectures.
|
|
54
|
+
|
|
55
|
+
So, if your (Typescript) code base
|
|
56
|
+
- has multiple subsystems, and
|
|
57
|
+
- you want your emitted GAS code to isolate code for each subsystem into its own namespace, and
|
|
58
|
+
- you are horrified at the prospect of using brittle search and replace on strings to post-modify webpack output
|
|
59
|
+
|
|
60
|
+
then this plugin is for you.
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
When generating code `gas-demodulify` completely discards Webpack’s emitted runtime artifacts
|
|
64
|
+
— including the __webpack_require__
|
|
65
|
+
mechanism and its wrapping [IIFE](https://developer.mozilla.org/en-US/docs/Glossary/IIFE).
|
|
66
|
+
Instead, it generates fresh, GAS-safe JavaScript
|
|
67
|
+
compatible with both the GAS runtime and
|
|
68
|
+
the [HtmlService](https://developers.google.com/apps-script/reference/html/html-service)
|
|
69
|
+
delivery model using:
|
|
70
|
+
|
|
71
|
+
- user supplied namespace configuration metadata
|
|
72
|
+
- transpiled module sources provided by Webpack’s compilation pipeline
|
|
73
|
+
(after module dependency resolution and type-checking, but before runtime execution)
|
|
74
|
+
|
|
75
|
+
Caveat: Our plugin disallows certain patterns and configurations -- in both source code and Webpack config --
|
|
76
|
+
that produce invalid GAS code. See the [Restrictions](#restrictions) section for details.
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
## Support for Modern Architectures Comprised of Subsystems
|
|
81
|
+
|
|
82
|
+
A modern architecture for a complex GAS add-on typically comprises subsystems, with a common organization
|
|
83
|
+
breaking down into: **ui**, **backend (gas)**, and **common**. We will describe the operation of the plugin assuming
|
|
84
|
+
this tri-layer organization, but the plugin can be adapted to other architectures as well,
|
|
85
|
+
as discussed in the configuration section, below.
|
|
86
|
+
|
|
87
|
+
### UI subsystem
|
|
88
|
+
|
|
89
|
+
The UI subsystem typically consists of:
|
|
90
|
+
|
|
91
|
+
- HTMLService dialogs # (not typically bundled, but pushed 'raw' by clasp)
|
|
92
|
+
- Sidebar interfaces # (not typically bundled, but pushed 'raw' by clasp)
|
|
93
|
+
- svg images for icons # (not typically bundled, but pushed 'raw' by clasp)
|
|
94
|
+
- Client-side controller logic running in browser
|
|
95
|
+
- Multi-step orchestration flows unsuitable for pure GAS execution
|
|
96
|
+
|
|
97
|
+
### Backend (GAS) subsystem
|
|
98
|
+
|
|
99
|
+
This subsystem contains:
|
|
100
|
+
|
|
101
|
+
- Apps Script entrypoint functions invoked from the UI
|
|
102
|
+
- Spreadsheet/Drive API logic
|
|
103
|
+
- Custom menu handlers
|
|
104
|
+
- Trigger functions (`onOpen`, `onEdit`, ...)
|
|
105
|
+
- Business logic executed on Google’s servers
|
|
106
|
+
|
|
107
|
+
### Common subsystem
|
|
108
|
+
|
|
109
|
+
This subsystem hosts shared utility code that must exist in **both** UI and backend bundles:
|
|
110
|
+
|
|
111
|
+
- Logging support
|
|
112
|
+
- Data models
|
|
113
|
+
- Any reusable logic shared across UI and backend
|
|
114
|
+
|
|
115
|
+
This tri-layer architecture reflects the natural separation required by
|
|
116
|
+
Apps Script: `ui` code runs in a browser iframe, `backend` code runs in the
|
|
117
|
+
GAS runtime, and `common` code must be bundled twice. The double bundling of `common` code
|
|
118
|
+
is necessary because we need to make common code available in two different generated code artifacts. One is to
|
|
119
|
+
be included in the HTML served to
|
|
120
|
+
the client via
|
|
121
|
+
[HtmlService.createHtmlOutputFromFile](https://developers.google.com/apps-script/reference/html/html-service#createHtmlOutputFromFile(String)),
|
|
122
|
+
and this requires the code live in a file with extension '.html'. The other is to
|
|
123
|
+
be included in the server-side GAS code, and thus must have extension '.gs'.
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
A normal Webpack build cannot satisfy the above requirements, as well as some of the more subtle
|
|
127
|
+
requirements discussed in [this section](#finer-points-regarding-how-code-must-be-bundled-for-gas)
|
|
128
|
+
.
|
|
129
|
+
Furthermore, Google Apps Script cannot:
|
|
130
|
+
- run Webpack's module runtime, `__webpack_require__`,
|
|
131
|
+
- nor its wrapping IIFE,
|
|
132
|
+
- nor resolve its internal module map.
|
|
133
|
+
|
|
134
|
+
Modern TypeScript/ESM code must therefore be **demodulified** — stripped of all the webpack require stuff,
|
|
135
|
+
and flattened into plain top-level functions.
|
|
136
|
+
|
|
137
|
+
**gas-demodulify** performs exactly this transformation.
|
|
138
|
+
|
|
139
|
+
------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
### Example
|
|
142
|
+
|
|
143
|
+
Suppose you are developing a Google Sheets add-on named **MyAddon**.
|
|
144
|
+
|
|
145
|
+
Assume your subsystems import export the following:
|
|
146
|
+
|
|
147
|
+
### Backend subsystem (`gas/`)
|
|
148
|
+
|
|
149
|
+
import { Logger } from '../common/logger';
|
|
150
|
+
|
|
151
|
+
export function getData() {
|
|
152
|
+
Logger.log('getData called');
|
|
153
|
+
return "backend-data";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
### Common subsystem (`common/`)
|
|
157
|
+
|
|
158
|
+
export class Logger {
|
|
159
|
+
static log(msg: string) {
|
|
160
|
+
console.log(`LOG: ${msg}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
### UI subsystem (`ui/`)
|
|
165
|
+
|
|
166
|
+
import { Logger } from '../common/logger';
|
|
167
|
+
|
|
168
|
+
export function startUiFlow() {
|
|
169
|
+
Logger.log("UI flow started");
|
|
170
|
+
google.script.run
|
|
171
|
+
.withSuccessHandler(result => Logger.log(`Backend returned: ${result}`))
|
|
172
|
+
.getData(); // must be invoked via google.script.run
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
## What the Plugin Generates
|
|
178
|
+
|
|
179
|
+
### 1. Backend bundle (`backend.gs`)
|
|
180
|
+
|
|
181
|
+
// Namespace initialization
|
|
182
|
+
(function init(ns) {
|
|
183
|
+
let o = globalThis;
|
|
184
|
+
for (const p of ns.split(".")) o = o[p] = o[p] || {};
|
|
185
|
+
})("MYADDON.GAS");
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
Logger = MYADDON.COMMON.Logger.
|
|
189
|
+
|
|
190
|
+
// Flattened backend code
|
|
191
|
+
function getData() {
|
|
192
|
+
Logger.log("getData called");
|
|
193
|
+
return "backend-data";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Export surface
|
|
197
|
+
globalThis.MYADDON.GAS.getData = getData;
|
|
198
|
+
|
|
199
|
+
### 2. Common subsystem bundles
|
|
200
|
+
|
|
201
|
+
The COMMON bundle is emitted **twice**, once for backend and once for
|
|
202
|
+
UI.
|
|
203
|
+
|
|
204
|
+
#### COMMON for backend (`common.gs`)
|
|
205
|
+
|
|
206
|
+
(function init(ns) {
|
|
207
|
+
let o = globalThis;
|
|
208
|
+
for (const p of ns.split(".")) o = o[p] = o[p] || {};
|
|
209
|
+
})("MYADDON.GAS");
|
|
210
|
+
|
|
211
|
+
// Imported symbol bindings
|
|
212
|
+
const Logger = MYADDON.COMMON.Logger;
|
|
213
|
+
|
|
214
|
+
class Logger {
|
|
215
|
+
static log(msg) {
|
|
216
|
+
console.log(`LOG: ${msg}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
globalThis.MYADDON.GAS.Logger = Logger;
|
|
221
|
+
|
|
222
|
+
#### COMMON for UI (`common.html`)
|
|
223
|
+
|
|
224
|
+
<script>
|
|
225
|
+
// Namespace initialization
|
|
226
|
+
(function init(ns) {
|
|
227
|
+
let o = globalThis;
|
|
228
|
+
for (const p of ns.split(".")) o = o[p] = o[p] || {};
|
|
229
|
+
})("MYADDON.UI");
|
|
230
|
+
|
|
231
|
+
class Logger {
|
|
232
|
+
static log(msg) {
|
|
233
|
+
console.log(`LOG: ${msg}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
globalThis.MYADDON.UI.Logger = Logger;
|
|
238
|
+
</script>
|
|
239
|
+
|
|
240
|
+
### 3. UI bundle (`ui.html`)
|
|
241
|
+
|
|
242
|
+
<script>
|
|
243
|
+
// Namespace initialization
|
|
244
|
+
(function init(ns) {
|
|
245
|
+
let o = globalThis;
|
|
246
|
+
for (const p of ns.split(".")) o = o[p] = o[p] || {};
|
|
247
|
+
})("MYADDON.UI");
|
|
248
|
+
|
|
249
|
+
// Import bindings
|
|
250
|
+
const Logger = MYADDON.COMMON.Logger;
|
|
251
|
+
|
|
252
|
+
// UI function that uses COMMON and calls backend
|
|
253
|
+
function startUiFlow() {
|
|
254
|
+
MYADDON.COMMON.Logger.log("UI flow started");
|
|
255
|
+
google.script.run
|
|
256
|
+
.withSuccessHandler(result =>
|
|
257
|
+
MYADDON.COMMON.Logger.log(`Backend returned: ${result}`)
|
|
258
|
+
)
|
|
259
|
+
.getData();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Export to namespace
|
|
263
|
+
globalThis.MYADDON.UI.startUiFlow = startUiFlow;
|
|
264
|
+
</script>
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
## Finer Points Regarding How Code Must Be Bundled for GAS
|
|
271
|
+
|
|
272
|
+
### Why should client-side browser code be processed with Webpack at all?
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
Although the UI code of a GAS add-on ultimately executes inside your
|
|
276
|
+
browser (for example, within a dialog or sidebar iframe),
|
|
277
|
+
the browser never receives your JavaScript as a distinct chunk separate from the surrounding mark-up.
|
|
278
|
+
All UI code must be delivered through GAS's
|
|
279
|
+
[HtmlService](https://developers.google.com/apps-script/reference/html/html-service),
|
|
280
|
+
which expects you to load exactly one HTML file,
|
|
281
|
+
with all JavaScript imports resolved and all code inlined and delivered
|
|
282
|
+
together with the HTML markup as a single unit.
|
|
283
|
+
|
|
284
|
+
In a conventional web application, client-side JavaScript may be split
|
|
285
|
+
across many files and loaded dynamically by the browser using ES modules.
|
|
286
|
+
For example, HTML such as:
|
|
287
|
+
|
|
288
|
+
```html
|
|
289
|
+
<script type="module" src="./main.js"></script>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
instructs the browser to treat main.js as an ES module entrypoint. The
|
|
293
|
+
browser will then issue an HTTP request such as:
|
|
294
|
+
|
|
295
|
+
```http
|
|
296
|
+
GET /main.js
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
and, as additional import statements are encountered, will issue further
|
|
300
|
+
requests for each referenced module. These requests are resolved by a web
|
|
301
|
+
server that exposes URL-addressable JavaScript resources (for example,
|
|
302
|
+
static files served from the application’s document root).
|
|
303
|
+
|
|
304
|
+
Google Apps Script does not provide such a delivery model. HtmlService emits
|
|
305
|
+
a single, generated HTML document and does not expose a web server capable
|
|
306
|
+
of responding to follow-on requests for JavaScript modules. As a result,
|
|
307
|
+
there are no URL-addressable resources corresponding to ./main.js (or any
|
|
308
|
+
other imported module), and ES module loading via
|
|
309
|
+
|
|
310
|
+
```html
|
|
311
|
+
<script type="module">
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
is fundamentally unsupported in GAS. Therefore, even though the
|
|
315
|
+
browser environment itself is fully capable of executing ES, GAS cannot deliver ES modules.
|
|
316
|
+
For this reason, all UI code must be bundled
|
|
317
|
+
into a single, flat `<script>` block, with all imports resolved ahead of
|
|
318
|
+
time, no import or export syntax remaining, and no Webpack runtime
|
|
319
|
+
present. Webpack, in conjunction with our plugin, performs this 'flattening'
|
|
320
|
+
and demodulification automatically.
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
### How Load Order Can Be Leveraged to Manage Inter-Subsystem Dependencies -- OBSOLETE
|
|
326
|
+
|
|
327
|
+
Most complex GAS add-ons begin with a tri-layer structure:
|
|
328
|
+
|
|
329
|
+
- `ui` (browser code)
|
|
330
|
+
- `gas` (backend server code)
|
|
331
|
+
- `common` (shared utilities)
|
|
332
|
+
|
|
333
|
+
But some grow to include additional layers, such as:
|
|
334
|
+
|
|
335
|
+
- `charts`
|
|
336
|
+
- `api`
|
|
337
|
+
- `models`
|
|
338
|
+
- `validation`
|
|
339
|
+
- `sheets`
|
|
340
|
+
- `forms`
|
|
341
|
+
|
|
342
|
+
Each of these may depend on others, and the load order of generated
|
|
343
|
+
`.gs` files becomes important.
|
|
344
|
+
|
|
345
|
+
#### GAS Load Order Constraints
|
|
346
|
+
|
|
347
|
+
Google Apps Script evaluates `.gs` files in **lexicographical
|
|
348
|
+
(alphabetical)** order at runtime. This ordering is not configurable. It
|
|
349
|
+
imposes the following rule:
|
|
350
|
+
|
|
351
|
+
> Any subsystem that provides shared utilities must be emitted
|
|
352
|
+
> **before** subsystems that depend on it.
|
|
353
|
+
|
|
354
|
+
Example: the backend (`GAS`) subsystem normally depends on `COMMON`.
|
|
355
|
+
Therefore:
|
|
356
|
+
|
|
357
|
+
- `COMMON` must appear **first** in `.gs` load order.
|
|
358
|
+
- `GAS` must appear **after COMMON**.
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
In older build systems, developers often handled this requirement using ad-hoc
|
|
362
|
+
post-processing scripts or manual renaming, commonly prefixing shared bundles
|
|
363
|
+
with names like `AAA_common.gs` to force correct load order.
|
|
364
|
+
|
|
365
|
+
Our plugin, together with standard webpack configuration options, eliminates
|
|
366
|
+
the need for such fragile post-processing by ensuring
|
|
367
|
+
that all generated bundles are clean, GAS-compatible artifacts whose load order
|
|
368
|
+
is determined entirely by standard Webpack configuration. In particular,
|
|
369
|
+
lexicographical load ordering is enforced by choosing appropriate values for
|
|
370
|
+
Webpack’s `output.filename`, allowing shared dependencies to sort before the
|
|
371
|
+
subsystems that depend on them. You still have to think about your inter-subsystem dependencies
|
|
372
|
+
and choose names accordingly, but you can handle this entirely via webpack configuration, with no
|
|
373
|
+
tedious post-processing required.
|
|
374
|
+
|
|
375
|
+
At a minimum: ensure that your `common` bundle is named
|
|
376
|
+
so that it sorts before any other `.gs` bundles that depend on it, using `output.filename`.
|
|
377
|
+
For example the configuration below would produce an output file named `00_common.[contenthash].gs`,
|
|
378
|
+
which sorts before `01_gas.[contenthash].gs` and `02_charts.[contenthash].gs` etc.
|
|
379
|
+
|
|
380
|
+
```javascript
|
|
381
|
+
|
|
382
|
+
module.exports = {
|
|
383
|
+
|
|
384
|
+
....
|
|
385
|
+
|
|
386
|
+
entry: {
|
|
387
|
+
common: "./src/common/index.ts",
|
|
388
|
+
}
|
|
389
|
+
...
|
|
390
|
+
|
|
391
|
+
output: {
|
|
392
|
+
filename: "00_[name].[contenthash].gs"
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
------------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
## Restrictions
|
|
400
|
+
|
|
401
|
+
This plugin enforces a small set of source-level and build-time restrictions.
|
|
402
|
+
Please design your code to avoid the following patterns; violations will
|
|
403
|
+
either be rejected by the plugin at build time or code you want to keep will be stripped (which may
|
|
404
|
+
cause hard-to-diagnose bugs that change runtime behavior).
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
- Forbidden Webpack runtime artifacts
|
|
409
|
+
- Any substring matching the values in [FORBIDDEN_WEBPACK_RUNTIME_SUBSTRINGS](src/plugin/invariants.ts)
|
|
410
|
+
is not allowed in emitted output. Currently, this includes:
|
|
411
|
+
- `__webpack_` (any Webpack helper/runtime identifier)
|
|
412
|
+
- `.__esModule` (ES module interop artifact)
|
|
413
|
+
- Rationale: GAS cannot execute Webpack's module runtime (for example `__webpack_require__`) or interop boilerplate.
|
|
414
|
+
- Fix: Remove direct references to Webpack internals from your source.
|
|
415
|
+
|
|
416
|
+
- No wildcard re-exports
|
|
417
|
+
- Patterns rejected: `export * from './module'`, `export * as ns from './module'`, and bare `export *`.
|
|
418
|
+
- Rationale: wildcard re-exports create a non-deterministic export surface which cannot be
|
|
419
|
+
reliably flattened to a single GAS namespace.
|
|
420
|
+
- Fix: Replace wildcard re-exports with explicit, named re-exports, for example:
|
|
421
|
+
- Bad: `export * from './utils'`
|
|
422
|
+
- Good: `export { foo, bar } from './utils'`
|
|
423
|
+
|
|
424
|
+
- Avoid dynamic/conditional module loading patterns
|
|
425
|
+
- Patterns such as dynamic `import(...)`, `require()` with non-static arguments, or runtime code generation
|
|
426
|
+
that depends on bundler behavior are fragile and may not demodulify correctly.
|
|
427
|
+
For example: `const mod = require("./helpers/" + helperName);`
|
|
428
|
+
- Fix: Prefer static, unconditional imports/exports so Webpack can produce deterministic, statically-analyzable output.
|
|
429
|
+
Note there is no enforcement of this by the plugin, but such patterns may lead to runtime errors.
|
|
430
|
+
|
|
431
|
+
- Source files and source maps
|
|
432
|
+
- The plugin strips some runtime helpers and rewrites lines; try to preserve source maps
|
|
433
|
+
during your toolchain if you rely on debugging information. Avoid constructs that cause significant codegen wrapper insertion.
|
|
434
|
+
|
|
435
|
+
- Exactly one TypeScript entry module
|
|
436
|
+
- The plugin requires exactly one TypeScript-authored entry module per build. The entry module defines the
|
|
437
|
+
entire public API surface exposed to the Google Apps Script runtime. All GAS-visible functions must
|
|
438
|
+
be exported from this module, either directly or via explicit named re-exports. Other files may participate
|
|
439
|
+
freely in implementation via imports, but only the entry module’s exports are attached to the GAS namespace.
|
|
440
|
+
- Disallowed entry configurations
|
|
441
|
+
- The following are explicitly not supported, even though Webpack itself may allow them:
|
|
442
|
+
- Array-based entries, e.g.: `entry: { gas: ["./a.ts", "./b.ts"] }`
|
|
443
|
+
- Glob-based or auto-discovered entries, e.g.: ` entry: { gas: glob.sync("src/gas/*.ts") }`
|
|
444
|
+
- See [here](docs/plugin-design.md#why-exactly-one-webpack-entry-is-required) for more details.
|
|
445
|
+
|
|
446
|
+
- Output filename is intentionally ignored
|
|
447
|
+
- When gas-demodulify is enabled, we ignore, and actually delete the JavaScript bundle that
|
|
448
|
+
Webpack would otherwise emit. This is because that bundle contains runtime artifacts that GAS
|
|
449
|
+
cannot execute. To make this obvious and avoid accidental misuse, the plugin requires a sentinel value
|
|
450
|
+
be specified for `output.filename` in your Webpack config: `output: { filename: "OUTPUT-BUNDLE-FILENAME-DERIVED-FROM-ENTRY-NAME" ...`
|
|
451
|
+
Any other value — including omitting `output.filename` — is rejected.
|
|
452
|
+
- See [here](docs/plugin-design.md#how-gas-demodulify-separates-wheat-application-code-from-chaff-webpack-boilerplate)
|
|
453
|
+
for more details.
|
|
454
|
+
|
|
455
|
+
- No aliased re-exports in the entry module
|
|
456
|
+
- Patterns rejected: `export { foo as bar } from './module'`
|
|
457
|
+
- Rationale:
|
|
458
|
+
- Re-exporting with an alias does **not** create a runtime identifier named `bar`
|
|
459
|
+
- Webpack erases alias intent during module graph construction
|
|
460
|
+
- gas-demodulify operates after this erasure and cannot safely recover the original binding
|
|
461
|
+
- Fix: Replace aliased re-exports with an explicit wrapper export in the entry module:
|
|
462
|
+
- Bad:
|
|
463
|
+
```ts
|
|
464
|
+
export { onOpen as handleOpen } from "./triggers";
|
|
465
|
+
```
|
|
466
|
+
- Good:
|
|
467
|
+
```ts
|
|
468
|
+
import { onOpen } from "./triggers";
|
|
469
|
+
export function handleOpen() {
|
|
470
|
+
return onOpen();
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
## Configuration
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
### General Options
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
#### module.exports.entry
|
|
483
|
+
|
|
484
|
+
The emitted output filename is derived from the Webpack entrypoint name.
|
|
485
|
+
For example, an entry named gas will emit gas.gs. This was discussed in more detail in the previous section's
|
|
486
|
+
discusion of the restriction *Exactly one TypeScript entry module*.
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
### Plugin Constructor Options
|
|
491
|
+
|
|
492
|
+
The code snippet below illustrates how to pass options to the GASDemodulifyPlugin constructor via
|
|
493
|
+
a standard Javascript dictionary:
|
|
494
|
+
|
|
495
|
+
> new GASDemodulifyPlugin({
|
|
496
|
+
> namespaceRoot: "MYADDON",
|
|
497
|
+
> subsystem: "GAS",
|
|
498
|
+
> buildMode: "gas",
|
|
499
|
+
> logLevel: "info"
|
|
500
|
+
> });
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
You can call the plugin with an empty options object, and all options will take their default values.
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
#### *namespaceRoot*
|
|
507
|
+
|
|
508
|
+
The top-level global namespace under which all generated symbols will be attached (e.g. MYADDON, MyCompany.ProjectFoo).
|
|
509
|
+
- Default: DEFAULT
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
#### *subsystem*
|
|
513
|
+
In most projects, this is a single identifier such as `UI`, `GAS`, or `COMMON`, and for the example above
|
|
514
|
+
we get the namespace: `MYADDON.UI` Advanced users may specify a dotted path to create deeper hierarchy:
|
|
515
|
+
|
|
516
|
+
namespaceRoot: "MYADDON"
|
|
517
|
+
subsystem: "UI.Dialogs"
|
|
518
|
+
|
|
519
|
+
Which produces `MYADDON.UI.Dialogs`
|
|
520
|
+
- Default: DEFAULT
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
#### *buildMode*
|
|
524
|
+
|
|
525
|
+
Controls which artifacts are emitted:
|
|
526
|
+
|
|
527
|
+
- "gas" → emits .gs
|
|
528
|
+
- "ui" → emits .html with inline script tags
|
|
529
|
+
- "common" → emits both .gs and .html
|
|
530
|
+
|
|
531
|
+
- Default: gas
|
|
532
|
+
|
|
533
|
+
#### *defaultExportName*
|
|
534
|
+
|
|
535
|
+
Controls how default exports are attached to the GAS namespace.
|
|
536
|
+
If this option is provided, the default export is mapped to the specified symbol name.
|
|
537
|
+
|
|
538
|
+
- Default: - defaultExport
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
###### Example
|
|
542
|
+
|
|
543
|
+
Given the following source code:
|
|
544
|
+
|
|
545
|
+
> export default function foo() {}
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
If no defaultExportName is specified, the generated output will be:
|
|
549
|
+
|
|
550
|
+
> globalThis.MYADDON.UI.defaultExport = defaultExport;
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
If defaultExportName is specified:
|
|
554
|
+
|
|
555
|
+
> new GASDemodulifyPlugin({
|
|
556
|
+
> namespaceRoot: "MYADDON",
|
|
557
|
+
> subsystem: "UI",
|
|
558
|
+
> buildMode: "ui",
|
|
559
|
+
> defaultExportName: "main",
|
|
560
|
+
> logLevel: "info"
|
|
561
|
+
> });
|
|
562
|
+
|
|
563
|
+
In this case, the default export is attached to the GAS namespace using the explicitly provided name main.
|
|
564
|
+
|
|
565
|
+
> globalThis.MYADDON.UI.main = main;
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
### Log level
|
|
570
|
+
|
|
571
|
+
Control the verbosity of the plugin's diagnostic output. Accepted values are:
|
|
572
|
+
|
|
573
|
+
- "silent" — no *info* or *debug* logging, only *warn* and *error*
|
|
574
|
+
- "info" — high-level lifecycle messages (default)
|
|
575
|
+
- "debug" — verbose internal diagnostics
|
|
576
|
+
|
|
577
|
+
Precedence and behavior:
|
|
578
|
+
|
|
579
|
+
- If the environment variable `LOGLEVEL` is present and set to a valid value, it overrides the explicit `logLevel`
|
|
580
|
+
option passed to the plugin. For example:
|
|
581
|
+
- `LOGLEVEL=debug npm run build` will enable debug output regardless of the
|
|
582
|
+
plugin config's `logLevel` option.
|
|
583
|
+
- `LOGLEVEL=silent npm run build` will surpress all output except for warnings and errors.
|
|
584
|
+
(useful for figuring out which tests in a suite failed without reams of log noise).
|
|
585
|
+
- If `LOGLEVEL` is not set, the plugin uses the explicit `logLevel` option when provided.
|
|
586
|
+
- If neither `LOGLEVEL` nor an explicit `logLevel` is provided, the default level is `info`.
|
|
587
|
+
- Invalid log level values (from the environment or the explicit option) are treated as configuration errors and
|
|
588
|
+
will cause the build to fail.
|
|
589
|
+
|
|
590
|
+
Tests may set `LOGLEVEL` in the environment or inject `logLevel` into fixture plugin instances.
|
|
591
|
+
The environment variable takes precedence.
|
|
592
|
+
|
|
593
|
+
- Default: info
|
|
594
|
+
|
|
595
|
+
## Of Interest to Contributors
|
|
596
|
+
|
|
597
|
+
If you’re interested in the internal architecture of this plugin or in contributing to its development, see:
|
|
598
|
+
|
|
599
|
+
- [Guidance for Plugin Maintainers](docs/guidance-for-plugin-maintainers.md)
|
|
600
|
+
- [Plugin Design](docs/plugin-design.md)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
The design discussion also includes a discussion of how webpack typically fits into build pipelines which target
|
|
604
|
+
GAS as an execution environment.
|
|
605
|
+
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package entrypoint and CommonJS compatibility boundary.
|
|
3
|
+
*
|
|
4
|
+
* This file defines the *public API surface* of the package.
|
|
5
|
+
* It intentionally exports exactly one value for Webpack users: the gas-demodulify-plugin plugin constructor.
|
|
6
|
+
*
|
|
7
|
+
* Why this exists:
|
|
8
|
+
* - The package is effectively published as CommonJS due to a combination of factors:
|
|
9
|
+
*
|
|
10
|
+
* 1) TypeScript compilation target:
|
|
11
|
+
* - `tsconfig.json` specifies `"module": "commonjs"`,
|
|
12
|
+
* which determines the emitted JavaScript semantics
|
|
13
|
+
* (`module.exports`, `require()`).
|
|
14
|
+
*
|
|
15
|
+
* 2) Package entrypoint declaration:
|
|
16
|
+
* - `package.json` exposes `dist/index.js` via the `main` field,
|
|
17
|
+
* defining how Node and bundlers load the package.
|
|
18
|
+
*
|
|
19
|
+
* 3) Node module interpretation rules:
|
|
20
|
+
* - The absence of `"type": "module"` in `package.json` causes Node
|
|
21
|
+
* to interpret `.js` files as CommonJS by default.
|
|
22
|
+
*
|
|
23
|
+
* Together, these establish a CommonJS contract for consumers.
|
|
24
|
+
*
|
|
25
|
+
* Consumer expectations:
|
|
26
|
+
* - Webpack plugins are typically consumed via `require("package-name")` -- not ESM `import` syntax.
|
|
27
|
+
* - Consumers expect that call to return the constructor itself,
|
|
28
|
+
* not a `{ default: ... }` wrapper or a namespace object.
|
|
29
|
+
*
|
|
30
|
+
* Implementation details:
|
|
31
|
+
* - Uses TypeScript `export =` syntax so the compiled output is:
|
|
32
|
+
* `module.exports = PluginConstructor`
|
|
33
|
+
* - Users of plugin use `require()` import style intentionally to preserve CommonJS semantics.
|
|
34
|
+
*
|
|
35
|
+
* Note on linting:
|
|
36
|
+
* - `require()` usage in this file is deliberate and confined to this
|
|
37
|
+
* package boundary.
|
|
38
|
+
* - ESLint rules discouraging `require()` are intended for application code,
|
|
39
|
+
* not for package entrypoint shims.
|
|
40
|
+
*/
|
|
41
|
+
import GASDemodulifyPlugin = require("./plugin/GASDemodulifyPlugin");
|
|
42
|
+
export = GASDemodulifyPlugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Package entrypoint and CommonJS compatibility boundary.
|
|
4
|
+
*
|
|
5
|
+
* This file defines the *public API surface* of the package.
|
|
6
|
+
* It intentionally exports exactly one value for Webpack users: the gas-demodulify-plugin plugin constructor.
|
|
7
|
+
*
|
|
8
|
+
* Why this exists:
|
|
9
|
+
* - The package is effectively published as CommonJS due to a combination of factors:
|
|
10
|
+
*
|
|
11
|
+
* 1) TypeScript compilation target:
|
|
12
|
+
* - `tsconfig.json` specifies `"module": "commonjs"`,
|
|
13
|
+
* which determines the emitted JavaScript semantics
|
|
14
|
+
* (`module.exports`, `require()`).
|
|
15
|
+
*
|
|
16
|
+
* 2) Package entrypoint declaration:
|
|
17
|
+
* - `package.json` exposes `dist/index.js` via the `main` field,
|
|
18
|
+
* defining how Node and bundlers load the package.
|
|
19
|
+
*
|
|
20
|
+
* 3) Node module interpretation rules:
|
|
21
|
+
* - The absence of `"type": "module"` in `package.json` causes Node
|
|
22
|
+
* to interpret `.js` files as CommonJS by default.
|
|
23
|
+
*
|
|
24
|
+
* Together, these establish a CommonJS contract for consumers.
|
|
25
|
+
*
|
|
26
|
+
* Consumer expectations:
|
|
27
|
+
* - Webpack plugins are typically consumed via `require("package-name")` -- not ESM `import` syntax.
|
|
28
|
+
* - Consumers expect that call to return the constructor itself,
|
|
29
|
+
* not a `{ default: ... }` wrapper or a namespace object.
|
|
30
|
+
*
|
|
31
|
+
* Implementation details:
|
|
32
|
+
* - Uses TypeScript `export =` syntax so the compiled output is:
|
|
33
|
+
* `module.exports = PluginConstructor`
|
|
34
|
+
* - Users of plugin use `require()` import style intentionally to preserve CommonJS semantics.
|
|
35
|
+
*
|
|
36
|
+
* Note on linting:
|
|
37
|
+
* - `require()` usage in this file is deliberate and confined to this
|
|
38
|
+
* package boundary.
|
|
39
|
+
* - ESLint rules discouraging `require()` are intended for application code,
|
|
40
|
+
* not for package entrypoint shims.
|
|
41
|
+
*/
|
|
42
|
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
43
|
+
const GASDemodulifyPlugin = require("./plugin/GASDemodulifyPlugin");
|
|
44
|
+
module.exports = GASDemodulifyPlugin;
|
|
45
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,0DAA0D;AAC1D,oEAAqE;AACrE,iBAAS,mBAAmB,CAAC"}
|