Skip to content

Commit

Permalink
feat: start of typescript plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
boneskull committed Jan 14, 2024
1 parent da61995 commit bea2846
Show file tree
Hide file tree
Showing 50 changed files with 923 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .wallaby.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ module.exports = () => {
instrument: false,
},
{
pattern: './packages/*/test/**/fixture/**/*',
pattern: './packages/plugin-typescript/template/**/*',
instrument: false,
},
{
Expand Down
48 changes: 48 additions & 0 deletions packages/plugin-typescript/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "@midnight-smoker/plugin-typescript",
"version": "0.1.0",
"description": "TypeScript plugin for midnight-smoker",
"repository": {
"type": "git",
"url": "https://github.com/boneskull/midnight-smoker",
"directory": "packages/plugin-typescript"
},
"homepage": "https://github.com/boneskull/midnight-smoker/tree/main/packages/plugin-typescript",
"bugs": {
"url": "https://github.com/boneskull/midnight-smoker/issues"
},
"author": "Christopher Hiller <[email protected]> (https://boneskull.com/)",
"license": "Apache-2.0",
"engines": {
"node": "^16.20.0 || ^18.0.0 || ^20.0.0",
"npm": ">=7"
},
"main": "./dist/index.js",
"types": "/dist/index.d.ts",
"files": [
"dist",
"src",
"template"
],
"keywords": [
"ts",
"typescript",
"midnight-smoker",
"smoker",
"smoke",
"test",
"testing"
],
"scripts": {},
"peerDependencies": {
"midnight-smoker": "^7.0.0",
"typescript": "^5.0.0"
},
"devDependencies": {
"@tsconfig/node-lts": "18.12.5",
"@tsconfig/node16": "16.1.1",
"@tsconfig/node20": "20.1.2",
"execa": "5.1.1",
"midnight-smoker": "7.0.3"
}
}
179 changes: 179 additions & 0 deletions packages/plugin-typescript/src/consumer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/**
* A "Consumer" is a transient package which consumes the package under test.
* This is how we verify compatibility
* @packageDocumentation
*/

import Debug from 'debug';
import type {PackedPackage} from 'midnight-smoker';
import {readFile, writeFile} from 'node:fs/promises';
import {dirname, join, parse} from 'node:path';

const debug = Debug('midnight-smoker:plugin-typescript:consumer');

export interface Consumer {
baseDir: string;
pkgJsonTpl: string;
tsconfigTpl: string;
entryPointTpl: string;
type: 'module' | 'commonjs';
desc: string;
}

interface ConsumerContent {
packageJson: string;
tsconfigJson: string;
entryPoint: string;
}

const LEGACY_ESM_CONSUMER = {
baseDir: join(__dirname, '..', 'template', 'legacy-esm'),
pkgJsonTpl: 'package.json.tpl',
tsconfigTpl: 'tsconfig.json.tpl',
entryPointTpl: 'index.js.tpl',
type: 'module',
desc: 'an ESM package using legacy node module resolution',
} as const satisfies Consumer;

const NODE16_ESM_CONSUMER = {
baseDir: join(__dirname, '..', 'template', 'node16-esm'),
pkgJsonTpl: 'package.json.tpl',
tsconfigTpl: 'tsconfig.json.tpl',
entryPointTpl: 'index.js.tpl',
type: 'module',
desc: 'an ESM package using node16 module resolution',
} as const satisfies Consumer;

const LEGACY_CJS_CONSUMER = {
baseDir: join(__dirname, '..', 'template', 'legacy-cjs'),
pkgJsonTpl: 'package.json.tpl',
tsconfigTpl: 'tsconfig.json.tpl',
entryPointTpl: 'index.js.tpl',
type: 'commonjs',
desc: 'a CJS package using legacy node module resolution',
} as const satisfies Consumer;

const NODE16_CJS_CONSUMER = {
baseDir: join(__dirname, '..', 'template', 'node16-cjs'),
pkgJsonTpl: 'package.json.tpl',
tsconfigTpl: 'tsconfig.json.tpl',
entryPointTpl: 'index.js.tpl',
type: 'commonjs',
desc: 'a CJS package using node16 module resolution',
} as const satisfies Consumer;

export const Consumers = {
'node16-cjs': NODE16_CJS_CONSUMER,
'node16-esm': NODE16_ESM_CONSUMER,
'legacy-cjs': LEGACY_CJS_CONSUMER,
'legacy-esm': LEGACY_ESM_CONSUMER,
} as const;

export async function initConsumers(packedPkg: PackedPackage) {
return Promise.all(
Object.values(Consumers).map(async (consumer) => {
// tricky: the dirname of the tarball filepath is the "package root";
// the consumer will be rendered alongside it.
const tmpDir = dirname(packedPkg.tarballFilepath);
await initConsumer(consumer, packedPkg, tmpDir);
return {...consumer, tmpDir};
}),
);
}

/**
*
* @param consumer
* @returns
* @internal
*/
async function readConsumer(consumer: Consumer): Promise<ConsumerContent> {
const tasks = [
readFile(join(consumer.baseDir, consumer.pkgJsonTpl), 'utf-8').then(
JSON.parse,
),
readFile(join(consumer.baseDir, consumer.tsconfigTpl), 'utf-8').then(
JSON.parse,
),
readFile(join(consumer.baseDir, consumer.entryPointTpl), 'utf-8'),
];

// note the clever assignment of `cwd`
const [packageJson, tsconfigJson, entryPoint] = await Promise.all(tasks);

return {packageJson, tsconfigJson, entryPoint};
}

/**
* Applies {@link PackedPackage} data to a template string
*
* This replaces:
* - `%MODULE%` with {@link PackedPackage.pkgName}. This assumes it is
* resolvable from the eventual destination directory
* @param template - Template
* @param packedPkg - Packed package
* @returns - Contents of template with variables replaced
* @internal
*/
function applyTemplate(template: string, packedPkg: PackedPackage) {
return template.replace(/%MODULE%/g, packedPkg.pkgName);
}

/**
* Given a {@link Consumer} `consumer` and associated contents `content`, write
* the consumer to `dest`.
* @param consumer - Consumer to write
* @param content - Contents of consumer (file content)
* @param dest - Destination directory
*/
async function writeConsumer(
consumer: Consumer,
{entryPoint, packageJson, tsconfigJson}: ConsumerContent,
dest: string,
): Promise<void> {
await Promise.all([
writeFile(
join(dest, parse(consumer.entryPointTpl).name),
entryPoint,
'utf-8',
),
writeFile(
join(dest, 'package.json'),
JSON.stringify(packageJson, null, 2),
'utf-8',
),
writeFile(
join(dest, 'tsconfig.json'),
JSON.stringify(tsconfigJson, null, 2),
'utf-8',
),
]);
}

/**
* Initializes a {@link Consumer} `consumer` configured to consume a
* {@link PackedPackage} `packedPkg`.
*
* {@link PackedPackage.pkgName} must be resolvable from `dest`!
*
* @param consumer - Consumer to initialize
* @param packedPkg - Packed package to configure Consumer for
* @param dest - Destination directory
* @returns Value of `dest`
*/
export async function initConsumer(
consumer: Consumer,
packedPkg: PackedPackage,
dest: string,
): Promise<string> {
const content = await readConsumer(consumer);

// apply template to source file
content.entryPoint = applyTemplate(content.entryPoint, packedPkg);

await writeConsumer(consumer, content, dest);

debug('Consumer from %s initialized in %s', consumer.baseDir, dest);

return dest;
}
12 changes: 12 additions & 0 deletions packages/plugin-typescript/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {PluginAPI} from 'midnight-smoker/plugin';
import compat from './rule/compat';

export * from './consumer';

const ruleDefs = [compat] as const;

export const plugin = ({createRule}: PluginAPI) => {
for (const ruleDef of ruleDefs) {
createRule(ruleDef);
}
};
12 changes: 12 additions & 0 deletions packages/plugin-typescript/src/rule/compat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {defineRule} from 'midnight-smoker/plugin';

const compat = defineRule({
async check() {
return undefined;
},
name: 'compat',
description:
'Ensures types are compatible with various consumer configurations',
});

export default compat;
Empty file.
Empty file.
Empty file.
13 changes: 13 additions & 0 deletions packages/plugin-typescript/src/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "../dist",
"rootDir": ".",
"composite": true,
"resolveJsonModule": true
},
"include": ["./**/*.ts"]
}
7 changes: 7 additions & 0 deletions packages/plugin-typescript/template/legacy-cjs/index.js.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const {foo} = require('%MODULE%');

// @ts-expect-error
const baz = foo / 2;

/** @type {import('%MODULE%').FooString} */
const quux = 'cows';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@midnight-smoker/compat-consumer-legacy-cjs",
"version": "0.0.0",
"description": "package.json template for type consumption",
"private": true,
"main": "index.js"
}
16 changes: 16 additions & 0 deletions packages/plugin-typescript/template/legacy-cjs/tsconfig.json.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"lib": ["es2021"],
"module": "commonjs",
"target": "es2021",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"noEmit": true,
"allowJs": true,
"checkJs": true
},
"files": ["index.js"]
}
7 changes: 7 additions & 0 deletions packages/plugin-typescript/template/legacy-esm/index.js.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const {foo} = require('%MODULE%');

// @ts-expect-error
const baz = foo / 2;

/** @type {import('%MODULE%').FooString} */
const quux = 'cows';
12 changes: 12 additions & 0 deletions packages/plugin-typescript/template/legacy-esm/package.json.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@midnight-smoker/compat-consumer-legacy-esm",
"version": "0.0.0",
"description": "package.json template for type consumption",
"private": true,
"exports": {
".": {
"import": "./index.js"
}
},
"type": "module"
}
16 changes: 16 additions & 0 deletions packages/plugin-typescript/template/legacy-esm/tsconfig.json.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"lib": ["es2021"],
"module": "esnext",
"target": "es2021",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"noEmit": true,
"allowJs": true,
"checkJs": true
},
"files": ["index.js"]
}
7 changes: 7 additions & 0 deletions packages/plugin-typescript/template/node16-cjs/index.js.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const {foo} = require('%MODULE%');

// @ts-expect-error
const baz = foo / 2;

/** @type {import('%MODULE%').FooString} */
const quux = 'cows';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@midnight-smoker/compat-consumer-node16-cjs",
"version": "0.0.0",
"description": "package.json template for type consumption",
"private": true,
"main": "index.js"
}
16 changes: 16 additions & 0 deletions packages/plugin-typescript/template/node16-cjs/tsconfig.json.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"lib": ["es2021"],
"module": "node16",
"target": "es2021",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node16",
"noEmit": true,
"allowJs": true,
"checkJs": true
},
"files": ["index.js"]
}
7 changes: 7 additions & 0 deletions packages/plugin-typescript/template/node16-esm/index.js.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {foo} from '%MODULE%';

// @ts-expect-error
const baz = foo / 2;

/** @type {import('%MODULE%').FooString} */
const quux = 'cows';
Loading

0 comments on commit bea2846

Please sign in to comment.