Skip to content

Commit

Permalink
Add manifest path support to conditional reporter (#206)
Browse files Browse the repository at this point in the history
  • Loading branch information
JakeLane authored Nov 18, 2024
1 parent 6e85f43 commit 80c3a97
Show file tree
Hide file tree
Showing 9 changed files with 313 additions and 54 deletions.
6 changes: 6 additions & 0 deletions packages/core/core/src/public/BundleGraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,9 @@ export default class BundleGraph<TBundle: IBundle>
dep,
bundleToInternalBundle(bundle),
),
`Failed to load depenendency for '${
dep.specifier
}' specifier from '${String(dep.sourcePath)}'`,
);
if (resolvedAsync?.type === 'asset') {
// Single bundle to load dynamically
Expand All @@ -390,6 +393,9 @@ export default class BundleGraph<TBundle: IBundle>
dep,
bundleToInternalBundle(bundle),
),
`Failed to load referenced bundle for '${
dep.specifier
}' specifier from '${String(dep.sourcePath)}'`,
),
this.#graph,
this.#options,
Expand Down
5 changes: 4 additions & 1 deletion packages/dev/eslint-config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ module.exports = {
rules: {
'import/no-extraneous-dependencies': 'off',
'monorepo/no-internal-import': 'off',
'monorepo/no-relative-import': 'off',
'@atlaspack/no-relative-import': 'off',
'mocha/no-exclusive-tests': 'error',
},
},
Expand All @@ -51,6 +51,9 @@ module.exports = {
'no-return-await': 'error',
'require-atomic-updates': 'off',
'require-await': 'error',
// Use internal rule
'monorepo/no-relative-import': 'off',
'@atlaspack/no-relative-import': 'error',
},
settings: {
flowtype: {
Expand Down
1 change: 1 addition & 0 deletions packages/dev/eslint-plugin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ module.exports = {
rules: {
'no-self-package-imports': require('./src/rules/no-self-package-imports'),
'no-ff-module-level-eval': require('./src/rules/no-ff-module-level-eval'),
'no-relative-import': require('./src/rules/no-relative-import'),
},
};
4 changes: 4 additions & 0 deletions packages/dev/eslint-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
"main": "index.js",
"scripts": {},
"dependencies": {
"eslint-module-utils": "^2.1.1",
"get-monorepo-packages": "^1.1.0",
"minimatch": "^3.0.4",
"path-is-inside": "^1.0.2",
"read-pkg-up": "^5.0.0"
},
"devDependencies": {
Expand Down
98 changes: 98 additions & 0 deletions packages/dev/eslint-plugin/src/rules/no-relative-import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Forked from https://github.com/azz/eslint-plugin-monorepo/blob/master/src/rules/no-relative-import.js

/**
* MIT License
Copyright (c) 2017 Lucas Azzola
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

const moduleVisitor = require('eslint-module-utils/moduleVisitor');

const getPackages = require('get-monorepo-packages');
const isInside = require('path-is-inside');
const minimatch = require('minimatch');
const {join, relative, parse} = require('path');
const resolve = require('eslint-module-utils/resolve');

const getPackageDir = (filePath, packages) => {
const match = packages.find((pkg) =>
minimatch(filePath, join(pkg.location, '**')),
);

if (match) {
return match.location;
}
};

module.exports = {
meta: {
docs: {
description:
'Disallow usage of relative imports instead of package names in monorepo',
},
fixable: 'code',
},
create(context) {
const {
options: [moduleUtilOptions],
} = context;
const sourceFsPath = context.getFilename();
const packages = getPackages(process.cwd());

return moduleVisitor.default((node) => {
const resolvedPath = resolve.default(node.value, context);
if (!resolvedPath) {
return;
}
const packageDir = getPackageDir(sourceFsPath, packages);

if (!packageDir || isInside(resolvedPath, packageDir)) {
return;
}

const pkg = packages.find((pkg) => isInside(resolvedPath, pkg.location));
if (!pkg) {
return;
}

const subPackagePath = relative(pkg.location, resolvedPath);
context.report({
node,
message: `Import for monorepo package '${pkg.package.name}' should be absolute.`,
fix: (fixer) => {
let {dir, name} = parse(
`${pkg.package.name}${
subPackagePath !== '.' ? `/${subPackagePath}` : ''
}`,
);

if (name !== '.' && name !== 'index') {
// Remove unneeded suffix
return fixer.replaceText(node, `'${dir}/${name}'`);
} else {
dir = dir.replace(/\/(src)|(lib)/, '');
return fixer.replaceText(node, `'${dir}'`);
}
},
});
}, moduleUtilOptions);
},
};
36 changes: 36 additions & 0 deletions packages/dev/eslint-plugin/test/rules/no-relative-import.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict';
// @flow

const {RuleTester} = require('eslint');
const rule = require('../../src/rules/no-relative-import');

const filename = __filename;

new RuleTester({
parser: require.resolve('@babel/eslint-parser'),
parserOptions: {ecmaVersion: 2018, sourceType: 'module'},
}).run('no-relative-import', rule, {
valid: [{code: "import logger from '@atlaspack/logger';", filename}],
invalid: [
{
code: `import Logger from '../../../../core/logger/src/Logger';`,
errors: [
{
message: `Import for monorepo package '@atlaspack/logger' should be absolute.`,
},
],
filename,
output: "import Logger from '@atlaspack/logger/src/Logger';",
},
{
code: `import type { PluginOptions } from '../../../../core/types-internal/src';`,
errors: [
{
message: `Import for monorepo package '@atlaspack/types-internal' should be absolute.`,
},
],
filename,
output: "import type { PluginOptions } from '@atlaspack/types-internal';",
},
],
});
Original file line number Diff line number Diff line change
@@ -1,62 +1,75 @@
// @flow strict-local
import {relative, join} from 'path';
import {Reporter} from '@atlaspack/plugin';
import type {
Async,
PluginLogger,
PluginOptions,
PluginTracer,
ReporterEvent,
} from '@atlaspack/types-internal';
import {getConfig} from './Config';

export default (new Reporter({
async report({event, options, logger}) {
if (event.type === 'buildSuccess') {
const bundles = event.bundleGraph.getConditionalBundleMapping();

// Replace bundles with file paths
const mapBundles = (bundles) =>
bundles.map((bundle) =>
relative(bundle.target.distDir, bundle.filePath),
);

const manifest = {};
for (const [bundle, conditions] of bundles.entries()) {
const bundleInfo = {};
for (const [key, cond] of conditions) {
bundleInfo[key] = {
ifTrueBundles: mapBundles(cond.ifTrueBundles).reverse(),
ifFalseBundles: mapBundles(cond.ifFalseBundles).reverse(),
};
}

manifest[bundle.target.name] ??= {};
manifest[bundle.target.name][
relative(bundle.target.distDir, bundle.filePath)
] = bundleInfo;
async function report({
event,
options,
logger,
}: {|
event: ReporterEvent,
options: PluginOptions,
logger: PluginLogger,
tracer: PluginTracer,
|}): Async<void> {
if (event.type === 'buildSuccess') {
const bundles = event.bundleGraph.getConditionalBundleMapping();

// Replace bundles with file paths
const mapBundles = (bundles) =>
bundles.map((bundle) => relative(bundle.target.distDir, bundle.filePath));

const manifest = {};
for (const [bundle, conditions] of bundles.entries()) {
const bundleInfo = {};
for (const [key, cond] of conditions) {
bundleInfo[key] = {
// Reverse bundles so we load children bundles first
ifTrueBundles: mapBundles(cond.ifTrueBundles).reverse(),
ifFalseBundles: mapBundles(cond.ifFalseBundles).reverse(),
};
}

// Error if there are multiple targets in the build
const targets = new Set(
event.bundleGraph.getBundles().map((bundle) => bundle.target),
manifest[bundle.target.name] ??= {};
manifest[bundle.target.name][
relative(bundle.target.distDir, bundle.filePath)
] = bundleInfo;
}

const targets = new Set(
event.bundleGraph.getBundles().map((bundle) => bundle.target),
);

const {filename} = await getConfig(options);

for (const target of targets) {
const conditionalManifestFilename = join(target.distDir, filename);

const conditionalManifest = JSON.stringify(
manifest[target.name],
null,
2,
);

for (const target of targets) {
const conditionalManifestFilename = join(
target.distDir,
'conditional-manifest.json',
);

const conditionalManifest = JSON.stringify(
manifest[target.name],
null,
2,
);

await options.outputFS.writeFile(
conditionalManifestFilename,
conditionalManifest,
{mode: 0o666},
);

logger.info({
message:
'Wrote conditional manifest to ' + conditionalManifestFilename,
});
}
await options.outputFS.writeFile(
conditionalManifestFilename,
conditionalManifest,
{mode: 0o666},
);

logger.info({
message: 'Wrote conditional manifest to ' + conditionalManifestFilename,
});
}
},
}): Reporter);
}
}

export default (new Reporter({report}): Reporter);
31 changes: 31 additions & 0 deletions packages/reporters/conditional-manifest/src/Config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// @flow strict-local
import {join} from 'path';
import type {PluginOptions} from '@atlaspack/types-internal';

type Config = {|
filename: string,
|};

export async function getConfig({
env,
inputFS,
projectRoot,
}: PluginOptions): Promise<Config> {
const packageJson = JSON.parse(
await inputFS.readFile(join(projectRoot, 'package.json'), 'utf8'),
);

const config = packageJson['@atlaspack/reporter-conditional-manifest'] ?? {};
for (const [key, value] of Object.entries(config)) {
// Replace values in the format of ${VARIABLE} with their corresponding env
if (typeof value === 'string') {
config[key] = value.replace(/\${([^}]+)}/g, (_, v) => env[v] ?? '');
}
}

const {filename} = config;

return {
filename: filename ?? 'conditional-manifest.json',
};
}
Loading

0 comments on commit 80c3a97

Please sign in to comment.