Skip to content

Commit b82eab5

Browse files
Add feature flag to throw on empty files
1 parent de50e27 commit b82eab5

File tree

9 files changed

+205
-7
lines changed

9 files changed

+205
-7
lines changed

packages/core/feature-flags/src/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const DEFAULT_FEATURE_FLAGS: FeatureFlags = {
1010
parcelV3: false,
1111
importRetry: false,
1212
ownedResolverStructures: false,
13+
panicOnEmptyFileImport: false,
1314
};
1415

1516
let featureFlagValues: FeatureFlags = {...DEFAULT_FEATURE_FLAGS};

packages/core/feature-flags/src/types.js

+5
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,9 @@ export type FeatureFlags = {|
1515
* Enable resolver refactor into owned data structures.
1616
*/
1717
ownedResolverStructures: boolean,
18+
19+
/**
20+
* Makes Parcel panic when an empty file is imported
21+
*/
22+
panicOnEmptyFileImport: boolean,
1823
|};

packages/core/integration-tests/.mocharc.json

+1-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,5 @@
22
"require": ["@parcel/babel-register", "@parcel/test-utils/src/mochaSetup.js"],
33
"timeout": 50000,
44
// TODO: Remove this when https://github.com/nodejs/node/pull/28788 is resolved
5-
"exit": true,
6-
// This helps reduce CI flakiness of the Mac OS test run
7-
"retries": 2
5+
"exit": true
86
}

packages/core/integration-tests/test/integration/env-mutate/node_modules/foo/package.json

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/integration-tests/test/javascript.js

+71-2
Original file line numberDiff line numberDiff line change
@@ -3104,6 +3104,9 @@ describe('javascript', function () {
31043104
path.join(__dirname, '/integration/bundle-text/javascript.js'),
31053105
{
31063106
mode: 'production',
3107+
featureFlags: {
3108+
panicOnEmptyFileImport: false,
3109+
},
31073110
},
31083111
);
31093112

@@ -5548,7 +5551,12 @@ describe('javascript', function () {
55485551
__dirname,
55495552
'/integration/scope-hoisting/es6/side-effects-re-exports-all-empty/a.js',
55505553
),
5551-
options,
5554+
{
5555+
...options,
5556+
featureFlags: {
5557+
panicOnEmptyFileImport: false,
5558+
},
5559+
},
55525560
);
55535561

55545562
if (usesSymbolPropagation) {
@@ -5559,6 +5567,26 @@ describe('javascript', function () {
55595567
},
55605568
);
55615569

5570+
it.v2(
5571+
'throws when deferring an unused ES6 re-export (wildcard, empty, unused)',
5572+
async function () {
5573+
await assert.rejects(
5574+
() =>
5575+
bundle(
5576+
path.join(
5577+
__dirname,
5578+
'/integration/scope-hoisting/es6/side-effects-re-exports-all-empty/a.js',
5579+
),
5580+
options,
5581+
),
5582+
{
5583+
message:
5584+
'integration/scope-hoisting/es6/side-effects-re-exports-all-empty/library/empty.js must export a value',
5585+
},
5586+
);
5587+
},
5588+
);
5589+
55625590
it.v2(
55635591
'supports deferring unused ES6 re-exports (reexport named used)',
55645592
async function () {
@@ -6116,6 +6144,44 @@ describe('javascript', function () {
61166144
},
61176145
);
61186146

6147+
it.v2(
6148+
'missing exports should throw when panicOnEmptyFileImport is enabled',
6149+
async function () {
6150+
await fsFixture(overlayFS, __dirname)`
6151+
empty.js:
6152+
// intentionally empty
6153+
6154+
index.js:
6155+
import {a} from './empty.js';
6156+
6157+
package.json:
6158+
{
6159+
"sideEffects": false
6160+
}
6161+
6162+
yarn.lock: {}
6163+
`;
6164+
6165+
await assert.rejects(
6166+
() =>
6167+
bundle(
6168+
path.join(
6169+
__dirname,
6170+
'/integration/scope-hoisting/es6/empty-module/a.js',
6171+
),
6172+
{
6173+
...options,
6174+
inputFS: overlayFS,
6175+
},
6176+
),
6177+
{
6178+
message:
6179+
'integration/scope-hoisting/es6/empty-module/b.js must export a value',
6180+
},
6181+
);
6182+
},
6183+
);
6184+
61196185
it.v2(
61206186
'supports namespace imports of theoretically excluded reexporting assets',
61216187
async function () {
@@ -6545,7 +6611,10 @@ describe('javascript', function () {
65456611
__dirname,
65466612
'/integration/scope-hoisting/es6/side-effects-commonjs/a.js',
65476613
),
6548-
options,
6614+
{
6615+
...options,
6616+
outputFS: inputFS,
6617+
},
65496618
);
65506619

65516620
if (usesSymbolPropagation) {

packages/core/integration-tests/test/scope-hoisting.js

+81
Original file line numberDiff line numberDiff line change
@@ -366,11 +366,64 @@ describe.v2('scope hoisting', function () {
366366
__dirname,
367367
'integration/scope-hoisting/es6/re-export-all-fallback-3/entry.js',
368368
),
369+
{
370+
featureFlags: {
371+
panicOnEmptyFileImport: false,
372+
},
373+
},
369374
);
370375
let output = await run(b);
371376
assert.strictEqual(output, 'FOOBAR!');
372377
});
373378

379+
it('produces an invalid bundle when there is an empty file with export *', async () => {
380+
await fsFixture(overlayFS, __dirname)`
381+
empty.js:
382+
// intentionally empty
383+
384+
a.js:
385+
export * from './b.js';
386+
export * from './c.js';
387+
388+
b.js:
389+
export * from './empty.js';
390+
export * from './test.js';
391+
392+
c.js:
393+
export * from './empty.js';
394+
395+
index.js:
396+
import {test} from './a.js';
397+
output(test);
398+
399+
package.json:
400+
{
401+
"sideEffects": false
402+
}
403+
404+
test.js:
405+
export const test = 'should not fail';
406+
407+
yarn.lock: {}
408+
`;
409+
410+
let result = await bundle(path.join(__dirname, 'index.js'), {
411+
featureFlags: {
412+
panicOnEmptyFileImport: false,
413+
},
414+
inputFS: overlayFS,
415+
});
416+
417+
let output;
418+
await run(result, {
419+
output(v) {
420+
output = v;
421+
},
422+
});
423+
424+
assert.equal(output, undefined);
425+
});
426+
374427
it('supports nested re-exporting all when falling back to namespace at runtime', async function () {
375428
let b = await bundle(
376429
path.join(
@@ -390,6 +443,9 @@ describe.v2('scope hoisting', function () {
390443
'/integration/scope-hoisting/es6/re-export-all-empty-no-side-effects/index.js',
391444
),
392445
{
446+
featureFlags: {
447+
panicOnEmptyFileImport: false,
448+
},
393449
mode: 'production',
394450
},
395451
);
@@ -405,6 +461,25 @@ describe.v2('scope hoisting', function () {
405461
assert.match(contents, /output="foo bar"/);
406462
});
407463

464+
it('panics when re-exporting all from an empty module without side effects and panicOnEmptyFileImport is enabled', async function () {
465+
await assert.rejects(
466+
() =>
467+
bundle(
468+
path.join(
469+
__dirname,
470+
'/integration/scope-hoisting/es6/re-export-all-empty-no-side-effects/index.js',
471+
),
472+
{
473+
mode: 'production',
474+
},
475+
),
476+
{
477+
message:
478+
'integration/scope-hoisting/es6/re-export-all-empty-no-side-effects/node\\_modules/lib/empty.js must export a value',
479+
},
480+
);
481+
});
482+
408483
it('supports re-exporting all with ambiguous CJS and non-renaming and renaming dependency retargeting', async function () {
409484
let b = await bundle(
410485
path.join(
@@ -6001,6 +6076,9 @@ describe.v2('scope hoisting', function () {
60016076

60026077
try {
60036078
let b = await bundle(path.join(testDir, 'index.html'), {
6079+
featureFlags: {
6080+
panicOnEmptyFileImport: false,
6081+
},
60046082
inputFS: slowFooFS,
60056083
outputFS: slowFooFS,
60066084
shouldDisableCache: true,
@@ -6015,6 +6093,9 @@ describe.v2('scope hoisting', function () {
60156093
let slowBarFS = new Proxy(overlayFS, waitHandler('bar.js', 'foo.js'));
60166094

60176095
let b2 = await bundle(path.join(testDir, 'index.html'), {
6096+
featureFlags: {
6097+
panicOnEmptyFileImport: false,
6098+
},
60186099
inputFS: slowBarFS,
60196100
outputFS: slowBarFS,
60206101
shouldDisableCache: true,

packages/core/test-utils/src/utils.js

+1
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ export function getParcelOptions(
133133
},
134134
},
135135
featureFlags: {
136+
panicOnEmptyFileImport: true,
136137
parcelV3: isParcelV3,
137138
},
138139
},

packages/transformers/js/src/JSTransformer.js

+41-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import type {
66
FilePath,
77
FileCreateInvalidation,
88
} from '@parcel/types';
9-
import type {SchemaEntity} from '@parcel/utils';
109
import type {Diagnostic} from '@parcel/diagnostic';
1110
import SourceMap from '@parcel/source-map';
1211
import {Transformer} from '@parcel/plugin';
@@ -18,7 +17,13 @@ import ThrowableDiagnostic, {
1817
encodeJSONKeyComponent,
1918
convertSourceLocationToHighlight,
2019
} from '@parcel/diagnostic';
21-
import {validateSchema, remapSourceLocation, globMatch} from '@parcel/utils';
20+
import {
21+
globMatch,
22+
relativePath,
23+
remapSourceLocation,
24+
type SchemaEntity,
25+
validateSchema,
26+
} from '@parcel/utils';
2227
import pkg from '../package.json';
2328

2429
const JSX_EXTENSIONS = {
@@ -862,6 +867,23 @@ export default (new Transformer({
862867

863868
asset.meta.id = asset.id;
864869
if (hoist_result) {
870+
if (
871+
options.featureFlags.panicOnEmptyFileImport &&
872+
!asset.pipeline &&
873+
!asset.sideEffects &&
874+
!hoist_result.exported_symbols.length &&
875+
!hoist_result.imported_symbols.length &&
876+
!hoist_result.re_exports.length
877+
) {
878+
throw new Error(
879+
`${relativePath(
880+
options.projectRoot,
881+
asset.filePath,
882+
false,
883+
)} must export a value`,
884+
);
885+
}
886+
865887
asset.symbols.ensure();
866888
for (let {
867889
exported,
@@ -974,6 +996,23 @@ export default (new Transformer({
974996
asset.meta.shouldWrap = hoist_result.should_wrap;
975997
} else {
976998
if (symbol_result) {
999+
if (
1000+
options.featureFlags.panicOnEmptyFileImport &&
1001+
!asset.pipeline &&
1002+
!asset.sideEffects &&
1003+
!symbol_result.exports.length &&
1004+
!symbol_result.exports_all.length &&
1005+
!symbol_result.imports.length
1006+
) {
1007+
throw new Error(
1008+
`${relativePath(
1009+
options.projectRoot,
1010+
asset.filePath,
1011+
false,
1012+
)} must export a value`,
1013+
);
1014+
}
1015+
9771016
let deps = new Map(
9781017
asset
9791018
.getDependencies()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = {};

0 commit comments

Comments
 (0)