ember.nvp 1.6.0 → 1.7.1

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 (99) hide show
  1. package/.github/workflows/ci.yml +5 -1
  2. package/.release-plan.json +51 -9
  3. package/CHANGELOG.md +30 -0
  4. package/README.md +41 -28
  5. package/RELEASE.md +16 -14
  6. package/package.json +1 -1
  7. package/packages/build-tooling-utils/README.md +7 -12
  8. package/packages/build-tooling-utils/package.json +1 -1
  9. package/packages/build-tooling-utils/src/extensions.ts +4 -3
  10. package/packages/build-tooling-utils/src/maybe-babel.ts +18 -15
  11. package/packages/rolldown/README.md +138 -98
  12. package/packages/rolldown/index.ts +66 -28
  13. package/packages/rolldown/package.json +1 -1
  14. package/packages/rolldown/src/app-reexports.test.ts +4 -3
  15. package/packages/rolldown/src/app-reexports.ts +37 -27
  16. package/packages/rolldown/src/babel.test.ts +10 -5
  17. package/packages/rolldown/src/babel.ts +117 -72
  18. package/packages/rolldown/src/bundle.test.ts +149 -0
  19. package/packages/rolldown/src/bundle.ts +71 -0
  20. package/packages/rolldown/src/config.ts +42 -18
  21. package/packages/rolldown/src/dts.test.ts +42 -26
  22. package/packages/rolldown/src/externals.test.ts +16 -11
  23. package/packages/rolldown/src/externals.ts +45 -28
  24. package/packages/rolldown/src/isolated-declarations.ts +56 -43
  25. package/packages/rolldown/src/plugin-like.ts +18 -15
  26. package/packages/rolldown/src/published-types.test.ts +18 -13
  27. package/packages/rolldown/src/transform.test.ts +26 -17
  28. package/packages/rolldown/src/transform.ts +68 -48
  29. package/packages/vite/README.md +10 -11
  30. package/packages/vite/package.json +1 -1
  31. package/packages/vite/src/ember.ts +44 -36
  32. package/packages/vite/src/test-html.ts +13 -11
  33. package/src/bases/README.md +6 -3
  34. package/src/bases/minimal-app/app.test.ts +7 -4
  35. package/src/bases/minimal-app/files/app/app.ts +2 -3
  36. package/src/bases/minimal-app/files/app/router.ts +3 -3
  37. package/src/bases/minimal-app/index.js +10 -6
  38. package/src/bases/minimal-custom-element/files/README.md +95 -0
  39. package/src/bases/minimal-custom-element/files/package.json +47 -0
  40. package/src/bases/minimal-custom-element/files/src/components/counter.gts +27 -0
  41. package/src/bases/minimal-custom-element/files/src/element.ts +45 -0
  42. package/src/bases/minimal-custom-element/files/src/index.ts +2 -0
  43. package/src/bases/minimal-custom-element/files/src/register.ts +5 -0
  44. package/src/bases/minimal-custom-element/files/tsconfig.json +10 -0
  45. package/src/bases/minimal-custom-element/files/tsdown.config.js +7 -0
  46. package/src/bases/minimal-custom-element/index.js +212 -0
  47. package/src/bases/minimal-extension/files/README.md +13 -5
  48. package/src/bases/minimal-extension/files/app/app.ts +2 -3
  49. package/src/bases/minimal-extension/files/app/config.ts +5 -3
  50. package/src/bases/minimal-extension/index.js +16 -11
  51. package/src/bases/minimal-library/files/README.md +8 -7
  52. package/src/bases/minimal-library/index.js +21 -15
  53. package/src/cli/args.js +1 -1
  54. package/src/cli/generator.js +4 -0
  55. package/src/cli/index.js +23 -18
  56. package/src/cli/questions/project-type.js +17 -2
  57. package/src/cli/questions/write.js +12 -10
  58. package/src/layers/are-the-types-wrong/index.js +6 -6
  59. package/src/layers/github-actions/index.js +7 -4
  60. package/src/layers/inspector-support/index.js +2 -2
  61. package/src/layers/publint/index.js +2 -2
  62. package/src/layers/qunit/index.js +21 -17
  63. package/src/layers/readme/index.js +55 -4
  64. package/src/layers/renovate/index.js +5 -5
  65. package/src/layers/typescript/index.js +20 -13
  66. package/src/layers/vitest/index.js +8 -4
  67. package/src/utils/babel.js +3 -2
  68. package/src/utils/buffer.js +2 -2
  69. package/src/utils/cli.js +4 -4
  70. package/src/utils/fs.js +23 -14
  71. package/src/utils/inspector-support.js +22 -19
  72. package/src/utils/npm.js +15 -12
  73. package/src/utils/project-type.js +19 -0
  74. package/src/utils/project.js +14 -2
  75. package/src/utils/remove-types.js +2 -4
  76. package/src/utils/rewrite-imports.js +43 -31
  77. package/src/utils/stage.js +44 -33
  78. package/src/utils/tsdown-config.js +6 -5
  79. package/src/utils/types.ts +10 -9
  80. package/test/ember.nvp/base-custom-element.test.ts +440 -0
  81. package/test/ember.nvp/base-library.test.ts +6 -4
  82. package/test/ember.nvp/generator-replace-or-update.test.ts +6 -4
  83. package/test/ember.nvp/inspector-support.test.ts +4 -3
  84. package/test/ember.nvp/layer-inspector-support.test.ts +2 -3
  85. package/test/ember.nvp/layer-publish-checks.test.ts +2 -3
  86. package/test/ember.nvp/layer-qunit.test.ts +2 -2
  87. package/test/ember.nvp/layer-readme.test.ts +63 -3
  88. package/test/ember.nvp/layer-vitest.test.ts +16 -9
  89. package/test/ember.nvp/layers-load.test.ts +26 -0
  90. package/test/ember.nvp/library-src-fixtures.ts +7 -3
  91. package/test/ember.nvp/permutations/custom-element-1.test.ts +3 -0
  92. package/test/ember.nvp/permutations/custom-element-2.test.ts +3 -0
  93. package/test/ember.nvp/permutations/custom-element-checks.test.ts +3 -0
  94. package/test/ember.nvp/permutations/shared.ts +22 -12
  95. package/test/ember.nvp/publish-configs.test.ts +44 -31
  96. package/test/ember.nvp/rewrite-imports.test.ts +4 -3
  97. package/test/ember.nvp/scoped-css-library.test.ts +6 -4
  98. package/test/test-helpers/permutate.test.ts +3 -3
  99. package/test/test-helpers.ts +12 -7
package/src/cli/index.js CHANGED
@@ -20,7 +20,7 @@ import { askReplaceOrUpdate } from "./questions/replace-or-update.js";
20
20
  * This whole file's primary purpose is to be an interactive CLI
21
21
  * for `generateProject`.
22
22
  *
23
- * Other tools can call `generateProject` themselves if they wish
23
+ * Other tools can call `generateProject` themselves
24
24
  * if they want to provide different TUI/GUIs.
25
25
  */
26
26
  async function main() {
@@ -38,19 +38,19 @@ async function main() {
38
38
  await askIfOK();
39
39
 
40
40
  /**
41
- * Everything is generated in a stage: a real directory (in the OS temp
42
- * dir) acting as a writable overlay of the target directory.
41
+ * Everything is generated in a stage:
42
+ * a real directory (in the OS temp dir) acting as a writable overlay of the target directory.
43
43
  *
44
- * When updating an existing project, the stage is seeded with a full
45
- * copy of it (sans node_modules/.git), so the bases and layers run
46
- * against the project's current state -- isSetup checks, package.json
47
- * reads, file existence checks, etc. all see the existing files, and
48
- * the codemods modify them in the stage exactly as if they were
49
- * operating in place. "replace" starts from an empty stage instead.
44
+ * When updating an existing project, the stage is seeded with a full copy of it
45
+ * (sans node_modules/.git), so the bases and layers run against the project's current state:
46
+ * - isSetup checks, package.json reads, file existence checks, etc. all see the existing files
47
+ * - the codemods modify them in the stage exactly as if they were operating in place
50
48
  *
51
- * The target itself is not touched until stage.commit() below, which
52
- * applies the difference between the stage and the target (or the
53
- * subset of it the user accepts during review).
49
+ * "replace" starts from an empty stage instead.
50
+ *
51
+ * The target itself is not touched until stage.commit() below.
52
+ * That applies the difference between the stage and the target
53
+ * (or the subset of it the user accepts during review).
54
54
  */
55
55
  const stage = await Stage.create(projectPath, { seed: replaceOrUpdate !== "replace" });
56
56
 
@@ -98,9 +98,12 @@ async function main() {
98
98
  }
99
99
 
100
100
  /**
101
- * New projects (and "replace", which was already confirmed) are written
102
- * without asking. Updates to an existing project must be confirmed:
103
- * write all / review each change (accept/reject) / cancel.
101
+ * New projects (and "replace", which was already confirmed) are written without asking.
102
+ *
103
+ * Updates to an existing project must be confirmed:
104
+ * - write all
105
+ * - review each change (accept/reject)
106
+ * - cancel
104
107
  */
105
108
  const isUpdate = !stage.isNew && replaceOrUpdate !== "replace";
106
109
 
@@ -123,9 +126,11 @@ async function main() {
123
126
 
124
127
  /**
125
128
  * Converted / generated files are written expecting the project's own
126
- * formatting and lint pass to run right away (e.g. blank lines between
127
- * import groups are `eslint --fix`'s job) -- so when the project has a
128
- * lint:fix script, surface it as the step right after install.
129
+ * formatting and lint pass to run right away.
130
+ * (e.g. blank lines between import groups are `eslint --fix`'s job)
131
+ *
132
+ * So when the project has a lint:fix script,
133
+ * surface it as the step right after install.
129
134
  */
130
135
  let lintFixStep = "";
131
136
  try {
@@ -4,12 +4,21 @@ import { answers, printArgInUse } from "#args";
4
4
  /**
5
5
  * addon is alias for library
6
6
  * web-extension is alias for extension
7
+ * web-component is alias for custom-element
7
8
  */
8
- const SUPPORTED = new Set(["app", "library", "addon", "extension", "web-extension"]);
9
+ const SUPPORTED = new Set([
10
+ "app",
11
+ "library",
12
+ "addon",
13
+ "extension",
14
+ "web-extension",
15
+ "custom-element",
16
+ "web-component",
17
+ ]);
9
18
 
10
19
  /**
11
20
  * @param {string | undefined} selected
12
- * @returns {selected is "app" | "library" | "addon" | "extension" | "web-extension"}
21
+ * @returns {selected is "app" | "library" | "addon" | "extension" | "web-extension" | "custom-element" | "web-component"}
13
22
  */
14
23
  function isValid(selected) {
15
24
  if (!selected) return false;
@@ -27,6 +36,7 @@ export async function askProjectType() {
27
36
 
28
37
  if (answers.type === "addon") return "library";
29
38
  if (answers.type === "web-extension") return "extension";
39
+ if (answers.type === "web-component") return "custom-element";
30
40
 
31
41
  return answers.type;
32
42
  }
@@ -45,6 +55,11 @@ export async function askProjectType() {
45
55
  label: "browser extension",
46
56
  hint: "Manifest V3 extension whose popup is an Ember app",
47
57
  },
58
+ {
59
+ value: "custom-element",
60
+ label: "custom element",
61
+ hint: "an Ember component packaged as a web component, usable from any page",
62
+ },
48
63
  ],
49
64
  });
50
65
 
@@ -17,10 +17,11 @@ function isValid(selected) {
17
17
  }
18
18
 
19
19
  /**
20
- * Nothing has been written to the target directory yet -- everything so
21
- * far only exists in the stage. Ask whether to write the staged changes,
22
- * with the option to review the diff change-by-change, accepting or
23
- * rejecting each one.
20
+ * Nothing has been written to the target directory yet.
21
+ * Everything so far only exists in the stage.
22
+ *
23
+ * Ask whether to write the staged changes,
24
+ * with the option to review the diff change-by-change, accepting or rejecting each one.
24
25
  *
25
26
  * @param {import('#utils/stage.js').Stage} stage
26
27
  * @param {import('#utils/stage.js').Change[]} changes
@@ -61,10 +62,11 @@ export async function askToWrite(stage, changes) {
61
62
  }
62
63
 
63
64
  /**
64
- * Show each change's diff and ask what to do with it. "accept remaining"
65
- * and "reject remaining" finish the review early, so they require an
66
- * extra confirmation; declining the confirmation returns to the change
67
- * being reviewed.
65
+ * Show each change's diff and ask what to do with it.
66
+ *
67
+ * "accept remaining" and "reject remaining" finish the review early,
68
+ * so they require an extra confirmation.
69
+ * Declining the confirmation returns to the change being reviewed.
68
70
  *
69
71
  * @param {import('#utils/stage.js').Stage} stage
70
72
  * @param {import('#utils/stage.js').Change[]} changes
@@ -83,8 +85,8 @@ async function reviewEachChange(stage, changes) {
83
85
 
84
86
  const remaining = changes.length - index;
85
87
 
86
- // Bypass clack for the diff itself: diffs are long, and clack's
87
- // log decorations wrap every line.
88
+ // Bypass clack for the diff itself.
89
+ // Diffs are long, and clack's log decorations wrap every line.
88
90
  console.log("\n" + (await stage.diff([change])));
89
91
 
90
92
  const answer = await p.select({
@@ -15,13 +15,13 @@ export default {
15
15
 
16
16
  async run(project) {
17
17
  // Publish checks: apps aren't published
18
- if (project.type !== "library") return;
18
+ if (!project.isLibrary) return;
19
19
 
20
20
  // Declarations are the thing being checked
21
21
  if (!(await project.hasOrWantsLayer("typescript"))) return;
22
22
 
23
- // ESM-only packages don't offer the CJS resolution modes the strict
24
- // profile requires
23
+ // ESM-only packages don't offer the CJS resolution modes
24
+ // that the strict profile requires
25
25
  await addTsdownConfigProperty(project, "attw", `attw: { profile: "esm-only" }`);
26
26
  await packageJson.addDevDependencies(await getLatest(deps), project.directory);
27
27
  },
@@ -42,9 +42,9 @@ export default {
42
42
  /** @type {string[]} */
43
43
  const reasons = [];
44
44
 
45
- // Nothing to set up outside TypeScript libraries, so nothing can be
46
- // missing
47
- if (project.type !== "library" || !(await project.hasOrWantsLayer("typescript"))) {
45
+ // Nothing to set up outside TypeScript libraries,
46
+ // so nothing can be missing
47
+ if (!project.isLibrary || !(await project.hasOrWantsLayer("typescript"))) {
48
48
  return explain ? { isSetup: true, reasons } : true;
49
49
  }
50
50
 
@@ -55,8 +55,9 @@ export default {
55
55
  };
56
56
 
57
57
  /**
58
- * This is a very silly way to handle YAML, but 🤷
59
- * will update if this starts getting to be troublesome.
58
+ * String concatenation is a very silly way to handle YAML, but 🤷
59
+ *
60
+ * Will update if this starts getting to be troublesome.
60
61
  */
61
62
  const preamble = `name: CI
62
63
 
@@ -135,8 +136,10 @@ async function transform(project) {
135
136
  content = await addOrUpdateTests(project, content);
136
137
 
137
138
  /**
138
- * If we've still added nothing, we can't actually add the preamble
139
- * because there are no jobs to run (we need at least one job).
139
+ * A workflow needs at least one job.
140
+ *
141
+ * If nothing was added, there is no job to run,
142
+ * so the preamble is not added either.
140
143
  */
141
144
  if (content === "") {
142
145
  return { content, original, didChange: original !== content, outputPath };
@@ -42,8 +42,8 @@ export default {
42
42
  await writeFile(path, wired);
43
43
  }
44
44
 
45
- // the dependency is only useful once the Application actually got
46
- // wired (the codemod leaves unrecognized Application shapes alone)
45
+ // The codemod leaves unrecognized Application shapes alone.
46
+ // The dependency is only useful once the Application is wired.
47
47
  if (!hasInspectorSupport(wired, path)) return;
48
48
 
49
49
  await packageJson.addDevDependencies(await getLatest(deps), project.directory);
@@ -15,7 +15,7 @@ export default {
15
15
 
16
16
  async run(project) {
17
17
  // Publish checks: apps aren't published
18
- if (project.type !== "library") return;
18
+ if (!project.isLibrary) return;
19
19
 
20
20
  await addTsdownConfigProperty(project, "publint", "publint: true");
21
21
  await packageJson.addDevDependencies(await getLatest(deps), project.directory);
@@ -38,7 +38,7 @@ export default {
38
38
  const reasons = [];
39
39
 
40
40
  // Nothing to set up outside libraries, so nothing can be missing
41
- if (project.type !== "library") {
41
+ if (!project.isLibrary) {
42
42
  return explain ? { isSetup: true, reasons } : true;
43
43
  }
44
44
 
@@ -18,9 +18,12 @@ const tsDeps = {
18
18
  };
19
19
 
20
20
  /**
21
- * Libraries don't have the app base's vite stack, so the test build
22
- * brings the pieces the generated configs and test-helper import
23
- * directly. Everything else (babel itself, the embroider machinery)
21
+ * Libraries don't have the app base's vite stack.
22
+ *
23
+ * So the test build brings the pieces that the generated configs
24
+ * and test-helper import directly.
25
+ *
26
+ * Everything else (babel itself, the embroider machinery)
24
27
  * comes with @nullvoxpopuli/ember-vite.
25
28
  */
26
29
  const libraryDeps = {
@@ -41,14 +44,14 @@ const TEST_BABEL_CONFIG = "config/test/babel.config.js";
41
44
  * @param {import('#utils/project.js').Project} project
42
45
  */
43
46
  function depsFor(project) {
44
- return project.type === "library" ? libraryDeps : deps;
47
+ return project.isLibrary ? libraryDeps : deps;
45
48
  }
46
49
 
47
50
  /**
48
51
  * @param {import('#utils/project.js').Project} project
49
52
  */
50
53
  function tsDepsFor(project) {
51
- return project.type === "library" ? libraryTsDeps : tsDeps;
54
+ return project.isLibrary ? libraryTsDeps : tsDeps;
52
55
  }
53
56
 
54
57
  /**
@@ -58,7 +61,7 @@ export default {
58
61
  label: "QUnit",
59
62
 
60
63
  async run(project) {
61
- let isLibrary = project.type === "library";
64
+ let isLibrary = project.isLibrary;
62
65
  let ts = await project.hasOrWantsLayer("typescript");
63
66
 
64
67
  if (isLibrary) {
@@ -79,7 +82,6 @@ export default {
79
82
  project.directory,
80
83
  );
81
84
 
82
- // Add scripts
83
85
  await packageJson.addScripts(
84
86
  {
85
87
  "build:test": "EMBER_ENV=test NODE_ENV=development vite build --mode development",
@@ -151,10 +153,11 @@ export default {
151
153
  };
152
154
 
153
155
  /**
154
- * Tests import the library's source (not its published dist), so the
155
- * `#src/*` subpath must exist before the test files are applied --
156
- * applying rewrites their import extensions by resolving each specifier
157
- * through this mapping.
156
+ * Tests import the library's source, not its published dist.
157
+ *
158
+ * The `#src/*` subpath must exist before the test files are applied:
159
+ * applying rewrites their import extensions
160
+ * by resolving each specifier through this mapping.
158
161
  *
159
162
  * @param {import('#utils/project.js').Project} project
160
163
  */
@@ -166,12 +169,13 @@ async function addSourceImports(project) {
166
169
  }
167
170
 
168
171
  /**
169
- * The test babel config is the qunit layer's own (a root babel config,
170
- * when a library has one, belongs to the publish build), so this layer
171
- * also keeps the config's TypeScript handling in step with the project:
172
- * the shipped config carries the TS plugin, which must not survive in a
173
- * JavaScript project, and must come back when a project adopts the
174
- * typescript layer later.
172
+ * The test babel config is the qunit layer's own.
173
+ * (a root babel config, when a library has one, belongs to the publish build)
174
+ *
175
+ * So this layer keeps the config's TypeScript handling in step with the project:
176
+ * - the shipped config carries the TS plugin
177
+ * - it must not survive in a JavaScript project
178
+ * - it must come back when a project adopts the typescript layer later
175
179
  *
176
180
  * @param {import('#utils/project.js').Project} project
177
181
  * @param {boolean} ts
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import { writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { formatLabel } from "#utils/cli.js";
5
+ import { tagNameFor } from "#bases/minimal-custom-element";
5
6
 
6
7
  /**
7
8
  * @param {import('#utils/project.js').Project} project
@@ -73,13 +74,13 @@ ${project.runPrefix} start
73
74
 
74
75
  ### Building
75
76
 
76
- To build the library:
77
+ To build the extension:
77
78
 
78
79
  \`\`\`sh
79
80
  ${project.runPrefix} build
80
81
  \`\`\`
81
82
 
82
- or
83
+ or, to rebuild on change:
83
84
 
84
85
  \`\`\`sh
85
86
  ${project.runPrefix} build:watch
@@ -113,6 +114,55 @@ ${project.runPrefix} build
113
114
  ${layerDocsMarkdown}`;
114
115
  }
115
116
 
117
+ /**
118
+ * @param {import('#utils/project.js').Project} project
119
+ * @param {string} layerDocsMarkdown
120
+ */
121
+ function customElementReadme(project, layerDocsMarkdown) {
122
+ let tagName = tagNameFor(project.name);
123
+
124
+ return `# ${project.name}
125
+
126
+ An Ember component, packaged as a custom element, created with \`ember.nvp\`.
127
+ The built package contains ember, so the page that uses the element needs nothing else.
128
+
129
+ ## Usage
130
+
131
+ Import the \`register\` entry once. It defines the \`<${tagName}>\` tag.
132
+
133
+ \`\`\`js
134
+ import "${project.name}/register";
135
+ \`\`\`
136
+
137
+ \`\`\`html
138
+ <${tagName} label="Clicks" step="2"></${tagName}>
139
+ \`\`\`
140
+
141
+ To choose your own tag name, import the class and define it yourself:
142
+
143
+ \`\`\`js
144
+ import { CounterElement } from "${project.name}";
145
+
146
+ customElements.define("my-counter", CounterElement);
147
+ \`\`\`
148
+
149
+ ## Getting Started
150
+
151
+ ### Prerequisites
152
+
153
+ - Node.js >= 24
154
+ - ${project.packageManager}
155
+
156
+ ### Development & Building
157
+
158
+ To build the package:
159
+
160
+ \`\`\`sh
161
+ ${project.runPrefix} build
162
+ \`\`\`
163
+ ${layerDocsMarkdown}`;
164
+ }
165
+
116
166
  export default {
117
167
  label: formatLabel("README.md", "generate project documentation"),
118
168
  hint: "project README",
@@ -125,7 +175,6 @@ export default {
125
175
  * @param {import('#utils/project.js').Project} project
126
176
  */
127
177
  async run(project) {
128
- // Collect layer docs
129
178
  const layerDocs = [];
130
179
  for (const layer of project.desires.layers) {
131
180
  if (layer.name === "readme" || !layer.readme) {
@@ -149,7 +198,6 @@ export default {
149
198
  layerDocsMarkdown = `\n## Features & Tooling\n\n${layerDocs.join("\n\n")}\n`;
150
199
  }
151
200
 
152
- // Merge the layers' snippets into the main readme template
153
201
  let content;
154
202
  switch (project.type) {
155
203
  case "extension":
@@ -158,6 +206,9 @@ export default {
158
206
  case "library":
159
207
  content = libraryReadme(project, layerDocsMarkdown);
160
208
  break;
209
+ case "custom-element":
210
+ content = customElementReadme(project, layerDocsMarkdown);
211
+ break;
161
212
  default:
162
213
  content = appReadme(project, layerDocsMarkdown);
163
214
  break;
@@ -14,11 +14,11 @@ export default {
14
14
  return;
15
15
  }
16
16
 
17
- // extensions aren't published to npm either, so they follow the app config
18
- let source =
19
- project.type === "library"
20
- ? join(import.meta.dirname, "files/library.json5")
21
- : join(import.meta.dirname, "files/app.json5");
17
+ // extensions aren't published to npm either,
18
+ // so they follow the app config
19
+ let source = project.isLibrary
20
+ ? join(import.meta.dirname, "files/library.json5")
21
+ : join(import.meta.dirname, "files/app.json5");
22
22
 
23
23
  let destination = join(project.directory, ".github/renovate.json5");
24
24
 
@@ -4,11 +4,13 @@ import { cp } from "node:fs/promises";
4
4
  import { existsSync } from "node:fs";
5
5
  import { hasConfiguredTSBabel, prependPlugin } from "#utils/babel.js";
6
6
  import { getLatest } from "#utils/npm.js";
7
+ import { isLibraryType } from "#utils/project-type.js";
7
8
 
8
9
  const bases = join(import.meta.dirname, "../../bases");
9
10
  const appBase = join(bases, "minimal-app/files");
10
11
  const extensionBase = join(bases, "minimal-extension/files");
11
12
  const libraryBase = join(bases, "minimal-library/files");
13
+ const customElementBase = join(bases, "minimal-custom-element/files");
12
14
 
13
15
  const sharedDeps = {
14
16
  "@glint/ember-tsc": "^1.0.8",
@@ -18,8 +20,8 @@ const sharedDeps = {
18
20
  };
19
21
 
20
22
  const appDeps = {
21
- // Apps strip types via their own babel.config.js; libraries have no babel
22
- // config -- ember() handles type stripping.
23
+ // Apps strip types via their own babel.config.js.
24
+ // Libraries have no babel config: ember() handles type stripping.
23
25
  "@babel/plugin-transform-typescript": "^7.28.5",
24
26
  "@ember/app-tsconfig": "^2.0.0",
25
27
  };
@@ -34,7 +36,7 @@ const libraryDeps = {
34
36
  function depsFor(project) {
35
37
  return {
36
38
  ...sharedDeps,
37
- ...(project.type === "library" ? libraryDeps : appDeps),
39
+ ...(project.isLibrary ? libraryDeps : appDeps),
38
40
  };
39
41
  }
40
42
 
@@ -45,21 +47,19 @@ export default {
45
47
  label: "TypeScript",
46
48
 
47
49
  /**
48
- * Libraries publish declarations, so they are TypeScript unless the
49
- * user opts out.
50
+ * Libraries publish declarations,
51
+ * so they are TypeScript unless the user opts out.
50
52
  *
51
53
  * @param {import('#types').ProjectType} projectType
52
54
  */
53
55
  defaultValue(projectType) {
54
- return projectType === "library";
56
+ return isLibraryType(projectType);
55
57
  },
56
58
 
57
59
  async run(project) {
58
60
  /**
59
- * if jsconfig exists, switch to tsconfig
60
- */
61
- /**
62
- * if tsconfig exists,
61
+ * TODO:
62
+ * - if jsconfig exists, switch to tsconfig
63
63
  */
64
64
  await addTSConfig(project);
65
65
  await updatePackageJson(project);
@@ -87,8 +87,8 @@ export default {
87
87
  reasons.push("tsconfig.json is missing");
88
88
  }
89
89
 
90
- // Only projects with their own babel config need the TS plugin in it;
91
- // without one (libraries), ember() strips types.
90
+ // Only projects with their own babel config need the TS plugin in it.
91
+ // Without one (libraries), ember() strips types.
92
92
  if (project.hasFile("babel.config.js") && !(await hasConfiguredTSBabel(project))) {
93
93
  if (!explain) return false;
94
94
 
@@ -149,6 +149,8 @@ async function updatePackageJson(project) {
149
149
  }
150
150
 
151
151
  /**
152
+ * Copies the base's tsconfig, unless the project already has one.
153
+ *
152
154
  * @param {import('#utils/project.js').Project} project
153
155
  */
154
156
  async function addTSConfig(project) {
@@ -168,6 +170,11 @@ async function addTSConfig(project) {
168
170
 
169
171
  if (project.type === "library") {
170
172
  await cp(join(libraryBase, "tsconfig.json"), project.path("tsconfig.json"));
173
+ return;
174
+ }
175
+
176
+ if (project.type === "custom-element") {
177
+ await cp(join(customElementBase, "tsconfig.json"), project.path("tsconfig.json"));
171
178
  }
172
179
  }
173
180
 
@@ -176,7 +183,7 @@ async function addTSConfig(project) {
176
183
  */
177
184
  async function updateBabelConfig(project) {
178
185
  if (!project.hasFile("babel.config.js")) {
179
- // No babel config to patch (libraries): ember() strips types.
186
+ // Nothing to patch (libraries): ember() strips types.
180
187
  return;
181
188
  }
182
189
 
@@ -13,9 +13,13 @@ const deps = {
13
13
  webdriverio: "^9.29.1",
14
14
  };
15
15
 
16
- // Apps already get these from their base. The babel plugins are resolved
17
- // by name from the project by @nullvoxpopuli/ember-vite's no-config babel
18
- // fallback (decorator-transforms is already a base dependency).
16
+ /**
17
+ * Apps already get these from their base.
18
+ *
19
+ * The babel plugins are resolved by name from the project,
20
+ * by @nullvoxpopuli/ember-vite's no-config babel fallback.
21
+ * (decorator-transforms is already a base dependency)
22
+ */
19
23
  const libraryDeps = {
20
24
  "@babel/plugin-transform-typescript": "^7.28.10",
21
25
  "@nullvoxpopuli/ember-vite": "workspace:*",
@@ -35,7 +39,7 @@ const scripts = {
35
39
  function depsFor(project) {
36
40
  return {
37
41
  ...deps,
38
- ...(project.type === "library" ? libraryDeps : {}),
42
+ ...(project.isLibrary ? libraryDeps : {}),
39
43
  };
40
44
  }
41
45
 
@@ -80,8 +80,9 @@ export async function prependPlugin(project, plugin, configPath = "babel.config.
80
80
  }
81
81
 
82
82
  /**
83
- * The plugins value may be the array itself, or the array with
84
- * methods chained off of it: `[ ... ].filter(Boolean)`
83
+ * The plugins value may be:
84
+ * - the array itself
85
+ * - the array with methods chained off of it: `[ ... ].filter(Boolean)`
85
86
  *
86
87
  * @param {any} node
87
88
  * @returns {any | undefined} the underlying ArrayExpression
@@ -1,8 +1,8 @@
1
1
  /**
2
+ * Takes a substring of a string based on byte offsets.
3
+ *
2
4
  * from https://github.com/ember-cli/ember-cli/blob/8ea5b2e22d37805f5749a4917e0ef25fac3c9cda/lib/models/blueprint.js#L1789
3
5
  *
4
- * Takes a substring of a string based on byte offsets.
5
-
6
6
  * @param {string} value : The input string.
7
7
  * @param {number} start : The byte index of the substring start.
8
8
  * @param {number} end : The byte index of the substring end.
package/src/utils/cli.js CHANGED
@@ -7,12 +7,12 @@ export function isInteractive() {
7
7
  }
8
8
 
9
9
  /**
10
- * I would like to have selecs look more like a table,
10
+ * I would like to have selects look more like a table,
11
11
  * but @clack/prompts makes this a bit hard.
12
12
  *
13
- * I tried using padding, but then when it prints your selection,
14
- * I don't think there is a way to format just that, so
15
- * there are a bunch of goofy spacings in the labels.
13
+ * I tried using padding.
14
+ * But when it prints your selection, I don't think there is a way to format just that.
15
+ * So there are a bunch of goofy spacings in the labels.
16
16
  */
17
17
  const PADDING = 10;
18
18