ember.nvp 1.3.0 → 1.5.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.
Files changed (34) hide show
  1. package/.release-plan.json +8 -14
  2. package/CHANGELOG.md +57 -0
  3. package/package.json +1 -1
  4. package/packages/rolldown/README.md +62 -3
  5. package/packages/rolldown/index.ts +2 -3
  6. package/packages/rolldown/package.json +12 -5
  7. package/packages/rolldown/src/app-reexports.ts +2 -2
  8. package/packages/rolldown/src/babel.test.ts +125 -2
  9. package/packages/rolldown/src/babel.ts +52 -7
  10. package/packages/rolldown/src/isolated-declarations.ts +28 -3
  11. package/packages/rolldown/src/plugin-like.ts +23 -0
  12. package/packages/rolldown/src/published-types.test.ts +102 -0
  13. package/packages/rolldown/src/transform.test.ts +50 -2
  14. package/packages/rolldown/src/transform.ts +8 -1
  15. package/packages/rolldown/tsdown.config.ts +11 -0
  16. package/src/bases/minimal-app/app.test.ts +102 -1
  17. package/src/bases/minimal-app/files/app/app.ts +1 -1
  18. package/src/bases/minimal-app/files/babel.config.js +17 -1
  19. package/src/bases/minimal-app/files/package.json +2 -2
  20. package/src/bases/minimal-extension/files/app/app.ts +1 -1
  21. package/src/bases/minimal-extension/files/babel.config.js +17 -1
  22. package/src/bases/minimal-extension/files/package.json +2 -2
  23. package/src/bases/minimal-library/files/package.json +1 -1
  24. package/src/layers/qunit/index.js +0 -1
  25. package/src/layers/qunit/library-files/tests/test-helper.ts +1 -1
  26. package/src/layers/readme/index.js +16 -16
  27. package/src/utils/babel.js +20 -2
  28. package/test/ember.nvp/inspector-support.test.ts +13 -13
  29. package/test/ember.nvp/layer-inspector-support.test.ts +1 -1
  30. package/test/ember.nvp/layer-readme.test.ts +16 -16
  31. package/test/ember.nvp/layer-vitest.test.ts +1 -1
  32. package/test/ember.nvp/publish-configs.test.ts +352 -0
  33. package/packages/rolldown/rolldown.config.js +0 -23
  34. package/packages/rolldown/src/isolated-declarations.test.ts +0 -122
@@ -0,0 +1,102 @@
1
+ import { execa } from "execa";
2
+ import { readdir, readFile, rm, stat } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { beforeAll, describe, expect, it } from "vitest";
6
+
7
+ /**
8
+ * This package's own published shape: consumers write their build config in
9
+ * TypeScript, so `import { ember } from "@nullvoxpopuli/ember-rolldown"` has to
10
+ * carry types. Without them it is an implicit `any` (TS7016), and a config file
11
+ * silently stops being type-checked.
12
+ */
13
+ const packageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
14
+
15
+ interface Manifest {
16
+ dependencies: Record<string, string>;
17
+ peerDependencies: Record<string, string>;
18
+ publishConfig: { exports: Record<string, Record<string, string>> };
19
+ }
20
+
21
+ async function readManifest(): Promise<Manifest> {
22
+ return JSON.parse(await readFile(path.join(packageDir, "package.json"), "utf8")) as Manifest;
23
+ }
24
+
25
+ async function exists(relative: string) {
26
+ return Boolean(await stat(path.join(packageDir, relative)).catch(() => null));
27
+ }
28
+
29
+ async function declarations() {
30
+ const files = (await readdir(path.join(packageDir, "dist"))).filter((file) =>
31
+ file.endsWith(".d.ts"),
32
+ );
33
+
34
+ return Object.fromEntries(
35
+ await Promise.all(
36
+ files.map(
37
+ async (file) =>
38
+ [file, await readFile(path.join(packageDir, "dist", file), "utf8")] as const,
39
+ ),
40
+ ),
41
+ );
42
+ }
43
+
44
+ describe("published types", () => {
45
+ beforeAll(async () => {
46
+ // `clean` is on, but don't let these assertions depend on that: stale
47
+ // artifacts from an earlier build satisfy every one of them on their own
48
+ await rm(path.join(packageDir, "dist"), { recursive: true, force: true });
49
+ await execa("pnpm", ["build"], { cwd: packageDir });
50
+ }, 120_000);
51
+
52
+ it("every publishConfig export declares types, and they exist", async () => {
53
+ const manifest = await readManifest();
54
+ const entries = Object.entries(manifest.publishConfig.exports);
55
+
56
+ expect(entries.length).toBeGreaterThan(0);
57
+
58
+ for (const [subpath, conditions] of entries) {
59
+ expect(conditions, `${subpath} declares types`).toHaveProperty("types");
60
+ expect(await exists(conditions["types"]!), `${subpath} -> ${conditions["types"]}`).toBe(true);
61
+ expect(await exists(conditions["default"]!), `${subpath} -> ${conditions["default"]}`).toBe(
62
+ true,
63
+ );
64
+ }
65
+ });
66
+
67
+ /**
68
+ * A declaration may only reach for packages a consumer is guaranteed to have.
69
+ * Reference a devDependency and there are two ways to lose, both quiet:
70
+ * the specifier survives and consumers can't resolve it (`skipLibCheck` hides
71
+ * that, degrading the type to an error type), or the declaration bundler
72
+ * inlines the whole type surface — a ~200kB copy of rolldown's types, in the
73
+ * case that prompted this test, nominally distinct from the consumer's own.
74
+ *
75
+ * `ember()` returns plugins, so `RolldownPluginLike` exists to keep rolldown
76
+ * on the devDependency side of that line.
77
+ */
78
+ it("declarations only reference packages consumers have", async () => {
79
+ const manifest = await readManifest();
80
+ const available = new Set([
81
+ ...Object.keys(manifest.dependencies),
82
+ ...Object.keys(manifest.peerDependencies),
83
+ ]);
84
+
85
+ for (const [file, contents] of Object.entries(await declarations())) {
86
+ const specifiers = [...contents.matchAll(/^import .* from "([^"]+)";$/gm)]
87
+ .map(([, specifier]) => specifier!)
88
+ .filter((specifier) => !specifier.startsWith("."));
89
+
90
+ for (const specifier of specifiers) {
91
+ const owner = specifier.startsWith("@")
92
+ ? specifier.split("/").slice(0, 2).join("/")
93
+ : specifier.split("/")[0]!;
94
+
95
+ expect(available, `${file} imports ${specifier}`).toContain(owner);
96
+ }
97
+
98
+ // an inlined copy of a bundler's types, rather than an import of them
99
+ expect(contents, `${file} inlines rolldown's types`).not.toMatch(/interface PluginContext/);
100
+ }
101
+ });
102
+ });
@@ -2,6 +2,7 @@ import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import path from "node:path";
4
4
  import { rolldown } from "rolldown";
5
+ import type { Plugin } from "rolldown";
5
6
  import { describe, expect, it } from "vitest";
6
7
 
7
8
  import { emberTransform } from "./transform.ts";
@@ -15,7 +16,10 @@ import { emberTransform } from "./transform.ts";
15
16
  */
16
17
  async function bundle(
17
18
  files: Record<string, string>,
18
- { input = ["index.ts"] }: { input?: string[] } = {},
19
+ {
20
+ input = ["index.ts"],
21
+ plugins = () => [],
22
+ }: { input?: string[]; plugins?: (dir: string) => Plugin[] } = {},
19
23
  ): Promise<string> {
20
24
  const dir = await mkdtemp(path.join(tmpdir(), "ember-rolldown-build-"));
21
25
 
@@ -29,7 +33,7 @@ async function bundle(
29
33
 
30
34
  const build = await rolldown({
31
35
  input: input.map((relative) => path.join(dir, relative)),
32
- plugins: [emberTransform()],
36
+ plugins: [emberTransform(), ...plugins(dir)],
33
37
  external: (id) => !isLocal(id),
34
38
  onwarn() {},
35
39
  });
@@ -174,6 +178,50 @@ describe("emberTransform (full plugin via rolldown)", () => {
174
178
  `);
175
179
  });
176
180
 
181
+ it("resolves an absolute .gts specifier imported from a plugin's virtual module", async () => {
182
+ /**
183
+ * The shape a code-generating plugin emits: a virtual module (id prefixed
184
+ * with `\0`, so it exists nowhere on disk) whose generated source imports
185
+ * real files by absolute path, because it has no directory to be relative
186
+ * to. `path.dirname` of such an id is `"\0."`, so the specifier must not be
187
+ * resolved against the importer's directory.
188
+ */
189
+ const registry = (dir: string): Plugin[] => {
190
+ const source = "./registry?virtual=views";
191
+ const virtualId = `\0${source}`;
192
+
193
+ return [
194
+ {
195
+ name: "test:virtual-registry",
196
+ resolveId(id) {
197
+ return id === source ? virtualId : null;
198
+ },
199
+ load(id) {
200
+ if (id !== virtualId) return null;
201
+
202
+ return [
203
+ `import example from '${path.join(dir, "components/view/example.gts")}';`,
204
+ `export const views = { example };`,
205
+ ].join("\n");
206
+ },
207
+ },
208
+ ];
209
+ };
210
+
211
+ const code = await bundle(
212
+ {
213
+ "index.ts": `export { views } from './registry?virtual=views';`,
214
+ "components/view/example.gts": `<template>example</template>`,
215
+ },
216
+ { plugins: registry },
217
+ );
218
+
219
+ // The absolutely-specified .gts got compiled and bundled in.
220
+ expect(code).not.toContain("<template>");
221
+ expect(code).toContain("template(");
222
+ expect(code).toContain("example");
223
+ });
224
+
177
225
  it("compiles a .gts module used directly as an entry (no importer)", async () => {
178
226
  const code = await bundle(
179
227
  {
@@ -93,7 +93,14 @@ export function emberTransform(): Plugin {
93
93
  };
94
94
  }
95
95
 
96
- const fileName = path.join(path.dirname(importer), id);
96
+ // An absolute specifier already names the file, so it needs no
97
+ // importer-relative resolution — and joining it onto the importer's
98
+ // directory would corrupt it. That is not hypothetical: a plugin's
99
+ // virtual module (id prefixed with `\0`) can generate imports of
100
+ // on-disk files by absolute path, and `path.dirname("\0./registry")`
101
+ // is `"\0."`, so joining yields `"\0./Users/.../thing.gts"` — a path
102
+ // no `existsSync`/`readFile` can accept (it has a null byte in it).
103
+ const fileName = path.isAbsolute(id) ? id : path.join(path.dirname(importer), id);
97
104
 
98
105
  if (id.endsWith(".gts")) {
99
106
  return {
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "tsdown";
2
+
3
+ import { emberConfig } from "./src/config.ts";
4
+
5
+ export default defineConfig({
6
+ entry: {
7
+ index: "index.ts",
8
+ "app-reexports": "src/app-reexports.ts",
9
+ },
10
+ plugins: [emberConfig()],
11
+ });
@@ -3,8 +3,9 @@ import { generate, build, reapply } from "#test-helpers";
3
3
  import { packageJson } from "ember-apply";
4
4
 
5
5
  import type { Project } from "ember.nvp";
6
- import { rm } from "node:fs/promises";
6
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
7
7
  import { globSync } from "node:fs";
8
+ import { join } from "node:path";
8
9
 
9
10
  const expect = hardExpect.soft;
10
11
 
@@ -100,6 +101,106 @@ describe("typescript", () => {
100
101
  });
101
102
  });
102
103
 
104
+ const DEBUG_MARKERS = {
105
+ assert: "__debug_fixture_assert__",
106
+ deprecate: "__debug_fixture_deprecate__",
107
+ warn: "__debug_fixture_warn__",
108
+ isDevelopingApp: "__debug_fixture_is_developing_app__",
109
+ };
110
+
111
+ /**
112
+ * Lives in app/services/ so that app.ts's eager
113
+ * `import.meta.glob("./services/**\/*")` pulls it into the build
114
+ * without needing to modify any generated files.
115
+ */
116
+ const debugFixture = `
117
+ import { assert, deprecate, warn } from "@ember/debug";
118
+ import { isDevelopingApp, macroCondition } from "@embroider/macros";
119
+
120
+ assert("${DEBUG_MARKERS.assert}", true);
121
+
122
+ deprecate("${DEBUG_MARKERS.deprecate}", true, {
123
+ id: "debug-fixture",
124
+ until: "999.0.0",
125
+ for: "debug-fixture",
126
+ since: { available: "0.0.0", enabled: "0.0.0" },
127
+ });
128
+
129
+ warn("${DEBUG_MARKERS.warn}", true, { id: "debug-fixture" });
130
+
131
+ if (macroCondition(isDevelopingApp())) {
132
+ console.log("${DEBUG_MARKERS.isDevelopingApp}");
133
+ }
134
+ `;
135
+
136
+ /**
137
+ * Only .js files -- the sourcemaps (.map) contain the original
138
+ * source (with markers) even in production builds.
139
+ */
140
+ async function builtJavaScript(project: Project): Promise<string> {
141
+ let files = globSync("dist/**/*.js", { cwd: project.directory });
142
+
143
+ hardExpect(files.length, "build produced JS files").toBeGreaterThan(0);
144
+
145
+ let contents = await Promise.all(
146
+ files.map((file) => readFile(join(project.directory, file), "utf-8")),
147
+ );
148
+
149
+ return contents.join("\n");
150
+ }
151
+
152
+ describe("debug macros", () => {
153
+ let project: Project;
154
+
155
+ beforeAll(async () => {
156
+ project = await generate({
157
+ type: "app",
158
+ layers: ["typescript"],
159
+ });
160
+
161
+ await mkdir(join(project.directory, "app/services"), { recursive: true });
162
+ await writeFile(join(project.directory, "app/services/debug-fixture.ts"), debugFixture);
163
+
164
+ let { exitCode } = await project.install();
165
+
166
+ hardExpect(exitCode, "Install succeeds").toBe(0);
167
+ });
168
+
169
+ afterAll(async () => {
170
+ await rm(project.directory, { recursive: true, force: true });
171
+ });
172
+
173
+ it("has the debug-macros babel plugin", async () => {
174
+ let content = await project.read("babel.config.js");
175
+
176
+ expect(content).toContain("babel-plugin-debug-macros");
177
+ });
178
+
179
+ it("development build keeps debug code", async () => {
180
+ let { exitCode } = await build(project);
181
+
182
+ hardExpect(exitCode).toBe(0);
183
+
184
+ let output = await builtJavaScript(project);
185
+
186
+ for (let [name, marker] of Object.entries(DEBUG_MARKERS)) {
187
+ expect(output, `development build contains ${name}`).toContain(marker);
188
+ }
189
+ });
190
+
191
+ it("production build strips assert(), deprecate(), warn(), and if (macroCondition(isDevelopingApp()))", async () => {
192
+ let { exitCode } = await build(project, "production");
193
+
194
+ hardExpect(exitCode).toBe(0);
195
+
196
+ let output = await builtJavaScript(project);
197
+
198
+ for (let [name, marker] of Object.entries(DEBUG_MARKERS)) {
199
+ expect(output, `production build strips ${name}`).not.toContain(marker);
200
+ }
201
+ });
202
+ });
203
+
103
204
  describe("javascript", () => {
104
205
  let project: Project;
105
206
  beforeAll(async () => {
@@ -10,7 +10,7 @@
10
10
  * - https://ember-primitives.pages.dev/6-utils/createService.md
11
11
  * - https://ember-primitives.pages.dev/6-utils/createAsyncService.md
12
12
  */
13
- import Application from "ember-strict-application-resolver";
13
+ import Application from "@ember/application";
14
14
 
15
15
  export default class App extends Application {
16
16
  modules = {
@@ -1,5 +1,7 @@
1
1
  import { buildMacros } from "@embroider/macros/babel";
2
2
 
3
+ const isProduction = process.env.NODE_ENV === "production";
4
+
3
5
  const macros = buildMacros({
4
6
  configure(config) {
5
7
  if (process.env.EMBER_ENV === "test") {
@@ -40,8 +42,22 @@ export default {
40
42
  regenerator: false,
41
43
  },
42
44
  ],
45
+ isProduction && [
46
+ "babel-plugin-debug-macros",
47
+ {
48
+ debugTools: {
49
+ isDebug: false,
50
+ source: "@ember/debug",
51
+ assertPredicateIndex: 1,
52
+ },
53
+ externalizeHelpers: {
54
+ module: "@ember/debug",
55
+ },
56
+ },
57
+ "@ember/debug stripping",
58
+ ],
43
59
  ...macros.babelMacros,
44
- ],
60
+ ].filter(Boolean),
45
61
 
46
62
  generatorOpts: {
47
63
  compact: false,
@@ -29,8 +29,7 @@
29
29
  "@embroider/router": "^3.0.6",
30
30
  "@glimmer/component": "^2.1.1",
31
31
  "decorator-transforms": "^2.3.2",
32
- "ember-source": "~7.0.0",
33
- "ember-strict-application-resolver": "^0.1.0"
32
+ "ember-source": "~7.2.0"
34
33
  },
35
34
  "devDependencies": {
36
35
  "@babel/core": "^7.29.7",
@@ -44,6 +43,7 @@
44
43
  "@glint/tsserver-plugin": "^2.5.5",
45
44
  "@nullvoxpopuli/ember-vite": "workspace:*",
46
45
  "@rollup/plugin-babel": "^7.1.0",
46
+ "babel-plugin-debug-macros": "^2.0.0",
47
47
  "babel-plugin-ember-template-compilation": "^4.0.0",
48
48
  "typescript": "^6.0.3",
49
49
  "vite": "^8.0.14"
@@ -10,7 +10,7 @@
10
10
  * - https://ember-primitives.pages.dev/6-utils/createService.md
11
11
  * - https://ember-primitives.pages.dev/6-utils/createAsyncService.md
12
12
  */
13
- import Application from "ember-strict-application-resolver";
13
+ import Application from "@ember/application";
14
14
 
15
15
  export default class App extends Application {
16
16
  modules = {
@@ -1,5 +1,7 @@
1
1
  import { buildMacros } from "@embroider/macros/babel";
2
2
 
3
+ const isProduction = process.env.NODE_ENV === "production";
4
+
3
5
  const macros = buildMacros({
4
6
  configure(config) {
5
7
  if (process.env.EMBER_ENV === "test") {
@@ -40,8 +42,22 @@ export default {
40
42
  regenerator: false,
41
43
  },
42
44
  ],
45
+ isProduction && [
46
+ "babel-plugin-debug-macros",
47
+ {
48
+ debugTools: {
49
+ isDebug: false,
50
+ source: "@ember/debug",
51
+ assertPredicateIndex: 1,
52
+ },
53
+ externalizeHelpers: {
54
+ module: "@ember/debug",
55
+ },
56
+ },
57
+ "@ember/debug stripping",
58
+ ],
43
59
  ...macros.babelMacros,
44
- ],
60
+ ].filter(Boolean),
45
61
 
46
62
  generatorOpts: {
47
63
  compact: false,
@@ -30,8 +30,7 @@
30
30
  "@embroider/router": "^3.0.6",
31
31
  "@glimmer/component": "^2.1.1",
32
32
  "decorator-transforms": "^2.3.2",
33
- "ember-source": "~7.0.0",
34
- "ember-strict-application-resolver": "^0.1.0"
33
+ "ember-source": "~7.2.0"
35
34
  },
36
35
  "devDependencies": {
37
36
  "@babel/core": "^7.29.7",
@@ -45,6 +44,7 @@
45
44
  "@glint/tsserver-plugin": "^2.5.5",
46
45
  "@nullvoxpopuli/ember-vite": "workspace:*",
47
46
  "@rollup/plugin-babel": "^7.1.0",
47
+ "babel-plugin-debug-macros": "^2.0.0",
48
48
  "babel-plugin-ember-template-compilation": "^4.0.0",
49
49
  "typescript": "^6.0.3",
50
50
  "vite": "^8.0.14"
@@ -36,7 +36,7 @@
36
36
  "@ember/library-tsconfig": "^2.0.0",
37
37
  "@glimmer/component": "^2.1.1",
38
38
  "@nullvoxpopuli/ember-rolldown": "workspace:*",
39
- "ember-source": "~7.0.0",
39
+ "ember-source": "~7.2.0",
40
40
  "tsdown": "^0.22.7",
41
41
  "typescript": "^6.0.3"
42
42
  },
@@ -28,7 +28,6 @@ const libraryDeps = {
28
28
  "@embroider/macros": "^1.20.3",
29
29
  "@nullvoxpopuli/ember-vite": "workspace:*",
30
30
  "babel-plugin-ember-template-compilation": "^4.0.0",
31
- "ember-strict-application-resolver": "^0.1.0",
32
31
  vite: "^8.0.14",
33
32
  };
34
33
  const libraryTsDeps = {
@@ -4,7 +4,7 @@ import * as QUnit from "qunit";
4
4
  import { setup } from "qunit-dom";
5
5
  import { setupEmberOnerrorValidation, start as qunitStart } from "ember-qunit";
6
6
  import { setTesting } from "@embroider/macros";
7
- import EmberApp from "ember-strict-application-resolver";
7
+ import EmberApp from "@ember/application";
8
8
 
9
9
  class Router extends EmberRouter {
10
10
  location = "none";
@@ -21,23 +21,23 @@ An Ember application created with \`ember.nvp\`.
21
21
 
22
22
  ### Installation
23
23
 
24
- ~~~sh
24
+ \`\`\`sh
25
25
  ${project.packageManager} install
26
- ~~~
26
+ \`\`\`
27
27
 
28
28
  ### Development
29
29
 
30
30
  To start the local development server:
31
31
 
32
- ~~~sh
32
+ \`\`\`sh
33
33
  ${project.runPrefix} dev
34
- ~~~
34
+ \`\`\`
35
35
 
36
36
  or
37
37
 
38
- ~~~sh
38
+ \`\`\`sh
39
39
  ${project.runPrefix} start
40
- ~~~
40
+ \`\`\`
41
41
  ${layerDocsMarkdown}`;
42
42
  }
43
43
 
@@ -61,29 +61,29 @@ A browser extension using Ember created with \`ember.nvp\`.
61
61
 
62
62
  To start development:
63
63
 
64
- ~~~sh
64
+ \`\`\`sh
65
65
  ${project.runPrefix} dev
66
- ~~~
66
+ \`\`\`
67
67
 
68
68
  or
69
69
 
70
- ~~~sh
70
+ \`\`\`sh
71
71
  ${project.runPrefix} start
72
- ~~~
72
+ \`\`\`
73
73
 
74
74
  ### Building
75
75
 
76
76
  To build the library:
77
77
 
78
- ~~~sh
78
+ \`\`\`sh
79
79
  ${project.runPrefix} build
80
- ~~~
80
+ \`\`\`
81
81
 
82
82
  or
83
83
 
84
- ~~~sh
84
+ \`\`\`sh
85
85
  ${project.runPrefix} build:watch
86
- ~~~
86
+ \`\`\`
87
87
  ${layerDocsMarkdown}`;
88
88
  }
89
89
 
@@ -107,9 +107,9 @@ An Ember library/addon created with \`ember.nvp\`.
107
107
 
108
108
  To build the library:
109
109
 
110
- ~~~sh
110
+ \`\`\`sh
111
111
  ${project.runPrefix} build
112
- ~~~
112
+ \`\`\`
113
113
  ${layerDocsMarkdown}`;
114
114
  }
115
115
 
@@ -65,15 +65,33 @@ export async function prependPlugin(project, plugin, configPath = "babel.config.
65
65
  root
66
66
  .find(j.Property, {
67
67
  key: { name: "plugins" },
68
- value: { type: "ArrayExpression" },
69
68
  })
70
69
  .forEach(
71
70
  /**
72
71
  * @param {any} path
73
72
  */
74
73
  (path) => {
75
- path.node.value.elements.unshift(plugin);
74
+ let array = pluginsArrayOf(path.node.value);
75
+
76
+ array?.elements.unshift(plugin);
76
77
  },
77
78
  );
78
79
  });
79
80
  }
81
+
82
+ /**
83
+ * The plugins value may be the array itself, or the array with
84
+ * methods chained off of it: `[ ... ].filter(Boolean)`
85
+ *
86
+ * @param {any} node
87
+ * @returns {any | undefined} the underlying ArrayExpression
88
+ */
89
+ function pluginsArrayOf(node) {
90
+ if (node.type === "ArrayExpression") return node;
91
+
92
+ if (node.type === "CallExpression" && node.callee.type === "MemberExpression") {
93
+ return pluginsArrayOf(node.callee.object);
94
+ }
95
+
96
+ return undefined;
97
+ }