-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(command/compile): first version of the compile command
- Loading branch information
1 parent
bf5e641
commit 9566a25
Showing
5 changed files
with
261 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,239 @@ | ||
import { Args, Command, Flags } from '@oclif/core' | ||
import * as p from '@clack/prompts' | ||
import chalk from 'chalk' | ||
import path from 'path' | ||
import fs from 'fs' | ||
import { getModelFromSource, GetModelFromSourceOptions } from '../compilation' | ||
import { RawRules } from '../commons' | ||
import { exitWithError, runWithSpinner } from '../utils/cli' | ||
import { resolveRuleTypes, RuleType } from '../compilation/ruleTypes' | ||
import Engine from 'publicodes' | ||
import { getPackageJson } from '../utils/pjson' | ||
|
||
export default class Compile extends Command { | ||
static override args = { | ||
files: Args.file({ description: 'Files to compile.' }), | ||
} | ||
|
||
static override strict = false | ||
|
||
static override summary = 'Compile publicodes files.' | ||
|
||
static override description = ` | ||
This command will compile all the specified publicodes files into standalone | ||
JSON file importable in any JavaScript along with the TypeScript types | ||
corresponding to the rules. | ||
To avoid passing arguments and flags every time, you can set their values in | ||
the package.json file under the \`publicodes\` key. For example: | ||
{ | ||
// ... | ||
"publicodes": { | ||
"files": ["src/"], | ||
"output": "build" | ||
} | ||
} | ||
` | ||
|
||
static override examples = [ | ||
{ | ||
command: '<%= config.bin %> <%= command.id %>', | ||
description: `Compile all publicodes files in the src/ directory into the build/ directory.`, | ||
}, | ||
{ | ||
command: '<%= config.bin %> <%= command.id %> src/**/*.publicodes', | ||
description: 'Compile all publicodes files in the src/ directory.', | ||
}, | ||
{ | ||
command: | ||
'<%= config.bin %> <%= command.id %> src/file1.publicodes src/file2.publicodes -o ../dist', | ||
description: | ||
'Compile only the specified files into the ../dist/ directory.', | ||
}, | ||
] | ||
|
||
static override flags = { | ||
output: Flags.string({ | ||
char: 'o', | ||
default: 'build', | ||
summary: 'Specify the output directory.', | ||
}), | ||
} | ||
|
||
public async run(): Promise<void> { | ||
const { argv, flags } = await this.parse(Compile) | ||
|
||
p.intro(chalk.bgHex('#2975d1')(' publicodes compile ')) | ||
const filesToCompile: string[] = | ||
argv.length === 0 | ||
? this.config.pjson?.publicodes?.files ?? ['src/'] | ||
: argv | ||
|
||
const outputDir = path.resolve( | ||
flags.output ?? this.config.pjson?.publicodes?.output ?? 'build', | ||
) | ||
|
||
const rawRules = await parseFiles(filesToCompile, { verbose: false }) | ||
const engine = await initEngine(rawRules) | ||
const pkgName = getPackageJson()?.name ?? path.basename(process.cwd()) | ||
|
||
// Create output directory if it doesn't exist | ||
if (!fs.existsSync(outputDir)) { | ||
fs.mkdirSync(outputDir, { recursive: true }) | ||
} | ||
|
||
await this.generateDTS(engine, outputDir) | ||
|
||
await generateBaseFiles(rawRules, outputDir, pkgName) | ||
|
||
p.outro('Compilation complete! 🎉') | ||
} | ||
|
||
async generateDTS(engine: Engine, outputDir: string): Promise<void> { | ||
return runWithSpinner('Generating types', 'Types generated.', () => { | ||
const ruleTypes = resolveRuleTypes(engine) | ||
const serializedRuleTypes = Object.entries(ruleTypes) | ||
.map(([name, type]) => ` "${name}": ${serializeType(type)}`) | ||
.join(',\n') | ||
|
||
const dts = `/** THIS FILE WAS GENERATED BY ${this.config.pjson.name} (v${this.config.pjson.version}). PLEASE, DO NOT EDIT IT MANUALLY. */ | ||
import { Rule } from 'publicodes' | ||
/** | ||
* String representing a date in the format 'DD/MM/YYYY' or 'MM/YYYY'. | ||
*/ | ||
export type PDate = string | ||
/** | ||
* Publicodes boolean type. | ||
*/ | ||
export type PBoolean = 'oui' | 'non' | ||
/** | ||
* String constant are enclosed in single quotes to differentiate them from | ||
* references. | ||
*/ | ||
export type PString = \`'\${string}'\` | ||
/** | ||
* Corresponding Publicodes situation with types inferred for each rule. | ||
*/ | ||
export type TypedSituation = { | ||
${serializedRuleTypes} | ||
} | ||
/** | ||
* All rule names available in the model. | ||
*/ | ||
export type RuleName = keyof TypedSituation | ||
declare let rules: Record<RuleName, Rule> | ||
export default rules | ||
` | ||
fs.writeFileSync(path.join(outputDir, 'index.d.ts'), dts) | ||
}) | ||
} | ||
} | ||
|
||
async function parseFiles( | ||
files: string[], | ||
// TODO: manage options | ||
options: GetModelFromSourceOptions, | ||
): Promise<RawRules> { | ||
return runWithSpinner('Resolving imports', 'Imports resolved.', (spinner) => { | ||
try { | ||
return getModelFromSource(files, { | ||
...options, | ||
logger: { | ||
log: p.log.info, | ||
error: p.log.error, | ||
warn: p.log.warn, | ||
}, | ||
}) | ||
} catch (error) { | ||
exitWithError({ | ||
ctx: 'An error occurred while parsing files:', | ||
msg: error.message, | ||
spinner, | ||
}) | ||
} | ||
}) | ||
} | ||
|
||
async function initEngine(rawRules: RawRules): Promise<Engine> { | ||
return runWithSpinner( | ||
'Checking rules', | ||
`No errors found in ${chalk.bold(Object.keys(rawRules).length)} rules.`, | ||
(spinner) => { | ||
try { | ||
return new Engine(rawRules) | ||
} catch (error) { | ||
exitWithError({ | ||
ctx: 'Parsing rules failed:', | ||
msg: error.message, | ||
spinner, | ||
}) | ||
} | ||
}, | ||
) | ||
} | ||
|
||
async function generateBaseFiles( | ||
rawRules: RawRules, | ||
outputDir: string, | ||
pkgName: string, | ||
): Promise<void> { | ||
return runWithSpinner('Emitting files', 'Files emitted.', async (spinner) => { | ||
try { | ||
// Extract package name without scope | ||
const basePkgName = pkgName.replace(/@.*\//, '') | ||
|
||
// Generate JSON file | ||
const jsonPath = path.join(outputDir, `${basePkgName}.model.json`) | ||
fs.writeFileSync(jsonPath, JSON.stringify(rawRules)) | ||
|
||
generateIndexFile(outputDir, jsonPath) | ||
} catch (error) { | ||
exitWithError({ | ||
ctx: 'An error occurred while generating files:', | ||
msg: error.message, | ||
spinner, | ||
}) | ||
} | ||
}) | ||
} | ||
|
||
function generateIndexFile(outputDir: string, jsonPath: string): void { | ||
fs.writeFileSync( | ||
path.join(outputDir, 'index.js'), | ||
`import rules from './${path.basename(jsonPath)}' assert { type: 'json' } | ||
export default rules`, | ||
) | ||
} | ||
|
||
function serializeType(type: RuleType): string { | ||
const nullable = type.isNullable ? ' | null' : '' | ||
switch (type.type) { | ||
case 'string': { | ||
return `PString${nullable}` | ||
} | ||
case 'number': { | ||
return `number${nullable}` | ||
} | ||
case 'boolean': { | ||
return `PBoolean${nullable}` | ||
} | ||
case 'date': { | ||
return `PDate${nullable}` | ||
} | ||
case 'enum': { | ||
return ( | ||
type.options.map((option) => `"'${option}'"`).join(' | ') + nullable | ||
) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { CLIExecutor, runInDir } from '../cli-utils' | ||
import fs from 'fs' | ||
import path from 'path' | ||
|
||
const cli = new CLIExecutor() | ||
|
||
describe('publicodes compile', () => { | ||
it('should compile with no arguments/flags', async () => { | ||
runInDir('tmp', async (cwd) => { | ||
const { stdout } = await cli.execCommand('compile') | ||
expect(stdout).toContain('Compilation complete!') | ||
expect(fs.existsSync('build')).toBe(true) | ||
expect(fs.existsSync('build/index.js')).toBe(true) | ||
expect(fs.existsSync(`build/${path.basename(cwd)}.model.json`)).toBe(true) | ||
expect(fs.existsSync(`build/index.d.ts`)).toBe(true) | ||
}) | ||
}) | ||
}) |