Skip to content

Commit

Permalink
feat(tool-tool): add support for tools that need to be built
Browse files Browse the repository at this point in the history
  • Loading branch information
ianwremmel committed Jun 10, 2024
1 parent 220dda2 commit 932c365
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 75 deletions.
1 change: 1 addition & 0 deletions packages/@clc/nx/src/create-nodes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ export const createNodes = [
executor: '@code-like-a-carpenter/tool-tool:tool',
inputs: ['{projectRoot}/tools/*.json'],
options: {
buildBeforeRun: !mjs,
schemaDir: '{projectRoot}/tools',
},
outputs: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"executors": {
"foundation": {
"description": "No description provided",
"implementation": "./src/__generated__/foundation-executor.ts",
"implementation": "./dist/cjs/__generated__/foundation-executor.cjs",
"schema": "./tools/foundation.json"
}
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 30 additions & 9 deletions packages/@code-like-a-carpenter/tool-tool/src/executors.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export async function addExecutorsToJson(metadata) {
for (const item of metadata.metadata) {
json.executors[item.toolName] = {
description: item.description,
implementation: `./${path.relative(metadata.root, item.executorShimPath)}`,
implementation: `./${path.relative(metadata.root, item.built ? item.buildDirExecutorPath : item.executorShimPath)}`,
schema: `./${path.relative(metadata.root, item.schemaPath)}`,
};
}
Expand Down Expand Up @@ -65,9 +65,29 @@ export async function generateExecutors(metadata) {
path.basename(typesImportPath)
);

await writePrettierFile(
item.executorPath,
`
if (item.built) {
await writePrettierFile(
item.executorPath,
`
import type {Executor} from '@nx/devkit';
import {handler} from '../${item.toolName}.ts';
import type {${item.typesImportName}} from './${typesImportPath}';
const executor: Executor<${item.typesImportName}> = async (args) => {
await handler(args);
return {success: true};
}
export default executor;
`
);
} else {
await writePrettierFile(
item.executorPath,
`
/**
* @template T
* @typedef {import('@nx/devkit').Executor<T>} Executor
Expand All @@ -87,11 +107,11 @@ export const executor = async (args) => {
return {success: true};
}
`
);
);

await writePrettierFile(
item.executorShimPath,
`
await writePrettierFile(
item.executorShimPath,
`
// @ts-expect-error
async function exec(...args) {
const {executor} = await import('./${path.basename(item.executorPath)}');
Expand All @@ -100,7 +120,8 @@ async function exec(...args) {
}
module.exports = exec;
`
);
);
}
})
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import kebabCase from 'lodash.kebabcase';
import {assert} from '@code-like-a-carpenter/assert';
import {writePrettierFile} from '@code-like-a-carpenter/tooling-common';

/** @typedef {import('./metadata.mjs').ToolMetadata} ToolMetadata */
/** @typedef {import('./types.mjs').ToolMetadata} ToolMetadata */

/**
* @param {ToolMetadata} metadata
*/
export async function generatePluginFile({generatedDir, metadata}) {
const pluginPath = path.join(generatedDir, 'plugin.mjs');
export async function generatePluginFile({built, generatedDir, metadata}) {
const pluginPath = path.join(generatedDir, `plugin.${built ? 'ts' : 'mjs'}`);

// @ts-ignore
await writePrettierFile(
Expand All @@ -23,7 +23,7 @@ import {definePlugin} from '@code-like-a-carpenter/cli-core';
${metadata
.map(
(m) =>
`import {handler as ${camelCase(m.toolName)}Handler} from '../${m.toolName}.mjs';`
`import {handler as ${camelCase(m.toolName)}Handler} from '../${m.toolName}.${built ? 'ts' : 'mjs'}';`
)
.join('\n')}
Expand Down
141 changes: 108 additions & 33 deletions packages/@code-like-a-carpenter/tool-tool/src/metadata.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ import upperFirst from 'lodash.upperfirst';
import {assert} from '@code-like-a-carpenter/assert';
import {readPackageJson} from '@code-like-a-carpenter/tooling-common';

/** @typedef {import('./types.mts').CommonToolMetadataItem} CommonToolMetadataItem */
/** @typedef {import('./types.mts').ToolMetadata} ToolMetadata */
/** @typedef {import('./__generated__/tool-types.mts').ToolTool} ToolTool */

/**
* @param {string} schemaDir
* @param {ToolTool} args
* @return {Promise<ToolMetadata>}
*/
export async function loadToolMetadata(schemaDir) {
export async function loadToolMetadata({buildBeforeRun = true, schemaDir}) {
const stats = await lstat(schemaDir);
/** @type {string[]} */
const schemaFiles = [];
assert(stats.isDirectory(), `schema ${schemaDir} must be a directory`);
const files = await readdir(schemaDir);
Expand All @@ -40,54 +43,126 @@ export async function loadToolMetadata(schemaDir) {
const root = path.dirname(rootPkg);
const generatedDir = path.join(root, 'src', '__generated__');

if (buildBeforeRun) {
return {
built: true,
executorsJson: path.join(root, 'executors.json'),
generatedDir,
metadata: await Promise.all(
schemaFiles.map(async (schemaPath) => {
const toolName = path.basename(schemaPath, path.extname(schemaPath));

return {
...(await collectCommonToolMetadataItem({
buildBeforeRun,
generatedDir,
pkg,
schemaFiles,
schemaPath,
toolName,
})),

buildDirExecutorPath: path.join(
root,
'dist',
'cjs',
'__generated__',
`${toolName}-executor.cjs`
),
built: true,
executorPath: path.join(
generatedDir,
`${kebabCase(toolName)}-executor.ts`
),
};
})
),
packageJson: path.join(root, 'package.json'),
root,
};
}

return {
built: false,
executorsJson: path.join(root, 'executors.json'),
generatedDir,
metadata: await Promise.all(
schemaFiles.map(async (schemaPath) => {
const toolName = path.basename(schemaPath, path.extname(schemaPath));

const jsonSchema = await readPackageJson(schemaPath);
assert(
'title' in jsonSchema,
`json schema at ${schemaPath} must have a title`
);
assert(
typeof jsonSchema.title === 'string',
'json schema title must be a string'
);
const typesImportName = upperFirst(camelCase(jsonSchema.title));

return {
commandName:
schemaFiles.length === 1
? toolName
: `${pkg.name
?.split('/')
?.pop()
?.replace(/^tool-/, '')}:${toolName}`,
// note, using || instead of ?? in case there's an empty string.
description: jsonSchema.description || 'No description provided',
executorPath: path.join(
...(await collectCommonToolMetadataItem({
buildBeforeRun,
generatedDir,
`${kebabCase(toolName)}-executor.mjs`
),
pkg,
schemaFiles,
schemaPath,
toolName,
})),

built: false,
executorShimPath: path.join(
generatedDir,
`${kebabCase(toolName)}-executor.shim.cjs`
),
schema: jsonSchema,
schemaPath,
toolName,
typesImportName,
typesPath: path.join(
generatedDir,
`${kebabCase(toolName)}-types.mts`
),
};
})
),
packageJson: path.join(root, 'package.json'),
root,
};
}

/**
* @param {Object} options
* @param {boolean} options.buildBeforeRun
* @param {string} options.generatedDir
* @param {import('@schemastore/package').JSONSchemaForNPMPackageJsonFiles} options.pkg
* @param {string[]} options.schemaFiles
* @param {string} options.schemaPath
* @param {string} options.toolName
* @returns {Promise<CommonToolMetadataItem>}
*/
async function collectCommonToolMetadataItem({
buildBeforeRun,
generatedDir,
pkg,
schemaFiles,
schemaPath,
toolName,
}) {
const jsonSchema = await readPackageJson(schemaPath);
assert(
'title' in jsonSchema,
`json schema at ${schemaPath} must have a title`
);
assert(
typeof jsonSchema.title === 'string',
'json schema title must be a string'
);
const typesImportName = upperFirst(camelCase(jsonSchema.title));

return {
commandName:
schemaFiles.length === 1
? toolName
: `${pkg.name
?.split('/')
?.pop()
?.replace(/^tool-/, '')}:${toolName}`,
// note, using || instead of ?? in case there's an empty string.
description: jsonSchema.description || 'No description provided',
executorPath: path.join(
generatedDir,
`${kebabCase(toolName)}-executor.mjs`
),
schema: jsonSchema,
schemaPath,
toolName,
typesImportName,
typesPath: path.join(
generatedDir,
`${kebabCase(toolName)}-types.${buildBeforeRun ? 'ts' : 'mts'}`
),
};
}
41 changes: 22 additions & 19 deletions packages/@code-like-a-carpenter/tool-tool/src/tool.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ import {loadToolMetadata} from './metadata.mjs';
* @param {ToolTool} args
* @return {Promise<void>}
*/
export async function handler({schemaDir}) {
const metadata = await loadToolMetadata(schemaDir);
export async function handler(args) {
const metadata = await loadToolMetadata(args);
for (const {schemaPath, typesPath} of metadata.metadata) {
await mkdir(path.dirname(typesPath), {recursive: true});

Expand Down Expand Up @@ -104,7 +104,7 @@ async function generateHandlers(metadata) {
const handlerPath = path.join(
metadata.root,
'src',
`${kebabCase(item.toolName)}.mjs`
`${kebabCase(item.toolName)}.${metadata.built ? 'ts' : 'mjs'}`
);

if (!existsSync(handlerPath)) {
Expand All @@ -119,28 +119,31 @@ export async function handler(args: ${item.typesImportName}): Promise<void> {}`
})
);

const indexFile = path.join(
metadata.root,
'src',
`index.${metadata.built ? 'ts' : 'mjs'}`
);

const pluginFile = `./${path.join(
'__generated__',
`plugin.${metadata.built ? 'ts' : 'mjs'}`
)}`;

const importLine = `export {plugin as default} from '${pluginFile}';`;

try {
const indexContent = await readFile(
path.join(metadata.root, 'src', 'index.mjs'),
'utf-8'
);

if (
!indexContent.includes(
`export {plugin as default} from './__generated__/plugin.mjs';`
)
) {
const indexContent = await readFile(indexFile, 'utf-8');

if (!indexContent.includes(importLine)) {
await writePrettierFile(
path.join(metadata.root, 'src', 'index.mjs'),
`export {plugin as default} from './__generated__/plugin.mjs';\n${indexContent.trim()}`
indexFile,
`${importLine}\n${indexContent.trim()}`
);
}
} catch (err) {
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
await writePrettierFile(
path.join(metadata.root, 'src', 'index.mjs'),
`export {plugin as default} from './__generated__/plugin';\n`
);
await writePrettierFile(indexFile, `${importLine}\n`);
return;
}
throw err;
Expand Down
Loading

0 comments on commit 932c365

Please sign in to comment.