From 1124f06a19c557eaf6fd9b2b0b8f0cdbd75a7a3f Mon Sep 17 00:00:00 2001 From: Simon Ihmig Date: Mon, 9 Dec 2024 18:40:36 +0100 Subject: [PATCH] Add rollup declarations plugin This invokes glint as a rollup plugin, and contains a workaround for broken gts imports, see https://github.com/typed-ember/glint/issues/628 --- packages/addon-dev/jest.config.js | 6 + packages/addon-dev/package.json | 7 +- packages/addon-dev/src/rollup-declarations.ts | 50 ++++++++ packages/addon-dev/src/rollup.ts | 5 + packages/addon-dev/tests/declarations.test.ts | 112 ++++++++++++++++++ pnpm-lock.yaml | 96 ++++++++++++++- 6 files changed, 272 insertions(+), 4 deletions(-) create mode 100644 packages/addon-dev/jest.config.js create mode 100644 packages/addon-dev/src/rollup-declarations.ts create mode 100644 packages/addon-dev/tests/declarations.test.ts diff --git a/packages/addon-dev/jest.config.js b/packages/addon-dev/jest.config.js new file mode 100644 index 000000000..7f4f45dca --- /dev/null +++ b/packages/addon-dev/jest.config.js @@ -0,0 +1,6 @@ +module.exports = { + testEnvironment: 'node', + testMatch: [ + '/tests/**/*.test.js', + ], +}; diff --git a/packages/addon-dev/package.json b/packages/addon-dev/package.json index 310b3f209..b08385c3a 100644 --- a/packages/addon-dev/package.json +++ b/packages/addon-dev/package.json @@ -37,6 +37,7 @@ "@embroider/core": "workspace:^", "@rollup/pluginutils": "^4.1.1", "content-tag": "^3.0.0", + "execa": "^5.1.1", "fs-extra": "^10.0.0", "minimatch": "^3.0.4", "rollup-plugin-copy-assets": "^2.0.3", @@ -46,11 +47,15 @@ "devDependencies": { "@embroider/test-support": "workspace:*", "@glimmer/syntax": "^0.84.2", + "@glint/core": "^1.5.0", + "@glint/template": "^1.5.0", + "@glint/environment-ember-loose": "^1.5.0", + "@glint/environment-ember-template-imports": "^1.5.0", "@types/fs-extra": "^9.0.12", "@types/minimatch": "^3.0.4", "@types/yargs": "^17.0.3", "rollup": "^3.23.0", - "tmp": "^0.1.0", + "scenario-tester": "^4.0.0", "typescript": "^5.4.5" }, "engines": { diff --git a/packages/addon-dev/src/rollup-declarations.ts b/packages/addon-dev/src/rollup-declarations.ts new file mode 100644 index 000000000..89db68851 --- /dev/null +++ b/packages/addon-dev/src/rollup-declarations.ts @@ -0,0 +1,50 @@ +import execa from 'execa'; +import walkSync from 'walk-sync'; +import { readFile, writeFile } from 'fs/promises'; + +export default function rollupDeclarationsPlugin(declarationsDir: string) { + let glintPromise: Promise; + + return { + name: 'glint-dts', + buildStart: () => { + const runGlint = async () => { + await execa('glint', ['--declaration'], { + stdio: 'inherit', + preferLocal: true, + }); + + await fixDeclarationsInMatchingFiles(declarationsDir); + }; + + // We just kick off glint here early in the rollup process, without making rollup wait for this to finish, by not returning the promise + // The output of this is not relevant to further stages of the rollup build, this is just happening in parallel to other rollup compilation + glintPromise = runGlint(); + }, + + // Make rollup wait for glint to have finished before calling the build job done + writeBundle: () => glintPromise, + }; +} + +async function fixDeclarationsInMatchingFiles(dir: string) { + const dtsFiles = walkSync(dir, { + globs: ['**/*.d.ts'], + directories: false, + includeBasePath: true, + }); + + return Promise.all( + dtsFiles.map(async (file) => { + const content = await readFile(file, { encoding: 'utf8' }); + + await writeFile(file, fixDeclarations(content)); + }) + ); +} + +// Strip any .gts extension from imports in d.ts files, as these won't resolve. See https://github.com/typed-ember/glint/issues/628 +// Once Glint v2 is available, this shouldn't be needed anymore. +function fixDeclarations(content: string) { + return content.replace(/from\s+['"]([^'"]+)\.gts['"]/g, `from '$1'`); +} diff --git a/packages/addon-dev/src/rollup.ts b/packages/addon-dev/src/rollup.ts index 3f83e2822..08d7db7ce 100644 --- a/packages/addon-dev/src/rollup.ts +++ b/packages/addon-dev/src/rollup.ts @@ -3,6 +3,7 @@ import { default as gjs } from './rollup-gjs-plugin'; import { default as publicEntrypoints } from './rollup-public-entrypoints'; import { default as appReexports } from './rollup-app-reexports'; import { default as keepAssets } from './rollup-keep-assets'; +import { default as declarations } from './rollup-declarations'; import { default as dependencies } from './rollup-addon-dependencies'; import { default as publicAssets, @@ -108,4 +109,8 @@ export class Addon { publicAssets(path: string, opts?: PublicAssetsOptions) { return publicAssets(path, opts); } + + declarations(path: string) { + return declarations(path); + } } diff --git a/packages/addon-dev/tests/declarations.test.ts b/packages/addon-dev/tests/declarations.test.ts new file mode 100644 index 000000000..e0391e4c1 --- /dev/null +++ b/packages/addon-dev/tests/declarations.test.ts @@ -0,0 +1,112 @@ +'use strict'; + +import rollupDeclarationsPlugin from '../src/rollup-declarations'; +import { Project } from 'scenario-tester'; +import { rollup } from 'rollup'; +import { readFile } from 'fs-extra'; +import { join } from 'path'; + +const projectBoilerplate = { + 'tsconfig.json': JSON.stringify({ + include: ['src/**/*'], + compilerOptions: { + declaration: true, + declarationDir: 'declarations', + emitDeclarationOnly: true, + rootDir: './src', + allowImportingTsExtensions: true, + }, + glint: { + environment: ['ember-loose', 'ember-template-imports'], + }, + }), +}; + +async function generateProject(src: {}): Promise { + const project = new Project('my-addon', { + files: { + ...projectBoilerplate, + src, + }, + }); + project.linkDevDependency('typescript', { baseDir: __dirname }); + project.linkDevDependency('@glint/core', { baseDir: __dirname }); + project.linkDevDependency('@glint/template', { baseDir: __dirname }); + project.linkDevDependency('@glint/environment-ember-loose', { + baseDir: __dirname, + }); + project.linkDevDependency('@glint/environment-ember-template-imports', { + baseDir: __dirname, + }); + + await project.write(); + + return project; +} + +async function runRollup(dir: string, rollupOptions = {}) { + const currentDir = process.cwd(); + process.chdir(dir); + + try { + const bundle = await rollup({ + input: './src/index.ts', + plugins: [rollupDeclarationsPlugin('declarations')], + ...rollupOptions, + }); + + await bundle.write({ format: 'esm', dir: 'dist' }); + } finally { + process.chdir(currentDir); + } +} + +describe('declarations', function () { + let project: Project | null; + + afterEach(() => { + project?.dispose(); + project = null; + }); + + test('it generates dts output', async function () { + project = await generateProject({ + 'index.ts': 'export default 123', + }); + + await runRollup(project.baseDir); + + expect( + await readFile(join(project.baseDir, 'declarations/index.d.ts'), { + encoding: 'utf8', + }) + ).toContain('export default'); + }); + + test('it has correct imports', async function () { + project = await generateProject({ + 'index.ts': ` + import foo from './foo.gts'; + import bar from './bar.gts'; + import baz from './baz.ts'; + export { foo, bar, baz }; + `, + 'foo.gts': 'export default 123', + 'bar.gts': 'export default 234', + 'baz.ts': 'export default 345', + }); + + await runRollup(project.baseDir); + + const output = await readFile( + join(project.baseDir, 'declarations/index.d.ts'), + { + encoding: 'utf8', + } + ); + + expect(output).toContain(`import foo from './foo';`); + expect(output).toContain(`import bar from './bar';`); + expect(output).toContain(`import baz from './baz.ts';`); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c48bb6fc9..fa4e7518b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,6 +66,9 @@ importers: content-tag: specifier: ^3.0.0 version: 3.0.0 + execa: + specifier: ^5.1.1 + version: 5.1.1 fs-extra: specifier: ^10.0.0 version: 10.1.0 @@ -88,6 +91,18 @@ importers: '@glimmer/syntax': specifier: ^0.84.2 version: 0.84.3 + '@glint/core': + specifier: ^1.5.0 + version: 1.5.0(typescript@5.6.3) + '@glint/environment-ember-loose': + specifier: ^1.5.0 + version: 1.5.0(@glimmer/component@1.1.2)(@glint/template@1.5.0)(ember-cli-htmlbars@6.3.0)(ember-modifier@4.2.0) + '@glint/environment-ember-template-imports': + specifier: ^1.5.0 + version: 1.5.0(@glint/environment-ember-loose@1.5.0)(@glint/template@1.5.0) + '@glint/template': + specifier: ^1.5.0 + version: 1.5.0 '@types/fs-extra': specifier: ^9.0.12 version: 9.0.13 @@ -100,9 +115,9 @@ importers: rollup: specifier: ^3.23.0 version: 3.29.5 - tmp: - specifier: ^0.1.0 - version: 0.1.0 + scenario-tester: + specifier: ^4.0.0 + version: 4.1.1 typescript: specifier: ^5.4.5 version: 5.6.3 @@ -6960,6 +6975,26 @@ packages: '@glimmer/util': 0.92.3 dev: true + /@glint/core@1.5.0(typescript@5.6.3): + resolution: {integrity: sha512-oo6ZDwX2S0Qqjai/CJH72LHg1U6rvzH1IyiFlWofaFiu/nSg04CDWZuJNPC3r47jz1+SaSI+mVMUaKJznzxzzQ==} + hasBin: true + peerDependencies: + typescript: '>=4.8.0' + dependencies: + '@glimmer/syntax': 0.84.3 + escape-string-regexp: 4.0.0 + semver: 7.6.3 + silent-error: 1.1.1 + typescript: 5.6.3 + uuid: 8.3.2 + vscode-languageserver: 8.1.0 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + dev: true + /@glint/environment-ember-loose@1.5.0(@glimmer/component@1.1.2)(@glint/template@1.5.0)(ember-cli-htmlbars@6.3.0)(ember-modifier@4.2.0): resolution: {integrity: sha512-QCP4pVupq8zGcBmMDcEq9XI5lfrnklwNOIuzdXb8OnbcY6qpuwz5Y6VOsA1WNGRcip/5wwOsmI6gsAEUTlbvPQ==} peerDependencies: @@ -6994,6 +7029,30 @@ packages: ember-modifier: 4.2.0(@babel/core@7.26.0)(ember-source@5.3.0) dev: true + /@glint/environment-ember-template-imports@1.5.0(@glint/environment-ember-loose@1.5.0)(@glint/template@1.5.0): + resolution: {integrity: sha512-SS+KNffLuNYcsT7iEmCr2jp2538E7KTMEAWY+KWNvUJ0ZMd6oe6xbIIF50+9BgCgGHWwj7oL/NdgCVkS3OqRdw==} + peerDependencies: + '@glint/environment-ember-loose': ^1.5.0 + '@glint/template': ^1.5.0 + '@types/ember__component': ^4.0.10 + '@types/ember__helper': ^4.0.1 + '@types/ember__modifier': ^4.0.3 + '@types/ember__routing': ^4.0.12 + peerDependenciesMeta: + '@types/ember__component': + optional: true + '@types/ember__helper': + optional: true + '@types/ember__modifier': + optional: true + '@types/ember__routing': + optional: true + dependencies: + '@glint/environment-ember-loose': 1.5.0(@glimmer/component@1.1.2)(@glint/template@1.5.0)(ember-cli-htmlbars@6.3.0)(ember-modifier@4.2.0) + '@glint/template': 1.5.0 + content-tag: 2.0.2 + dev: true + /@glint/template@1.5.0: resolution: {integrity: sha512-KyQUCWifxl8wDxo3SXzJcGKttHbIPgFBtqsoiu13Edx/o4CgGXr5rrM64jJR7Wvunn8sRM+Rq7Y0cHoB068Wuw==} @@ -25081,6 +25140,37 @@ packages: fsevents: 2.3.3 dev: true + /vscode-jsonrpc@8.1.0: + resolution: {integrity: sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==} + engines: {node: '>=14.0.0'} + dev: true + + /vscode-languageserver-protocol@3.17.3: + resolution: {integrity: sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==} + dependencies: + vscode-jsonrpc: 8.1.0 + vscode-languageserver-types: 3.17.3 + dev: true + + /vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + dev: true + + /vscode-languageserver-types@3.17.3: + resolution: {integrity: sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==} + dev: true + + /vscode-languageserver@8.1.0: + resolution: {integrity: sha512-eUt8f1z2N2IEUDBsKaNapkz7jl5QpskN2Y0G01T/ItMxBxw1fJwvtySGB9QMecatne8jFIWJGWI61dWjyTLQsw==} + hasBin: true + dependencies: + vscode-languageserver-protocol: 3.17.3 + dev: true + + /vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + dev: true + /w3c-hr-time@1.0.2: resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} deprecated: Use your platform's native performance.now() and performance.timeOrigin.