ember.nvp 0.6.0 → 1.0.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 (85) hide show
  1. package/.github/workflows/ci.yml +17 -2
  2. package/.release-plan.json +29 -5
  3. package/CHANGELOG.md +73 -0
  4. package/package.json +3 -2
  5. package/packages/build-tooling-utils/README.md +37 -0
  6. package/packages/build-tooling-utils/index.ts +2 -0
  7. package/packages/build-tooling-utils/package.json +42 -0
  8. package/packages/build-tooling-utils/rolldown.config.js +18 -0
  9. package/packages/build-tooling-utils/src/extensions.ts +18 -0
  10. package/packages/build-tooling-utils/src/maybe-babel.ts +146 -0
  11. package/packages/rolldown/README.md +120 -0
  12. package/packages/rolldown/index.ts +60 -0
  13. package/packages/rolldown/package.json +59 -0
  14. package/packages/rolldown/rolldown.config.js +21 -0
  15. package/packages/rolldown/src/babel.ts +136 -0
  16. package/packages/rolldown/src/config.ts +50 -0
  17. package/packages/rolldown/src/externals.ts +83 -0
  18. package/packages/rolldown/src/isolated-declarations.ts +65 -0
  19. package/packages/rolldown/src/transform.ts +162 -0
  20. package/packages/vite/README.md +7 -21
  21. package/packages/vite/index.ts +1 -1
  22. package/packages/vite/package.json +4 -6
  23. package/packages/vite/src/ember.ts +129 -15
  24. package/packages/vite/src/test-html.ts +86 -0
  25. package/src/bases/minimal-app/files/app/router.ts +1 -0
  26. package/src/bases/minimal-app/files/tsconfig.json +1 -0
  27. package/src/bases/minimal-app/index.js +2 -2
  28. package/src/bases/minimal-library/files/README.md +41 -0
  29. package/src/bases/minimal-library/files/package.json +49 -0
  30. package/src/bases/minimal-library/files/tsconfig.json +10 -0
  31. package/src/bases/minimal-library/files/tsdown.config.js +7 -0
  32. package/src/bases/minimal-library/index.js +158 -2
  33. package/src/cli/index.js +21 -2
  34. package/src/cli/questions/layers.js +5 -2
  35. package/src/cli/questions/project-type.js +0 -5
  36. package/src/layers/are-the-types-wrong/index.js +74 -0
  37. package/src/layers/git/files/gitignore +14 -1
  38. package/src/layers/git/index.js +6 -5
  39. package/src/layers/github-actions/index.js +12 -8
  40. package/src/layers/index.js +1 -1
  41. package/src/layers/inspector-support/index.js +101 -0
  42. package/src/layers/prettier/files/.prettierignore +4 -0
  43. package/src/layers/publint/index.js +68 -0
  44. package/src/layers/qunit/files/tests/unit/macros-test.ts +2 -2
  45. package/src/layers/qunit/index.js +107 -7
  46. package/src/layers/qunit/library-files/config/test/babel.config.js +41 -0
  47. package/src/layers/qunit/library-files/config/test/testem.cjs +26 -0
  48. package/src/layers/qunit/library-files/tests/test-helper.ts +36 -0
  49. package/src/layers/qunit/library-files/tests/unit/.gitkeep +0 -0
  50. package/src/layers/qunit/library-files/vite.config.mjs +6 -0
  51. package/src/layers/typescript/index.js +43 -7
  52. package/src/layers/vitest/files/vitest.config.mjs +11 -11
  53. package/src/layers/vitest/index.js +70 -32
  54. package/src/utils/babel.js +9 -6
  55. package/src/utils/fs.js +44 -3
  56. package/src/utils/inspector-support.js +196 -0
  57. package/src/utils/npm.js +21 -7
  58. package/src/utils/remove-types.js +5 -0
  59. package/src/utils/rewrite-imports.js +157 -0
  60. package/src/utils/tsdown-config.js +85 -0
  61. package/src/utils/types.ts +5 -1
  62. package/test/ember.nvp/base-library.test.ts +236 -0
  63. package/test/ember.nvp/generator-replace-or-update.test.ts +2 -3
  64. package/test/ember.nvp/inspector-support.test.ts +179 -0
  65. package/test/ember.nvp/layer-inspector-support.test.ts +69 -0
  66. package/test/ember.nvp/layer-publish-checks.test.ts +109 -0
  67. package/test/ember.nvp/layer-qunit.test.ts +381 -16
  68. package/test/ember.nvp/layer-vitest.test.ts +250 -0
  69. package/test/ember.nvp/layers.test.ts +16 -0
  70. package/test/ember.nvp/library-src-fixtures.ts +81 -0
  71. package/test/ember.nvp/permutations/app-1.test.ts +3 -0
  72. package/test/ember.nvp/permutations/app-2.test.ts +3 -0
  73. package/test/ember.nvp/permutations/library-1.test.ts +3 -0
  74. package/test/ember.nvp/permutations/library-2.test.ts +3 -0
  75. package/test/ember.nvp/permutations/shared.ts +239 -0
  76. package/test/ember.nvp/rewrite-imports.test.ts +235 -0
  77. package/test/test-helpers/permutate.test.ts +17 -0
  78. package/test/test-helpers.ts +46 -8
  79. package/tsconfig.json +1 -0
  80. package/packages/vite/src/maybe-babel.ts +0 -219
  81. package/src/layers/qunit/files/run-tests.mjs +0 -30
  82. package/src/layers/vitest/files/tests/test-helper.ts +0 -5
  83. package/test/ember.nvp/permutations.test.ts +0 -200
  84. /package/src/{layers/vitest/files/tests/integration/.gitkeep → bases/minimal-library/files/src/index.ts} +0 -0
  85. /package/src/layers/{vitest/files/tests/unit → qunit/library-files/tests/rendering}/.gitkeep +0 -0
@@ -1,9 +1,12 @@
1
+ import { buildMacros } from "@embroider/macros/babel";
1
2
  import { ember as embroiderEmber, extensions, resolver, templateTag } from "@embroider/vite";
2
3
  import { existsSync } from "node:fs";
3
4
  import { join, resolve } from "node:path";
4
5
  import type { ConfigEnv, ResolvedConfig } from "vite";
5
6
 
6
- import { maybeBabel } from "./maybe-babel.ts";
7
+ import { maybeBabel } from "@nullvoxpopuli/ember-build-tooling-utils";
8
+
9
+ import { testHtml } from "./test-html.ts";
7
10
 
8
11
  const emberConfig = embroiderEmber()[3]!;
9
12
 
@@ -12,6 +15,92 @@ function absolutePath(relativePath: string) {
12
15
  return join(cwd, relativePath);
13
16
  }
14
17
 
18
+ /**
19
+ * Everything a project without its own babel config needs:
20
+ *
21
+ * - TypeScript stripping
22
+ * - template compilation (wire format: vite serves/bundles for the
23
+ * project's own ember-source, so the final compile happens here)
24
+ * - build macros, evaluated at build time
25
+ * - decorator-transforms, with its runtime left as a bare specifier so it
26
+ * resolves from the project (which depends on `decorator-transforms`)
27
+ *
28
+ * The plugins are named, not imported: babel resolves them from the
29
+ * project, which carries them as its own dependencies.
30
+ */
31
+ function defaultBabelPlugins() {
32
+ const macros = buildMacros();
33
+
34
+ return [
35
+ [
36
+ "@babel/plugin-transform-typescript",
37
+ {
38
+ allExtensions: true,
39
+ onlyRemoveTypeImports: true,
40
+ allowDeclareFields: true,
41
+ },
42
+ ],
43
+ ["babel-plugin-ember-template-compilation", { transforms: [...macros.templateMacros] }],
44
+ ["module:decorator-transforms", { runtime: { import: "decorator-transforms/runtime-esm" } }],
45
+ ...macros.babelMacros,
46
+ ];
47
+ }
48
+
49
+ type BabelOptions =
50
+ | { configFile: string }
51
+ | {
52
+ configFile: false;
53
+ babelrc: false;
54
+ babelHelpers: "inline";
55
+ plugins: ReturnType<typeof defaultBabelPlugins>;
56
+ };
57
+
58
+ /**
59
+ * The project's babel config wins when it exists -- `babel.configFile`
60
+ * selects which one (e.g. a test-only `config/test/babel.config.js`).
61
+ * Without one (libraries), `defaultBabelPlugins` covers TypeScript,
62
+ * templates, macros, and decorators, so no config file is required --
63
+ * helpers are inlined because projects without a babel config don't
64
+ * carry `@babel/runtime`.
65
+ */
66
+ function babelOptions(nvpConfig: Config): BabelOptions {
67
+ const configFile = resolve(
68
+ join(process.cwd(), nvpConfig.babel?.configFile ?? "./babel.config.js"),
69
+ );
70
+
71
+ if (existsSync(configFile)) {
72
+ return { configFile };
73
+ }
74
+
75
+ return {
76
+ configFile: false,
77
+ babelrc: false,
78
+ babelHelpers: "inline",
79
+ plugins: defaultBabelPlugins(),
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Additional opt-ins beyond maybeBabel's built-in list.
85
+ *
86
+ * These libraries ship code that still needs babel (ember-concurrency's
87
+ * decorators, ember-scoped-css transforms, ember-intl's formatMessage macro),
88
+ * and the `initializeRuntimeMacrosConfig` runtime call must be compiled away.
89
+ */
90
+ function babelFilter(nvpConfig: Config) {
91
+ return {
92
+ include: {
93
+ imports: [
94
+ "ember-concurrency",
95
+ "ember-scoped-css",
96
+ "ember-intl/helpers/format-message",
97
+ ...(nvpConfig.babel?.include?.whenImporting ?? []),
98
+ ],
99
+ code: [/\bintl\.formatMessage\b/, "initializeRuntimeMacrosConfig"] as (string | RegExp)[],
100
+ },
101
+ };
102
+ }
103
+
15
104
  interface Config {
16
105
  babel?: {
17
106
  /**
@@ -20,6 +109,16 @@ interface Config {
20
109
  */
21
110
  parallel?: false;
22
111
 
112
+ /**
113
+ * which babel config file to use for this build, relative to the
114
+ * project root. defaults to "./babel.config.js".
115
+ *
116
+ * useful when the root babel config belongs to another pipeline
117
+ * (e.g. a library's publish build) and the vite build needs its own,
118
+ * such as "./config/test/babel.config.js".
119
+ */
120
+ configFile?: string;
121
+
23
122
  /**
24
123
  * optional way to configure when babel is activateed.
25
124
  * by default, all transforming is oxc, except when babel is needed
@@ -51,8 +150,8 @@ interface Config {
51
150
  }
52
151
 
53
152
  export function ember(nvpConfig: Config = {}) {
54
- const babelConfigFile = resolve(join(process.cwd(), "./babel.config.js"));
55
- const defaultEnv = { mode: process.env.NODE_ENV || "development" };
153
+ const babel = babelOptions(nvpConfig);
154
+ const filter = babelFilter(nvpConfig);
56
155
 
57
156
  /*
58
157
  * Plugins must be returned as top-level array entries — NOT pushed into
@@ -98,16 +197,21 @@ export function ember(nvpConfig: Config = {}) {
98
197
  // Libraries will have precompileTemplate and macros, etc,
99
198
  // and we need to compile that away using this app's
100
199
  // template compiler
101
- maybeBabel({ env, parallel: nvpConfig.babel?.parallel, configFile: babelConfigFile }),
200
+ maybeBabel({
201
+ ...babel,
202
+ parallel: nvpConfig.babel?.parallel,
203
+ filter,
204
+ }),
102
205
  ];
103
206
  },
104
207
  },
105
208
  resolver({ rolldown: true }),
106
209
  templateTag(),
210
+ testHtml(),
107
211
  maybeBabel({
108
- env: defaultEnv,
212
+ ...babel,
109
213
  parallel: nvpConfig.babel?.parallel,
110
- configFile: babelConfigFile,
214
+ filter,
111
215
  }),
112
216
  ];
113
217
  }
@@ -146,13 +250,10 @@ function dev(viteConfig: ResolvedConfig, nvpConfig: Config) {
146
250
  viteConfig.build.rolldownOptions.input ||= {};
147
251
  viteConfig.build.rolldownOptions.output ||= {};
148
252
 
149
- Object.assign(viteConfig.build.rolldownOptions.input, {
150
- main: absolutePath("./index.html"),
151
- });
152
-
153
- if (existsSync("./tests.html")) {
253
+ // libraries don't, by default, have a "demo app" entrypoint
254
+ if (existsSync("./index.html")) {
154
255
  Object.assign(viteConfig.build.rolldownOptions.input, {
155
- tests: absolutePath("./tests.html"),
256
+ main: absolutePath("./index.html"),
156
257
  });
157
258
  }
158
259
 
@@ -160,6 +261,16 @@ function dev(viteConfig: ResolvedConfig, nvpConfig: Config) {
160
261
  Object.assign(viteConfig.build.rolldownOptions.input, {
161
262
  tests: absolutePath("./tests/index.html"),
162
263
  });
264
+ } else if (existsSync("./tests.html")) {
265
+ Object.assign(viteConfig.build.rolldownOptions.input, {
266
+ tests: absolutePath("./tests.html"),
267
+ });
268
+ } else if (existsSync("./tests")) {
269
+ // no html of its own: the default-test-html plugin answers for
270
+ // tests/index.html
271
+ Object.assign(viteConfig.build.rolldownOptions.input, {
272
+ tests: absolutePath("./tests/index.html"),
273
+ });
163
274
  }
164
275
 
165
276
  Object.assign(viteConfig.build.rolldownOptions.output, {
@@ -203,9 +314,12 @@ function prod(viteConfig: ResolvedConfig, nvpConfig: Config) {
203
314
  viteConfig.build.rolldownOptions.optimization ||= {};
204
315
  viteConfig.build.rolldownOptions.output ||= {};
205
316
 
206
- Object.assign(viteConfig.build.rolldownOptions.input, {
207
- main: absolutePath("./index.html"),
208
- });
317
+ // libraries don't, by default, have a "demo app" entrypoint
318
+ if (existsSync("./index.html")) {
319
+ Object.assign(viteConfig.build.rolldownOptions.input, {
320
+ main: absolutePath("./index.html"),
321
+ });
322
+ }
209
323
 
210
324
  Object.assign(viteConfig.build.rolldownOptions.optimization, {
211
325
  inlineConst: {
@@ -0,0 +1,86 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { ViteDevServer } from "vite";
4
+
5
+ /**
6
+ * Everything a QUnit + testem run needs, with no project-specific
7
+ * references: root-relative specifiers keep it independent of where the
8
+ * project keeps its files, and the globs pick up tests/** as well as
9
+ * co-located `*-test` modules under src/**.
10
+ */
11
+ const DEFAULT_TEST_HTML = `<!doctype html>
12
+ <html>
13
+ <head>
14
+ <meta charset="utf-8" />
15
+ <title>Tests</title>
16
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
17
+ </head>
18
+ <body>
19
+ <div id="qunit"></div>
20
+ <div id="qunit-fixture">
21
+ <div id="ember-testing-container">
22
+ <div id="ember-testing"></div>
23
+ </div>
24
+ </div>
25
+
26
+ <script src="/testem.js" integrity="" data-embroider-ignore></script>
27
+ <script type="module">
28
+ import "ember-testing";
29
+ </script>
30
+
31
+ <script type="module">
32
+ import { start } from "/tests/test-helper";
33
+ import.meta.glob("/tests/**/*.{js,ts,gjs,gts}", { eager: true });
34
+ import.meta.glob("/src/**/*-test.{js,ts,gjs,gts}", { eager: true });
35
+ start();
36
+ </script>
37
+ </body>
38
+ </html>
39
+ `;
40
+
41
+ /**
42
+ * Lets a project run its tests without maintaining a tests/index.html:
43
+ * whenever that file is absent, this plugin answers for it -- `load`
44
+ * supplies the html to the build (so `tests/index.html` still lands in
45
+ * dist for testem), and the dev-server middleware serves it (through
46
+ * `transformIndexHtml`, so the html goes through the same pipeline as an
47
+ * on-disk file).
48
+ *
49
+ * A tests/index.html on disk always wins; every hook here defers to it.
50
+ */
51
+ export function testHtml() {
52
+ const htmlPath = join(process.cwd(), "tests", "index.html");
53
+
54
+ return {
55
+ name: "ember:default-test-html",
56
+
57
+ resolveId(id: string) {
58
+ if (id === htmlPath && !existsSync(htmlPath)) {
59
+ return id;
60
+ }
61
+ },
62
+
63
+ load(id: string) {
64
+ if (id === htmlPath && !existsSync(htmlPath)) {
65
+ return DEFAULT_TEST_HTML;
66
+ }
67
+ },
68
+
69
+ configureServer(server: ViteDevServer) {
70
+ server.middlewares.use(async (req, res, next) => {
71
+ if (existsSync(htmlPath)) return next();
72
+
73
+ const url = (req.url ?? "").split("?")[0];
74
+
75
+ if (url !== "/tests" && url !== "/tests/" && url !== "/tests/index.html") {
76
+ return next();
77
+ }
78
+
79
+ const html = await server.transformIndexHtml("/tests/index.html", DEFAULT_TEST_HTML);
80
+
81
+ res.setHeader("Content-Type", "text/html");
82
+ res.end(html);
83
+ });
84
+ },
85
+ };
86
+ }
@@ -1,4 +1,5 @@
1
1
  import EmbroiderRouter from "@embroider/router";
2
+
2
3
  import config from "#config";
3
4
 
4
5
  export default class Router extends EmbroiderRouter {
@@ -2,6 +2,7 @@
2
2
  "extends": "@ember/app-tsconfig",
3
3
  "compilerOptions": {
4
4
  "allowJs": true,
5
+ "lib": ["esnext", "dom", "dom.iterable"],
5
6
  "types": [
6
7
  "ember-source/types",
7
8
  "@embroider/core/virtual",
@@ -1,6 +1,6 @@
1
1
  import { packageJson, js } from "ember-apply";
2
+ import { join } from "node:path";
2
3
  import { getLatest } from "#utils/npm.js";
3
- import { fileURLToPath } from "node:url";
4
4
  import { applyFolderTo } from "#utils/fs.js";
5
5
  import { removeConfiguredPlugin } from "#utils/babel.js";
6
6
 
@@ -49,7 +49,7 @@ async function updateBabelConfig(project) {
49
49
  * @param {import('#utils/project.js').Project} project
50
50
  */
51
51
  async function applyFiles(project) {
52
- let filePath = fileURLToPath(new URL("files", import.meta.url));
52
+ let filePath = join(import.meta.dirname, "files");
53
53
 
54
54
  await applyFolderTo(filePath, project);
55
55
  }
@@ -0,0 +1,41 @@
1
+ # minimal-library
2
+
3
+ An Ember v2 library (addon) built with
4
+ [`@nullvoxpopuli/ember-rolldown`](https://github.com/NullVoxPopuli/ember.nvp/tree/main/packages/rolldown)
5
+ and [tsdown](https://tsdown.dev/).
6
+
7
+ ## Development
8
+
9
+ ```bash
10
+ pnpm install
11
+ ```
12
+
13
+ Build the distributable (JS + `.d.ts` into `dist/`):
14
+
15
+ ```bash
16
+ pnpm build
17
+ ```
18
+
19
+ Rebuild on change while developing:
20
+
21
+ ```bash
22
+ pnpm start
23
+ ```
24
+
25
+ ## Structure
26
+
27
+ - `src/` — your library source. Author components in `.gts`/`.gjs`
28
+ (template-tag) and plain modules in `.ts`/`.js`.
29
+ - `src/index.ts` — the public entry point. Everything a consumer can import
30
+ must be re-exported from here (or added as an entry in `tsdown.config.js`).
31
+ - `dist/` — the built output that gets published (git-ignored).
32
+
33
+ Declarations are emitted with
34
+ [isolated declarations](https://www.typescriptlang.org/tsconfig/#isolatedDeclarations),
35
+ so every exported value needs an explicit type annotation — in particular,
36
+ annotate exported template-only components with `TOC<...>`.
37
+
38
+ ## Publishing
39
+
40
+ `dist/` is built automatically on `prepack`, so `npm publish` ships the
41
+ compiled output plus your `src/`.
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "minimal-library",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "description": "Your Ember library's description here",
6
+ "license": "MIT",
7
+ "author": "",
8
+ "type": "module",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./*": {
15
+ "types": "./dist/*.d.ts",
16
+ "default": "./dist/*.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "src"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsdown",
25
+ "prepack": "tsdown",
26
+ "start": "tsdown --watch"
27
+ },
28
+ "dependencies": {
29
+ "decorator-transforms": "^2.3.2"
30
+ },
31
+ "peerDependencies": {
32
+ "@glimmer/component": "^2.0.0",
33
+ "ember-source": ">= 6.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@ember/library-tsconfig": "^2.0.0",
37
+ "@glimmer/component": "^2.1.1",
38
+ "@nullvoxpopuli/ember-rolldown": "workspace:*",
39
+ "ember-source": "~7.0.0",
40
+ "tsdown": "^0.22.7",
41
+ "typescript": "^6.0.3"
42
+ },
43
+ "engines": {
44
+ "node": ">= 24"
45
+ },
46
+ "ember": {
47
+ "edition": "octane"
48
+ }
49
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "@ember/library-tsconfig",
3
+ "include": ["src"],
4
+ "compilerOptions": {
5
+ "rootDir": "./src",
6
+ "lib": ["esnext", "dom", "dom.iterable"],
7
+ "isolatedDeclarations": true,
8
+ "types": ["ember-source/types", "@glint/ember-tsc/types"]
9
+ }
10
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "tsdown";
2
+ import { ember } from "@nullvoxpopuli/ember-rolldown";
3
+
4
+ export default defineConfig({
5
+ entry: ["./src/index.ts"],
6
+ plugins: [ember()],
7
+ });
@@ -1,10 +1,166 @@
1
+ import { packageJson } from "ember-apply";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { getLatest } from "#utils/npm.js";
5
+ import { applyFolderTo } from "#utils/fs.js";
6
+ import { removeConfiguredPlugin } from "#utils/babel.js";
7
+
8
+ /**
9
+ * Minimal Library Base
10
+ *
11
+ * Provides the absolute bare minimum for an Ember v2 library (addon):
12
+ * - type: "module" in package.json
13
+ * - built with tsdown + @nullvoxpopuli/ember-rolldown
14
+ * - a sample `<template>` component and a plain module
15
+ * - No babel.config.js: ember() covers TS stripping, template compilation,
16
+ * and decorators
17
+ * - No testing framework
18
+ * - No linting or formatting
19
+ *
20
+ * Testing, linting, formatting, etc. are opt-in via layers.
21
+ */
1
22
  export default {
2
23
  label: "Minimal Library Base",
3
- description: "Bare minimum library structure",
24
+ description: "Bare minimum Ember v2 library (addon) structure",
4
25
 
5
26
  /**
27
+ * 1. Apply files
28
+ * a. Remove TS if needed
29
+ * 2. Update name(s)
30
+ * 3. Make publishable
31
+ * 4. Remove TS deps/files/config if needed
32
+ * 5. Upgrade in-range dependencies
33
+ * 6. Update an existing babel config, if any
6
34
  *
7
35
  * @param {import('#utils/project.js').Project} project
8
36
  */
9
- async run(project) {},
37
+ async run(project) {
38
+ await applyFiles(project);
39
+ await updateName(project);
40
+ await makePublishable(project);
41
+ await makeJavaScript(project);
42
+ await upgradeDependencies(project);
43
+ await updateBabelConfig(project);
44
+ },
10
45
  };
46
+
47
+ /**
48
+ * @param {import('#utils/project.js').Project} project
49
+ */
50
+ async function applyFiles(project) {
51
+ let filePath = join(import.meta.dirname, "files");
52
+
53
+ await applyFolderTo(filePath, project);
54
+ }
55
+
56
+ /**
57
+ * Operates on known files where the name matters
58
+ *
59
+ * @param {import('#utils/project.js').Project} project
60
+ */
61
+ async function updateName(project) {
62
+ await packageJson.modify((json) => {
63
+ json.name = project.desires.name;
64
+ }, project.directory);
65
+ }
66
+
67
+ /**
68
+ * The template's package.json is `private: true` so it can never be
69
+ * published from this repo; the generated library exists to be published,
70
+ * so the flag is removed entirely.
71
+ *
72
+ * @param {import('#utils/project.js').Project} project
73
+ */
74
+ async function makePublishable(project) {
75
+ await packageJson.modify((json) => {
76
+ delete json.private;
77
+ }, project.directory);
78
+ }
79
+
80
+ /**
81
+ * When the project is JavaScript (no typescript layer), remove the TS
82
+ * toolchain, drop the tsconfig, and point the build at the emitted `.js`
83
+ * entry (declarations can't be produced without types).
84
+ *
85
+ * @param {import('#utils/project.js').Project} project
86
+ */
87
+ async function makeJavaScript(project) {
88
+ if (await project.hasOrWantsLayer("typescript")) return;
89
+
90
+ await packageJson.removeDevDependencies(
91
+ ["@babel/plugin-transform-typescript", "@ember/library-tsconfig", "typescript"],
92
+ project.directory,
93
+ );
94
+
95
+ await project.removeFile("tsconfig.json");
96
+
97
+ await removeTypesExports(project);
98
+ await pointBuildAtJavaScript(project);
99
+ }
100
+
101
+ /**
102
+ * No declarations are emitted for a JavaScript library, so the `types`
103
+ * export conditions would point at files that never exist.
104
+ *
105
+ * @param {import('#utils/project.js').Project} project
106
+ */
107
+ async function removeTypesExports(project) {
108
+ await packageJson.modify((json) => {
109
+ for (let condition of Object.values(json.exports ?? {})) {
110
+ if (condition && typeof condition === "object") {
111
+ delete condition.types;
112
+ }
113
+ }
114
+ }, project.directory);
115
+ }
116
+
117
+ /**
118
+ * Rewrites the tsdown config so it builds the JavaScript entry and stops
119
+ * emitting declarations (defineConfig defaults to `dts: true`).
120
+ *
121
+ * @param {import('#utils/project.js').Project} project
122
+ */
123
+ async function pointBuildAtJavaScript(project) {
124
+ let configPath = project.path("tsdown.config.js");
125
+ let contents = await readFile(configPath, "utf-8");
126
+
127
+ contents = contents.replace(
128
+ `entry: ["./src/index.ts"],`,
129
+ `entry: ["./src/index.js"],\n dts: false,`,
130
+ );
131
+
132
+ await writeFile(configPath, contents);
133
+ }
134
+
135
+ /**
136
+ * Bumps in-range only.
137
+ * Majors will need to go through PR to this repo.
138
+ *
139
+ * @param {import('#utils/project.js').Project} project
140
+ */
141
+ async function upgradeDependencies(project) {
142
+ let existing = await packageJson.read(project.directory);
143
+
144
+ await packageJson.modify(async (json) => {
145
+ if (json.dependencies) {
146
+ Object.assign(json.dependencies, await getLatest(existing.dependencies));
147
+ }
148
+ if (json.devDependencies) {
149
+ Object.assign(json.devDependencies, await getLatest(existing.devDependencies));
150
+ }
151
+ }, project.directory);
152
+ }
153
+
154
+ /**
155
+ * The base doesn't emit a babel.config.js, but this can run over an
156
+ * existing project that has one -- and a JavaScript project's config
157
+ * must not reference the TS plugin.
158
+ *
159
+ * @param {import('#utils/project.js').Project} project
160
+ */
161
+ async function updateBabelConfig(project) {
162
+ if (await project.hasOrWantsLayer("typescript")) return;
163
+ if (!project.hasFile("babel.config.js")) return;
164
+
165
+ await removeConfiguredPlugin(project, "@babel/plugin-transform-typescript");
166
+ }
package/src/cli/index.js CHANGED
@@ -10,7 +10,8 @@ import { askPath } from "./questions/path.js";
10
10
  import { askIfOK } from "./questions/ok.js";
11
11
  import { askToWrite } from "./questions/write.js";
12
12
  import { styleText } from "node:util";
13
- import { rm } from "node:fs/promises";
13
+ import { readFile, rm } from "node:fs/promises";
14
+ import { join } from "node:path";
14
15
  import { Project } from "#utils/project.js";
15
16
  import { Stage } from "#utils/stage.js";
16
17
  import { askReplaceOrUpdate } from "./questions/replace-or-update.js";
@@ -31,7 +32,7 @@ async function main() {
31
32
  const projectPath = await askPath(projectName);
32
33
  const replaceOrUpdate = await askReplaceOrUpdate(projectPath);
33
34
  const projectType = await askProjectType();
34
- const selectedLayers = await askLayers();
35
+ const selectedLayers = await askLayers(projectType);
35
36
  const packageManager = await askPackageManager();
36
37
 
37
38
  await askIfOK();
@@ -120,9 +121,27 @@ async function main() {
120
121
 
121
122
  p.log.success(`Applied ${toApply.length} change${toApply.length === 1 ? "" : "s"}`);
122
123
 
124
+ /**
125
+ * 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
+ */
130
+ let lintFixStep = "";
131
+ try {
132
+ const manifest = JSON.parse(await readFile(join(projectPath, "package.json"), "utf-8"));
133
+
134
+ if (manifest.scripts?.["lint:fix"]) {
135
+ lintFixStep = `${packageManager} ${packageManager === "npm" ? "run " : ""}lint:fix\n`;
136
+ }
137
+ } catch {
138
+ // no readable package.json -- skip the suggestion
139
+ }
140
+
123
141
  p.note(
124
142
  `cd ${projectName}\n` +
125
143
  `${packageManager} install\n` +
144
+ lintFixStep +
126
145
  `${packageManager} ${packageManager === "npm" ? "run " : ""}start`,
127
146
  "Next steps",
128
147
  );
@@ -3,7 +3,10 @@ import { discoverLayers } from "#layers";
3
3
 
4
4
  import * as p from "@clack/prompts";
5
5
 
6
- export async function askLayers() {
6
+ /**
7
+ * @param {import('#types').ProjectType} projectType
8
+ */
9
+ export async function askLayers(projectType) {
7
10
  const optionalLayers = (await discoverLayers()).filter(
8
11
  (layer) => typeof layer.run === "function",
9
12
  );
@@ -34,7 +37,7 @@ export async function askLayers() {
34
37
  const defaultValues = (
35
38
  await Promise.all(
36
39
  optionalLayers.map(async (layer) => {
37
- let result = await layer.defaultValue?.();
40
+ let result = await layer.defaultValue?.(projectType);
38
41
 
39
42
  return result && layer.name;
40
43
  }),
@@ -41,11 +41,6 @@ export async function askProjectType() {
41
41
  ],
42
42
  });
43
43
 
44
- if (answer === "library") {
45
- p.cancel("Not implemented yet, apologies!");
46
- return process.exit(0);
47
- }
48
-
49
44
  if (p.isCancel(answer)) {
50
45
  p.cancel("Operation cancelled");
51
46
  return process.exit(0);