Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update build process to allow for future additional entrypoints #8

Merged
merged 4 commits into from
Jan 13, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
349 changes: 214 additions & 135 deletions scripts/rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import fs from 'node:fs/promises';
import path from 'node:path/posix';
import { readFileSync } from 'node:fs';

import * as prettier from 'prettier';
Expand All @@ -8,6 +10,61 @@ import terser from '@rollup/plugin-terser';
import cjsCheck from 'rollup-plugin-cjs-check';
import dts from 'rollup-plugin-dts';

const normalize = name => []
.concat(name)
.join(' ')
.replace(/[@\s/.]+/g, ' ')
.trim()
.replace(/\s+/, '-')
.toLowerCase();

const extension = name => {
if (/\.d.ts$/.test(name)) {
return '.d.ts';
} else {
return path.extname(name);
}
};

const meta = JSON.parse(readFileSync('package.json'));
const name = normalize(meta.name);

const externalModules = [
...Object.keys(meta.dependencies || {}),
...Object.keys(meta.peerDependencies || {}),
];

const external = new RegExp(`^(${externalModules.join('|')})($|/)`);

const exports = {};
for (const key in meta.exports) {
const entry = meta.exports[key];
if (typeof entry === 'object' && !!entry.source) {
const entryPath = normalize(key);
const entryName = normalize([name, entryPath]);
exports[entryName] = {
path: entryPath,
...entry,
};
}
}

const commonConfig = {
input: Object.entries(exports).reduce((input, [exportName, entry]) => {
input[exportName] = entry.source;
return input;
}, {}),
onwarn: () => {},
external(id) {
return external.test(id);
},
treeshake: {
unknownGlobalSideEffects: false,
tryCatchDeoptimization: false,
moduleSideEffects: false,
},
};

const commonPlugins = [
resolve({
extensions: ['.mjs', '.js', '.ts'],
Expand All @@ -22,154 +79,176 @@ const commonPlugins = [
}),
];

const packageJson = JSON.parse(readFileSync('package.json'));
const externalModules = Object.keys(packageJson.dependencies);
const externalPredicate = new RegExp(`^(${externalModules.join('|')})($|/)`);
const external = (id) => externalPredicate.test(id);
const commonOutput = {
dir: './',
exports: 'auto',
sourcemap: true,
sourcemapExcludeSources: false,
hoistTransitiveImports: false,
indent: false,
freeze: false,
strict: false,
generatedCode: {
preset: 'es5',
reservedNamesAsProps: false,
objectShorthand: false,
constBindings: false,
},
};

const jsPlugins = [
...commonPlugins,
const outputPlugins = [
{
name: 'outputPackageJsons',
async writeBundle() {
for (const key in exports) {
const entry = exports[key];
if (entry.path) {
const output = path.relative(entry.path, process.cwd());
const json = JSON.stringify({
name: key,
private: true,
version: '0.0.0',
main: path.join(output, entry.require),
module: path.join(output, entry.import),
types: path.join(output, entry.types),
source: path.join(output, entry.source),
exports: {
'.': {
types: path.join(output, entry.types),
import: path.join(output, entry.import),
require: path.join(output, entry.require),
source: path.join(output, entry.source),
},
},
}, null, 2);

babel({
babelrc: false,
babelHelpers: 'bundled',
extensions: ['mjs', 'js', 'jsx', 'ts', 'tsx'],
exclude: 'node_modules/**',
presets: [],
plugins: [
'@babel/plugin-transform-typescript',
'@babel/plugin-transform-block-scoping',
],
}),
];
await fs.mkdir(entry.path, { recursive: true });
await fs.writeFile(path.join(entry.path, 'package.json'), json);
}
}
},
},

cjsCheck(),

const dtsPlugins = [
...commonPlugins,
dts(),
terser({
warnings: true,
ecma: 2015,
keep_fnames: true,
ie8: false,
compress: {
pure_getters: true,
toplevel: true,
booleans_as_integers: false,
keep_fnames: true,
keep_fargs: true,
if_return: false,
ie8: false,
sequences: false,
loops: false,
conditionals: false,
join_vars: false,
},
mangle: {
module: true,
keep_fnames: true,
},
output: {
beautify: true,
braces: true,
indent_level: 2,
},
}),
];

const output = format => {
const extension = format === 'esm' ? '.mjs' : '.js';
return {
chunkFileNames: '[hash]' + extension,
entryFileNames: '[name]' + extension,
dir: './dist',
exports: 'named',
sourcemap: true,
sourcemapExcludeSources: false,
indent: false,
freeze: false,
strict: false,
format,
// NOTE: All below settings are important for cjs-module-lexer to detect the export
// When this changes (and terser mangles the output) this will interfere with Node.js ESM intercompatibility
esModule: format !== 'esm',
externalLiveBindings: format !== 'esm',
export default [
{
...commonConfig,
plugins: [
cjsCheck(),

terser({
warnings: true,
ecma: 2015,
keep_fnames: true,
ie8: false,
compress: {
pure_getters: true,
toplevel: true,
booleans_as_integers: false,
keep_fnames: true,
keep_fargs: true,
if_return: false,
ie8: false,
sequences: false,
loops: false,
conditionals: false,
join_vars: false,
...commonPlugins,
babel({
babelrc: false,
babelHelpers: 'bundled',
extensions: ['mjs', 'js', 'jsx', 'ts', 'tsx'],
exclude: 'node_modules/**',
presets: [],
plugins: [
'@babel/plugin-transform-typescript',
'@babel/plugin-transform-block-scoping',
],
}),
],
output: [
{
...commonOutput,
format: 'esm',
chunkFileNames(chunk) {
return `dist/chunks/[name]-chunk${extension(chunk.name) || '.mjs'}`;
},
entryFileNames(chunk) {
return chunk.isEntry
? path.normalize(exports[chunk.name].import)
: `dist/[name].mjs`;
},
mangle: {
module: true,
keep_fnames: true,
plugins: outputPlugins,
},
{
...commonOutput,
format: 'cjs',
esModule: true,
externalLiveBindings: true,
chunkFileNames(chunk) {
return `dist/chunks/[name]-chunk${extension(chunk.name) || '.js'}`;
},
output: {
beautify: true,
braces: true,
indent_level: 2,
entryFileNames(chunk) {
return chunk.isEntry
? path.normalize(exports[chunk.name].require)
: `dist/[name].js`;
},
}),
plugins: outputPlugins,
},
],
generatedCode: {
preset: 'es5',
reservedNamesAsProps: false,
objectShorthand: false,
constBindings: false,
},
};
};

const commonConfig = {
input: {
'gql-tada': './src/index.ts',
},
onwarn: () => {},
external,
treeshake: {
unknownGlobalSideEffects: false,
tryCatchDeoptimization: false,
moduleSideEffects: false,
},
};

const jsConfig = {
...commonConfig,
plugins: jsPlugins,
output: [
output('esm'),
output('cjs'),
],
};

const dtsConfig = {
...commonConfig,
input: {
'gql-tada': './src/index.ts',
},
onwarn: () => {},
external,
plugins: dtsPlugins,
treeshake: {
unknownGlobalSideEffects: false,
tryCatchDeoptimization: false,
moduleSideEffects: false,
},
output: {
dir: './dist',
entryFileNames: '[name].d.ts',
format: 'es',
{
...commonConfig,
plugins: [
{
renderChunk(code, chunk) {
if (chunk.fileName.endsWith('d.ts')) {
const gqlImportRe = /(import\s+(?:[*\s{}\w\d]+)\s*from\s*'graphql';?)/g;
code = code.replace(gqlImportRe, x => '/*!@ts-ignore*/\n' + x);

code = prettier.format(code, {
filepath: chunk.fileName,
parser: 'typescript',
singleQuote: true,
tabWidth: 2,
printWidth: 100,
trailingComma: 'es5',
});

return code;
}
},
},
...commonPlugins,
dts(),
],
},
};
output: {
...commonOutput,
sourcemap: false,
format: 'dts',
chunkFileNames(chunk) {
return `dist/chunks/[name]-chunk${extension(chunk.name) || '.d.ts'}`;
},
entryFileNames(chunk) {
return chunk.isEntry
? path.normalize(exports[chunk.name].types)
: `dist/[name].d.ts`;
},
plugins: [
{
renderChunk(code, chunk) {
if (chunk.fileName.endsWith('d.ts')) {
const gqlImportRe = /(import\s+(?:[*\s{}\w\d]+)\s*from\s*'graphql';?)/g;
code = code.replace(gqlImportRe, x => '/*!@ts-ignore*/\n' + x);

export default [
jsConfig,
dtsConfig,
code = prettier.format(code, {
filepath: chunk.fileName,
parser: 'typescript',
singleQuote: true,
tabWidth: 2,
printWidth: 100,
trailingComma: 'es5',
});

return code;
}
},
},
],
},
},
];
Loading