}>
+{ /* @ts-expect-error - TS17004 - Cannot use JSX unless the '--jsx' flag is provided. */}
);
@@ -28,16 +33,23 @@ const App = () => {
console.log('FeatureWithUI', FeatureWithUI);
console.log('Feature', Feature);
return (
+// @ts-expect-error - TS17004 - Cannot use JSX unless the '--jsx' flag is provided.
+{ /* @ts-expect-error - TS17004 - Cannot use JSX unless the '--jsx' flag is provided. */}
Hello from React
+{ /* @ts-expect-error - TS17004 - Cannot use JSX unless the '--jsx' flag is provided. */}
+{ /* @ts-expect-error - TS17004 - Cannot use JSX unless the '--jsx' flag is provided. */}
Conditional Feature: {Feature()}
+{ /* @ts-expect-error - TS17004 - Cannot use JSX unless the '--jsx' flag is provided. */}
+{ /* @ts-expect-error - TS17004 - Cannot use JSX unless the '--jsx' flag is provided. */}
{showLazyComponent ? : null}
);
};
+// @ts-expect-error - TS17004 - Cannot use JSX unless the '--jsx' flag is provided.
ReactDOM.render(, document.getElementById('container'));
diff --git a/packages/examples/conditional-bundling/src/lazy-component.tsx b/packages/examples/conditional-bundling/src/lazy-component.tsx
index 3fe033598..2736cd1ae 100644
--- a/packages/examples/conditional-bundling/src/lazy-component.tsx
+++ b/packages/examples/conditional-bundling/src/lazy-component.tsx
@@ -3,7 +3,9 @@ import Button from '@atlaskit/button';
export default function LazyComponent() {
return (
+// @ts-expect-error - TS17004 - Cannot use JSX unless the '--jsx' flag is provided.
+{ /* @ts-expect-error - TS17004 - Cannot use JSX unless the '--jsx' flag is provided. */}
This is a lazy component. It has a button.
);
diff --git a/packages/examples/conditional-bundling/src/regular-dynamic-import.ts b/packages/examples/conditional-bundling/src/regular-dynamic-import.ts
index d9f7f6f74..97fb7fcc9 100644
--- a/packages/examples/conditional-bundling/src/regular-dynamic-import.ts
+++ b/packages/examples/conditional-bundling/src/regular-dynamic-import.ts
@@ -7,5 +7,6 @@ export const DynamicExportWithCondition = () => {
'feature.async.condition',
'./async-feature-enabled.ts',
'./async-feature-disabled.ts',
+ // @ts-expect-error - TS2339 - Property 'Feature' does not exist on type 'ConditionalImport'.
).Feature();
};
diff --git a/packages/examples/typechecking/src/index.ts b/packages/examples/typechecking/src/index.ts
index ba38e0975..93e298626 100644
--- a/packages/examples/typechecking/src/index.ts
+++ b/packages/examples/typechecking/src/index.ts
@@ -3,5 +3,6 @@ type Params = {
};
export default function test(params: Params) {
+ // @ts-expect-error - TS2339 - Property 'world' does not exist on type 'Params'.
return params.world;
}
diff --git a/packages/migrations/parcel-to-atlaspack/src/cli.ts b/packages/migrations/parcel-to-atlaspack/src/cli.ts
index 6e4cd14ac..01a33ec01 100644
--- a/packages/migrations/parcel-to-atlaspack/src/cli.ts
+++ b/packages/migrations/parcel-to-atlaspack/src/cli.ts
@@ -1,5 +1,6 @@
import {Command} from 'commander';
+// @ts-expect-error - TS2732 - Cannot find module '../package.json'. Consider using '--resolveJsonModule' to import module with '.json' extension.
import packageJson from '../package.json';
import {migratePackageJson} from './migrations/migrate-package-json';
diff --git a/packages/namers/default/package.json b/packages/namers/default/package.json
index 494a5bd6d..a0f319c6f 100644
--- a/packages/namers/default/package.json
+++ b/packages/namers/default/package.json
@@ -10,7 +10,8 @@
"url": "https://github.com/atlassian-labs/atlaspack.git"
},
"main": "lib/DefaultNamer.js",
- "source": "src/DefaultNamer.js",
+ "types": "src/DefaultNamer.ts",
+ "source": "src/DefaultNamer.ts",
"engines": {
"node": ">= 16.0.0",
"parcel": "^2.12.0"
diff --git a/packages/namers/default/src/DefaultNamer.js b/packages/namers/default/src/DefaultNamer.js
deleted file mode 100644
index cc89b11ab..000000000
--- a/packages/namers/default/src/DefaultNamer.js
+++ /dev/null
@@ -1,145 +0,0 @@
-// @flow strict-local
-
-import type {Bundle, FilePath} from '@atlaspack/types';
-
-import {Namer} from '@atlaspack/plugin';
-import ThrowableDiagnostic, {
- convertSourceLocationToHighlight,
- md,
-} from '@atlaspack/diagnostic';
-import assert from 'assert';
-import path from 'path';
-import nullthrows from 'nullthrows';
-
-const COMMON_NAMES = new Set(['index', 'src', 'lib']);
-const ALLOWED_EXTENSIONS = {
- js: ['js', 'mjs', 'cjs'],
-};
-
-export default (new Namer({
- name({bundle, bundleGraph}) {
- let bundleGroup = bundleGraph.getBundleGroupsContainingBundle(bundle)[0];
- let bundleGroupBundles = bundleGraph.getBundlesInBundleGroup(bundleGroup, {
- includeInline: true,
- });
- let isEntry = bundleGraph.isEntryBundleGroup(bundleGroup);
-
- if (bundle.needsStableName) {
- let entryBundlesOfType = bundleGroupBundles.filter(
- b => b.needsStableName && b.type === bundle.type,
- );
- assert(
- entryBundlesOfType.length === 1,
- // Otherwise, we'd end up naming two bundles the same thing.
- `Bundle group cannot have more than one entry bundle of the same type. The offending bundle type is ${entryBundlesOfType[0].type}`,
- );
- }
-
- let mainBundle = nullthrows(
- bundleGroupBundles.find(b =>
- b.getEntryAssets().some(a => a.id === bundleGroup.entryAssetId),
- ),
- );
-
- if (
- bundle.id === mainBundle.id &&
- isEntry &&
- bundle.target &&
- bundle.target.distEntry != null
- ) {
- let loc = bundle.target.loc;
- let distEntry = bundle.target.distEntry;
- let distExtension = path.extname(bundle.target.distEntry).slice(1);
- let allowedExtensions = ALLOWED_EXTENSIONS[bundle.type] || [bundle.type];
- if (!allowedExtensions.includes(distExtension) && loc) {
- let fullName = path.relative(
- path.dirname(loc.filePath),
- path.join(bundle.target.distDir, distEntry),
- );
- let err = new ThrowableDiagnostic({
- diagnostic: {
- message: md`Target "${bundle.target.name}" declares an output file path of "${fullName}" which does not match the compiled bundle type "${bundle.type}".`,
- codeFrames: [
- {
- filePath: loc.filePath,
- codeHighlights: [
- convertSourceLocationToHighlight(
- loc,
- md`Did you mean "${
- fullName.slice(0, -path.extname(fullName).length) +
- '.' +
- bundle.type
- }"?`,
- ),
- ],
- },
- ],
- hints: [
- `Try changing the file extension of "${
- bundle.target.name
- }" in ${path.relative(process.cwd(), loc.filePath)}.`,
- ],
- },
- });
- throw err;
- }
-
- return bundle.target.distEntry;
- }
-
- // Base split bundle names on the first bundle in their group.
- // e.g. if `index.js` imports `foo.css`, the css bundle should be called
- // `index.css`.
- let name = nameFromContent(
- mainBundle,
- isEntry,
- bundleGroup.entryAssetId,
- bundleGraph.getEntryRoot(bundle.target),
- );
- if (!bundle.needsStableName) {
- name += '.' + bundle.hashReference;
- }
-
- return name + '.' + bundle.type;
- },
-}): Namer);
-
-function nameFromContent(
- bundle: Bundle,
- isEntry: boolean,
- entryAssetId: string,
- entryRoot: FilePath,
-): string {
- let entryFilePath = nullthrows(
- bundle.getEntryAssets().find(a => a.id === entryAssetId),
- ).filePath;
- let name = basenameWithoutExtension(entryFilePath);
-
- // If this is an entry bundle, use the original relative path.
- if (bundle.needsStableName) {
- // Match name of target entry if possible, but with a different extension.
- if (isEntry && bundle.target.distEntry != null) {
- return basenameWithoutExtension(bundle.target.distEntry);
- }
-
- return path
- .join(path.relative(entryRoot, path.dirname(entryFilePath)), name)
- .replace(/\.\.(\/|\\)/g, 'up_$1');
- } else {
- // If this is an index file or common directory name, use the parent
- // directory name instead, which is probably more descriptive.
- while (COMMON_NAMES.has(name)) {
- entryFilePath = path.dirname(entryFilePath);
- name = path.basename(entryFilePath);
- if (name.startsWith('.')) {
- name = name.replace('.', '');
- }
- }
-
- return name || 'bundle';
- }
-}
-
-function basenameWithoutExtension(file) {
- return path.basename(file, path.extname(file));
-}
diff --git a/packages/namers/default/src/DefaultNamer.ts b/packages/namers/default/src/DefaultNamer.ts
new file mode 100644
index 000000000..6cf105aa6
--- /dev/null
+++ b/packages/namers/default/src/DefaultNamer.ts
@@ -0,0 +1,146 @@
+import type {Bundle, FilePath} from '@atlaspack/types';
+
+import {Namer} from '@atlaspack/plugin';
+import ThrowableDiagnostic, {
+ convertSourceLocationToHighlight,
+ md,
+} from '@atlaspack/diagnostic';
+import assert from 'assert';
+import path from 'path';
+import nullthrows from 'nullthrows';
+
+const COMMON_NAMES = new Set(['index', 'src', 'lib']);
+const ALLOWED_EXTENSIONS = {
+ js: ['js', 'mjs', 'cjs'],
+} as const;
+
+export default new Namer({
+ name({bundle, bundleGraph}) {
+ let bundleGroup = bundleGraph.getBundleGroupsContainingBundle(bundle)[0];
+ let bundleGroupBundles = bundleGraph.getBundlesInBundleGroup(bundleGroup, {
+ includeInline: true,
+ });
+ let isEntry = bundleGraph.isEntryBundleGroup(bundleGroup);
+
+ if (bundle.needsStableName) {
+ let entryBundlesOfType = bundleGroupBundles.filter(
+ (b) => b.needsStableName && b.type === bundle.type,
+ );
+ assert(
+ entryBundlesOfType.length === 1,
+ // Otherwise, we'd end up naming two bundles the same thing.
+ `Bundle group cannot have more than one entry bundle of the same type. The offending bundle type is ${entryBundlesOfType[0].type}`,
+ );
+ }
+
+ let mainBundle = nullthrows(
+ bundleGroupBundles.find((b) =>
+ b.getEntryAssets().some((a) => a.id === bundleGroup.entryAssetId),
+ ),
+ );
+
+ if (
+ bundle.id === mainBundle.id &&
+ isEntry &&
+ bundle.target &&
+ bundle.target.distEntry != null
+ ) {
+ let loc = bundle.target.loc;
+ let distEntry = bundle.target.distEntry;
+ let distExtension = path.extname(bundle.target.distEntry).slice(1);
+ // @ts-expect-error - TS7053 - Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ readonly js: readonly ["js", "mjs", "cjs"]; }'.
+ let allowedExtensions = ALLOWED_EXTENSIONS[bundle.type] || [bundle.type];
+ if (!allowedExtensions.includes(distExtension) && loc) {
+ let fullName = path.relative(
+ path.dirname(loc.filePath),
+ path.join(bundle.target.distDir, distEntry),
+ );
+ let err = new ThrowableDiagnostic({
+ diagnostic: {
+ // @ts-expect-error - TS2345 - Argument of type 'TemplateStringsArray' is not assignable to parameter of type 'string[]'.
+ message: md`Target "${bundle.target.name}" declares an output file path of "${fullName}" which does not match the compiled bundle type "${bundle.type}".`,
+ codeFrames: [
+ {
+ filePath: loc.filePath,
+ codeHighlights: [
+ convertSourceLocationToHighlight(
+ loc,
+ // @ts-expect-error - TS2345 - Argument of type 'TemplateStringsArray' is not assignable to parameter of type 'string[]'.
+ md`Did you mean "${
+ fullName.slice(0, -path.extname(fullName).length) +
+ '.' +
+ bundle.type
+ }"?`,
+ ),
+ ],
+ },
+ ],
+ hints: [
+ `Try changing the file extension of "${
+ bundle.target.name
+ }" in ${path.relative(process.cwd(), loc.filePath)}.`,
+ ],
+ },
+ });
+ throw err;
+ }
+
+ return bundle.target.distEntry;
+ }
+
+ // Base split bundle names on the first bundle in their group.
+ // e.g. if `index.js` imports `foo.css`, the css bundle should be called
+ // `index.css`.
+ let name = nameFromContent(
+ mainBundle,
+ isEntry,
+ bundleGroup.entryAssetId,
+ bundleGraph.getEntryRoot(bundle.target),
+ );
+ if (!bundle.needsStableName) {
+ name += '.' + bundle.hashReference;
+ }
+
+ return name + '.' + bundle.type;
+ },
+}) as Namer;
+
+function nameFromContent(
+ bundle: Bundle,
+ isEntry: boolean,
+ entryAssetId: string,
+ entryRoot: FilePath,
+): string {
+ let entryFilePath = nullthrows(
+ bundle.getEntryAssets().find((a) => a.id === entryAssetId),
+ ).filePath;
+ let name = basenameWithoutExtension(entryFilePath);
+
+ // If this is an entry bundle, use the original relative path.
+ if (bundle.needsStableName) {
+ // Match name of target entry if possible, but with a different extension.
+ if (isEntry && bundle.target.distEntry != null) {
+ return basenameWithoutExtension(bundle.target.distEntry);
+ }
+
+ return path
+ .join(path.relative(entryRoot, path.dirname(entryFilePath)), name)
+ .replace(/\.\.(\/|\\)/g, 'up_$1');
+ } else {
+ // If this is an index file or common directory name, use the parent
+ // directory name instead, which is probably more descriptive.
+ while (COMMON_NAMES.has(name)) {
+ entryFilePath = path.dirname(entryFilePath);
+ name = path.basename(entryFilePath);
+ if (name.startsWith('.')) {
+ name = name.replace('.', '');
+ }
+ }
+
+ return name || 'bundle';
+ }
+}
+
+function basenameWithoutExtension(file: FilePath) {
+ return path.basename(file, path.extname(file));
+}
diff --git a/packages/optimizers/blob-url/package.json b/packages/optimizers/blob-url/package.json
index 6fd71ce28..5be4e5c3e 100644
--- a/packages/optimizers/blob-url/package.json
+++ b/packages/optimizers/blob-url/package.json
@@ -10,7 +10,8 @@
"url": "https://github.com/atlassian-labs/atlaspack.git"
},
"main": "lib/BlobURLOptimizer.js",
- "source": "src/BlobURLOptimizer.js",
+ "types": "src/BlobURLOptimizer.ts",
+ "source": "src/BlobURLOptimizer.ts",
"engines": {
"node": ">= 16.0.0",
"parcel": "^2.12.0"
diff --git a/packages/optimizers/blob-url/src/BlobURLOptimizer.js b/packages/optimizers/blob-url/src/BlobURLOptimizer.js
deleted file mode 100644
index ea57f2e28..000000000
--- a/packages/optimizers/blob-url/src/BlobURLOptimizer.js
+++ /dev/null
@@ -1,21 +0,0 @@
-// @flow strict-local
-
-import {Optimizer} from '@atlaspack/plugin';
-import {blobToString} from '@atlaspack/utils';
-
-export default (new Optimizer({
- async optimize({contents}) {
- // Inspired by webpack's worker plugin:
- // https://github.com/webpack-contrib/worker-loader/blob/b82585a1ddb8ae295fd4b1c302bca6b162665de2/src/workers/InlineWorker.js
- // which itself draws from:
- // http://stackoverflow.com/questions/10343913/how-to-create-a-web-worker-from-a-string
- //
- // This version only uses the Blob constructor, which is available in IE 10+:
- // https://developer.mozilla.org/en-US/docs/Web/API/Blob
- return {
- contents: `URL.createObjectURL(new Blob([${JSON.stringify(
- await blobToString(contents),
- )}]))`,
- };
- },
-}): Optimizer);
diff --git a/packages/optimizers/blob-url/src/BlobURLOptimizer.ts b/packages/optimizers/blob-url/src/BlobURLOptimizer.ts
new file mode 100644
index 000000000..a0d8e5228
--- /dev/null
+++ b/packages/optimizers/blob-url/src/BlobURLOptimizer.ts
@@ -0,0 +1,19 @@
+import {Optimizer} from '@atlaspack/plugin';
+import {blobToString} from '@atlaspack/utils';
+
+export default new Optimizer({
+ async optimize({contents}) {
+ // Inspired by webpack's worker plugin:
+ // https://github.com/webpack-contrib/worker-loader/blob/b82585a1ddb8ae295fd4b1c302bca6b162665de2/src/workers/InlineWorker.js
+ // which itself draws from:
+ // http://stackoverflow.com/questions/10343913/how-to-create-a-web-worker-from-a-string
+ //
+ // This version only uses the Blob constructor, which is available in IE 10+:
+ // https://developer.mozilla.org/en-US/docs/Web/API/Blob
+ return {
+ contents: `URL.createObjectURL(new Blob([${JSON.stringify(
+ await blobToString(contents),
+ )}]))`,
+ };
+ },
+}) as Optimizer;
diff --git a/packages/optimizers/css/package.json b/packages/optimizers/css/package.json
index ff663d146..fab316787 100644
--- a/packages/optimizers/css/package.json
+++ b/packages/optimizers/css/package.json
@@ -10,7 +10,8 @@
"url": "https://github.com/atlassian-labs/atlaspack.git"
},
"main": "lib/CSSOptimizer.js",
- "source": "src/CSSOptimizer.js",
+ "types": "src/CSSOptimizer.ts",
+ "source": "src/CSSOptimizer.ts",
"engines": {
"node": ">= 16.0.0",
"parcel": "^2.12.0"
diff --git a/packages/optimizers/css/src/CSSOptimizer.js b/packages/optimizers/css/src/CSSOptimizer.js
deleted file mode 100644
index 8312cc5db..000000000
--- a/packages/optimizers/css/src/CSSOptimizer.js
+++ /dev/null
@@ -1,219 +0,0 @@
-// @flow strict-local
-
-import SourceMap from '@parcel/source-map';
-import {Optimizer} from '@atlaspack/plugin';
-// $FlowFixMe - init for browser build.
-import init, {
- transform,
- transformStyleAttribute,
- browserslistToTargets,
-} from 'lightningcss';
-import {blobToBuffer} from '@atlaspack/utils';
-import browserslist from 'browserslist';
-import nullthrows from 'nullthrows';
-import path from 'path';
-import {
- convertSourceLocationToHighlight,
- md,
- generateJSONCodeHighlights,
-} from '@atlaspack/diagnostic';
-
-export default (new Optimizer({
- async loadConfig({config, logger, options}) {
- const configFile = await config.getConfig(
- [
- '.cssnanorc',
- 'cssnano.config.json',
- 'cssnano.config.js',
- 'cssnano.config.cjs',
- ],
- {
- packageKey: 'cssnano',
- },
- );
- if (configFile) {
- let filename = path.basename(configFile.filePath);
- let codeHighlights;
- let message;
- if (filename === 'package.json') {
- message = md`
-Atlaspack\'s default CSS minifer changed from cssnano to lightningcss, but a "cssnano" key was found in **package.json**. Either remove this configuration, or configure Parcel to use @atlaspack/optimizer-cssnano instead.
- `;
- let contents = await options.inputFS.readFile(
- configFile.filePath,
- 'utf8',
- );
- codeHighlights = generateJSONCodeHighlights(contents, [
- {key: '/cssnano', type: 'key'},
- ]);
- } else {
- message = md`Parcel\'s default CSS minifer changed from cssnano to lightningcss, but a __${filename}__ config file was found. Either remove this config file, or configure Parcel to use @atlaspack/optimizer-cssnano instead.`;
- codeHighlights = [
- {
- start: {line: 1, column: 1},
- end: {line: 1, column: 1},
- },
- ];
- }
-
- logger.warn({
- message,
- documentationURL: 'https://parceljs.org/languages/css/#minification',
- codeFrames: [
- {
- filePath: configFile.filePath,
- codeHighlights,
- },
- ],
- });
- }
- },
- async optimize({
- bundle,
- bundleGraph,
- logger,
- contents: prevContents,
- getSourceMapReference,
- map: prevMap,
- options,
- }) {
- if (!bundle.env.shouldOptimize) {
- return {contents: prevContents, map: prevMap};
- }
-
- let targets = getTargets(bundle.env.engines.browsers);
- let code = await blobToBuffer(prevContents);
-
- let unusedSymbols;
- if (bundle.env.shouldScopeHoist) {
- unusedSymbols = [];
- bundle.traverseAssets(asset => {
- if (
- asset.symbols.isCleared ||
- asset.meta.cssModulesCompiled === 'postcss'
- ) {
- return;
- }
-
- let usedSymbols = bundleGraph.getUsedSymbols(asset);
- if (usedSymbols == null) {
- return;
- }
-
- let defaultImport = null;
- if (usedSymbols.has('default')) {
- let incoming = bundleGraph.getIncomingDependencies(asset);
- defaultImport = incoming.find(d =>
- d.symbols.hasExportSymbol('default'),
- );
- if (defaultImport) {
- let loc = defaultImport.symbols.get('default')?.loc;
- logger.warn({
- message:
- 'CSS modules cannot be tree shaken when imported with a default specifier',
- ...(loc && {
- codeFrames: [
- {
- filePath: nullthrows(
- loc?.filePath ?? defaultImport.sourcePath,
- ),
- codeHighlights: [convertSourceLocationToHighlight(loc)],
- },
- ],
- }),
- hints: [
- `Instead do: import * as style from "${defaultImport.specifier}";`,
- ],
- documentationURL:
- 'https://parceljs.org/languages/css/#tree-shaking',
- });
- }
- }
-
- if (!defaultImport && !usedSymbols.has('*')) {
- for (let [symbol, {local}] of asset.symbols) {
- if (local !== 'default' && !usedSymbols.has(symbol)) {
- unusedSymbols.push(local);
- }
- }
- }
- });
- }
-
- // Inline style attributes in HTML need to be parsed differently from full CSS files.
- if (bundle.bundleBehavior === 'inline') {
- let entry = bundle.getMainEntry();
- if (entry?.meta.type === 'attr') {
- let result = transformStyleAttribute({
- code,
- minify: true,
- targets,
- });
-
- return {
- contents: Buffer.from(result.code),
- };
- }
- }
-
- // $FlowFixMe
- if (process.browser) {
- await init();
- }
-
- let result = transform({
- filename: bundle.name,
- code,
- minify: true,
- sourceMap: !!bundle.env.sourceMap,
- targets,
- unusedSymbols,
- });
-
- let map;
- if (result.map != null) {
- let vlqMap = JSON.parse(Buffer.from(result.map).toString());
- map = new SourceMap(options.projectRoot);
- map.addVLQMap(vlqMap);
- if (prevMap) {
- map.extends(prevMap);
- }
- }
-
- let contents = Buffer.from(result.code);
- if (bundle.env.sourceMap) {
- let reference = await getSourceMapReference(map);
- if (reference != null) {
- contents =
- contents.toString() +
- '\n' +
- '/*# sourceMappingURL=' +
- reference +
- ' */\n';
- }
- }
-
- return {
- contents: Buffer.from(contents),
- map,
- };
- },
-}): Optimizer);
-
-let cache = new Map();
-
-function getTargets(browsers) {
- if (browsers == null) {
- return undefined;
- }
-
- let cached = cache.get(browsers);
- if (cached != null) {
- return cached;
- }
-
- let targets = browserslistToTargets(browserslist(browsers));
-
- cache.set(browsers, targets);
- return targets;
-}
diff --git a/packages/optimizers/css/src/CSSOptimizer.ts b/packages/optimizers/css/src/CSSOptimizer.ts
new file mode 100644
index 000000000..edb428914
--- /dev/null
+++ b/packages/optimizers/css/src/CSSOptimizer.ts
@@ -0,0 +1,224 @@
+import SourceMap from '@parcel/source-map';
+import {Optimizer} from '@atlaspack/plugin';
+import init, {
+ transform,
+ transformStyleAttribute,
+ browserslistToTargets,
+} from 'lightningcss';
+import {blobToBuffer} from '@atlaspack/utils';
+import browserslist from 'browserslist';
+import nullthrows from 'nullthrows';
+import path from 'path';
+import {
+ convertSourceLocationToHighlight,
+ md,
+ generateJSONCodeHighlights,
+} from '@atlaspack/diagnostic';
+
+export default new Optimizer({
+ async loadConfig({config, logger, options}) {
+ const configFile = await config.getConfig(
+ [
+ '.cssnanorc',
+ 'cssnano.config.json',
+ 'cssnano.config.js',
+ 'cssnano.config.cjs',
+ ],
+ {
+ packageKey: 'cssnano',
+ },
+ );
+ if (configFile) {
+ let filename = path.basename(configFile.filePath);
+ let codeHighlights;
+ let message;
+ if (filename === 'package.json') {
+ // @ts-expect-error - TS2345 - Argument of type 'TemplateStringsArray' is not assignable to parameter of type 'string[]'.
+ message = md`
+Atlaspack\'s default CSS minifer changed from cssnano to lightningcss, but a "cssnano" key was found in **package.json**. Either remove this configuration, or configure Parcel to use @atlaspack/optimizer-cssnano instead.
+ `;
+ let contents = await options.inputFS.readFile(
+ configFile.filePath,
+ 'utf8',
+ );
+ codeHighlights = generateJSONCodeHighlights(contents, [
+ {key: '/cssnano', type: 'key'},
+ ]);
+ } else {
+ // @ts-expect-error - TS2345 - Argument of type 'TemplateStringsArray' is not assignable to parameter of type 'string[]'.
+ message = md`Parcel\'s default CSS minifer changed from cssnano to lightningcss, but a __${filename}__ config file was found. Either remove this config file, or configure Parcel to use @atlaspack/optimizer-cssnano instead.`;
+ codeHighlights = [
+ {
+ start: {line: 1, column: 1},
+ end: {line: 1, column: 1},
+ },
+ ];
+ }
+
+ logger.warn({
+ message,
+ documentationURL: 'https://parceljs.org/languages/css/#minification',
+ codeFrames: [
+ {
+ filePath: configFile.filePath,
+ codeHighlights,
+ },
+ ],
+ });
+ }
+ },
+ async optimize({
+ bundle,
+ bundleGraph,
+ logger,
+ contents: prevContents,
+ getSourceMapReference,
+ map: prevMap,
+ options,
+ }) {
+ if (!bundle.env.shouldOptimize) {
+ return {contents: prevContents, map: prevMap};
+ }
+
+ let targets = getTargets(bundle.env.engines.browsers);
+ let code = await blobToBuffer(prevContents);
+
+ let unusedSymbols;
+ if (bundle.env.shouldScopeHoist) {
+ unusedSymbols = [];
+ bundle.traverseAssets((asset) => {
+ if (
+ asset.symbols.isCleared ||
+ asset.meta.cssModulesCompiled === 'postcss'
+ ) {
+ return;
+ }
+
+ let usedSymbols = bundleGraph.getUsedSymbols(asset);
+ if (usedSymbols == null) {
+ return;
+ }
+
+ let defaultImport = null;
+ if (usedSymbols.has('default')) {
+ let incoming = bundleGraph.getIncomingDependencies(asset);
+ defaultImport = incoming.find((d) =>
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'symbol'.
+ d.symbols.hasExportSymbol('default'),
+ );
+ if (defaultImport) {
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'symbol'.
+ let loc = defaultImport.symbols.get('default')?.loc;
+ logger.warn({
+ message:
+ 'CSS modules cannot be tree shaken when imported with a default specifier',
+ ...(loc && {
+ codeFrames: [
+ {
+ filePath: nullthrows(
+ loc?.filePath ?? defaultImport.sourcePath,
+ ),
+ codeHighlights: [convertSourceLocationToHighlight(loc)],
+ },
+ ],
+ }),
+ hints: [
+ `Instead do: import * as style from "${defaultImport.specifier}";`,
+ ],
+ documentationURL:
+ 'https://parceljs.org/languages/css/#tree-shaking',
+ });
+ }
+ }
+
+ if (!defaultImport && !usedSymbols.has('*')) {
+ for (let [symbol, {local}] of asset.symbols) {
+ // @ts-expect-error - TS2367 - This condition will always return 'true' since the types 'symbol' and 'string' have no overlap.
+ if (local !== 'default' && !usedSymbols.has(symbol)) {
+ unusedSymbols.push(local);
+ }
+ }
+ }
+ });
+ }
+
+ // Inline style attributes in HTML need to be parsed differently from full CSS files.
+ if (bundle.bundleBehavior === 'inline') {
+ let entry = bundle.getMainEntry();
+ if (entry?.meta.type === 'attr') {
+ let result = transformStyleAttribute({
+ code,
+ minify: true,
+ targets,
+ });
+
+ return {
+ contents: Buffer.from(result.code),
+ };
+ }
+ }
+
+ // @ts-expect-error - TS2339 - Property 'browser' does not exist on type 'Process'.
+ if (process.browser) {
+ // @ts-expect-error - TS2349 - This expression is not callable.
+ await init();
+ }
+
+ let result = transform({
+ filename: bundle.name,
+ code,
+ minify: true,
+ sourceMap: !!bundle.env.sourceMap,
+ targets,
+ unusedSymbols,
+ });
+
+ let map;
+ if (result.map != null) {
+ let vlqMap = JSON.parse(Buffer.from(result.map).toString());
+ map = new SourceMap(options.projectRoot);
+ map.addVLQMap(vlqMap);
+ if (prevMap) {
+ // @ts-expect-error - TS2345 - Argument of type 'SourceMap' is not assignable to parameter of type 'Buffer'.
+ map.extends(prevMap);
+ }
+ }
+
+ let contents = Buffer.from(result.code);
+ if (bundle.env.sourceMap) {
+ let reference = await getSourceMapReference(map);
+ if (reference != null) {
+ // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'Buffer'.
+ contents =
+ contents.toString() +
+ '\n' +
+ '/*# sourceMappingURL=' +
+ reference +
+ ' */\n';
+ }
+ }
+
+ return {
+ contents: Buffer.from(contents),
+ map,
+ };
+ },
+}) as Optimizer;
+
+let cache = new Map();
+
+function getTargets(browsers: undefined | string | Array) {
+ if (browsers == null) {
+ return undefined;
+ }
+
+ let cached = cache.get(browsers);
+ if (cached != null) {
+ return cached;
+ }
+
+ let targets = browserslistToTargets(browserslist(browsers));
+
+ cache.set(browsers, targets);
+ return targets;
+}
diff --git a/packages/optimizers/cssnano/package.json b/packages/optimizers/cssnano/package.json
index b7026b9cb..4f6b26311 100644
--- a/packages/optimizers/cssnano/package.json
+++ b/packages/optimizers/cssnano/package.json
@@ -10,7 +10,8 @@
"url": "https://github.com/atlassian-labs/atlaspack.git"
},
"main": "lib/CSSNanoOptimizer.js",
- "source": "src/CSSNanoOptimizer.js",
+ "types": "src/CSSNanoOptimizer.ts",
+ "source": "src/CSSNanoOptimizer.ts",
"engines": {
"node": ">= 16.0.0",
"parcel": "^2.12.0"
diff --git a/packages/optimizers/cssnano/src/CSSNanoOptimizer.js b/packages/optimizers/cssnano/src/CSSNanoOptimizer.js
deleted file mode 100644
index 9abe2fb25..000000000
--- a/packages/optimizers/cssnano/src/CSSNanoOptimizer.js
+++ /dev/null
@@ -1,78 +0,0 @@
-// @flow strict-local
-
-import SourceMap from '@parcel/source-map';
-import {Optimizer} from '@atlaspack/plugin';
-import postcss from 'postcss';
-import cssnano from 'cssnano';
-import type {CSSNanoOptions} from 'cssnano'; // TODO the type is based on cssnano 4
-
-export default (new Optimizer({
- async loadConfig({config}) {
- const configFile = await config.getConfig(
- [
- '.cssnanorc',
- 'cssnano.config.json',
- 'cssnano.config.js',
- 'cssnano.config.cjs',
- 'cssnano.config.mjs',
- ],
- {
- packageKey: 'cssnano',
- },
- );
- if (configFile) {
- return configFile.contents;
- }
- },
-
- async optimize({
- bundle,
- contents: prevContents,
- getSourceMapReference,
- map: prevMap,
- config,
- options,
- }) {
- if (!bundle.env.shouldOptimize) {
- return {contents: prevContents, map: prevMap};
- }
-
- if (typeof prevContents !== 'string') {
- throw new Error(
- 'CSSNanoOptimizer: Only string contents are currently supported',
- );
- }
-
- const result = await postcss([
- cssnano((config ?? {}: CSSNanoOptions)),
- ]).process(prevContents, {
- // Suppress postcss's warning about a missing `from` property. In this
- // case, the input map contains all of the sources.
- from: undefined,
- map: {
- annotation: false,
- inline: false,
- prev: prevMap ? await prevMap.stringify({}) : null,
- },
- });
-
- let map;
- if (result.map != null) {
- map = new SourceMap(options.projectRoot);
- map.addVLQMap(result.map.toJSON());
- }
-
- let contents = result.css;
- if (bundle.env.sourceMap) {
- let reference = await getSourceMapReference(map);
- if (reference != null) {
- contents += '\n' + '/*# sourceMappingURL=' + reference + ' */\n';
- }
- }
-
- return {
- contents,
- map,
- };
- },
-}): Optimizer);
diff --git a/packages/optimizers/cssnano/src/CSSNanoOptimizer.ts b/packages/optimizers/cssnano/src/CSSNanoOptimizer.ts
new file mode 100644
index 000000000..4bf389413
--- /dev/null
+++ b/packages/optimizers/cssnano/src/CSSNanoOptimizer.ts
@@ -0,0 +1,80 @@
+import SourceMap from '@parcel/source-map';
+import {Optimizer} from '@atlaspack/plugin';
+import postcss from 'postcss';
+// @ts-expect-error - TS7016 - Could not find a declaration file for module 'cssnano'. '/home/ubuntu/parcel/node_modules/cssnano/dist/index.js' implicitly has an 'any' type.
+import cssnano from 'cssnano';
+// @ts-expect-error - TS7016 - Could not find a declaration file for module 'cssnano'. '/home/ubuntu/parcel/node_modules/cssnano/dist/index.js' implicitly has an 'any' type.
+import type {CSSNanoOptions} from 'cssnano'; // TODO the type is based on cssnano 4
+
+export default new Optimizer({
+ async loadConfig({config}) {
+ const configFile = await config.getConfig(
+ [
+ '.cssnanorc',
+ 'cssnano.config.json',
+ 'cssnano.config.js',
+ 'cssnano.config.cjs',
+ 'cssnano.config.mjs',
+ ],
+ {
+ packageKey: 'cssnano',
+ },
+ );
+ if (configFile) {
+ return configFile.contents;
+ }
+ },
+
+ async optimize({
+ bundle,
+ contents: prevContents,
+ getSourceMapReference,
+ map: prevMap,
+ config,
+ options,
+ }) {
+ if (!bundle.env.shouldOptimize) {
+ return {contents: prevContents, map: prevMap};
+ }
+
+ if (typeof prevContents !== 'string') {
+ throw new Error(
+ 'CSSNanoOptimizer: Only string contents are currently supported',
+ );
+ }
+
+ const result = await postcss([
+ cssnano(config ?? ({} as CSSNanoOptions)),
+ ]).process(prevContents, {
+ // Suppress postcss's warning about a missing `from` property. In this
+ // case, the input map contains all of the sources.
+ from: undefined,
+ map: {
+ annotation: false,
+ inline: false,
+ // @ts-expect-error - TS2322 - Type 'string | Readonly<{ sources: readonly string[]; sourcesContent?: readonly (string | null)[] | undefined; names: readonly string[]; mappings: string; version?: number | undefined; file?: string | undefined; sourceRoot?: string | undefined; }> | null' is not assignable to type 'string | boolean | object | ((file: string) => string) | undefined'.
+ prev: prevMap ? await prevMap.stringify({}) : null,
+ },
+ });
+
+ let map;
+ if (result.map != null) {
+ map = new SourceMap(options.projectRoot);
+ // @ts-expect-error - TS2345 - Argument of type 'RawSourceMap' is not assignable to parameter of type 'Readonly<{ sources: readonly string[]; sourcesContent?: readonly (string | null)[] | undefined; names: readonly string[]; mappings: string; version?: number | undefined; file?: string | undefined; sourceRoot?: string | undefined; }>'.
+ map.addVLQMap(result.map.toJSON());
+ }
+
+ let contents = result.css;
+ if (bundle.env.sourceMap) {
+ let reference = await getSourceMapReference(map);
+ if (reference != null) {
+ contents += '\n' + '/*# sourceMappingURL=' + reference + ' */\n';
+ }
+ }
+
+ return {
+ contents,
+ map,
+ };
+ },
+}) as Optimizer;
diff --git a/packages/optimizers/data-url/package.json b/packages/optimizers/data-url/package.json
index 5e1a87486..0ab2eff0a 100644
--- a/packages/optimizers/data-url/package.json
+++ b/packages/optimizers/data-url/package.json
@@ -10,7 +10,8 @@
"url": "https://github.com/atlassian-labs/atlaspack.git"
},
"main": "lib/DataURLOptimizer.js",
- "source": "src/DataURLOptimizer.js",
+ "types": "src/DataURLOptimizer.ts",
+ "source": "src/DataURLOptimizer.ts",
"engines": {
"node": ">= 16.0.0",
"parcel": "^2.12.0"
diff --git a/packages/optimizers/data-url/src/DataURLOptimizer.js b/packages/optimizers/data-url/src/DataURLOptimizer.js
deleted file mode 100644
index 164ee859d..000000000
--- a/packages/optimizers/data-url/src/DataURLOptimizer.js
+++ /dev/null
@@ -1,32 +0,0 @@
-// @flow strict-local
-
-import {Optimizer} from '@atlaspack/plugin';
-import {blobToBuffer} from '@atlaspack/utils';
-import mime from 'mime';
-import {isBinaryFile} from 'isbinaryfile';
-
-const fixedEncodeURIComponent = (str: string): string => {
- return encodeURIComponent(str).replace(/[!'()*]/g, function (c) {
- return '%' + c.charCodeAt(0).toString(16);
- });
-};
-
-export default (new Optimizer({
- async optimize({bundle, contents}) {
- let bufferContents = await blobToBuffer(contents);
- let hasBinaryContent = await isBinaryFile(bufferContents);
-
- // Follows the data url format referenced here:
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
- let mimeType = mime.getType(bundle.name) ?? '';
- let encoding = hasBinaryContent ? ';base64' : '';
- let content = fixedEncodeURIComponent(
- hasBinaryContent
- ? bufferContents.toString('base64')
- : bufferContents.toString(),
- );
- return {
- contents: `data:${mimeType}${encoding},${content}`,
- };
- },
-}): Optimizer);
diff --git a/packages/optimizers/data-url/src/DataURLOptimizer.ts b/packages/optimizers/data-url/src/DataURLOptimizer.ts
new file mode 100644
index 000000000..9036d222a
--- /dev/null
+++ b/packages/optimizers/data-url/src/DataURLOptimizer.ts
@@ -0,0 +1,31 @@
+import {Optimizer} from '@atlaspack/plugin';
+import {blobToBuffer} from '@atlaspack/utils';
+// @ts-expect-error - TS7016 - Could not find a declaration file for module 'mime'. '/home/ubuntu/parcel/packages/optimizers/data-url/node_modules/mime/index.js' implicitly has an 'any' type.
+import mime from 'mime';
+import {isBinaryFile} from 'isbinaryfile';
+
+const fixedEncodeURIComponent = (str: string): string => {
+ return encodeURIComponent(str).replace(/[!'()*]/g, function (c) {
+ return '%' + c.charCodeAt(0).toString(16);
+ });
+};
+
+export default new Optimizer({
+ async optimize({bundle, contents}) {
+ let bufferContents = await blobToBuffer(contents);
+ let hasBinaryContent = await isBinaryFile(bufferContents);
+
+ // Follows the data url format referenced here:
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
+ let mimeType = mime.getType(bundle.name) ?? '';
+ let encoding = hasBinaryContent ? ';base64' : '';
+ let content = fixedEncodeURIComponent(
+ hasBinaryContent
+ ? bufferContents.toString('base64')
+ : bufferContents.toString(),
+ );
+ return {
+ contents: `data:${mimeType}${encoding},${content}`,
+ };
+ },
+}) as Optimizer;
diff --git a/packages/optimizers/htmlnano/package.json b/packages/optimizers/htmlnano/package.json
index faaebf1d8..052ca0d4b 100644
--- a/packages/optimizers/htmlnano/package.json
+++ b/packages/optimizers/htmlnano/package.json
@@ -10,7 +10,8 @@
"url": "https://github.com/atlassian-labs/atlaspack.git"
},
"main": "lib/HTMLNanoOptimizer.js",
- "source": "src/HTMLNanoOptimizer.js",
+ "types": "src/HTMLNanoOptimizer.ts",
+ "source": "src/HTMLNanoOptimizer.ts",
"engines": {
"node": ">= 16.0.0",
"parcel": "^2.12.0"
diff --git a/packages/optimizers/htmlnano/src/HTMLNanoOptimizer.js b/packages/optimizers/htmlnano/src/HTMLNanoOptimizer.js
deleted file mode 100644
index 0a7c236ff..000000000
--- a/packages/optimizers/htmlnano/src/HTMLNanoOptimizer.js
+++ /dev/null
@@ -1,160 +0,0 @@
-// @flow strict-local
-import type {PostHTMLNode} from 'posthtml';
-
-import htmlnano from 'htmlnano';
-import {Optimizer} from '@atlaspack/plugin';
-import posthtml from 'posthtml';
-import path from 'path';
-import {SVG_ATTRS, SVG_TAG_NAMES} from './svgMappings';
-
-export default (new Optimizer({
- async loadConfig({config, options}) {
- let userConfig = await config.getConfigFrom(
- path.join(options.projectRoot, 'index.html'),
- [
- '.htmlnanorc',
- '.htmlnanorc.json',
- '.htmlnanorc.js',
- '.htmlnanorc.cjs',
- '.htmlnanorc.mjs',
- 'htmlnano.config.js',
- 'htmlnano.config.cjs',
- 'htmlnano.config.mjs',
- ],
- {
- packageKey: 'htmlnano',
- },
- );
-
- return userConfig?.contents;
- },
- async optimize({bundle, contents, map, config}) {
- if (!bundle.env.shouldOptimize) {
- return {contents, map};
- }
-
- if (typeof contents !== 'string') {
- throw new Error(
- 'HTMLNanoOptimizer: Only string contents are currently supported',
- );
- }
-
- const clonedConfig = config || {};
-
- // $FlowFixMe
- const presets = htmlnano.presets;
- const preset =
- typeof clonedConfig.preset === 'string'
- ? presets[clonedConfig.preset]
- : {};
- delete clonedConfig.preset;
-
- const htmlNanoConfig = {
- // Inline {
- res = node;
- return node;
- });
-
- return res;
-}
-
-function findBundleInsertIndex(content) {
- // HTML document order (https://html.spec.whatwg.org/multipage/syntax.html#writing)
- // - Any number of comments and ASCII whitespace.
- // - A DOCTYPE.
- // - Any number of comments and ASCII whitespace.
- // - The document element, in the form of an html element.
- // - Any number of comments and ASCII whitespace.
- //
- // -> Insert before first non-metadata (or script) element; if none was found, after the doctype
-
- let doctypeIndex;
- for (let index = 0; index < content.length; index++) {
- const node = content[index];
- if (node && node.tag && !metadataContent.has(node.tag)) {
- return index;
- }
- if (
- typeof node === 'string' &&
- node.toLowerCase().startsWith(' = [];
+ bundle.traverseAssets((asset) => {
+ assets.push(asset);
+ });
+
+ assert.equal(assets.length, 1, 'HTML bundles must only contain one asset');
+
+ let asset = assets[0];
+ let code = await asset.getCode();
+
+ // Add bundles in the same bundle group that are not inline. For example, if two inline
+ // bundles refer to the same library that is extracted into a shared bundle.
+ let referencedBundles = [
+ ...setDifference(
+ new Set(bundleGraph.getReferencedBundles(bundle)),
+ new Set(bundleGraph.getReferencedBundles(bundle, {recursive: false})),
+ ),
+ ];
+ // @ts-expect-error - TS2571 - Object is of type 'unknown'.
+ let renderConfig = config?.render;
+
+ let {html} = await posthtml([
+ // @ts-expect-error - TS2345 - Argument of type 'unknown[]' is not assignable to parameter of type 'NamedBundle[]'.
+ (tree: any) => insertBundleReferences(referencedBundles, tree),
+ (tree: any) =>
+ replaceInlineAssetContent(bundleGraph, getInlineBundleContents, tree),
+ ]).process(code, {
+ ...renderConfig,
+ xmlMode: bundle.type === 'xhtml',
+ closingSingleTag: bundle.type === 'xhtml' ? 'slash' : undefined,
+ });
+
+ let {contents, map} = replaceURLReferences({
+ bundle,
+ bundleGraph,
+ contents: html,
+ relative: false,
+ getReplacement: (contents) => contents.replace(/"/g, '"'),
+ });
+
+ return replaceInlineReferences({
+ bundle,
+ bundleGraph,
+ contents,
+ getInlineBundleContents,
+ getInlineReplacement: (dep, inlineType, contents) => ({
+ from: dep.id,
+ to: contents.replace(/"/g, '"').trim(),
+ }),
+ map,
+ });
+ },
+}) as Packager;
+
+async function getAssetContent(
+ bundleGraph: BundleGraph,
+ getInlineBundleContents: (
+ arg1: Bundle,
+ arg2: BundleGraph,
+ ) => Async<{
+ contents: Blob;
+ }>,
+ assetId: any,
+) {
+ let inlineBundle: Bundle | null | undefined;
+ bundleGraph.traverseBundles((bundle, context, {stop}) => {
+ let entryAssets = bundle.getEntryAssets();
+ if (entryAssets.some((a) => a.uniqueKey === assetId)) {
+ inlineBundle = bundle;
+ stop();
+ }
+ });
+
+ if (inlineBundle) {
+ const bundleResult = await getInlineBundleContents(
+ inlineBundle,
+ bundleGraph,
+ );
+
+ return {bundle: inlineBundle, contents: bundleResult.contents};
+ }
+
+ return null;
+}
+
+async function replaceInlineAssetContent(
+ bundleGraph: BundleGraph,
+ getInlineBundleContents: (
+ arg1: Bundle,
+ arg2: BundleGraph,
+ ) => Async<{
+ contents: Blob;
+ }>,
+ tree: any,
+) {
+ const inlineNodes: Array = [];
+ // @ts-expect-error - TS7006 - Parameter 'node' implicitly has an 'any' type.
+ tree.walk((node) => {
+ if (node.attrs && node.attrs['data-parcel-key']) {
+ inlineNodes.push(node);
+ }
+ return node;
+ });
+
+ for (let node of inlineNodes) {
+ let newContent = await getAssetContent(
+ bundleGraph,
+ getInlineBundleContents,
+ node.attrs['data-parcel-key'],
+ );
+
+ if (newContent != null) {
+ let {contents, bundle} = newContent;
+ node.content = (
+ contents instanceof Readable ? await bufferStream(contents) : contents
+ ).toString();
+
+ if (
+ node.tag === 'script' &&
+ nullthrows(bundle).env.outputFormat === 'esmodule'
+ ) {
+ node.attrs.type = 'module';
+ }
+
+ // Escape closing script tags and HTML comments in JS content.
+ // https://www.w3.org/TR/html52/semantics-scripting.html#restrictions-for-contents-of-script-elements
+ // Avoid replacing , tree: any) {
+ const bundles = [];
+
+ for (let bundle of siblingBundles) {
+ if (bundle.type === 'css') {
+ bundles.push({
+ tag: 'link',
+ attrs: {
+ rel: 'stylesheet',
+ href: urlJoin(bundle.target.publicUrl, bundle.name),
+ },
+ });
+ } else if (bundle.type === 'js') {
+ let nomodule =
+ bundle.env.outputFormat !== 'esmodule' &&
+ bundle.env.sourceType === 'module' &&
+ bundle.env.shouldScopeHoist;
+ bundles.push({
+ tag: 'script',
+ attrs: {
+ type: bundle.env.outputFormat === 'esmodule' ? 'module' : undefined,
+ nomodule: nomodule ? '' : undefined,
+ defer: nomodule ? '' : undefined,
+ src: urlJoin(bundle.target.publicUrl, bundle.name),
+ },
+ });
+ }
+ }
+
+ addBundlesToTree(bundles, tree);
+}
+
+// @ts-expect-error - TS7006 - Parameter 'bundles' implicitly has an 'any' type.
+function addBundlesToTree(bundles, tree: any) {
+ const main = find(tree, 'head') || find(tree, 'html');
+ // @ts-expect-error - TS2339 - Property 'content' does not exist on type 'never'. | TS2339 - Property 'content' does not exist on type 'never'.
+ const content = main ? main.content || (main.content = []) : tree;
+ const index = findBundleInsertIndex(content);
+
+ content.splice(index, 0, ...bundles);
+}
+
+function find(tree: any, tag: string) {
+ let res;
+ // @ts-expect-error - TS7006 - Parameter 'node' implicitly has an 'any' type.
+ tree.match({tag}, (node) => {
+ res = node;
+ return node;
+ });
+
+ return res;
+}
+
+function findBundleInsertIndex(content: any) {
+ // HTML document order (https://html.spec.whatwg.org/multipage/syntax.html#writing)
+ // - Any number of comments and ASCII whitespace.
+ // - A DOCTYPE.
+ // - Any number of comments and ASCII whitespace.
+ // - The document element, in the form of an html element.
+ // - Any number of comments and ASCII whitespace.
+ //
+ // -> Insert before first non-metadata (or script) element; if none was found, after the doctype
+
+ let doctypeIndex;
+ for (let index = 0; index < content.length; index++) {
+ const node = content[index];
+ if (node && node.tag && !metadataContent.has(node.tag)) {
+ return index;
+ }
+ if (
+ typeof node === 'string' &&
+ node.toLowerCase().startsWith('= 16.0.0",
"parcel": "^2.12.0"
diff --git a/packages/packagers/js/src/CJSOutputFormat.js b/packages/packagers/js/src/CJSOutputFormat.js
deleted file mode 100644
index 899341f43..000000000
--- a/packages/packagers/js/src/CJSOutputFormat.js
+++ /dev/null
@@ -1,42 +0,0 @@
-// @flow
-import type {
- ScopeHoistingPackager,
- OutputFormat,
-} from './ScopeHoistingPackager';
-
-export class CJSOutputFormat implements OutputFormat {
- packager: ScopeHoistingPackager;
-
- constructor(packager: ScopeHoistingPackager) {
- this.packager = packager;
- }
-
- buildBundlePrelude(): [string, number] {
- let res = '';
- let lines = 0;
-
- for (let [source, specifiers] of this.packager.externals) {
- // CJS only supports the namespace symbol. This ensures that all accesses
- // are live and the `this` binding is correct.
- let namespace = specifiers.get('*');
- if (namespace) {
- res += `var ${namespace} = require(${JSON.stringify(source)});\n`;
- lines++;
- } else {
- res += `require(${JSON.stringify(source)});\n`;
- lines++;
- }
- }
-
- if (res.length > 0) {
- res += '\n';
- lines++;
- }
-
- return [res, lines];
- }
-
- buildBundlePostlude(): [string, number] {
- return ['', 0];
- }
-}
diff --git a/packages/packagers/js/src/CJSOutputFormat.ts b/packages/packagers/js/src/CJSOutputFormat.ts
new file mode 100644
index 000000000..ec2236d64
--- /dev/null
+++ b/packages/packagers/js/src/CJSOutputFormat.ts
@@ -0,0 +1,41 @@
+import type {
+ ScopeHoistingPackager,
+ OutputFormat,
+} from './ScopeHoistingPackager';
+
+export class CJSOutputFormat implements OutputFormat {
+ packager: ScopeHoistingPackager;
+
+ constructor(packager: ScopeHoistingPackager) {
+ this.packager = packager;
+ }
+
+ buildBundlePrelude(): [string, number] {
+ let res = '';
+ let lines = 0;
+
+ for (let [source, specifiers] of this.packager.externals) {
+ // CJS only supports the namespace symbol. This ensures that all accesses
+ // are live and the `this` binding is correct.
+ let namespace = specifiers.get('*');
+ if (namespace) {
+ res += `var ${namespace} = require(${JSON.stringify(source)});\n`;
+ lines++;
+ } else {
+ res += `require(${JSON.stringify(source)});\n`;
+ lines++;
+ }
+ }
+
+ if (res.length > 0) {
+ res += '\n';
+ lines++;
+ }
+
+ return [res, lines];
+ }
+
+ buildBundlePostlude(): [string, number] {
+ return ['', 0];
+ }
+}
diff --git a/packages/packagers/js/src/DevPackager.js b/packages/packagers/js/src/DevPackager.js
deleted file mode 100644
index ea0ee0cbd..000000000
--- a/packages/packagers/js/src/DevPackager.js
+++ /dev/null
@@ -1,252 +0,0 @@
-// @flow strict-local
-import type {BundleGraph, PluginOptions, NamedBundle} from '@atlaspack/types';
-
-import {
- PromiseQueue,
- relativeBundlePath,
- countLines,
- normalizeSeparators,
-} from '@atlaspack/utils';
-import SourceMap from '@parcel/source-map';
-import invariant from 'assert';
-import path from 'path';
-import fs from 'fs';
-import {replaceScriptDependencies, getSpecifier} from './utils';
-
-const PRELUDE = fs
- .readFileSync(path.join(__dirname, 'dev-prelude.js'), 'utf8')
- .trim()
- .replace(/;$/, '');
-
-export class DevPackager {
- options: PluginOptions;
- bundleGraph: BundleGraph;
- bundle: NamedBundle;
- parcelRequireName: string;
-
- constructor(
- options: PluginOptions,
- bundleGraph: BundleGraph,
- bundle: NamedBundle,
- parcelRequireName: string,
- ) {
- this.options = options;
- this.bundleGraph = bundleGraph;
- this.bundle = bundle;
- this.parcelRequireName = parcelRequireName;
- }
-
- async package(): Promise<{|contents: string, map: ?SourceMap|}> {
- // Load assets
- let queue = new PromiseQueue({maxConcurrent: 32});
- this.bundle.traverseAssets(asset => {
- queue.add(async () => {
- let [code, mapBuffer] = await Promise.all([
- asset.getCode(),
- this.bundle.env.sourceMap && asset.getMapBuffer(),
- ]);
- return {code, mapBuffer};
- });
- });
-
- let results = await queue.run();
-
- let assets = '';
- let i = 0;
- let first = true;
- let map = new SourceMap(this.options.projectRoot);
-
- let prefix = this.getPrefix();
- let lineOffset = countLines(prefix);
- let script: ?{|code: string, mapBuffer: ?Buffer|} = null;
-
- this.bundle.traverse(node => {
- let wrapped = first ? '' : ',';
-
- if (node.type === 'dependency') {
- let resolved = this.bundleGraph.getResolvedAsset(
- node.value,
- this.bundle,
- );
- if (resolved && resolved.type !== 'js') {
- // if this is a reference to another javascript asset, we should not include
- // its output, as its contents should already be loaded.
- invariant(!this.bundle.hasAsset(resolved));
- wrapped +=
- JSON.stringify(this.bundleGraph.getAssetPublicId(resolved)) +
- ':[function() {},{}]';
- } else {
- return;
- }
- }
-
- if (node.type === 'asset') {
- let asset = node.value;
- invariant(
- asset.type === 'js',
- 'all assets in a js bundle must be js assets',
- );
-
- // If this is the main entry of a script rather than a module, we need to hoist it
- // outside the bundle wrapper function so that its variables are exposed as globals.
- if (
- this.bundle.env.sourceType === 'script' &&
- asset === this.bundle.getMainEntry()
- ) {
- script = results[i++];
- return;
- }
-
- let deps = {};
- let dependencies = this.bundleGraph.getDependencies(asset);
- for (let dep of dependencies) {
- let resolved = this.bundleGraph.getResolvedAsset(dep, this.bundle);
- let specifier = getSpecifier(dep);
- if (this.bundleGraph.isDependencySkipped(dep)) {
- deps[specifier] = false;
- } else if (resolved) {
- deps[specifier] = this.bundleGraph.getAssetPublicId(resolved);
- } else {
- // An external module - map placeholder to original specifier.
- deps[specifier] = dep.specifier;
- }
- }
-
- let {code, mapBuffer} = results[i];
- let output = code || '';
- wrapped +=
- JSON.stringify(this.bundleGraph.getAssetPublicId(asset)) +
- ':[function(require,module,exports,__globalThis) {\n' +
- output +
- '\n},';
- wrapped += JSON.stringify(deps);
- wrapped += ']';
-
- if (
- this.bundle.env.isNode() &&
- asset.meta.has_node_replacements === true
- ) {
- const relPath = normalizeSeparators(
- path.relative(
- this.bundle.target.distDir,
- path.dirname(asset.filePath),
- ),
- );
- wrapped = wrapped.replace('$parcel$dirnameReplace', relPath);
- wrapped = wrapped.replace('$parcel$filenameReplace', relPath);
- }
-
- if (this.bundle.env.sourceMap) {
- if (mapBuffer) {
- map.addBuffer(mapBuffer, lineOffset);
- } else {
- map.addEmptyMap(
- path
- .relative(this.options.projectRoot, asset.filePath)
- .replace(/\\+/g, '/'),
- output,
- lineOffset,
- );
- }
-
- lineOffset += countLines(output) + 1;
- }
- i++;
- }
-
- assets += wrapped;
- first = false;
- });
-
- let entries = this.bundle.getEntryAssets();
- let mainEntry = this.bundle.getMainEntry();
- if (
- (!this.isEntry() && this.bundle.env.outputFormat === 'global') ||
- this.bundle.env.sourceType === 'script'
- ) {
- // In async bundles we don't want the main entry to execute until we require it
- // as there might be dependencies in a sibling bundle that hasn't loaded yet.
- entries = entries.filter(a => a.id !== mainEntry?.id);
- mainEntry = null;
- }
-
- let contents =
- prefix +
- '({' +
- assets +
- '},' +
- JSON.stringify(
- entries.map(asset => this.bundleGraph.getAssetPublicId(asset)),
- ) +
- ', ' +
- JSON.stringify(
- mainEntry ? this.bundleGraph.getAssetPublicId(mainEntry) : null,
- ) +
- ', ' +
- JSON.stringify(this.parcelRequireName) +
- ')' +
- '\n';
-
- // The entry asset of a script bundle gets hoisted outside the bundle wrapper function
- // so that its variables become globals. We need to replace any require calls for
- // runtimes with a parcelRequire call.
- if (this.bundle.env.sourceType === 'script' && script) {
- let entryMap;
- let mapBuffer = script.mapBuffer;
- if (mapBuffer) {
- entryMap = new SourceMap(this.options.projectRoot, mapBuffer);
- }
- contents += replaceScriptDependencies(
- this.bundleGraph,
- this.bundle,
- script.code,
- entryMap,
- this.parcelRequireName,
- );
- if (this.bundle.env.sourceMap && entryMap) {
- map.addSourceMap(entryMap, lineOffset);
- }
- }
-
- return {
- contents,
- map,
- };
- }
-
- getPrefix(): string {
- let interpreter: ?string;
- let mainEntry = this.bundle.getMainEntry();
- if (mainEntry && this.isEntry() && !this.bundle.target.env.isBrowser()) {
- let _interpreter = mainEntry.meta.interpreter;
- invariant(_interpreter == null || typeof _interpreter === 'string');
- interpreter = _interpreter;
- }
-
- let importScripts = '';
- if (this.bundle.env.isWorker()) {
- let bundles = this.bundleGraph.getReferencedBundles(this.bundle);
- for (let b of bundles) {
- importScripts += `importScripts("${relativeBundlePath(
- this.bundle,
- b,
- )}");\n`;
- }
- }
-
- return (
- // If the entry asset included a hashbang, repeat it at the top of the bundle
- (interpreter != null ? `#!${interpreter}\n` : '') +
- importScripts +
- PRELUDE
- );
- }
-
- isEntry(): boolean {
- return (
- !this.bundleGraph.hasParentBundleOfType(this.bundle, 'js') ||
- this.bundle.env.isIsolated() ||
- this.bundle.bundleBehavior === 'isolated'
- );
- }
-}
diff --git a/packages/packagers/js/src/DevPackager.ts b/packages/packagers/js/src/DevPackager.ts
new file mode 100644
index 000000000..677c3e90a
--- /dev/null
+++ b/packages/packagers/js/src/DevPackager.ts
@@ -0,0 +1,265 @@
+import type {BundleGraph, PluginOptions, NamedBundle} from '@atlaspack/types';
+
+import {
+ PromiseQueue,
+ relativeBundlePath,
+ countLines,
+ normalizeSeparators,
+} from '@atlaspack/utils';
+import SourceMap from '@parcel/source-map';
+import invariant from 'assert';
+import path from 'path';
+import fs from 'fs';
+import {replaceScriptDependencies, getSpecifier} from './utils';
+
+const PRELUDE = fs
+ .readFileSync(path.join(__dirname, 'dev-prelude.js'), 'utf8')
+ .trim()
+ .replace(/;$/, '');
+
+export class DevPackager {
+ options: PluginOptions;
+ bundleGraph: BundleGraph;
+ bundle: NamedBundle;
+ parcelRequireName: string;
+
+ constructor(
+ options: PluginOptions,
+ bundleGraph: BundleGraph,
+ bundle: NamedBundle,
+ parcelRequireName: string,
+ ) {
+ this.options = options;
+ this.bundleGraph = bundleGraph;
+ this.bundle = bundle;
+ this.parcelRequireName = parcelRequireName;
+ }
+
+ async package(): Promise<{
+ contents: string;
+ map: SourceMap | null | undefined;
+ }> {
+ // Load assets
+ let queue = new PromiseQueue({maxConcurrent: 32});
+ this.bundle.traverseAssets((asset) => {
+ queue.add(async () => {
+ let [code, mapBuffer] = await Promise.all([
+ asset.getCode(),
+ this.bundle.env.sourceMap && asset.getMapBuffer(),
+ ]);
+ return {code, mapBuffer};
+ });
+ });
+
+ let results = await queue.run();
+
+ let assets = '';
+ let i = 0;
+ let first = true;
+ let map = new SourceMap(this.options.projectRoot);
+
+ let prefix = this.getPrefix();
+ let lineOffset = countLines(prefix);
+ let script:
+ | {
+ code: string;
+ mapBuffer: Buffer | null | undefined;
+ }
+ | null
+ | undefined = null;
+
+ this.bundle.traverse((node) => {
+ let wrapped = first ? '' : ',';
+
+ if (node.type === 'dependency') {
+ let resolved = this.bundleGraph.getResolvedAsset(
+ node.value,
+ this.bundle,
+ );
+ if (resolved && resolved.type !== 'js') {
+ // if this is a reference to another javascript asset, we should not include
+ // its output, as its contents should already be loaded.
+ invariant(!this.bundle.hasAsset(resolved));
+ wrapped +=
+ JSON.stringify(this.bundleGraph.getAssetPublicId(resolved)) +
+ ':[function() {},{}]';
+ } else {
+ return;
+ }
+ }
+
+ if (node.type === 'asset') {
+ let asset = node.value;
+ invariant(
+ asset.type === 'js',
+ 'all assets in a js bundle must be js assets',
+ );
+
+ // If this is the main entry of a script rather than a module, we need to hoist it
+ // outside the bundle wrapper function so that its variables are exposed as globals.
+ if (
+ this.bundle.env.sourceType === 'script' &&
+ asset === this.bundle.getMainEntry()
+ ) {
+ // @ts-expect-error - TS2322 - Type 'unknown' is not assignable to type '{ code: string; mapBuffer: Buffer | null | undefined; } | null | undefined'.
+ script = results[i++];
+ return;
+ }
+
+ let deps: Record = {};
+ let dependencies = this.bundleGraph.getDependencies(asset);
+ for (let dep of dependencies) {
+ let resolved = this.bundleGraph.getResolvedAsset(dep, this.bundle);
+ let specifier = getSpecifier(dep);
+ if (this.bundleGraph.isDependencySkipped(dep)) {
+ deps[specifier] = false;
+ } else if (resolved) {
+ deps[specifier] = this.bundleGraph.getAssetPublicId(resolved);
+ } else {
+ // An external module - map placeholder to original specifier.
+ deps[specifier] = dep.specifier;
+ }
+ }
+
+ // @ts-expect-error - TS2339 - Property 'code' does not exist on type 'unknown'. | TS2339 - Property 'mapBuffer' does not exist on type 'unknown'.
+ let {code, mapBuffer} = results[i];
+ let output = code || '';
+ wrapped +=
+ JSON.stringify(this.bundleGraph.getAssetPublicId(asset)) +
+ ':[function(require,module,exports,__globalThis) {\n' +
+ output +
+ '\n},';
+ wrapped += JSON.stringify(deps);
+ wrapped += ']';
+
+ if (
+ this.bundle.env.isNode() &&
+ asset.meta.has_node_replacements === true
+ ) {
+ const relPath = normalizeSeparators(
+ path.relative(
+ this.bundle.target.distDir,
+ path.dirname(asset.filePath),
+ ),
+ );
+ wrapped = wrapped.replace('$parcel$dirnameReplace', relPath);
+ wrapped = wrapped.replace('$parcel$filenameReplace', relPath);
+ }
+
+ if (this.bundle.env.sourceMap) {
+ if (mapBuffer) {
+ map.addBuffer(mapBuffer, lineOffset);
+ } else {
+ map.addEmptyMap(
+ path
+ .relative(this.options.projectRoot, asset.filePath)
+ .replace(/\\+/g, '/'),
+ output,
+ lineOffset,
+ );
+ }
+
+ lineOffset += countLines(output) + 1;
+ }
+ i++;
+ }
+
+ assets += wrapped;
+ first = false;
+ });
+
+ let entries = this.bundle.getEntryAssets();
+ let mainEntry = this.bundle.getMainEntry();
+ if (
+ (!this.isEntry() && this.bundle.env.outputFormat === 'global') ||
+ this.bundle.env.sourceType === 'script'
+ ) {
+ // In async bundles we don't want the main entry to execute until we require it
+ // as there might be dependencies in a sibling bundle that hasn't loaded yet.
+ entries = entries.filter((a) => a.id !== mainEntry?.id);
+ mainEntry = null;
+ }
+
+ let contents =
+ prefix +
+ '({' +
+ assets +
+ '},' +
+ JSON.stringify(
+ entries.map((asset) => this.bundleGraph.getAssetPublicId(asset)),
+ ) +
+ ', ' +
+ JSON.stringify(
+ mainEntry ? this.bundleGraph.getAssetPublicId(mainEntry) : null,
+ ) +
+ ', ' +
+ JSON.stringify(this.parcelRequireName) +
+ ')' +
+ '\n';
+
+ // The entry asset of a script bundle gets hoisted outside the bundle wrapper function
+ // so that its variables become globals. We need to replace any require calls for
+ // runtimes with a parcelRequire call.
+ if (this.bundle.env.sourceType === 'script' && script) {
+ let entryMap;
+ // @ts-expect-error - TS2339 - Property 'mapBuffer' does not exist on type 'never'.
+ let mapBuffer = script.mapBuffer;
+ if (mapBuffer) {
+ entryMap = new SourceMap(this.options.projectRoot, mapBuffer);
+ }
+ contents += replaceScriptDependencies(
+ this.bundleGraph,
+ this.bundle,
+ // @ts-expect-error - TS2339 - Property 'code' does not exist on type 'never'.
+ script.code,
+ entryMap,
+ this.parcelRequireName,
+ );
+ if (this.bundle.env.sourceMap && entryMap) {
+ // @ts-expect-error - TS2551 - Property 'addSourceMap' does not exist on type 'SourceMap'. Did you mean 'addSources'?
+ map.addSourceMap(entryMap, lineOffset);
+ }
+ }
+
+ return {
+ contents,
+ map,
+ };
+ }
+
+ getPrefix(): string {
+ let interpreter: string | null | undefined;
+ let mainEntry = this.bundle.getMainEntry();
+ if (mainEntry && this.isEntry() && !this.bundle.target.env.isBrowser()) {
+ let _interpreter = mainEntry.meta.interpreter;
+ invariant(_interpreter == null || typeof _interpreter === 'string');
+ interpreter = _interpreter;
+ }
+
+ let importScripts = '';
+ if (this.bundle.env.isWorker()) {
+ let bundles = this.bundleGraph.getReferencedBundles(this.bundle);
+ for (let b of bundles) {
+ importScripts += `importScripts("${relativeBundlePath(
+ this.bundle,
+ b,
+ )}");\n`;
+ }
+ }
+
+ return (
+ // If the entry asset included a hashbang, repeat it at the top of the bundle
+ (interpreter != null ? `#!${interpreter}\n` : '') +
+ importScripts +
+ PRELUDE
+ );
+ }
+
+ isEntry(): boolean {
+ return (
+ !this.bundleGraph.hasParentBundleOfType(this.bundle, 'js') ||
+ this.bundle.env.isIsolated() ||
+ this.bundle.bundleBehavior === 'isolated'
+ );
+ }
+}
diff --git a/packages/packagers/js/src/ESMOutputFormat.js b/packages/packagers/js/src/ESMOutputFormat.js
deleted file mode 100644
index 16a7217e9..000000000
--- a/packages/packagers/js/src/ESMOutputFormat.js
+++ /dev/null
@@ -1,129 +0,0 @@
-// @flow
-import type {
- ScopeHoistingPackager,
- OutputFormat,
-} from './ScopeHoistingPackager';
-import {isValidIdentifier} from './utils';
-
-export class ESMOutputFormat implements OutputFormat {
- packager: ScopeHoistingPackager;
-
- constructor(packager: ScopeHoistingPackager) {
- this.packager = packager;
- }
-
- buildBundlePrelude(): [string, number] {
- let res = '';
- let lines = 0;
- for (let [source, specifiers] of this.packager.externals) {
- let defaultSpecifier = null;
- let namespaceSpecifier = null;
- let namedSpecifiers = [];
- for (let [imported, symbol] of specifiers) {
- if (imported === 'default' /* || isCommonJS*/) {
- defaultSpecifier = symbol;
- } else if (imported === '*') {
- namespaceSpecifier = `* as ${symbol}`;
- } else {
- let specifier = imported;
- if (!isValidIdentifier(specifier)) {
- specifier = JSON.stringify(specifier);
- }
- if (symbol !== imported) {
- specifier += ` as ${symbol}`;
- }
-
- namedSpecifiers.push(specifier);
- }
- }
-
- // ESModule syntax allows combining default and namespace specifiers, or default and named, but not all three.
-
- let imported = '';
- if (namespaceSpecifier) {
- let s = namespaceSpecifier;
- if (defaultSpecifier) {
- s = `${defaultSpecifier}, ${namespaceSpecifier}`;
- }
-
- res += `import ${s} from ${JSON.stringify(source)};\n`;
- lines++;
- } else if (defaultSpecifier) {
- imported = defaultSpecifier;
- if (namedSpecifiers.length > 0) {
- imported += `, {${namedSpecifiers.join(', ')}}`;
- }
- } else if (namedSpecifiers.length > 0) {
- imported = `{${namedSpecifiers.join(', ')}}`;
- }
-
- if (imported.length > 0) {
- res += `import ${imported} from ${JSON.stringify(source)};\n`;
- lines++;
- } else if (!namespaceSpecifier) {
- res += `import ${JSON.stringify(source)};\n`;
- lines++;
- }
- }
-
- if (res.length > 0) {
- res += '\n';
- lines++;
- }
-
- return [res, lines];
- }
-
- buildBundlePostlude(): [string, number] {
- let res = '';
- let lines = 0;
- let exportSpecifiers = [];
- for (let {
- asset,
- exportSymbol,
- local,
- exportAs,
- } of this.packager.exportedSymbols.values()) {
- if (this.packager.wrappedAssets.has(asset.id)) {
- let obj = `parcelRequire("${this.packager.bundleGraph.getAssetPublicId(
- asset,
- )}")`;
- res += `\nvar ${local} = ${this.packager.getPropertyAccess(
- obj,
- exportSymbol,
- )};`;
- lines++;
- }
-
- for (let as of exportAs) {
- let specifier = local;
- if (as !== local) {
- if (!isValidIdentifier(as)) {
- as = JSON.stringify(as);
- }
- specifier += ` as ${as}`;
- }
-
- exportSpecifiers.push(specifier);
- }
- }
-
- if (exportSpecifiers.length > 0) {
- res += `\nexport {${exportSpecifiers.join(', ')}};`;
- lines++;
- }
-
- if (
- this.packager.needsPrelude &&
- this.packager.shouldBundleQueue(this.packager.bundle)
- ) {
- // Should be last thing the bundle executes on intial eval
- res += `\n$parcel$global.rlb(${JSON.stringify(
- this.packager.bundle.publicId,
- )})`;
- lines++;
- }
-
- return [res, lines];
- }
-}
diff --git a/packages/packagers/js/src/ESMOutputFormat.ts b/packages/packagers/js/src/ESMOutputFormat.ts
new file mode 100644
index 000000000..8d17961a6
--- /dev/null
+++ b/packages/packagers/js/src/ESMOutputFormat.ts
@@ -0,0 +1,128 @@
+import type {
+ ScopeHoistingPackager,
+ OutputFormat,
+} from './ScopeHoistingPackager';
+import {isValidIdentifier} from './utils';
+
+export class ESMOutputFormat implements OutputFormat {
+ packager: ScopeHoistingPackager;
+
+ constructor(packager: ScopeHoistingPackager) {
+ this.packager = packager;
+ }
+
+ buildBundlePrelude(): [string, number] {
+ let res = '';
+ let lines = 0;
+ for (let [source, specifiers] of this.packager.externals) {
+ let defaultSpecifier = null;
+ let namespaceSpecifier = null;
+ let namedSpecifiers: Array = [];
+ for (let [imported, symbol] of specifiers) {
+ if (imported === 'default' /* || isCommonJS*/) {
+ defaultSpecifier = symbol;
+ } else if (imported === '*') {
+ namespaceSpecifier = `* as ${symbol}`;
+ } else {
+ let specifier = imported;
+ if (!isValidIdentifier(specifier)) {
+ specifier = JSON.stringify(specifier);
+ }
+ if (symbol !== imported) {
+ specifier += ` as ${symbol}`;
+ }
+
+ namedSpecifiers.push(specifier);
+ }
+ }
+
+ // ESModule syntax allows combining default and namespace specifiers, or default and named, but not all three.
+
+ let imported = '';
+ if (namespaceSpecifier) {
+ let s = namespaceSpecifier;
+ if (defaultSpecifier) {
+ s = `${defaultSpecifier}, ${namespaceSpecifier}`;
+ }
+
+ res += `import ${s} from ${JSON.stringify(source)};\n`;
+ lines++;
+ } else if (defaultSpecifier) {
+ imported = defaultSpecifier;
+ if (namedSpecifiers.length > 0) {
+ imported += `, {${namedSpecifiers.join(', ')}}`;
+ }
+ } else if (namedSpecifiers.length > 0) {
+ imported = `{${namedSpecifiers.join(', ')}}`;
+ }
+
+ if (imported.length > 0) {
+ res += `import ${imported} from ${JSON.stringify(source)};\n`;
+ lines++;
+ } else if (!namespaceSpecifier) {
+ res += `import ${JSON.stringify(source)};\n`;
+ lines++;
+ }
+ }
+
+ if (res.length > 0) {
+ res += '\n';
+ lines++;
+ }
+
+ return [res, lines];
+ }
+
+ buildBundlePostlude(): [string, number] {
+ let res = '';
+ let lines = 0;
+ let exportSpecifiers: Array = [];
+ for (let {
+ asset,
+ exportSymbol,
+ local,
+ exportAs,
+ } of this.packager.exportedSymbols.values()) {
+ if (this.packager.wrappedAssets.has(asset.id)) {
+ let obj = `parcelRequire("${this.packager.bundleGraph.getAssetPublicId(
+ asset,
+ )}")`;
+ res += `\nvar ${local} = ${this.packager.getPropertyAccess(
+ obj,
+ exportSymbol,
+ )};`;
+ lines++;
+ }
+
+ for (let as of exportAs) {
+ let specifier = local;
+ if (as !== local) {
+ if (!isValidIdentifier(as)) {
+ as = JSON.stringify(as);
+ }
+ specifier += ` as ${as}`;
+ }
+
+ exportSpecifiers.push(specifier);
+ }
+ }
+
+ if (exportSpecifiers.length > 0) {
+ res += `\nexport {${exportSpecifiers.join(', ')}};`;
+ lines++;
+ }
+
+ if (
+ this.packager.needsPrelude &&
+ this.packager.shouldBundleQueue(this.packager.bundle)
+ ) {
+ // Should be last thing the bundle executes on intial eval
+ res += `\n$parcel$global.rlb(${JSON.stringify(
+ this.packager.bundle.publicId,
+ )})`;
+ lines++;
+ }
+
+ return [res, lines];
+ }
+}
diff --git a/packages/packagers/js/src/GlobalOutputFormat.js b/packages/packagers/js/src/GlobalOutputFormat.js
deleted file mode 100644
index 68ac11281..000000000
--- a/packages/packagers/js/src/GlobalOutputFormat.js
+++ /dev/null
@@ -1,24 +0,0 @@
-// @flow
-import type {
- ScopeHoistingPackager,
- OutputFormat,
-} from './ScopeHoistingPackager';
-
-export class GlobalOutputFormat implements OutputFormat {
- packager: ScopeHoistingPackager;
-
- constructor(packager: ScopeHoistingPackager) {
- this.packager = packager;
- }
-
- buildBundlePrelude(): [string, number] {
- let prelude = this.packager.bundle.env.supports('arrow-functions', true)
- ? '(() => {\n'
- : '(function () {\n';
- return [prelude, 1];
- }
-
- buildBundlePostlude(): [string, number] {
- return ['})();', 0];
- }
-}
diff --git a/packages/packagers/js/src/GlobalOutputFormat.ts b/packages/packagers/js/src/GlobalOutputFormat.ts
new file mode 100644
index 000000000..61bbbbf18
--- /dev/null
+++ b/packages/packagers/js/src/GlobalOutputFormat.ts
@@ -0,0 +1,23 @@
+import type {
+ ScopeHoistingPackager,
+ OutputFormat,
+} from './ScopeHoistingPackager';
+
+export class GlobalOutputFormat implements OutputFormat {
+ packager: ScopeHoistingPackager;
+
+ constructor(packager: ScopeHoistingPackager) {
+ this.packager = packager;
+ }
+
+ buildBundlePrelude(): [string, number] {
+ let prelude = this.packager.bundle.env.supports('arrow-functions', true)
+ ? '(() => {\n'
+ : '(function () {\n';
+ return [prelude, 1];
+ }
+
+ buildBundlePostlude(): [string, number] {
+ return ['})();', 0];
+ }
+}
diff --git a/packages/packagers/js/src/ScopeHoistingPackager.js b/packages/packagers/js/src/ScopeHoistingPackager.js
deleted file mode 100644
index c0630dd7a..000000000
--- a/packages/packagers/js/src/ScopeHoistingPackager.js
+++ /dev/null
@@ -1,1503 +0,0 @@
-// @flow
-
-import type {
- Asset,
- BundleGraph,
- Dependency,
- PluginOptions,
- NamedBundle,
- PluginLogger,
-} from '@atlaspack/types';
-
-import {
- DefaultMap,
- PromiseQueue,
- relativeBundlePath,
- countLines,
- normalizeSeparators,
-} from '@atlaspack/utils';
-import SourceMap from '@parcel/source-map';
-import nullthrows from 'nullthrows';
-import invariant, {AssertionError} from 'assert';
-import ThrowableDiagnostic, {
- convertSourceLocationToHighlight,
-} from '@atlaspack/diagnostic';
-import globals from 'globals';
-import path from 'path';
-import {getFeatureFlag} from '@atlaspack/feature-flags';
-
-import {ESMOutputFormat} from './ESMOutputFormat';
-import {CJSOutputFormat} from './CJSOutputFormat';
-import {GlobalOutputFormat} from './GlobalOutputFormat';
-import {prelude, helpers, bundleQueuePrelude, fnExpr} from './helpers';
-import {
- replaceScriptDependencies,
- getSpecifier,
- isValidIdentifier,
- makeValidIdentifier,
-} from './utils';
-
-// General regex used to replace imports with the resolved code, references with resolutions,
-// and count the number of newlines in the file for source maps.
-const REPLACEMENT_RE =
- /\n|import\s+"([0-9a-f]{16,20}:.+?)";|(?:\$[0-9a-f]{16,20}\$exports)|(?:\$[0-9a-f]{16,20}\$(?:import|importAsync|require)\$[0-9a-f]+(?:\$[0-9a-f]+)?)/g;
-
-const BUILTINS = Object.keys(globals.builtin);
-const GLOBALS_BY_CONTEXT = {
- browser: new Set([...BUILTINS, ...Object.keys(globals.browser)]),
- 'web-worker': new Set([...BUILTINS, ...Object.keys(globals.worker)]),
- 'service-worker': new Set([
- ...BUILTINS,
- ...Object.keys(globals.serviceworker),
- ]),
- worklet: new Set([...BUILTINS]),
- node: new Set([...BUILTINS, ...Object.keys(globals.node)]),
- 'electron-main': new Set([...BUILTINS, ...Object.keys(globals.node)]),
- 'electron-renderer': new Set([
- ...BUILTINS,
- ...Object.keys(globals.node),
- ...Object.keys(globals.browser),
- ]),
-};
-
-const OUTPUT_FORMATS = {
- esmodule: ESMOutputFormat,
- commonjs: CJSOutputFormat,
- global: GlobalOutputFormat,
-};
-
-export interface OutputFormat {
- buildBundlePrelude(): [string, number];
- buildBundlePostlude(): [string, number];
-}
-
-export class ScopeHoistingPackager {
- options: PluginOptions;
- bundleGraph: BundleGraph;
- bundle: NamedBundle;
- parcelRequireName: string;
- useAsyncBundleRuntime: boolean;
- outputFormat: OutputFormat;
- isAsyncBundle: boolean;
- globalNames: $ReadOnlySet;
- assetOutputs: Map;
- exportedSymbols: Map<
- string,
- {|
- asset: Asset,
- exportSymbol: string,
- local: string,
- exportAs: Array,
- |},
- > = new Map();
- externals: Map> = new Map();
- topLevelNames: Map = new Map();
- seenAssets: Set = new Set();
- wrappedAssets: Set = new Set();
- hoistedRequires: Map> = new Map();
- needsPrelude: boolean = false;
- usedHelpers: Set = new Set();
- externalAssets: Set = new Set();
- forceSkipWrapAssets: Array = [];
- logger: PluginLogger;
-
- constructor(
- options: PluginOptions,
- bundleGraph: BundleGraph,
- bundle: NamedBundle,
- parcelRequireName: string,
- useAsyncBundleRuntime: boolean,
- forceSkipWrapAssets: Array,
- logger: PluginLogger,
- ) {
- this.options = options;
- this.bundleGraph = bundleGraph;
- this.bundle = bundle;
- this.parcelRequireName = parcelRequireName;
- this.useAsyncBundleRuntime = useAsyncBundleRuntime;
- this.forceSkipWrapAssets = forceSkipWrapAssets ?? [];
- this.logger = logger;
-
- let OutputFormat = OUTPUT_FORMATS[this.bundle.env.outputFormat];
- this.outputFormat = new OutputFormat(this);
-
- this.isAsyncBundle =
- this.bundleGraph.hasParentBundleOfType(this.bundle, 'js') &&
- !this.bundle.env.isIsolated() &&
- this.bundle.bundleBehavior !== 'isolated';
-
- this.globalNames = GLOBALS_BY_CONTEXT[bundle.env.context];
- }
-
- async package(): Promise<{|contents: string, map: ?SourceMap|}> {
- let wrappedAssets = await this.loadAssets();
- this.buildExportedSymbols();
-
- // If building a library, the target is actually another bundler rather
- // than the final output that could be loaded in a browser. So, loader
- // runtimes are excluded, and instead we add imports into the entry bundle
- // of each bundle group pointing at the sibling bundles. These can be
- // picked up by another bundler later at which point runtimes will be added.
- if (
- this.bundle.env.isLibrary ||
- this.bundle.env.outputFormat === 'commonjs'
- ) {
- for (let b of this.bundleGraph.getReferencedBundles(this.bundle, {
- recursive: false,
- })) {
- this.externals.set(relativeBundlePath(this.bundle, b), new Map());
- }
- }
-
- let res = '';
- let lineCount = 0;
- let sourceMap = null;
- let processAsset = asset => {
- let [content, map, lines] = this.visitAsset(asset);
- if (sourceMap && map) {
- sourceMap.addSourceMap(map, lineCount);
- } else if (this.bundle.env.sourceMap) {
- sourceMap = map;
- }
-
- res += content + '\n';
- lineCount += lines + 1;
- };
-
- // Hoist wrapped asset to the top of the bundle to ensure that they are registered
- // before they are used.
- for (let asset of wrappedAssets) {
- if (!this.seenAssets.has(asset.id)) {
- processAsset(asset);
- }
- }
-
- // Add each asset that is directly connected to the bundle. Dependencies will be handled
- // by replacing `import` statements in the code.
- this.bundle.traverseAssets((asset, _, actions) => {
- if (this.seenAssets.has(asset.id)) {
- actions.skipChildren();
- return;
- }
-
- processAsset(asset);
- actions.skipChildren();
- });
-
- let [prelude, preludeLines] = this.buildBundlePrelude();
- res = prelude + res;
- lineCount += preludeLines;
- sourceMap?.offsetLines(1, preludeLines);
-
- let entries = this.bundle.getEntryAssets();
- let mainEntry = this.bundle.getMainEntry();
- if (this.isAsyncBundle) {
- // In async bundles we don't want the main entry to execute until we require it
- // as there might be dependencies in a sibling bundle that hasn't loaded yet.
- entries = entries.filter(a => a.id !== mainEntry?.id);
- mainEntry = null;
- }
-
- let needsBundleQueue = this.shouldBundleQueue(this.bundle);
-
- // If any of the entry assets are wrapped, call parcelRequire so they are executed.
- for (let entry of entries) {
- if (this.wrappedAssets.has(entry.id) && !this.isScriptEntry(entry)) {
- let parcelRequire = `parcelRequire(${JSON.stringify(
- this.bundleGraph.getAssetPublicId(entry),
- )});\n`;
-
- let entryExports = entry.symbols.get('*')?.local;
-
- if (
- entryExports &&
- entry === mainEntry &&
- this.exportedSymbols.has(entryExports)
- ) {
- invariant(
- !needsBundleQueue,
- 'Entry exports are not yet compaitble with async bundles',
- );
- res += `\nvar ${entryExports} = ${parcelRequire}`;
- } else {
- if (needsBundleQueue) {
- parcelRequire = this.runWhenReady(this.bundle, parcelRequire);
- }
-
- res += `\n${parcelRequire}`;
- }
-
- lineCount += 2;
- }
- }
-
- let [postlude, postludeLines] = this.outputFormat.buildBundlePostlude();
- res += postlude;
- lineCount += postludeLines;
-
- // The entry asset of a script bundle gets hoisted outside the bundle wrapper so that
- // its top-level variables become globals like a real browser script. We need to replace
- // all dependency references for runtimes with a parcelRequire call.
- if (
- this.bundle.env.outputFormat === 'global' &&
- this.bundle.env.sourceType === 'script'
- ) {
- res += '\n';
- lineCount++;
-
- let mainEntry = nullthrows(this.bundle.getMainEntry());
- let {code, map: mapBuffer} = nullthrows(
- this.assetOutputs.get(mainEntry.id),
- );
- let map;
- if (mapBuffer) {
- map = new SourceMap(this.options.projectRoot, mapBuffer);
- }
- res += replaceScriptDependencies(
- this.bundleGraph,
- this.bundle,
- code,
- map,
- this.parcelRequireName,
- );
- if (sourceMap && map) {
- sourceMap.addSourceMap(map, lineCount);
- }
- }
-
- return {
- contents: res,
- map: sourceMap,
- };
- }
-
- shouldBundleQueue(bundle: NamedBundle): boolean {
- let referencingBundles = this.bundleGraph.getReferencingBundles(bundle);
- let hasHtmlReference = referencingBundles.some(b => b.type === 'html');
-
- return (
- this.useAsyncBundleRuntime &&
- bundle.type === 'js' &&
- bundle.bundleBehavior !== 'inline' &&
- bundle.env.outputFormat === 'esmodule' &&
- !bundle.env.isIsolated() &&
- bundle.bundleBehavior !== 'isolated' &&
- hasHtmlReference
- );
- }
-
- runWhenReady(bundle: NamedBundle, codeToRun: string): string {
- let deps = this.bundleGraph
- .getReferencedBundles(bundle)
- .filter(b => this.shouldBundleQueue(b))
- .map(b => b.publicId);
-
- if (deps.length === 0) {
- // If no deps we can safely execute immediately
- return codeToRun;
- }
-
- let params = [
- JSON.stringify(this.bundle.publicId),
- fnExpr(this.bundle.env, [], [codeToRun]),
- JSON.stringify(deps),
- ];
-
- return `$parcel$global.rwr(${params.join(', ')});`;
- }
-
- async loadAssets(): Promise> {
- let queue = new PromiseQueue({maxConcurrent: 32});
- let wrapped = [];
- this.bundle.traverseAssets(asset => {
- queue.add(async () => {
- let [code, map] = await Promise.all([
- asset.getCode(),
- this.bundle.env.sourceMap ? asset.getMapBuffer() : null,
- ]);
- return [asset.id, {code, map}];
- });
-
- if (
- asset.meta.shouldWrap ||
- this.bundle.env.sourceType === 'script' ||
- this.bundleGraph.isAssetReferenced(this.bundle, asset) ||
- this.bundleGraph
- .getIncomingDependencies(asset)
- .some(dep => dep.meta.shouldWrap && dep.specifierType !== 'url')
- ) {
- // Don't wrap constant "entry" modules _except_ if they are referenced by any lazy dependency
- if (
- !asset.meta.isConstantModule ||
- this.bundleGraph
- .getIncomingDependencies(asset)
- .some(dep => dep.priority === 'lazy')
- ) {
- this.wrappedAssets.add(asset.id);
- wrapped.push(asset);
- }
- }
- });
-
- for (let wrappedAssetRoot of [...wrapped]) {
- this.bundle.traverseAssets((asset, _, actions) => {
- if (asset === wrappedAssetRoot) {
- return;
- }
-
- if (this.wrappedAssets.has(asset.id)) {
- actions.skipChildren();
- return;
- }
- // This prevents children of a wrapped asset also being wrapped - it's an "unsafe" optimisation
- // that should only be used when you know (or think you know) what you're doing.
- //
- // In particular this can force an async bundle to be scope hoisted where it previously would not be
- // due to the entry asset being wrapped.
- if (
- this.forceSkipWrapAssets.length > 0 &&
- this.forceSkipWrapAssets.some(
- p => p === path.relative(this.options.projectRoot, asset.filePath),
- )
- ) {
- this.logger.verbose({
- message: `Force skipping wrapping of ${path.relative(
- this.options.projectRoot,
- asset.filePath,
- )}`,
- });
- actions.skipChildren();
- return;
- }
- if (!asset.meta.isConstantModule) {
- this.wrappedAssets.add(asset.id);
- wrapped.push(asset);
- }
- }, wrappedAssetRoot);
- }
-
- this.assetOutputs = new Map(await queue.run());
- return wrapped;
- }
-
- buildExportedSymbols() {
- if (
- !this.bundle.env.isLibrary ||
- this.bundle.env.outputFormat !== 'esmodule'
- ) {
- return;
- }
-
- // TODO: handle ESM exports of wrapped entry assets...
- let entry = this.bundle.getMainEntry();
- if (entry && !this.wrappedAssets.has(entry.id)) {
- let hasNamespace = entry.symbols.hasExportSymbol('*');
-
- for (let {
- asset,
- exportAs,
- symbol,
- exportSymbol,
- } of this.bundleGraph.getExportedSymbols(entry)) {
- if (typeof symbol === 'string') {
- // If the module has a namespace (e.g. commonjs), and this is not an entry, only export the namespace
- // as default, without individual exports. This mirrors the importing logic in addExternal, avoiding
- // extra unused exports and potential for non-identifier export names.
- if (hasNamespace && this.isAsyncBundle && exportAs !== '*') {
- continue;
- }
-
- let symbols = this.exportedSymbols.get(
- symbol === '*' ? nullthrows(entry.symbols.get('*')?.local) : symbol,
- )?.exportAs;
-
- if (!symbols) {
- symbols = [];
- this.exportedSymbols.set(symbol, {
- asset,
- exportSymbol,
- local: symbol,
- exportAs: symbols,
- });
- }
-
- if (exportAs === '*') {
- exportAs = 'default';
- }
-
- symbols.push(exportAs);
- } else if (symbol === null) {
- // TODO `meta.exportsIdentifier[exportSymbol]` should be exported
- // let relativePath = relative(options.projectRoot, asset.filePath);
- // throw getThrowableDiagnosticForNode(
- // md`${relativePath} couldn't be statically analyzed when importing '${exportSymbol}'`,
- // entry.filePath,
- // loc,
- // );
- } else if (symbol !== false) {
- // let relativePath = relative(options.projectRoot, asset.filePath);
- // throw getThrowableDiagnosticForNode(
- // md`${relativePath} does not export '${exportSymbol}'`,
- // entry.filePath,
- // loc,
- // );
- }
- }
- }
- }
-
- getTopLevelName(name: string): string {
- name = makeValidIdentifier(name);
- if (this.globalNames.has(name)) {
- name = '_' + name;
- }
-
- let count = this.topLevelNames.get(name);
- if (count == null) {
- this.topLevelNames.set(name, 1);
- return name;
- }
-
- this.topLevelNames.set(name, count + 1);
- return name + count;
- }
-
- getPropertyAccess(obj: string, property: string): string {
- if (isValidIdentifier(property)) {
- return `${obj}.${property}`;
- }
-
- return `${obj}[${JSON.stringify(property)}]`;
- }
-
- visitAsset(asset: Asset): [string, ?SourceMap, number] {
- invariant(!this.seenAssets.has(asset.id), 'Already visited asset');
- this.seenAssets.add(asset.id);
-
- let {code, map} = nullthrows(this.assetOutputs.get(asset.id));
- return this.buildAsset(asset, code, map);
- }
-
- buildAsset(
- asset: Asset,
- code: string,
- map: ?Buffer,
- ): [string, ?SourceMap, number] {
- let shouldWrap = this.wrappedAssets.has(asset.id);
- let deps = this.bundleGraph.getDependencies(asset);
-
- let sourceMap =
- this.bundle.env.sourceMap && map
- ? new SourceMap(this.options.projectRoot, map)
- : null;
-
- // If this asset is skipped, just add dependencies and not the asset's content.
- if (this.shouldSkipAsset(asset)) {
- let depCode = '';
- let lineCount = 0;
- for (let dep of deps) {
- let resolved = this.bundleGraph.getResolvedAsset(dep, this.bundle);
- let skipped = this.bundleGraph.isDependencySkipped(dep);
- if (skipped) {
- continue;
- }
-
- if (!resolved) {
- if (!dep.isOptional) {
- this.addExternal(dep);
- }
-
- continue;
- }
-
- if (
- this.bundle.hasAsset(resolved) &&
- !this.seenAssets.has(resolved.id)
- ) {
- let [code, map, lines] = this.visitAsset(resolved);
- depCode += code + '\n';
- if (sourceMap && map) {
- sourceMap.addSourceMap(map, lineCount);
- }
- lineCount += lines + 1;
- }
- }
-
- return [depCode, sourceMap, lineCount];
- }
-
- // TODO: maybe a meta prop?
- if (code.includes('$parcel$global')) {
- this.usedHelpers.add('$parcel$global');
- }
-
- if (this.bundle.env.isNode() && asset.meta.has_node_replacements) {
- const relPath = normalizeSeparators(
- path.relative(this.bundle.target.distDir, path.dirname(asset.filePath)),
- );
- code = code.replace('$parcel$dirnameReplace', relPath);
- code = code.replace('$parcel$filenameReplace', relPath);
- }
-
- let [depMap, replacements] = this.buildReplacements(asset, deps);
- let [prepend, prependLines, append] = this.buildAssetPrelude(
- asset,
- deps,
- replacements,
- );
- if (prependLines > 0) {
- sourceMap?.offsetLines(1, prependLines);
- code = prepend + code;
- }
-
- code += append;
-
- let lineCount = 0;
- let depContent = [];
- if (depMap.size === 0 && replacements.size === 0) {
- // If there are no dependencies or replacements, use a simple function to count the number of lines.
- lineCount = countLines(code) - 1;
- } else {
- // Otherwise, use a regular expression to perform replacements.
- // We need to track how many newlines there are for source maps, replace
- // all import statements with dependency code, and perform inline replacements
- // of all imported symbols with their resolved export symbols. This is all done
- // in a single regex so that we only do one pass over the whole code.
- let offset = 0;
- let columnStartIndex = 0;
- code = code.replace(REPLACEMENT_RE, (m, d, i) => {
- if (m === '\n') {
- columnStartIndex = i + offset + 1;
- lineCount++;
- return '\n';
- }
-
- // If we matched an import, replace with the source code for the dependency.
- if (d != null) {
- let deps = depMap.get(d);
- if (!deps) {
- return m;
- }
-
- let replacement = '';
-
- // A single `${id}:${specifier}:esm` might have been resolved to multiple assets due to
- // reexports.
- for (let dep of deps) {
- let resolved = this.bundleGraph.getResolvedAsset(dep, this.bundle);
- let skipped = this.bundleGraph.isDependencySkipped(dep);
- if (resolved && !skipped) {
- // Hoist variable declarations for the referenced parcelRequire dependencies
- // after the dependency is declared. This handles the case where the resulting asset
- // is wrapped, but the dependency in this asset is not marked as wrapped. This means
- // that it was imported/required at the top-level, so its side effects should run immediately.
- let [res, lines] = this.getHoistedParcelRequires(
- asset,
- dep,
- resolved,
- );
- let map;
- if (
- this.bundle.hasAsset(resolved) &&
- !this.seenAssets.has(resolved.id)
- ) {
- // If this asset is wrapped, we need to hoist the code for the dependency
- // outside our parcelRequire.register wrapper. This is safe because all
- // assets referenced by this asset will also be wrapped. Otherwise, inline the
- // asset content where the import statement was.
- if (shouldWrap) {
- depContent.push(this.visitAsset(resolved));
- } else {
- let [depCode, depMap, depLines] = this.visitAsset(resolved);
- res = depCode + '\n' + res;
- lines += 1 + depLines;
- map = depMap;
- }
- }
-
- // Push this asset's source mappings down by the number of lines in the dependency
- // plus the number of hoisted parcelRequires. Then insert the source map for the dependency.
- if (sourceMap) {
- if (lines > 0) {
- sourceMap.offsetLines(lineCount + 1, lines);
- }
-
- if (map) {
- sourceMap.addSourceMap(map, lineCount);
- }
- }
-
- replacement += res;
- lineCount += lines;
- }
- }
- return replacement;
- }
-
- // If it wasn't a dependency, then it was an inline replacement (e.g. $id$import$foo -> $id$export$foo).
- let replacement = replacements.get(m) ?? m;
- if (sourceMap) {
- // Offset the source map columns for this line if the replacement was a different length.
- // This assumes that the match and replacement both do not contain any newlines.
- let lengthDifference = replacement.length - m.length;
- if (lengthDifference !== 0) {
- sourceMap.offsetColumns(
- lineCount + 1,
- i + offset - columnStartIndex + m.length,
- lengthDifference,
- );
- offset += lengthDifference;
- }
- }
- return replacement;
- });
- }
-
- // If the asset is wrapped, we need to insert the dependency code outside the parcelRequire.register
- // wrapper. Dependencies must be inserted AFTER the asset is registered so that circular dependencies work.
- if (shouldWrap) {
- // Offset by one line for the parcelRequire.register wrapper.
- sourceMap?.offsetLines(1, 1);
- lineCount++;
-
- code = `parcelRegister(${JSON.stringify(
- this.bundleGraph.getAssetPublicId(asset),
- )}, function(module, exports) {
-${code}
-});
-`;
-
- lineCount += 2;
-
- for (let [depCode, map, lines] of depContent) {
- if (!depCode) continue;
- code += depCode + '\n';
- if (sourceMap && map) {
- sourceMap.addSourceMap(map, lineCount);
- }
- lineCount += lines + 1;
- }
-
- this.needsPrelude = true;
- }
-
- if (
- !shouldWrap &&
- this.shouldBundleQueue(this.bundle) &&
- this.bundle.getEntryAssets().some(entry => entry.id === asset.id)
- ) {
- code = this.runWhenReady(this.bundle, code);
- }
-
- return [code, sourceMap, lineCount];
- }
-
- buildReplacements(
- asset: Asset,
- deps: Array,
- ): [Map>, Map] {
- let assetId = asset.meta.id;
- invariant(typeof assetId === 'string');
-
- // Build two maps: one of import specifiers, and one of imported symbols to replace.
- // These will be used to build a regex below.
- let depMap = new DefaultMap>(() => []);
- let replacements = new Map();
- for (let dep of deps) {
- let specifierType =
- dep.specifierType === 'esm' ? `:${dep.specifierType}` : '';
- depMap
- .get(
- `${assetId}:${getSpecifier(dep)}${
- !dep.meta.placeholder ? specifierType : ''
- }`,
- )
- .push(dep);
-
- let asyncResolution = this.bundleGraph.resolveAsyncDependency(
- dep,
- this.bundle,
- );
- let resolved =
- asyncResolution?.type === 'asset'
- ? // Prefer the underlying asset over a runtime to load it. It will
- // be wrapped in Promise.resolve() later.
- asyncResolution.value
- : this.bundleGraph.getResolvedAsset(dep, this.bundle);
- if (
- !resolved &&
- !dep.isOptional &&
- !this.bundleGraph.isDependencySkipped(dep)
- ) {
- this.addExternal(dep, replacements);
- }
-
- if (!resolved) {
- continue;
- }
-
- // Handle imports from other bundles in libraries.
- if (this.bundle.env.isLibrary && !this.bundle.hasAsset(resolved)) {
- let referencedBundle = this.bundleGraph.getReferencedBundle(
- dep,
- this.bundle,
- );
- if (
- referencedBundle &&
- referencedBundle.getMainEntry() === resolved &&
- referencedBundle.type === 'js' &&
- !this.bundleGraph.isAssetReferenced(referencedBundle, resolved)
- ) {
- this.addExternal(dep, replacements, referencedBundle);
- this.externalAssets.add(resolved);
- continue;
- }
- }
-
- for (let [imported, {local}] of dep.symbols) {
- if (local === '*') {
- continue;
- }
-
- let symbol = this.getSymbolResolution(asset, resolved, imported, dep);
- replacements.set(
- local,
- // If this was an internalized async asset, wrap in a Promise.resolve.
- asyncResolution?.type === 'asset'
- ? `Promise.resolve(${symbol})`
- : symbol,
- );
- }
-
- // Async dependencies need a namespace object even if all used symbols were statically analyzed.
- // This is recorded in the promiseSymbol meta property set by the transformer rather than in
- // symbols so that we don't mark all symbols as used.
- if (dep.priority === 'lazy' && dep.meta.promiseSymbol) {
- let promiseSymbol = dep.meta.promiseSymbol;
- invariant(typeof promiseSymbol === 'string');
- let symbol = this.getSymbolResolution(asset, resolved, '*', dep);
- replacements.set(
- promiseSymbol,
- asyncResolution?.type === 'asset'
- ? `Promise.resolve(${symbol})`
- : symbol,
- );
- }
- }
-
- // If this asset is wrapped, we need to replace the exports namespace with `module.exports`,
- // which will be provided to us by the wrapper.
- if (
- this.wrappedAssets.has(asset.id) ||
- (this.bundle.env.outputFormat === 'commonjs' &&
- asset === this.bundle.getMainEntry())
- ) {
- let exportsName = asset.symbols.get('*')?.local || `$${assetId}$exports`;
- replacements.set(exportsName, 'module.exports');
- }
-
- return [depMap, replacements];
- }
-
- addExternal(
- dep: Dependency,
- replacements?: Map,
- referencedBundle?: NamedBundle,
- ) {
- if (this.bundle.env.outputFormat === 'global') {
- throw new ThrowableDiagnostic({
- diagnostic: {
- message:
- 'External modules are not supported when building for browser',
- codeFrames: [
- {
- filePath: nullthrows(dep.sourcePath),
- codeHighlights: dep.loc
- ? [convertSourceLocationToHighlight(dep.loc)]
- : [],
- },
- ],
- },
- });
- }
-
- let specifier = dep.specifier;
- if (referencedBundle) {
- specifier = relativeBundlePath(this.bundle, referencedBundle);
- }
-
- // Map of DependencySpecifier -> Map>
- let external = this.externals.get(specifier);
- if (!external) {
- external = new Map();
- this.externals.set(specifier, external);
- }
-
- for (let [imported, {local}] of dep.symbols) {
- // If already imported, just add the already renamed variable to the mapping.
- let renamed = external.get(imported);
- if (renamed && local !== '*' && replacements) {
- replacements.set(local, renamed);
- continue;
- }
-
- // For CJS output, always use a property lookup so that exports remain live.
- // For ESM output, use named imports which are always live.
- if (this.bundle.env.outputFormat === 'commonjs') {
- renamed = external.get('*');
- if (!renamed) {
- if (referencedBundle) {
- let entry = nullthrows(referencedBundle.getMainEntry());
- renamed =
- entry.symbols.get('*')?.local ??
- `$${String(entry.meta.id)}$exports`;
- } else {
- renamed = this.getTopLevelName(
- `$${this.bundle.publicId}$${specifier}`,
- );
- }
-
- external.set('*', renamed);
- }
-
- if (local !== '*' && replacements) {
- let replacement;
- if (imported === '*') {
- replacement = renamed;
- } else if (imported === 'default') {
- let needsDefaultInterop = true;
- if (referencedBundle) {
- let entry = nullthrows(referencedBundle.getMainEntry());
- needsDefaultInterop = this.needsDefaultInterop(entry);
- }
- if (needsDefaultInterop) {
- replacement = `($parcel$interopDefault(${renamed}))`;
- this.usedHelpers.add('$parcel$interopDefault');
- } else {
- replacement = `${renamed}.default`;
- }
- } else {
- replacement = this.getPropertyAccess(renamed, imported);
- }
-
- replacements.set(local, replacement);
- }
- } else {
- let property;
- if (referencedBundle) {
- let entry = nullthrows(referencedBundle.getMainEntry());
- if (entry.symbols.hasExportSymbol('*')) {
- // If importing * and the referenced module has a * export (e.g. CJS), use default instead.
- // This mirrors the logic in buildExportedSymbols.
- property = imported;
- imported =
- referencedBundle?.env.outputFormat === 'esmodule'
- ? 'default'
- : '*';
- } else {
- if (imported === '*') {
- let exportedSymbols = this.bundleGraph.getExportedSymbols(entry);
- if (local === '*') {
- // Re-export all symbols.
- for (let exported of exportedSymbols) {
- if (exported.symbol) {
- external.set(exported.exportSymbol, exported.symbol);
- }
- }
- continue;
- }
- }
- renamed = this.bundleGraph.getSymbolResolution(
- entry,
- imported,
- this.bundle,
- ).symbol;
- }
- }
-
- // Rename the specifier so that multiple local imports of the same imported specifier
- // are deduplicated. We have to prefix the imported name with the bundle id so that
- // local variables do not shadow it.
- if (!renamed) {
- if (this.exportedSymbols.has(local)) {
- renamed = local;
- } else if (imported === 'default' || imported === '*') {
- renamed = this.getTopLevelName(
- `$${this.bundle.publicId}$${specifier}`,
- );
- } else {
- renamed = this.getTopLevelName(
- `$${this.bundle.publicId}$${imported}`,
- );
- }
- }
-
- external.set(imported, renamed);
- if (local !== '*' && replacements) {
- let replacement = renamed;
- if (property === '*') {
- replacement = renamed;
- } else if (property === 'default') {
- replacement = `($parcel$interopDefault(${renamed}))`;
- this.usedHelpers.add('$parcel$interopDefault');
- } else if (property) {
- replacement = this.getPropertyAccess(renamed, property);
- }
- replacements.set(local, replacement);
- }
- }
- }
- }
-
- isWrapped(resolved: Asset, parentAsset: Asset): boolean {
- if (resolved.meta.isConstantModule) {
- if (!this.bundle.hasAsset(resolved)) {
- throw new AssertionError({
- message: `Constant module ${path.relative(
- this.options.projectRoot,
- resolved.filePath,
- )} referenced from ${path.relative(
- this.options.projectRoot,
- parentAsset.filePath,
- )} not found in bundle ${this.bundle.name}`,
- });
- }
- return false;
- }
- return (
- (!this.bundle.hasAsset(resolved) && !this.externalAssets.has(resolved)) ||
- (this.wrappedAssets.has(resolved.id) && resolved !== parentAsset)
- );
- }
-
- getSymbolResolution(
- parentAsset: Asset,
- resolved: Asset,
- imported: string,
- dep?: Dependency,
- replacements?: Map,
- ): string {
- let {
- asset: resolvedAsset,
- exportSymbol,
- symbol,
- } = this.bundleGraph.getSymbolResolution(resolved, imported, this.bundle);
-
- if (
- resolvedAsset.type !== 'js' ||
- (dep && this.bundleGraph.isDependencySkipped(dep))
- ) {
- // Graceful fallback for non-js imports or when trying to resolve a symbol
- // that is actually unused but we still need a placeholder value.
- return '{}';
- }
-
- let isWrapped = this.isWrapped(resolvedAsset, parentAsset);
- let staticExports = resolvedAsset.meta.staticExports !== false;
- let publicId = this.bundleGraph.getAssetPublicId(resolvedAsset);
-
- // External CommonJS dependencies need to be accessed as an object property rather than imported
- // directly to maintain live binding.
- let isExternalCommonJS =
- !isWrapped &&
- this.bundle.env.isLibrary &&
- this.bundle.env.outputFormat === 'commonjs' &&
- !this.bundle.hasAsset(resolvedAsset);
-
- // If the resolved asset is wrapped, but imported at the top-level by this asset,
- // then we hoist parcelRequire calls to the top of this asset so side effects run immediately.
- if (
- isWrapped &&
- dep &&
- !dep?.meta.shouldWrap &&
- symbol !== false &&
- // Only do this if the asset is part of a different bundle (so it was definitely
- // parcelRequire.register'ed there), or if it is indeed registered in this bundle.
- (!this.bundle.hasAsset(resolvedAsset) ||
- !this.shouldSkipAsset(resolvedAsset))
- ) {
- let hoisted = this.hoistedRequires.get(dep.id);
- if (!hoisted) {
- hoisted = new Map();
- this.hoistedRequires.set(dep.id, hoisted);
- }
-
- hoisted.set(
- resolvedAsset.id,
- `var $${publicId} = parcelRequire(${JSON.stringify(publicId)});`,
- );
- }
-
- if (isWrapped) {
- this.needsPrelude = true;
- }
-
- // If this is an ESM default import of a CJS module with a `default` symbol,
- // and no __esModule flag, we need to resolve to the namespace instead.
- let isDefaultInterop =
- exportSymbol === 'default' &&
- staticExports &&
- !isWrapped &&
- (dep?.meta.kind === 'Import' || dep?.meta.kind === 'Export') &&
- resolvedAsset.symbols.hasExportSymbol('*') &&
- resolvedAsset.symbols.hasExportSymbol('default') &&
- !resolvedAsset.symbols.hasExportSymbol('__esModule');
-
- // Find the namespace object for the resolved module. If wrapped and this
- // is an inline require (not top-level), use a parcelRequire call, otherwise
- // the hoisted variable declared above. Otherwise, if not wrapped, use the
- // namespace export symbol.
- let assetId = resolvedAsset.meta.id;
- invariant(typeof assetId === 'string');
- let obj;
- if (isWrapped && (!dep || dep?.meta.shouldWrap)) {
- // Wrap in extra parenthesis to not change semantics, e.g.`new (parcelRequire("..."))()`.
- obj = `(parcelRequire(${JSON.stringify(publicId)}))`;
- } else if (isWrapped && dep) {
- obj = `$${publicId}`;
- } else {
- obj = resolvedAsset.symbols.get('*')?.local || `$${assetId}$exports`;
- obj = replacements?.get(obj) || obj;
- }
-
- if (imported === '*' || exportSymbol === '*' || isDefaultInterop) {
- // Resolve to the namespace object if requested or this is a CJS default interop reqiure.
- if (
- parentAsset === resolvedAsset &&
- this.wrappedAssets.has(resolvedAsset.id)
- ) {
- // Directly use module.exports for wrapped assets importing themselves.
- return 'module.exports';
- } else {
- return obj;
- }
- } else if (
- (!staticExports || isWrapped || !symbol || isExternalCommonJS) &&
- resolvedAsset !== parentAsset
- ) {
- // If the resolved asset is wrapped or has non-static exports,
- // we need to use a member access off the namespace object rather
- // than a direct reference. If importing default from a CJS module,
- // use a helper to check the __esModule flag at runtime.
- let kind = dep?.meta.kind;
- if (
- (!dep || kind === 'Import' || kind === 'Export') &&
- exportSymbol === 'default' &&
- resolvedAsset.symbols.hasExportSymbol('*') &&
- this.needsDefaultInterop(resolvedAsset)
- ) {
- this.usedHelpers.add('$parcel$interopDefault');
- return `(/*@__PURE__*/$parcel$interopDefault(${obj}))`;
- } else {
- return this.getPropertyAccess(obj, exportSymbol);
- }
- } else if (!symbol) {
- invariant(false, 'Asset was skipped or not found.');
- } else {
- return replacements?.get(symbol) || symbol;
- }
- }
-
- getHoistedParcelRequires(
- parentAsset: Asset,
- dep: Dependency,
- resolved: Asset,
- ): [string, number] {
- if (resolved.type !== 'js') {
- return ['', 0];
- }
-
- let hoisted = this.hoistedRequires.get(dep.id);
- let res = '';
- let lineCount = 0;
- let isWrapped = this.isWrapped(resolved, parentAsset);
-
- // If the resolved asset is wrapped and is imported in the top-level by this asset,
- // we need to run side effects when this asset runs. If the resolved asset is not
- // the first one in the hoisted requires, we need to insert a parcelRequire here
- // so it runs first.
- if (
- isWrapped &&
- !dep.meta.shouldWrap &&
- (!hoisted || hoisted.keys().next().value !== resolved.id) &&
- !this.bundleGraph.isDependencySkipped(dep) &&
- !this.shouldSkipAsset(resolved)
- ) {
- this.needsPrelude = true;
- res += `parcelRequire(${JSON.stringify(
- this.bundleGraph.getAssetPublicId(resolved),
- )});`;
- }
-
- if (hoisted) {
- this.needsPrelude = true;
- res += '\n' + [...hoisted.values()].join('\n');
- lineCount += hoisted.size;
- }
-
- return [res, lineCount];
- }
-
- buildAssetPrelude(
- asset: Asset,
- deps: Array,
- replacements: Map,
- ): [string, number, string] {
- let prepend = '';
- let prependLineCount = 0;
- let append = '';
-
- let shouldWrap = this.wrappedAssets.has(asset.id);
- let usedSymbols = nullthrows(this.bundleGraph.getUsedSymbols(asset));
- let assetId = asset.meta.id;
- invariant(typeof assetId === 'string');
-
- // If the asset has a namespace export symbol, it is CommonJS.
- // If there's no __esModule flag, and default is a used symbol, we need
- // to insert an interop helper.
- let defaultInterop =
- asset.symbols.hasExportSymbol('*') &&
- usedSymbols.has('default') &&
- !asset.symbols.hasExportSymbol('__esModule');
-
- let usedNamespace =
- // If the asset has * in its used symbols, we might need the exports namespace.
- // The one case where this isn't true is in ESM library entries, where the only
- // dependency on * is the entry dependency. In this case, we will use ESM exports
- // instead of the namespace object.
- (usedSymbols.has('*') &&
- (this.bundle.env.outputFormat !== 'esmodule' ||
- !this.bundle.env.isLibrary ||
- asset !== this.bundle.getMainEntry() ||
- this.bundleGraph
- .getIncomingDependencies(asset)
- .some(
- dep =>
- !dep.isEntry &&
- this.bundle.hasDependency(dep) &&
- nullthrows(this.bundleGraph.getUsedSymbols(dep)).has('*'),
- ))) ||
- // If a symbol is imported (used) from a CJS asset but isn't listed in the symbols,
- // we fallback on the namespace object.
- (asset.symbols.hasExportSymbol('*') &&
- [...usedSymbols].some(s => !asset.symbols.hasExportSymbol(s))) ||
- // If the exports has this asset's namespace (e.g. ESM output from CJS input),
- // include the namespace object for the default export.
- this.exportedSymbols.has(`$${assetId}$exports`) ||
- // CommonJS library bundle entries always need a namespace.
- (this.bundle.env.isLibrary &&
- this.bundle.env.outputFormat === 'commonjs' &&
- asset === this.bundle.getMainEntry());
-
- // If the asset doesn't have static exports, should wrap, the namespace is used,
- // or we need default interop, then we need to synthesize a namespace object for
- // this asset.
- if (
- asset.meta.staticExports === false ||
- shouldWrap ||
- usedNamespace ||
- defaultInterop
- ) {
- // Insert a declaration for the exports namespace object. If the asset is wrapped
- // we don't need to do this, because we'll use the `module.exports` object provided
- // by the wrapper instead. This is also true of CommonJS entry assets, which will use
- // the `module.exports` object provided by CJS.
- if (
- !shouldWrap &&
- (this.bundle.env.outputFormat !== 'commonjs' ||
- asset !== this.bundle.getMainEntry())
- ) {
- prepend += `var $${assetId}$exports = {};\n`;
- prependLineCount++;
- }
-
- // Insert the __esModule interop flag for this module if it has a `default` export
- // and the namespace symbol is used.
- // TODO: only if required by CJS?
- if (asset.symbols.hasExportSymbol('default') && usedSymbols.has('*')) {
- prepend += `\n$parcel$defineInteropFlag($${assetId}$exports);\n`;
- prependLineCount += 2;
- this.usedHelpers.add('$parcel$defineInteropFlag');
- }
-
- // Find wildcard re-export dependencies, and make sure their exports are also included in
- // ours. Importantly, add them before the asset's own exports so that wildcard exports get
- // correctly overwritten by own exports of the same name.
- for (let dep of deps) {
- let resolved = this.bundleGraph.getResolvedAsset(dep, this.bundle);
- if (dep.isOptional || this.bundleGraph.isDependencySkipped(dep)) {
- continue;
- }
-
- let isWrapped = resolved && resolved.meta.shouldWrap;
-
- for (let [imported, {local}] of dep.symbols) {
- if (imported === '*' && local === '*') {
- if (!resolved) {
- // Re-exporting an external module. This should have already been handled in buildReplacements.
- let external = nullthrows(
- nullthrows(this.externals.get(dep.specifier)).get('*'),
- );
- append += `$parcel$exportWildcard($${assetId}$exports, ${external});\n`;
- this.usedHelpers.add('$parcel$exportWildcard');
- continue;
- }
-
- // If the resolved asset has an exports object, use the $parcel$exportWildcard helper
- // to re-export all symbols. Otherwise, if there's no namespace object available, add
- // $parcel$export calls for each used symbol of the dependency.
- if (
- isWrapped ||
- resolved.meta.staticExports === false ||
- nullthrows(this.bundleGraph.getUsedSymbols(resolved)).has('*') ||
- // an empty asset
- (!resolved.meta.hasCJSExports &&
- resolved.symbols.hasExportSymbol('*'))
- ) {
- let obj = this.getSymbolResolution(
- asset,
- resolved,
- '*',
- dep,
- replacements,
- );
- append += `$parcel$exportWildcard($${assetId}$exports, ${obj});\n`;
- this.usedHelpers.add('$parcel$exportWildcard');
- } else {
- for (let symbol of nullthrows(
- this.bundleGraph.getUsedSymbols(dep),
- )) {
- if (
- symbol === 'default' || // `export * as ...` does not include the default export
- symbol === '__esModule'
- ) {
- continue;
- }
-
- let resolvedSymbol = this.getSymbolResolution(
- asset,
- resolved,
- symbol,
- undefined,
- replacements,
- );
- let get = this.buildFunctionExpression([], resolvedSymbol);
- let set = asset.meta.hasCJSExports
- ? ', ' +
- this.buildFunctionExpression(['v'], `${resolvedSymbol} = v`)
- : '';
- prepend += `$parcel$export($${assetId}$exports, ${JSON.stringify(
- symbol,
- )}, ${get}${set});\n`;
- this.usedHelpers.add('$parcel$export');
- prependLineCount++;
- }
- }
- }
- }
- }
-
- // Find the used exports of this module. This is based on the used symbols of
- // incoming dependencies rather than the asset's own used exports so that we include
- // re-exported symbols rather than only symbols declared in this asset.
- let incomingDeps = this.bundleGraph.getIncomingDependencies(asset);
- let usedExports = [...asset.symbols.exportSymbols()].filter(symbol => {
- if (symbol === '*') {
- return false;
- }
-
- // If we need default interop, then all symbols are needed because the `default`
- // symbol really maps to the whole namespace.
- if (defaultInterop) {
- return true;
- }
-
- let unused = incomingDeps.every(d => {
- let symbols = nullthrows(this.bundleGraph.getUsedSymbols(d));
- return !symbols.has(symbol) && !symbols.has('*');
- });
- return !unused;
- });
-
- if (usedExports.length > 0) {
- // Insert $parcel$export calls for each of the used exports. This creates a getter/setter
- // for the symbol so that when the value changes the object property also changes. This is
- // required to simulate ESM live bindings. It's easier to do it this way rather than inserting
- // additional assignments after each mutation of the original binding.
- prepend += `\n${usedExports
- .map(exp => {
- let resolved = this.getSymbolResolution(
- asset,
- asset,
- exp,
- undefined,
- replacements,
- );
- let get = this.buildFunctionExpression([], resolved);
- let isEsmExport = !!asset.symbols.get(exp)?.meta?.isEsm;
- let set =
- !isEsmExport && asset.meta.hasCJSExports
- ? ', ' + this.buildFunctionExpression(['v'], `${resolved} = v`)
- : '';
- return `$parcel$export($${assetId}$exports, ${JSON.stringify(
- exp,
- )}, ${get}${set});`;
- })
- .join('\n')}\n`;
- this.usedHelpers.add('$parcel$export');
- prependLineCount += 1 + usedExports.length;
- }
- }
-
- return [prepend, prependLineCount, append];
- }
-
- buildBundlePrelude(): [string, number] {
- let enableSourceMaps = this.bundle.env.sourceMap;
- let res = '';
- let lines = 0;
-
- // Add hashbang if the entry asset recorded an interpreter.
- let mainEntry = this.bundle.getMainEntry();
- if (
- mainEntry &&
- !this.isAsyncBundle &&
- !this.bundle.target.env.isBrowser()
- ) {
- let interpreter = mainEntry.meta.interpreter;
- invariant(interpreter == null || typeof interpreter === 'string');
- if (interpreter != null) {
- res += `#!${interpreter}\n`;
- lines++;
- }
- }
-
- // The output format may have specific things to add at the start of the bundle (e.g. imports).
- let [outputFormatPrelude, outputFormatLines] =
- this.outputFormat.buildBundlePrelude();
- res += outputFormatPrelude;
- lines += outputFormatLines;
-
- // Add used helpers.
- if (this.needsPrelude) {
- this.usedHelpers.add('$parcel$global');
- }
-
- for (let helper of this.usedHelpers) {
- let currentHelper = helpers[helper];
- if (typeof currentHelper === 'function') {
- currentHelper = helpers[helper](this.bundle.env);
- }
- res += currentHelper;
- if (enableSourceMaps) {
- lines += countLines(currentHelper) - 1;
- }
- }
-
- if (this.needsPrelude) {
- // Add the prelude if this is potentially the first JS bundle to load in a
- // particular context (e.g. entry scripts in HTML, workers, etc.).
- let parentBundles = this.bundleGraph.getParentBundles(this.bundle);
- let mightBeFirstJS =
- parentBundles.length === 0 ||
- parentBundles.some(b => b.type !== 'js') ||
- this.bundleGraph
- .getBundleGroupsContainingBundle(this.bundle)
- .some(g => this.bundleGraph.isEntryBundleGroup(g)) ||
- this.bundle.env.isIsolated() ||
- this.bundle.bundleBehavior === 'isolated';
-
- if (mightBeFirstJS) {
- let preludeCode = prelude(this.parcelRequireName);
- res += preludeCode;
- if (enableSourceMaps) {
- lines += countLines(preludeCode) - 1;
- }
-
- if (this.shouldBundleQueue(this.bundle)) {
- let bundleQueuePreludeCode = bundleQueuePrelude(this.bundle.env);
- res += bundleQueuePreludeCode;
- if (enableSourceMaps) {
- lines += countLines(bundleQueuePreludeCode) - 1;
- }
- }
- } else {
- // Otherwise, get the current parcelRequire global.
- const escaped = JSON.stringify(this.parcelRequireName);
- res += `var parcelRequire = $parcel$global[${escaped}];\n`;
- lines++;
- res += `var parcelRegister = parcelRequire.register;\n`;
- lines++;
- }
- }
-
- // Add importScripts for sibling bundles in workers.
- if (this.bundle.env.isWorker() || this.bundle.env.isWorklet()) {
- let importScripts = '';
- let bundles = this.bundleGraph.getReferencedBundles(this.bundle);
- for (let b of bundles) {
- if (this.bundle.env.outputFormat === 'esmodule') {
- // importScripts() is not allowed in native ES module workers.
- importScripts += `import "${relativeBundlePath(this.bundle, b)}";\n`;
- } else {
- importScripts += `importScripts("${relativeBundlePath(
- this.bundle,
- b,
- )}");\n`;
- }
- }
-
- res += importScripts;
- lines += bundles.length;
- }
-
- return [res, lines];
- }
-
- needsDefaultInterop(asset: Asset): boolean {
- if (
- asset.symbols.hasExportSymbol('*') &&
- !asset.symbols.hasExportSymbol('default')
- ) {
- if (getFeatureFlag('fastNeedsDefaultInterop')) {
- return true;
- }
-
- let deps = this.bundleGraph.getIncomingDependencies(asset);
- return deps.some(
- dep =>
- this.bundle.hasDependency(dep) &&
- // dep.meta.isES6Module &&
- dep.symbols.hasExportSymbol('default'),
- );
- }
-
- return false;
- }
-
- shouldSkipAsset(asset: Asset): boolean {
- if (this.isScriptEntry(asset)) {
- return true;
- }
-
- return (
- asset.sideEffects === false &&
- nullthrows(this.bundleGraph.getUsedSymbols(asset)).size == 0 &&
- !this.bundleGraph.isAssetReferenced(this.bundle, asset)
- );
- }
-
- isScriptEntry(asset: Asset): boolean {
- return (
- this.bundle.env.outputFormat === 'global' &&
- this.bundle.env.sourceType === 'script' &&
- asset === this.bundle.getMainEntry()
- );
- }
-
- buildFunctionExpression(args: Array, expr: string): string {
- return this.bundle.env.supports('arrow-functions', true)
- ? `(${args.join(', ')}) => ${expr}`
- : `function (${args.join(', ')}) { return ${expr}; }`;
- }
-}
diff --git a/packages/packagers/js/src/ScopeHoistingPackager.ts b/packages/packagers/js/src/ScopeHoistingPackager.ts
new file mode 100644
index 000000000..bc53c09f2
--- /dev/null
+++ b/packages/packagers/js/src/ScopeHoistingPackager.ts
@@ -0,0 +1,1583 @@
+import type {
+ Asset,
+ BundleGraph,
+ Dependency,
+ PluginOptions,
+ NamedBundle,
+ PluginLogger,
+} from '@atlaspack/types';
+
+import {
+ DefaultMap,
+ PromiseQueue,
+ relativeBundlePath,
+ countLines,
+ normalizeSeparators,
+} from '@atlaspack/utils';
+import SourceMap from '@parcel/source-map';
+import nullthrows from 'nullthrows';
+import invariant, {AssertionError} from 'assert';
+import ThrowableDiagnostic, {
+ convertSourceLocationToHighlight,
+} from '@atlaspack/diagnostic';
+import globals from 'globals';
+import path from 'path';
+import {getFeatureFlag} from '@atlaspack/feature-flags';
+
+import {ESMOutputFormat} from './ESMOutputFormat';
+import {CJSOutputFormat} from './CJSOutputFormat';
+import {GlobalOutputFormat} from './GlobalOutputFormat';
+import {prelude, helpers, bundleQueuePrelude, fnExpr} from './helpers';
+import {
+ replaceScriptDependencies,
+ getSpecifier,
+ isValidIdentifier,
+ makeValidIdentifier,
+} from './utils';
+
+// General regex used to replace imports with the resolved code, references with resolutions,
+// and count the number of newlines in the file for source maps.
+const REPLACEMENT_RE =
+ /\n|import\s+"([0-9a-f]{16,20}:.+?)";|(?:\$[0-9a-f]{16,20}\$exports)|(?:\$[0-9a-f]{16,20}\$(?:import|importAsync|require)\$[0-9a-f]+(?:\$[0-9a-f]+)?)/g;
+
+const BUILTINS = Object.keys(globals.builtin);
+const GLOBALS_BY_CONTEXT = {
+ browser: new Set([...BUILTINS, ...Object.keys(globals.browser)]),
+ 'web-worker': new Set([...BUILTINS, ...Object.keys(globals.worker)]),
+ 'service-worker': new Set([
+ ...BUILTINS,
+ ...Object.keys(globals.serviceworker),
+ ]),
+ worklet: new Set([...BUILTINS]),
+ node: new Set([...BUILTINS, ...Object.keys(globals.node)]),
+ 'electron-main': new Set([...BUILTINS, ...Object.keys(globals.node)]),
+ 'electron-renderer': new Set([
+ ...BUILTINS,
+ ...Object.keys(globals.node),
+ ...Object.keys(globals.browser),
+ ]),
+} as const;
+
+const OUTPUT_FORMATS = {
+ esmodule: ESMOutputFormat,
+ commonjs: CJSOutputFormat,
+ global: GlobalOutputFormat,
+} as const;
+
+export interface OutputFormat {
+ buildBundlePrelude(): [string, number];
+ buildBundlePostlude(): [string, number];
+}
+
+export class ScopeHoistingPackager {
+ options: PluginOptions;
+ bundleGraph: BundleGraph;
+ bundle: NamedBundle;
+ parcelRequireName: string;
+ useAsyncBundleRuntime: boolean;
+ outputFormat: OutputFormat;
+ isAsyncBundle: boolean;
+ globalNames: $ReadOnlySet;
+ // @ts-expect-error - TS2564 - Property 'assetOutputs' has no initializer and is not definitely assigned in the constructor.
+ assetOutputs: Map<
+ string,
+ {
+ code: string;
+ map: Buffer | null | undefined;
+ }
+ >;
+ exportedSymbols: Map<
+ string,
+ {
+ asset: Asset;
+ exportSymbol: string;
+ local: string;
+ exportAs: Array;
+ }
+ > = new Map();
+ externals: Map> = new Map();
+ topLevelNames: Map = new Map();
+ seenAssets: Set = new Set();
+ wrappedAssets: Set = new Set();
+ hoistedRequires: Map> = new Map();
+ needsPrelude: boolean = false;
+ usedHelpers: Set = new Set();
+ externalAssets: Set = new Set();
+ forceSkipWrapAssets: Array = [];
+ logger: PluginLogger;
+
+ constructor(
+ options: PluginOptions,
+ bundleGraph: BundleGraph,
+ bundle: NamedBundle,
+ parcelRequireName: string,
+ useAsyncBundleRuntime: boolean,
+ forceSkipWrapAssets: Array,
+ logger: PluginLogger,
+ ) {
+ this.options = options;
+ this.bundleGraph = bundleGraph;
+ this.bundle = bundle;
+ this.parcelRequireName = parcelRequireName;
+ this.useAsyncBundleRuntime = useAsyncBundleRuntime;
+ this.forceSkipWrapAssets = forceSkipWrapAssets ?? [];
+ this.logger = logger;
+
+ let OutputFormat = OUTPUT_FORMATS[this.bundle.env.outputFormat];
+ this.outputFormat = new OutputFormat(this);
+
+ this.isAsyncBundle =
+ this.bundleGraph.hasParentBundleOfType(this.bundle, 'js') &&
+ !this.bundle.env.isIsolated() &&
+ this.bundle.bundleBehavior !== 'isolated';
+
+ this.globalNames = GLOBALS_BY_CONTEXT[bundle.env.context];
+ }
+
+ async package(): Promise<{
+ contents: string;
+ map: SourceMap | null | undefined;
+ }> {
+ let wrappedAssets = await this.loadAssets();
+ this.buildExportedSymbols();
+
+ // If building a library, the target is actually another bundler rather
+ // than the final output that could be loaded in a browser. So, loader
+ // runtimes are excluded, and instead we add imports into the entry bundle
+ // of each bundle group pointing at the sibling bundles. These can be
+ // picked up by another bundler later at which point runtimes will be added.
+ if (
+ this.bundle.env.isLibrary ||
+ this.bundle.env.outputFormat === 'commonjs'
+ ) {
+ for (let b of this.bundleGraph.getReferencedBundles(this.bundle, {
+ recursive: false,
+ })) {
+ this.externals.set(relativeBundlePath(this.bundle, b), new Map());
+ }
+ }
+
+ let res = '';
+ let lineCount = 0;
+ // @ts-expect-error - TS7034 - Variable 'sourceMap' implicitly has type 'any' in some locations where its type cannot be determined.
+ let sourceMap = null;
+ let processAsset = (asset: Asset) => {
+ let [content, map, lines] = this.visitAsset(asset);
+ // @ts-expect-error - TS7005 - Variable 'sourceMap' implicitly has an 'any' type.
+ if (sourceMap && map) {
+ // @ts-expect-error - TS7005 - Variable 'sourceMap' implicitly has an 'any' type.
+ sourceMap.addSourceMap(map, lineCount);
+ } else if (this.bundle.env.sourceMap) {
+ sourceMap = map;
+ }
+
+ res += content + '\n';
+ lineCount += lines + 1;
+ };
+
+ // Hoist wrapped asset to the top of the bundle to ensure that they are registered
+ // before they are used.
+ for (let asset of wrappedAssets) {
+ if (!this.seenAssets.has(asset.id)) {
+ processAsset(asset);
+ }
+ }
+
+ // Add each asset that is directly connected to the bundle. Dependencies will be handled
+ // by replacing `import` statements in the code.
+ this.bundle.traverseAssets((asset, _, actions) => {
+ if (this.seenAssets.has(asset.id)) {
+ actions.skipChildren();
+ return;
+ }
+
+ processAsset(asset);
+ actions.skipChildren();
+ });
+
+ let [prelude, preludeLines] = this.buildBundlePrelude();
+ res = prelude + res;
+ lineCount += preludeLines;
+ // @ts-expect-error - TS2339 - Property 'offsetLines' does not exist on type 'never'.
+ sourceMap?.offsetLines(1, preludeLines);
+
+ let entries = this.bundle.getEntryAssets();
+ let mainEntry = this.bundle.getMainEntry();
+ if (this.isAsyncBundle) {
+ // In async bundles we don't want the main entry to execute until we require it
+ // as there might be dependencies in a sibling bundle that hasn't loaded yet.
+ entries = entries.filter((a) => a.id !== mainEntry?.id);
+ mainEntry = null;
+ }
+
+ let needsBundleQueue = this.shouldBundleQueue(this.bundle);
+
+ // If any of the entry assets are wrapped, call parcelRequire so they are executed.
+ for (let entry of entries) {
+ if (this.wrappedAssets.has(entry.id) && !this.isScriptEntry(entry)) {
+ let parcelRequire = `parcelRequire(${JSON.stringify(
+ this.bundleGraph.getAssetPublicId(entry),
+ )});\n`;
+
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'symbol'.
+ let entryExports = entry.symbols.get('*')?.local;
+
+ if (
+ entryExports &&
+ entry === mainEntry &&
+ // @ts-expect-error - TS2345 - Argument of type 'symbol' is not assignable to parameter of type 'string'.
+ this.exportedSymbols.has(entryExports)
+ ) {
+ invariant(
+ !needsBundleQueue,
+ 'Entry exports are not yet compaitble with async bundles',
+ );
+ // @ts-expect-error - TS2731 - Implicit conversion of a 'symbol' to a 'string' will fail at runtime. Consider wrapping this expression in 'String(...)'.
+ res += `\nvar ${entryExports} = ${parcelRequire}`;
+ } else {
+ if (needsBundleQueue) {
+ parcelRequire = this.runWhenReady(this.bundle, parcelRequire);
+ }
+
+ res += `\n${parcelRequire}`;
+ }
+
+ lineCount += 2;
+ }
+ }
+
+ let [postlude, postludeLines] = this.outputFormat.buildBundlePostlude();
+ res += postlude;
+ lineCount += postludeLines;
+
+ // The entry asset of a script bundle gets hoisted outside the bundle wrapper so that
+ // its top-level variables become globals like a real browser script. We need to replace
+ // all dependency references for runtimes with a parcelRequire call.
+ if (
+ this.bundle.env.outputFormat === 'global' &&
+ this.bundle.env.sourceType === 'script'
+ ) {
+ res += '\n';
+ lineCount++;
+
+ let mainEntry = nullthrows(this.bundle.getMainEntry());
+ let {code, map: mapBuffer} = nullthrows(
+ this.assetOutputs.get(mainEntry.id),
+ );
+ let map;
+ if (mapBuffer) {
+ map = new SourceMap(this.options.projectRoot, mapBuffer);
+ }
+ res += replaceScriptDependencies(
+ this.bundleGraph,
+ this.bundle,
+ code,
+ map,
+ this.parcelRequireName,
+ );
+ if (sourceMap && map) {
+ // @ts-expect-error - TS2339 - Property 'addSourceMap' does not exist on type 'never'.
+ sourceMap.addSourceMap(map, lineCount);
+ }
+ }
+
+ return {
+ contents: res,
+ map: sourceMap,
+ };
+ }
+
+ shouldBundleQueue(bundle: NamedBundle): boolean {
+ let referencingBundles = this.bundleGraph.getReferencingBundles(bundle);
+ let hasHtmlReference = referencingBundles.some((b) => b.type === 'html');
+
+ return (
+ this.useAsyncBundleRuntime &&
+ bundle.type === 'js' &&
+ bundle.bundleBehavior !== 'inline' &&
+ bundle.env.outputFormat === 'esmodule' &&
+ !bundle.env.isIsolated() &&
+ bundle.bundleBehavior !== 'isolated' &&
+ hasHtmlReference
+ );
+ }
+
+ runWhenReady(bundle: NamedBundle, codeToRun: string): string {
+ let deps = this.bundleGraph
+ .getReferencedBundles(bundle)
+ .filter((b) => this.shouldBundleQueue(b))
+ .map((b) => b.publicId);
+
+ if (deps.length === 0) {
+ // If no deps we can safely execute immediately
+ return codeToRun;
+ }
+
+ let params = [
+ JSON.stringify(this.bundle.publicId),
+ fnExpr(this.bundle.env, [], [codeToRun]),
+ JSON.stringify(deps),
+ ];
+
+ return `$parcel$global.rwr(${params.join(', ')});`;
+ }
+
+ async loadAssets(): Promise> {
+ let queue = new PromiseQueue({maxConcurrent: 32});
+ let wrapped: Array = [];
+ this.bundle.traverseAssets((asset) => {
+ queue.add(async () => {
+ let [code, map] = await Promise.all([
+ asset.getCode(),
+ this.bundle.env.sourceMap ? asset.getMapBuffer() : null,
+ ]);
+ return [asset.id, {code, map}];
+ });
+
+ if (
+ asset.meta.shouldWrap ||
+ this.bundle.env.sourceType === 'script' ||
+ this.bundleGraph.isAssetReferenced(this.bundle, asset) ||
+ this.bundleGraph
+ .getIncomingDependencies(asset)
+ .some((dep) => dep.meta.shouldWrap && dep.specifierType !== 'url')
+ ) {
+ // Don't wrap constant "entry" modules _except_ if they are referenced by any lazy dependency
+ if (
+ !asset.meta.isConstantModule ||
+ this.bundleGraph
+ .getIncomingDependencies(asset)
+ .some((dep) => dep.priority === 'lazy')
+ ) {
+ this.wrappedAssets.add(asset.id);
+ wrapped.push(asset);
+ }
+ }
+ });
+
+ for (let wrappedAssetRoot of [...wrapped]) {
+ this.bundle.traverseAssets((asset, _, actions) => {
+ if (asset === wrappedAssetRoot) {
+ return;
+ }
+
+ if (this.wrappedAssets.has(asset.id)) {
+ actions.skipChildren();
+ return;
+ }
+ // This prevents children of a wrapped asset also being wrapped - it's an "unsafe" optimisation
+ // that should only be used when you know (or think you know) what you're doing.
+ //
+ // In particular this can force an async bundle to be scope hoisted where it previously would not be
+ // due to the entry asset being wrapped.
+ if (
+ this.forceSkipWrapAssets.length > 0 &&
+ this.forceSkipWrapAssets.some(
+ (p) =>
+ p === path.relative(this.options.projectRoot, asset.filePath),
+ )
+ ) {
+ this.logger.verbose({
+ message: `Force skipping wrapping of ${path.relative(
+ this.options.projectRoot,
+ asset.filePath,
+ )}`,
+ });
+ actions.skipChildren();
+ return;
+ }
+ if (!asset.meta.isConstantModule) {
+ this.wrappedAssets.add(asset.id);
+ wrapped.push(asset);
+ }
+ }, wrappedAssetRoot);
+ }
+
+ // @ts-expect-error - TS2769 - No overload matches this call.
+ this.assetOutputs = new Map(await queue.run());
+ return wrapped;
+ }
+
+ buildExportedSymbols() {
+ if (
+ !this.bundle.env.isLibrary ||
+ this.bundle.env.outputFormat !== 'esmodule'
+ ) {
+ return;
+ }
+
+ // TODO: handle ESM exports of wrapped entry assets...
+ let entry = this.bundle.getMainEntry();
+ if (entry && !this.wrappedAssets.has(entry.id)) {
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'symbol'.
+ let hasNamespace = entry.symbols.hasExportSymbol('*');
+
+ for (let {
+ asset,
+ exportAs,
+ symbol,
+ exportSymbol,
+ } of this.bundleGraph.getExportedSymbols(entry)) {
+ if (typeof symbol === 'string') {
+ // If the module has a namespace (e.g. commonjs), and this is not an entry, only export the namespace
+ // as default, without individual exports. This mirrors the importing logic in addExternal, avoiding
+ // extra unused exports and potential for non-identifier export names.
+ if (hasNamespace && this.isAsyncBundle && exportAs !== '*') {
+ continue;
+ }
+
+ let symbols = this.exportedSymbols.get(
+ // @ts-expect-error - TS2345 - Argument of type 'symbol' is not assignable to parameter of type 'string'. | TS2345 - Argument of type 'string' is not assignable to parameter of type 'symbol'.
+ symbol === '*' ? nullthrows(entry.symbols.get('*')?.local) : symbol,
+ )?.exportAs;
+
+ if (!symbols) {
+ symbols = [];
+ this.exportedSymbols.set(symbol, {
+ asset,
+ // @ts-expect-error - TS2322 - Type 'string | symbol' is not assignable to type 'string'.
+ exportSymbol,
+ local: symbol,
+ exportAs: symbols,
+ });
+ }
+
+ if (exportAs === '*') {
+ exportAs = 'default';
+ }
+
+ // @ts-expect-error - TS2345 - Argument of type 'string | symbol' is not assignable to parameter of type 'string'.
+ symbols.push(exportAs);
+ } else if (symbol === null) {
+ // TODO `meta.exportsIdentifier[exportSymbol]` should be exported
+ // let relativePath = relative(options.projectRoot, asset.filePath);
+ // throw getThrowableDiagnosticForNode(
+ // md`${relativePath} couldn't be statically analyzed when importing '${exportSymbol}'`,
+ // entry.filePath,
+ // loc,
+ // );
+ } else if (symbol !== false) {
+ // let relativePath = relative(options.projectRoot, asset.filePath);
+ // throw getThrowableDiagnosticForNode(
+ // md`${relativePath} does not export '${exportSymbol}'`,
+ // entry.filePath,
+ // loc,
+ // );
+ }
+ }
+ }
+ }
+
+ getTopLevelName(name: string): string {
+ name = makeValidIdentifier(name);
+ if (this.globalNames.has(name)) {
+ name = '_' + name;
+ }
+
+ let count = this.topLevelNames.get(name);
+ if (count == null) {
+ this.topLevelNames.set(name, 1);
+ return name;
+ }
+
+ this.topLevelNames.set(name, count + 1);
+ return name + count;
+ }
+
+ getPropertyAccess(obj: string, property: string): string {
+ if (isValidIdentifier(property)) {
+ return `${obj}.${property}`;
+ }
+
+ return `${obj}[${JSON.stringify(property)}]`;
+ }
+
+ visitAsset(asset: Asset): [string, SourceMap | null | undefined, number] {
+ invariant(!this.seenAssets.has(asset.id), 'Already visited asset');
+ this.seenAssets.add(asset.id);
+
+ let {code, map} = nullthrows(this.assetOutputs.get(asset.id));
+ return this.buildAsset(asset, code, map);
+ }
+
+ buildAsset(
+ asset: Asset,
+ code: string,
+ map?: Buffer | null,
+ ): [string, SourceMap | null | undefined, number] {
+ let shouldWrap = this.wrappedAssets.has(asset.id);
+ let deps = this.bundleGraph.getDependencies(asset);
+
+ let sourceMap =
+ this.bundle.env.sourceMap && map
+ ? new SourceMap(this.options.projectRoot, map)
+ : null;
+
+ // If this asset is skipped, just add dependencies and not the asset's content.
+ if (this.shouldSkipAsset(asset)) {
+ let depCode = '';
+ let lineCount = 0;
+ for (let dep of deps) {
+ let resolved = this.bundleGraph.getResolvedAsset(dep, this.bundle);
+ let skipped = this.bundleGraph.isDependencySkipped(dep);
+ if (skipped) {
+ continue;
+ }
+
+ if (!resolved) {
+ if (!dep.isOptional) {
+ this.addExternal(dep);
+ }
+
+ continue;
+ }
+
+ if (
+ this.bundle.hasAsset(resolved) &&
+ !this.seenAssets.has(resolved.id)
+ ) {
+ let [code, map, lines] = this.visitAsset(resolved);
+ depCode += code + '\n';
+ if (sourceMap && map) {
+ // @ts-expect-error - TS2551 - Property 'addSourceMap' does not exist on type 'SourceMap'. Did you mean 'addSources'?
+ sourceMap.addSourceMap(map, lineCount);
+ }
+ lineCount += lines + 1;
+ }
+ }
+
+ return [depCode, sourceMap, lineCount];
+ }
+
+ // TODO: maybe a meta prop?
+ if (code.includes('$parcel$global')) {
+ this.usedHelpers.add('$parcel$global');
+ }
+
+ if (this.bundle.env.isNode() && asset.meta.has_node_replacements) {
+ const relPath = normalizeSeparators(
+ path.relative(this.bundle.target.distDir, path.dirname(asset.filePath)),
+ );
+ code = code.replace('$parcel$dirnameReplace', relPath);
+ code = code.replace('$parcel$filenameReplace', relPath);
+ }
+
+ let [depMap, replacements] = this.buildReplacements(asset, deps);
+ let [prepend, prependLines, append] = this.buildAssetPrelude(
+ asset,
+ deps,
+ replacements,
+ );
+ if (prependLines > 0) {
+ sourceMap?.offsetLines(1, prependLines);
+ code = prepend + code;
+ }
+
+ code += append;
+
+ let lineCount = 0;
+ let depContent: Array<[string, NodeSourceMap | null | undefined, number]> =
+ [];
+ if (depMap.size === 0 && replacements.size === 0) {
+ // If there are no dependencies or replacements, use a simple function to count the number of lines.
+ lineCount = countLines(code) - 1;
+ } else {
+ // Otherwise, use a regular expression to perform replacements.
+ // We need to track how many newlines there are for source maps, replace
+ // all import statements with dependency code, and perform inline replacements
+ // of all imported symbols with their resolved export symbols. This is all done
+ // in a single regex so that we only do one pass over the whole code.
+ let offset = 0;
+ let columnStartIndex = 0;
+ code = code.replace(REPLACEMENT_RE, (m, d, i) => {
+ if (m === '\n') {
+ columnStartIndex = i + offset + 1;
+ lineCount++;
+ return '\n';
+ }
+
+ // If we matched an import, replace with the source code for the dependency.
+ if (d != null) {
+ let deps = depMap.get(d);
+ if (!deps) {
+ return m;
+ }
+
+ let replacement = '';
+
+ // A single `${id}:${specifier}:esm` might have been resolved to multiple assets due to
+ // reexports.
+ for (let dep of deps) {
+ let resolved = this.bundleGraph.getResolvedAsset(dep, this.bundle);
+ let skipped = this.bundleGraph.isDependencySkipped(dep);
+ if (resolved && !skipped) {
+ // Hoist variable declarations for the referenced parcelRequire dependencies
+ // after the dependency is declared. This handles the case where the resulting asset
+ // is wrapped, but the dependency in this asset is not marked as wrapped. This means
+ // that it was imported/required at the top-level, so its side effects should run immediately.
+ let [res, lines] = this.getHoistedParcelRequires(
+ asset,
+ dep,
+ resolved,
+ );
+ let map;
+ if (
+ this.bundle.hasAsset(resolved) &&
+ !this.seenAssets.has(resolved.id)
+ ) {
+ // If this asset is wrapped, we need to hoist the code for the dependency
+ // outside our parcelRequire.register wrapper. This is safe because all
+ // assets referenced by this asset will also be wrapped. Otherwise, inline the
+ // asset content where the import statement was.
+ if (shouldWrap) {
+ depContent.push(this.visitAsset(resolved));
+ } else {
+ let [depCode, depMap, depLines] = this.visitAsset(resolved);
+ res = depCode + '\n' + res;
+ lines += 1 + depLines;
+ map = depMap;
+ }
+ }
+
+ // Push this asset's source mappings down by the number of lines in the dependency
+ // plus the number of hoisted parcelRequires. Then insert the source map for the dependency.
+ if (sourceMap) {
+ if (lines > 0) {
+ sourceMap.offsetLines(lineCount + 1, lines);
+ }
+
+ if (map) {
+ // @ts-expect-error - TS2551 - Property 'addSourceMap' does not exist on type 'SourceMap'. Did you mean 'addSources'?
+ sourceMap.addSourceMap(map, lineCount);
+ }
+ }
+
+ replacement += res;
+ lineCount += lines;
+ }
+ }
+ return replacement;
+ }
+
+ // If it wasn't a dependency, then it was an inline replacement (e.g. $id$import$foo -> $id$export$foo).
+ let replacement = replacements.get(m) ?? m;
+ if (sourceMap) {
+ // Offset the source map columns for this line if the replacement was a different length.
+ // This assumes that the match and replacement both do not contain any newlines.
+ let lengthDifference = replacement.length - m.length;
+ if (lengthDifference !== 0) {
+ sourceMap.offsetColumns(
+ lineCount + 1,
+ i + offset - columnStartIndex + m.length,
+ lengthDifference,
+ );
+ offset += lengthDifference;
+ }
+ }
+ return replacement;
+ });
+ }
+
+ // If the asset is wrapped, we need to insert the dependency code outside the parcelRequire.register
+ // wrapper. Dependencies must be inserted AFTER the asset is registered so that circular dependencies work.
+ if (shouldWrap) {
+ // Offset by one line for the parcelRequire.register wrapper.
+ sourceMap?.offsetLines(1, 1);
+ lineCount++;
+
+ code = `parcelRegister(${JSON.stringify(
+ this.bundleGraph.getAssetPublicId(asset),
+ )}, function(module, exports) {
+${code}
+});
+`;
+
+ lineCount += 2;
+
+ for (let [depCode, map, lines] of depContent) {
+ if (!depCode) continue;
+ code += depCode + '\n';
+ if (sourceMap && map) {
+ // @ts-expect-error - TS2551 - Property 'addSourceMap' does not exist on type 'SourceMap'. Did you mean 'addSources'?
+ sourceMap.addSourceMap(map, lineCount);
+ }
+ lineCount += lines + 1;
+ }
+
+ this.needsPrelude = true;
+ }
+
+ if (
+ !shouldWrap &&
+ this.shouldBundleQueue(this.bundle) &&
+ this.bundle.getEntryAssets().some((entry) => entry.id === asset.id)
+ ) {
+ code = this.runWhenReady(this.bundle, code);
+ }
+
+ return [code, sourceMap, lineCount];
+ }
+
+ buildReplacements(
+ asset: Asset,
+ deps: Array,
+ ): [Map>, Map] {
+ let assetId = asset.meta.id;
+ invariant(typeof assetId === 'string');
+
+ // Build two maps: one of import specifiers, and one of imported symbols to replace.
+ // These will be used to build a regex below.
+ let depMap = new DefaultMap>(() => []);
+ let replacements = new Map();
+ for (let dep of deps) {
+ let specifierType =
+ dep.specifierType === 'esm' ? `:${dep.specifierType}` : '';
+ depMap
+ .get(
+ `${assetId}:${getSpecifier(dep)}${
+ !dep.meta.placeholder ? specifierType : ''
+ }`,
+ )
+ .push(dep);
+
+ let asyncResolution = this.bundleGraph.resolveAsyncDependency(
+ dep,
+ this.bundle,
+ );
+ let resolved =
+ asyncResolution?.type === 'asset'
+ ? // Prefer the underlying asset over a runtime to load it. It will
+ // be wrapped in Promise.resolve() later.
+ asyncResolution.value
+ : this.bundleGraph.getResolvedAsset(dep, this.bundle);
+ if (
+ !resolved &&
+ !dep.isOptional &&
+ !this.bundleGraph.isDependencySkipped(dep)
+ ) {
+ this.addExternal(dep, replacements);
+ }
+
+ if (!resolved) {
+ continue;
+ }
+
+ // Handle imports from other bundles in libraries.
+ if (this.bundle.env.isLibrary && !this.bundle.hasAsset(resolved)) {
+ let referencedBundle = this.bundleGraph.getReferencedBundle(
+ dep,
+ this.bundle,
+ );
+ if (
+ referencedBundle &&
+ referencedBundle.getMainEntry() === resolved &&
+ referencedBundle.type === 'js' &&
+ !this.bundleGraph.isAssetReferenced(referencedBundle, resolved)
+ ) {
+ this.addExternal(dep, replacements, referencedBundle);
+ this.externalAssets.add(resolved);
+ continue;
+ }
+ }
+
+ for (let [imported, {local}] of dep.symbols) {
+ // @ts-expect-error - TS2367 - This condition will always return 'false' since the types 'symbol' and 'string' have no overlap.
+ if (local === '*') {
+ continue;
+ }
+
+ // @ts-expect-error - TS2345 - Argument of type 'symbol' is not assignable to parameter of type 'string'.
+ let symbol = this.getSymbolResolution(asset, resolved, imported, dep);
+ replacements.set(
+ local,
+ // If this was an internalized async asset, wrap in a Promise.resolve.
+ asyncResolution?.type === 'asset'
+ ? `Promise.resolve(${symbol})`
+ : symbol,
+ );
+ }
+
+ // Async dependencies need a namespace object even if all used symbols were statically analyzed.
+ // This is recorded in the promiseSymbol meta property set by the transformer rather than in
+ // symbols so that we don't mark all symbols as used.
+ if (dep.priority === 'lazy' && dep.meta.promiseSymbol) {
+ let promiseSymbol = dep.meta.promiseSymbol;
+ invariant(typeof promiseSymbol === 'string');
+ let symbol = this.getSymbolResolution(asset, resolved, '*', dep);
+ replacements.set(
+ promiseSymbol,
+ asyncResolution?.type === 'asset'
+ ? `Promise.resolve(${symbol})`
+ : symbol,
+ );
+ }
+ }
+
+ // If this asset is wrapped, we need to replace the exports namespace with `module.exports`,
+ // which will be provided to us by the wrapper.
+ if (
+ this.wrappedAssets.has(asset.id) ||
+ (this.bundle.env.outputFormat === 'commonjs' &&
+ asset === this.bundle.getMainEntry())
+ ) {
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'symbol'.
+ let exportsName = asset.symbols.get('*')?.local || `$${assetId}$exports`;
+ replacements.set(exportsName, 'module.exports');
+ }
+
+ return [depMap, replacements];
+ }
+
+ addExternal(
+ dep: Dependency,
+ replacements?: Map,
+ referencedBundle?: NamedBundle,
+ ) {
+ if (this.bundle.env.outputFormat === 'global') {
+ throw new ThrowableDiagnostic({
+ diagnostic: {
+ message:
+ 'External modules are not supported when building for browser',
+ codeFrames: [
+ {
+ filePath: nullthrows(dep.sourcePath),
+ codeHighlights: dep.loc
+ ? [convertSourceLocationToHighlight(dep.loc)]
+ : [],
+ },
+ ],
+ },
+ });
+ }
+
+ let specifier = dep.specifier;
+ if (referencedBundle) {
+ specifier = relativeBundlePath(this.bundle, referencedBundle);
+ }
+
+ // Map of DependencySpecifier -> Map>
+ let external = this.externals.get(specifier);
+ if (!external) {
+ external = new Map();
+ this.externals.set(specifier, external);
+ }
+
+ for (let [imported, {local}] of dep.symbols) {
+ // If already imported, just add the already renamed variable to the mapping.
+ // @ts-expect-error - TS2345 - Argument of type 'symbol' is not assignable to parameter of type 'string'.
+ let renamed = external.get(imported);
+ // @ts-expect-error - TS2367 - This condition will always return 'true' since the types 'symbol' and 'string' have no overlap.
+ if (renamed && local !== '*' && replacements) {
+ // @ts-expect-error - TS2345 - Argument of type 'symbol' is not assignable to parameter of type 'string'.
+ replacements.set(local, renamed);
+ continue;
+ }
+
+ // For CJS output, always use a property lookup so that exports remain live.
+ // For ESM output, use named imports which are always live.
+ if (this.bundle.env.outputFormat === 'commonjs') {
+ renamed = external.get('*');
+ if (!renamed) {
+ if (referencedBundle) {
+ let entry = nullthrows(referencedBundle.getMainEntry());
+ // @ts-expect-error - TS2322 - Type 'string | symbol' is not assignable to type 'string | undefined'.
+ renamed =
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'symbol'.
+ entry.symbols.get('*')?.local ??
+ `$${String(entry.meta.id)}$exports`;
+ } else {
+ renamed = this.getTopLevelName(
+ `$${this.bundle.publicId}$${specifier}`,
+ );
+ }
+
+ // @ts-expect-error - TS2345 - Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
+ external.set('*', renamed);
+ }
+
+ // @ts-expect-error - TS2367 - This condition will always return 'true' since the types 'symbol' and 'string' have no overlap.
+ if (local !== '*' && replacements) {
+ let replacement;
+ // @ts-expect-error - TS2367 - This condition will always return 'false' since the types 'symbol' and 'string' have no overlap.
+ if (imported === '*') {
+ replacement = renamed;
+ // @ts-expect-error - TS2367 - This condition will always return 'false' since the types 'symbol' and 'string' have no overlap.
+ } else if (imported === 'default') {
+ let needsDefaultInterop = true;
+ if (referencedBundle) {
+ let entry = nullthrows(referencedBundle.getMainEntry());
+ needsDefaultInterop = this.needsDefaultInterop(entry);
+ }
+ if (needsDefaultInterop) {
+ replacement = `($parcel$interopDefault(${renamed}))`;
+ this.usedHelpers.add('$parcel$interopDefault');
+ } else {
+ replacement = `${renamed}.default`;
+ }
+ } else {
+ // @ts-expect-error - TS2345 - Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
+ replacement = this.getPropertyAccess(renamed, imported);
+ }
+
+ // @ts-expect-error - TS2345 - Argument of type 'symbol' is not assignable to parameter of type 'string'.
+ replacements.set(local, replacement);
+ }
+ } else {
+ let property;
+ if (referencedBundle) {
+ let entry = nullthrows(referencedBundle.getMainEntry());
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'symbol'.
+ if (entry.symbols.hasExportSymbol('*')) {
+ // If importing * and the referenced module has a * export (e.g. CJS), use default instead.
+ // This mirrors the logic in buildExportedSymbols.
+ property = imported;
+ // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'symbol'.
+ imported =
+ referencedBundle?.env.outputFormat === 'esmodule'
+ ? 'default'
+ : '*';
+ } else {
+ // @ts-expect-error - TS2367 - This condition will always return 'false' since the types 'symbol' and 'string' have no overlap.
+ if (imported === '*') {
+ let exportedSymbols = this.bundleGraph.getExportedSymbols(entry);
+ // @ts-expect-error - TS2367 - This condition will always return 'false' since the types 'symbol' and 'string' have no overlap.
+ if (local === '*') {
+ // Re-export all symbols.
+ for (let exported of exportedSymbols) {
+ if (exported.symbol) {
+ // @ts-expect-error - TS2345 - Argument of type 'string | symbol' is not assignable to parameter of type 'string'.
+ external.set(exported.exportSymbol, exported.symbol);
+ }
+ }
+ continue;
+ }
+ }
+ // @ts-expect-error - TS2322 - Type 'false | symbol | null | undefined' is not assignable to type 'string | undefined'.
+ renamed = this.bundleGraph.getSymbolResolution(
+ entry,
+ imported,
+ this.bundle,
+ ).symbol;
+ }
+ }
+
+ // Rename the specifier so that multiple local imports of the same imported specifier
+ // are deduplicated. We have to prefix the imported name with the bundle id so that
+ // local variables do not shadow it.
+ if (!renamed) {
+ // @ts-expect-error - TS2345 - Argument of type 'symbol' is not assignable to parameter of type 'string'.
+ if (this.exportedSymbols.has(local)) {
+ // @ts-expect-error - TS2322 - Type 'symbol' is not assignable to type 'string | undefined'.
+ renamed = local;
+ // @ts-expect-error - TS2367 - This condition will always return 'false' since the types 'symbol' and 'string' have no overlap. | TS2367 - This condition will always return 'false' since the types 'symbol' and 'string' have no overlap.
+ } else if (imported === 'default' || imported === '*') {
+ renamed = this.getTopLevelName(
+ `$${this.bundle.publicId}$${specifier}`,
+ );
+ } else {
+ renamed = this.getTopLevelName(
+ // @ts-expect-error - TS2731 - Implicit conversion of a 'symbol' to a 'string' will fail at runtime. Consider wrapping this expression in 'String(...)'.
+ `$${this.bundle.publicId}$${imported}`,
+ );
+ }
+ }
+
+ // @ts-expect-error - TS2345 - Argument of type 'symbol' is not assignable to parameter of type 'string'.
+ external.set(imported, renamed);
+ // @ts-expect-error - TS2367 - This condition will always return 'true' since the types 'symbol' and 'string' have no overlap.
+ if (local !== '*' && replacements) {
+ let replacement = renamed;
+ // @ts-expect-error - TS2367 - This condition will always return 'false' since the types 'symbol | undefined' and 'string' have no overlap.
+ if (property === '*') {
+ replacement = renamed;
+ // @ts-expect-error - TS2367 - This condition will always return 'false' since the types 'symbol | undefined' and 'string' have no overlap.
+ } else if (property === 'default') {
+ replacement = `($parcel$interopDefault(${renamed}))`;
+ this.usedHelpers.add('$parcel$interopDefault');
+ } else if (property) {
+ // @ts-expect-error - TS2345 - Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
+ replacement = this.getPropertyAccess(renamed, property);
+ }
+ // @ts-expect-error - TS2345 - Argument of type 'symbol' is not assignable to parameter of type 'string'.
+ replacements.set(local, replacement);
+ }
+ }
+ }
+ }
+
+ isWrapped(resolved: Asset, parentAsset: Asset): boolean {
+ if (resolved.meta.isConstantModule) {
+ if (!this.bundle.hasAsset(resolved)) {
+ throw new AssertionError({
+ message: `Constant module ${path.relative(
+ this.options.projectRoot,
+ resolved.filePath,
+ )} referenced from ${path.relative(
+ this.options.projectRoot,
+ parentAsset.filePath,
+ )} not found in bundle ${this.bundle.name}`,
+ });
+ }
+ return false;
+ }
+ return (
+ (!this.bundle.hasAsset(resolved) && !this.externalAssets.has(resolved)) ||
+ (this.wrappedAssets.has(resolved.id) && resolved !== parentAsset)
+ );
+ }
+
+ getSymbolResolution(
+ parentAsset: Asset,
+ resolved: Asset,
+ imported: string,
+ dep?: Dependency,
+ replacements?: Map,
+ ): string {
+ let {
+ asset: resolvedAsset,
+ exportSymbol,
+ symbol,
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'symbol'.
+ } = this.bundleGraph.getSymbolResolution(resolved, imported, this.bundle);
+
+ if (
+ resolvedAsset.type !== 'js' ||
+ (dep && this.bundleGraph.isDependencySkipped(dep))
+ ) {
+ // Graceful fallback for non-js imports or when trying to resolve a symbol
+ // that is actually unused but we still need a placeholder value.
+ return '{}';
+ }
+
+ let isWrapped = this.isWrapped(resolvedAsset, parentAsset);
+ let staticExports = resolvedAsset.meta.staticExports !== false;
+ let publicId = this.bundleGraph.getAssetPublicId(resolvedAsset);
+
+ // External CommonJS dependencies need to be accessed as an object property rather than imported
+ // directly to maintain live binding.
+ let isExternalCommonJS =
+ !isWrapped &&
+ this.bundle.env.isLibrary &&
+ this.bundle.env.outputFormat === 'commonjs' &&
+ !this.bundle.hasAsset(resolvedAsset);
+
+ // If the resolved asset is wrapped, but imported at the top-level by this asset,
+ // then we hoist parcelRequire calls to the top of this asset so side effects run immediately.
+ if (
+ isWrapped &&
+ dep &&
+ !dep?.meta.shouldWrap &&
+ symbol !== false &&
+ // Only do this if the asset is part of a different bundle (so it was definitely
+ // parcelRequire.register'ed there), or if it is indeed registered in this bundle.
+ (!this.bundle.hasAsset(resolvedAsset) ||
+ !this.shouldSkipAsset(resolvedAsset))
+ ) {
+ let hoisted = this.hoistedRequires.get(dep.id);
+ if (!hoisted) {
+ hoisted = new Map();
+ this.hoistedRequires.set(dep.id, hoisted);
+ }
+
+ hoisted.set(
+ resolvedAsset.id,
+ `var $${publicId} = parcelRequire(${JSON.stringify(publicId)});`,
+ );
+ }
+
+ if (isWrapped) {
+ this.needsPrelude = true;
+ }
+
+ // If this is an ESM default import of a CJS module with a `default` symbol,
+ // and no __esModule flag, we need to resolve to the namespace instead.
+ let isDefaultInterop =
+ exportSymbol === 'default' &&
+ staticExports &&
+ !isWrapped &&
+ (dep?.meta.kind === 'Import' || dep?.meta.kind === 'Export') &&
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'symbol'.
+ resolvedAsset.symbols.hasExportSymbol('*') &&
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'symbol'.
+ resolvedAsset.symbols.hasExportSymbol('default') &&
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'symbol'.
+ !resolvedAsset.symbols.hasExportSymbol('__esModule');
+
+ // Find the namespace object for the resolved module. If wrapped and this
+ // is an inline require (not top-level), use a parcelRequire call, otherwise
+ // the hoisted variable declared above. Otherwise, if not wrapped, use the
+ // namespace export symbol.
+ let assetId = resolvedAsset.meta.id;
+ invariant(typeof assetId === 'string');
+ let obj;
+ if (isWrapped && (!dep || dep?.meta.shouldWrap)) {
+ // Wrap in extra parenthesis to not change semantics, e.g.`new (parcelRequire("..."))()`.
+ obj = `(parcelRequire(${JSON.stringify(publicId)}))`;
+ } else if (isWrapped && dep) {
+ obj = `$${publicId}`;
+ } else {
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'symbol'.
+ obj = resolvedAsset.symbols.get('*')?.local || `$${assetId}$exports`;
+ // @ts-expect-error - TS2345 - Argument of type 'string | symbol' is not assignable to parameter of type 'string'.
+ obj = replacements?.get(obj) || obj;
+ }
+
+ if (imported === '*' || exportSymbol === '*' || isDefaultInterop) {
+ // Resolve to the namespace object if requested or this is a CJS default interop reqiure.
+ if (
+ parentAsset === resolvedAsset &&
+ this.wrappedAssets.has(resolvedAsset.id)
+ ) {
+ // Directly use module.exports for wrapped assets importing themselves.
+ return 'module.exports';
+ } else {
+ // @ts-expect-error - TS2322 - Type 'string | symbol' is not assignable to type 'string'.
+ return obj;
+ }
+ } else if (
+ (!staticExports || isWrapped || !symbol || isExternalCommonJS) &&
+ resolvedAsset !== parentAsset
+ ) {
+ // If the resolved asset is wrapped or has non-static exports,
+ // we need to use a member access off the namespace object rather
+ // than a direct reference. If importing default from a CJS module,
+ // use a helper to check the __esModule flag at runtime.
+ let kind = dep?.meta.kind;
+ if (
+ (!dep || kind === 'Import' || kind === 'Export') &&
+ exportSymbol === 'default' &&
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'symbol'.
+ resolvedAsset.symbols.hasExportSymbol('*') &&
+ this.needsDefaultInterop(resolvedAsset)
+ ) {
+ this.usedHelpers.add('$parcel$interopDefault');
+ // @ts-expect-error - TS2731 - Implicit conversion of a 'symbol' to a 'string' will fail at runtime. Consider wrapping this expression in 'String(...)'.
+ return `(/*@__PURE__*/$parcel$interopDefault(${obj}))`;
+ } else {
+ // @ts-expect-error - TS2345 - Argument of type 'string | symbol' is not assignable to parameter of type 'string'.
+ return this.getPropertyAccess(obj, exportSymbol);
+ }
+ } else if (!symbol) {
+ invariant(false, 'Asset was skipped or not found.');
+ } else {
+ // @ts-expect-error - TS2322 - Type 'string | symbol' is not assignable to type 'string'. | TS2345 - Argument of type 'symbol' is not assignable to parameter of type 'string'.
+ return replacements?.get(symbol) || symbol;
+ }
+ }
+
+ getHoistedParcelRequires(
+ parentAsset: Asset,
+ dep: Dependency,
+ resolved: Asset,
+ ): [string, number] {
+ if (resolved.type !== 'js') {
+ return ['', 0];
+ }
+
+ let hoisted = this.hoistedRequires.get(dep.id);
+ let res = '';
+ let lineCount = 0;
+ let isWrapped = this.isWrapped(resolved, parentAsset);
+
+ // If the resolved asset is wrapped and is imported in the top-level by this asset,
+ // we need to run side effects when this asset runs. If the resolved asset is not
+ // the first one in the hoisted requires, we need to insert a parcelRequire here
+ // so it runs first.
+ if (
+ isWrapped &&
+ !dep.meta.shouldWrap &&
+ (!hoisted || hoisted.keys().next().value !== resolved.id) &&
+ !this.bundleGraph.isDependencySkipped(dep) &&
+ !this.shouldSkipAsset(resolved)
+ ) {
+ this.needsPrelude = true;
+ res += `parcelRequire(${JSON.stringify(
+ this.bundleGraph.getAssetPublicId(resolved),
+ )});`;
+ }
+
+ if (hoisted) {
+ this.needsPrelude = true;
+ res += '\n' + [...hoisted.values()].join('\n');
+ lineCount += hoisted.size;
+ }
+
+ return [res, lineCount];
+ }
+
+ buildAssetPrelude(
+ asset: Asset,
+ deps: Array,
+ replacements: Map,
+ ): [string, number, string] {
+ let prepend = '';
+ let prependLineCount = 0;
+ let append = '';
+
+ let shouldWrap = this.wrappedAssets.has(asset.id);
+ let usedSymbols = nullthrows(this.bundleGraph.getUsedSymbols(asset));
+ let assetId = asset.meta.id;
+ invariant(typeof assetId === 'string');
+
+ // If the asset has a namespace export symbol, it is CommonJS.
+ // If there's no __esModule flag, and default is a used symbol, we need
+ // to insert an interop helper.
+ let defaultInterop =
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'symbol'.
+ asset.symbols.hasExportSymbol('*') &&
+ usedSymbols.has('default') &&
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'symbol'.
+ !asset.symbols.hasExportSymbol('__esModule');
+
+ let usedNamespace =
+ // If the asset has * in its used symbols, we might need the exports namespace.
+ // The one case where this isn't true is in ESM library entries, where the only
+ // dependency on * is the entry dependency. In this case, we will use ESM exports
+ // instead of the namespace object.
+ (usedSymbols.has('*') &&
+ (this.bundle.env.outputFormat !== 'esmodule' ||
+ !this.bundle.env.isLibrary ||
+ asset !== this.bundle.getMainEntry() ||
+ this.bundleGraph
+ .getIncomingDependencies(asset)
+ .some(
+ (dep) =>
+ !dep.isEntry &&
+ this.bundle.hasDependency(dep) &&
+ nullthrows(this.bundleGraph.getUsedSymbols(dep)).has('*'),
+ ))) ||
+ // If a symbol is imported (used) from a CJS asset but isn't listed in the symbols,
+ // we fallback on the namespace object.
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'symbol'.
+ (asset.symbols.hasExportSymbol('*') &&
+ [...usedSymbols].some((s) => !asset.symbols.hasExportSymbol(s))) ||
+ // If the exports has this asset's namespace (e.g. ESM output from CJS input),
+ // include the namespace object for the default export.
+ this.exportedSymbols.has(`$${assetId}$exports`) ||
+ // CommonJS library bundle entries always need a namespace.
+ (this.bundle.env.isLibrary &&
+ this.bundle.env.outputFormat === 'commonjs' &&
+ asset === this.bundle.getMainEntry());
+
+ // If the asset doesn't have static exports, should wrap, the namespace is used,
+ // or we need default interop, then we need to synthesize a namespace object for
+ // this asset.
+ if (
+ asset.meta.staticExports === false ||
+ shouldWrap ||
+ usedNamespace ||
+ defaultInterop
+ ) {
+ // Insert a declaration for the exports namespace object. If the asset is wrapped
+ // we don't need to do this, because we'll use the `module.exports` object provided
+ // by the wrapper instead. This is also true of CommonJS entry assets, which will use
+ // the `module.exports` object provided by CJS.
+ if (
+ !shouldWrap &&
+ (this.bundle.env.outputFormat !== 'commonjs' ||
+ asset !== this.bundle.getMainEntry())
+ ) {
+ prepend += `var $${assetId}$exports = {};\n`;
+ prependLineCount++;
+ }
+
+ // Insert the __esModule interop flag for this module if it has a `default` export
+ // and the namespace symbol is used.
+ // TODO: only if required by CJS?
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'symbol'.
+ if (asset.symbols.hasExportSymbol('default') && usedSymbols.has('*')) {
+ prepend += `\n$parcel$defineInteropFlag($${assetId}$exports);\n`;
+ prependLineCount += 2;
+ this.usedHelpers.add('$parcel$defineInteropFlag');
+ }
+
+ // Find wildcard re-export dependencies, and make sure their exports are also included in
+ // ours. Importantly, add them before the asset's own exports so that wildcard exports get
+ // correctly overwritten by own exports of the same name.
+ for (let dep of deps) {
+ let resolved = this.bundleGraph.getResolvedAsset(dep, this.bundle);
+ if (dep.isOptional || this.bundleGraph.isDependencySkipped(dep)) {
+ continue;
+ }
+
+ let isWrapped = resolved && resolved.meta.shouldWrap;
+
+ for (let [imported, {local}] of dep.symbols) {
+ // @ts-expect-error - TS2367 - This condition will always return 'false' since the types 'symbol' and 'string' have no overlap. | TS2367 - This condition will always return 'false' since the types 'symbol' and 'string' have no overlap.
+ if (imported === '*' && local === '*') {
+ if (!resolved) {
+ // Re-exporting an external module. This should have already been handled in buildReplacements.
+ let external = nullthrows(
+ nullthrows(this.externals.get(dep.specifier)).get('*'),
+ );
+ append += `$parcel$exportWildcard($${assetId}$exports, ${external});\n`;
+ this.usedHelpers.add('$parcel$exportWildcard');
+ continue;
+ }
+
+ // If the resolved asset has an exports object, use the $parcel$exportWildcard helper
+ // to re-export all symbols. Otherwise, if there's no namespace object available, add
+ // $parcel$export calls for each used symbol of the dependency.
+ if (
+ isWrapped ||
+ resolved.meta.staticExports === false ||
+ nullthrows(this.bundleGraph.getUsedSymbols(resolved)).has('*') ||
+ // an empty asset
+ (!resolved.meta.hasCJSExports &&
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'symbol'.
+ resolved.symbols.hasExportSymbol('*'))
+ ) {
+ let obj = this.getSymbolResolution(
+ asset,
+ resolved,
+ '*',
+ dep,
+ replacements,
+ );
+ append += `$parcel$exportWildcard($${assetId}$exports, ${obj});\n`;
+ this.usedHelpers.add('$parcel$exportWildcard');
+ } else {
+ for (let symbol of nullthrows(
+ this.bundleGraph.getUsedSymbols(dep),
+ )) {
+ if (
+ symbol === 'default' || // `export * as ...` does not include the default export
+ symbol === '__esModule'
+ ) {
+ continue;
+ }
+
+ let resolvedSymbol = this.getSymbolResolution(
+ asset,
+ resolved,
+ symbol,
+ undefined,
+ replacements,
+ );
+ let get = this.buildFunctionExpression([], resolvedSymbol);
+ let set = asset.meta.hasCJSExports
+ ? ', ' +
+ this.buildFunctionExpression(['v'], `${resolvedSymbol} = v`)
+ : '';
+ prepend += `$parcel$export($${assetId}$exports, ${JSON.stringify(
+ symbol,
+ )}, ${get}${set});\n`;
+ this.usedHelpers.add('$parcel$export');
+ prependLineCount++;
+ }
+ }
+ }
+ }
+ }
+
+ // Find the used exports of this module. This is based on the used symbols of
+ // incoming dependencies rather than the asset's own used exports so that we include
+ // re-exported symbols rather than only symbols declared in this asset.
+ let incomingDeps = this.bundleGraph.getIncomingDependencies(asset);
+ let usedExports = [...asset.symbols.exportSymbols()].filter((symbol) => {
+ // @ts-expect-error - TS2367 - This condition will always return 'false' since the types 'symbol' and 'string' have no overlap.
+ if (symbol === '*') {
+ return false;
+ }
+
+ // If we need default interop, then all symbols are needed because the `default`
+ // symbol really maps to the whole namespace.
+ if (defaultInterop) {
+ return true;
+ }
+
+ let unused = incomingDeps.every((d) => {
+ let symbols = nullthrows(this.bundleGraph.getUsedSymbols(d));
+ return !symbols.has(symbol) && !symbols.has('*');
+ });
+ return !unused;
+ });
+
+ if (usedExports.length > 0) {
+ // Insert $parcel$export calls for each of the used exports. This creates a getter/setter
+ // for the symbol so that when the value changes the object property also changes. This is
+ // required to simulate ESM live bindings. It's easier to do it this way rather than inserting
+ // additional assignments after each mutation of the original binding.
+ prepend += `\n${usedExports
+ .map((exp) => {
+ let resolved = this.getSymbolResolution(
+ asset,
+ asset,
+ // @ts-expect-error - TS2345 - Argument of type 'symbol' is not assignable to parameter of type 'string'.
+ exp,
+ undefined,
+ replacements,
+ );
+ let get = this.buildFunctionExpression([], resolved);
+ let isEsmExport = !!asset.symbols.get(exp)?.meta?.isEsm;
+ let set =
+ !isEsmExport && asset.meta.hasCJSExports
+ ? ', ' + this.buildFunctionExpression(['v'], `${resolved} = v`)
+ : '';
+ return `$parcel$export($${assetId}$exports, ${JSON.stringify(
+ exp,
+ )}, ${get}${set});`;
+ })
+ .join('\n')}\n`;
+ this.usedHelpers.add('$parcel$export');
+ prependLineCount += 1 + usedExports.length;
+ }
+ }
+
+ return [prepend, prependLineCount, append];
+ }
+
+ buildBundlePrelude(): [string, number] {
+ let enableSourceMaps = this.bundle.env.sourceMap;
+ let res = '';
+ let lines = 0;
+
+ // Add hashbang if the entry asset recorded an interpreter.
+ let mainEntry = this.bundle.getMainEntry();
+ if (
+ mainEntry &&
+ !this.isAsyncBundle &&
+ !this.bundle.target.env.isBrowser()
+ ) {
+ let interpreter = mainEntry.meta.interpreter;
+ invariant(interpreter == null || typeof interpreter === 'string');
+ if (interpreter != null) {
+ res += `#!${interpreter}\n`;
+ lines++;
+ }
+ }
+
+ // The output format may have specific things to add at the start of the bundle (e.g. imports).
+ let [outputFormatPrelude, outputFormatLines] =
+ this.outputFormat.buildBundlePrelude();
+ res += outputFormatPrelude;
+ lines += outputFormatLines;
+
+ // Add used helpers.
+ if (this.needsPrelude) {
+ this.usedHelpers.add('$parcel$global');
+ }
+
+ for (let helper of this.usedHelpers) {
+ // @ts-expect-error - TS7053 - Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ readonly $parcel$export: "\nfunction $parcel$export(e, n, v, s) {\n Object.defineProperty(e, n, {get: v, set: s, enumerable: true, configurable: true});\n}\n"; readonly $parcel$exportWildcard: "\nfunction $parcel$exportWildcard(dest, source) {\n Object.keys(source).forEach(function(key) {\n if (key === 'defau...'.
+ let currentHelper = helpers[helper];
+ if (typeof currentHelper === 'function') {
+ // @ts-expect-error - TS7053 - Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ readonly $parcel$export: "\nfunction $parcel$export(e, n, v, s) {\n Object.defineProperty(e, n, {get: v, set: s, enumerable: true, configurable: true});\n}\n"; readonly $parcel$exportWildcard: "\nfunction $parcel$exportWildcard(dest, source) {\n Object.keys(source).forEach(function(key) {\n if (key === 'defau...'.
+ currentHelper = helpers[helper](this.bundle.env);
+ }
+ res += currentHelper;
+ if (enableSourceMaps) {
+ lines += countLines(currentHelper) - 1;
+ }
+ }
+
+ if (this.needsPrelude) {
+ // Add the prelude if this is potentially the first JS bundle to load in a
+ // particular context (e.g. entry scripts in HTML, workers, etc.).
+ let parentBundles = this.bundleGraph.getParentBundles(this.bundle);
+ let mightBeFirstJS =
+ parentBundles.length === 0 ||
+ parentBundles.some((b) => b.type !== 'js') ||
+ this.bundleGraph
+ .getBundleGroupsContainingBundle(this.bundle)
+ .some((g) => this.bundleGraph.isEntryBundleGroup(g)) ||
+ this.bundle.env.isIsolated() ||
+ this.bundle.bundleBehavior === 'isolated';
+
+ if (mightBeFirstJS) {
+ let preludeCode = prelude(this.parcelRequireName);
+ res += preludeCode;
+ if (enableSourceMaps) {
+ lines += countLines(preludeCode) - 1;
+ }
+
+ if (this.shouldBundleQueue(this.bundle)) {
+ let bundleQueuePreludeCode = bundleQueuePrelude(this.bundle.env);
+ res += bundleQueuePreludeCode;
+ if (enableSourceMaps) {
+ lines += countLines(bundleQueuePreludeCode) - 1;
+ }
+ }
+ } else {
+ // Otherwise, get the current parcelRequire global.
+ const escaped = JSON.stringify(this.parcelRequireName);
+ res += `var parcelRequire = $parcel$global[${escaped}];\n`;
+ lines++;
+ res += `var parcelRegister = parcelRequire.register;\n`;
+ lines++;
+ }
+ }
+
+ // Add importScripts for sibling bundles in workers.
+ if (this.bundle.env.isWorker() || this.bundle.env.isWorklet()) {
+ let importScripts = '';
+ let bundles = this.bundleGraph.getReferencedBundles(this.bundle);
+ for (let b of bundles) {
+ if (this.bundle.env.outputFormat === 'esmodule') {
+ // importScripts() is not allowed in native ES module workers.
+ importScripts += `import "${relativeBundlePath(this.bundle, b)}";\n`;
+ } else {
+ importScripts += `importScripts("${relativeBundlePath(
+ this.bundle,
+ b,
+ )}");\n`;
+ }
+ }
+
+ res += importScripts;
+ lines += bundles.length;
+ }
+
+ return [res, lines];
+ }
+
+ needsDefaultInterop(asset: Asset): boolean {
+ if (
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'symbol'.
+ asset.symbols.hasExportSymbol('*') &&
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'symbol'.
+ !asset.symbols.hasExportSymbol('default')
+ ) {
+ if (getFeatureFlag('fastNeedsDefaultInterop')) {
+ return true;
+ }
+
+ let deps = this.bundleGraph.getIncomingDependencies(asset);
+ return deps.some(
+ (dep) =>
+ this.bundle.hasDependency(dep) &&
+ // dep.meta.isES6Module &&
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'symbol'.
+ dep.symbols.hasExportSymbol('default'),
+ );
+ }
+
+ return false;
+ }
+
+ shouldSkipAsset(asset: Asset): boolean {
+ if (this.isScriptEntry(asset)) {
+ return true;
+ }
+
+ return (
+ asset.sideEffects === false &&
+ nullthrows(this.bundleGraph.getUsedSymbols(asset)).size == 0 &&
+ !this.bundleGraph.isAssetReferenced(this.bundle, asset)
+ );
+ }
+
+ isScriptEntry(asset: Asset): boolean {
+ return (
+ this.bundle.env.outputFormat === 'global' &&
+ this.bundle.env.sourceType === 'script' &&
+ asset === this.bundle.getMainEntry()
+ );
+ }
+
+ buildFunctionExpression(args: Array, expr: string): string {
+ return this.bundle.env.supports('arrow-functions', true)
+ ? `(${args.join(', ')}) => ${expr}`
+ : `function (${args.join(', ')}) { return ${expr}; }`;
+ }
+}
diff --git a/packages/packagers/js/src/helpers.js b/packages/packagers/js/src/helpers.js
deleted file mode 100644
index 1ece9a297..000000000
--- a/packages/packagers/js/src/helpers.js
+++ /dev/null
@@ -1,169 +0,0 @@
-// @flow strict-local
-import type {Environment} from '@atlaspack/types';
-
-export const prelude = (parcelRequireName: string): string => `
-var $parcel$modules = {};
-var $parcel$inits = {};
-
-var parcelRequire = $parcel$global[${JSON.stringify(parcelRequireName)}];
-
-if (parcelRequire == null) {
- parcelRequire = function(id) {
- if (id in $parcel$modules) {
- return $parcel$modules[id].exports;
- }
- if (id in $parcel$inits) {
- var init = $parcel$inits[id];
- delete $parcel$inits[id];
- var module = {id: id, exports: {}};
- $parcel$modules[id] = module;
- init.call(module.exports, module, module.exports);
- return module.exports;
- }
- var err = new Error("Cannot find module '" + id + "'");
- err.code = 'MODULE_NOT_FOUND';
- throw err;
- };
-
- parcelRequire.register = function register(id, init) {
- $parcel$inits[id] = init;
- };
-
- $parcel$global[${JSON.stringify(parcelRequireName)}] = parcelRequire;
-}
-
-var parcelRegister = parcelRequire.register;
-`;
-
-export const fnExpr = (
- env: Environment,
- params: Array,
- body: Array,
-): string => {
- let block = `{ ${body.join(' ')} }`;
-
- if (env.supports('arrow-functions')) {
- return `(${params.join(', ')}) => ${block}`;
- }
-
- return `function (${params.join(', ')}) ${block}`;
-};
-
-export const bundleQueuePrelude = (env: Environment): string => `
-if (!$parcel$global.lb) {
- // Set of loaded bundles
- $parcel$global.lb = new Set();
- // Queue of bundles to execute once they're dep bundles are loaded
- $parcel$global.bq = [];
-
- // Register loaded bundle
- $parcel$global.rlb = ${fnExpr(
- env,
- ['bundle'],
- ['$parcel$global.lb.add(bundle);', '$parcel$global.pq();'],
- )}
-
- // Run when ready
- $parcel$global.rwr = ${fnExpr(
- env,
- // b = bundle public id
- // r = run function to execute the bundle entry
- // d = list of dependent bundles this bundle requires before executing
- ['b', 'r', 'd'],
- ['$parcel$global.bq.push({b, r, d});', '$parcel$global.pq();'],
- )}
-
- // Process queue
- $parcel$global.pq = ${fnExpr(
- env,
- [],
- [
- `var runnableEntry = $parcel$global.bq.find(${fnExpr(
- env,
- ['i'],
- [
- `return i.d.every(${fnExpr(
- env,
- ['dep'],
- ['return $parcel$global.lb.has(dep);'],
- )});`,
- ],
- )});`,
- 'if (runnableEntry) {',
- `$parcel$global.bq = $parcel$global.bq.filter(${fnExpr(
- env,
- ['i'],
- ['return i.b !== runnableEntry.b;'],
- )});`,
- 'runnableEntry.r();',
- '$parcel$global.pq();',
- '}',
- ],
- )}
-}
-`;
-
-const $parcel$export = `
-function $parcel$export(e, n, v, s) {
- Object.defineProperty(e, n, {get: v, set: s, enumerable: true, configurable: true});
-}
-`;
-
-const $parcel$exportWildcard = `
-function $parcel$exportWildcard(dest, source) {
- Object.keys(source).forEach(function(key) {
- if (key === 'default' || key === '__esModule' || Object.prototype.hasOwnProperty.call(dest, key)) {
- return;
- }
-
- Object.defineProperty(dest, key, {
- enumerable: true,
- get: function get() {
- return source[key];
- }
- });
- });
-
- return dest;
-}
-`;
-
-const $parcel$interopDefault = `
-function $parcel$interopDefault(a) {
- return a && a.__esModule ? a.default : a;
-}
-`;
-
-const $parcel$global = (env: Environment): string => {
- if (env.supports('global-this')) {
- return `
- var $parcel$global = globalThis;
- `;
- }
- return `
- var $parcel$global =
- typeof globalThis !== 'undefined'
- ? globalThis
- : typeof self !== 'undefined'
- ? self
- : typeof window !== 'undefined'
- ? window
- : typeof global !== 'undefined'
- ? global
- : {};
- `;
-};
-
-const $parcel$defineInteropFlag = `
-function $parcel$defineInteropFlag(a) {
- Object.defineProperty(a, '__esModule', {value: true, configurable: true});
-}
-`;
-
-export const helpers = {
- $parcel$export,
- $parcel$exportWildcard,
- $parcel$interopDefault,
- $parcel$global,
- $parcel$defineInteropFlag,
-};
diff --git a/packages/packagers/js/src/helpers.ts b/packages/packagers/js/src/helpers.ts
new file mode 100644
index 000000000..b3d1553e9
--- /dev/null
+++ b/packages/packagers/js/src/helpers.ts
@@ -0,0 +1,168 @@
+import type {Environment} from '@atlaspack/types';
+
+export const prelude = (parcelRequireName: string): string => `
+var $parcel$modules = {};
+var $parcel$inits = {};
+
+var parcelRequire = $parcel$global[${JSON.stringify(parcelRequireName)}];
+
+if (parcelRequire == null) {
+ parcelRequire = function(id) {
+ if (id in $parcel$modules) {
+ return $parcel$modules[id].exports;
+ }
+ if (id in $parcel$inits) {
+ var init = $parcel$inits[id];
+ delete $parcel$inits[id];
+ var module = {id: id, exports: {}};
+ $parcel$modules[id] = module;
+ init.call(module.exports, module, module.exports);
+ return module.exports;
+ }
+ var err = new Error("Cannot find module '" + id + "'");
+ err.code = 'MODULE_NOT_FOUND';
+ throw err;
+ };
+
+ parcelRequire.register = function register(id, init) {
+ $parcel$inits[id] = init;
+ };
+
+ $parcel$global[${JSON.stringify(parcelRequireName)}] = parcelRequire;
+}
+
+var parcelRegister = parcelRequire.register;
+`;
+
+export const fnExpr = (
+ env: Environment,
+ params: Array,
+ body: Array,
+): string => {
+ let block = `{ ${body.join(' ')} }`;
+
+ if (env.supports('arrow-functions')) {
+ return `(${params.join(', ')}) => ${block}`;
+ }
+
+ return `function (${params.join(', ')}) ${block}`;
+};
+
+export const bundleQueuePrelude = (env: Environment): string => `
+if (!$parcel$global.lb) {
+ // Set of loaded bundles
+ $parcel$global.lb = new Set();
+ // Queue of bundles to execute once they're dep bundles are loaded
+ $parcel$global.bq = [];
+
+ // Register loaded bundle
+ $parcel$global.rlb = ${fnExpr(
+ env,
+ ['bundle'],
+ ['$parcel$global.lb.add(bundle);', '$parcel$global.pq();'],
+ )}
+
+ // Run when ready
+ $parcel$global.rwr = ${fnExpr(
+ env,
+ // b = bundle public id
+ // r = run function to execute the bundle entry
+ // d = list of dependent bundles this bundle requires before executing
+ ['b', 'r', 'd'],
+ ['$parcel$global.bq.push({b, r, d});', '$parcel$global.pq();'],
+ )}
+
+ // Process queue
+ $parcel$global.pq = ${fnExpr(
+ env,
+ [],
+ [
+ `var runnableEntry = $parcel$global.bq.find(${fnExpr(
+ env,
+ ['i'],
+ [
+ `return i.d.every(${fnExpr(
+ env,
+ ['dep'],
+ ['return $parcel$global.lb.has(dep);'],
+ )});`,
+ ],
+ )});`,
+ 'if (runnableEntry) {',
+ `$parcel$global.bq = $parcel$global.bq.filter(${fnExpr(
+ env,
+ ['i'],
+ ['return i.b !== runnableEntry.b;'],
+ )});`,
+ 'runnableEntry.r();',
+ '$parcel$global.pq();',
+ '}',
+ ],
+ )}
+}
+`;
+
+const $parcel$export = `
+function $parcel$export(e, n, v, s) {
+ Object.defineProperty(e, n, {get: v, set: s, enumerable: true, configurable: true});
+}
+`;
+
+const $parcel$exportWildcard = `
+function $parcel$exportWildcard(dest, source) {
+ Object.keys(source).forEach(function(key) {
+ if (key === 'default' || key === '__esModule' || Object.prototype.hasOwnProperty.call(dest, key)) {
+ return;
+ }
+
+ Object.defineProperty(dest, key, {
+ enumerable: true,
+ get: function get() {
+ return source[key];
+ }
+ });
+ });
+
+ return dest;
+}
+`;
+
+const $parcel$interopDefault = `
+function $parcel$interopDefault(a) {
+ return a && a.__esModule ? a.default : a;
+}
+`;
+
+const $parcel$global = (env: Environment): string => {
+ if (env.supports('global-this')) {
+ return `
+ var $parcel$global = globalThis;
+ `;
+ }
+ return `
+ var $parcel$global =
+ typeof globalThis !== 'undefined'
+ ? globalThis
+ : typeof self !== 'undefined'
+ ? self
+ : typeof window !== 'undefined'
+ ? window
+ : typeof global !== 'undefined'
+ ? global
+ : {};
+ `;
+};
+
+const $parcel$defineInteropFlag = `
+function $parcel$defineInteropFlag(a) {
+ Object.defineProperty(a, '__esModule', {value: true, configurable: true});
+}
+`;
+
+export const helpers = {
+ $parcel$export,
+ $parcel$exportWildcard,
+ $parcel$interopDefault,
+ $parcel$global,
+ $parcel$defineInteropFlag,
+} as const;
diff --git a/packages/packagers/js/src/index.js b/packages/packagers/js/src/index.js
deleted file mode 100644
index 08304cc38..000000000
--- a/packages/packagers/js/src/index.js
+++ /dev/null
@@ -1,162 +0,0 @@
-// @flow strict-local
-import type {Async} from '@atlaspack/types';
-import type SourceMap from '@parcel/source-map';
-import {Packager} from '@atlaspack/plugin';
-import {
- replaceInlineReferences,
- replaceURLReferences,
- validateSchema,
- type SchemaEntity,
-} from '@atlaspack/utils';
-import {encodeJSONKeyComponent} from '@atlaspack/diagnostic';
-import {hashString} from '@atlaspack/rust';
-import nullthrows from 'nullthrows';
-import {DevPackager} from './DevPackager';
-import {ScopeHoistingPackager} from './ScopeHoistingPackager';
-
-type JSPackagerConfig = {|
- parcelRequireName: string,
- unstable_asyncBundleRuntime: boolean,
- unstable_forceSkipWrapAssets: Array,
-|};
-
-const CONFIG_SCHEMA: SchemaEntity = {
- type: 'object',
- properties: {
- unstable_asyncBundleRuntime: {
- type: 'boolean',
- },
- unstable_forceSkipWrapAssets: {
- type: 'array',
- items: {
- type: 'string',
- },
- },
- },
- additionalProperties: false,
-};
-
-export default (new Packager({
- async loadConfig({config, options}): Promise {
- let packageKey = '@atlaspack/packager-js';
- let conf = await config.getConfigFrom(options.projectRoot + '/index', [], {
- packageKey,
- });
-
- if (conf?.contents) {
- validateSchema.diagnostic(
- CONFIG_SCHEMA,
- {
- data: conf?.contents,
- source: await options.inputFS.readFile(conf.filePath, 'utf8'),
- filePath: conf.filePath,
- prependKey: `/${encodeJSONKeyComponent(packageKey)}`,
- },
- packageKey,
- `Invalid config for ${packageKey}`,
- );
- }
-
- // Generate a name for the global parcelRequire function that is unique to this project.
- // This allows multiple parcel builds to coexist on the same page.
- let packageName = await config.getConfigFrom(
- options.projectRoot + '/index',
- [],
- {
- packageKey: 'name',
- },
- );
-
- let name = packageName?.contents?.name ?? '';
- return {
- parcelRequireName: 'parcelRequire' + hashString(name).slice(-4),
- unstable_asyncBundleRuntime: Boolean(
- conf?.contents?.unstable_asyncBundleRuntime,
- ),
- unstable_forceSkipWrapAssets:
- conf?.contents?.unstable_forceSkipWrapAssets ?? [],
- };
- },
- async package({
- bundle,
- bundleGraph,
- getInlineBundleContents,
- getSourceMapReference,
- config,
- options,
- logger,
- }) {
- // If this is a non-module script, and there is only one asset with no dependencies,
- // then we don't need to package at all and can pass through the original code un-wrapped.
- let contents, map;
- if (bundle.env.sourceType === 'script') {
- let entries = bundle.getEntryAssets();
- if (
- entries.length === 1 &&
- bundleGraph.getDependencies(entries[0]).length === 0
- ) {
- contents = await entries[0].getCode();
- map = await entries[0].getMap();
- }
- }
-
- if (contents == null) {
- let packager = bundle.env.shouldScopeHoist
- ? new ScopeHoistingPackager(
- options,
- bundleGraph,
- bundle,
- nullthrows(config).parcelRequireName,
- nullthrows(config).unstable_asyncBundleRuntime,
- nullthrows(config).unstable_forceSkipWrapAssets,
- logger,
- )
- : new DevPackager(
- options,
- bundleGraph,
- bundle,
- nullthrows(config).parcelRequireName,
- );
-
- ({contents, map} = await packager.package());
- }
-
- contents += '\n' + (await getSourceMapSuffix(getSourceMapReference, map));
-
- // For library builds, we need to replace URL references with their final resolved paths.
- // For non-library builds, this is handled in the JS runtime.
- if (bundle.env.isLibrary) {
- ({contents, map} = replaceURLReferences({
- bundle,
- bundleGraph,
- contents,
- map,
- getReplacement: s => JSON.stringify(s).slice(1, -1),
- }));
- }
-
- return replaceInlineReferences({
- bundle,
- bundleGraph,
- contents,
- getInlineReplacement: (dependency, inlineType, content) => ({
- from: `"${dependency.id}"`,
- to: inlineType === 'string' ? JSON.stringify(content) : content,
- }),
- getInlineBundleContents,
- map,
- });
- },
-}): Packager);
-
-async function getSourceMapSuffix(
- getSourceMapReference: (?SourceMap) => Async,
- map: ?SourceMap,
-): Promise {
- let sourcemapReference = await getSourceMapReference(map);
- if (sourcemapReference != null) {
- return '//# sourceMappingURL=' + sourcemapReference + '\n';
- } else {
- return '';
- }
-}
diff --git a/packages/packagers/js/src/index.ts b/packages/packagers/js/src/index.ts
new file mode 100644
index 000000000..99d22bc41
--- /dev/null
+++ b/packages/packagers/js/src/index.ts
@@ -0,0 +1,170 @@
+import type {Async} from '@atlaspack/types';
+import type SourceMap from '@parcel/source-map';
+import {Packager} from '@atlaspack/plugin';
+import {
+ replaceInlineReferences,
+ replaceURLReferences,
+ validateSchema,
+ SchemaEntity,
+} from '@atlaspack/utils';
+import {encodeJSONKeyComponent} from '@atlaspack/diagnostic';
+import {hashString} from '@atlaspack/rust';
+import nullthrows from 'nullthrows';
+import {DevPackager} from './DevPackager';
+import {ScopeHoistingPackager} from './ScopeHoistingPackager';
+
+type JSPackagerConfig = {
+ parcelRequireName: string;
+ unstable_asyncBundleRuntime: boolean;
+ unstable_forceSkipWrapAssets: Array;
+};
+
+const CONFIG_SCHEMA: SchemaEntity = {
+ type: 'object',
+ properties: {
+ unstable_asyncBundleRuntime: {
+ type: 'boolean',
+ },
+ unstable_forceSkipWrapAssets: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
+ },
+ additionalProperties: false,
+};
+
+export default new Packager({
+ async loadConfig({config, options}): Promise {
+ let packageKey = '@atlaspack/packager-js';
+ let conf = await config.getConfigFrom(options.projectRoot + '/index', [], {
+ packageKey,
+ });
+
+ if (conf?.contents) {
+ validateSchema.diagnostic(
+ CONFIG_SCHEMA,
+ {
+ data: conf?.contents,
+ source: await options.inputFS.readFile(conf.filePath, 'utf8'),
+ filePath: conf.filePath,
+ prependKey: `/${encodeJSONKeyComponent(packageKey)}`,
+ },
+ packageKey,
+ `Invalid config for ${packageKey}`,
+ );
+ }
+
+ // Generate a name for the global parcelRequire function that is unique to this project.
+ // This allows multiple parcel builds to coexist on the same page.
+ let packageName = await config.getConfigFrom(
+ options.projectRoot + '/index',
+ [],
+ {
+ packageKey: 'name',
+ },
+ );
+
+ // @ts-expect-error - TS2571 - Object is of type 'unknown'.
+ let name = packageName?.contents?.name ?? '';
+ return {
+ parcelRequireName: 'parcelRequire' + hashString(name).slice(-4),
+ unstable_asyncBundleRuntime: Boolean(
+ // @ts-expect-error - TS2571 - Object is of type 'unknown'.
+ conf?.contents?.unstable_asyncBundleRuntime,
+ ),
+ unstable_forceSkipWrapAssets:
+ // @ts-expect-error - TS2571 - Object is of type 'unknown'.
+ conf?.contents?.unstable_forceSkipWrapAssets ?? [],
+ };
+ },
+ async package({
+ bundle,
+ bundleGraph,
+ getInlineBundleContents,
+ getSourceMapReference,
+ config,
+ options,
+ logger,
+ }) {
+ // If this is a non-module script, and there is only one asset with no dependencies,
+ // then we don't need to package at all and can pass through the original code un-wrapped.
+ let contents, map;
+ if (bundle.env.sourceType === 'script') {
+ let entries = bundle.getEntryAssets();
+ if (
+ entries.length === 1 &&
+ bundleGraph.getDependencies(entries[0]).length === 0
+ ) {
+ contents = await entries[0].getCode();
+ map = await entries[0].getMap();
+ }
+ }
+
+ if (contents == null) {
+ let packager = bundle.env.shouldScopeHoist
+ ? new ScopeHoistingPackager(
+ options,
+ bundleGraph,
+ bundle,
+ // @ts-expect-error - TS2571 - Object is of type 'unknown'.
+ nullthrows(config).parcelRequireName,
+ // @ts-expect-error - TS2571 - Object is of type 'unknown'.
+ nullthrows(config).unstable_asyncBundleRuntime,
+ // @ts-expect-error - TS2571 - Object is of type 'unknown'.
+ nullthrows(config).unstable_forceSkipWrapAssets,
+ logger,
+ )
+ : new DevPackager(
+ options,
+ bundleGraph,
+ bundle,
+ // @ts-expect-error - TS2571 - Object is of type 'unknown'.
+ nullthrows(config).parcelRequireName,
+ );
+
+ ({contents, map} = await packager.package());
+ }
+
+ contents += '\n' + (await getSourceMapSuffix(getSourceMapReference, map));
+
+ // For library builds, we need to replace URL references with their final resolved paths.
+ // For non-library builds, this is handled in the JS runtime.
+ if (bundle.env.isLibrary) {
+ ({contents, map} = replaceURLReferences({
+ bundle,
+ bundleGraph,
+ contents,
+ map,
+ getReplacement: (s) => JSON.stringify(s).slice(1, -1),
+ }));
+ }
+
+ return replaceInlineReferences({
+ bundle,
+ bundleGraph,
+ contents,
+ getInlineReplacement: (dependency, inlineType, content) => ({
+ from: `"${dependency.id}"`,
+ to: inlineType === 'string' ? JSON.stringify(content) : content,
+ }),
+ getInlineBundleContents,
+ map,
+ });
+ },
+}) as Packager;
+
+async function getSourceMapSuffix(
+ getSourceMapReference: (
+ arg1?: SourceMap | null | undefined,
+ ) => Async,
+ map?: SourceMap | null,
+): Promise {
+ let sourcemapReference = await getSourceMapReference(map);
+ if (sourcemapReference != null) {
+ return '//# sourceMappingURL=' + sourcemapReference + '\n';
+ } else {
+ return '';
+ }
+}
diff --git a/packages/packagers/js/src/utils.js b/packages/packagers/js/src/utils.js
deleted file mode 100644
index c731bb1cf..000000000
--- a/packages/packagers/js/src/utils.js
+++ /dev/null
@@ -1,74 +0,0 @@
-// @flow
-import type {BundleGraph, Dependency, NamedBundle} from '@atlaspack/types';
-import type SourceMap from '@parcel/source-map';
-import nullthrows from 'nullthrows';
-
-// This replaces __parcel__require__ references left by the transformer with
-// parcelRequire calls of the resolved asset id. This lets runtimes work within
-// script bundles, which must be outside the bundle wrapper so their variables are global.
-export function replaceScriptDependencies(
- bundleGraph: BundleGraph,
- bundle: NamedBundle,
- code: string,
- map: ?SourceMap,
- parcelRequireName: string,
-): string {
- let entry = nullthrows(bundle.getMainEntry());
- let dependencies = bundleGraph.getDependencies(entry);
-
- let lineCount = 0;
- let offset = 0;
- let columnStartIndex = 0;
- code = code.replace(/\n|__parcel__require__\(['"](.*?)['"]\)/g, (m, s, i) => {
- if (m === '\n') {
- columnStartIndex = i + offset + 1;
- lineCount++;
- return '\n';
- }
-
- let dep = nullthrows(dependencies.find(d => getSpecifier(d) === s));
- let resolved = nullthrows(bundleGraph.getResolvedAsset(dep, bundle));
- let publicId = bundleGraph.getAssetPublicId(resolved);
- let replacement = `${parcelRequireName}("${publicId}")`;
- if (map) {
- let lengthDifference = replacement.length - m.length;
- if (lengthDifference !== 0) {
- map.offsetColumns(
- lineCount + 1,
- i + offset - columnStartIndex + m.length,
- lengthDifference,
- );
- offset += lengthDifference;
- }
- }
-
- return replacement;
- });
-
- return code;
-}
-
-export function getSpecifier(dep: Dependency): string {
- if (typeof dep.meta.placeholder === 'string') {
- return dep.meta.placeholder;
- }
-
- return dep.specifier;
-}
-
-// https://262.ecma-international.org/6.0/#sec-names-and-keywords
-const IDENTIFIER_RE = /^[$_\p{ID_Start}][$_\u200C\u200D\p{ID_Continue}]*$/u;
-const ID_START_RE = /^[$_\p{ID_Start}]/u;
-const NON_ID_CONTINUE_RE = /[^$_\u200C\u200D\p{ID_Continue}]/gu;
-
-export function isValidIdentifier(id: string): boolean {
- return IDENTIFIER_RE.test(id);
-}
-
-export function makeValidIdentifier(name: string): string {
- name = name.replace(NON_ID_CONTINUE_RE, '');
- if (!ID_START_RE.test(name)) {
- name = '_' + name;
- }
- return name;
-}
diff --git a/packages/packagers/js/src/utils.ts b/packages/packagers/js/src/utils.ts
new file mode 100644
index 000000000..c7b432d49
--- /dev/null
+++ b/packages/packagers/js/src/utils.ts
@@ -0,0 +1,73 @@
+import type {BundleGraph, Dependency, NamedBundle} from '@atlaspack/types';
+import type SourceMap from '@parcel/source-map';
+import nullthrows from 'nullthrows';
+
+// This replaces __parcel__require__ references left by the transformer with
+// parcelRequire calls of the resolved asset id. This lets runtimes work within
+// script bundles, which must be outside the bundle wrapper so their variables are global.
+export function replaceScriptDependencies(
+ bundleGraph: BundleGraph,
+ bundle: NamedBundle,
+ code: string,
+ map: SourceMap | null | undefined,
+ parcelRequireName: string,
+): string {
+ let entry = nullthrows(bundle.getMainEntry());
+ let dependencies = bundleGraph.getDependencies(entry);
+
+ let lineCount = 0;
+ let offset = 0;
+ let columnStartIndex = 0;
+ code = code.replace(/\n|__parcel__require__\(['"](.*?)['"]\)/g, (m, s, i) => {
+ if (m === '\n') {
+ columnStartIndex = i + offset + 1;
+ lineCount++;
+ return '\n';
+ }
+
+ let dep = nullthrows(dependencies.find((d) => getSpecifier(d) === s));
+ let resolved = nullthrows(bundleGraph.getResolvedAsset(dep, bundle));
+ let publicId = bundleGraph.getAssetPublicId(resolved);
+ let replacement = `${parcelRequireName}("${publicId}")`;
+ if (map) {
+ let lengthDifference = replacement.length - m.length;
+ if (lengthDifference !== 0) {
+ map.offsetColumns(
+ lineCount + 1,
+ i + offset - columnStartIndex + m.length,
+ lengthDifference,
+ );
+ offset += lengthDifference;
+ }
+ }
+
+ return replacement;
+ });
+
+ return code;
+}
+
+export function getSpecifier(dep: Dependency): string {
+ if (typeof dep.meta.placeholder === 'string') {
+ return dep.meta.placeholder;
+ }
+
+ return dep.specifier;
+}
+
+// https://262.ecma-international.org/6.0/#sec-names-and-keywords
+const IDENTIFIER_RE = /^[$_\p{ID_Start}][$_\u200C\u200D\p{ID_Continue}]*$/u;
+const ID_START_RE = /^[$_\p{ID_Start}]/u;
+const NON_ID_CONTINUE_RE = /[^$_\u200C\u200D\p{ID_Continue}]/gu;
+
+export function isValidIdentifier(id: string): boolean {
+ return IDENTIFIER_RE.test(id);
+}
+
+export function makeValidIdentifier(name: string): string {
+ name = name.replace(NON_ID_CONTINUE_RE, '');
+ if (!ID_START_RE.test(name)) {
+ name = '_' + name;
+ }
+ return name;
+}
diff --git a/packages/packagers/raw-url/package.json b/packages/packagers/raw-url/package.json
index 3a1e23736..15acbae60 100644
--- a/packages/packagers/raw-url/package.json
+++ b/packages/packagers/raw-url/package.json
@@ -10,7 +10,8 @@
"url": "https://github.com/atlassian-labs/atlaspack.git"
},
"main": "lib/RawUrlPackager.js",
- "source": "src/RawUrlPackager.js",
+ "types": "src/RawUrlPackager.ts",
+ "source": "src/RawUrlPackager.ts",
"engines": {
"node": ">= 16.0.0",
"parcel": "^2.12.0"
diff --git a/packages/packagers/raw-url/src/RawUrlPackager.js b/packages/packagers/raw-url/src/RawUrlPackager.js
deleted file mode 100644
index cfd954816..000000000
--- a/packages/packagers/raw-url/src/RawUrlPackager.js
+++ /dev/null
@@ -1,24 +0,0 @@
-// @flow strict-local
-
-import assert from 'assert';
-import {Packager} from '@atlaspack/plugin';
-import {replaceURLReferences} from '@atlaspack/utils';
-
-export default (new Packager({
- async package({bundle, bundleGraph}) {
- let assets = [];
- bundle.traverseAssets(asset => {
- assets.push(asset);
- });
-
- assert.equal(assets.length, 1, 'Raw bundles must only contain one asset');
- let {contents} = replaceURLReferences({
- bundle,
- bundleGraph,
- contents: await assets[0].getCode(),
- relative: false,
- getReplacement: s => s,
- });
- return {contents};
- },
-}): Packager);
diff --git a/packages/packagers/raw-url/src/RawUrlPackager.ts b/packages/packagers/raw-url/src/RawUrlPackager.ts
new file mode 100644
index 000000000..ffaa2d57e
--- /dev/null
+++ b/packages/packagers/raw-url/src/RawUrlPackager.ts
@@ -0,0 +1,22 @@
+import assert from 'assert';
+import {Packager} from '@atlaspack/plugin';
+import {replaceURLReferences} from '@atlaspack/utils';
+
+export default new Packager({
+ async package({bundle, bundleGraph}) {
+ let assets: Array = [];
+ bundle.traverseAssets((asset) => {
+ assets.push(asset);
+ });
+
+ assert.equal(assets.length, 1, 'Raw bundles must only contain one asset');
+ let {contents} = replaceURLReferences({
+ bundle,
+ bundleGraph,
+ contents: await assets[0].getCode(),
+ relative: false,
+ getReplacement: (s) => s,
+ });
+ return {contents};
+ },
+}) as Packager;
diff --git a/packages/packagers/raw/package.json b/packages/packagers/raw/package.json
index 72c3009ce..f6332c121 100644
--- a/packages/packagers/raw/package.json
+++ b/packages/packagers/raw/package.json
@@ -10,7 +10,8 @@
"url": "https://github.com/atlassian-labs/atlaspack.git"
},
"main": "lib/RawPackager.js",
- "source": "src/RawPackager.js",
+ "types": "src/RawPackager.ts",
+ "source": "src/RawPackager.ts",
"engines": {
"node": ">= 16.0.0",
"parcel": "^2.12.0"
diff --git a/packages/packagers/raw/src/RawPackager.js b/packages/packagers/raw/src/RawPackager.js
deleted file mode 100644
index 60d03f966..000000000
--- a/packages/packagers/raw/src/RawPackager.js
+++ /dev/null
@@ -1,16 +0,0 @@
-// @flow strict-local
-
-import assert from 'assert';
-import {Packager} from '@atlaspack/plugin';
-
-export default (new Packager({
- package({bundle}) {
- let assets = [];
- bundle.traverseAssets(asset => {
- assets.push(asset);
- });
-
- assert.equal(assets.length, 1, 'Raw bundles must only contain one asset');
- return {contents: assets[0].getStream()};
- },
-}): Packager);
diff --git a/packages/packagers/raw/src/RawPackager.ts b/packages/packagers/raw/src/RawPackager.ts
new file mode 100644
index 000000000..f8f11a463
--- /dev/null
+++ b/packages/packagers/raw/src/RawPackager.ts
@@ -0,0 +1,14 @@
+import assert from 'assert';
+import {Packager} from '@atlaspack/plugin';
+
+export default new Packager({
+ package({bundle}) {
+ let assets: Array = [];
+ bundle.traverseAssets((asset) => {
+ assets.push(asset);
+ });
+
+ assert.equal(assets.length, 1, 'Raw bundles must only contain one asset');
+ return {contents: assets[0].getStream()};
+ },
+}) as Packager;
diff --git a/packages/packagers/svg/package.json b/packages/packagers/svg/package.json
index d1c3fb1fd..9989fad7d 100644
--- a/packages/packagers/svg/package.json
+++ b/packages/packagers/svg/package.json
@@ -10,7 +10,8 @@
"url": "https://github.com/atlassian-labs/atlaspack.git"
},
"main": "lib/SVGPackager.js",
- "source": "src/SVGPackager.js",
+ "types": "src/SVGPackager.ts",
+ "source": "src/SVGPackager.ts",
"engines": {
"node": ">= 16.0.0",
"parcel": "^2.12.0"
diff --git a/packages/packagers/svg/src/SVGPackager.js b/packages/packagers/svg/src/SVGPackager.js
deleted file mode 100644
index 14f67537f..000000000
--- a/packages/packagers/svg/src/SVGPackager.js
+++ /dev/null
@@ -1,169 +0,0 @@
-// @flow
-
-import type {Bundle, BundleGraph, NamedBundle} from '@atlaspack/types';
-import assert from 'assert';
-import {Packager} from '@atlaspack/plugin';
-import posthtml from 'posthtml';
-import {
- blobToString,
- replaceInlineReferences,
- replaceURLReferences,
- urlJoin,
- setDifference,
-} from '@atlaspack/utils';
-
-export default (new Packager({
- async package({bundle, bundleGraph, getInlineBundleContents}) {
- const assets = [];
- bundle.traverseAssets(asset => {
- assets.push(asset);
- });
-
- assert.strictEqual(
- assets.length,
- 1,
- 'SVG bundles must only contain one asset',
- );
-
- // Add bundles in the same bundle group that are not inline. For example, if two inline
- // bundles refer to the same library that is extracted into a shared bundle.
- let referencedBundles = [
- ...setDifference(
- new Set(bundleGraph.getReferencedBundles(bundle)),
- new Set(bundleGraph.getReferencedBundles(bundle, {recursive: false})),
- ),
- ];
-
- const asset = assets[0];
- const code = await asset.getCode();
- const options = {
- directives: [
- {
- name: /^\?/,
- start: '<',
- end: '>',
- },
- ],
- xmlMode: true,
- };
-
- let {html: svg} = await posthtml([
- tree => insertBundleReferences(referencedBundles, tree),
- tree =>
- replaceInlineAssetContent(bundleGraph, getInlineBundleContents, tree),
- ]).process(code, options);
-
- const {contents, map} = replaceURLReferences({
- bundle,
- bundleGraph,
- contents: svg,
- relative: false,
- getReplacement: contents => contents.replace(/"/g, '"'),
- });
-
- return replaceInlineReferences({
- bundle,
- bundleGraph,
- contents,
- getInlineBundleContents,
- getInlineReplacement: (dep, inlineType, contents) => ({
- from: dep.id,
- to: contents.replace(/"/g, '"').trim(),
- }),
- map,
- });
- },
-}): Packager);
-
-async function replaceInlineAssetContent(
- bundleGraph: BundleGraph,
- getInlineBundleContents,
- tree,
-) {
- const inlineNodes = [];
- tree.walk(node => {
- if (node.attrs && node.attrs['data-parcel-key']) {
- inlineNodes.push(node);
- }
- return node;
- });
-
- for (const node of inlineNodes) {
- const newContent = await getAssetContent(
- bundleGraph,
- getInlineBundleContents,
- node.attrs['data-parcel-key'],
- );
-
- if (newContent === null) {
- continue;
- }
-
- node.content = await blobToString(newContent.contents);
-
- // Wrap scripts and styles with CDATA if needed to ensure characters are not interpreted as XML
- if (node.tag === 'script' || node.tag === 'style') {
- if (node.content.includes('<') || node.content.includes('&')) {
- node.content = node.content.replace(/]]>/g, ']\\]>');
- node.content = ``;
- }
- }
-
- // remove attr from output
- delete node.attrs['data-parcel-key'];
- }
-
- return tree;
-}
-
-async function getAssetContent(
- bundleGraph: BundleGraph,
- getInlineBundleContents,
- assetId,
-) {
- let inlineBundle: ?Bundle;
- bundleGraph.traverseBundles((bundle, context, {stop}) => {
- const entryAssets = bundle.getEntryAssets();
- if (entryAssets.some(a => a.uniqueKey === assetId)) {
- inlineBundle = bundle;
- stop();
- }
- });
-
- if (!inlineBundle) {
- return null;
- }
-
- const bundleResult = await getInlineBundleContents(inlineBundle, bundleGraph);
-
- return {bundle: inlineBundle, contents: bundleResult.contents};
-}
-
-function insertBundleReferences(siblingBundles, tree) {
- let scripts = [];
- let stylesheets = [];
-
- for (let bundle of siblingBundles) {
- if (bundle.type === 'css') {
- stylesheets.push(
- ``,
- );
- } else if (bundle.type === 'js') {
- scripts.push({
- tag: 'script',
- attrs: {
- href: urlJoin(bundle.target.publicUrl, bundle.name),
- },
- });
- }
- }
-
- tree.unshift(...stylesheets);
- if (scripts.length > 0) {
- tree.match({tag: 'svg'}, node => {
- node.content.unshift(...scripts);
- });
- }
-}
diff --git a/packages/packagers/svg/src/SVGPackager.ts b/packages/packagers/svg/src/SVGPackager.ts
new file mode 100644
index 000000000..eda19780d
--- /dev/null
+++ b/packages/packagers/svg/src/SVGPackager.ts
@@ -0,0 +1,189 @@
+import type {Bundle, BundleGraph, NamedBundle} from '@atlaspack/types';
+import assert from 'assert';
+import {Packager} from '@atlaspack/plugin';
+import posthtml from 'posthtml';
+import {
+ blobToString,
+ replaceInlineReferences,
+ replaceURLReferences,
+ urlJoin,
+ setDifference,
+} from '@atlaspack/utils';
+
+export default new Packager({
+ async package({bundle, bundleGraph, getInlineBundleContents}) {
+ const assets: Array = [];
+ bundle.traverseAssets((asset) => {
+ assets.push(asset);
+ });
+
+ assert.strictEqual(
+ assets.length,
+ 1,
+ 'SVG bundles must only contain one asset',
+ );
+
+ // Add bundles in the same bundle group that are not inline. For example, if two inline
+ // bundles refer to the same library that is extracted into a shared bundle.
+ let referencedBundles = [
+ ...setDifference(
+ new Set(bundleGraph.getReferencedBundles(bundle)),
+ new Set(bundleGraph.getReferencedBundles(bundle, {recursive: false})),
+ ),
+ ];
+
+ const asset = assets[0];
+ const code = await asset.getCode();
+ const options = {
+ directives: [
+ {
+ name: /^\?/,
+ start: '<',
+ end: '>',
+ },
+ ],
+ xmlMode: true,
+ } as const;
+
+ let {html: svg} = await posthtml([
+ // @ts-expect-error - TS2345 - Argument of type 'unknown[]' is not assignable to parameter of type 'NamedBundle[]'.
+ (tree: any) => insertBundleReferences(referencedBundles, tree),
+ (tree: any) =>
+ replaceInlineAssetContent(bundleGraph, getInlineBundleContents, tree),
+ // @ts-expect-error - TS2559 - Type '{ readonly directives: readonly [{ readonly name: RegExp; readonly start: "<"; readonly end: ">"; }]; readonly xmlMode: true; }' has no properties in common with type 'Options'.
+ ]).process(code, options);
+
+ const {contents, map} = replaceURLReferences({
+ bundle,
+ bundleGraph,
+ contents: svg,
+ relative: false,
+ getReplacement: (contents) => contents.replace(/"/g, '"'),
+ });
+
+ return replaceInlineReferences({
+ bundle,
+ bundleGraph,
+ contents,
+ getInlineBundleContents,
+ getInlineReplacement: (dep, inlineType, contents) => ({
+ from: dep.id,
+ to: contents.replace(/"/g, '"').trim(),
+ }),
+ map,
+ });
+ },
+}) as Packager;
+
+async function replaceInlineAssetContent(
+ bundleGraph: BundleGraph,
+ getInlineBundleContents: (
+ arg1: Bundle,
+ arg2: BundleGraph,
+ ) => Async<{
+ contents: Blob;
+ }>,
+ tree: any,
+) {
+ const inlineNodes: Array = [];
+ // @ts-expect-error - TS7006 - Parameter 'node' implicitly has an 'any' type.
+ tree.walk((node) => {
+ if (node.attrs && node.attrs['data-parcel-key']) {
+ inlineNodes.push(node);
+ }
+ return node;
+ });
+
+ for (const node of inlineNodes) {
+ const newContent = await getAssetContent(
+ bundleGraph,
+ getInlineBundleContents,
+ node.attrs['data-parcel-key'],
+ );
+
+ if (newContent === null) {
+ continue;
+ }
+
+ node.content = await blobToString(newContent.contents);
+
+ // Wrap scripts and styles with CDATA if needed to ensure characters are not interpreted as XML
+ if (node.tag === 'script' || node.tag === 'style') {
+ if (node.content.includes('<') || node.content.includes('&')) {
+ node.content = node.content.replace(/]]>/g, ']\\]>');
+ node.content = ``;
+ }
+ }
+
+ // remove attr from output
+ delete node.attrs['data-parcel-key'];
+ }
+
+ return tree;
+}
+
+async function getAssetContent(
+ bundleGraph: BundleGraph,
+ getInlineBundleContents: (
+ arg1: Bundle,
+ arg2: BundleGraph,
+ ) => Async<{
+ contents: Blob;
+ }>,
+ assetId: any,
+) {
+ let inlineBundle: Bundle | null | undefined;
+ bundleGraph.traverseBundles((bundle, context, {stop}) => {
+ const entryAssets = bundle.getEntryAssets();
+ if (entryAssets.some((a) => a.uniqueKey === assetId)) {
+ inlineBundle = bundle;
+ stop();
+ }
+ });
+
+ if (!inlineBundle) {
+ return null;
+ }
+
+ const bundleResult = await getInlineBundleContents(inlineBundle, bundleGraph);
+
+ return {bundle: inlineBundle, contents: bundleResult.contents};
+}
+
+function insertBundleReferences(siblingBundles: Array, tree: any) {
+ let scripts: Array<
+ | any
+ | {
+ attrs: {
+ href: string;
+ };
+ tag: string;
+ }
+ > = [];
+ let stylesheets: Array = [];
+
+ for (let bundle of siblingBundles) {
+ if (bundle.type === 'css') {
+ stylesheets.push(
+ ``,
+ );
+ } else if (bundle.type === 'js') {
+ scripts.push({
+ tag: 'script',
+ attrs: {
+ href: urlJoin(bundle.target.publicUrl, bundle.name),
+ },
+ });
+ }
+ }
+
+ tree.unshift(...stylesheets);
+ if (scripts.length > 0) {
+ // @ts-expect-error - TS7006 - Parameter 'node' implicitly has an 'any' type.
+ tree.match({tag: 'svg'}, (node) => {
+ node.content.unshift(...scripts);
+ });
+ }
+}
diff --git a/packages/packagers/ts/package.json b/packages/packagers/ts/package.json
index e530d653f..be80c5556 100644
--- a/packages/packagers/ts/package.json
+++ b/packages/packagers/ts/package.json
@@ -10,7 +10,8 @@
"url": "https://github.com/atlassian-labs/atlaspack.git"
},
"main": "lib/TSPackager.js",
- "source": "src/TSPackager.js",
+ "types": "src/TSPackager.ts",
+ "source": "src/TSPackager.ts",
"engines": {
"node": ">= 16.0.0",
"parcel": "^2.12.0"
diff --git a/packages/packagers/ts/src/TSPackager.js b/packages/packagers/ts/src/TSPackager.js
deleted file mode 100644
index 34cf05364..000000000
--- a/packages/packagers/ts/src/TSPackager.js
+++ /dev/null
@@ -1,25 +0,0 @@
-// @flow strict-local
-
-import assert from 'assert';
-import {Packager} from '@atlaspack/plugin';
-
-export default (new Packager({
- async package({bundle, getSourceMapReference}) {
- let assets = [];
- bundle.traverseAssets(asset => {
- assets.push(asset);
- });
-
- assert.equal(assets.length, 1, 'TS bundles must only contain one asset');
- let code = await assets[0].getCode();
- let map = await assets[0].getMap();
- if (map) {
- let sourcemapReference = await getSourceMapReference(map);
- if (sourcemapReference != null) {
- code += '\n//# sourceMappingURL=' + sourcemapReference + '\n';
- }
- }
-
- return {contents: code, map};
- },
-}): Packager);
diff --git a/packages/packagers/ts/src/TSPackager.ts b/packages/packagers/ts/src/TSPackager.ts
new file mode 100644
index 000000000..728cd538a
--- /dev/null
+++ b/packages/packagers/ts/src/TSPackager.ts
@@ -0,0 +1,23 @@
+import assert from 'assert';
+import {Packager} from '@atlaspack/plugin';
+
+export default new Packager({
+ async package({bundle, getSourceMapReference}) {
+ let assets: Array = [];
+ bundle.traverseAssets((asset) => {
+ assets.push(asset);
+ });
+
+ assert.equal(assets.length, 1, 'TS bundles must only contain one asset');
+ let code = await assets[0].getCode();
+ let map = await assets[0].getMap();
+ if (map) {
+ let sourcemapReference = await getSourceMapReference(map);
+ if (sourcemapReference != null) {
+ code += '\n//# sourceMappingURL=' + sourcemapReference + '\n';
+ }
+ }
+
+ return {contents: code, map};
+ },
+}) as Packager;
diff --git a/packages/packagers/wasm/package.json b/packages/packagers/wasm/package.json
index 2bbc2ab88..979ce2e50 100644
--- a/packages/packagers/wasm/package.json
+++ b/packages/packagers/wasm/package.json
@@ -10,7 +10,8 @@
"url": "https://github.com/atlassian-labs/atlaspack.git"
},
"main": "lib/WasmPackager.js",
- "source": "src/WasmPackager.js",
+ "types": "src/WasmPackager.ts",
+ "source": "src/WasmPackager.ts",
"engines": {
"node": ">=16.0.0",
"parcel": "^2.12.0"
diff --git a/packages/packagers/wasm/src/WasmPackager.js b/packages/packagers/wasm/src/WasmPackager.js
deleted file mode 100644
index 33af4ed73..000000000
--- a/packages/packagers/wasm/src/WasmPackager.js
+++ /dev/null
@@ -1,39 +0,0 @@
-// @flow strict-local
-
-import assert from 'assert';
-import {Packager} from '@atlaspack/plugin';
-import * as wasmmap from './wasm-sourcemap';
-
-export default (new Packager({
- async package({bundle, getSourceMapReference}) {
- let assets = [];
- bundle.traverseAssets(asset => {
- assets.push(asset);
- });
-
- assert.equal(assets.length, 1, 'Wasm bundles must only contain one asset');
-
- let [contents, map] = await Promise.all([
- assets[0].getBuffer(),
- assets[0].getMap(),
- ]);
- let sourcemapReference = await getSourceMapReference(map);
- if (sourcemapReference != null) {
- return {
- contents: Buffer.from(
- wasmmap.SetSourceMapURL(
- contents,
- sourcemapReference,
- sourcemapReference.includes('HASH_REF_')
- ? // HASH_REF_\w{16} -> \w{8}
- sourcemapReference.length - (9 + 16 - 8)
- : undefined,
- ),
- ),
- map,
- };
- } else {
- return {contents, map};
- }
- },
-}): Packager);
diff --git a/packages/packagers/wasm/src/WasmPackager.ts b/packages/packagers/wasm/src/WasmPackager.ts
new file mode 100644
index 000000000..8cfffa4cf
--- /dev/null
+++ b/packages/packagers/wasm/src/WasmPackager.ts
@@ -0,0 +1,38 @@
+import assert from 'assert';
+import {Packager} from '@atlaspack/plugin';
+// @ts-expect-error - TS7016 - Could not find a declaration file for module './wasm-sourcemap'. '/home/ubuntu/parcel/packages/packagers/wasm/src/wasm-sourcemap.js' implicitly has an 'any' type.
+import * as wasmmap from './wasm-sourcemap';
+
+export default new Packager({
+ async package({bundle, getSourceMapReference}) {
+ let assets: Array = [];
+ bundle.traverseAssets((asset) => {
+ assets.push(asset);
+ });
+
+ assert.equal(assets.length, 1, 'Wasm bundles must only contain one asset');
+
+ let [contents, map] = await Promise.all([
+ assets[0].getBuffer(),
+ assets[0].getMap(),
+ ]);
+ let sourcemapReference = await getSourceMapReference(map);
+ if (sourcemapReference != null) {
+ return {
+ contents: Buffer.from(
+ wasmmap.SetSourceMapURL(
+ contents,
+ sourcemapReference,
+ sourcemapReference.includes('HASH_REF_')
+ ? // HASH_REF_\w{16} -> \w{8}
+ sourcemapReference.length - (9 + 16 - 8)
+ : undefined,
+ ),
+ ),
+ map,
+ };
+ } else {
+ return {contents, map};
+ }
+ },
+}) as Packager;
diff --git a/packages/packagers/webextension/package.json b/packages/packagers/webextension/package.json
index b2e2d945d..3a06e5910 100644
--- a/packages/packagers/webextension/package.json
+++ b/packages/packagers/webextension/package.json
@@ -10,7 +10,8 @@
"url": "https://github.com/atlassian-labs/atlaspack.git"
},
"main": "lib/WebExtensionPackager.js",
- "source": "src/WebExtensionPackager.js",
+ "types": "src/WebExtensionPackager.ts",
+ "source": "src/WebExtensionPackager.ts",
"engines": {
"node": ">=16.0.0",
"parcel": "^2.12.0"
diff --git a/packages/packagers/webextension/src/WebExtensionPackager.js b/packages/packagers/webextension/src/WebExtensionPackager.js
deleted file mode 100644
index af1c64c94..000000000
--- a/packages/packagers/webextension/src/WebExtensionPackager.js
+++ /dev/null
@@ -1,108 +0,0 @@
-// @flow strict-local
-
-import assert from 'assert';
-import nullthrows from 'nullthrows';
-import {Packager} from '@atlaspack/plugin';
-import {replaceURLReferences, relativeBundlePath} from '@atlaspack/utils';
-
-export default (new Packager({
- async package({bundle, bundleGraph}) {
- let assets = [];
- bundle.traverseAssets(asset => {
- assets.push(asset);
- });
- const manifestAssets = assets.filter(a => a.meta.webextEntry === true);
-
- assert(
- assets.length == 2 && manifestAssets.length == 1,
- 'Web extension bundles must contain exactly one manifest asset and one runtime asset',
- );
- const asset = manifestAssets[0];
-
- const relPath = b =>
- relativeBundlePath(bundle, b, {leadingDotSlash: false});
-
- const manifest = JSON.parse(await asset.getCode());
-
- if (manifest.background?.type === 'module') {
- // service workers are built with output format 'global'
- // see: https://github.com/parcel-bundler/parcel/blob/3329469f50de9326c5b02ef0ab1c0ce41393279c/packages/transformers/js/src/JSTransformer.js#L577
- delete manifest.background.type;
- }
-
- const deps = asset.getDependencies();
- const war = [];
- for (const contentScript of manifest.content_scripts || []) {
- const srcBundles = deps
- .filter(
- d =>
- contentScript.js?.includes(d.id) ||
- contentScript.css?.includes(d.id),
- )
- .map(d => nullthrows(bundleGraph.getReferencedBundle(d, bundle)));
-
- contentScript.css = [
- ...new Set(
- srcBundles
- .flatMap(b => bundleGraph.getReferencedBundles(b))
- .filter(b => b.type == 'css')
- .map(relPath)
- .concat(contentScript.css || []),
- ),
- ];
-
- contentScript.js = [
- ...new Set(
- srcBundles
- .flatMap(b => bundleGraph.getReferencedBundles(b))
- .filter(b => b.type == 'js')
- .map(relPath)
- .concat(contentScript.js || []),
- ),
- ];
-
- const resources = srcBundles
- .flatMap(b => {
- const children = [];
- const siblings = bundleGraph.getReferencedBundles(b);
- bundleGraph.traverseBundles(child => {
- if (b !== child && !siblings.includes(child)) {
- children.push(child);
- }
- }, b);
- return children;
- })
- .map(relPath);
-
- if (resources.length > 0) {
- war.push({
- matches: contentScript.matches.map(match => {
- if (/^(((http|ws)s?)|ftp|\*):\/\//.test(match)) {
- let pathIndex = match.indexOf('/', match.indexOf('://') + 3);
- // Avoids creating additional errors in invalid match URLs
- if (pathIndex == -1) pathIndex = match.length;
- return match.slice(0, pathIndex) + '/*';
- }
- return match;
- }),
- resources,
- });
- }
- }
-
- const warResult = (manifest.web_accessible_resources || []).concat(
- manifest.manifest_version == 2
- ? [...new Set(war.flatMap(entry => entry.resources))]
- : war,
- );
-
- if (warResult.length > 0) manifest.web_accessible_resources = warResult;
-
- let {contents} = replaceURLReferences({
- bundle,
- bundleGraph,
- contents: JSON.stringify(manifest),
- });
- return {contents};
- },
-}): Packager);
diff --git a/packages/packagers/webextension/src/WebExtensionPackager.ts b/packages/packagers/webextension/src/WebExtensionPackager.ts
new file mode 100644
index 000000000..8a1acafd9
--- /dev/null
+++ b/packages/packagers/webextension/src/WebExtensionPackager.ts
@@ -0,0 +1,120 @@
+import assert from 'assert';
+import nullthrows from 'nullthrows';
+import {Packager} from '@atlaspack/plugin';
+import {replaceURLReferences, relativeBundlePath} from '@atlaspack/utils';
+
+export default new Packager({
+ async package({bundle, bundleGraph}) {
+ let assets: Array = [];
+ bundle.traverseAssets((asset) => {
+ assets.push(asset);
+ });
+ const manifestAssets = assets.filter((a) => a.meta.webextEntry === true);
+
+ assert(
+ assets.length == 2 && manifestAssets.length == 1,
+ 'Web extension bundles must contain exactly one manifest asset and one runtime asset',
+ );
+ const asset = manifestAssets[0];
+
+ const relPath = (b: NamedBundle) =>
+ relativeBundlePath(bundle, b, {leadingDotSlash: false});
+
+ const manifest = JSON.parse(await asset.getCode());
+
+ if (manifest.background?.type === 'module') {
+ // service workers are built with output format 'global'
+ // see: https://github.com/parcel-bundler/parcel/blob/3329469f50de9326c5b02ef0ab1c0ce41393279c/packages/transformers/js/src/JSTransformer.js#L577
+ delete manifest.background.type;
+ }
+
+ const deps = asset.getDependencies();
+ const war: Array<
+ | any
+ | {
+ matches: never;
+ resources: Array;
+ }
+ > = [];
+ for (const contentScript of manifest.content_scripts || []) {
+ const srcBundles = deps
+ .filter(
+ // @ts-expect-error - TS7006 - Parameter 'd' implicitly has an 'any' type.
+ (d) =>
+ contentScript.js?.includes(d.id) ||
+ contentScript.css?.includes(d.id),
+ )
+ // @ts-expect-error - TS7006 - Parameter 'd' implicitly has an 'any' type.
+ .map((d) => nullthrows(bundleGraph.getReferencedBundle(d, bundle)));
+
+ contentScript.css = [
+ ...new Set(
+ srcBundles
+ // @ts-expect-error - TS7006 - Parameter 'b' implicitly has an 'any' type.
+ .flatMap((b) => bundleGraph.getReferencedBundles(b))
+ // @ts-expect-error - TS7006 - Parameter 'b' implicitly has an 'any' type.
+ .filter((b) => b.type == 'css')
+ .map(relPath)
+ .concat(contentScript.css || []),
+ ),
+ ];
+
+ contentScript.js = [
+ ...new Set(
+ srcBundles
+ // @ts-expect-error - TS7006 - Parameter 'b' implicitly has an 'any' type.
+ .flatMap((b) => bundleGraph.getReferencedBundles(b))
+ // @ts-expect-error - TS7006 - Parameter 'b' implicitly has an 'any' type.
+ .filter((b) => b.type == 'js')
+ .map(relPath)
+ .concat(contentScript.js || []),
+ ),
+ ];
+
+ const resources = srcBundles
+ // @ts-expect-error - TS7006 - Parameter 'b' implicitly has an 'any' type.
+ .flatMap((b) => {
+ const children: Array = [];
+ const siblings = bundleGraph.getReferencedBundles(b);
+ bundleGraph.traverseBundles((child) => {
+ if (b !== child && !siblings.includes(child)) {
+ children.push(child);
+ }
+ }, b);
+ return children;
+ })
+ .map(relPath);
+
+ if (resources.length > 0) {
+ war.push({
+ // @ts-expect-error - TS7006 - Parameter 'match' implicitly has an 'any' type.
+ matches: contentScript.matches.map((match) => {
+ if (/^(((http|ws)s?)|ftp|\*):\/\//.test(match)) {
+ let pathIndex = match.indexOf('/', match.indexOf('://') + 3);
+ // Avoids creating additional errors in invalid match URLs
+ if (pathIndex == -1) pathIndex = match.length;
+ return match.slice(0, pathIndex) + '/*';
+ }
+ return match;
+ }),
+ resources,
+ });
+ }
+ }
+
+ const warResult = (manifest.web_accessible_resources || []).concat(
+ manifest.manifest_version == 2
+ ? [...new Set(war.flatMap((entry) => entry.resources))]
+ : war,
+ );
+
+ if (warResult.length > 0) manifest.web_accessible_resources = warResult;
+
+ let {contents} = replaceURLReferences({
+ bundle,
+ bundleGraph,
+ contents: JSON.stringify(manifest),
+ });
+ return {contents};
+ },
+}) as Packager;
diff --git a/packages/packagers/xml/package.json b/packages/packagers/xml/package.json
index f3570dd1c..88a5e081a 100644
--- a/packages/packagers/xml/package.json
+++ b/packages/packagers/xml/package.json
@@ -10,7 +10,8 @@
"url": "https://github.com/atlassian-labs/atlaspack.git"
},
"main": "lib/XMLPackager.js",
- "source": "src/XMLPackager.js",
+ "types": "src/XMLPackager.ts",
+ "source": "src/XMLPackager.ts",
"engines": {
"node": ">= 16.0.0",
"parcel": "^2.12.0"
diff --git a/packages/packagers/xml/src/XMLPackager.js b/packages/packagers/xml/src/XMLPackager.js
deleted file mode 100644
index 61df5adbf..000000000
--- a/packages/packagers/xml/src/XMLPackager.js
+++ /dev/null
@@ -1,112 +0,0 @@
-// @flow
-
-import type {Bundle, BundleGraph, NamedBundle} from '@atlaspack/types';
-import assert from 'assert';
-import {Packager} from '@atlaspack/plugin';
-import {
- blobToString,
- replaceInlineReferences,
- replaceURLReferences,
-} from '@atlaspack/utils';
-import {DOMParser, XMLSerializer} from '@xmldom/xmldom';
-
-export default (new Packager({
- async package({bundle, bundleGraph, getInlineBundleContents}) {
- const assets = [];
- bundle.traverseAssets(asset => {
- assets.push(asset);
- });
-
- assert.strictEqual(
- assets.length,
- 1,
- 'XML bundles must only contain one asset',
- );
-
- let asset = assets[0];
- let code = await asset.getCode();
- let parser = new DOMParser();
- let dom = parser.parseFromString(code);
-
- let inlineElements = dom.getElementsByTagNameNS(
- 'https://parceljs.org',
- 'inline',
- );
- if (inlineElements.length > 0) {
- for (let element of Array.from(inlineElements)) {
- let key = element.getAttribute('key');
- let type = element.getAttribute('type');
-
- const newContent = await getAssetContent(
- bundleGraph,
- getInlineBundleContents,
- key,
- );
-
- if (newContent === null) {
- continue;
- }
-
- let contents = await blobToString(newContent.contents);
- if (type === 'xhtml' || type === 'xml') {
- let parsed = new DOMParser().parseFromString(
- contents,
- 'application/xml',
- );
- if (parsed.documentElement != null) {
- let parent = element.parentNode;
- parent.removeChild(element);
- parent.appendChild(parsed.documentElement);
- }
- } else {
- element.parentNode.textContent = contents;
- }
- }
-
- code = new XMLSerializer().serializeToString(dom);
- }
-
- const {contents, map} = replaceURLReferences({
- bundle,
- bundleGraph,
- contents: code,
- relative: false,
- getReplacement: contents => contents.replace(/"/g, '"'),
- });
-
- return replaceInlineReferences({
- bundle,
- bundleGraph,
- contents,
- getInlineBundleContents,
- getInlineReplacement: (dep, inlineType, contents) => ({
- from: dep.id,
- to: contents.replace(/"/g, '"').trim(),
- }),
- map,
- });
- },
-}): Packager);
-
-async function getAssetContent(
- bundleGraph: BundleGraph,
- getInlineBundleContents,
- assetId,
-) {
- let inlineBundle: ?Bundle;
- bundleGraph.traverseBundles((bundle, context, {stop}) => {
- const entryAssets = bundle.getEntryAssets();
- if (entryAssets.some(a => a.uniqueKey === assetId)) {
- inlineBundle = bundle;
- stop();
- }
- });
-
- if (!inlineBundle) {
- return null;
- }
-
- const bundleResult = await getInlineBundleContents(inlineBundle, bundleGraph);
-
- return {bundle: inlineBundle, contents: bundleResult.contents};
-}
diff --git a/packages/packagers/xml/src/XMLPackager.ts b/packages/packagers/xml/src/XMLPackager.ts
new file mode 100644
index 000000000..0ba1896c4
--- /dev/null
+++ b/packages/packagers/xml/src/XMLPackager.ts
@@ -0,0 +1,118 @@
+import type {Bundle, BundleGraph, NamedBundle} from '@atlaspack/types';
+import assert from 'assert';
+import {Packager} from '@atlaspack/plugin';
+import {
+ blobToString,
+ replaceInlineReferences,
+ replaceURLReferences,
+} from '@atlaspack/utils';
+import {DOMParser, XMLSerializer} from '@xmldom/xmldom';
+
+export default new Packager({
+ async package({bundle, bundleGraph, getInlineBundleContents}) {
+ const assets: Array = [];
+ bundle.traverseAssets((asset) => {
+ assets.push(asset);
+ });
+
+ assert.strictEqual(
+ assets.length,
+ 1,
+ 'XML bundles must only contain one asset',
+ );
+
+ let asset = assets[0];
+ let code = await asset.getCode();
+ let parser = new DOMParser();
+ let dom = parser.parseFromString(code);
+
+ let inlineElements = dom.getElementsByTagNameNS(
+ 'https://parceljs.org',
+ 'inline',
+ );
+ if (inlineElements.length > 0) {
+ for (let element of Array.from(inlineElements)) {
+ let key = element.getAttribute('key');
+ let type = element.getAttribute('type');
+
+ const newContent = await getAssetContent(
+ bundleGraph,
+ getInlineBundleContents,
+ key,
+ );
+
+ if (newContent === null) {
+ continue;
+ }
+
+ let contents = await blobToString(newContent.contents);
+ if (type === 'xhtml' || type === 'xml') {
+ let parsed = new DOMParser().parseFromString(
+ contents,
+ 'application/xml',
+ );
+ if (parsed.documentElement != null) {
+ let parent = element.parentNode;
+ // @ts-expect-error - TS2531 - Object is possibly 'null'.
+ parent.removeChild(element);
+ // @ts-expect-error - TS2531 - Object is possibly 'null'.
+ parent.appendChild(parsed.documentElement);
+ }
+ } else {
+ // @ts-expect-error - TS2531 - Object is possibly 'null'.
+ element.parentNode.textContent = contents;
+ }
+ }
+
+ code = new XMLSerializer().serializeToString(dom);
+ }
+
+ const {contents, map} = replaceURLReferences({
+ bundle,
+ bundleGraph,
+ contents: code,
+ relative: false,
+ getReplacement: (contents) => contents.replace(/"/g, '"'),
+ });
+
+ return replaceInlineReferences({
+ bundle,
+ bundleGraph,
+ contents,
+ getInlineBundleContents,
+ getInlineReplacement: (dep, inlineType, contents) => ({
+ from: dep.id,
+ to: contents.replace(/"/g, '"').trim(),
+ }),
+ map,
+ });
+ },
+}) as Packager;
+
+async function getAssetContent(
+ bundleGraph: BundleGraph,
+ getInlineBundleContents: (
+ arg1: Bundle,
+ arg2: BundleGraph,
+ ) => Async<{
+ contents: Blob;
+ }>,
+ assetId: any,
+) {
+ let inlineBundle: Bundle | null | undefined;
+ bundleGraph.traverseBundles((bundle, context, {stop}) => {
+ const entryAssets = bundle.getEntryAssets();
+ if (entryAssets.some((a) => a.uniqueKey === assetId)) {
+ inlineBundle = bundle;
+ stop();
+ }
+ });
+
+ if (!inlineBundle) {
+ return null;
+ }
+
+ const bundleResult = await getInlineBundleContents(inlineBundle, bundleGraph);
+
+ return {bundle: inlineBundle, contents: bundleResult.contents};
+}
diff --git a/packages/reporters/build-metrics/package.json b/packages/reporters/build-metrics/package.json
index 3e30fb3a2..ec477df3e 100644
--- a/packages/reporters/build-metrics/package.json
+++ b/packages/reporters/build-metrics/package.json
@@ -10,7 +10,8 @@
"url": "https://github.com/atlassian-labs/atlaspack.git"
},
"main": "lib/BuildMetricsReporter.js",
- "source": "src/BuildMetricsReporter.js",
+ "types": "src/BuildMetricsReporter.ts",
+ "source": "src/BuildMetricsReporter.ts",
"engines": {
"node": ">= 16.0.0",
"parcel": "^2.12.0"
diff --git a/packages/reporters/build-metrics/src/BuildMetricsReporter.js b/packages/reporters/build-metrics/src/BuildMetricsReporter.js
deleted file mode 100644
index 25148e75b..000000000
--- a/packages/reporters/build-metrics/src/BuildMetricsReporter.js
+++ /dev/null
@@ -1,72 +0,0 @@
-// @flow strict-local
-import path from 'path';
-
-import {Reporter} from '@atlaspack/plugin';
-import {generateBuildMetrics} from '@atlaspack/utils';
-
-type TimingValue = {|
- timings: {[key: string]: number, ...},
- lastPhase: string,
-|};
-
-let timingsMap = new Map();
-const getValue = (instanceId: string): TimingValue => {
- if (!timingsMap.has(instanceId)) {
- timingsMap.set(instanceId, {
- timings: {},
- lastPhase: 'resolving',
- });
- }
-
- // $FlowFixMe
- return timingsMap.get(instanceId);
-};
-
-export default (new Reporter({
- async report({event, options}) {
- if (event.type === 'buildProgress') {
- let value = getValue(options.instanceId);
-
- value.timings[event.phase] = Date.now();
- if (value.lastPhase !== event.phase) {
- value.timings[value.lastPhase] =
- Date.now() - value.timings[value.lastPhase];
- }
- value.lastPhase = event.phase;
- } else if (event.type === 'buildSuccess') {
- let value = getValue(options.instanceId);
-
- value.timings[value.lastPhase] =
- Date.now() - value.timings[value.lastPhase];
- let metricsFilePath = path.join(
- options.projectRoot,
- 'parcel-metrics.json',
- );
-
- let {bundles} = await generateBuildMetrics(
- event.bundleGraph.getBundles(),
- options.outputFS,
- options.projectRoot,
- );
-
- let metrics = {
- phaseTimings: value.timings,
- buildTime: event.buildTime,
- bundles: bundles.map(b => {
- return {
- filePath: b.filePath,
- size: b.size,
- time: b.time,
- largestAssets: b.assets.slice(0, 10),
- totalAssets: b.assets.length,
- };
- }),
- };
-
- await options.outputFS.writeFile(
- metricsFilePath,
- JSON.stringify(metrics),
- );
- }
- },
-}): Reporter);
diff --git a/packages/reporters/build-metrics/src/BuildMetricsReporter.ts b/packages/reporters/build-metrics/src/BuildMetricsReporter.ts
new file mode 100644
index 000000000..e0ff4d3d2
--- /dev/null
+++ b/packages/reporters/build-metrics/src/BuildMetricsReporter.ts
@@ -0,0 +1,72 @@
+import path from 'path';
+
+import {Reporter} from '@atlaspack/plugin';
+import {generateBuildMetrics} from '@atlaspack/utils';
+
+type TimingValue = {
+ timings: {
+ [key: string]: number;
+ };
+ lastPhase: string;
+};
+
+let timingsMap = new Map();
+const getValue = (instanceId: string): TimingValue => {
+ if (!timingsMap.has(instanceId)) {
+ timingsMap.set(instanceId, {
+ timings: {},
+ lastPhase: 'resolving',
+ });
+ }
+
+ return timingsMap.get(instanceId);
+};
+
+export default new Reporter({
+ async report({event, options}) {
+ if (event.type === 'buildProgress') {
+ let value = getValue(options.instanceId);
+
+ value.timings[event.phase] = Date.now();
+ if (value.lastPhase !== event.phase) {
+ value.timings[value.lastPhase] =
+ Date.now() - value.timings[value.lastPhase];
+ }
+ value.lastPhase = event.phase;
+ } else if (event.type === 'buildSuccess') {
+ let value = getValue(options.instanceId);
+
+ value.timings[value.lastPhase] =
+ Date.now() - value.timings[value.lastPhase];
+ let metricsFilePath = path.join(
+ options.projectRoot,
+ 'parcel-metrics.json',
+ );
+
+ let {bundles} = await generateBuildMetrics(
+ event.bundleGraph.getBundles(),
+ options.outputFS,
+ options.projectRoot,
+ );
+
+ let metrics = {
+ phaseTimings: value.timings,
+ buildTime: event.buildTime,
+ bundles: bundles.map((b) => {
+ return {
+ filePath: b.filePath,
+ size: b.size,
+ time: b.time,
+ largestAssets: b.assets.slice(0, 10),
+ totalAssets: b.assets.length,
+ };
+ }),
+ };
+
+ await options.outputFS.writeFile(
+ metricsFilePath,
+ JSON.stringify(metrics),
+ );
+ }
+ },
+}) as Reporter;
diff --git a/packages/reporters/bundle-analyzer/client/index.js b/packages/reporters/bundle-analyzer/client/index.js
index b1d46c58b..0debf3f29 100644
--- a/packages/reporters/bundle-analyzer/client/index.js
+++ b/packages/reporters/bundle-analyzer/client/index.js
@@ -64,7 +64,7 @@ let foamtree = new CarrotSearchFoamTree({
},
});
-visualization.addEventListener('mousemove', e => {
+visualization.addEventListener('mousemove', (e) => {
if (tooltip == null) {
return;
}
diff --git a/packages/reporters/bundle-analyzer/index.js b/packages/reporters/bundle-analyzer/index.js
deleted file mode 100644
index b7d10a23a..000000000
--- a/packages/reporters/bundle-analyzer/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-// @flow strict-local
-
-export * from './src/BundleAnalyzerReporter';
diff --git a/packages/reporters/bundle-analyzer/index.ts b/packages/reporters/bundle-analyzer/index.ts
new file mode 100644
index 000000000..7022a75ef
--- /dev/null
+++ b/packages/reporters/bundle-analyzer/index.ts
@@ -0,0 +1 @@
+export * from './src/BundleAnalyzerReporter';
diff --git a/packages/reporters/bundle-analyzer/package.json b/packages/reporters/bundle-analyzer/package.json
index 7f3b921d4..da7b5d640 100644
--- a/packages/reporters/bundle-analyzer/package.json
+++ b/packages/reporters/bundle-analyzer/package.json
@@ -10,7 +10,8 @@
"url": "https://github.com/atlassian-labs/atlaspack.git"
},
"main": "lib/BundleAnalyzerReporter.js",
- "source": "src/BundleAnalyzerReporter.js",
+ "types": "src/BundleAnalyzerReporter.ts",
+ "source": "src/BundleAnalyzerReporter.ts",
"engines": {
"node": ">= 16.0.0",
"parcel": "^2.12.0"
diff --git a/packages/reporters/bundle-analyzer/src/BundleAnalyzerReporter.js b/packages/reporters/bundle-analyzer/src/BundleAnalyzerReporter.js
deleted file mode 100644
index d3d7beebe..000000000
--- a/packages/reporters/bundle-analyzer/src/BundleAnalyzerReporter.js
+++ /dev/null
@@ -1,192 +0,0 @@
-// @flow strict-local
-
-import type {FilePath, PackagedBundle, PluginOptions} from '@atlaspack/types';
-
-import invariant from 'assert';
-import {Reporter} from '@atlaspack/plugin';
-import {DefaultMap, generateBuildMetrics} from '@atlaspack/utils';
-import path from 'path';
-import nullthrows from 'nullthrows';
-
-export default (new Reporter({
- async report({event, options}) {
- if (event.type !== 'buildSuccess') {
- return;
- }
-
- let bundlesByTarget: DefaultMap<
- string /* target name */,
- Array,
- > = new DefaultMap(() => []);
- for (let bundle of event.bundleGraph.getBundles()) {
- bundlesByTarget.get(bundle.target.name).push(bundle);
- }
-
- let reportsDir = path.join(options.projectRoot, 'parcel-bundle-reports');
- await options.outputFS.mkdirp(reportsDir);
-
- await Promise.all(
- [...bundlesByTarget.entries()].map(async ([targetName, bundles]) => {
- return options.outputFS.writeFile(
- path.join(reportsDir, `${targetName}.html`),
- `
-
-
-
- Atlaspack Bundle Analyzer | ${targetName}
-
-
-
-
-
-
-
-
- `,
- );
- }),
- );
- },
-}): Reporter);
-
-type BundleData = {|
- groups: Array,
-|};
-
-async function getBundleData(
- bundles: Array,
- options: PluginOptions,
-): Promise {
- let groups = await Promise.all(
- bundles.map(bundle => getBundleNode(bundle, options)),
- );
- return {
- groups,
- };
-}
-
-type File = {|
- basename: string,
- size: number,
-|};
-type DirMapValue = File | DirMap;
-type DirMap = DefaultMap;
-let createMap: () => DirMap = () => new DefaultMap(() => createMap());
-
-async function getBundleNode(bundle: PackagedBundle, options: PluginOptions) {
- let buildMetrics = await generateBuildMetrics(
- [bundle],
- options.outputFS,
- options.projectRoot,
- );
- let bundleData = buildMetrics.bundles[0];
- let dirMap = createMap();
- for (let asset of bundleData.assets) {
- let relativePath = path.relative(options.projectRoot, asset.filePath);
- let parts = relativePath.split(path.sep);
- let dirs = parts.slice(0, parts.length - 1);
- let basename = path.basename(asset.filePath);
-
- let map = dirMap;
- for (let dir of dirs) {
- invariant(map instanceof DefaultMap);
- map = map.get(dir);
- }
-
- invariant(map instanceof DefaultMap);
- map.set(basename, {
- basename,
- size: asset.size,
- });
- }
-
- return {
- label: path.relative(options.projectRoot, bundle.filePath),
- weight: bundle.stats.size,
- groups: generateGroups(dirMap),
- };
-}
-
-type Group = {|
- label: string,
- weight: number,
- groups?: Array,
-|};
-
-function generateGroups(dirMap: DirMap): Array {
- let groups = [];
-
- for (let [directoryName, contents] of dirMap) {
- if (contents instanceof DefaultMap) {
- let childrenGroups = generateGroups(contents);
- if (childrenGroups.length === 1) {
- let firstChild = childrenGroups[0];
- groups.push({
- ...firstChild,
- label: path.join(directoryName, firstChild.label),
- });
- } else {
- groups.push({
- label: directoryName,
- weight: childrenGroups.reduce(
- (acc, g) => acc + nullthrows(g.weight),
- 0,
- ),
- groups: childrenGroups,
- });
- }
- } else {
- // file
- groups.push({
- label:
- contents.basename === ''
- ? 'Code from unknown source files'
- : contents.basename,
- weight: contents.size,
- });
- }
- }
-
- return groups;
-}
diff --git a/packages/reporters/bundle-analyzer/src/BundleAnalyzerReporter.ts b/packages/reporters/bundle-analyzer/src/BundleAnalyzerReporter.ts
new file mode 100644
index 000000000..4efec046f
--- /dev/null
+++ b/packages/reporters/bundle-analyzer/src/BundleAnalyzerReporter.ts
@@ -0,0 +1,193 @@
+import type {FilePath, PackagedBundle, PluginOptions} from '@atlaspack/types';
+
+import invariant from 'assert';
+import {Reporter} from '@atlaspack/plugin';
+import {DefaultMap, generateBuildMetrics} from '@atlaspack/utils';
+import path from 'path';
+import nullthrows from 'nullthrows';
+
+export default new Reporter({
+ async report({event, options}) {
+ if (event.type !== 'buildSuccess') {
+ return;
+ }
+
+ let bundlesByTarget: DefaultMap<
+ string /* target name */,
+ Array
+ > = new DefaultMap(() => []);
+ for (let bundle of event.bundleGraph.getBundles()) {
+ bundlesByTarget.get(bundle.target.name).push(bundle);
+ }
+
+ let reportsDir = path.join(options.projectRoot, 'parcel-bundle-reports');
+ await options.outputFS.mkdirp(reportsDir);
+
+ await Promise.all(
+ [...bundlesByTarget.entries()].map(
+ async ([targetName, bundles]: [any, any]) => {
+ return options.outputFS.writeFile(
+ path.join(reportsDir, `${targetName}.html`),
+ `
+
+
+
+ Atlaspack Bundle Analyzer | ${targetName}
+
+
+
+
+
+
+
+
+ `,
+ );
+ },
+ ),
+ );
+ },
+}) as Reporter;
+
+type BundleData = {
+ groups: Array;
+};
+
+async function getBundleData(
+ bundles: Array,
+ options: PluginOptions,
+): Promise {
+ let groups = await Promise.all(
+ bundles.map((bundle) => getBundleNode(bundle, options)),
+ );
+ return {
+ groups,
+ };
+}
+
+type File = {
+ basename: string;
+ size: number;
+};
+type DirMapValue = File | DirMap;
+type DirMap = DefaultMap;
+let createMap: () => DirMap = () => new DefaultMap(() => createMap());
+
+async function getBundleNode(bundle: PackagedBundle, options: PluginOptions) {
+ let buildMetrics = await generateBuildMetrics(
+ [bundle],
+ options.outputFS,
+ options.projectRoot,
+ );
+ let bundleData = buildMetrics.bundles[0];
+ let dirMap = createMap();
+ for (let asset of bundleData.assets) {
+ let relativePath = path.relative(options.projectRoot, asset.filePath);
+ let parts = relativePath.split(path.sep);
+ let dirs = parts.slice(0, parts.length - 1);
+ let basename = path.basename(asset.filePath);
+
+ let map = dirMap;
+ for (let dir of dirs) {
+ invariant(map instanceof DefaultMap);
+ // @ts-expect-error - TS2322 - Type 'DirMapValue' is not assignable to type 'DirMap'.
+ map = map.get(dir);
+ }
+
+ invariant(map instanceof DefaultMap);
+ map.set(basename, {
+ basename,
+ size: asset.size,
+ });
+ }
+
+ return {
+ label: path.relative(options.projectRoot, bundle.filePath),
+ weight: bundle.stats.size,
+ groups: generateGroups(dirMap),
+ };
+}
+
+type Group = {
+ label: string;
+ weight: number;
+ groups?: Array;
+};
+
+function generateGroups(dirMap: DirMap): Array {
+ let groups: Array = [];
+
+ for (let [directoryName, contents] of dirMap) {
+ if (contents instanceof DefaultMap) {
+ let childrenGroups = generateGroups(contents);
+ if (childrenGroups.length === 1) {
+ let firstChild = childrenGroups[0];
+ groups.push({
+ ...firstChild,
+ label: path.join(directoryName, firstChild.label),
+ });
+ } else {
+ groups.push({
+ label: directoryName,
+ weight: childrenGroups.reduce(
+ (acc, g) => acc + nullthrows(g.weight),
+ 0,
+ ),
+ groups: childrenGroups,
+ });
+ }
+ } else {
+ // file
+ groups.push({
+ label:
+ contents.basename === ''
+ ? 'Code from unknown source files'
+ : contents.basename,
+ weight: contents.size,
+ });
+ }
+ }
+
+ return groups;
+}
diff --git a/packages/reporters/bundle-buddy/package.json b/packages/reporters/bundle-buddy/package.json
index 4bcaf190e..e1636486b 100644
--- a/packages/reporters/bundle-buddy/package.json
+++ b/packages/reporters/bundle-buddy/package.json
@@ -10,7 +10,8 @@
"url": "https://github.com/atlassian-labs/atlaspack.git"
},
"main": "lib/BundleBuddyReporter.js",
- "source": "src/BundleBuddyReporter.js",
+ "types": "src/BundleBuddyReporter.ts",
+ "source": "src/BundleBuddyReporter.ts",
"engines": {
"node": ">= 16.0.0",
"parcel": "^2.12.0"
diff --git a/packages/reporters/bundle-buddy/src/BundleBuddyReporter.js b/packages/reporters/bundle-buddy/src/BundleBuddyReporter.js
deleted file mode 100644
index c5670335f..000000000
--- a/packages/reporters/bundle-buddy/src/BundleBuddyReporter.js
+++ /dev/null
@@ -1,55 +0,0 @@
-// @flow strict-local
-import type {PackagedBundle} from '@atlaspack/types';
-import {Reporter} from '@atlaspack/plugin';
-import path from 'path';
-
-export default (new Reporter({
- async report({event, options, logger}) {
- if (event.type !== 'buildSuccess') {
- return;
- }
-
- let bundlesByTarget: Map> = new Map();
- for (let bundle of event.bundleGraph.getBundles()) {
- let bundles = bundlesByTarget.get(bundle.target.distDir);
- if (!bundles) {
- bundles = [];
- bundlesByTarget.set(bundle.target.distDir, bundles);
- }
-
- bundles.push(bundle);
- }
-
- for (let [targetDir, bundles] of bundlesByTarget) {
- let out = [];
-
- for (let bundle of bundles) {
- bundle.traverseAssets(asset => {
- let deps = event.bundleGraph.getDependencies(asset);
- for (let dep of deps) {
- let resolved = event.bundleGraph.getResolvedAsset(dep);
- if (!resolved) {
- continue;
- }
-
- out.push({
- source: path.relative(options.projectRoot, asset.filePath),
- target: path.relative(options.projectRoot, resolved.filePath),
- });
- }
- });
- }
-
- await options.outputFS.writeFile(
- path.join(targetDir, 'bundle-buddy.json'),
- JSON.stringify(out),
- );
- logger.info({
- message: `Wrote report to ${path.relative(
- options.outputFS.cwd(),
- path.join(targetDir, 'bundle-buddy.json'),
- )}`,
- });
- }
- },
-}): Reporter);
diff --git a/packages/reporters/bundle-buddy/src/BundleBuddyReporter.ts b/packages/reporters/bundle-buddy/src/BundleBuddyReporter.ts
new file mode 100644
index 000000000..b3c74d6d8
--- /dev/null
+++ b/packages/reporters/bundle-buddy/src/BundleBuddyReporter.ts
@@ -0,0 +1,57 @@
+import type {PackagedBundle} from '@atlaspack/types';
+import {Reporter} from '@atlaspack/plugin';
+import path from 'path';
+
+export default new Reporter({
+ async report({event, options, logger}) {
+ if (event.type !== 'buildSuccess') {
+ return;
+ }
+
+ let bundlesByTarget: Map> = new Map();
+ for (let bundle of event.bundleGraph.getBundles()) {
+ let bundles = bundlesByTarget.get(bundle.target.distDir);
+ if (!bundles) {
+ bundles = [];
+ bundlesByTarget.set(bundle.target.distDir, bundles);
+ }
+
+ bundles.push(bundle);
+ }
+
+ for (let [targetDir, bundles] of bundlesByTarget) {
+ let out: Array<{
+ source: string;
+ target: string;
+ }> = [];
+
+ for (let bundle of bundles) {
+ bundle.traverseAssets((asset) => {
+ let deps = event.bundleGraph.getDependencies(asset);
+ for (let dep of deps) {
+ let resolved = event.bundleGraph.getResolvedAsset(dep);
+ if (!resolved) {
+ continue;
+ }
+
+ out.push({
+ source: path.relative(options.projectRoot, asset.filePath),
+ target: path.relative(options.projectRoot, resolved.filePath),
+ });
+ }
+ });
+ }
+
+ await options.outputFS.writeFile(
+ path.join(targetDir, 'bundle-buddy.json'),
+ JSON.stringify(out),
+ );
+ logger.info({
+ message: `Wrote report to ${path.relative(
+ options.outputFS.cwd(),
+ path.join(targetDir, 'bundle-buddy.json'),
+ )}`,
+ });
+ }
+ },
+}) as Reporter;
diff --git a/packages/reporters/bundle-stats/package.json b/packages/reporters/bundle-stats/package.json
index dceefe24e..396ff6ad1 100644
--- a/packages/reporters/bundle-stats/package.json
+++ b/packages/reporters/bundle-stats/package.json
@@ -5,7 +5,8 @@
"access": "public"
},
"main": "lib/BundleStatsReporter.js",
- "source": "src/BundleStatsReporter.js",
+ "types": "src/BundleStatsReporter.ts",
+ "source": "src/BundleStatsReporter.ts",
"bin": {
"atlaspack-bundle-stats": "bin.js"
},
diff --git a/packages/reporters/bundle-stats/src/BundleStatsReporter.js b/packages/reporters/bundle-stats/src/BundleStatsReporter.js
deleted file mode 100644
index cfd97e681..000000000
--- a/packages/reporters/bundle-stats/src/BundleStatsReporter.js
+++ /dev/null
@@ -1,99 +0,0 @@
-// @flow strict-local
-
-import type {PackagedBundle, PluginOptions} from '@atlaspack/types';
-
-import {Reporter} from '@atlaspack/plugin';
-import {DefaultMap} from '@atlaspack/utils';
-
-import assert from 'assert';
-import path from 'path';
-
-export type AssetStat = {|
- size: number,
- name: string,
- bundles: Array,
-|};
-
-export type BundleStat = {|
- size: number,
- id: string,
- assets: Array,
-|};
-
-export type BundleStats = {|
- bundles: {[key: string]: BundleStat},
- assets: {[key: string]: AssetStat},
-|};
-
-export default (new Reporter({
- async report({event, options}) {
- if (event.type !== 'buildSuccess') {
- return;
- }
-
- let bundlesByTarget: DefaultMap<
- string /* target name */,
- Array,
- > = new DefaultMap(() => []);
- for (let bundle of event.bundleGraph.getBundles()) {
- bundlesByTarget.get(bundle.target.name).push(bundle);
- }
-
- let reportsDir = path.join(options.projectRoot, 'parcel-bundle-reports');
- await options.outputFS.mkdirp(reportsDir);
-
- await Promise.all(
- [...bundlesByTarget.entries()].map(([targetName, bundles]) =>
- options.outputFS.writeFile(
- path.join(reportsDir, `${targetName}-stats.json`),
- JSON.stringify(getBundleStats(bundles, options), null, 2),
- ),
- ),
- );
- },
-}): Reporter);
-
-export function getBundleStats(
- bundles: Array,
- options: PluginOptions,
-): BundleStats {
- let bundlesByName = new Map();
- let assetsById = new Map();
-
- // let seen = new Map();
-
- for (let bundle of bundles) {
- let bundleName = path.relative(options.projectRoot, bundle.filePath);
-
- // If we've already seen this bundle, we can skip it... right?
- if (bundlesByName.has(bundleName)) {
- // Sanity check: this is the same bundle, right?
- assert(bundlesByName.get(bundleName)?.size === bundle.stats.size);
- continue;
- }
-
- let assets = [];
- bundle.traverseAssets(({id, filePath, stats: {size}}) => {
- assets.push(id);
- let assetName = path.relative(options.projectRoot, filePath);
- if (assetsById.has(id)) {
- assert(assetsById.get(id)?.name === assetName);
- assert(assetsById.get(id)?.size === size);
- assetsById.get(id)?.bundles.push(bundleName);
- } else {
- assetsById.set(id, {name: assetName, size, bundles: [bundleName]});
- }
- });
-
- bundlesByName.set(bundleName, {
- id: bundle.id,
- size: bundle.stats.size,
- assets,
- });
- }
-
- return {
- bundles: Object.fromEntries(bundlesByName),
- assets: Object.fromEntries(assetsById),
- };
-}
diff --git a/packages/reporters/bundle-stats/src/BundleStatsReporter.ts b/packages/reporters/bundle-stats/src/BundleStatsReporter.ts
new file mode 100644
index 000000000..5cfcc9582
--- /dev/null
+++ b/packages/reporters/bundle-stats/src/BundleStatsReporter.ts
@@ -0,0 +1,101 @@
+import type {PackagedBundle, PluginOptions} from '@atlaspack/types';
+
+import {Reporter} from '@atlaspack/plugin';
+import {DefaultMap} from '@atlaspack/utils';
+
+import assert from 'assert';
+import path from 'path';
+
+export type AssetStat = {
+ size: number;
+ name: string;
+ bundles: Array;
+};
+
+export type BundleStat = {
+ size: number;
+ id: string;
+ assets: Array;
+};
+
+export type BundleStats = {
+ bundles: {
+ [key: string]: BundleStat;
+ };
+ assets: {
+ [key: string]: AssetStat;
+ };
+};
+
+export default new Reporter({
+ async report({event, options}) {
+ if (event.type !== 'buildSuccess') {
+ return;
+ }
+
+ let bundlesByTarget: DefaultMap<
+ string /* target name */,
+ Array
+ > = new DefaultMap(() => []);
+ for (let bundle of event.bundleGraph.getBundles()) {
+ bundlesByTarget.get(bundle.target.name).push(bundle);
+ }
+
+ let reportsDir = path.join(options.projectRoot, 'parcel-bundle-reports');
+ await options.outputFS.mkdirp(reportsDir);
+
+ await Promise.all(
+ [...bundlesByTarget.entries()].map(([targetName, bundles]: [any, any]) =>
+ options.outputFS.writeFile(
+ path.join(reportsDir, `${targetName}-stats.json`),
+ JSON.stringify(getBundleStats(bundles, options), null, 2),
+ ),
+ ),
+ );
+ },
+}) as Reporter;
+
+export function getBundleStats(
+ bundles: Array,
+ options: PluginOptions,
+): BundleStats {
+ let bundlesByName = new Map();
+ let assetsById = new Map();
+
+ // let seen = new Map();
+
+ for (let bundle of bundles) {
+ let bundleName = path.relative(options.projectRoot, bundle.filePath);
+
+ // If we've already seen this bundle, we can skip it... right?
+ if (bundlesByName.has(bundleName)) {
+ // Sanity check: this is the same bundle, right?
+ assert(bundlesByName.get(bundleName)?.size === bundle.stats.size);
+ continue;
+ }
+
+ let assets: Array = [];
+ bundle.traverseAssets(({id, filePath, stats: {size}}) => {
+ assets.push(id);
+ let assetName = path.relative(options.projectRoot, filePath);
+ if (assetsById.has(id)) {
+ assert(assetsById.get(id)?.name === assetName);
+ assert(assetsById.get(id)?.size === size);
+ assetsById.get(id)?.bundles.push(bundleName);
+ } else {
+ assetsById.set(id, {name: assetName, size, bundles: [bundleName]});
+ }
+ });
+
+ bundlesByName.set(bundleName, {
+ id: bundle.id,
+ size: bundle.stats.size,
+ assets,
+ });
+ }
+
+ return {
+ bundles: Object.fromEntries(bundlesByName),
+ assets: Object.fromEntries(assetsById),
+ };
+}
diff --git a/packages/reporters/cli/package.json b/packages/reporters/cli/package.json
index 71f3185c5..319692925 100644
--- a/packages/reporters/cli/package.json
+++ b/packages/reporters/cli/package.json
@@ -10,7 +10,8 @@
"url": "https://github.com/atlassian-labs/atlaspack.git"
},
"main": "lib/CLIReporter.js",
- "source": "src/CLIReporter.js",
+ "types": "src/CLIReporter.ts",
+ "source": "src/CLIReporter.ts",
"engines": {
"node": ">= 16.0.0",
"parcel": "^2.12.0"
diff --git a/packages/reporters/cli/src/CLIReporter.js b/packages/reporters/cli/src/CLIReporter.js
deleted file mode 100644
index 8f8f43b04..000000000
--- a/packages/reporters/cli/src/CLIReporter.js
+++ /dev/null
@@ -1,298 +0,0 @@
-// @flow
-import type {ReporterEvent, PluginOptions} from '@atlaspack/types';
-import type {Diagnostic} from '@atlaspack/diagnostic';
-import type {Color} from 'chalk';
-
-import {Reporter} from '@atlaspack/plugin';
-import {
- getProgressMessage,
- prettifyTime,
- prettyDiagnostic,
- throttle,
-} from '@atlaspack/utils';
-import chalk from 'chalk';
-
-import {getTerminalWidth} from './utils';
-import logLevels from './logLevels';
-import bundleReport from './bundleReport';
-import phaseReport from './phaseReport';
-import {
- writeOut,
- updateSpinner,
- persistSpinner,
- isTTY,
- resetWindow,
- persistMessage,
-} from './render';
-import * as emoji from './emoji';
-import wrapAnsi from 'wrap-ansi';
-
-const THROTTLE_DELAY = 100;
-const seenWarnings = new Set();
-const seenPhases = new Set();
-const seenPhasesGen = new Set();
-
-let phaseStartTimes = {};
-let pendingIncrementalBuild = false;
-
-let statusThrottle = throttle((message: string) => {
- updateSpinner(message);
-}, THROTTLE_DELAY);
-
-// Exported only for test
-export async function _report(
- event: ReporterEvent,
- options: PluginOptions,
-): Promise {
- let logLevelFilter = logLevels[options.logLevel || 'info'];
-
- switch (event.type) {
- case 'buildStart': {
- seenWarnings.clear();
- seenPhases.clear();
- if (logLevelFilter < logLevels.info) {
- break;
- }
-
- // Clear any previous output
- resetWindow();
-
- if (options.serveOptions) {
- persistMessage(
- chalk.blue.bold(
- `Server running at ${
- options.serveOptions.https ? 'https' : 'http'
- }://${options.serveOptions.host ?? 'localhost'}:${
- options.serveOptions.port
- }`,
- ),
- );
- }
-
- break;
- }
- case 'buildProgress': {
- if (logLevelFilter < logLevels.info) {
- break;
- }
-
- if (pendingIncrementalBuild) {
- pendingIncrementalBuild = false;
- phaseStartTimes = {};
- seenPhasesGen.clear();
- seenPhases.clear();
- }
-
- if (!seenPhasesGen.has(event.phase)) {
- phaseStartTimes[event.phase] = Date.now();
- seenPhasesGen.add(event.phase);
- }
-
- if (!isTTY && logLevelFilter != logLevels.verbose) {
- if (event.phase == 'transforming' && !seenPhases.has('transforming')) {
- updateSpinner('Building...');
- } else if (event.phase == 'bundling' && !seenPhases.has('bundling')) {
- updateSpinner('Bundling...');
- } else if (
- (event.phase == 'packaging' || event.phase == 'optimizing') &&
- !seenPhases.has('packaging') &&
- !seenPhases.has('optimizing')
- ) {
- updateSpinner('Packaging & Optimizing...');
- }
- seenPhases.add(event.phase);
- break;
- }
-
- let message = getProgressMessage(event);
- if (message != null) {
- if (isTTY) {
- statusThrottle(chalk.gray.bold(message));
- } else {
- updateSpinner(message);
- }
- }
- break;
- }
- case 'buildSuccess':
- if (logLevelFilter < logLevels.info) {
- break;
- }
-
- phaseStartTimes['buildSuccess'] = Date.now();
-
- persistSpinner(
- 'buildProgress',
- 'success',
- chalk.green.bold(`Built in ${prettifyTime(event.buildTime)}`),
- );
-
- if (options.mode === 'production') {
- await bundleReport(
- event.bundleGraph,
- options.outputFS,
- options.projectRoot,
- options.detailedReport?.assetsPerBundle,
- );
- } else {
- pendingIncrementalBuild = true;
- }
-
- if (process.env.ATLASPACK_SHOW_PHASE_TIMES) {
- phaseReport(phaseStartTimes);
- }
- break;
- case 'buildFailure':
- if (logLevelFilter < logLevels.error) {
- break;
- }
-
- resetWindow();
-
- persistSpinner('buildProgress', 'error', chalk.red.bold('Build failed.'));
-
- await writeDiagnostic(options, event.diagnostics, 'red', true);
- break;
- case 'cache':
- if (event.size > 500000) {
- switch (event.phase) {
- case 'start':
- updateSpinner('Writing cache to disk');
- break;
- case 'end':
- persistSpinner(
- 'cache',
- 'success',
- chalk.grey.bold(`Cache written to disk`),
- );
- break;
- }
- }
- break;
- case 'log': {
- if (logLevelFilter < logLevels[event.level]) {
- break;
- }
-
- switch (event.level) {
- case 'success':
- writeOut(chalk.green(event.message));
- break;
- case 'progress':
- writeOut(event.message);
- break;
- case 'verbose':
- case 'info':
- await writeDiagnostic(options, event.diagnostics, 'blue');
- break;
- case 'warn':
- if (
- event.diagnostics.some(
- diagnostic => !seenWarnings.has(diagnostic.message),
- )
- ) {
- await writeDiagnostic(options, event.diagnostics, 'yellow', true);
- for (let diagnostic of event.diagnostics) {
- seenWarnings.add(diagnostic.message);
- }
- }
- break;
- case 'error':
- await writeDiagnostic(options, event.diagnostics, 'red', true);
- break;
- default:
- throw new Error('Unknown log level ' + event.level);
- }
- }
- }
-}
-
-async function writeDiagnostic(
- options: PluginOptions,
- diagnostics: Array,
- color: Color,
- isError: boolean = false,
-) {
- let columns = getTerminalWidth().columns;
- let indent = 2;
- let spaceAfter = isError;
- for (let diagnostic of diagnostics) {
- let {message, stack, codeframe, hints, documentation} =
- await prettyDiagnostic(diagnostic, options, columns - indent);
- // $FlowFixMe[incompatible-use]
- message = chalk[color](message);
-
- if (spaceAfter) {
- writeOut('');
- }
-
- if (message) {
- writeOut(wrapWithIndent(message), isError);
- }
-
- if (stack || codeframe) {
- writeOut('');
- }
-
- if (stack) {
- writeOut(chalk.gray(wrapWithIndent(stack, indent)), isError);
- }
-
- if (codeframe) {
- writeOut(indentString(codeframe, indent), isError);
- }
-
- if ((stack || codeframe) && (hints.length > 0 || documentation)) {
- writeOut('');
- }
-
- // Write hints
- let hintIndent = stack || codeframe ? indent : 0;
- for (let hint of hints) {
- writeOut(
- wrapWithIndent(
- `${emoji.hint} ${chalk.blue.bold(hint)}`,
- hintIndent + 3,
- hintIndent,
- ),
- );
- }
-
- if (documentation) {
- writeOut(
- wrapWithIndent(
- `${emoji.docs} ${chalk.magenta.bold(documentation)}`,
- hintIndent + 3,
- hintIndent,
- ),
- );
- }
-
- spaceAfter = stack || codeframe || hints.length > 0 || documentation;
- }
-
- if (spaceAfter) {
- writeOut('');
- }
-}
-
-function wrapWithIndent(string, indent = 0, initialIndent = indent) {
- let width = getTerminalWidth().columns;
- return indentString(
- wrapAnsi(string.trimEnd(), width - indent, {trim: false}),
- indent,
- initialIndent,
- );
-}
-
-function indentString(string, indent = 0, initialIndent = indent) {
- return (
- ' '.repeat(initialIndent) + string.replace(/\n/g, '\n' + ' '.repeat(indent))
- );
-}
-
-export default (new Reporter({
- report({event, options}) {
- return _report(event, options);
- },
-}): Reporter);
diff --git a/packages/reporters/cli/src/CLIReporter.ts b/packages/reporters/cli/src/CLIReporter.ts
new file mode 100644
index 000000000..28325ae02
--- /dev/null
+++ b/packages/reporters/cli/src/CLIReporter.ts
@@ -0,0 +1,302 @@
+import type {ReporterEvent, PluginOptions} from '@atlaspack/types';
+import type {Diagnostic} from '@atlaspack/diagnostic';
+import type {Color} from 'chalk';
+
+import {Reporter} from '@atlaspack/plugin';
+import {
+ getProgressMessage,
+ prettifyTime,
+ prettyDiagnostic,
+ throttle,
+} from '@atlaspack/utils';
+import chalk from 'chalk';
+
+import {getTerminalWidth} from './utils';
+import logLevels from './logLevels';
+import bundleReport from './bundleReport';
+import phaseReport from './phaseReport';
+import {
+ writeOut,
+ updateSpinner,
+ persistSpinner,
+ isTTY,
+ resetWindow,
+ persistMessage,
+} from './render';
+import * as emoji from './emoji';
+// @ts-expect-error - TS7016 - Could not find a declaration file for module 'wrap-ansi'. '/home/ubuntu/parcel/node_modules/wrap-ansi/index.js' implicitly has an 'any' type.
+import wrapAnsi from 'wrap-ansi';
+
+const THROTTLE_DELAY = 100;
+const seenWarnings = new Set();
+const seenPhases = new Set();
+const seenPhasesGen = new Set();
+
+let phaseStartTimes: Record = {};
+let pendingIncrementalBuild = false;
+
+let statusThrottle = throttle((message: string) => {
+ updateSpinner(message);
+}, THROTTLE_DELAY);
+
+// Exported only for test
+export async function _report(
+ event: ReporterEvent,
+ options: PluginOptions,
+): Promise {
+ let logLevelFilter = logLevels[options.logLevel || 'info'];
+
+ switch (event.type) {
+ case 'buildStart': {
+ seenWarnings.clear();
+ seenPhases.clear();
+ if (logLevelFilter < logLevels.info) {
+ break;
+ }
+
+ // Clear any previous output
+ resetWindow();
+
+ if (options.serveOptions) {
+ persistMessage(
+ chalk.blue.bold(
+ `Server running at ${
+ options.serveOptions.https ? 'https' : 'http'
+ }://${options.serveOptions.host ?? 'localhost'}:${
+ options.serveOptions.port
+ }`,
+ ),
+ );
+ }
+
+ break;
+ }
+ case 'buildProgress': {
+ if (logLevelFilter < logLevels.info) {
+ break;
+ }
+
+ if (pendingIncrementalBuild) {
+ pendingIncrementalBuild = false;
+ phaseStartTimes = {};
+ seenPhasesGen.clear();
+ seenPhases.clear();
+ }
+
+ if (!seenPhasesGen.has(event.phase)) {
+ phaseStartTimes[event.phase] = Date.now();
+ seenPhasesGen.add(event.phase);
+ }
+
+ if (!isTTY && logLevelFilter != logLevels.verbose) {
+ if (event.phase == 'transforming' && !seenPhases.has('transforming')) {
+ updateSpinner('Building...');
+ } else if (event.phase == 'bundling' && !seenPhases.has('bundling')) {
+ updateSpinner('Bundling...');
+ } else if (
+ (event.phase == 'packaging' || event.phase == 'optimizing') &&
+ !seenPhases.has('packaging') &&
+ !seenPhases.has('optimizing')
+ ) {
+ updateSpinner('Packaging & Optimizing...');
+ }
+ seenPhases.add(event.phase);
+ break;
+ }
+
+ let message = getProgressMessage(event);
+ if (message != null) {
+ if (isTTY) {
+ statusThrottle(chalk.gray.bold(message));
+ } else {
+ updateSpinner(message);
+ }
+ }
+ break;
+ }
+ case 'buildSuccess':
+ if (logLevelFilter < logLevels.info) {
+ break;
+ }
+
+ phaseStartTimes['buildSuccess'] = Date.now();
+
+ persistSpinner(
+ 'buildProgress',
+ 'success',
+ chalk.green.bold(`Built in ${prettifyTime(event.buildTime)}`),
+ );
+
+ if (options.mode === 'production') {
+ await bundleReport(
+ event.bundleGraph,
+ options.outputFS,
+ options.projectRoot,
+ options.detailedReport?.assetsPerBundle,
+ );
+ } else {
+ pendingIncrementalBuild = true;
+ }
+
+ if (process.env.ATLASPACK_SHOW_PHASE_TIMES) {
+ phaseReport(phaseStartTimes);
+ }
+ break;
+ case 'buildFailure':
+ if (logLevelFilter < logLevels.error) {
+ break;
+ }
+
+ resetWindow();
+
+ persistSpinner('buildProgress', 'error', chalk.red.bold('Build failed.'));
+
+ await writeDiagnostic(options, event.diagnostics, 'red', true);
+ break;
+ case 'cache':
+ if (event.size > 500000) {
+ switch (event.phase) {
+ case 'start':
+ updateSpinner('Writing cache to disk');
+ break;
+ case 'end':
+ persistSpinner(
+ 'cache',
+ 'success',
+ chalk.grey.bold(`Cache written to disk`),
+ );
+ break;
+ }
+ }
+ break;
+ case 'log': {
+ if (logLevelFilter < logLevels[event.level]) {
+ break;
+ }
+
+ switch (event.level) {
+ case 'success':
+ writeOut(chalk.green(event.message));
+ break;
+ case 'progress':
+ writeOut(event.message);
+ break;
+ case 'verbose':
+ case 'info':
+ await writeDiagnostic(options, event.diagnostics, 'blue');
+ break;
+ case 'warn':
+ if (
+ event.diagnostics.some(
+ (diagnostic) => !seenWarnings.has(diagnostic.message),
+ )
+ ) {
+ await writeDiagnostic(options, event.diagnostics, 'yellow', true);
+ for (let diagnostic of event.diagnostics) {
+ seenWarnings.add(diagnostic.message);
+ }
+ }
+ break;
+ case 'error':
+ await writeDiagnostic(options, event.diagnostics, 'red', true);
+ break;
+ default:
+ // @ts-expect-error - TS2339 - Property 'level' does not exist on type 'never'.
+ throw new Error('Unknown log level ' + event.level);
+ }
+ }
+ }
+}
+
+async function writeDiagnostic(
+ options: PluginOptions,
+ diagnostics: Array,
+ // @ts-expect-error - TS2749 - 'Color' refers to a value, but is being used as a type here. Did you mean 'typeof Color'?
+ color: Color,
+ isError: boolean = false,
+) {
+ let columns = getTerminalWidth().columns;
+ let indent = 2;
+ let spaceAfter = isError;
+ for (let diagnostic of diagnostics) {
+ let {message, stack, codeframe, hints, documentation} =
+ await prettyDiagnostic(diagnostic, options, columns - indent);
+ // @ts-expect-error - TS7053 - Element implicitly has an 'any' type because expression of type 'Color' can't be used to index type 'Chalk & ChalkFunction & { supportsColor: false | ColorSupport; Level: Level; Color: Color; ForegroundColor: ForegroundColor; BackgroundColor: BackgroundColor; Modifiers: Modifiers; stderr: Chalk & { ...; }; }'.
+ message = chalk[color](message);
+
+ if (spaceAfter) {
+ writeOut('');
+ }
+
+ if (message) {
+ writeOut(wrapWithIndent(message), isError);
+ }
+
+ if (stack || codeframe) {
+ writeOut('');
+ }
+
+ if (stack) {
+ writeOut(chalk.gray(wrapWithIndent(stack, indent)), isError);
+ }
+
+ if (codeframe) {
+ writeOut(indentString(codeframe, indent), isError);
+ }
+
+ if ((stack || codeframe) && (hints.length > 0 || documentation)) {
+ writeOut('');
+ }
+
+ // Write hints
+ let hintIndent = stack || codeframe ? indent : 0;
+ for (let hint of hints) {
+ writeOut(
+ wrapWithIndent(
+ `${emoji.hint} ${chalk.blue.bold(hint)}`,
+ hintIndent + 3,
+ hintIndent,
+ ),
+ );
+ }
+
+ if (documentation) {
+ writeOut(
+ wrapWithIndent(
+ `${emoji.docs} ${chalk.magenta.bold(documentation)}`,
+ hintIndent + 3,
+ hintIndent,
+ ),
+ );
+ }
+
+ // @ts-expect-error - TS2322 - Type 'string | true' is not assignable to type 'boolean'.
+ spaceAfter = stack || codeframe || hints.length > 0 || documentation;
+ }
+
+ if (spaceAfter) {
+ writeOut('');
+ }
+}
+
+function wrapWithIndent(string: string, indent = 0, initialIndent = indent) {
+ let width = getTerminalWidth().columns;
+ return indentString(
+ wrapAnsi(string.trimEnd(), width - indent, {trim: false}),
+ indent,
+ initialIndent,
+ );
+}
+
+// @ts-expect-error - TS7006 - Parameter 'string' implicitly has an 'any' type.
+function indentString(string, indent = 0, initialIndent = indent) {
+ return (
+ ' '.repeat(initialIndent) + string.replace(/\n/g, '\n' + ' '.repeat(indent))
+ );
+}
+
+export default new Reporter({
+ report({event, options}) {
+ return _report(event, options);
+ },
+}) as Reporter;
diff --git a/packages/reporters/cli/src/bundleReport.js b/packages/reporters/cli/src/bundleReport.js
deleted file mode 100644
index 522b3c7bb..000000000
--- a/packages/reporters/cli/src/bundleReport.js
+++ /dev/null
@@ -1,99 +0,0 @@
-// @flow
-import type {BundleGraph, FilePath, PackagedBundle} from '@atlaspack/types';
-import type {FileSystem} from '@atlaspack/fs';
-
-import {generateBuildMetrics, prettifyTime} from '@atlaspack/utils';
-import filesize from 'filesize';
-import chalk from 'chalk';
-import nullthrows from 'nullthrows';
-
-import * as emoji from './emoji';
-import {writeOut, table} from './render';
-import {formatFilename} from './utils';
-
-const LARGE_BUNDLE_SIZE = 1024 * 1024;
-const COLUMNS = [
- {align: 'left'}, // name
- {align: 'right'}, // size
- {align: 'right'}, // time
-];
-
-export default async function bundleReport(
- bundleGraph: BundleGraph,
- fs: FileSystem,
- projectRoot: FilePath,
- assetCount: number = 0,
-) {
- let bundleList = bundleGraph.getBundles();
-
- // Get a list of bundles sorted by size
- let {bundles} =
- assetCount > 0
- ? await generateBuildMetrics(bundleList, fs, projectRoot)
- : {
- bundles: bundleList.map(b => {
- return {
- filePath: nullthrows(b.filePath),
- size: b.stats.size,
- time: b.stats.time,
- assets: [],
- };
- }),
- };
- let rows = [];
-
- for (let bundle of bundles) {
- // Add a row for the bundle
- rows.push([
- formatFilename(bundle.filePath || '', chalk.cyan.bold),
- chalk.bold(prettifySize(bundle.size, bundle.size > LARGE_BUNDLE_SIZE)),
- chalk.green.bold(prettifyTime(bundle.time)),
- ]);
-
- if (assetCount > 0) {
- let largestAssets = bundle.assets.slice(0, assetCount);
- for (let asset of largestAssets) {
- let columns: Array = [
- asset == largestAssets[largestAssets.length - 1] ? '└── ' : '├── ',
- chalk.dim(prettifySize(asset.size)),
- chalk.dim(chalk.green(prettifyTime(asset.time))),
- ];
-
- if (asset.filePath !== '') {
- columns[0] += formatFilename(asset.filePath, chalk.reset);
- } else {
- columns[0] += 'Code from unknown sourcefiles';
- }
-
- // Add a row for the asset.
- rows.push(columns);
- }
-
- if (bundle.assets.length > largestAssets.length) {
- rows.push([
- '└── ' +
- chalk.dim(
- `+ ${bundle.assets.length - largestAssets.length} more assets`,
- ),
- ]);
- }
-
- // If this isn't the last bundle, add an empty row before the next one
- if (bundle !== bundles[bundles.length - 1]) {
- rows.push([]);
- }
- }
- }
-
- // Render table
- writeOut('');
- table(COLUMNS, rows);
-}
-
-function prettifySize(size, isLarge) {
- let res = filesize(size);
- if (isLarge) {
- return chalk.yellow(emoji.warning + ' ' + res);
- }
- return chalk.magenta(res);
-}
diff --git a/packages/reporters/cli/src/bundleReport.ts b/packages/reporters/cli/src/bundleReport.ts
new file mode 100644
index 000000000..18babde44
--- /dev/null
+++ b/packages/reporters/cli/src/bundleReport.ts
@@ -0,0 +1,100 @@
+import type {BundleGraph, FilePath, PackagedBundle} from '@atlaspack/types';
+import type {FileSystem} from '@atlaspack/fs';
+
+import {generateBuildMetrics, prettifyTime} from '@atlaspack/utils';
+import filesize from 'filesize';
+import chalk from 'chalk';
+import nullthrows from 'nullthrows';
+
+import * as emoji from './emoji';
+import {writeOut, table} from './render';
+import {formatFilename} from './utils';
+
+const LARGE_BUNDLE_SIZE = 1024 * 1024;
+const COLUMNS = [
+ {align: 'left'}, // name
+ {align: 'right'}, // size
+ {align: 'right'}, // time
+];
+
+export default async function bundleReport(
+ bundleGraph: BundleGraph,
+ fs: FileSystem,
+ projectRoot: FilePath,
+ assetCount: number = 0,
+) {
+ let bundleList = bundleGraph.getBundles();
+
+ // Get a list of bundles sorted by size
+ let {bundles} =
+ assetCount > 0
+ ? await generateBuildMetrics(bundleList, fs, projectRoot)
+ : {
+ bundles: bundleList.map((b) => {
+ return {
+ filePath: nullthrows(b.filePath),
+ size: b.stats.size,
+ time: b.stats.time,
+ assets: [],
+ };
+ }),
+ };
+ let rows: Array> = [];
+
+ for (let bundle of bundles) {
+ // Add a row for the bundle
+ rows.push([
+ formatFilename(bundle.filePath || '', chalk.cyan.bold),
+ chalk.bold(prettifySize(bundle.size, bundle.size > LARGE_BUNDLE_SIZE)),
+ chalk.green.bold(prettifyTime(bundle.time)),
+ ]);
+
+ if (assetCount > 0) {
+ let largestAssets = bundle.assets.slice(0, assetCount);
+ for (let asset of largestAssets) {
+ let columns: Array = [
+ asset == largestAssets[largestAssets.length - 1] ? '└── ' : '├── ',
+ // @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
+ chalk.dim(prettifySize(asset.size)),
+ chalk.dim(chalk.green(prettifyTime(asset.time))),
+ ];
+
+ if (asset.filePath !== '') {
+ columns[0] += formatFilename(asset.filePath, chalk.reset);
+ } else {
+ columns[0] += 'Code from unknown sourcefiles';
+ }
+
+ // Add a row for the asset.
+ rows.push(columns);
+ }
+
+ if (bundle.assets.length > largestAssets.length) {
+ rows.push([
+ '└── ' +
+ chalk.dim(
+ `+ ${bundle.assets.length - largestAssets.length} more assets`,
+ ),
+ ]);
+ }
+
+ // If this isn't the last bundle, add an empty row before the next one
+ if (bundle !== bundles[bundles.length - 1]) {
+ rows.push([]);
+ }
+ }
+ }
+
+ // Render table
+ writeOut('');
+ // @ts-expect-error - TS2345 - Argument of type '{ align: string; }[]' is not assignable to parameter of type 'ColumnType[]'.
+ table(COLUMNS, rows);
+}
+
+function prettifySize(size: number, isLarge: undefined | boolean) {
+ let res = filesize(size);
+ if (isLarge) {
+ return chalk.yellow(emoji.warning + ' ' + res);
+ }
+ return chalk.magenta(res);
+}
diff --git a/packages/reporters/cli/src/emoji.js b/packages/reporters/cli/src/emoji.js
deleted file mode 100644
index 6b05521ce..000000000
--- a/packages/reporters/cli/src/emoji.js
+++ /dev/null
@@ -1,29 +0,0 @@
-// @flow strict-local
-
-// From https://github.com/sindresorhus/is-unicode-supported/blob/8f123916d5c25a87c4f966dcc248b7ca5df2b4ca/index.js
-// This package is ESM-only so it has to be vendored
-function isUnicodeSupported() {
- if (process.platform !== 'win32') {
- return process.env.TERM !== 'linux'; // Linux console (kernel)
- }
-
- return (
- Boolean(process.env.CI) ||
- Boolean(process.env.WT_SESSION) || // Windows Terminal
- process.env.ConEmuTask === '{cmd::Cmder}' || // ConEmu and cmder
- process.env.TERM_PROGRAM === 'vscode' ||
- process.env.TERM === 'xterm-256color' ||
- process.env.TERM === 'alacritty'
- );
-}
-
-const supportsEmoji = isUnicodeSupported();
-
-// Fallback symbols for Windows from https://en.wikipedia.org/wiki/Code_page_437
-export const progress: string = supportsEmoji ? '⏳' : '∞';
-export const success: string = supportsEmoji ? '✨' : '√';
-export const error: string = supportsEmoji ? '🚨' : '×';
-export const warning: string = supportsEmoji ? '⚠️' : '‼';
-export const info: string = supportsEmoji ? 'ℹ️' : 'ℹ';
-export const hint: string = supportsEmoji ? '💡' : 'ℹ';
-export const docs: string = supportsEmoji ? '📝' : 'ℹ';
diff --git a/packages/reporters/cli/src/emoji.ts b/packages/reporters/cli/src/emoji.ts
new file mode 100644
index 000000000..e3a53bea1
--- /dev/null
+++ b/packages/reporters/cli/src/emoji.ts
@@ -0,0 +1,27 @@
+// From https://github.com/sindresorhus/is-unicode-supported/blob/8f123916d5c25a87c4f966dcc248b7ca5df2b4ca/index.js
+// This package is ESM-only so it has to be vendored
+function isUnicodeSupported() {
+ if (process.platform !== 'win32') {
+ return process.env.TERM !== 'linux'; // Linux console (kernel)
+ }
+
+ return (
+ Boolean(process.env.CI) ||
+ Boolean(process.env.WT_SESSION) || // Windows Terminal
+ process.env.ConEmuTask === '{cmd::Cmder}' || // ConEmu and cmder
+ process.env.TERM_PROGRAM === 'vscode' ||
+ process.env.TERM === 'xterm-256color' ||
+ process.env.TERM === 'alacritty'
+ );
+}
+
+const supportsEmoji = isUnicodeSupported();
+
+// Fallback symbols for Windows from https://en.wikipedia.org/wiki/Code_page_437
+export const progress: string = supportsEmoji ? '⏳' : '∞';
+export const success: string = supportsEmoji ? '✨' : '√';
+export const error: string = supportsEmoji ? '🚨' : '×';
+export const warning: string = supportsEmoji ? '⚠️' : '‼';
+export const info: string = supportsEmoji ? 'ℹ️' : 'ℹ';
+export const hint: string = supportsEmoji ? '💡' : 'ℹ';
+export const docs: string = supportsEmoji ? '📝' : 'ℹ';
diff --git a/packages/reporters/cli/src/logLevels.js b/packages/reporters/cli/src/logLevels.js
deleted file mode 100644
index db1cf2557..000000000
--- a/packages/reporters/cli/src/logLevels.js
+++ /dev/null
@@ -1,13 +0,0 @@
-// @flow strict-local
-
-const logLevels = {
- none: 0,
- error: 1,
- warn: 2,
- info: 3,
- progress: 3,
- success: 3,
- verbose: 4,
-};
-
-export default logLevels;
diff --git a/packages/reporters/cli/src/logLevels.ts b/packages/reporters/cli/src/logLevels.ts
new file mode 100644
index 000000000..b42d28fc6
--- /dev/null
+++ b/packages/reporters/cli/src/logLevels.ts
@@ -0,0 +1,11 @@
+const logLevels = {
+ none: 0,
+ error: 1,
+ warn: 2,
+ info: 3,
+ progress: 3,
+ success: 3,
+ verbose: 4,
+} as const;
+
+export default logLevels;
diff --git a/packages/reporters/cli/src/phaseReport.js b/packages/reporters/cli/src/phaseReport.js
deleted file mode 100644
index 431d90699..000000000
--- a/packages/reporters/cli/src/phaseReport.js
+++ /dev/null
@@ -1,33 +0,0 @@
-// @flow
-import {prettifyTime} from '@atlaspack/utils';
-import chalk from 'chalk';
-import {writeOut} from './render';
-import invariant from 'assert';
-
-export default function phaseReport(phaseStartTimes: {[string]: number}) {
- let phaseTimes = {};
- if (phaseStartTimes['transforming'] && phaseStartTimes['bundling']) {
- phaseTimes['Transforming'] =
- phaseStartTimes['bundling'] - phaseStartTimes['transforming'];
- }
-
- let packagingAndOptimizing =
- phaseStartTimes['packaging'] && phaseStartTimes['optimizing']
- ? Math.min(phaseStartTimes['packaging'], phaseStartTimes['optimizing'])
- : phaseStartTimes['packaging'] || phaseStartTimes['optimizing'];
-
- if (phaseStartTimes['bundling'] && packagingAndOptimizing) {
- phaseTimes['Bundling'] =
- packagingAndOptimizing - phaseStartTimes['bundling'];
- }
-
- if (packagingAndOptimizing && phaseStartTimes['buildSuccess']) {
- phaseTimes['Packaging & Optimizing'] =
- phaseStartTimes['buildSuccess'] - packagingAndOptimizing;
- }
-
- for (let [phase, time] of Object.entries(phaseTimes)) {
- invariant(typeof time === 'number');
- writeOut(chalk.green.bold(`${phase} finished in ${prettifyTime(time)}`));
- }
-}
diff --git a/packages/reporters/cli/src/phaseReport.ts b/packages/reporters/cli/src/phaseReport.ts
new file mode 100644
index 000000000..e76833ed8
--- /dev/null
+++ b/packages/reporters/cli/src/phaseReport.ts
@@ -0,0 +1,32 @@
+import {prettifyTime} from '@atlaspack/utils';
+import chalk from 'chalk';
+import {writeOut} from './render';
+import invariant from 'assert';
+
+export default function phaseReport(phaseStartTimes: {[key: string]: number}) {
+ let phaseTimes: Record = {};
+ if (phaseStartTimes['transforming'] && phaseStartTimes['bundling']) {
+ phaseTimes['Transforming'] =
+ phaseStartTimes['bundling'] - phaseStartTimes['transforming'];
+ }
+
+ let packagingAndOptimizing =
+ phaseStartTimes['packaging'] && phaseStartTimes['optimizing']
+ ? Math.min(phaseStartTimes['packaging'], phaseStartTimes['optimizing'])
+ : phaseStartTimes['packaging'] || phaseStartTimes['optimizing'];
+
+ if (phaseStartTimes['bundling'] && packagingAndOptimizing) {
+ phaseTimes['Bundling'] =
+ packagingAndOptimizing - phaseStartTimes['bundling'];
+ }
+
+ if (packagingAndOptimizing && phaseStartTimes['buildSuccess']) {
+ phaseTimes['Packaging & Optimizing'] =
+ phaseStartTimes['buildSuccess'] - packagingAndOptimizing;
+ }
+
+ for (let [phase, time] of Object.entries(phaseTimes)) {
+ invariant(typeof time === 'number');
+ writeOut(chalk.green.bold(`${phase} finished in ${prettifyTime(time)}`));
+ }
+}
diff --git a/packages/reporters/cli/src/render.js b/packages/reporters/cli/src/render.js
deleted file mode 100644
index 4cdc6df0e..000000000
--- a/packages/reporters/cli/src/render.js
+++ /dev/null
@@ -1,149 +0,0 @@
-// @flow
-import type {Writable} from 'stream';
-
-import readline from 'readline';
-import ora from 'ora';
-import stringWidth from 'string-width';
-
-import type {PadAlign} from './utils';
-import {pad, countLines} from './utils';
-import * as emoji from './emoji';
-
-type ColumnType = {|
- align: PadAlign,
-|};
-
-export const isTTY: any | boolean | true =
- // $FlowFixMe
- process.env.NODE_ENV !== 'test' && process.stdout.isTTY;
-
-let stdout = process.stdout;
-let stderr = process.stderr;
-
-// Some state so we clear the output properly
-let lineCount = 0;
-let errorLineCount = 0;
-let statusPersisted = false;
-
-export function _setStdio(stdoutLike: Writable, stderrLike: Writable) {
- stdout = stdoutLike;
- stderr = stderrLike;
-}
-
-let spinner = ora({
- color: 'green',
- stream: stdout,
- discardStdin: false,
-});
-let persistedMessages = [];
-
-export function writeOut(message: string, isError: boolean = false) {
- let processedMessage = message + '\n';
- let hasSpinner = spinner.isSpinning;
-
- // Stop spinner so we don't duplicate it
- if (hasSpinner) {
- spinner.stop();
- }
-
- let lines = countLines(message);
- if (isError) {
- stderr.write(processedMessage);
- errorLineCount += lines;
- } else {
- stdout.write(processedMessage);
- lineCount += lines;
- }
-
- // Restart the spinner
- if (hasSpinner) {
- spinner.start();
- }
-}
-
-export function persistMessage(message: string) {
- if (persistedMessages.includes(message)) return;
-
- persistedMessages.push(message);
- writeOut(message);
-}
-
-export function updateSpinner(message: string) {
- // This helps the spinner play well with the tests
- if (!isTTY) {
- writeOut(message);
- return;
- }
-
- spinner.text = message + '\n';
- if (!spinner.isSpinning) {
- spinner.start();
- }
-}
-
-export function persistSpinner(
- name: string,
- status: 'success' | 'error',
- message: string,
-) {
- spinner.stopAndPersist({
- symbol: emoji[status],
- text: message,
- });
-
- statusPersisted = true;
-}
-
-function clearStream(stream: Writable, lines: number) {
- if (!isTTY) return;
-
- readline.moveCursor(stream, 0, -lines);
- readline.clearScreenDown(stream);
-}
-
-// Reset the window's state
-export function resetWindow() {
- if (!isTTY) return;
-
- // If status has been persisted we add a line
- // Otherwise final states would remain in the terminal for rebuilds
- if (statusPersisted) {
- lineCount++;
- statusPersisted = false;
- }
-
- clearStream(stderr, errorLineCount);
- errorLineCount = 0;
-
- clearStream(stdout, lineCount);
- lineCount = 0;
-
- for (let m of persistedMessages) {
- writeOut(m);
- }
-}
-
-export function table(columns: Array, table: Array>) {
- // Measure column widths
- let colWidths = [];
- for (let row of table) {
- let i = 0;
- for (let item of row) {
- colWidths[i] = Math.max(colWidths[i] || 0, stringWidth(item));
- i++;
- }
- }
-
- // Render rows
- for (let row of table) {
- let items = row.map((item, i) => {
- // Add padding between columns unless the alignment is the opposite to the
- // next column and pad to the column width.
- let padding =
- !columns[i + 1] || columns[i + 1].align === columns[i].align ? 4 : 0;
- return pad(item, colWidths[i] + padding, columns[i].align);
- });
-
- writeOut(items.join(''));
- }
-}
diff --git a/packages/reporters/cli/src/render.ts b/packages/reporters/cli/src/render.ts
new file mode 100644
index 000000000..510a556a8
--- /dev/null
+++ b/packages/reporters/cli/src/render.ts
@@ -0,0 +1,150 @@
+import type {Writable} from 'stream';
+
+import readline from 'readline';
+import ora from 'ora';
+import stringWidth from 'string-width';
+
+import type {PadAlign} from './utils';
+import {pad, countLines} from './utils';
+import * as emoji from './emoji';
+
+type ColumnType = {
+ align: PadAlign;
+};
+
+export const isTTY: any | boolean | true =
+ // $FlowFixMe
+ process.env.NODE_ENV !== 'test' && process.stdout.isTTY;
+
+let stdout = process.stdout;
+let stderr = process.stderr;
+
+// Some state so we clear the output properly
+let lineCount = 0;
+let errorLineCount = 0;
+let statusPersisted = false;
+
+export function _setStdio(stdoutLike: Writable, stderrLike: Writable) {
+ // @ts-expect-error - TS2322 - Type 'Writable' is not assignable to type 'WriteStream & { fd: 1; }'.
+ stdout = stdoutLike;
+ // @ts-expect-error - TS2322 - Type 'Writable' is not assignable to type 'WriteStream & { fd: 2; }'.
+ stderr = stderrLike;
+}
+
+let spinner = ora({
+ color: 'green',
+ stream: stdout,
+ discardStdin: false,
+});
+let persistedMessages: Array = [];
+
+export function writeOut(message: string, isError: boolean = false) {
+ let processedMessage = message + '\n';
+ let hasSpinner = spinner.isSpinning;
+
+ // Stop spinner so we don't duplicate it
+ if (hasSpinner) {
+ spinner.stop();
+ }
+
+ let lines = countLines(message);
+ if (isError) {
+ stderr.write(processedMessage);
+ errorLineCount += lines;
+ } else {
+ stdout.write(processedMessage);
+ lineCount += lines;
+ }
+
+ // Restart the spinner
+ if (hasSpinner) {
+ spinner.start();
+ }
+}
+
+export function persistMessage(message: string) {
+ if (persistedMessages.includes(message)) return;
+
+ persistedMessages.push(message);
+ writeOut(message);
+}
+
+export function updateSpinner(message: string) {
+ // This helps the spinner play well with the tests
+ if (!isTTY) {
+ writeOut(message);
+ return;
+ }
+
+ spinner.text = message + '\n';
+ if (!spinner.isSpinning) {
+ spinner.start();
+ }
+}
+
+export function persistSpinner(
+ name: string,
+ status: 'success' | 'error',
+ message: string,
+) {
+ spinner.stopAndPersist({
+ symbol: emoji[status],
+ text: message,
+ });
+
+ statusPersisted = true;
+}
+
+function clearStream(stream: Writable, lines: number) {
+ if (!isTTY) return;
+
+ readline.moveCursor(stream, 0, -lines);
+ readline.clearScreenDown(stream);
+}
+
+// Reset the window's state
+export function resetWindow() {
+ if (!isTTY) return;
+
+ // If status has been persisted we add a line
+ // Otherwise final states would remain in the terminal for rebuilds
+ if (statusPersisted) {
+ lineCount++;
+ statusPersisted = false;
+ }
+
+ clearStream(stderr, errorLineCount);
+ errorLineCount = 0;
+
+ clearStream(stdout, lineCount);
+ lineCount = 0;
+
+ for (let m of persistedMessages) {
+ writeOut(m);
+ }
+}
+
+export function table(columns: Array, table: Array>) {
+ // Measure column widths
+ let colWidths: Array = [];
+ for (let row of table) {
+ let i = 0;
+ for (let item of row) {
+ colWidths[i] = Math.max(colWidths[i] || 0, stringWidth(item));
+ i++;
+ }
+ }
+
+ // Render rows
+ for (let row of table) {
+ let items = row.map((item, i) => {
+ // Add padding between columns unless the alignment is the opposite to the
+ // next column and pad to the column width.
+ let padding =
+ !columns[i + 1] || columns[i + 1].align === columns[i].align ? 4 : 0;
+ return pad(item, colWidths[i] + padding, columns[i].align);
+ });
+
+ writeOut(items.join(''));
+ }
+}
diff --git a/packages/reporters/cli/src/utils.js b/packages/reporters/cli/src/utils.js
deleted file mode 100644
index c63360d7f..000000000
--- a/packages/reporters/cli/src/utils.js
+++ /dev/null
@@ -1,48 +0,0 @@
-// @flow
-import path from 'path';
-import chalk from 'chalk';
-import stringWidth from 'string-width';
-import termSize from 'term-size';
-import {stripAnsi} from '@atlaspack/utils';
-
-export type PadAlign = 'left' | 'right';
-let terminalSize = termSize();
-process.stdout.on('resize', function () {
- terminalSize = termSize();
-});
-
-export function getTerminalWidth(): any {
- return terminalSize;
-}
-
-// Pad a string with spaces on either side
-export function pad(
- text: string,
- length: number,
- align: PadAlign = 'left',
-): string {
- let pad = ' '.repeat(length - stringWidth(text));
- if (align === 'right') {
- return pad + text;
- }
-
- return text + pad;
-}
-
-export function formatFilename(
- filename: string,
- color: (s: string) => string = chalk.reset,
-): string {
- let dir = path.relative(process.cwd(), path.dirname(filename));
- return (
- chalk.dim(dir + (dir ? path.sep : '')) + color(path.basename(filename))
- );
-}
-
-export function countLines(message: string): number {
- let {columns} = terminalSize;
-
- return stripAnsi(message)
- .split('\n')
- .reduce((p, line) => p + Math.ceil((stringWidth(line) || 1) / columns), 0);
-}
diff --git a/packages/reporters/cli/src/utils.ts b/packages/reporters/cli/src/utils.ts
new file mode 100644
index 000000000..05a9cb368
--- /dev/null
+++ b/packages/reporters/cli/src/utils.ts
@@ -0,0 +1,47 @@
+import path from 'path';
+import chalk from 'chalk';
+import stringWidth from 'string-width';
+import termSize from 'term-size';
+import {stripAnsi} from '@atlaspack/utils';
+
+export type PadAlign = 'left' | 'right';
+let terminalSize = termSize();
+process.stdout.on('resize', function () {
+ terminalSize = termSize();
+});
+
+export function getTerminalWidth(): any {
+ return terminalSize;
+}
+
+// Pad a string with spaces on either side
+export function pad(
+ text: string,
+ length: number,
+ align: PadAlign = 'left',
+): string {
+ let pad = ' '.repeat(length - stringWidth(text));
+ if (align === 'right') {
+ return pad + text;
+ }
+
+ return text + pad;
+}
+
+export function formatFilename(
+ filename: string,
+ color: (s: string) => string = chalk.reset,
+): string {
+ let dir = path.relative(process.cwd(), path.dirname(filename));
+ return (
+ chalk.dim(dir + (dir ? path.sep : '')) + color(path.basename(filename))
+ );
+}
+
+export function countLines(message: string): number {
+ let {columns} = terminalSize;
+
+ return stripAnsi(message)
+ .split('\n')
+ .reduce((p, line) => p + Math.ceil((stringWidth(line) || 1) / columns), 0);
+}
diff --git a/packages/reporters/cli/test/CLIReporter.test.js b/packages/reporters/cli/test/CLIReporter.test.js
deleted file mode 100644
index 67ba1ab1f..000000000
--- a/packages/reporters/cli/test/CLIReporter.test.js
+++ /dev/null
@@ -1,257 +0,0 @@
-// @flow strict-local
-
-import assert from 'assert';
-import sinon from 'sinon';
-import {PassThrough} from 'stream';
-import {_report} from '../src/CLIReporter';
-import * as render from '../src/render';
-import {_setStdio} from '../src/render';
-import {inputFS, outputFS} from '@atlaspack/test-utils';
-import {NodePackageManager} from '@atlaspack/package-manager';
-import stripAnsi from 'strip-ansi';
-import * as bundleReport from '../src/bundleReport';
-import {DEFAULT_FEATURE_FLAGS} from '@atlaspack/feature-flags';
-
-const EMPTY_OPTIONS = {
- cacheDir: '.parcel-cache',
- parcelVersion: '',
- entries: [],
- logLevel: 'info',
- targets: [],
- projectRoot: '',
- distDir: 'dist',
- lockFile: undefined,
- shouldAutoInstall: false,
- shouldBuildLazily: false,
- hmrOptions: undefined,
- serveOptions: false,
- mode: 'development',
- shouldScopeHoist: false,
- shouldOptimize: false,
- env: {},
- shouldDisableCache: false,
- sourceMaps: false,
- inputFS,
- outputFS,
- instanceId: 'test',
- packageManager: new NodePackageManager(inputFS, '/'),
- detailedReport: {
- assetsPerBundle: 10,
- },
- featureFlags: DEFAULT_FEATURE_FLAGS,
-};
-
-describe('CLIReporter', () => {
- let originalStdout;
- let originalStderr;
- let stdoutOutput;
- let stderrOutput;
-
- beforeEach(async () => {
- // Stub these out to avoid writing noise to real stdio and to read from these
- // otherwise only writable streams
- originalStdout = process.stdout;
- originalStderr = process.stderr;
-
- stdoutOutput = '';
- stderrOutput = '';
-
- let mockStdout = new PassThrough();
- mockStdout.on('data', d => (stdoutOutput += stripAnsi(d.toString())));
- let mockStderr = new PassThrough();
- mockStderr.on('data', d => (stderrOutput += stripAnsi(d.toString())));
- _setStdio(mockStdout, mockStderr);
-
- await _report(
- {
- type: 'buildStart',
- },
- EMPTY_OPTIONS,
- );
- });
-
- afterEach(() => {
- _setStdio(originalStdout, originalStderr);
- });
-
- it('writes log, info, success, and verbose log messages to stdout', async () => {
- let options = {
- ...EMPTY_OPTIONS,
- logLevel: 'verbose',
- };
-
- await _report(
- {
- type: 'log',
- level: 'info',
- diagnostics: [
- {
- origin: 'test',
- message: 'info',
- },
- ],
- },
- options,
- );
- await _report({type: 'log', level: 'success', message: 'success'}, options);
- await _report(
- {
- type: 'log',
- level: 'verbose',
- diagnostics: [
- {
- origin: 'test',
- message: 'verbose',
- },
- ],
- },
- options,
- );
-
- assert.equal(stdoutOutput, 'test: info\nsuccess\ntest: verbose\n');
- });
-
- it('writes errors and warnings to stderr', async () => {
- await _report(
- {
- type: 'log',
- level: 'error',
- diagnostics: [
- {
- origin: 'test',
- message: 'error',
- },
- ],
- },
- EMPTY_OPTIONS,
- );
- await _report(
- {
- type: 'log',
- level: 'warn',
- diagnostics: [
- {
- origin: 'test',
- message: 'warn',
- },
- ],
- },
- EMPTY_OPTIONS,
- );
-
- assert.equal(stdoutOutput, '\n\n');
- assert.equal(stderrOutput, 'test: error\ntest: warn\n');
- });
-
- it('prints errors nicely', async () => {
- await _report(
- {
- type: 'log',
- level: 'error',
- diagnostics: [
- {
- origin: 'test',
- message: 'error',
- },
- ],
- },
- EMPTY_OPTIONS,
- );
- await _report(
- {
- type: 'log',
- level: 'warn',
- diagnostics: [
- {
- origin: 'test',
- message: 'warn',
- },
- ],
- },
- EMPTY_OPTIONS,
- );
-
- assert.equal(stdoutOutput, '\n\n');
- assert(stderrOutput.includes('test: error\n'));
- assert(stderrOutput.includes('test: warn\n'));
- });
-
- it('writes buildProgress messages to stdout on the default loglevel', async () => {
- await _report({type: 'buildProgress', phase: 'bundling'}, EMPTY_OPTIONS);
- assert.equal(stdoutOutput, 'Bundling...\n');
- });
-
- it('writes buildSuccess messages to stdout on the default loglevel', async () => {
- await _report({type: 'buildProgress', phase: 'bundling'}, EMPTY_OPTIONS);
- assert.equal(stdoutOutput, 'Bundling...\n');
- });
-
- it('writes phase timings to stdout when ATLASPACK_SHOW_PHASE_TIMES is set', async () => {
- let oldPhaseTimings = process.env['ATLASPACK_SHOW_PHASE_TIMES'];
- const bundleReportStub = sinon.stub(bundleReport, 'default');
- const persistSpinnerStub = sinon.stub(render, 'persistSpinner');
-
- after(() => {
- bundleReportStub.restore();
- persistSpinnerStub.restore();
- process.env['ATLASPACK_SHOW_PHASE_TIMES'] = oldPhaseTimings;
- });
-
- // emit a buildSuccess event to reset the timings and seen phases
- // from the previous test
- process.env['ATLASPACK_SHOW_PHASE_TIMES'] = undefined;
- // $FlowFixMe
- await _report({type: 'buildSuccess'}, EMPTY_OPTIONS);
-
- process.env['ATLASPACK_SHOW_PHASE_TIMES'] = 'true';
- await _report(
- {type: 'buildProgress', phase: 'transforming', filePath: 'foo.js'},
- EMPTY_OPTIONS,
- );
- await _report({type: 'buildProgress', phase: 'bundling'}, EMPTY_OPTIONS);
- await _report(
- // $FlowFixMe
- {
- type: 'buildProgress',
- phase: 'packaging',
- bundle: {
- displayName: 'test',
- },
- },
- EMPTY_OPTIONS,
- );
- // $FlowFixMe
- await _report({type: 'buildSuccess'}, EMPTY_OPTIONS);
- const expected =
- /Building...\nBundling...\nPackaging & Optimizing...\nTransforming finished in [0-9]ms\nBundling finished in [0-9]ms\nPackaging & Optimizing finished in [0-9]ms/;
-
- assert.equal(expected.test(stdoutOutput), true);
-
- stdoutOutput = '';
-
- await _report(
- {type: 'buildProgress', phase: 'transforming', filePath: 'foo.js'},
- EMPTY_OPTIONS,
- );
- await _report({type: 'buildProgress', phase: 'bundling'}, EMPTY_OPTIONS);
- await _report(
- // $FlowFixMe
- {
- type: 'buildProgress',
- phase: 'packaging',
- bundle: {
- displayName: 'test',
- },
- },
- EMPTY_OPTIONS,
- );
- // $FlowFixMe
- await _report({type: 'buildSuccess'}, EMPTY_OPTIONS);
-
- assert.equal(
- expected.test(stdoutOutput),
- true,
- 'STDOUT output did not match',
- );
- });
-});
diff --git a/packages/reporters/cli/test/CLIReporter.test.ts b/packages/reporters/cli/test/CLIReporter.test.ts
new file mode 100644
index 000000000..86aae92c7
--- /dev/null
+++ b/packages/reporters/cli/test/CLIReporter.test.ts
@@ -0,0 +1,269 @@
+import assert from 'assert';
+// @ts-expect-error - TS7016 - Could not find a declaration file for module 'sinon'. '/home/ubuntu/parcel/node_modules/sinon/lib/sinon.js' implicitly has an 'any' type.
+import sinon from 'sinon';
+import {PassThrough} from 'stream';
+import {_report} from '../src/CLIReporter';
+import * as render from '../src/render';
+import {_setStdio} from '../src/render';
+import {inputFS, outputFS} from '@atlaspack/test-utils';
+import {NodePackageManager} from '@atlaspack/package-manager';
+import stripAnsi from 'strip-ansi';
+import * as bundleReport from '../src/bundleReport';
+import {DEFAULT_FEATURE_FLAGS} from '@atlaspack/feature-flags';
+
+const EMPTY_OPTIONS = {
+ cacheDir: '.parcel-cache',
+ parcelVersion: '',
+ entries: [],
+ logLevel: 'info',
+ targets: [],
+ projectRoot: '',
+ distDir: 'dist',
+ lockFile: undefined,
+ shouldAutoInstall: false,
+ shouldBuildLazily: false,
+ hmrOptions: undefined,
+ serveOptions: false,
+ mode: 'development',
+ shouldScopeHoist: false,
+ shouldOptimize: false,
+ env: {},
+ shouldDisableCache: false,
+ sourceMaps: false,
+ inputFS,
+ outputFS,
+ instanceId: 'test',
+ packageManager: new NodePackageManager(inputFS, '/'),
+ detailedReport: {
+ assetsPerBundle: 10,
+ },
+ featureFlags: DEFAULT_FEATURE_FLAGS,
+} as const;
+
+describe('CLIReporter', () => {
+ let originalStdout: any;
+ let originalStderr: any;
+ let stdoutOutput: any;
+ let stderrOutput: any;
+
+ beforeEach(async () => {
+ // Stub these out to avoid writing noise to real stdio and to read from these
+ // otherwise only writable streams
+ originalStdout = process.stdout;
+ originalStderr = process.stderr;
+
+ stdoutOutput = '';
+ stderrOutput = '';
+
+ let mockStdout = new PassThrough();
+ mockStdout.on(
+ 'data',
+ (d: any) => (stdoutOutput += stripAnsi(d.toString())),
+ );
+ let mockStderr = new PassThrough();
+ mockStderr.on(
+ 'data',
+ (d: any) => (stderrOutput += stripAnsi(d.toString())),
+ );
+ _setStdio(mockStdout, mockStderr);
+
+ await _report(
+ {
+ type: 'buildStart',
+ },
+ EMPTY_OPTIONS,
+ );
+ });
+
+ afterEach(() => {
+ _setStdio(originalStdout, originalStderr);
+ });
+
+ it('writes log, info, success, and verbose log messages to stdout', async () => {
+ let options = {
+ ...EMPTY_OPTIONS,
+ logLevel: 'verbose',
+ };
+
+ await _report(
+ {
+ type: 'log',
+ level: 'info',
+ diagnostics: [
+ {
+ origin: 'test',
+ message: 'info',
+ },
+ ],
+ },
+ // @ts-expect-error - TS2345 - Argument of type '{ logLevel: string; cacheDir: ".parcel-cache"; parcelVersion: ""; entries: readonly []; targets: readonly []; projectRoot: ""; distDir: "dist"; lockFile: undefined; shouldAutoInstall: false; shouldBuildLazily: false; ... 13 more ...; featureFlags: FeatureFlags; }' is not assignable to parameter of type 'PluginOptions'.
+ options,
+ );
+ // @ts-expect-error - TS2345 - Argument of type '{ logLevel: string; cacheDir: ".parcel-cache"; parcelVersion: ""; entries: readonly []; targets: readonly []; projectRoot: ""; distDir: "dist"; lockFile: undefined; shouldAutoInstall: false; shouldBuildLazily: false; ... 13 more ...; featureFlags: FeatureFlags; }' is not assignable to parameter of type 'PluginOptions'.
+ await _report({type: 'log', level: 'success', message: 'success'}, options);
+ await _report(
+ {
+ type: 'log',
+ level: 'verbose',
+ diagnostics: [
+ {
+ origin: 'test',
+ message: 'verbose',
+ },
+ ],
+ },
+ // @ts-expect-error - TS2345 - Argument of type '{ logLevel: string; cacheDir: ".parcel-cache"; parcelVersion: ""; entries: readonly []; targets: readonly []; projectRoot: ""; distDir: "dist"; lockFile: undefined; shouldAutoInstall: false; shouldBuildLazily: false; ... 13 more ...; featureFlags: FeatureFlags; }' is not assignable to parameter of type 'PluginOptions'.
+ options,
+ );
+
+ assert.equal(stdoutOutput, 'test: info\nsuccess\ntest: verbose\n');
+ });
+
+ it('writes errors and warnings to stderr', async () => {
+ await _report(
+ {
+ type: 'log',
+ level: 'error',
+ diagnostics: [
+ {
+ origin: 'test',
+ message: 'error',
+ },
+ ],
+ },
+ EMPTY_OPTIONS,
+ );
+ await _report(
+ {
+ type: 'log',
+ level: 'warn',
+ diagnostics: [
+ {
+ origin: 'test',
+ message: 'warn',
+ },
+ ],
+ },
+ EMPTY_OPTIONS,
+ );
+
+ assert.equal(stdoutOutput, '\n\n');
+ assert.equal(stderrOutput, 'test: error\ntest: warn\n');
+ });
+
+ it('prints errors nicely', async () => {
+ await _report(
+ {
+ type: 'log',
+ level: 'error',
+ diagnostics: [
+ {
+ origin: 'test',
+ message: 'error',
+ },
+ ],
+ },
+ EMPTY_OPTIONS,
+ );
+ await _report(
+ {
+ type: 'log',
+ level: 'warn',
+ diagnostics: [
+ {
+ origin: 'test',
+ message: 'warn',
+ },
+ ],
+ },
+ EMPTY_OPTIONS,
+ );
+
+ assert.equal(stdoutOutput, '\n\n');
+ assert(stderrOutput.includes('test: error\n'));
+ assert(stderrOutput.includes('test: warn\n'));
+ });
+
+ it('writes buildProgress messages to stdout on the default loglevel', async () => {
+ await _report({type: 'buildProgress', phase: 'bundling'}, EMPTY_OPTIONS);
+ assert.equal(stdoutOutput, 'Bundling...\n');
+ });
+
+ it('writes buildSuccess messages to stdout on the default loglevel', async () => {
+ await _report({type: 'buildProgress', phase: 'bundling'}, EMPTY_OPTIONS);
+ assert.equal(stdoutOutput, 'Bundling...\n');
+ });
+
+ it('writes phase timings to stdout when ATLASPACK_SHOW_PHASE_TIMES is set', async () => {
+ let oldPhaseTimings = process.env['ATLASPACK_SHOW_PHASE_TIMES'];
+ const bundleReportStub = sinon.stub(bundleReport, 'default');
+ const persistSpinnerStub = sinon.stub(render, 'persistSpinner');
+
+ after(() => {
+ bundleReportStub.restore();
+ persistSpinnerStub.restore();
+ process.env['ATLASPACK_SHOW_PHASE_TIMES'] = oldPhaseTimings;
+ });
+
+ // emit a buildSuccess event to reset the timings and seen phases
+ // from the previous test
+ process.env['ATLASPACK_SHOW_PHASE_TIMES'] = undefined;
+ // @ts-expect-error - TS2345 - Argument of type '{ type: "buildSuccess"; }' is not assignable to parameter of type 'ReporterEvent'.
+ await _report({type: 'buildSuccess'}, EMPTY_OPTIONS);
+
+ process.env['ATLASPACK_SHOW_PHASE_TIMES'] = 'true';
+ await _report(
+ {type: 'buildProgress', phase: 'transforming', filePath: 'foo.js'},
+ EMPTY_OPTIONS,
+ );
+ await _report({type: 'buildProgress', phase: 'bundling'}, EMPTY_OPTIONS);
+ await _report(
+ // $FlowFixMe
+ {
+ type: 'buildProgress',
+ // @ts-expect-error - TS2322 - Type '"packaging"' is not assignable to type '"optimizing"'.
+ phase: 'packaging',
+ // @ts-expect-error - TS2740 - Type '{ displayName: string; }' is missing the following properties from type 'NamedBundle': publicId, name, id, type, and 12 more.
+ bundle: {
+ displayName: 'test',
+ },
+ },
+ EMPTY_OPTIONS,
+ );
+ // @ts-expect-error - TS2345 - Argument of type '{ type: "buildSuccess"; }' is not assignable to parameter of type 'ReporterEvent'.
+ await _report({type: 'buildSuccess'}, EMPTY_OPTIONS);
+ const expected =
+ /Building...\nBundling...\nPackaging & Optimizing...\nTransforming finished in [0-9]ms\nBundling finished in [0-9]ms\nPackaging & Optimizing finished in [0-9]ms/;
+
+ assert.equal(expected.test(stdoutOutput), true);
+
+ stdoutOutput = '';
+
+ await _report(
+ {type: 'buildProgress', phase: 'transforming', filePath: 'foo.js'},
+ EMPTY_OPTIONS,
+ );
+ await _report({type: 'buildProgress', phase: 'bundling'}, EMPTY_OPTIONS);
+ await _report(
+ // $FlowFixMe
+ {
+ type: 'buildProgress',
+ // @ts-expect-error - TS2322 - Type '"packaging"' is not assignable to type '"optimizing"'.
+ phase: 'packaging',
+ // @ts-expect-error - TS2740 - Type '{ displayName: string; }' is missing the following properties from type 'NamedBundle': publicId, name, id, type, and 12 more.
+ bundle: {
+ displayName: 'test',
+ },
+ },
+ EMPTY_OPTIONS,
+ );
+ // @ts-expect-error - TS2345 - Argument of type '{ type: "buildSuccess"; }' is not assignable to parameter of type 'ReporterEvent'.
+ await _report({type: 'buildSuccess'}, EMPTY_OPTIONS);
+
+ assert.equal(
+ expected.test(stdoutOutput),
+ true,
+ 'STDOUT output did not match',
+ );
+ });
+});
diff --git a/packages/reporters/dev-server-sw/package.json b/packages/reporters/dev-server-sw/package.json
index 07eee7eb6..940ff8c82 100644
--- a/packages/reporters/dev-server-sw/package.json
+++ b/packages/reporters/dev-server-sw/package.json
@@ -9,7 +9,8 @@
"url": "https://github.com/atlassian-labs/atlaspack.git"
},
"main": "lib/ServerReporter.js",
- "source": "src/ServerReporter.js",
+ "types": "src/ServerReporter.ts",
+ "source": "src/ServerReporter.ts",
"engines": {
"node": ">= 16.0.0",
"parcel": "^2.11.0"
diff --git a/packages/reporters/dev-server-sw/src/HMRServer.js b/packages/reporters/dev-server-sw/src/HMRServer.js
deleted file mode 100644
index 092bd1c6b..000000000
--- a/packages/reporters/dev-server-sw/src/HMRServer.js
+++ /dev/null
@@ -1,187 +0,0 @@
-// @flow
-
-import type {
- BuildSuccessEvent,
- Dependency,
- PluginOptions,
- BundleGraph,
- PackagedBundle,
- Asset,
-} from '@atlaspack/types';
-import type {Diagnostic} from '@atlaspack/diagnostic';
-import type {AnsiDiagnosticResult} from '@atlaspack/utils';
-
-import invariant from 'assert';
-import {ansiHtml, prettyDiagnostic, PromiseQueue} from '@atlaspack/utils';
-
-const HMR_ENDPOINT = '/__parcel_hmr/';
-
-type HMRAsset = {|
- id: string,
- url: string,
- type: string,
- output: string,
- envHash: string,
- depsByBundle: {[string]: {[string]: string, ...}, ...},
-|};
-
-export type HMRMessage =
- | {|
- type: 'update',
- assets: Array,
- |}
- | {|
- type: 'error',
- diagnostics: {|
- ansi: Array,
- html: Array<$Rest>,
- |},
- |};
-
-const FS_CONCURRENCY = 64;
-
-export default class HMRServer {
- unresolvedError: HMRMessage | null = null;
- broadcast: HMRMessage => void;
-
- constructor(broadcast: HMRMessage => void) {
- this.broadcast = broadcast;
- }
-
- async emitError(options: PluginOptions, diagnostics: Array) {
- let renderedDiagnostics = await Promise.all(
- diagnostics.map(d => prettyDiagnostic(d, options)),
- );
-
- // store the most recent error so we can notify new connections
- // and so we can broadcast when the error is resolved
- this.unresolvedError = {
- type: 'error',
- diagnostics: {
- ansi: renderedDiagnostics,
- html: renderedDiagnostics.map((d, i) => {
- return {
- message: ansiHtml(d.message),
- stack: ansiHtml(d.stack),
- frames: d.frames.map(f => ({
- location: f.location,
- code: ansiHtml(f.code),
- })),
- hints: d.hints.map(hint => ansiHtml(hint)),
- documentation: diagnostics[i].documentationURL ?? '',
- };
- }),
- },
- };
-
- this.broadcast(this.unresolvedError);
- }
-
- async emitUpdate(event: BuildSuccessEvent) {
- this.unresolvedError = null;
-
- let changedAssets = new Set(event.changedAssets.values());
- if (changedAssets.size === 0) return;
-
- let queue = new PromiseQueue({maxConcurrent: FS_CONCURRENCY});
- for (let asset of changedAssets) {
- if (asset.type !== 'js' && asset.type !== 'css') {
- // If all of the incoming dependencies of the asset actually resolve to a JS asset
- // rather than the original, we can mark the runtimes as changed instead. URL runtimes
- // have a cache busting query param added with HMR enabled which will trigger a reload.
- let runtimes = new Set();
- let incomingDeps = event.bundleGraph.getIncomingDependencies(asset);
- let isOnlyReferencedByRuntimes = incomingDeps.every(dep => {
- let resolved = event.bundleGraph.getResolvedAsset(dep);
- let isRuntime = resolved?.type === 'js' && resolved !== asset;
- if (resolved && isRuntime) {
- runtimes.add(resolved);
- }
- return isRuntime;
- });
-
- if (isOnlyReferencedByRuntimes) {
- for (let runtime of runtimes) {
- changedAssets.add(runtime);
- }
-
- continue;
- }
- }
-
- queue.add(async () => {
- let dependencies = event.bundleGraph.getDependencies(asset);
- let depsByBundle = {};
- for (let bundle of event.bundleGraph.getBundlesWithAsset(asset)) {
- let deps = {};
- for (let dep of dependencies) {
- let resolved = event.bundleGraph.getResolvedAsset(dep, bundle);
- if (resolved) {
- deps[getSpecifier(dep)] =
- event.bundleGraph.getAssetPublicId(resolved);
- }
- }
- depsByBundle[bundle.id] = deps;
- }
-
- return {
- id: event.bundleGraph.getAssetPublicId(asset),
- url: getSourceURL(event.bundleGraph, asset),
- type: asset.type,
- // No need to send the contents of non-JS assets to the client.
- output:
- asset.type === 'js'
- ? await getHotAssetContents(event.bundleGraph, asset)
- : '',
- envHash: asset.env.id,
- depsByBundle,
- };
- });
- }
-
- let assets = await queue.run();
- this.broadcast({
- type: 'update',
- assets: assets,
- });
- }
-}
-
-function getSpecifier(dep: Dependency): string {
- if (typeof dep.meta.placeholder === 'string') {
- return dep.meta.placeholder;
- }
-
- return dep.specifier;
-}
-
-export async function getHotAssetContents(
- bundleGraph: BundleGraph,
- asset: Asset,
-): Promise {
- let output = await asset.getCode();
- if (asset.type === 'js') {
- let publicId = bundleGraph.getAssetPublicId(asset);
- output = `parcelHotUpdate['${publicId}'] = function (require, module, exports) {${output}}`;
- }
-
- let sourcemap = await asset.getMap();
- if (sourcemap) {
- let sourcemapStringified = await sourcemap.stringify({
- format: 'inline',
- sourceRoot: '/__parcel_source_root/',
- // $FlowFixMe
- fs: asset.fs,
- });
-
- invariant(typeof sourcemapStringified === 'string');
- output += `\n//# sourceMappingURL=${sourcemapStringified}`;
- output += `\n//# sourceURL=${getSourceURL(bundleGraph, asset)}\n`;
- }
-
- return output;
-}
-
-function getSourceURL(bundleGraph, asset) {
- return HMR_ENDPOINT + asset.id;
-}
diff --git a/packages/reporters/dev-server-sw/src/HMRServer.ts b/packages/reporters/dev-server-sw/src/HMRServer.ts
new file mode 100644
index 000000000..3f0832075
--- /dev/null
+++ b/packages/reporters/dev-server-sw/src/HMRServer.ts
@@ -0,0 +1,203 @@
+// @ts-expect-error - TS2307 - Cannot find module 'flow-to-typescript-codemod' or its corresponding type declarations.
+import {Flow} from 'flow-to-typescript-codemod';
+
+import type {
+ BuildSuccessEvent,
+ Dependency,
+ PluginOptions,
+ BundleGraph,
+ PackagedBundle,
+ Asset,
+} from '@atlaspack/types';
+import type {Diagnostic} from '@atlaspack/diagnostic';
+import type {AnsiDiagnosticResult} from '@atlaspack/utils';
+
+import invariant from 'assert';
+import {ansiHtml, prettyDiagnostic, PromiseQueue} from '@atlaspack/utils';
+
+const HMR_ENDPOINT = '/__parcel_hmr/';
+
+type HMRAsset = {
+ id: string;
+ url: string;
+ type: string;
+ output: string;
+ envHash: string;
+ depsByBundle: {
+ [key: string]: {
+ [key: string]: string;
+ };
+ };
+};
+
+export type HMRMessage =
+ | {
+ type: 'update';
+ assets: Array;
+ }
+ | {
+ type: 'error';
+ diagnostics: {
+ ansi: Array;
+ html: Array<
+ Partial<
+ Flow.Diff<
+ AnsiDiagnosticResult,
+ {
+ codeframe: string;
+ }
+ >
+ >
+ >;
+ };
+ };
+
+const FS_CONCURRENCY = 64;
+
+export default class HMRServer {
+ unresolvedError: HMRMessage | null = null;
+ broadcast: (arg1: HMRMessage) => void;
+
+ constructor(broadcast: (arg1: HMRMessage) => void) {
+ this.broadcast = broadcast;
+ }
+
+ async emitError(options: PluginOptions, diagnostics: Array) {
+ let renderedDiagnostics = await Promise.all(
+ diagnostics.map((d) => prettyDiagnostic(d, options)),
+ );
+
+ // store the most recent error so we can notify new connections
+ // and so we can broadcast when the error is resolved
+ this.unresolvedError = {
+ type: 'error',
+ diagnostics: {
+ ansi: renderedDiagnostics,
+ html: renderedDiagnostics.map((d, i) => {
+ return {
+ message: ansiHtml(d.message),
+ stack: ansiHtml(d.stack),
+ frames: d.frames.map((f) => ({
+ location: f.location,
+ code: ansiHtml(f.code),
+ })),
+ hints: d.hints.map((hint) => ansiHtml(hint)),
+ documentation: diagnostics[i].documentationURL ?? '',
+ };
+ }),
+ },
+ };
+
+ this.broadcast(this.unresolvedError);
+ }
+
+ async emitUpdate(event: BuildSuccessEvent) {
+ this.unresolvedError = null;
+
+ let changedAssets = new Set(event.changedAssets.values());
+ if (changedAssets.size === 0) return;
+
+ let queue = new PromiseQueue({maxConcurrent: FS_CONCURRENCY});
+ for (let asset of changedAssets) {
+ if (asset.type !== 'js' && asset.type !== 'css') {
+ // If all of the incoming dependencies of the asset actually resolve to a JS asset
+ // rather than the original, we can mark the runtimes as changed instead. URL runtimes
+ // have a cache busting query param added with HMR enabled which will trigger a reload.
+ let runtimes = new Set();
+ let incomingDeps = event.bundleGraph.getIncomingDependencies(asset);
+ let isOnlyReferencedByRuntimes = incomingDeps.every((dep) => {
+ let resolved = event.bundleGraph.getResolvedAsset(dep);
+ let isRuntime = resolved?.type === 'js' && resolved !== asset;
+ if (resolved && isRuntime) {
+ runtimes.add(resolved);
+ }
+ return isRuntime;
+ });
+
+ if (isOnlyReferencedByRuntimes) {
+ for (let runtime of runtimes) {
+ // @ts-expect-error - TS2345 - Argument of type 'unknown' is not assignable to parameter of type 'Asset'.
+ changedAssets.add(runtime);
+ }
+
+ continue;
+ }
+ }
+
+ queue.add(async () => {
+ let dependencies = event.bundleGraph.getDependencies(asset);
+ let depsByBundle: Record = {};
+ for (let bundle of event.bundleGraph.getBundlesWithAsset(asset)) {
+ let deps: Record = {};
+ for (let dep of dependencies) {
+ let resolved = event.bundleGraph.getResolvedAsset(dep, bundle);
+ if (resolved) {
+ deps[getSpecifier(dep)] =
+ event.bundleGraph.getAssetPublicId(resolved);
+ }
+ }
+ depsByBundle[bundle.id] = deps;
+ }
+
+ return {
+ id: event.bundleGraph.getAssetPublicId(asset),
+ url: getSourceURL(event.bundleGraph, asset),
+ type: asset.type,
+ // No need to send the contents of non-JS assets to the client.
+ output:
+ asset.type === 'js'
+ ? await getHotAssetContents(event.bundleGraph, asset)
+ : '',
+ envHash: asset.env.id,
+ depsByBundle,
+ };
+ });
+ }
+
+ let assets = await queue.run();
+ this.broadcast({
+ type: 'update',
+ // @ts-expect-error - TS2322 - Type 'unknown[]' is not assignable to type 'HMRAsset[]'.
+ assets: assets,
+ });
+ }
+}
+
+function getSpecifier(dep: Dependency): string {
+ if (typeof dep.meta.placeholder === 'string') {
+ return dep.meta.placeholder;
+ }
+
+ return dep.specifier;
+}
+
+export async function getHotAssetContents(
+ bundleGraph: BundleGraph,
+ asset: Asset,
+): Promise {
+ let output = await asset.getCode();
+ if (asset.type === 'js') {
+ let publicId = bundleGraph.getAssetPublicId(asset);
+ output = `parcelHotUpdate['${publicId}'] = function (require, module, exports) {${output}}`;
+ }
+
+ let sourcemap = await asset.getMap();
+ if (sourcemap) {
+ let sourcemapStringified = await sourcemap.stringify({
+ format: 'inline',
+ sourceRoot: '/__parcel_source_root/',
+ // $FlowFixMe
+ fs: asset.fs,
+ });
+
+ invariant(typeof sourcemapStringified === 'string');
+ output += `\n//# sourceMappingURL=${sourcemapStringified}`;
+ output += `\n//# sourceURL=${getSourceURL(bundleGraph, asset)}\n`;
+ }
+
+ return output;
+}
+
+function getSourceURL(bundleGraph: BundleGraph, asset: Asset) {
+ return HMR_ENDPOINT + asset.id;
+}
diff --git a/packages/reporters/dev-server-sw/src/ServerReporter.js b/packages/reporters/dev-server-sw/src/ServerReporter.js
deleted file mode 100644
index 78e3af949..000000000
--- a/packages/reporters/dev-server-sw/src/ServerReporter.js
+++ /dev/null
@@ -1,62 +0,0 @@
-// @flow
-import {Reporter} from '@atlaspack/plugin';
-import HMRServer, {getHotAssetContents} from './HMRServer';
-
-let hmrServer;
-let hmrAssetSourceCleanup: (() => void) | void;
-
-export default (new Reporter({
- async report({event, options}) {
- let {hmrOptions} = options;
- switch (event.type) {
- case 'watchStart': {
- if (hmrOptions) {
- hmrServer = new HMRServer(data =>
- // $FlowFixMe
- globalThis.ATLASPACK_SERVICE_WORKER('hmrUpdate', data),
- );
- }
- break;
- }
- case 'watchEnd':
- break;
- case 'buildStart':
- break;
- case 'buildSuccess':
- {
- let files: {|[string]: string|} = {};
- for (let f of await options.outputFS.readdir('/app/dist')) {
- files[f] = await options.outputFS.readFile(
- '/app/dist/' + f,
- 'utf8',
- );
- }
- // $FlowFixMe
- await globalThis.ATLASPACK_SERVICE_WORKER('setFS', files);
-
- hmrAssetSourceCleanup?.();
- // $FlowFixMe
- hmrAssetSourceCleanup = globalThis.ATLASPACK_SERVICE_WORKER_REGISTER(
- 'hmrAssetSource',
- async id => {
- let bundleGraph = event.bundleGraph;
- let asset = bundleGraph.getAssetById(id);
- return [
- asset.type,
- await getHotAssetContents(bundleGraph, asset),
- ];
- },
- );
-
- if (hmrServer) {
- await hmrServer?.emitUpdate(event);
- }
- }
- break;
- // We show this in the "frontend" as opposed to the iframe
- // case 'buildFailure':
- // await hmrServer?.emitError(options, event.diagnostics);
- // break;
- }
- },
-}): Reporter);
diff --git a/packages/reporters/dev-server-sw/src/ServerReporter.ts b/packages/reporters/dev-server-sw/src/ServerReporter.ts
new file mode 100644
index 000000000..f4c052be5
--- /dev/null
+++ b/packages/reporters/dev-server-sw/src/ServerReporter.ts
@@ -0,0 +1,68 @@
+import {Reporter} from '@atlaspack/plugin';
+import HMRServer, {getHotAssetContents} from './HMRServer';
+
+// @ts-expect-error - TS7034 - Variable 'hmrServer' implicitly has type 'any' in some locations where its type cannot be determined.
+let hmrServer;
+let hmrAssetSourceCleanup: (() => void) | undefined;
+
+export default new Reporter({
+ async report({event, options}) {
+ let {hmrOptions} = options;
+ switch (event.type) {
+ case 'watchStart': {
+ if (hmrOptions) {
+ hmrServer = new HMRServer((data: HMRMessage) =>
+ // $FlowFixMe
+ // @ts-expect-error - TS7017 - Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.
+ globalThis.ATLASPACK_SERVICE_WORKER('hmrUpdate', data),
+ );
+ }
+ break;
+ }
+ case 'watchEnd':
+ break;
+ case 'buildStart':
+ break;
+ case 'buildSuccess':
+ {
+ let files: {
+ [key: string]: string;
+ } = {};
+ for (let f of await options.outputFS.readdir('/app/dist')) {
+ files[f] = await options.outputFS.readFile(
+ '/app/dist/' + f,
+ 'utf8',
+ );
+ }
+ // @ts-expect-error - TS7017 - Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.
+ await globalThis.ATLASPACK_SERVICE_WORKER('setFS', files);
+
+ hmrAssetSourceCleanup?.();
+ // @ts-expect-error - TS7017 - Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.
+ hmrAssetSourceCleanup = globalThis.ATLASPACK_SERVICE_WORKER_REGISTER(
+ 'hmrAssetSource',
+ // @ts-expect-error - TS7006 - Parameter 'id' implicitly has an 'any' type.
+ async (id) => {
+ let bundleGraph = event.bundleGraph;
+ let asset = bundleGraph.getAssetById(id);
+ return [
+ asset.type,
+ await getHotAssetContents(bundleGraph, asset),
+ ];
+ },
+ );
+
+ // @ts-expect-error - TS7005 - Variable 'hmrServer' implicitly has an 'any' type.
+ if (hmrServer) {
+ // @ts-expect-error - TS7005 - Variable 'hmrServer' implicitly has an 'any' type.
+ await hmrServer?.emitUpdate(event);
+ }
+ }
+ break;
+ // We show this in the "frontend" as opposed to the iframe
+ // case 'buildFailure':
+ // await hmrServer?.emitError(options, event.diagnostics);
+ // break;
+ }
+ },
+}) as Reporter;
diff --git a/packages/reporters/dev-server/package.json b/packages/reporters/dev-server/package.json
index b2f89389c..3ca6ba768 100644
--- a/packages/reporters/dev-server/package.json
+++ b/packages/reporters/dev-server/package.json
@@ -11,7 +11,8 @@
"url": "https://github.com/atlassian-labs/atlaspack.git"
},
"main": "lib/ServerReporter.js",
- "source": "src/ServerReporter.js",
+ "types": "src/ServerReporter.ts",
+ "source": "src/ServerReporter.ts",
"engines": {
"node": ">= 16.0.0",
"parcel": "^2.12.0"
diff --git a/packages/reporters/dev-server/src/HMRServer.js b/packages/reporters/dev-server/src/HMRServer.js
deleted file mode 100644
index f16b5be39..000000000
--- a/packages/reporters/dev-server/src/HMRServer.js
+++ /dev/null
@@ -1,301 +0,0 @@
-// @flow
-import type {
- Asset,
- BundleGraph,
- Dependency,
- NamedBundle,
- PackagedBundle,
- PluginOptions,
-} from '@atlaspack/types';
-import type {Diagnostic} from '@atlaspack/diagnostic';
-import type {AnsiDiagnosticResult} from '@atlaspack/utils';
-import type {
- ServerError,
- HMRServerOptions,
- Request,
- Response,
-} from './types.js.flow';
-import {setHeaders, SOURCES_ENDPOINT} from './Server';
-
-import nullthrows from 'nullthrows';
-import url from 'url';
-import mime from 'mime-types';
-import WebSocket from 'ws';
-import invariant from 'assert';
-import {
- ansiHtml,
- createHTTPServer,
- prettyDiagnostic,
- PromiseQueue,
-} from '@atlaspack/utils';
-
-export type HMRAsset = {|
- id: string,
- url: string,
- type: string,
- output: string,
- envHash: string,
- outputFormat: string,
- depsByBundle: {[string]: {[string]: string, ...}, ...},
-|};
-
-export type HMRMessage =
- | {|
- type: 'update',
- assets: Array,
- |}
- | {|
- type: 'reload',
- |}
- | {|
- type: 'error',
- diagnostics: {|
- ansi: Array,
- html: Array<$Rest>,
- |},
- |};
-
-const FS_CONCURRENCY = 64;
-const HMR_ENDPOINT = '/__parcel_hmr';
-const BROADCAST_MAX_ASSETS = 10000;
-
-export default class HMRServer {
- wss: WebSocket.Server;
- unresolvedError: HMRMessage | null = null;
- options: HMRServerOptions;
- bundleGraph: BundleGraph | BundleGraph | null =
- null;
- stopServer: ?() => Promise;
-
- constructor(options: HMRServerOptions) {
- this.options = options;
- }
-
- async start() {
- let server = this.options.devServer;
- if (!server) {
- let result = await createHTTPServer({
- https: this.options.https,
- inputFS: this.options.inputFS,
- outputFS: this.options.outputFS,
- cacheDir: this.options.cacheDir,
- listener: (req, res) => {
- setHeaders(res);
- if (!this.handle(req, res)) {
- res.statusCode = 404;
- res.end();
- }
- },
- });
- server = result.server;
- server.listen(this.options.port, this.options.host);
- this.stopServer = result.stop;
- } else {
- this.options.addMiddleware?.((req, res) => this.handle(req, res));
- }
- this.wss = new WebSocket.Server({server});
-
- this.wss.on('connection', ws => {
- if (this.unresolvedError) {
- ws.send(JSON.stringify(this.unresolvedError));
- }
- });
-
- // $FlowFixMe[incompatible-exact]
- this.wss.on('error', err => this.handleSocketError(err));
- }
-
- handle(req: Request, res: Response): boolean {
- let {pathname} = url.parse(req.originalUrl || req.url);
- if (pathname != null && pathname.startsWith(HMR_ENDPOINT)) {
- let id = pathname.slice(HMR_ENDPOINT.length + 1);
- let bundleGraph = nullthrows(this.bundleGraph);
- let asset = bundleGraph.getAssetById(id);
- this.getHotAssetContents(asset).then(output => {
- res.setHeader('Content-Type', mime.contentType(asset.type));
- res.end(output);
- });
- return true;
- }
- return false;
- }
-
- async stop() {
- if (this.stopServer != null) {
- await this.stopServer();
- this.stopServer = null;
- }
- this.wss.close();
- }
-
- async emitError(options: PluginOptions, diagnostics: Array) {
- let renderedDiagnostics = await Promise.all(
- diagnostics.map(d => prettyDiagnostic(d, options)),
- );
-
- // store the most recent error so we can notify new connections
- // and so we can broadcast when the error is resolved
- this.unresolvedError = {
- type: 'error',
- diagnostics: {
- ansi: renderedDiagnostics,
- html: renderedDiagnostics.map((d, i) => {
- return {
- message: ansiHtml(d.message),
- stack: ansiHtml(d.stack),
- frames: d.frames.map(f => ({
- location: f.location,
- code: ansiHtml(f.code),
- })),
- hints: d.hints.map(hint => ansiHtml(hint)),
- documentation: diagnostics[i].documentationURL ?? '',
- };
- }),
- },
- };
-
- this.broadcast(this.unresolvedError);
- }
-
- async emitUpdate(event: {
- +bundleGraph: BundleGraph | BundleGraph,
- +changedAssets: Map,
- ...
- }) {
- this.unresolvedError = null;
- this.bundleGraph = event.bundleGraph;
-
- let changedAssets = new Set(event.changedAssets.values());
- if (changedAssets.size === 0) return;
-
- let queue = new PromiseQueue({maxConcurrent: FS_CONCURRENCY});
- for (let asset of changedAssets) {
- if (asset.type !== 'js' && asset.type !== 'css') {
- // If all of the incoming dependencies of the asset actually resolve to a JS asset
- // rather than the original, we can mark the runtimes as changed instead. URL runtimes
- // have a cache busting query param added with HMR enabled which will trigger a reload.
- let runtimes = new Set();
- let incomingDeps = event.bundleGraph.getIncomingDependencies(asset);
- let isOnlyReferencedByRuntimes = incomingDeps.every(dep => {
- let resolved = event.bundleGraph.getResolvedAsset(dep);
- let isRuntime = resolved?.type === 'js' && resolved !== asset;
- if (resolved && isRuntime) {
- runtimes.add(resolved);
- }
- return isRuntime;
- });
-
- if (isOnlyReferencedByRuntimes) {
- for (let runtime of runtimes) {
- changedAssets.add(runtime);
- }
-
- continue;
- }
- }
-
- queue.add(async () => {
- let dependencies = event.bundleGraph.getDependencies(asset);
- let depsByBundle = {};
- for (let bundle of event.bundleGraph.getBundlesWithAsset(asset)) {
- let deps = {};
- for (let dep of dependencies) {
- let resolved = event.bundleGraph.getResolvedAsset(dep, bundle);
- if (resolved) {
- deps[getSpecifier(dep)] =
- event.bundleGraph.getAssetPublicId(resolved);
- }
- }
- depsByBundle[bundle.id] = deps;
- }
-
- return {
- id: event.bundleGraph.getAssetPublicId(asset),
- url: this.getSourceURL(asset),
- type: asset.type,
- // No need to send the contents of non-JS assets to the client.
- output:
- asset.type === 'js' ? await this.getHotAssetContents(asset) : '',
- envHash: asset.env.id,
- outputFormat: asset.env.outputFormat,
- depsByBundle,
- };
- });
- }
-
- let assets = await queue.run();
-
- if (assets.length >= BROADCAST_MAX_ASSETS) {
- // Too many assets to send via an update without errors, just reload instead
- this.broadcast({type: 'reload'});
- } else {
- this.broadcast({
- type: 'update',
- assets,
- });
- }
- }
-
- async getHotAssetContents(asset: Asset): Promise {
- let output = await asset.getCode();
- let bundleGraph = nullthrows(this.bundleGraph);
- if (asset.type === 'js') {
- let publicId = bundleGraph.getAssetPublicId(asset);
- output = `parcelHotUpdate['${publicId}'] = function (require, module, exports) {${output}}`;
- }
-
- let sourcemap = await asset.getMap();
- if (sourcemap) {
- let sourcemapStringified = await sourcemap.stringify({
- format: 'inline',
- sourceRoot: SOURCES_ENDPOINT + '/',
- // $FlowFixMe
- fs: asset.fs,
- });
-
- invariant(typeof sourcemapStringified === 'string');
- output += `\n//# sourceMappingURL=${sourcemapStringified}`;
- output += `\n//# sourceURL=${encodeURI(this.getSourceURL(asset))}\n`;
- }
-
- return output;
- }
-
- getSourceURL(asset: Asset): string {
- let origin = '';
- if (!this.options.devServer) {
- origin = `http://${this.options.host || 'localhost'}:${
- this.options.port
- }`;
- }
- return origin + HMR_ENDPOINT + '/' + asset.id;
- }
-
- handleSocketError(err: ServerError) {
- if (err.code === 'ECONNRESET') {
- // This gets triggered on page refresh, ignore this
- return;
- }
-
- this.options.logger.warn({
- origin: '@atlaspack/reporter-dev-server',
- message: `[${err.code}]: ${err.message}`,
- stack: err.stack,
- });
- }
-
- broadcast(msg: HMRMessage) {
- const json = JSON.stringify(msg);
- for (let ws of this.wss.clients) {
- ws.send(json);
- }
- }
-}
-
-function getSpecifier(dep: Dependency): string {
- if (typeof dep.meta.placeholder === 'string') {
- return dep.meta.placeholder;
- }
-
- return dep.specifier;
-}
diff --git a/packages/reporters/dev-server/src/HMRServer.ts b/packages/reporters/dev-server/src/HMRServer.ts
new file mode 100644
index 000000000..d1d5541cc
--- /dev/null
+++ b/packages/reporters/dev-server/src/HMRServer.ts
@@ -0,0 +1,326 @@
+// @ts-expect-error - TS2307 - Cannot find module 'flow-to-typescript-codemod' or its corresponding type declarations.
+import {Flow} from 'flow-to-typescript-codemod';
+import type {
+ Asset,
+ BundleGraph,
+ Dependency,
+ NamedBundle,
+ PackagedBundle,
+ PluginOptions,
+} from '@atlaspack/types';
+import type {Diagnostic} from '@atlaspack/diagnostic';
+import type {AnsiDiagnosticResult} from '@atlaspack/utils';
+import type {
+ ServerError,
+ HMRServerOptions,
+ Request,
+ Response,
+ // @ts-expect-error - TS2307 - Cannot find module './types.js.flow' or its corresponding type declarations.
+} from './types.js.flow';
+import {setHeaders, SOURCES_ENDPOINT} from './Server';
+
+import nullthrows from 'nullthrows';
+import url from 'url';
+// @ts-expect-error - TS7016 - Could not find a declaration file for module 'mime-types'. '/home/ubuntu/parcel/node_modules/mime-types/index.js' implicitly has an 'any' type.
+import mime from 'mime-types';
+// @ts-expect-error - TS7016 - Could not find a declaration file for module 'ws'. '/home/ubuntu/parcel/node_modules/ws/index.js' implicitly has an 'any' type.
+import WebSocket from 'ws';
+import invariant from 'assert';
+import {
+ ansiHtml,
+ createHTTPServer,
+ prettyDiagnostic,
+ PromiseQueue,
+} from '@atlaspack/utils';
+
+export type HMRAsset = {
+ id: string;
+ url: string;
+ type: string;
+ output: string;
+ envHash: string;
+ outputFormat: string;
+ depsByBundle: {
+ [key: string]: {
+ [key: string]: string;
+ };
+ };
+};
+
+export type HMRMessage =
+ | {
+ type: 'update';
+ assets: Array;
+ }
+ | {
+ type: 'reload';
+ }
+ | {
+ type: 'error';
+ diagnostics: {
+ ansi: Array;
+ html: Array<
+ Partial<
+ Flow.Diff<
+ AnsiDiagnosticResult,
+ {
+ codeframe: string;
+ }
+ >
+ >
+ >;
+ };
+ };
+
+const FS_CONCURRENCY = 64;
+const HMR_ENDPOINT = '/__parcel_hmr';
+const BROADCAST_MAX_ASSETS = 10000;
+
+export default class HMRServer {
+ wss: WebSocket.Server;
+ unresolvedError: HMRMessage | null = null;
+ options: HMRServerOptions;
+ bundleGraph: BundleGraph | BundleGraph | null =
+ null;
+ // @ts-expect-error - TS2564 - Property 'stopServer' has no initializer and is not definitely assigned in the constructor.
+ stopServer: () => Promise | null | undefined;
+
+ constructor(options: HMRServerOptions) {
+ this.options = options;
+ }
+
+ async start() {
+ let server = this.options.devServer;
+ if (!server) {
+ let result = await createHTTPServer({
+ https: this.options.https,
+ inputFS: this.options.inputFS,
+ outputFS: this.options.outputFS,
+ cacheDir: this.options.cacheDir,
+ listener: (req, res) => {
+ setHeaders(res);
+ if (!this.handle(req, res)) {
+ res.statusCode = 404;
+ res.end();
+ }
+ },
+ });
+ server = result.server;
+ server.listen(this.options.port, this.options.host);
+ this.stopServer = result.stop;
+ } else {
+ this.options.addMiddleware?.((req: Request, res: Response) =>
+ this.handle(req, res),
+ );
+ }
+ this.wss = new WebSocket.Server({server});
+
+ // @ts-expect-error - TS7006 - Parameter 'ws' implicitly has an 'any' type.
+ this.wss.on('connection', (ws) => {
+ if (this.unresolvedError) {
+ ws.send(JSON.stringify(this.unresolvedError));
+ }
+ });
+
+ // @ts-expect-error - TS7006 - Parameter 'err' implicitly has an 'any' type.
+ this.wss.on('error', (err) => this.handleSocketError(err));
+ }
+
+ handle(req: Request, res: Response): boolean {
+ let {pathname} = url.parse(req.originalUrl || req.url);
+ if (pathname != null && pathname.startsWith(HMR_ENDPOINT)) {
+ let id = pathname.slice(HMR_ENDPOINT.length + 1);
+ let bundleGraph = nullthrows(this.bundleGraph);
+ let asset = bundleGraph.getAssetById(id);
+ this.getHotAssetContents(asset).then((output) => {
+ res.setHeader('Content-Type', mime.contentType(asset.type));
+ res.end(output);
+ });
+ return true;
+ }
+ return false;
+ }
+
+ async stop() {
+ if (this.stopServer != null) {
+ await this.stopServer();
+ // @ts-expect-error - TS2322 - Type 'null' is not assignable to type '() => Promise | null | undefined'.
+ this.stopServer = null;
+ }
+ this.wss.close();
+ }
+
+ async emitError(options: PluginOptions, diagnostics: Array) {
+ let renderedDiagnostics = await Promise.all(
+ diagnostics.map((d) => prettyDiagnostic(d, options)),
+ );
+
+ // store the most recent error so we can notify new connections
+ // and so we can broadcast when the error is resolved
+ this.unresolvedError = {
+ type: 'error',
+ diagnostics: {
+ ansi: renderedDiagnostics,
+ html: renderedDiagnostics.map((d, i) => {
+ return {
+ message: ansiHtml(d.message),
+ stack: ansiHtml(d.stack),
+ frames: d.frames.map((f) => ({
+ location: f.location,
+ code: ansiHtml(f.code),
+ })),
+ hints: d.hints.map((hint) => ansiHtml(hint)),
+ documentation: diagnostics[i].documentationURL ?? '',
+ };
+ }),
+ },
+ };
+
+ this.broadcast(this.unresolvedError);
+ }
+
+ async emitUpdate(event: {
+ readonly bundleGraph:
+ | BundleGraph
+ | BundleGraph;
+ readonly changedAssets: Map;
+ }) {
+ this.unresolvedError = null;
+ this.bundleGraph = event.bundleGraph;
+
+ let changedAssets = new Set(event.changedAssets.values());
+ if (changedAssets.size === 0) return;
+
+ let queue = new PromiseQueue({maxConcurrent: FS_CONCURRENCY});
+ for (let asset of changedAssets) {
+ if (asset.type !== 'js' && asset.type !== 'css') {
+ // If all of the incoming dependencies of the asset actually resolve to a JS asset
+ // rather than the original, we can mark the runtimes as changed instead. URL runtimes
+ // have a cache busting query param added with HMR enabled which will trigger a reload.
+ let runtimes = new Set();
+ let incomingDeps = event.bundleGraph.getIncomingDependencies(asset);
+ let isOnlyReferencedByRuntimes = incomingDeps.every((dep) => {
+ let resolved = event.bundleGraph.getResolvedAsset(dep);
+ let isRuntime = resolved?.type === 'js' && resolved !== asset;
+ if (resolved && isRuntime) {
+ runtimes.add(resolved);
+ }
+ return isRuntime;
+ });
+
+ if (isOnlyReferencedByRuntimes) {
+ for (let runtime of runtimes) {
+ // @ts-expect-error - TS2345 - Argument of type 'unknown' is not assignable to parameter of type 'Asset'.
+ changedAssets.add(runtime);
+ }
+
+ continue;
+ }
+ }
+
+ queue.add(async () => {
+ let dependencies = event.bundleGraph.getDependencies(asset);
+ let depsByBundle: Record = {};
+ for (let bundle of event.bundleGraph.getBundlesWithAsset(asset)) {
+ let deps: Record = {};
+ for (let dep of dependencies) {
+ let resolved = event.bundleGraph.getResolvedAsset(dep, bundle);
+ if (resolved) {
+ deps[getSpecifier(dep)] =
+ event.bundleGraph.getAssetPublicId(resolved);
+ }
+ }
+ depsByBundle[bundle.id] = deps;
+ }
+
+ return {
+ id: event.bundleGraph.getAssetPublicId(asset),
+ url: this.getSourceURL(asset),
+ type: asset.type,
+ // No need to send the contents of non-JS assets to the client.
+ output:
+ asset.type === 'js' ? await this.getHotAssetContents(asset) : '',
+ envHash: asset.env.id,
+ outputFormat: asset.env.outputFormat,
+ depsByBundle,
+ };
+ });
+ }
+
+ let assets = await queue.run();
+
+ if (assets.length >= BROADCAST_MAX_ASSETS) {
+ // Too many assets to send via an update without errors, just reload instead
+ this.broadcast({type: 'reload'});
+ } else {
+ this.broadcast({
+ type: 'update',
+ // @ts-expect-error - TS2322 - Type 'unknown[]' is not assignable to type 'HMRAsset[]'.
+ assets,
+ });
+ }
+ }
+
+ async getHotAssetContents(asset: Asset): Promise {
+ let output = await asset.getCode();
+ let bundleGraph = nullthrows(this.bundleGraph);
+ if (asset.type === 'js') {
+ let publicId = bundleGraph.getAssetPublicId(asset);
+ output = `parcelHotUpdate['${publicId}'] = function (require, module, exports) {${output}}`;
+ }
+
+ let sourcemap = await asset.getMap();
+ if (sourcemap) {
+ let sourcemapStringified = await sourcemap.stringify({
+ format: 'inline',
+ sourceRoot: SOURCES_ENDPOINT + '/',
+ // $FlowFixMe
+ fs: asset.fs,
+ });
+
+ invariant(typeof sourcemapStringified === 'string');
+ output += `\n//# sourceMappingURL=${sourcemapStringified}`;
+ output += `\n//# sourceURL=${encodeURI(this.getSourceURL(asset))}\n`;
+ }
+
+ return output;
+ }
+
+ getSourceURL(asset: Asset): string {
+ let origin = '';
+ if (!this.options.devServer) {
+ origin = `http://${this.options.host || 'localhost'}:${
+ this.options.port
+ }`;
+ }
+ return origin + HMR_ENDPOINT + '/' + asset.id;
+ }
+
+ handleSocketError(err: ServerError) {
+ if (err.code === 'ECONNRESET') {
+ // This gets triggered on page refresh, ignore this
+ return;
+ }
+
+ this.options.logger.warn({
+ origin: '@atlaspack/reporter-dev-server',
+ message: `[${err.code}]: ${err.message}`,
+ stack: err.stack,
+ });
+ }
+
+ broadcast(msg: HMRMessage) {
+ const json = JSON.stringify(msg);
+ for (let ws of this.wss.clients) {
+ ws.send(json);
+ }
+ }
+}
+
+function getSpecifier(dep: Dependency): string {
+ if (typeof dep.meta.placeholder === 'string') {
+ return dep.meta.placeholder;
+ }
+
+ return dep.specifier;
+}
diff --git a/packages/reporters/dev-server/src/Server.js b/packages/reporters/dev-server/src/Server.js
deleted file mode 100644
index 857c92c72..000000000
--- a/packages/reporters/dev-server/src/Server.js
+++ /dev/null
@@ -1,544 +0,0 @@
-// @flow
-
-import type {DevServerOptions, Request, Response} from './types.js.flow';
-import type {
- BuildSuccessEvent,
- BundleGraph,
- FilePath,
- PluginOptions,
- PackagedBundle,
-} from '@atlaspack/types';
-import type {Diagnostic} from '@atlaspack/diagnostic';
-import type {FileSystem} from '@atlaspack/fs';
-import type {HTTPServer, FormattedCodeFrame} from '@atlaspack/utils';
-
-import invariant from 'assert';
-import path from 'path';
-import url from 'url';
-import {
- ansiHtml,
- createHTTPServer,
- resolveConfig,
- readConfig,
- prettyDiagnostic,
- relativePath,
-} from '@atlaspack/utils';
-import serverErrors from './serverErrors';
-import fs from 'fs';
-import ejs from 'ejs';
-import connect from 'connect';
-import serveHandler from 'serve-handler';
-import {createProxyMiddleware} from 'http-proxy-middleware';
-import {URL, URLSearchParams} from 'url';
-import launchEditor from 'launch-editor';
-import fresh from 'fresh';
-
-export function setHeaders(res: Response) {
- res.setHeader('Access-Control-Allow-Origin', '*');
- res.setHeader(
- 'Access-Control-Allow-Methods',
- 'GET, HEAD, PUT, PATCH, POST, DELETE',
- );
- res.setHeader(
- 'Access-Control-Allow-Headers',
- 'Origin, X-Requested-With, Content-Type, Accept, Content-Type',
- );
- res.setHeader('Cache-Control', 'max-age=0, must-revalidate');
-}
-
-const SLASH_REGEX = /\//g;
-
-export const SOURCES_ENDPOINT = '/__parcel_source_root';
-const EDITOR_ENDPOINT = '/__parcel_launch_editor';
-const TEMPLATE_404 = fs.readFileSync(
- path.join(__dirname, 'templates/404.html'),
- 'utf8',
-);
-
-const TEMPLATE_500 = fs.readFileSync(
- path.join(__dirname, 'templates/500.html'),
- 'utf8',
-);
-type NextFunction = (req: Request, res: Response, next?: (any) => any) => any;
-
-export default class Server {
- pending: boolean;
- pendingRequests: Array<[Request, Response]>;
- middleware: Array<(req: Request, res: Response) => boolean>;
- options: DevServerOptions;
- rootPath: string;
- bundleGraph: BundleGraph | null;
- requestBundle: ?(bundle: PackagedBundle) => Promise;
- errors: Array<{|
- message: string,
- stack: ?string,
- frames: Array,
- hints: Array,
- documentation: string,
- |}> | null;
- stopServer: ?() => Promise;
-
- constructor(options: DevServerOptions) {
- this.options = options;
- try {
- this.rootPath = new URL(options.publicUrl).pathname;
- } catch (e) {
- this.rootPath = options.publicUrl;
- }
- this.pending = true;
- this.pendingRequests = [];
- this.middleware = [];
- this.bundleGraph = null;
- this.requestBundle = null;
- this.errors = null;
- }
-
- buildStart() {
- this.pending = true;
- }
-
- buildSuccess(
- bundleGraph: BundleGraph,
- requestBundle: (bundle: PackagedBundle) => Promise,
- ) {
- this.bundleGraph = bundleGraph;
- this.requestBundle = requestBundle;
- this.errors = null;
- this.pending = false;
-
- if (this.pendingRequests.length > 0) {
- let pendingRequests = this.pendingRequests;
- this.pendingRequests = [];
- for (let [req, res] of pendingRequests) {
- this.respond(req, res);
- }
- }
- }
-
- async buildError(options: PluginOptions, diagnostics: Array) {
- this.pending = false;
- this.errors = await Promise.all(
- diagnostics.map(async d => {
- let ansiDiagnostic = await prettyDiagnostic(d, options);
-
- return {
- message: ansiHtml(ansiDiagnostic.message),
- stack: ansiDiagnostic.stack ? ansiHtml(ansiDiagnostic.stack) : null,
- frames: ansiDiagnostic.frames.map(f => ({
- location: f.location,
- code: ansiHtml(f.code),
- })),
- hints: ansiDiagnostic.hints.map(hint => ansiHtml(hint)),
- documentation: d.documentationURL ?? '',
- };
- }),
- );
- }
-
- respond(req: Request, res: Response): mixed {
- if (this.middleware.some(handler => handler(req, res))) return;
- let {pathname, search} = url.parse(req.originalUrl || req.url);
- if (pathname == null) {
- pathname = '/';
- }
-
- if (pathname.startsWith(EDITOR_ENDPOINT) && search) {
- let query = new URLSearchParams(search);
- let file = query.get('file');
- if (file) {
- // File location might start with /__parcel_source_root if it came from a source map.
- if (file.startsWith(SOURCES_ENDPOINT)) {
- file = file.slice(SOURCES_ENDPOINT.length + 1);
- }
- launchEditor(file);
- }
- res.end();
- } else if (this.errors) {
- return this.send500(req, res);
- } else if (path.extname(pathname) === '') {
- // If the URL doesn't start with the public path, or the URL doesn't
- // have a file extension, send the main HTML bundle.
- return this.sendIndex(req, res);
- } else if (pathname.startsWith(SOURCES_ENDPOINT)) {
- req.url = pathname.slice(SOURCES_ENDPOINT.length);
- return this.serve(
- this.options.inputFS,
- this.options.projectRoot,
- req,
- res,
- () => this.send404(req, res),
- );
- } else if (pathname.startsWith(this.rootPath)) {
- // Otherwise, serve the file from the dist folder
- req.url =
- this.rootPath === '/' ? pathname : pathname.slice(this.rootPath.length);
- if (req.url[0] !== '/') {
- req.url = '/' + req.url;
- }
- return this.serveBundle(req, res, () => this.sendIndex(req, res));
- } else {
- return this.send404(req, res);
- }
- }
-
- sendIndex(req: Request, res: Response) {
- if (this.bundleGraph) {
- // If the main asset is an HTML file, serve it
- let htmlBundleFilePaths = this.bundleGraph
- .getBundles()
- .filter(bundle => path.posix.extname(bundle.name) === '.html')
- .map(bundle => {
- return `/${relativePath(
- this.options.distDir,
- bundle.filePath,
- false,
- )}`;
- });
-
- let indexFilePath = null;
- let {pathname: reqURL} = url.parse(req.originalUrl || req.url);
-
- if (!reqURL) {
- reqURL = '/';
- }
-
- if (htmlBundleFilePaths.length === 1) {
- indexFilePath = htmlBundleFilePaths[0];
- } else {
- let bestMatch = null;
- for (let bundle of htmlBundleFilePaths) {
- let bundleDir = path.posix.dirname(bundle);
- let bundleDirSubdir = bundleDir === '/' ? bundleDir : bundleDir + '/';
- let withoutExtension = path.posix.basename(
- bundle,
- path.posix.extname(bundle),
- );
- let isIndex = withoutExtension === 'index';
-
- let matchesIsIndex = null;
- if (
- isIndex &&
- (reqURL.startsWith(bundleDirSubdir) || reqURL === bundleDir)
- ) {
- // bundle is /bar/index.html and (/bar or something inside of /bar/** was requested was requested)
- matchesIsIndex = true;
- } else if (reqURL == path.posix.join(bundleDir, withoutExtension)) {
- // bundle is /bar/foo.html and /bar/foo was requested
- matchesIsIndex = false;
- }
- if (matchesIsIndex != null) {
- let depth = bundle.match(SLASH_REGEX)?.length ?? 0;
- if (
- bestMatch == null ||
- // This one is more specific (deeper)
- bestMatch.depth < depth ||
- // This one is just as deep, but the bundle name matches and not just index.html
- (bestMatch.depth === depth && bestMatch.isIndex)
- ) {
- bestMatch = {bundle, depth, isIndex: matchesIsIndex};
- }
- }
- }
- indexFilePath = bestMatch?.['bundle'] ?? htmlBundleFilePaths[0];
- }
-
- if (indexFilePath) {
- req.url = indexFilePath;
- this.serveBundle(req, res, () => this.send404(req, res));
- } else {
- this.send404(req, res);
- }
- } else {
- this.send404(req, res);
- }
- }
-
- async serveBundle(
- req: Request,
- res: Response,
- next: NextFunction,
- ): Promise {
- let bundleGraph = this.bundleGraph;
- if (bundleGraph) {
- let {pathname} = url.parse(req.url);
- if (!pathname) {
- this.send500(req, res);
- return;
- }
-
- let requestedPath = path.normalize(pathname.slice(1));
- let bundle = bundleGraph
- .getBundles()
- .find(
- b =>
- path.relative(this.options.distDir, b.filePath) === requestedPath,
- );
- if (!bundle) {
- this.serveDist(req, res, next);
- return;
- }
-
- invariant(this.requestBundle != null);
- try {
- await this.requestBundle(bundle);
- } catch (err) {
- this.send500(req, res);
- return;
- }
-
- this.serveDist(req, res, next);
- } else {
- this.send404(req, res);
- }
- }
-
- serveDist(
- req: Request,
- res: Response,
- next: NextFunction,
- ): Promise | Promise {
- return this.serve(
- this.options.outputFS,
- this.options.distDir,
- req,
- res,
- next,
- );
- }
-
- async serve(
- fs: FileSystem,
- root: FilePath,
- req: Request,
- res: Response,
- next: NextFunction,
- ): Promise {
- if (req.method !== 'GET' && req.method !== 'HEAD') {
- // method not allowed
- res.statusCode = 405;
- res.setHeader('Allow', 'GET, HEAD');
- res.setHeader('Content-Length', '0');
- res.end();
- return;
- }
-
- try {
- var filePath = url.parse(req.url).pathname || '';
- filePath = decodeURIComponent(filePath);
- } catch (err) {
- return this.sendError(res, 400);
- }
-
- filePath = path.normalize('.' + path.sep + filePath);
-
- // malicious path
- if (filePath.includes(path.sep + '..' + path.sep)) {
- return this.sendError(res, 403);
- }
-
- // join / normalize from the root dir
- if (!path.isAbsolute(filePath)) {
- filePath = path.normalize(path.join(root, filePath));
- }
-
- try {
- var stat = await fs.stat(filePath);
- } catch (err) {
- if (err.code === 'ENOENT') {
- return next(req, res);
- }
-
- return this.sendError(res, 500);
- }
-
- // Fall back to next handler if not a file
- if (!stat || !stat.isFile()) {
- return next(req, res);
- }
-
- if (fresh(req.headers, {'last-modified': stat.mtime.toUTCString()})) {
- res.statusCode = 304;
- res.end();
- return;
- }
-
- return serveHandler(
- req,
- res,
- {
- public: root,
- cleanUrls: false,
- },
- {
- lstat: path => fs.stat(path),
- realpath: path => fs.realpath(path),
- createReadStream: (path, options) => fs.createReadStream(path, options),
- readdir: path => fs.readdir(path),
- },
- );
- }
-
- sendError(res: Response, statusCode: number) {
- res.statusCode = statusCode;
- res.end();
- }
-
- send404(req: Request, res: Response) {
- res.statusCode = 404;
- res.end(TEMPLATE_404);
- }
-
- send500(req: Request, res: Response): void | Response {
- res.setHeader('Content-Type', 'text/html; charset=utf-8');
- res.writeHead(500);
-
- if (this.errors) {
- return res.end(
- ejs.render(TEMPLATE_500, {
- errors: this.errors,
- hmrOptions: this.options.hmrOptions,
- }),
- );
- }
- }
-
- logAccessIfVerbose(req: Request) {
- this.options.logger.verbose({
- message: `Request: ${req.headers.host}${req.originalUrl || req.url}`,
- });
- }
-
- /**
- * Load proxy table from package.json and apply them.
- */
- async applyProxyTable(app: any): Promise {
- // avoid skipping project root
- const fileInRoot: string = path.join(this.options.projectRoot, 'index');
-
- const configFilePath = await resolveConfig(
- this.options.inputFS,
- fileInRoot,
- [
- '.proxyrc.cts',
- '.proxyrc.mts',
- '.proxyrc.ts',
- '.proxyrc.cjs',
- '.proxyrc.mjs',
- '.proxyrc.js',
- '.proxyrc',
- '.proxyrc.json',
- ],
- this.options.projectRoot,
- );
-
- if (!configFilePath) {
- return this;
- }
-
- const filename = path.basename(configFilePath);
-
- if (filename === '.proxyrc' || filename === '.proxyrc.json') {
- let conf = await readConfig(this.options.inputFS, configFilePath);
- if (!conf) {
- return this;
- }
- let cfg = conf.config;
- if (typeof cfg !== 'object') {
- this.options.logger.warn({
- message:
- "Proxy table in '.proxyrc' should be of object type. Skipping...",
- });
- return this;
- }
- for (const [context, options] of Object.entries(cfg)) {
- // each key is interpreted as context, and value as middleware options
- app.use(createProxyMiddleware(context, options));
- }
- } else {
- let cfg = await this.options.packageManager.require(
- configFilePath,
- fileInRoot,
- );
- if (
- // $FlowFixMe
- Object.prototype.toString.call(cfg) === '[object Module]'
- ) {
- cfg = cfg.default;
- }
-
- if (typeof cfg !== 'function') {
- this.options.logger.warn({
- message: `Proxy configuration file '${filename}' should export a function. Skipping...`,
- });
- return this;
- }
- cfg(app);
- }
-
- return this;
- }
-
- async start(): Promise {
- const finalHandler = (req: Request, res: Response) => {
- this.logAccessIfVerbose(req);
-
- // Wait for the parcelInstance to finish bundling if needed
- if (this.pending) {
- this.pendingRequests.push([req, res]);
- } else {
- this.respond(req, res);
- }
- };
-
- const app = connect();
- app.use((req, res, next) => {
- setHeaders(res);
- next();
- });
-
- app.use((req, res, next) => {
- if (req.url === '/__parcel_healthcheck') {
- res.statusCode = 200;
- res.write(`${Date.now()}`);
- res.end();
- } else {
- next();
- }
- });
-
- await this.applyProxyTable(app);
- app.use(finalHandler);
-
- let {server, stop} = await createHTTPServer({
- cacheDir: this.options.cacheDir,
- https: this.options.https,
- inputFS: this.options.inputFS,
- listener: app,
- outputFS: this.options.outputFS,
- host: this.options.host,
- });
- this.stopServer = stop;
-
- server.listen(this.options.port, this.options.host);
- return new Promise((resolve, reject) => {
- server.once('error', err => {
- this.options.logger.error(
- ({
- message: serverErrors(err, this.options.port),
- }: Diagnostic),
- );
- reject(err);
- });
-
- server.once('listening', () => {
- resolve(server);
- });
- });
- }
-
- async stop(): Promise {
- invariant(this.stopServer != null);
- await this.stopServer();
- this.stopServer = null;
- }
-}
diff --git a/packages/reporters/dev-server/src/Server.ts b/packages/reporters/dev-server/src/Server.ts
new file mode 100644
index 000000000..5f2f39618
--- /dev/null
+++ b/packages/reporters/dev-server/src/Server.ts
@@ -0,0 +1,570 @@
+// @ts-expect-error - TS2307 - Cannot find module './types.js.flow' or its corresponding type declarations.
+import type {DevServerOptions, Request, Response} from './types.js.flow';
+import type {
+ BuildSuccessEvent,
+ BundleGraph,
+ FilePath,
+ PluginOptions,
+ PackagedBundle,
+} from '@atlaspack/types';
+import type {Diagnostic} from '@atlaspack/diagnostic';
+import type {FileSystem} from '@atlaspack/fs';
+import type {HTTPServer, FormattedCodeFrame} from '@atlaspack/utils';
+
+import invariant from 'assert';
+import path from 'path';
+import url from 'url';
+import {
+ ansiHtml,
+ createHTTPServer,
+ resolveConfig,
+ readConfig,
+ prettyDiagnostic,
+ relativePath,
+} from '@atlaspack/utils';
+import serverErrors from './serverErrors';
+import fs from 'fs';
+// @ts-expect-error - TS7016 - Could not find a declaration file for module 'ejs'. '/home/ubuntu/parcel/node_modules/ejs/lib/ejs.js' implicitly has an 'any' type.
+import ejs from 'ejs';
+// @ts-expect-error - TS7016 - Could not find a declaration file for module 'connect'. '/home/ubuntu/parcel/node_modules/connect/index.js' implicitly has an 'any' type.
+import connect from 'connect';
+// @ts-expect-error - TS7016 - Could not find a declaration file for module 'serve-handler'. '/home/ubuntu/parcel/node_modules/serve-handler/src/index.js' implicitly has an 'any' type.
+import serveHandler from 'serve-handler';
+import {createProxyMiddleware} from 'http-proxy-middleware';
+import {URL, URLSearchParams} from 'url';
+// @ts-expect-error - TS7016 - Could not find a declaration file for module 'launch-editor'. '/home/ubuntu/parcel/node_modules/launch-editor/index.js' implicitly has an 'any' type.
+import launchEditor from 'launch-editor';
+// @ts-expect-error - TS7016 - Could not find a declaration file for module 'fresh'. '/home/ubuntu/parcel/node_modules/fresh/index.js' implicitly has an 'any' type.
+import fresh from 'fresh';
+
+export function setHeaders(res: Response) {
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader(
+ 'Access-Control-Allow-Methods',
+ 'GET, HEAD, PUT, PATCH, POST, DELETE',
+ );
+ res.setHeader(
+ 'Access-Control-Allow-Headers',
+ 'Origin, X-Requested-With, Content-Type, Accept, Content-Type',
+ );
+ res.setHeader('Cache-Control', 'max-age=0, must-revalidate');
+}
+
+const SLASH_REGEX = /\//g;
+
+export const SOURCES_ENDPOINT = '/__parcel_source_root';
+const EDITOR_ENDPOINT = '/__parcel_launch_editor';
+const TEMPLATE_404 = fs.readFileSync(
+ path.join(__dirname, 'templates/404.html'),
+ 'utf8',
+);
+
+const TEMPLATE_500 = fs.readFileSync(
+ path.join(__dirname, 'templates/500.html'),
+ 'utf8',
+);
+type NextFunction = (
+ req: Request,
+ res: Response,
+ next?: (arg1?: any) => any,
+) => any;
+
+export default class Server {
+ pending: boolean;
+ pendingRequests: Array<[Request, Response]>;
+ middleware: Array<(req: Request, res: Response) => boolean>;
+ options: DevServerOptions;
+ rootPath: string;
+ bundleGraph: BundleGraph | null;
+ requestBundle: (
+ bundle: PackagedBundle,
+ ) => Promise | null | undefined;
+ errors: Array<{
+ message: string;
+ stack: string | null | undefined;
+ frames: Array;
+ hints: Array;
+ documentation: string;
+ }> | null;
+ // @ts-expect-error - TS2564 - Property 'stopServer' has no initializer and is not definitely assigned in the constructor.
+ stopServer: () => Promise | null | undefined;
+
+ constructor(options: DevServerOptions) {
+ this.options = options;
+ try {
+ this.rootPath = new URL(options.publicUrl).pathname;
+ } catch (e: any) {
+ this.rootPath = options.publicUrl;
+ }
+ this.pending = true;
+ this.pendingRequests = [];
+ this.middleware = [];
+ this.bundleGraph = null;
+ // @ts-expect-error - TS2322 - Type 'null' is not assignable to type '(bundle: PackagedBundle) => Promise | null | undefined'.
+ this.requestBundle = null;
+ this.errors = null;
+ }
+
+ buildStart() {
+ this.pending = true;
+ }
+
+ buildSuccess(
+ bundleGraph: BundleGraph,
+ requestBundle: (bundle: PackagedBundle) => Promise,
+ ) {
+ this.bundleGraph = bundleGraph;
+ this.requestBundle = requestBundle;
+ this.errors = null;
+ this.pending = false;
+
+ if (this.pendingRequests.length > 0) {
+ let pendingRequests = this.pendingRequests;
+ this.pendingRequests = [];
+ for (let [req, res] of pendingRequests) {
+ this.respond(req, res);
+ }
+ }
+ }
+
+ async buildError(options: PluginOptions, diagnostics: Array) {
+ this.pending = false;
+ this.errors = await Promise.all(
+ diagnostics.map(async (d) => {
+ let ansiDiagnostic = await prettyDiagnostic(d, options);
+
+ return {
+ message: ansiHtml(ansiDiagnostic.message),
+ stack: ansiDiagnostic.stack ? ansiHtml(ansiDiagnostic.stack) : null,
+ frames: ansiDiagnostic.frames.map((f) => ({
+ location: f.location,
+ code: ansiHtml(f.code),
+ })),
+ hints: ansiDiagnostic.hints.map((hint) => ansiHtml(hint)),
+ documentation: d.documentationURL ?? '',
+ };
+ }),
+ );
+ }
+
+ respond(req: Request, res: Response): unknown {
+ if (this.middleware.some((handler) => handler(req, res))) return;
+ let {pathname, search} = url.parse(req.originalUrl || req.url);
+ if (pathname == null) {
+ pathname = '/';
+ }
+
+ if (pathname.startsWith(EDITOR_ENDPOINT) && search) {
+ let query = new URLSearchParams(search);
+ let file = query.get('file');
+ if (file) {
+ // File location might start with /__parcel_source_root if it came from a source map.
+ if (file.startsWith(SOURCES_ENDPOINT)) {
+ file = file.slice(SOURCES_ENDPOINT.length + 1);
+ }
+ launchEditor(file);
+ }
+ res.end();
+ } else if (this.errors) {
+ return this.send500(req, res);
+ } else if (path.extname(pathname) === '') {
+ // If the URL doesn't start with the public path, or the URL doesn't
+ // have a file extension, send the main HTML bundle.
+ return this.sendIndex(req, res);
+ } else if (pathname.startsWith(SOURCES_ENDPOINT)) {
+ req.url = pathname.slice(SOURCES_ENDPOINT.length);
+ return this.serve(
+ this.options.inputFS,
+ this.options.projectRoot,
+ req,
+ res,
+ () => this.send404(req, res),
+ );
+ } else if (pathname.startsWith(this.rootPath)) {
+ // Otherwise, serve the file from the dist folder
+ req.url =
+ this.rootPath === '/' ? pathname : pathname.slice(this.rootPath.length);
+ if (req.url[0] !== '/') {
+ req.url = '/' + req.url;
+ }
+ return this.serveBundle(req, res, () => this.sendIndex(req, res));
+ } else {
+ return this.send404(req, res);
+ }
+ }
+
+ sendIndex(req: Request, res: Response) {
+ if (this.bundleGraph) {
+ // If the main asset is an HTML file, serve it
+ let htmlBundleFilePaths = this.bundleGraph
+ .getBundles()
+ .filter((bundle) => path.posix.extname(bundle.name) === '.html')
+ .map((bundle) => {
+ return `/${relativePath(
+ this.options.distDir,
+ bundle.filePath,
+ false,
+ )}`;
+ });
+
+ let indexFilePath = null;
+ let {pathname: reqURL} = url.parse(req.originalUrl || req.url);
+
+ if (!reqURL) {
+ reqURL = '/';
+ }
+
+ if (htmlBundleFilePaths.length === 1) {
+ indexFilePath = htmlBundleFilePaths[0];
+ } else {
+ let bestMatch = null;
+ for (let bundle of htmlBundleFilePaths) {
+ let bundleDir = path.posix.dirname(bundle);
+ let bundleDirSubdir = bundleDir === '/' ? bundleDir : bundleDir + '/';
+ let withoutExtension = path.posix.basename(
+ bundle,
+ path.posix.extname(bundle),
+ );
+ let isIndex = withoutExtension === 'index';
+
+ let matchesIsIndex = null;
+ if (
+ isIndex &&
+ (reqURL.startsWith(bundleDirSubdir) || reqURL === bundleDir)
+ ) {
+ // bundle is /bar/index.html and (/bar or something inside of /bar/** was requested was requested)
+ matchesIsIndex = true;
+ } else if (reqURL == path.posix.join(bundleDir, withoutExtension)) {
+ // bundle is /bar/foo.html and /bar/foo was requested
+ matchesIsIndex = false;
+ }
+ if (matchesIsIndex != null) {
+ let depth = bundle.match(SLASH_REGEX)?.length ?? 0;
+ if (
+ bestMatch == null ||
+ // This one is more specific (deeper)
+ bestMatch.depth < depth ||
+ // This one is just as deep, but the bundle name matches and not just index.html
+ (bestMatch.depth === depth && bestMatch.isIndex)
+ ) {
+ bestMatch = {bundle, depth, isIndex: matchesIsIndex};
+ }
+ }
+ }
+ indexFilePath = bestMatch?.['bundle'] ?? htmlBundleFilePaths[0];
+ }
+
+ if (indexFilePath) {
+ req.url = indexFilePath;
+ this.serveBundle(req, res, () => this.send404(req, res));
+ } else {
+ this.send404(req, res);
+ }
+ } else {
+ this.send404(req, res);
+ }
+ }
+
+ async serveBundle(
+ req: Request,
+ res: Response,
+ next: NextFunction,
+ ): Promise {
+ let bundleGraph = this.bundleGraph;
+ if (bundleGraph) {
+ let {pathname} = url.parse(req.url);
+ if (!pathname) {
+ this.send500(req, res);
+ return;
+ }
+
+ let requestedPath = path.normalize(pathname.slice(1));
+ let bundle = bundleGraph
+ .getBundles()
+ .find(
+ (b) =>
+ path.relative(this.options.distDir, b.filePath) === requestedPath,
+ );
+ if (!bundle) {
+ this.serveDist(req, res, next);
+ return;
+ }
+
+ invariant(this.requestBundle != null);
+ try {
+ await this.requestBundle(bundle);
+ } catch (err: any) {
+ this.send500(req, res);
+ return;
+ }
+
+ this.serveDist(req, res, next);
+ } else {
+ this.send404(req, res);
+ }
+ }
+
+ serveDist(
+ req: Request,
+ res: Response,
+ next: NextFunction,
+ ): Promise | Promise {
+ return this.serve(
+ this.options.outputFS,
+ this.options.distDir,
+ req,
+ res,
+ next,
+ );
+ }
+
+ async serve(
+ fs: FileSystem,
+ root: FilePath,
+ req: Request,
+ res: Response,
+ next: NextFunction,
+ ): Promise {
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
+ // method not allowed
+ res.statusCode = 405;
+ res.setHeader('Allow', 'GET, HEAD');
+ res.setHeader('Content-Length', '0');
+ res.end();
+ return;
+ }
+
+ try {
+ var filePath = url.parse(req.url).pathname || '';
+ filePath = decodeURIComponent(filePath);
+ } catch (err: any) {
+ return this.sendError(res, 400);
+ }
+
+ filePath = path.normalize('.' + path.sep + filePath);
+
+ // malicious path
+ if (filePath.includes(path.sep + '..' + path.sep)) {
+ return this.sendError(res, 403);
+ }
+
+ // join / normalize from the root dir
+ if (!path.isAbsolute(filePath)) {
+ filePath = path.normalize(path.join(root, filePath));
+ }
+
+ try {
+ var stat = await fs.stat(filePath);
+ } catch (err: any) {
+ if (err.code === 'ENOENT') {
+ return next(req, res);
+ }
+
+ return this.sendError(res, 500);
+ }
+
+ // Fall back to next handler if not a file
+ if (!stat || !stat.isFile()) {
+ return next(req, res);
+ }
+
+ if (fresh(req.headers, {'last-modified': stat.mtime.toUTCString()})) {
+ res.statusCode = 304;
+ res.end();
+ return;
+ }
+
+ return serveHandler(
+ req,
+ res,
+ {
+ public: root,
+ cleanUrls: false,
+ },
+ {
+ // @ts-expect-error - TS7006 - Parameter 'path' implicitly has an 'any' type.
+ lstat: (path) => fs.stat(path),
+ // @ts-expect-error - TS7006 - Parameter 'path' implicitly has an 'any' type.
+ realpath: (path) => fs.realpath(path),
+ // @ts-expect-error - TS7006 - Parameter 'path' implicitly has an 'any' type. | TS7006 - Parameter 'options' implicitly has an 'any' type.
+ createReadStream: (path, options) => fs.createReadStream(path, options),
+ // @ts-expect-error - TS7006 - Parameter 'path' implicitly has an 'any' type.
+ readdir: (path) => fs.readdir(path),
+ },
+ );
+ }
+
+ sendError(res: Response, statusCode: number) {
+ res.statusCode = statusCode;
+ res.end();
+ }
+
+ send404(req: Request, res: Response) {
+ res.statusCode = 404;
+ res.end(TEMPLATE_404);
+ }
+
+ send500(req: Request, res: Response): undefined | Response {
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
+ res.writeHead(500);
+
+ if (this.errors) {
+ return res.end(
+ ejs.render(TEMPLATE_500, {
+ errors: this.errors,
+ hmrOptions: this.options.hmrOptions,
+ }),
+ );
+ }
+ }
+
+ logAccessIfVerbose(req: Request) {
+ this.options.logger.verbose({
+ message: `Request: ${req.headers.host}${req.originalUrl || req.url}`,
+ });
+ }
+
+ /**
+ * Load proxy table from package.json and apply them.
+ */
+ async applyProxyTable(app: any): Promise {
+ // avoid skipping project root
+ const fileInRoot: string = path.join(this.options.projectRoot, 'index');
+
+ const configFilePath = await resolveConfig(
+ this.options.inputFS,
+ fileInRoot,
+ [
+ '.proxyrc.cts',
+ '.proxyrc.mts',
+ '.proxyrc.ts',
+ '.proxyrc.cjs',
+ '.proxyrc.mjs',
+ '.proxyrc.js',
+ '.proxyrc',
+ '.proxyrc.json',
+ ],
+ this.options.projectRoot,
+ );
+
+ if (!configFilePath) {
+ return this;
+ }
+
+ const filename = path.basename(configFilePath);
+
+ if (filename === '.proxyrc' || filename === '.proxyrc.json') {
+ let conf = await readConfig(this.options.inputFS, configFilePath);
+ if (!conf) {
+ return this;
+ }
+ let cfg = conf.config;
+ if (typeof cfg !== 'object') {
+ this.options.logger.warn({
+ message:
+ "Proxy table in '.proxyrc' should be of object type. Skipping...",
+ });
+ return this;
+ }
+ for (const [context, options] of Object.entries(cfg)) {
+ // each key is interpreted as context, and value as middleware options
+ // @ts-expect-error - TS2345 - Argument of type 'unknown' is not assignable to parameter of type 'Options | undefined'.
+ app.use(createProxyMiddleware(context, options));
+ }
+ } else {
+ let cfg = await this.options.packageManager.require(
+ configFilePath,
+ fileInRoot,
+ );
+ if (
+ // $FlowFixMe
+ Object.prototype.toString.call(cfg) === '[object Module]'
+ ) {
+ cfg = cfg.default;
+ }
+
+ if (typeof cfg !== 'function') {
+ this.options.logger.warn({
+ message: `Proxy configuration file '${filename}' should export a function. Skipping...`,
+ });
+ return this;
+ }
+ cfg(app);
+ }
+
+ return this;
+ }
+
+ async start(): Promise {
+ const finalHandler = (req: Request, res: Response) => {
+ this.logAccessIfVerbose(req);
+
+ // Wait for the parcelInstance to finish bundling if needed
+ if (this.pending) {
+ this.pendingRequests.push([req, res]);
+ } else {
+ this.respond(req, res);
+ }
+ };
+
+ const app = connect();
+ // @ts-expect-error - TS7006 - Parameter 'req' implicitly has an 'any' type. | TS7006 - Parameter 'res' implicitly has an 'any' type. | TS7006 - Parameter 'next' implicitly has an 'any' type.
+ app.use((req, res, next) => {
+ setHeaders(res);
+ next();
+ });
+
+ // @ts-expect-error - TS7006 - Parameter 'req' implicitly has an 'any' type. | TS7006 - Parameter 'res' implicitly has an 'any' type. | TS7006 - Parameter 'next' implicitly has an 'any' type.
+ app.use((req, res, next) => {
+ if (req.url === '/__parcel_healthcheck') {
+ res.statusCode = 200;
+ res.write(`${Date.now()}`);
+ res.end();
+ } else {
+ next();
+ }
+ });
+
+ await this.applyProxyTable(app);
+ app.use(finalHandler);
+
+ let {server, stop} = await createHTTPServer({
+ cacheDir: this.options.cacheDir,
+ https: this.options.https,
+ inputFS: this.options.inputFS,
+ listener: app,
+ outputFS: this.options.outputFS,
+ host: this.options.host,
+ });
+ this.stopServer = stop;
+
+ server.listen(this.options.port, this.options.host);
+ // @ts-expect-error - TS2322 - Type 'Server' is not assignable to type 'HTTPServer'.
+ return new Promise(
+ (
+ resolve: (result: Promise | Server) => void,
+ reject: (error?: any) => void,
+ ) => {
+ server.once('error', (err) => {
+ this.options.logger.error({
+ // @ts-expect-error - TS2345 - Argument of type 'Error' is not assignable to parameter of type 'ServerError'.
+ message: serverErrors(err, this.options.port),
+ } as Diagnostic);
+ reject(err);
+ });
+
+ server.once('listening', () => {
+ // @ts-expect-error - TS2345 - Argument of type 'HTTPServer' is not assignable to parameter of type 'Server | Promise'.
+ resolve(server);
+ });
+ },
+ );
+ }
+
+ async stop(): Promise {
+ invariant(this.stopServer != null);
+ await this.stopServer();
+ // @ts-expect-error - TS2322 - Type 'null' is not assignable to type '() => Promise | null | undefined'.
+ this.stopServer = null;
+ }
+}
diff --git a/packages/reporters/dev-server/src/ServerReporter.js b/packages/reporters/dev-server/src/ServerReporter.js
deleted file mode 100644
index 3867b4e6e..000000000
--- a/packages/reporters/dev-server/src/ServerReporter.js
+++ /dev/null
@@ -1,143 +0,0 @@
-// @flow
-
-import {Reporter} from '@atlaspack/plugin';
-import HMRServer from './HMRServer';
-import Server from './Server';
-
-let servers: Map = new Map();
-let hmrServers: Map = new Map();
-export default (new Reporter({
- async report({event, options, logger}) {
- let {serveOptions, hmrOptions} = options;
- let server = serveOptions ? servers.get(serveOptions.port) : undefined;
- let hmrPort =
- (hmrOptions && hmrOptions.port) || (serveOptions && serveOptions.port);
- let hmrServer = hmrPort ? hmrServers.get(hmrPort) : undefined;
- switch (event.type) {
- case 'watchStart': {
- if (serveOptions) {
- // If there's already a server when watching has just started, something
- // is wrong.
- if (server) {
- return logger.warn({
- message: 'Trying to create the devserver but it already exists.',
- });
- }
-
- let serverOptions = {
- ...serveOptions,
- projectRoot: options.projectRoot,
- cacheDir: options.cacheDir,
- // Override the target's publicUrl as that is likely meant for production.
- // This could be configurable in the future.
- publicUrl: serveOptions.publicUrl ?? '/',
- inputFS: options.inputFS,
- outputFS: options.outputFS,
- packageManager: options.packageManager,
- logger,
- hmrOptions,
- };
-
- server = new Server(serverOptions);
- servers.set(serveOptions.port, server);
- const devServer = await server.start();
-
- if (hmrOptions && hmrOptions.port === serveOptions.port) {
- let hmrServerOptions = {
- port: serveOptions.port,
- host: hmrOptions.host,
- devServer,
- addMiddleware: handler => {
- server?.middleware.push(handler);
- },
- logger,
- https: options.serveOptions ? options.serveOptions.https : false,
- cacheDir: options.cacheDir,
- inputFS: options.inputFS,
- outputFS: options.outputFS,
- };
- hmrServer = new HMRServer(hmrServerOptions);
- hmrServers.set(serveOptions.port, hmrServer);
- await hmrServer.start();
- return;
- }
- }
-
- let port = hmrOptions?.port;
- if (typeof port === 'number') {
- let hmrServerOptions = {
- port,
- host: hmrOptions?.host,
- logger,
- https: options.serveOptions ? options.serveOptions.https : false,
- cacheDir: options.cacheDir,
- inputFS: options.inputFS,
- outputFS: options.outputFS,
- };
- hmrServer = new HMRServer(hmrServerOptions);
- hmrServers.set(port, hmrServer);
- await hmrServer.start();
- }
- break;
- }
- case 'watchEnd':
- if (serveOptions) {
- if (!server) {
- return logger.warn({
- message:
- 'Could not shutdown devserver because it does not exist.',
- });
- }
- await server.stop();
- servers.delete(server.options.port);
- }
- if (hmrOptions && hmrServer) {
- await hmrServer.stop();
- // $FlowFixMe[prop-missing]
- hmrServers.delete(hmrServer.wss.options.port);
- }
- break;
- case 'buildStart':
- if (server) {
- server.buildStart();
- }
- break;
- case 'buildProgress':
- if (
- event.phase === 'bundled' &&
- hmrServer &&
- // Only send HMR updates before packaging if the built in dev server is used to ensure that
- // no stale bundles are served. Otherwise emit it for 'buildSuccess'.
- options.serveOptions !== false
- ) {
- await hmrServer.emitUpdate(event);
- }
- break;
- case 'buildSuccess':
- if (serveOptions) {
- if (!server) {
- return logger.warn({
- message:
- 'Could not send success event to devserver because it does not exist.',
- });
- }
-
- server.buildSuccess(event.bundleGraph, event.requestBundle);
- }
- if (hmrServer && options.serveOptions === false) {
- await hmrServer.emitUpdate(event);
- }
- break;
- case 'buildFailure':
- // On buildFailure watchStart sometimes has not been called yet
- // do not throw an additional warning here
- if (server) {
- await server.buildError(options, event.diagnostics);
- }
- if (hmrServer) {
- await hmrServer.emitError(options, event.diagnostics);
- }
- break;
- }
- },
-}): Reporter);
diff --git a/packages/reporters/dev-server/src/ServerReporter.ts b/packages/reporters/dev-server/src/ServerReporter.ts
new file mode 100644
index 000000000..981a00f3d
--- /dev/null
+++ b/packages/reporters/dev-server/src/ServerReporter.ts
@@ -0,0 +1,141 @@
+import {Reporter} from '@atlaspack/plugin';
+import HMRServer from './HMRServer';
+import Server from './Server';
+
+let servers: Map = new Map();
+let hmrServers: Map = new Map();
+export default new Reporter({
+ async report({event, options, logger}) {
+ let {serveOptions, hmrOptions} = options;
+ let server = serveOptions ? servers.get(serveOptions.port) : undefined;
+ let hmrPort =
+ (hmrOptions && hmrOptions.port) || (serveOptions && serveOptions.port);
+ let hmrServer = hmrPort ? hmrServers.get(hmrPort) : undefined;
+ switch (event.type) {
+ case 'watchStart': {
+ if (serveOptions) {
+ // If there's already a server when watching has just started, something
+ // is wrong.
+ if (server) {
+ return logger.warn({
+ message: 'Trying to create the devserver but it already exists.',
+ });
+ }
+
+ let serverOptions = {
+ ...serveOptions,
+ projectRoot: options.projectRoot,
+ cacheDir: options.cacheDir,
+ // Override the target's publicUrl as that is likely meant for production.
+ // This could be configurable in the future.
+ publicUrl: serveOptions.publicUrl ?? '/',
+ inputFS: options.inputFS,
+ outputFS: options.outputFS,
+ packageManager: options.packageManager,
+ logger,
+ hmrOptions,
+ };
+
+ server = new Server(serverOptions);
+ servers.set(serveOptions.port, server);
+ const devServer = await server.start();
+
+ if (hmrOptions && hmrOptions.port === serveOptions.port) {
+ let hmrServerOptions = {
+ port: serveOptions.port,
+ host: hmrOptions.host,
+ devServer,
+ // @ts-expect-error - TS7006 - Parameter 'handler' implicitly has an 'any' type.
+ addMiddleware: (handler) => {
+ server?.middleware.push(handler);
+ },
+ logger,
+ https: options.serveOptions ? options.serveOptions.https : false,
+ cacheDir: options.cacheDir,
+ inputFS: options.inputFS,
+ outputFS: options.outputFS,
+ };
+ hmrServer = new HMRServer(hmrServerOptions);
+ hmrServers.set(serveOptions.port, hmrServer);
+ await hmrServer.start();
+ return;
+ }
+ }
+
+ let port = hmrOptions?.port;
+ if (typeof port === 'number') {
+ let hmrServerOptions = {
+ port,
+ host: hmrOptions?.host,
+ logger,
+ https: options.serveOptions ? options.serveOptions.https : false,
+ cacheDir: options.cacheDir,
+ inputFS: options.inputFS,
+ outputFS: options.outputFS,
+ };
+ hmrServer = new HMRServer(hmrServerOptions);
+ hmrServers.set(port, hmrServer);
+ await hmrServer.start();
+ }
+ break;
+ }
+ case 'watchEnd':
+ if (serveOptions) {
+ if (!server) {
+ return logger.warn({
+ message:
+ 'Could not shutdown devserver because it does not exist.',
+ });
+ }
+ await server.stop();
+ servers.delete(server.options.port);
+ }
+ if (hmrOptions && hmrServer) {
+ await hmrServer.stop();
+ hmrServers.delete(hmrServer.wss.options.port);
+ }
+ break;
+ case 'buildStart':
+ if (server) {
+ server.buildStart();
+ }
+ break;
+ case 'buildProgress':
+ if (
+ event.phase === 'bundled' &&
+ hmrServer &&
+ // Only send HMR updates before packaging if the built in dev server is used to ensure that
+ // no stale bundles are served. Otherwise emit it for 'buildSuccess'.
+ options.serveOptions !== false
+ ) {
+ await hmrServer.emitUpdate(event);
+ }
+ break;
+ case 'buildSuccess':
+ if (serveOptions) {
+ if (!server) {
+ return logger.warn({
+ message:
+ 'Could not send success event to devserver because it does not exist.',
+ });
+ }
+
+ server.buildSuccess(event.bundleGraph, event.requestBundle);
+ }
+ if (hmrServer && options.serveOptions === false) {
+ await hmrServer.emitUpdate(event);
+ }
+ break;
+ case 'buildFailure':
+ // On buildFailure watchStart sometimes has not been called yet
+ // do not throw an additional warning here
+ if (server) {
+ await server.buildError(options, event.diagnostics);
+ }
+ if (hmrServer) {
+ await hmrServer.emitError(options, event.diagnostics);
+ }
+ break;
+ }
+ },
+}) as Reporter;
diff --git a/packages/reporters/dev-server/src/serverErrors.js b/packages/reporters/dev-server/src/serverErrors.js
deleted file mode 100644
index 05ee4166f..000000000
--- a/packages/reporters/dev-server/src/serverErrors.js
+++ /dev/null
@@ -1,21 +0,0 @@
-// @flow
-export type ServerError = Error & {|
- code: string,
-|};
-
-const serverErrorList = {
- EACCES: "You don't have access to bind the server to port {port}.",
- EADDRINUSE: 'There is already a process listening on port {port}.',
-};
-
-export default function serverErrors(err: ServerError, port: number): string {
- let desc = `Error: ${
- err.code
- } occurred while setting up server on port ${port.toString()}.`;
-
- if (serverErrorList[err.code]) {
- desc = serverErrorList[err.code].replace(/{port}/g, port);
- }
-
- return desc;
-}
diff --git a/packages/reporters/dev-server/src/serverErrors.ts b/packages/reporters/dev-server/src/serverErrors.ts
new file mode 100644
index 000000000..b50ef1473
--- /dev/null
+++ b/packages/reporters/dev-server/src/serverErrors.ts
@@ -0,0 +1,22 @@
+export type ServerError = Error & {
+ code: string;
+};
+
+const serverErrorList = {
+ EACCES: "You don't have access to bind the server to port {port}.",
+ EADDRINUSE: 'There is already a process listening on port {port}.',
+} as const;
+
+export default function serverErrors(err: ServerError, port: number): string {
+ let desc = `Error: ${
+ err.code
+ } occurred while setting up server on port ${port.toString()}.`;
+
+ // @ts-expect-error - TS7053 - Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ readonly EACCES: "You don't have access to bind the server to port {port}."; readonly EADDRINUSE: "There is already a process listening on port {port}."; }'.
+ if (serverErrorList[err.code]) {
+ // @ts-expect-error - TS7053 - Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ readonly EACCES: "You don't have access to bind the server to port {port}."; readonly EADDRINUSE: "There is already a process listening on port {port}."; }'.
+ desc = serverErrorList[err.code].replace(/{port}/g, port);
+ }
+
+ return desc;
+}
diff --git a/packages/reporters/json/package.json b/packages/reporters/json/package.json
index a5371e207..c956dca6c 100644
--- a/packages/reporters/json/package.json
+++ b/packages/reporters/json/package.json
@@ -10,7 +10,8 @@
"url": "https://github.com/atlassian-labs/atlaspack.git"
},
"main": "lib/JSONReporter.js",
- "source": "src/JSONReporter.js",
+ "types": "src/JSONReporter.ts",
+ "source": "src/JSONReporter.ts",
"engines": {
"node": ">= 16.0.0",
"parcel": "^2.12.0"
diff --git a/packages/reporters/json/src/JSONReporter.js b/packages/reporters/json/src/JSONReporter.js
deleted file mode 100644
index 28197433d..000000000
--- a/packages/reporters/json/src/JSONReporter.js
+++ /dev/null
@@ -1,176 +0,0 @@
-// @flow strict-local
-import type {BuildProgressEvent, LogEvent} from '@atlaspack/types';
-import type {BuildMetrics} from '@atlaspack/utils';
-
-import {Reporter} from '@atlaspack/plugin';
-import {generateBuildMetrics} from '@atlaspack/utils';
-
-/* eslint-disable no-console */
-const writeToStdout = makeWriter(console.log);
-const writeToStderr = makeWriter(console.error);
-/* eslint-enable no-console */
-
-const LOG_LEVELS = {
- none: 0,
- error: 1,
- warn: 2,
- info: 3,
- progress: 3,
- success: 3,
- verbose: 4,
-};
-
-export default (new Reporter({
- async report({event, options}) {
- let logLevelFilter = options.logLevel || 'info';
-
- switch (event.type) {
- case 'buildStart':
- if (LOG_LEVELS[logLevelFilter] >= LOG_LEVELS.info) {
- writeToStdout({type: 'buildStart'}, logLevelFilter);
- }
- break;
- case 'buildFailure':
- if (LOG_LEVELS[logLevelFilter] >= LOG_LEVELS.error) {
- writeToStderr(
- {type: 'buildFailure', message: event.diagnostics[0].message},
- logLevelFilter,
- );
- }
- break;
- case 'buildProgress':
- if (LOG_LEVELS[logLevelFilter] >= LOG_LEVELS.progress) {
- let jsonEvent = progressEventToJSONEvent(event);
- if (jsonEvent != null) {
- writeToStdout(jsonEvent, logLevelFilter);
- }
- }
- break;
- case 'buildSuccess':
- if (LOG_LEVELS[logLevelFilter] >= LOG_LEVELS.success) {
- let {bundles} = await generateBuildMetrics(
- event.bundleGraph.getBundles(),
- options.outputFS,
- options.projectRoot,
- );
-
- writeToStdout(
- {
- type: 'buildSuccess',
- buildTime: event.buildTime,
- bundles: bundles,
- },
- logLevelFilter,
- );
- }
- break;
- case 'log':
- writeLogEvent(event, logLevelFilter);
- }
- },
-}): Reporter);
-
-function makeWriter(
- write: string => mixed,
-): (JSONReportEvent, $Keys) => void {
- return (
- event: JSONReportEvent,
- logLevelFilter: $Keys,
- ): void => {
- let stringified;
- try {
- stringified = JSON.stringify(event);
- } catch (err) {
- // This should never happen so long as JSONReportEvent is easily serializable
- if (LOG_LEVELS[logLevelFilter] >= LOG_LEVELS.error) {
- writeToStderr(
- {
- type: 'log',
- level: 'error',
- diagnostics: [
- {
- origin: '@atlaspack/reporter-json',
- message: err.message,
- stack: err.stack,
- },
- ],
- },
- logLevelFilter,
- );
- }
- return;
- }
-
- write(stringified);
- };
-}
-
-function writeLogEvent(
- event: LogEvent,
- logLevelFilter: $Keys,
-): void {
- if (LOG_LEVELS[logLevelFilter] < LOG_LEVELS[event.level]) {
- return;
- }
- switch (event.level) {
- case 'info':
- case 'progress':
- case 'success':
- case 'verbose':
- writeToStdout(event, logLevelFilter);
- break;
- case 'warn':
- case 'error':
- writeToStderr(event, logLevelFilter);
- break;
- }
-}
-
-function progressEventToJSONEvent(
- progressEvent: BuildProgressEvent,
-): ?JSONProgressEvent {
- switch (progressEvent.phase) {
- case 'transforming':
- return {
- type: 'buildProgress',
- phase: 'transforming',
- filePath: progressEvent.filePath,
- };
- case 'bundling':
- return {
- type: 'buildProgress',
- phase: 'bundling',
- };
- case 'optimizing':
- case 'packaging':
- return {
- type: 'buildProgress',
- phase: progressEvent.phase,
- bundleName: progressEvent.bundle.displayName,
- };
- }
-}
-
-type JSONReportEvent =
- | LogEvent
- | {|+type: 'buildStart'|}
- | {|+type: 'buildFailure', message: string|}
- | {|
- +type: 'buildSuccess',
- buildTime: number,
- bundles?: $PropertyType,
- |}
- | JSONProgressEvent;
-
-type JSONProgressEvent =
- | {|
- +type: 'buildProgress',
- phase: 'transforming',
- filePath: string,
- |}
- | {|+type: 'buildProgress', phase: 'bundling'|}
- | {|
- +type: 'buildProgress',
- +phase: 'packaging' | 'optimizing',
- bundleName?: string,
- |};
diff --git a/packages/reporters/json/src/JSONReporter.ts b/packages/reporters/json/src/JSONReporter.ts
new file mode 100644
index 000000000..e20d32cbb
--- /dev/null
+++ b/packages/reporters/json/src/JSONReporter.ts
@@ -0,0 +1,183 @@
+import type {BuildProgressEvent, LogEvent} from '@atlaspack/types';
+import type {BuildMetrics} from '@atlaspack/utils';
+
+import {Reporter} from '@atlaspack/plugin';
+import {generateBuildMetrics} from '@atlaspack/utils';
+
+/* eslint-disable no-console */
+const writeToStdout = makeWriter(console.log);
+const writeToStderr = makeWriter(console.error);
+/* eslint-enable no-console */
+
+const LOG_LEVELS = {
+ none: 0,
+ error: 1,
+ warn: 2,
+ info: 3,
+ progress: 3,
+ success: 3,
+ verbose: 4,
+} as const;
+
+export default new Reporter({
+ async report({event, options}) {
+ let logLevelFilter = options.logLevel || 'info';
+
+ switch (event.type) {
+ case 'buildStart':
+ if (LOG_LEVELS[logLevelFilter] >= LOG_LEVELS.info) {
+ writeToStdout({type: 'buildStart'}, logLevelFilter);
+ }
+ break;
+ case 'buildFailure':
+ if (LOG_LEVELS[logLevelFilter] >= LOG_LEVELS.error) {
+ writeToStderr(
+ {type: 'buildFailure', message: event.diagnostics[0].message},
+ logLevelFilter,
+ );
+ }
+ break;
+ case 'buildProgress':
+ if (LOG_LEVELS[logLevelFilter] >= LOG_LEVELS.progress) {
+ let jsonEvent = progressEventToJSONEvent(event);
+ if (jsonEvent != null) {
+ writeToStdout(jsonEvent, logLevelFilter);
+ }
+ }
+ break;
+ case 'buildSuccess':
+ if (LOG_LEVELS[logLevelFilter] >= LOG_LEVELS.success) {
+ let {bundles} = await generateBuildMetrics(
+ event.bundleGraph.getBundles(),
+ options.outputFS,
+ options.projectRoot,
+ );
+
+ writeToStdout(
+ {
+ type: 'buildSuccess',
+ buildTime: event.buildTime,
+ bundles: bundles,
+ },
+ logLevelFilter,
+ );
+ }
+ break;
+ case 'log':
+ writeLogEvent(event, logLevelFilter);
+ }
+ },
+}) as Reporter;
+
+function makeWriter(
+ write: (arg1: string) => unknown,
+): (arg1: JSONReportEvent, arg2: keyof typeof LOG_LEVELS) => void {
+ return (
+ event: JSONReportEvent,
+ logLevelFilter: keyof typeof LOG_LEVELS,
+ ): void => {
+ let stringified;
+ try {
+ stringified = JSON.stringify(event);
+ } catch (err: any) {
+ // This should never happen so long as JSONReportEvent is easily serializable
+ if (LOG_LEVELS[logLevelFilter] >= LOG_LEVELS.error) {
+ writeToStderr(
+ {
+ type: 'log',
+ level: 'error',
+ diagnostics: [
+ {
+ origin: '@atlaspack/reporter-json',
+ message: err.message,
+ stack: err.stack,
+ },
+ ],
+ },
+ logLevelFilter,
+ );
+ }
+ return;
+ }
+
+ write(stringified);
+ };
+}
+
+function writeLogEvent(
+ event: LogEvent,
+ logLevelFilter: keyof typeof LOG_LEVELS,
+): void {
+ if (LOG_LEVELS[logLevelFilter] < LOG_LEVELS[event.level]) {
+ return;
+ }
+ switch (event.level) {
+ case 'info':
+ case 'progress':
+ case 'success':
+ case 'verbose':
+ writeToStdout(event, logLevelFilter);
+ break;
+ case 'warn':
+ case 'error':
+ writeToStderr(event, logLevelFilter);
+ break;
+ }
+}
+
+function progressEventToJSONEvent(
+ progressEvent: BuildProgressEvent,
+): JSONProgressEvent | null | undefined {
+ switch (progressEvent.phase) {
+ case 'transforming':
+ return {
+ type: 'buildProgress',
+ phase: 'transforming',
+ filePath: progressEvent.filePath,
+ };
+ case 'bundling':
+ return {
+ type: 'buildProgress',
+ phase: 'bundling',
+ };
+ case 'optimizing':
+ case 'packaging':
+ return {
+ type: 'buildProgress',
+ phase: progressEvent.phase,
+ bundleName: progressEvent.bundle.displayName,
+ };
+ }
+}
+
+type JSONReportEvent =
+ | LogEvent
+ | {
+ readonly type: 'buildStart';
+ }
+ | {
+ readonly type: 'buildFailure';
+ message: string;
+ }
+ | {
+ readonly type: 'buildSuccess';
+ buildTime: number;
+ bundles?: BuildMetrics['bundles'];
+ }
+ | JSONProgressEvent;
+
+type JSONProgressEvent =
+ | {
+ readonly type: 'buildProgress';
+ phase: 'transforming';
+ filePath: string;
+ }
+ | {
+ readonly type: 'buildProgress';
+ phase: 'bundling';
+ }
+ | {
+ readonly type: 'buildProgress';
+ readonly phase: 'packaging' | 'optimizing';
+ bundleName?: string;
+ };
diff --git a/packages/reporters/lsp-reporter/package.json b/packages/reporters/lsp-reporter/package.json
index 9d6c44401..8dadf2adf 100644
--- a/packages/reporters/lsp-reporter/package.json
+++ b/packages/reporters/lsp-reporter/package.json
@@ -10,7 +10,8 @@
"url": "https://github.com/atlassian-labs/atlaspack.git"
},
"main": "lib/LspReporter.js",
- "source": "src/LspReporter.js",
+ "types": "src/LspReporter.ts",
+ "source": "src/LspReporter.ts",
"engines": {
"node": ">= 16.0.0",
"parcel": "^2.12.0"
diff --git a/packages/reporters/lsp-reporter/src/LspReporter.js b/packages/reporters/lsp-reporter/src/LspReporter.js
deleted file mode 100644
index ab196b822..000000000
--- a/packages/reporters/lsp-reporter/src/LspReporter.js
+++ /dev/null
@@ -1,455 +0,0 @@
-// @flow strict-local
-
-import type {Diagnostic as ParcelDiagnostic} from '@atlaspack/diagnostic';
-import type {BundleGraph, FilePath, PackagedBundle} from '@atlaspack/types';
-import type {Program, Query} from 'ps-node';
-import type {Diagnostic, DocumentUri} from 'vscode-languageserver';
-import type {MessageConnection} from 'vscode-jsonrpc/node';
-import type {ParcelSeverity} from './utils';
-
-import {
- DefaultMap,
- getProgressMessage,
- makeDeferredWithPromise,
-} from '@atlaspack/utils';
-import {Reporter} from '@atlaspack/plugin';
-import path from 'path';
-import os from 'os';
-import url from 'url';
-import fs from 'fs';
-import nullthrows from 'nullthrows';
-import * as ps from 'ps-node';
-import {promisify} from 'util';
-
-import {createServer} from './ipc';
-import {
- type PublishDiagnostic,
- NotificationBuildStatus,
- NotificationWorkspaceDiagnostics,
- RequestDocumentDiagnostics,
- RequestImporters,
-} from '@atlaspack/lsp-protocol';
-
-import {
- DiagnosticSeverity,
- DiagnosticTag,
- normalizeFilePath,
- parcelSeverityToLspSeverity,
-} from './utils';
-import type {FSWatcher} from 'fs';
-
-const lookupPid: Query => Program[] = promisify(ps.lookup);
-
-const ignoreFail = func => {
- try {
- func();
- } catch (e) {
- /**/
- }
-};
-
-const BASEDIR = fs.realpathSync(path.join(os.tmpdir(), 'parcel-lsp'));
-const SOCKET_FILE = path.join(BASEDIR, `parcel-${process.pid}`);
-const META_FILE = path.join(BASEDIR, `parcel-${process.pid}.json`);
-
-let workspaceDiagnostics: DefaultMap<
- string,
- Array,
-> = new DefaultMap(() => []);
-
-const getWorkspaceDiagnostics = (): Array =>
- [...workspaceDiagnostics].map(([uri, diagnostics]) => ({uri, diagnostics}));
-
-let server;
-let connections: Array = [];
-
-let bundleGraphDeferrable =
- makeDeferredWithPromise>();
-let bundleGraph: Promise> =
- bundleGraphDeferrable.promise;
-
-let watchStarted = false;
-let lspStarted = false;
-let watchStartPromise;
-
-const LSP_SENTINEL_FILENAME = 'lsp-server';
-const LSP_SENTINEL_FILE = path.join(BASEDIR, LSP_SENTINEL_FILENAME);
-
-async function watchLspActive(): Promise {
- // Check for lsp-server when reporter is first started
- try {
- await fs.promises.access(LSP_SENTINEL_FILE, fs.constants.F_OK);
- lspStarted = true;
- } catch {
- //
- }
-
- return fs.watch(BASEDIR, (eventType: string, filename: string) => {
- switch (eventType) {
- case 'rename':
- if (filename === LSP_SENTINEL_FILENAME) {
- fs.access(LSP_SENTINEL_FILE, fs.constants.F_OK, err => {
- if (err) {
- lspStarted = false;
- } else {
- lspStarted = true;
- }
- });
- }
- }
- });
-}
-
-async function doWatchStart(options) {
- await fs.promises.mkdir(BASEDIR, {recursive: true});
-
- // For each existing file, check if the pid matches a running process.
- // If no process matches, delete the file, assuming it was orphaned
- // by a process that quit unexpectedly.
- for (let filename of fs.readdirSync(BASEDIR)) {
- if (filename.endsWith('.json')) continue;
- let pid = parseInt(filename.slice('parcel-'.length), 10);
- let resultList = await lookupPid({pid});
- if (resultList.length > 0) continue;
- fs.unlinkSync(path.join(BASEDIR, filename));
- ignoreFail(() => fs.unlinkSync(path.join(BASEDIR, filename + '.json')));
- }
-
- server = await createServer(SOCKET_FILE, connection => {
- // console.log('got connection');
- connections.push(connection);
- connection.onClose(() => {
- connections = connections.filter(c => c !== connection);
- });
-
- connection.onRequest(RequestDocumentDiagnostics, async uri => {
- let graph = await bundleGraph;
- if (!graph) return;
-
- return getDiagnosticsUnusedExports(graph, uri);
- });
-
- connection.onRequest(RequestImporters, async params => {
- let graph = await bundleGraph;
- if (!graph) return null;
-
- return getImporters(graph, params);
- });
-
- sendDiagnostics();
- });
- await fs.promises.writeFile(
- META_FILE,
- JSON.stringify({
- projectRoot: options.projectRoot,
- pid: process.pid,
- argv: process.argv,
- }),
- );
-}
-
-watchLspActive();
-
-export default (new Reporter({
- async report({event, options}) {
- if (event.type === 'watchStart') {
- watchStarted = true;
- }
-
- if (watchStarted && lspStarted) {
- if (!watchStartPromise) {
- watchStartPromise = doWatchStart(options);
- }
- await watchStartPromise;
- }
-
- switch (event.type) {
- case 'watchStart': {
- break;
- }
-
- case 'buildStart': {
- bundleGraphDeferrable = makeDeferredWithPromise();
- bundleGraph = bundleGraphDeferrable.promise;
- updateBuildState('start');
- clearDiagnostics();
- break;
- }
- case 'buildSuccess':
- bundleGraphDeferrable.deferred.resolve(event.bundleGraph);
- updateBuildState('end');
- sendDiagnostics();
- break;
- case 'buildFailure': {
- bundleGraphDeferrable.deferred.resolve(undefined);
- updateDiagnostics(event.diagnostics, 'error', options.projectRoot);
- updateBuildState('end');
- sendDiagnostics();
- break;
- }
- case 'log':
- if (
- event.diagnostics != null &&
- (event.level === 'error' ||
- event.level === 'warn' ||
- event.level === 'info' ||
- event.level === 'verbose')
- ) {
- updateDiagnostics(
- event.diagnostics,
- event.level,
- options.projectRoot,
- );
- }
- break;
- case 'buildProgress': {
- let message = getProgressMessage(event);
- if (message != null) {
- updateBuildState('progress', message);
- }
- break;
- }
- case 'watchEnd':
- connections.forEach(c => c.end());
- await server.close();
- ignoreFail(() => fs.unlinkSync(META_FILE));
- break;
- }
- },
-}): Reporter);
-
-function updateBuildState(
- state: 'start' | 'progress' | 'end',
- message: string | void,
-) {
- connections.forEach(c =>
- c.sendNotification(NotificationBuildStatus, state, message),
- );
-}
-
-function clearDiagnostics() {
- workspaceDiagnostics.clear();
-}
-function sendDiagnostics() {
- // console.log('send', getWorkspaceDiagnostics());
- connections.forEach(c =>
- c.sendNotification(
- NotificationWorkspaceDiagnostics,
- getWorkspaceDiagnostics(),
- ),
- );
-}
-
-function updateDiagnostics(
- parcelDiagnostics: Array,
- parcelSeverity: ParcelSeverity,
- projectRoot: FilePath,
-): void {
- for (let diagnostic of parcelDiagnostics) {
- const codeFrames = diagnostic.codeFrames;
- if (codeFrames == null) {
- continue;
- }
-
- const firstCodeFrame = codeFrames[0];
- const filePath = firstCodeFrame.filePath;
- if (filePath == null) {
- continue;
- }
-
- // We use the first highlight of the first codeFrame as the main Diagnostic,
- // and we place everything else in the current Parcel diagnostic
- // in relatedInformation
- // https://code.visualstudio.com/api/references/vscode-api#DiagnosticRelatedInformation
- const firstFrameHighlight = codeFrames[0].codeHighlights[0];
- if (firstFrameHighlight == null) {
- continue;
- }
-
- const relatedInformation = [];
- for (const codeFrame of codeFrames) {
- for (const highlight of codeFrame.codeHighlights) {
- const filePath = codeFrame.filePath;
- if (highlight === firstFrameHighlight || filePath == null) {
- continue;
- }
-
- relatedInformation.push({
- location: {
- uri: `file://${normalizeFilePath(filePath, projectRoot)}`,
- range: {
- start: {
- line: highlight.start.line - 1,
- character: highlight.start.column - 1,
- },
- end: {
- line: highlight.end.line - 1,
- character: highlight.end.column,
- },
- },
- },
- message: highlight.message ?? diagnostic.message,
- });
- }
- }
-
- workspaceDiagnostics
- .get(`file://${normalizeFilePath(filePath, projectRoot)}`)
- .push({
- range: {
- start: {
- line: firstFrameHighlight.start.line - 1,
- character: firstFrameHighlight.start.column - 1,
- },
- end: {
- line: firstFrameHighlight.end.line - 1,
- character: firstFrameHighlight.end.column,
- },
- },
- source: diagnostic.origin,
- severity: parcelSeverityToLspSeverity(parcelSeverity),
- message:
- diagnostic.message +
- (firstFrameHighlight.message == null
- ? ''
- : ' ' + firstFrameHighlight.message),
- relatedInformation,
- });
- }
-}
-
-function getDiagnosticsUnusedExports(
- bundleGraph: BundleGraph,
- document: string,
-): Array {
- let filename = url.fileURLToPath(document);
- let diagnostics = [];
-
- let asset = bundleGraph.traverse((node, context, actions) => {
- if (node.type === 'asset' && node.value.filePath === filename) {
- actions.stop();
- return node.value;
- }
- });
-
- if (asset) {
- const generateDiagnostic = (loc, type) => ({
- range: {
- start: {
- line: loc.start.line - 1,
- character: loc.start.column - 1,
- },
- end: {
- line: loc.end.line - 1,
- character: loc.end.column,
- },
- },
- source: '@atlaspack/core',
- severity: DiagnosticSeverity.Hint,
- message: `Unused ${type}.`,
- tags: [DiagnosticTag.Unnecessary],
- });
-
- let usedSymbols = bundleGraph.getUsedSymbols(asset);
- if (usedSymbols) {
- for (let [exported, symbol] of asset.symbols) {
- if (!usedSymbols.has(exported)) {
- if (symbol.loc) {
- diagnostics.push(generateDiagnostic(symbol.loc, 'export'));
- }
- }
- }
- // if (usedSymbols.size === 0 && asset.sideEffects !== false) {
- // diagnostics.push({
- // range: {
- // start: {
- // line: 0,
- // character: 0,
- // },
- // end: {
- // line: 0,
- // character: 1,
- // },
- // },
- // source: '@atlaspack/core',
- // severity: DiagnosticSeverity.Warning,
- // message: `Asset has no used exports, but is not marked as sideEffect-free so it cannot be excluded automatically.`,
- // });
- // }
- }
-
- for (let dep of asset.getDependencies()) {
- let usedSymbols = bundleGraph.getUsedSymbols(dep);
- if (usedSymbols) {
- for (let [exported, symbol] of dep.symbols) {
- if (!usedSymbols.has(exported) && symbol.isWeak && symbol.loc) {
- diagnostics.push(generateDiagnostic(symbol.loc, 'reexport'));
- }
- }
- }
- }
- }
- return diagnostics;
-}
-
-// function getDefinition(
-// bundleGraph: BundleGraph,
-// document: string,
-// position: Position,
-// ): Array | void {
-// let filename = url.fileURLToPath(document);
-
-// let asset = bundleGraph.traverse((node, context, actions) => {
-// if (node.type === 'asset' && node.value.filePath === filename) {
-// actions.stop();
-// return node.value;
-// }
-// });
-
-// if (asset) {
-// for (let dep of bundleGraph.getDependencies(asset)) {
-// let loc = dep.loc;
-// if (loc && isInRange(loc, position)) {
-// let resolution = bundleGraph.getResolvedAsset(dep);
-// if (resolution) {
-// return [
-// {
-// originSelectionRange: {
-// start: {
-// line: loc.start.line - 1,
-// character: loc.start.column - 1,
-// },
-// end: {line: loc.end.line - 1, character: loc.end.column},
-// },
-// targetUri: `file://${resolution.filePath}`,
-// targetRange: RANGE_DUMMY,
-// targetSelectionRange: RANGE_DUMMY,
-// },
-// ];
-// }
-// }
-// }
-// }
-// }
-
-function getImporters(
- bundleGraph: BundleGraph,
- document: string,
-): Array | null {
- let filename = url.fileURLToPath(document);
-
- let asset = bundleGraph.traverse((node, context, actions) => {
- if (node.type === 'asset' && node.value.filePath === filename) {
- actions.stop();
- return node.value;
- }
- });
-
- if (asset) {
- let incoming = bundleGraph.getIncomingDependencies(asset);
- return incoming
- .filter(dep => dep.sourcePath != null)
- .map(dep => `file://${nullthrows(dep.sourcePath)}`);
- }
- return null;
-}
diff --git a/packages/reporters/lsp-reporter/src/LspReporter.ts b/packages/reporters/lsp-reporter/src/LspReporter.ts
new file mode 100644
index 000000000..2ffdbe048
--- /dev/null
+++ b/packages/reporters/lsp-reporter/src/LspReporter.ts
@@ -0,0 +1,473 @@
+import type {Diagnostic as ParcelDiagnostic} from '@atlaspack/diagnostic';
+import type {BundleGraph, FilePath, PackagedBundle} from '@atlaspack/types';
+// @ts-expect-error - TS7016 - Could not find a declaration file for module 'ps-node'. '/home/ubuntu/parcel/node_modules/ps-node/index.js' implicitly has an 'any' type.
+import type {Program, Query} from 'ps-node';
+import type {Diagnostic, DocumentUri} from 'vscode-languageserver';
+import type {MessageConnection} from 'vscode-jsonrpc/node';
+import type {ParcelSeverity} from './utils';
+
+import {
+ DefaultMap,
+ getProgressMessage,
+ makeDeferredWithPromise,
+} from '@atlaspack/utils';
+import {Reporter} from '@atlaspack/plugin';
+import path from 'path';
+import os from 'os';
+import url from 'url';
+import fs from 'fs';
+import nullthrows from 'nullthrows';
+// @ts-expect-error - TS7016 - Could not find a declaration file for module 'ps-node'. '/home/ubuntu/parcel/node_modules/ps-node/index.js' implicitly has an 'any' type.
+import * as ps from 'ps-node';
+import {promisify} from 'util';
+
+import {createServer} from './ipc';
+import {
+ // @ts-expect-error - TS2305 - Module '"@atlaspack/lsp-protocol"' has no exported member 'PublishDiagnostic'.
+ PublishDiagnostic,
+ NotificationBuildStatus,
+ NotificationWorkspaceDiagnostics,
+ RequestDocumentDiagnostics,
+ RequestImporters,
+} from '@atlaspack/lsp-protocol';
+
+import {
+ DiagnosticSeverity,
+ DiagnosticTag,
+ normalizeFilePath,
+ parcelSeverityToLspSeverity,
+} from './utils';
+import type {FSWatcher} from 'fs';
+
+const lookupPid: (arg1: Query) => Program[] = promisify(ps.lookup);
+
+const ignoreFail = (func: () => void) => {
+ try {
+ func();
+ } catch (e: any) {
+ /**/
+ }
+};
+
+const BASEDIR = fs.realpathSync(path.join(os.tmpdir(), 'parcel-lsp'));
+const SOCKET_FILE = path.join(BASEDIR, `parcel-${process.pid}`);
+const META_FILE = path.join(BASEDIR, `parcel-${process.pid}.json`);
+
+let workspaceDiagnostics: DefaultMap<
+ string,
+ Array
+> = new DefaultMap(() => []);
+
+const getWorkspaceDiagnostics = (): Array =>
+ [...workspaceDiagnostics].map(([uri, diagnostics]: [any, any]) => ({
+ uri,
+ diagnostics,
+ }));
+
+// @ts-expect-error - TS7034 - Variable 'server' implicitly has type 'any' in some locations where its type cannot be determined.
+let server;
+let connections: Array = [];
+
+let bundleGraphDeferrable = makeDeferredWithPromise<
+ BundleGraph | null | undefined
+>();
+let bundleGraph: Promise | null | undefined> =
+ bundleGraphDeferrable.promise;
+
+let watchStarted = false;
+let lspStarted = false;
+// @ts-expect-error - TS7034 - Variable 'watchStartPromise' implicitly has type 'any' in some locations where its type cannot be determined.
+let watchStartPromise;
+
+const LSP_SENTINEL_FILENAME = 'lsp-server';
+const LSP_SENTINEL_FILE = path.join(BASEDIR, LSP_SENTINEL_FILENAME);
+
+async function watchLspActive(): Promise {
+ // Check for lsp-server when reporter is first started
+ try {
+ await fs.promises.access(LSP_SENTINEL_FILE, fs.constants.F_OK);
+ lspStarted = true;
+ } catch {
+ //
+ }
+
+ // @ts-expect-error - TS2769 - No overload matches this call.
+ return fs.watch(BASEDIR, (eventType: string, filename: string) => {
+ switch (eventType) {
+ case 'rename':
+ if (filename === LSP_SENTINEL_FILENAME) {
+ fs.access(LSP_SENTINEL_FILE, fs.constants.F_OK, (err) => {
+ if (err) {
+ lspStarted = false;
+ } else {
+ lspStarted = true;
+ }
+ });
+ }
+ }
+ });
+}
+
+async function doWatchStart(options: PluginOptions) {
+ await fs.promises.mkdir(BASEDIR, {recursive: true});
+
+ // For each existing file, check if the pid matches a running process.
+ // If no process matches, delete the file, assuming it was orphaned
+ // by a process that quit unexpectedly.
+ for (let filename of fs.readdirSync(BASEDIR)) {
+ if (filename.endsWith('.json')) continue;
+ let pid = parseInt(filename.slice('parcel-'.length), 10);
+ let resultList = await lookupPid({pid});
+ if (resultList.length > 0) continue;
+ fs.unlinkSync(path.join(BASEDIR, filename));
+ ignoreFail(() => fs.unlinkSync(path.join(BASEDIR, filename + '.json')));
+ }
+
+ server = await createServer(SOCKET_FILE, (connection) => {
+ // console.log('got connection');
+ connections.push(connection);
+ connection.onClose(() => {
+ connections = connections.filter((c) => c !== connection);
+ });
+
+ connection.onRequest(RequestDocumentDiagnostics, async (uri) => {
+ let graph = await bundleGraph;
+ if (!graph) return;
+
+ // @ts-expect-error - TS2345 - Argument of type 'CancellationToken' is not assignable to parameter of type 'string'.
+ return getDiagnosticsUnusedExports(graph, uri);
+ });
+
+ connection.onRequest(RequestImporters, async (params) => {
+ let graph = await bundleGraph;
+ if (!graph) return null;
+
+ // @ts-expect-error - TS2345 - Argument of type 'CancellationToken' is not assignable to parameter of type 'string'.
+ return getImporters(graph, params);
+ });
+
+ sendDiagnostics();
+ });
+ await fs.promises.writeFile(
+ META_FILE,
+ JSON.stringify({
+ projectRoot: options.projectRoot,
+ pid: process.pid,
+ argv: process.argv,
+ }),
+ );
+}
+
+watchLspActive();
+
+export default new Reporter({
+ async report({event, options}) {
+ if (event.type === 'watchStart') {
+ watchStarted = true;
+ }
+
+ if (watchStarted && lspStarted) {
+ // @ts-expect-error - TS7005 - Variable 'watchStartPromise' implicitly has an 'any' type.
+ if (!watchStartPromise) {
+ watchStartPromise = doWatchStart(options);
+ }
+ // @ts-expect-error - TS7005 - Variable 'watchStartPromise' implicitly has an 'any' type.
+ await watchStartPromise;
+ }
+
+ switch (event.type) {
+ case 'watchStart': {
+ break;
+ }
+
+ case 'buildStart': {
+ bundleGraphDeferrable = makeDeferredWithPromise();
+ bundleGraph = bundleGraphDeferrable.promise;
+ updateBuildState('start');
+ clearDiagnostics();
+ break;
+ }
+ case 'buildSuccess':
+ bundleGraphDeferrable.deferred.resolve(event.bundleGraph);
+ updateBuildState('end');
+ sendDiagnostics();
+ break;
+ case 'buildFailure': {
+ bundleGraphDeferrable.deferred.resolve(undefined);
+ updateDiagnostics(event.diagnostics, 'error', options.projectRoot);
+ updateBuildState('end');
+ sendDiagnostics();
+ break;
+ }
+ case 'log':
+ if (
+ // @ts-expect-error - TS2339 - Property 'diagnostics' does not exist on type 'ProgressLogEvent | DiagnosticLogEvent | TextLogEvent'.
+ event.diagnostics != null &&
+ (event.level === 'error' ||
+ event.level === 'warn' ||
+ event.level === 'info' ||
+ event.level === 'verbose')
+ ) {
+ updateDiagnostics(
+ event.diagnostics,
+ event.level,
+ options.projectRoot,
+ );
+ }
+ break;
+ case 'buildProgress': {
+ let message = getProgressMessage(event);
+ if (message != null) {
+ updateBuildState('progress', message);
+ }
+ break;
+ }
+ case 'watchEnd':
+ connections.forEach((c) => c.end());
+ // @ts-expect-error - TS7005 - Variable 'server' implicitly has an 'any' type.
+ await server.close();
+ ignoreFail(() => fs.unlinkSync(META_FILE));
+ break;
+ }
+ },
+}) as Reporter;
+
+function updateBuildState(
+ state: 'start' | 'progress' | 'end',
+ message?: string,
+) {
+ connections.forEach((c) =>
+ c.sendNotification(NotificationBuildStatus, state, message),
+ );
+}
+
+function clearDiagnostics() {
+ workspaceDiagnostics.clear();
+}
+function sendDiagnostics() {
+ // console.log('send', getWorkspaceDiagnostics());
+ connections.forEach((c) =>
+ c.sendNotification(
+ NotificationWorkspaceDiagnostics,
+ getWorkspaceDiagnostics(),
+ ),
+ );
+}
+
+function updateDiagnostics(
+ parcelDiagnostics: Array,
+ parcelSeverity: ParcelSeverity,
+ projectRoot: FilePath,
+): void {
+ for (let diagnostic of parcelDiagnostics) {
+ const codeFrames = diagnostic.codeFrames;
+ if (codeFrames == null) {
+ continue;
+ }
+
+ const firstCodeFrame = codeFrames[0];
+ const filePath = firstCodeFrame.filePath;
+ if (filePath == null) {
+ continue;
+ }
+
+ // We use the first highlight of the first codeFrame as the main Diagnostic,
+ // and we place everything else in the current Parcel diagnostic
+ // in relatedInformation
+ // https://code.visualstudio.com/api/references/vscode-api#DiagnosticRelatedInformation
+ const firstFrameHighlight = codeFrames[0].codeHighlights[0];
+ if (firstFrameHighlight == null) {
+ continue;
+ }
+
+ const relatedInformation: Array = [];
+ for (const codeFrame of codeFrames) {
+ for (const highlight of codeFrame.codeHighlights) {
+ const filePath = codeFrame.filePath;
+ if (highlight === firstFrameHighlight || filePath == null) {
+ continue;
+ }
+
+ relatedInformation.push({
+ location: {
+ uri: `file://${normalizeFilePath(filePath, projectRoot)}`,
+ range: {
+ start: {
+ line: highlight.start.line - 1,
+ character: highlight.start.column - 1,
+ },
+ end: {
+ line: highlight.end.line - 1,
+ character: highlight.end.column,
+ },
+ },
+ },
+ message: highlight.message ?? diagnostic.message,
+ });
+ }
+ }
+
+ workspaceDiagnostics
+ .get(`file://${normalizeFilePath(filePath, projectRoot)}`)
+ .push({
+ range: {
+ start: {
+ line: firstFrameHighlight.start.line - 1,
+ character: firstFrameHighlight.start.column - 1,
+ },
+ end: {
+ line: firstFrameHighlight.end.line - 1,
+ character: firstFrameHighlight.end.column,
+ },
+ },
+ source: diagnostic.origin,
+ severity: parcelSeverityToLspSeverity(parcelSeverity),
+ message:
+ diagnostic.message +
+ (firstFrameHighlight.message == null
+ ? ''
+ : ' ' + firstFrameHighlight.message),
+ relatedInformation,
+ });
+ }
+}
+
+function getDiagnosticsUnusedExports(
+ bundleGraph: BundleGraph,
+ document: string,
+): Array {
+ let filename = url.fileURLToPath(document);
+ let diagnostics: Array = [];
+
+ let asset = bundleGraph.traverse((node, context, actions) => {
+ if (node.type === 'asset' && node.value.filePath === filename) {
+ actions.stop();
+ return node.value;
+ }
+ });
+
+ if (asset) {
+ const generateDiagnostic = (loc: SourceLocation, type: string) => ({
+ range: {
+ start: {
+ line: loc.start.line - 1,
+ character: loc.start.column - 1,
+ },
+ end: {
+ line: loc.end.line - 1,
+ character: loc.end.column,
+ },
+ },
+ source: '@atlaspack/core',
+ severity: DiagnosticSeverity.Hint,
+ message: `Unused ${type}.`,
+ tags: [DiagnosticTag.Unnecessary],
+ });
+
+ // @ts-expect-error - TS2345 - Argument of type 'unknown' is not assignable to parameter of type 'Dependency | Asset'.
+ let usedSymbols = bundleGraph.getUsedSymbols(asset);
+ if (usedSymbols) {
+ // @ts-expect-error - TS2571 - Object is of type 'unknown'.
+ for (let [exported, symbol] of asset.symbols) {
+ if (!usedSymbols.has(exported)) {
+ if (symbol.loc) {
+ diagnostics.push(generateDiagnostic(symbol.loc, 'export'));
+ }
+ }
+ }
+ // if (usedSymbols.size === 0 && asset.sideEffects !== false) {
+ // diagnostics.push({
+ // range: {
+ // start: {
+ // line: 0,
+ // character: 0,
+ // },
+ // end: {
+ // line: 0,
+ // character: 1,
+ // },
+ // },
+ // source: '@atlaspack/core',
+ // severity: DiagnosticSeverity.Warning,
+ // message: `Asset has no used exports, but is not marked as sideEffect-free so it cannot be excluded automatically.`,
+ // });
+ // }
+ }
+
+ // @ts-expect-error - TS2571 - Object is of type 'unknown'.
+ for (let dep of asset.getDependencies()) {
+ let usedSymbols = bundleGraph.getUsedSymbols(dep);
+ if (usedSymbols) {
+ for (let [exported, symbol] of dep.symbols) {
+ if (!usedSymbols.has(exported) && symbol.isWeak && symbol.loc) {
+ diagnostics.push(generateDiagnostic(symbol.loc, 'reexport'));
+ }
+ }
+ }
+ }
+ }
+ return diagnostics;
+}
+
+// function getDefinition(
+// bundleGraph: BundleGraph,
+// document: string,
+// position: Position,
+// ): Array | void {
+// let filename = url.fileURLToPath(document);
+
+// let asset = bundleGraph.traverse((node, context, actions) => {
+// if (node.type === 'asset' && node.value.filePath === filename) {
+// actions.stop();
+// return node.value;
+// }
+// });
+
+// if (asset) {
+// for (let dep of bundleGraph.getDependencies(asset)) {
+// let loc = dep.loc;
+// if (loc && isInRange(loc, position)) {
+// let resolution = bundleGraph.getResolvedAsset(dep);
+// if (resolution) {
+// return [
+// {
+// originSelectionRange: {
+// start: {
+// line: loc.start.line - 1,
+// character: loc.start.column - 1,
+// },
+// end: {line: loc.end.line - 1, character: loc.end.column},
+// },
+// targetUri: `file://${resolution.filePath}`,
+// targetRange: RANGE_DUMMY,
+// targetSelectionRange: RANGE_DUMMY,
+// },
+// ];
+// }
+// }
+// }
+// }
+// }
+
+function getImporters(
+ bundleGraph: BundleGraph,
+ document: string,
+): Array | null {
+ let filename = url.fileURLToPath(document);
+
+ let asset = bundleGraph.traverse((node, context, actions) => {
+ if (node.type === 'asset' && node.value.filePath === filename) {
+ actions.stop();
+ return node.value;
+ }
+ });
+
+ if (asset) {
+ // @ts-expect-error - TS2345 - Argument of type 'unknown' is not assignable to parameter of type 'Asset'.
+ let incoming = bundleGraph.getIncomingDependencies(asset);
+ return incoming
+ .filter((dep) => dep.sourcePath != null)
+ .map((dep) => `file://${nullthrows(dep.sourcePath)}`);
+ }
+ return null;
+}
diff --git a/packages/reporters/lsp-reporter/src/ipc.js b/packages/reporters/lsp-reporter/src/ipc.js
deleted file mode 100644
index 4822a4ce3..000000000
--- a/packages/reporters/lsp-reporter/src/ipc.js
+++ /dev/null
@@ -1,55 +0,0 @@
-// @flow
-import * as net from 'net';
-import type {
- MessageReader,
- MessageWriter,
- MessageConnection,
-} from 'vscode-jsonrpc/node';
-import {
- createMessageConnection,
- SocketMessageReader,
- SocketMessageWriter,
-} from 'vscode-jsonrpc/node';
-
-function createClientPipeTransport(
- pipeName: string,
- onConnected: (reader: MessageReader, writer: MessageWriter) => void,
-): Promise<{|close: () => Promise|}> {
- return new Promise((resolve, reject) => {
- let server: net.Server = net.createServer((socket: net.Socket) => {
- onConnected(
- new SocketMessageReader(socket),
- new SocketMessageWriter(socket),
- );
- });
- server.on('error', reject);
- server.listen(pipeName, () => {
- server.removeListener('error', reject);
- resolve({
- close() {
- return new Promise((res, rej) => {
- server.close(e => {
- if (e) rej(e);
- else res();
- });
- });
- },
- });
- });
- });
-}
-
-export function createServer(
- filename: string,
- setup: (connection: MessageConnection) => void,
-): Promise<{|close: () => Promise|}> {
- return createClientPipeTransport(
- filename,
- (reader: MessageReader, writer: MessageWriter) => {
- let connection = createMessageConnection(reader, writer);
- connection.listen();
-
- setup(connection);
- },
- );
-}
diff --git a/packages/reporters/lsp-reporter/src/ipc.ts b/packages/reporters/lsp-reporter/src/ipc.ts
new file mode 100644
index 000000000..28feb616c
--- /dev/null
+++ b/packages/reporters/lsp-reporter/src/ipc.ts
@@ -0,0 +1,77 @@
+import * as net from 'net';
+import type {
+ MessageReader,
+ MessageWriter,
+ MessageConnection,
+} from 'vscode-jsonrpc/node';
+import {
+ createMessageConnection,
+ SocketMessageReader,
+ SocketMessageWriter,
+} from 'vscode-jsonrpc/node';
+
+function createClientPipeTransport(
+ pipeName: string,
+ onConnected: (reader: MessageReader, writer: MessageWriter) => void,
+): Promise<{
+ close: () => Promise;
+}> {
+ return new Promise(
+ (
+ resolve: (
+ result:
+ | Promise<{
+ close(): Promise;
+ }>
+ | {
+ close(): Promise