Skip to content

Commit

Permalink
feat: standalone version for each package (#23)
Browse files Browse the repository at this point in the history
Co-authored-by: SukkaW <[email protected]>
  • Loading branch information
antfu and SukkaW authored Sep 7, 2023
1 parent 2106f9b commit c822758
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 98 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
{
"files": [
"bump.config.ts",
"*.d.ts",
"packages/**/*.ts",
"packages/**/*.tsx"
],
Expand Down
8 changes: 8 additions & 0 deletions bump.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from 'bumpp';

export default defineConfig({
files: [
'package.json',
'packages/tools/cli/package.json'
]
});
242 changes: 154 additions & 88 deletions create.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,20 @@
const fsPromises = require('fs/promises');
const path = require('path');
const ezspawn = require('@jsdevtools/ez-spawn');
const { PathScurry } = require('path-scurry');
const colors = require('picocolors');
const { dequal } = require('dequal');

const currentPackageJson = require('./package.json');

const { compareAndWriteFile } = require('@nolyfill/internal');
const { fileExists, compareAndWriteFile } = require('@nolyfill/internal');

/**
* @typedef {Object} VirtualPackage
* @prop {string} path
* @prop {Record<string, string>} files
* @prop {import('type-fest').PackageJson} packageJson
*/

const autoGeneratedPackagesList = /** @type {const} */ ([
['array-includes', 'Array.prototype.includes', false],
Expand Down Expand Up @@ -561,10 +571,11 @@ export const allPackages = ${JSON.stringify(allPackagesList, null, 2)};\n`;
),
compareAndWriteFile(
path.join(__dirname, 'DOWNLOAD_STATS.md'),
generateDonwloadStats()
generateDownloadStats()
)
]);

console.log('Updating pnpm-lock.yaml...');
await ezspawn.async('pnpm', ['i']);
})();

Expand All @@ -577,32 +588,14 @@ export const allPackages = ${JSON.stringify(allPackagesList, null, 2)};\n`;
* @param {string | null} [bindTo]
*/
async function createEsShimLikePackage(packageName, packageImplementation, isStatic, extraDependencies = {}, minimumNodeVersion = '>=12.4.0', bindTo = null) {
const packagePath = path.join(__dirname, 'packages/generated', packageName);
await fsPromises.mkdir(
packagePath,
{ recursive: true }
);

await Promise.all([
compareAndWriteFile(
path.join(packagePath, 'implementation.js'),
`'use strict';\nmodule.exports = ${packageImplementation};\n`
),
compareAndWriteFile(
path.join(packagePath, 'polyfill.js'),
`'use strict';\nmodule.exports = () => ${packageImplementation};\n`
),
compareAndWriteFile(
path.join(packagePath, 'shim.js'),
`'use strict';\nmodule.exports = () => ${packageImplementation};\n`
),
compareAndWriteFile(
path.join(packagePath, 'auto.js'),
'\'use strict\';\n/* noop */\n'
),
compareAndWriteFile(
path.join(packagePath, 'index.js'),
[
const pkg = /** @type {VirtualPackage} */ {
path: path.join(__dirname, 'packages/generated', packageName),
files: {
'implementation.js': `'use strict';\nmodule.exports = ${packageImplementation};\n`,
'polyfill.js': `'use strict';\nmodule.exports = () => ${packageImplementation};\n`,
'shim.js': `'use strict';\nmodule.exports = () => ${packageImplementation};\n`,
'auto.js': '\'use strict\';\n/* noop */\n',
'index.js': [
'\'use strict\';',
isStatic
? 'const { makeEsShim } = require(\'@nolyfill/shared\');'
Expand All @@ -615,33 +608,30 @@ async function createEsShimLikePackage(packageName, packageImplementation, isSta
'module.exports = bound;',
''
].join('\n')
),
compareAndWriteFile(
path.join(packagePath, 'package.json'),
`${JSON.stringify({
name: `@nolyfill/${packageName}`,
version: currentPackageJson.version,
repository: {
type: 'git',
url: 'https://github.com/SukkaW/nolyfill',
directory: `packages/generated/${packageName}`
},
main: './index.js',
license: 'MIT',
files: ['*.js'],
scripts: {},
dependencies: {
'@nolyfill/shared': 'workspace:*',
...extraDependencies
},
engines: {
node: minimumNodeVersion
}
}, null, 2)}\n`
)
]);
},
packageJson: {
name: `@nolyfill/${packageName}`,
version: currentPackageJson.version,
repository: {
type: 'git',
url: 'https://github.com/SukkaW/nolyfill',
directory: `packages/generated/${packageName}`
},
main: './index.js',
license: 'MIT',
files: ['*.js'],
scripts: {},
dependencies: {
'@nolyfill/shared': 'workspace:*',
...extraDependencies
},
engines: {
node: minimumNodeVersion
}
}
};

console.log(`[${packageName}] created`);
return writePackage(pkg);
}

/**
Expand All @@ -651,43 +641,34 @@ async function createEsShimLikePackage(packageName, packageImplementation, isSta
* @param {string} [minimumNodeVersion]
*/
async function createSingleFilePackage(packageName, implementation, extraDependencies = {}, minimumNodeVersion = '>=12.4.0') {
const packagePath = path.join(__dirname, 'packages/generated', packageName);
await fsPromises.mkdir(
packagePath,
{ recursive: true }
);

await Promise.all([
compareAndWriteFile(
path.join(packagePath, 'index.js'),
`'use strict';\n${implementation}\n`
),
compareAndWriteFile(
path.join(packagePath, 'package.json'),
`${JSON.stringify({
name: `@nolyfill/${packageName}`,
version: currentPackageJson.version,
repository: {
type: 'git',
url: 'https://github.com/SukkaW/nolyfill',
directory: `packages/generated/${packageName}`
},
main: './index.js',
license: 'MIT',
files: ['*.js'],
scripts: {},
dependencies: extraDependencies,
engines: {
node: minimumNodeVersion
}
}, null, 2)}\n`
)
]);
const pkg = /** @type {VirtualPackage} */ {
path: path.join(__dirname, 'packages/generated', packageName),
files: {
'index.js': `'use strict';\n${implementation}\n`
},
packageJson: {
name: `@nolyfill/${packageName}`,
version: currentPackageJson.version,
repository: {
type: 'git',
url: 'https://github.com/SukkaW/nolyfill',
directory: `packages/generated/${packageName}`
},
main: './index.js',
license: 'MIT',
files: ['*.js'],
scripts: {},
dependencies: extraDependencies,
engines: {
node: minimumNodeVersion
}
}
};

console.log(`[${packageName}] created`);
return writePackage(pkg);
}

function generateDonwloadStats() {
function generateDownloadStats() {
const pkgList = [
...autoGeneratedPackagesList.map(pkg => `@nolyfill/${pkg[0]}`),
...singleFilePackagesList.map(pkg => `@nolyfill/${pkg[0]}`),
Expand All @@ -701,3 +682,88 @@ function generateDonwloadStats() {
).join('\n')
);
}

const ignoredFilesInPackages = new Set(['dist', 'node_modules', 'package.json']);
/**
* @param {VirtualPackage} pkg
*/
async function writePackage(pkg) {
await fsPromises.mkdir(pkg.path, { recursive: true });
let hasChanged = false;

const promises = [];

/** @type {Set<string>} */
const existingFileFullpaths = new Set();

const ps = new PathScurry(pkg.path);
for await (const file of ps) {
if (file.name[0] === '.' || ignoredFilesInPackages.has(file.name)) {
continue;
}

if (file.isFile()) {
const relativePath = file.relativePosix();
if (!(relativePath in pkg.files)) {
// remove extra files
hasChanged = true;

promises.push(
fsPromises.rm(path.join(pkg.path, relativePath))
);
} else {
existingFileFullpaths.add(file.fullpathPosix());
}
}
}

const packageJsonPath = path.join(pkg.path, 'package.json');

// write files, and check if they changed
Object.entries(pkg.files).forEach(([file, content]) => {
const filePath = path.join(pkg.path, file);
promises.push(
compareAndWriteFile(filePath, content, existingFileFullpaths)
.then(written => {
if (written) {
hasChanged = true;
}
})
);
});

// check if package.json changed
promises.push((async () => {
const existingPackageJson = (
existingFileFullpaths.has(packageJsonPath)
|| await fileExists(packageJsonPath)
) ? JSON.parse(await fsPromises.readFile(packageJsonPath, 'utf8'))
: {};

// exclude version from comparison
if (!dequal({ ...existingPackageJson, version: undefined }, { ...pkg.packageJson, version: undefined })) {
hasChanged = true;
}
})());

await Promise.all(promises);

// if the package has changed, bump the version
if (hasChanged) {
pkg.packageJson.version = bumpVersion(pkg.packageJson.version || currentPackageJson.version);
await fsPromises.writeFile(
packageJsonPath,
`${JSON.stringify(pkg.packageJson, null, 2)}\n`,
'utf-8'
);
console.log(colors.blue(`[${pkg.packageJson.name}] bumped to ${pkg.packageJson.version}`));
} else {
console.log(colors.dim(`[${pkg.packageJson.name}] unchanged`));
}
}

function bumpVersion(version) {
// TODO: use semver
const [major, minor, patch] = version.split('.');
return `${major}.${minor}.${+patch + 1}`;
}
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"build:analyze": "ANALYZE=true turbo run build",
"codegen": "node create.cjs",
"prerelease": "turbo run build",
"release": "bumpp -r --all --commit=\"release: %s\" --tag=\"%s\""
"release": "bumpp --all --commit=\"release: %s\" --tag=\"%s\""
},
"devDependencies": {
"@jsdevtools/ez-spawn": "^3.0.4",
Expand All @@ -16,11 +16,14 @@
"@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.1",
"bumpp": "^9.2.0",
"dequal": "2.0.3",
"eslint": "^8.47.0",
"eslint-config-sukka": "^3.0.4",
"eslint-import-resolver-typescript": "^3.6.0",
"eslint-plugin-import": "npm:[email protected]",
"eslint-plugin-n": "^16.0.2",
"path-scurry": "^1.10.1",
"picocolors": "^1.0.0",
"turbo": "^1.10.13",
"type-fest": "^4.2.0",
"typescript": "^5.1.6"
Expand Down
2 changes: 1 addition & 1 deletion packages/tools/internal/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export function fileExists(path: string): Promise<boolean>;
export function compareAndWriteFile(filePath: string, fileContent: string): Promise<void>;
export function compareAndWriteFile(filePath: string, fileContent: string, existingFiles?: Set<string> | undefined): Promise<boolean>;
23 changes: 16 additions & 7 deletions packages/tools/internal/index.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
'use strict';

// @ts-check
const fs = require('fs');
const fsPromises = fs.promises;

/**
* @param {string} path
*/
const fileExists = (path) => fsPromises.access(path, fs.constants.F_OK).then(() => true, () => false);

/**
* If filePath doesn't exist, create new file with content.
* If filePath already exists, compare content with existing file, only update the file when content changes.
* - If filePath doesn't exist, create new file with content, then return true.
* - If filePath already exists, compare content with existing file, update the file when
* content changed and return true, otherwise return false.
*
* @param {string} filePath
* @param {string} fileContent
* @param {Set<string> | undefined} [existingFiles] - Set of existing files, if provided, will help speed up fileExists check.
* @returns {Promise<boolean>}
*/
async function compareAndWriteFile(filePath, fileContent) {
if (await fileExists(filePath)) {
async function compareAndWriteFile(filePath, fileContent, existingFiles) {
if ((existingFiles && existingFiles.has(filePath)) || await fileExists(filePath)) {
const existingContent = await fsPromises.readFile(filePath, { encoding: 'utf8' });
if (existingContent !== fileContent) {
return fsPromises.writeFile(filePath, fileContent, { encoding: 'utf-8' });
await fsPromises.writeFile(filePath, fileContent, { encoding: 'utf-8' });
return true;
}
} else {
return fsPromises.writeFile(filePath, fileContent, { encoding: 'utf-8' });

return false;
}

await fsPromises.writeFile(filePath, fileContent, { encoding: 'utf-8' });
return true;
}

module.exports.fileExists = fileExists;
Expand Down
Loading

0 comments on commit c822758

Please sign in to comment.