Skip to content

Commit de45ac1

Browse files
authored
Merge pull request #1046 from typed-ember/warn-on-js-ts-collision
Issue a warning if we detect a .js/.ts file collision
2 parents 4573e1c + d2ec32c commit de45ac1

File tree

11 files changed

+138
-18
lines changed

11 files changed

+138
-18
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@babel/plugin-proposal-optional-chaining": "^7.6.0",
4444
"@babel/plugin-transform-typescript": "~7.8.0",
4545
"ansi-to-html": "^0.6.6",
46+
"broccoli-stew": "^3.0.0",
4647
"debug": "^4.0.0",
4748
"ember-cli-babel-plugin-helpers": "^1.0.0",
4849
"execa": "^3.0.0",
@@ -79,6 +80,8 @@
7980
"@typescript-eslint/eslint-plugin": "2.17.0",
8081
"@typescript-eslint/parser": "2.17.0",
8182
"broccoli-asset-rev": "3.0.0",
83+
"broccoli-node-api": "^1.7.0",
84+
"broccoli-plugin": "^4.0.1",
8285
"capture-console": "1.0.1",
8386
"co": "4.6.0",
8487
"commitlint-azure-pipelines-cli": "1.0.3",

tests/dummy/app/shadowed-file.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

tests/dummy/lib/in-repo-a/app/shadowed-file.ts

Lines changed: 0 additions & 4 deletions
This file was deleted.

tests/dummy/lib/in-repo-b/app/shadowed-file.js

Lines changed: 0 additions & 4 deletions
This file was deleted.

ts/addon.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Remote } from 'stagehand';
33
import { connect } from 'stagehand/lib/adapters/child-process';
44
import { hasPlugin, addPlugin, AddPluginOptions } from 'ember-cli-babel-plugin-helpers';
55
import Addon from 'ember-cli/lib/models/addon';
6+
import PreprocessRegistry from 'ember-cli-preprocess-registry';
67
import { addon } from './lib/utilities/ember-cli-entities';
78
import fork from './lib/utilities/fork';
89
import TypecheckWorker from './lib/typechecking/worker';
@@ -18,6 +19,7 @@ export default addon({
1819

1920
included() {
2021
this._super.included.apply(this, arguments);
22+
2123
this._checkDevelopment();
2224
this._checkAddonAppFiles();
2325
this._checkBabelVersion();
@@ -69,9 +71,16 @@ export default addon({
6971
}
7072
},
7173

72-
setupPreprocessorRegistry(type) {
74+
setupPreprocessorRegistry(type, registry) {
7375
if (type !== 'parent') return;
7476

77+
// If we're acting on behalf of the root app, issue a warning if we detect
78+
// a situation where a .js file from an addon has the same name as a .ts
79+
// file in the app, as which file wins is nondeterministic.
80+
if (this.parent === this.project) {
81+
this._registerCollisionDetectionPreprocessor(registry);
82+
}
83+
7584
// Normally this is the sort of logic that would live in `included()`, but
7685
// ember-cli-babel reads the configured extensions when setting up the
7786
// preprocessor registry, so we need to beat it to the punch.
@@ -97,6 +106,47 @@ export default addon({
97106
return !['in-repo-a', 'in-repo-b'].includes(addon.name);
98107
},
99108

109+
_registerCollisionDetectionPreprocessor(registry: PreprocessRegistry) {
110+
registry.add('js', {
111+
name: 'ember-cli-typescript-collision-check',
112+
toTree: (input, path) => {
113+
if (path !== '/') return input;
114+
115+
let addon = this;
116+
let checked = false;
117+
let stew = require('broccoli-stew') as typeof import('broccoli-stew');
118+
119+
return stew.afterBuild(input, function() {
120+
if (!checked) {
121+
checked = true;
122+
addon._checkForFileCollisions(this.inputPaths[0]);
123+
}
124+
});
125+
},
126+
});
127+
},
128+
129+
_checkForFileCollisions(directory: string) {
130+
let walkSync = require('walk-sync') as typeof import('walk-sync');
131+
let files = new Set(walkSync(directory, ['**/*.{js,ts}']));
132+
133+
let collisions = [];
134+
for (let file of files) {
135+
if (file.endsWith('.js') && files.has(file.replace(/\.js$/, '.ts'))) {
136+
collisions.push(file.replace(/\.js$/, '.{js,ts}'));
137+
}
138+
}
139+
140+
if (collisions.length) {
141+
this.ui.writeWarnLine(
142+
'Detected collisions between .js and .ts files of the same name. ' +
143+
'This can result in nondeterministic build output; ' +
144+
'see https://git.io/JvIwo for more information.\n - ' +
145+
collisions.join('\n - ')
146+
);
147+
}
148+
},
149+
100150
_checkBabelVersion() {
101151
let babel = this.parent.addons.find(addon => addon.name === 'ember-cli-babel');
102152
let version = babel && babel.pkg.version;

ts/tests/acceptance/build-test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,33 @@ describe('Acceptance: build', function() {
101101

102102
await server.waitForBuild();
103103
});
104+
105+
it('emits a warning when .js and .ts files conflict in the app/ tree', async () => {
106+
// Set up an in-repo addon
107+
app.updatePackageJSON(pkg => {
108+
pkg['ember-addon'].paths.push('lib/in-repo-addon');
109+
});
110+
111+
app.writeFile('lib/in-repo-addon/index.js', 'module.exports = { name: "in-repo-addon" };');
112+
app.writeFile(
113+
'lib/in-repo-addon/package.json',
114+
JSON.stringify({
115+
name: 'in-repo-addon',
116+
keywords: ['ember-addon'],
117+
})
118+
);
119+
120+
// Have it export a .js app file and attempt to overwrite it in the host with a .ts file
121+
app.writeFile('lib/in-repo-addon/app/foo.js', '// addon');
122+
app.writeFile('app/foo.ts', '// app');
123+
124+
let output = await app.build();
125+
126+
expect(output.all).to.include('skeleton-app/foo.{js,ts}');
127+
expect(output.all).to.include(
128+
'WARNING: Detected collisions between .js and .ts files of the same name.'
129+
);
130+
});
104131
});
105132

106133
function isExpressionStatement(stmt: Statement | ModuleDeclaration): stmt is ExpressionStatement {

ts/tests/helpers/skeleton-app.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default class SkeletonApp {
4444
));
4545
}
4646

47-
updatePackageJSON(callback: (arg: any) => string) {
47+
updatePackageJSON(callback: (arg: any) => any) {
4848
let pkgPath = `${this.root}/package.json`;
4949
let pkg = fs.readJSONSync(pkgPath);
5050
fs.writeJSONSync(pkgPath, callback(pkg) || pkg, { spaces: 2 });

ts/types/broccoli-stew/index.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
declare module 'broccoli-stew' {
2+
import { Node as BroccoliNode } from 'broccoli-node-api';
3+
import Plugin from 'broccoli-plugin';
4+
5+
export function afterBuild(node: BroccoliNode, callback: (this: Plugin) => void): BroccoliNode;
6+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
declare module 'ember-cli-preprocess-registry' {
2+
import { Node as BroccoliNode } from 'broccoli-node-api';
3+
4+
export = PreprocessRegistry;
5+
6+
class PreprocessRegistry {
7+
add(type: string, plugin: PreprocessPlugin): void;
8+
load(type: string): Array<PreprocessPlugin>;
9+
extensionsForType(type: string): Array<string>;
10+
remove(type: string, plugin: PreprocessPlugin): void;
11+
}
12+
13+
interface PreprocessPlugin {
14+
name: string;
15+
toTree(input: BroccoliNode, path: string): BroccoliNode;
16+
}
17+
}

ts/types/ember-cli/index.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ declare module 'ember-cli/lib/models/addon' {
1313
import Project from 'ember-cli/lib/models/project';
1414
import Command from 'ember-cli/lib/models/command';
1515
import EmberApp from 'ember-cli/lib/broccoli/ember-app';
16+
import PreprocessRegistry from 'ember-cli-preprocess-registry';
1617

1718
export default class Addon extends CoreObject {
1819
name: string;
@@ -37,7 +38,7 @@ declare module 'ember-cli/lib/models/addon' {
3738
isDevelopingAddon(): boolean;
3839
serverMiddleware(options: { app: Application }): void | Promise<void>;
3940
testemMiddleware(app: Application): void;
40-
setupPreprocessorRegistry(type: 'self' | 'parent', registry: unknown): void;
41+
setupPreprocessorRegistry(type: 'self' | 'parent', registry: PreprocessRegistry): void;
4142
}
4243
}
4344

0 commit comments

Comments
 (0)