ember.nvp 1.14.0 → 1.15.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.
@@ -0,0 +1,515 @@
1
+ import { packageJson, tsconfig } from "ember-apply";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { readJSON } from "#utils/fs.js";
6
+ import { installedDependencies, v1AddonsIn } from "../../addons.js";
7
+ import { beyondEmberRolldown, readBabelConfig } from "../../babel.js";
8
+ import { filesWithExtension, firstExisting, listFiles, remove } from "../../files.js";
9
+ import { changedTestemFlags, removeToolingOfWantedLayers } from "../../layers.js";
10
+ import {
11
+ buildWithTsdown,
12
+ entriesFor,
13
+ renameCommonJSFiles,
14
+ useEmberSourceForTests,
15
+ usesCss,
16
+ } from "../../library.js";
17
+ import { readRollupConfig } from "./rollup.js";
18
+
19
+ /**
20
+ * @typedef {import('#types').Finding} Finding
21
+ * @typedef {import('#types').MigrationReport} MigrationReport
22
+ * @typedef {import('#utils/project.js').Project} Project
23
+ */
24
+
25
+ const ROLLUP_CONFIGS = ["rollup.config.mjs", "rollup.config.js", "rollup.config.cjs"];
26
+
27
+ const BABEL_CONFIGS = [
28
+ "babel.publish.config.cjs",
29
+ "babel.publish.config.mjs",
30
+ "babel.publish.config.js",
31
+ "babel.publish.config.json",
32
+ "babel.config.json",
33
+ "babel.config.cjs",
34
+ "babel.config.mjs",
35
+ "babel.config.js",
36
+ ".babelrc",
37
+ ".babelrc.json",
38
+ ];
39
+
40
+ /**
41
+ * The test setup that the blueprint generates.
42
+ * The qunit layer has its own.
43
+ */
44
+ const TEST_SETUP = [
45
+ "testem.cjs",
46
+ "testem.js",
47
+ "tests/index.html",
48
+ "tests/test-helper.ts",
49
+ "tests/test-helper.js",
50
+ ];
51
+
52
+ /**
53
+ * Only the build, tests, and demo app use these.
54
+ * Each one either goes away with the migration, or comes back through a layer.
55
+ */
56
+ const REPLACED_DEV_DEPENDENCIES = [
57
+ "@embroider/addon-dev",
58
+ "@embroider/compat",
59
+ "@embroider/core",
60
+ "@embroider/test-setup",
61
+ "@embroider/vite",
62
+ "@rollup/plugin-babel",
63
+ "ember-strict-application-resolver",
64
+ "rollup",
65
+ "rollup-plugin-delete",
66
+ "rollup-plugin-glimmer-template-tag",
67
+ "rollup-plugin-ts",
68
+ "testem",
69
+ ];
70
+
71
+ /**
72
+ * What the blueprint's demo app has: a router without routes,
73
+ * and an application template with a title and a greeting.
74
+ */
75
+ const DEMO_APP_FILES = new Set([
76
+ "demo-app/app.gjs",
77
+ "demo-app/app.gts",
78
+ "demo-app/styles.css",
79
+ "demo-app/templates/application.gjs",
80
+ "demo-app/templates/application.gts",
81
+ ]);
82
+ const DEMO_TEMPLATE =
83
+ /import\s*\{\s*pageTitle\s*\}\s*from\s*["']ember-page-title["'];?|const greeting = ["']hello["'];?|<\/?template>|\{\{pageTitle "Demo App"\}\}|<h1>Welcome to ember!<\/h1>|\{\{greeting\}\}, world!/g;
84
+
85
+ /**
86
+ * Compiler options that the blueprint's tsconfig files set.
87
+ * The library base's tsconfig.json covers what a library needs.
88
+ */
89
+ const BLUEPRINT_COMPILER_OPTIONS = new Set([
90
+ "allowImportingTsExtensions",
91
+ "allowJs",
92
+ "declaration",
93
+ "declarationDir",
94
+ "emitDeclarationOnly",
95
+ "lib",
96
+ "noEmit",
97
+ "noEmitOnError",
98
+ "rootDir",
99
+ "skipLibCheck",
100
+ "types",
101
+ ]);
102
+
103
+ /**
104
+ * A v2 addon that builds with rollup and `@embroider/addon-dev`.
105
+ *
106
+ * This covers `@ember/addon-blueprint`, and the addon package of its predecessor,
107
+ * `@embroider/addon-blueprint` (a monorepo with a separate test-app).
108
+ *
109
+ * The migration moves the build to tsdown with `@nullvoxpopuli/ember-rolldown`.
110
+ * The addon keeps its `ember-addon` metadata and `addon-main.cjs`,
111
+ * so that apps with ember-cli can still use it.
112
+ *
113
+ * @type {import('#types').Migration}
114
+ */
115
+ export default {
116
+ label: "@ember/addon-blueprint",
117
+ type: "library",
118
+
119
+ detect(directory) {
120
+ let manifest = readJSON(join(directory, "package.json"));
121
+
122
+ if (manifest?.["ember-addon"]?.version !== 2) return false;
123
+
124
+ let config = firstExisting(directory, ROLLUP_CONFIGS);
125
+
126
+ return Boolean(
127
+ config && readFileSync(join(directory, config), "utf-8").includes("@embroider/addon-dev"),
128
+ );
129
+ },
130
+
131
+ async check(project) {
132
+ /** @type {MigrationReport} */
133
+ let report = { unsupported: [], todo: [], layers: [] };
134
+ let root = project.directory;
135
+ let manifest = await packageJson.read(root);
136
+ let rollupFile = /** @type {string} */ (firstExisting(root, ROLLUP_CONFIGS));
137
+ let rollup = readRollupConfig(root, rollupFile);
138
+
139
+ if (!rollup) {
140
+ report.unsupported.push({
141
+ title: "A rollup config that ember.nvp cannot read",
142
+ where: [rollupFile],
143
+ action:
144
+ "Use `new Addon(...)` from @embroider/addon-dev/rollup, the way the blueprint does.",
145
+ });
146
+
147
+ return report;
148
+ }
149
+
150
+ let templates = filesWithExtension(root, [rollup.srcDir], [".hbs"]);
151
+
152
+ if (templates.length > 0) {
153
+ report.unsupported.push({
154
+ title: "Templates in .hbs files",
155
+ where: templates,
156
+ action:
157
+ "Move each template into its component as <template>, in a .gjs or .gts file.\n" +
158
+ "The build compiles <template> only.",
159
+ });
160
+ }
161
+
162
+ if (rollup.publicAssets) {
163
+ report.unsupported.push({
164
+ title: "Public assets",
165
+ where: [rollup.publicAssets],
166
+ action:
167
+ "Import each asset from the module that uses it, or publish the assets in another package.\n" +
168
+ "The build does not copy public assets into apps.",
169
+ });
170
+ }
171
+
172
+ let v1 = v1AddonsIn(installedDependencies(project.desires.path, manifest.dependencies ?? {}));
173
+
174
+ if (v1.length > 0) {
175
+ report.unsupported.push({
176
+ title: "v1 addons in dependencies",
177
+ where: v1,
178
+ action:
179
+ "Upgrade each addon to a v2 version, or remove it.\nApps without ember-cli cannot load v1 addons.",
180
+ });
181
+ }
182
+
183
+ if (rollup.unreadEntrypoints) {
184
+ report.todo.push({
185
+ title: "publicEntrypoints that are not a list of strings",
186
+ where: [rollupFile],
187
+ action: "Check `entry` in tsdown.config.js. The migration made every module an entry.",
188
+ });
189
+ }
190
+
191
+ if (rollup.appReexportsOptions) {
192
+ report.todo.push({
193
+ title: "appReexports options",
194
+ where: [rollup.appReexportsOptions],
195
+ action: "Add these options to `appReexports(...)` in tsdown.config.js.",
196
+ });
197
+ }
198
+
199
+ if (rollup.otherPlugins.length > 0) {
200
+ report.todo.push({
201
+ title: "Rollup plugins that the new build does not have",
202
+ where: rollup.otherPlugins,
203
+ action:
204
+ "Add the plugins that you still need to `plugins` in tsdown.config.js.\n" +
205
+ "tsdown runs most rollup plugins.",
206
+ });
207
+ }
208
+
209
+ let babel = publishBabelConfig(root, rollup.babelConfig);
210
+ let extraBabel = babel ? beyondEmberRolldown(babel.config) : [];
211
+
212
+ if (babel && extraBabel.length > 0) {
213
+ report.todo.push({
214
+ title: `A babel config with more than the build's defaults`,
215
+ where: [`${babel.file}: ${extraBabel.join(", ")}`],
216
+ action:
217
+ "The build uses this config in place of its defaults.\n" +
218
+ "Remove what the build does not need, then run the build.",
219
+ });
220
+ }
221
+
222
+ let typescript = usesTypeScript(root, rollup.srcDir);
223
+
224
+ if (typescript) {
225
+ report.layers.push("typescript");
226
+ report.todo.push({
227
+ title: "Declarations from isolatedDeclarations",
228
+ where: ["tsconfig.json"],
229
+ action:
230
+ "Run the build, and add the explicit types that TypeScript asks for.\n" +
231
+ "tsdown makes .d.ts files with isolatedDeclarations, which needs a type on each export.",
232
+ });
233
+
234
+ let options = await customCompilerOptions(project);
235
+
236
+ if (options.length > 0) {
237
+ report.todo.push({
238
+ title: "Compiler options that the new tsconfig.json does not have",
239
+ where: options,
240
+ action: "Add the options that you still need to tsconfig.json.",
241
+ });
242
+ }
243
+ }
244
+
245
+ if (hasTests(root)) {
246
+ report.layers.push("qunit");
247
+ }
248
+
249
+ if (listFiles(root, "demo-app").length > 0 && !isDefaultDemoApp(root)) {
250
+ report.todo.push({
251
+ title: "A demo app",
252
+ where: ["index.html", "demo-app/"],
253
+ action:
254
+ "Move the demo into its own app, or delete it.\n" +
255
+ "The test build also builds index.html, so the demo must keep building until it moves.",
256
+ });
257
+ }
258
+
259
+ let eslintConfig = firstExisting(root, [
260
+ "eslint.config.mjs",
261
+ "eslint.config.js",
262
+ "eslint.config.cjs",
263
+ ]);
264
+
265
+ if (
266
+ eslintConfig &&
267
+ readFileSync(join(root, eslintConfig), "utf-8").includes("@babel/eslint-parser")
268
+ ) {
269
+ report.todo.push({
270
+ title: "An ESLint config that parses with babel",
271
+ where: [eslintConfig],
272
+ action:
273
+ "Select the eslint-bundled layer, or configure @babel/eslint-parser yourself.\n" +
274
+ "The babel config that it reads goes away with the migration.",
275
+ });
276
+ }
277
+
278
+ let testem = changedTestemFlags(root, "testem.cjs", "library");
279
+
280
+ if (testem) report.todo.push(testem);
281
+
282
+ if (existsSync(join(root, ".try.mjs"))) {
283
+ report.todo.push({
284
+ title: "ember-try scenarios",
285
+ where: [".try.mjs"],
286
+ action:
287
+ "Remove the scenarios that set ENABLE_COMPAT_BUILD or use ember-source below 7.2.\n" +
288
+ "The new test setup has no compat mode, and it needs the strict resolver of ember-source 7.2.",
289
+ });
290
+ }
291
+
292
+ return report;
293
+ },
294
+
295
+ async run(project, report) {
296
+ let root = project.directory;
297
+ let rollupFile = /** @type {string} */ (firstExisting(root, ROLLUP_CONFIGS));
298
+ let rollup = /** @type {import('./rollup.js').RollupSetup} */ (
299
+ readRollupConfig(root, rollupFile)
300
+ );
301
+ let typescript = await project.hasOrWantsLayer("typescript");
302
+ let babel = publishBabelConfig(root, rollup.babelConfig);
303
+
304
+ await removeToolingOfWantedLayers(project, report);
305
+
306
+ await remove(project, [rollupFile, "config/ember-cli-update.json", "tsconfig.publish.json"]);
307
+ // the blueprint's config for editors and tests, not for the build
308
+ await remove(project, otherBabelConfigs(root, babel?.file));
309
+
310
+ if (babel && beyondEmberRolldown(babel.config).length === 0) {
311
+ await remove(project, [babel.file]);
312
+ } else if (babel) {
313
+ await withoutColocationPlugin(project, babel.file);
314
+ }
315
+
316
+ // the old test setup builds with @embroider/vite, which leaves with the migration
317
+ await remove(project, TEST_SETUP);
318
+ await removeBlueprintViteConfig(project);
319
+
320
+ if (hasTests(root) && !project.wantsLayer("qunit")) {
321
+ report.todo.push({
322
+ title: "Tests without a test setup",
323
+ where: ["tests/"],
324
+ action: "Run ember.nvp again with the qunit layer, or delete the tests.",
325
+ });
326
+ }
327
+
328
+ // a demo app of your own is in the report, and keeps the resolver that it imports
329
+ let demo = listFiles(root, "demo-app").length > 0;
330
+ let defaultDemo = demo && isDefaultDemoApp(root);
331
+
332
+ if (defaultDemo) {
333
+ await remove(project, ["index.html", "demo-app"]);
334
+ }
335
+
336
+ if (onlyComments(root, "unpublished-development-types/index.d.ts")) {
337
+ await remove(project, ["unpublished-development-types"]);
338
+ }
339
+
340
+ // the library base writes its own
341
+ await remove(project, ["tsconfig.json"]);
342
+
343
+ await packageJson.removeDevDependencies(
344
+ demo && !defaultDemo
345
+ ? REPLACED_DEV_DEPENDENCIES.filter((name) => name !== "ember-strict-application-resolver")
346
+ : REPLACED_DEV_DEPENDENCIES,
347
+ root,
348
+ );
349
+ await packageJson.modify((json) => {
350
+ // the old test setup's script, which the qunit layer replaces
351
+ if (json.scripts?.test?.includes("testem")) delete json.scripts.test;
352
+ }, root);
353
+
354
+ await renameCommonJSFiles(project);
355
+
356
+ await buildWithTsdown(project, {
357
+ entry: entriesFor(rollup.srcDir, rollup.publicEntrypoints),
358
+ appReexports: rollup.appReexports,
359
+ css: usesCss(root, rollup.srcDir),
360
+ typescript,
361
+ outDir: rollup.destDir === "dist" ? undefined : rollup.destDir,
362
+ });
363
+
364
+ await useEmberSourceForTests(project);
365
+ },
366
+ };
367
+
368
+ /**
369
+ * The babel config that the rollup build reads.
370
+ *
371
+ * @param {string} root
372
+ * @param {string | undefined} named the file that rollup.config names
373
+ * @returns {{ file: string, config: import('../../babel.js').BabelConfig } | undefined}
374
+ */
375
+ function publishBabelConfig(root, named) {
376
+ let file = named && existsSync(join(root, named)) ? named : firstExisting(root, BABEL_CONFIGS);
377
+
378
+ if (!file) return;
379
+
380
+ let config = readBabelConfig(root, file) ?? {
381
+ plugins: [],
382
+ presets: [],
383
+ hasCode: true,
384
+ templateTransforms: [],
385
+ };
386
+
387
+ return { file, config };
388
+ }
389
+
390
+ /**
391
+ * @param {string} root
392
+ * @param {string | undefined} publishConfig
393
+ * @returns {string[]}
394
+ */
395
+ function otherBabelConfigs(root, publishConfig) {
396
+ return BABEL_CONFIGS.filter((file) => file !== publishConfig && existsSync(join(root, file)));
397
+ }
398
+
399
+ /**
400
+ * The colocation plugin joins .hbs files to their components.
401
+ * The migration requires that no .hbs files remain,
402
+ * and the plugin leaves with @embroider/addon-dev.
403
+ *
404
+ * @param {Project} project
405
+ * @param {string} file
406
+ */
407
+ async function withoutColocationPlugin(project, file) {
408
+ let path = project.path(file);
409
+ let source = readFileSync(path, "utf-8");
410
+ let result = source.replace(
411
+ /\s*["']@embroider\/addon-dev\/template-colocation-plugin["'],?/g,
412
+ "",
413
+ );
414
+
415
+ if (result !== source) {
416
+ await writeFile(path, result);
417
+ }
418
+ }
419
+
420
+ /**
421
+ * The blueprint's vite.config.mjs builds the tests and the demo app.
422
+ * The qunit layer writes its own.
423
+ *
424
+ * @param {Project} project
425
+ */
426
+ async function removeBlueprintViteConfig(project) {
427
+ for (let file of ["vite.config.mjs", "vite.config.js", "vite.config.ts"]) {
428
+ let source = await project.read(file);
429
+
430
+ if (source?.includes("@embroider/vite")) {
431
+ await remove(project, [file]);
432
+ }
433
+ }
434
+ }
435
+
436
+ /**
437
+ * @param {string} root
438
+ * @param {string} srcDir
439
+ */
440
+ function usesTypeScript(root, srcDir) {
441
+ return (
442
+ existsSync(join(root, "tsconfig.json")) ||
443
+ filesWithExtension(root, [srcDir], [".ts", ".gts"]).some((file) => !file.endsWith(".d.ts"))
444
+ );
445
+ }
446
+
447
+ /**
448
+ * @param {string} root
449
+ */
450
+ function hasTests(root) {
451
+ return listFiles(root, "tests").some((file) => /-test\.(js|ts|gjs|gts)$/.test(file));
452
+ }
453
+
454
+ /**
455
+ * Whether index.html and demo-app/ are still the blueprint's demo app
456
+ *
457
+ * @param {string} root
458
+ */
459
+ function isDefaultDemoApp(root) {
460
+ for (let file of listFiles(root, "demo-app")) {
461
+ if (!DEMO_APP_FILES.has(file)) return false;
462
+ }
463
+
464
+ let app = firstExisting(root, ["demo-app/app.gts", "demo-app/app.gjs"]);
465
+
466
+ if (app && /this\.route\(/.test(readFileSync(join(root, app), "utf-8"))) return false;
467
+
468
+ let template = firstExisting(root, [
469
+ "demo-app/templates/application.gts",
470
+ "demo-app/templates/application.gjs",
471
+ ]);
472
+
473
+ if (!template) return true;
474
+
475
+ return readFileSync(join(root, template), "utf-8").replace(DEMO_TEMPLATE, "").trim() === "";
476
+ }
477
+
478
+ /**
479
+ * @param {string} root
480
+ * @param {string} file
481
+ * @returns {boolean} whether the file has nothing but comments, like the blueprint's type registry
482
+ */
483
+ function onlyComments(root, file) {
484
+ let path = join(root, file);
485
+
486
+ if (!existsSync(path)) return false;
487
+
488
+ return (
489
+ readFileSync(path, "utf-8")
490
+ .replace(/\/\*[\s\S]*?\*\//g, "")
491
+ .replace(/\/\/.*$/gm, "")
492
+ .trim() === ""
493
+ );
494
+ }
495
+
496
+ /**
497
+ * @param {Project} project
498
+ * @returns {Promise<string[]>} compiler options beyond the blueprint's, from tsconfig.json and tsconfig.publish.json
499
+ */
500
+ async function customCompilerOptions(project) {
501
+ /** @type {string[]} */
502
+ let custom = [];
503
+
504
+ for (let file of ["tsconfig.json", "tsconfig.publish.json"]) {
505
+ if (!project.hasFile(file)) continue;
506
+
507
+ let config = await tsconfig.read(project.path(file)).catch(() => undefined);
508
+
509
+ for (let option of Object.keys(config?.compilerOptions ?? {})) {
510
+ if (!BLUEPRINT_COMPILER_OPTIONS.has(option)) custom.push(`${file}: ${option}`);
511
+ }
512
+ }
513
+
514
+ return custom;
515
+ }
@@ -0,0 +1,188 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import {
4
+ importedNames,
5
+ parseProgram,
6
+ propertyValue,
7
+ sourceOf,
8
+ stringArray,
9
+ stringValue,
10
+ walk,
11
+ } from "../../javascript.js";
12
+
13
+ /**
14
+ * Rollup plugins whose work the tsdown build does by itself
15
+ */
16
+ const REPLACED_PLUGINS = new Set([
17
+ "@embroider/addon-dev/rollup",
18
+ "@rollup/plugin-babel",
19
+ "rollup-plugin-glimmer-template-tag",
20
+ "rollup-plugin-ts",
21
+ "@rollup/plugin-typescript",
22
+ "rollup-plugin-delete",
23
+ ]);
24
+
25
+ /**
26
+ * `@embroider/addon-dev` plugins whose work the tsdown build does by itself
27
+ */
28
+ const REPLACED_ADDON_METHODS = new Set([
29
+ "dependencies",
30
+ "hbs",
31
+ "gjs",
32
+ "clean",
33
+ "declarations",
34
+ "keepAssets",
35
+ "output",
36
+ ]);
37
+
38
+ /**
39
+ * What a rollup.config asks of `@embroider/addon-dev`
40
+ *
41
+ * @typedef {object} RollupSetup
42
+ * @property {string} srcDir
43
+ * @property {string} destDir
44
+ * @property {string[]} publicEntrypoints globs for built files, relative to `destDir`
45
+ * @property {boolean} unreadEntrypoints whether `publicEntrypoints` was not a list of strings
46
+ * @property {string[] | undefined} appReexports globs for built files, relative to `destDir`
47
+ * @property {string | undefined} appReexportsOptions the source of the options argument
48
+ * @property {string | undefined} publicAssets the source of the `publicAssets` call
49
+ * @property {string | undefined} babelConfig the babel config file that the build reads, when the config names one
50
+ * @property {string[]} otherPlugins the source of each plugin that the tsdown build does not replace
51
+ */
52
+
53
+ /**
54
+ * @param {string} root
55
+ * @param {string} file relative to `root`
56
+ * @returns {RollupSetup | undefined} undefined when the config has no `new Addon(...)`
57
+ */
58
+ export function readRollupConfig(root, file) {
59
+ let source = readFileSync(join(root, file), "utf-8");
60
+ let program = parseProgram(source, file);
61
+
62
+ if (!program) return;
63
+
64
+ let imports = importedNames(program);
65
+
66
+ /** @type {RollupSetup} */
67
+ let setup = {
68
+ srcDir: "src",
69
+ destDir: "dist",
70
+ publicEntrypoints: ["**/*.js"],
71
+ unreadEntrypoints: false,
72
+ appReexports: undefined,
73
+ appReexportsOptions: undefined,
74
+ publicAssets: undefined,
75
+ babelConfig: babelConfigNamedIn(source),
76
+ otherPlugins: [],
77
+ };
78
+
79
+ /** @type {string | undefined} */
80
+ let addonName;
81
+
82
+ walk(program, (node, parent) => {
83
+ if (node.type !== "NewExpression" || node.callee?.type !== "Identifier") return;
84
+ if (imports.get(node.callee.name) !== "@embroider/addon-dev/rollup") return;
85
+
86
+ let options = node.arguments?.[0];
87
+
88
+ setup.srcDir = stringValue(propertyValue(options, "srcDir")) ?? setup.srcDir;
89
+ setup.destDir = stringValue(propertyValue(options, "destDir")) ?? setup.destDir;
90
+
91
+ if (parent?.type === "VariableDeclarator" && parent.id?.type === "Identifier") {
92
+ addonName = parent.id.name;
93
+ }
94
+ });
95
+
96
+ if (!addonName) return;
97
+
98
+ let plugins = pluginsArray(program);
99
+
100
+ for (let plugin of plugins?.elements ?? []) {
101
+ if (!plugin) continue;
102
+
103
+ let method = addonMethod(plugin, addonName);
104
+
105
+ if (method === "publicEntrypoints") {
106
+ let globs = stringArray(plugin.arguments?.[0]);
107
+
108
+ setup.publicEntrypoints = globs ?? setup.publicEntrypoints;
109
+ setup.unreadEntrypoints = !globs;
110
+ continue;
111
+ }
112
+
113
+ if (method === "appReexports") {
114
+ setup.appReexports = stringArray(plugin.arguments?.[0]) ?? ["**/*.js"];
115
+
116
+ let options = plugin.arguments?.[1];
117
+
118
+ setup.appReexportsOptions = options ? sourceOf(source, options) : undefined;
119
+ continue;
120
+ }
121
+
122
+ if (method === "publicAssets") {
123
+ setup.publicAssets = sourceOf(source, plugin);
124
+ continue;
125
+ }
126
+
127
+ if (method && REPLACED_ADDON_METHODS.has(method)) continue;
128
+
129
+ let callee = plugin.type === "CallExpression" ? plugin.callee : undefined;
130
+ let from = callee?.type === "Identifier" ? imports.get(callee.name) : undefined;
131
+
132
+ if (from && REPLACED_PLUGINS.has(from)) continue;
133
+
134
+ setup.otherPlugins.push(sourceOf(source, plugin).replace(/\s+/g, " "));
135
+ }
136
+
137
+ return setup;
138
+ }
139
+
140
+ /**
141
+ * @param {import('../../javascript.js').Node} program
142
+ * @returns {import('../../javascript.js').Node | undefined} the first `plugins: [...]` array
143
+ */
144
+ function pluginsArray(program) {
145
+ /** @type {import('../../javascript.js').Node | undefined} */
146
+ let found;
147
+
148
+ walk(program, (node) => {
149
+ if (found || node.type !== "ObjectExpression") return;
150
+
151
+ let plugins = propertyValue(node, "plugins");
152
+
153
+ if (plugins?.type === "ArrayExpression") found = plugins;
154
+ });
155
+
156
+ return found;
157
+ }
158
+
159
+ /**
160
+ * @param {import('../../javascript.js').Node} node
161
+ * @param {string} addonName
162
+ * @returns {string | undefined} `hbs` for `addon.hbs()`
163
+ */
164
+ function addonMethod(node, addonName) {
165
+ if (node.type !== "CallExpression") return;
166
+
167
+ let callee = node.callee;
168
+
169
+ if (callee?.type !== "MemberExpression") return;
170
+ if (callee.object?.type !== "Identifier" || callee.object.name !== addonName) return;
171
+
172
+ return callee.property?.name;
173
+ }
174
+
175
+ /**
176
+ * The blueprint names the config with a path:
177
+ *
178
+ * const babelConfig = resolve(rootDirectory, './babel.publish.config.cjs');
179
+ *
180
+ * @param {string} source
181
+ * @returns {string | undefined}
182
+ */
183
+ function babelConfigNamedIn(source) {
184
+ let match =
185
+ /["'`](?:\.\/)?((?:config\/)?babel(?:\.publish)?\.config\.(?:c|m)?js(?:on)?)["'`]/.exec(source);
186
+
187
+ return match?.[1];
188
+ }