Skip to content

Commit

Permalink
feat: add dico build command
Browse files Browse the repository at this point in the history
  • Loading branch information
lihbr committed Jun 2, 2021
1 parent 540827b commit d1d3c0d
Show file tree
Hide file tree
Showing 11 changed files with 315 additions and 3 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"debug": "4.3.1",
"detect-indent": "6.1.0",
"exit": "0.1.2",
"globby": "11.0.3",
"inquirer": "8.1.0",
"latest-version": "5.1.0",
"listr": "0.14.3",
Expand Down
5 changes: 5 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ cli
await commands.init(cli, options);
});

cli.command("build", "Build current project dico").action(async options => {
await middlewares.signedInOnly();
await commands.build(cli, options);
});

cli.version(VERSION);
cli.help(commands.help);

Expand Down
198 changes: 198 additions & 0 deletions src/commands/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import chalk from "chalk";
import { CAC } from "cac";
// @ts-expect-error it's our fork and we don't care
import UpdateRenderer from "@lihbr/listr-update-renderer";
import Listr, { ListrTaskWrapper } from "listr";
import { Observable } from "rxjs";

import { lineBreak, logger } from "../lib";
import * as dicojson from "../core/dicojson";
import * as project from "../core/project";
import * as messages from "../messages";
import exit from "exit";
import { DEFAULT_TIMEOUT } from "../const";
import { ConfigJSON } from "../types";

class BuildError extends Error {}

interface BuildDicoContext {
paths: string[];
keys: { path: string; key: string }[];
schema: ConfigJSON["schema"];
conflicts: { path: string; key: string }[][];
}

const buildDico = new Listr(
[
{
title: "Looking for source files...",
// @ts-expect-error listr types are broken
task: (ctx: BuildDicoContext, task: ListrTaskWrapper) =>
new Observable(observer => {
Promise.all([
project.findSources(),
new Promise(resolve => setTimeout(resolve, DEFAULT_TIMEOUT * 0.5))
])
.then(([paths, _]) => {
if (paths.length === 0) {
observer.error(new BuildError(messages.NoSourceFilesFound));
}

if (paths.length === 1) {
task.title = "One source file found";
} else {
task.title = `${paths.length} source files found`;
}

ctx.paths = paths;
observer.complete();
})
.catch(error => {
observer.error(error);
});
})
},
{
title: "Looking for keys in source files...",
// @ts-expect-error listr types are broken
task: (ctx: BuildDicoContext, task: ListrTaskWrapper) =>
new Observable(observer => {
const promises = [];
for (let i = 0; i < ctx.paths.length; i++) {
const path = ctx.paths[i];

promises.push(
new Promise<{ path: string; key: string }[]>((res, rej) => {
setTimeout(() => {
observer.next(`(${i + 1}/${ctx.paths.length}) ${path}`);
try {
const keys = project.crawlFile(path);
res(keys.map(key => ({ path, key })));
} catch (error) {
rej(error);
}
}, (DEFAULT_TIMEOUT / Math.max(ctx.paths.length, 24)) * i);
})
);
}

Promise.all(promises)
.then(keys => {
const flatKeys = keys.flat();

if (flatKeys.length === 1) {
task.title = "One key found";
} else {
task.title = `${flatKeys.length} keys found`;
}

ctx.keys = flatKeys;
observer.complete();
})
.catch(error => {
observer.error(error);
});
})
},
{
title: "Updating schema...",
// @ts-expect-error listr types are broken
task: (ctx: BuildDicoContext, task: ListrTaskWrapper) =>
new Observable(observer => {
Promise.all([
new Promise(resolve => setTimeout(resolve, DEFAULT_TIMEOUT * 0.5))
]).then(_ => {
try {
const { schema, conflicts } = dicojson.createSchema(ctx.keys);

ctx.schema = schema;
ctx.conflicts = conflicts;

if (conflicts.length) {
if (conflicts.length === 1) {
throw new BuildError(
`1 conflict detected, fix it and try again`
);
} else {
throw new BuildError(
`${conflicts.length} conflicts detected, fix them and try again`
);
}
}

// Update config file
const config = dicojson.read();
config.schema = schema;
dicojson.write(config);

task.title = "Schema updated";
observer.complete();
} catch (error) {
observer.error(error);
}
});
})
}
],
{ renderer: UpdateRenderer }
);

export const build = async (
_: CAC,
__: { [key: string]: never }
): Promise<void> => {
if (!dicojson.exists()) {
logger.error(messages.DicoJSONNotFound, dicojson.getPath());
exit(1);
}

lineBreak();

try {
await buildDico.run();
} catch (error) {
// Handle conflicts error
if (error instanceof BuildError && error.message.includes("conflict")) {
// @ts-expect-error listr types are broken
const context: BuildDicoContext = error.context;

if (!context.conflicts.length) {
throw error;
}

logger.error(`${error.message}:`);
context.conflicts.forEach(conflict => {
if (conflict.length === 1) {
console.log(
` Key ${chalk.cyan(conflict[0].key)} (${chalk.cyan(
conflict[0].path
)}) is conflicting`
);
} else {
console.log(
` Key ${chalk.cyan(conflict[0].key)} (${chalk.cyan(
conflict[0].path
)}) is conflicting with key ${chalk.cyan(
conflict[1].key
)} (${chalk.cyan(conflict[1].path)})${
conflict.length > 2 ? ` and ${conflict.length - 2} more...` : ""
}`
);
}
});

lineBreak();
logger.info(
"Conflicts happen when you have a key declared at the same location of a collection"
);
lineBreak();

exit(1);
} else {
throw error;
}
}

logger.success(messages.CommandSuccessful, "build");
lineBreak();
};
1 change: 1 addition & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from "./signout";
export * from "./whoami";

export * from "./init";
export * from "./build";

export * from "./help";
export * from "./default";
9 changes: 7 additions & 2 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ const initConfig = {
observer.error(error);
}

task.title = "`dico.config.json` created!";
task.title = "`dico.config.json` created";
ctx.config = config;
observer.complete();
})
Expand All @@ -120,9 +120,14 @@ export const init = async (
exit(1);
}

lineBreak();
if (options.force) {
logger.warn(messages.CommandWithForce, "init");
} else {
lineBreak();
}
const { dicos } = await getDicos.run();
const { dico } = await pickDico.run(dicos);
await initConfig.run(dico);
logger.success(messages.CommandSuccessful, "init");
lineBreak();
};
1 change: 1 addition & 0 deletions src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export const DEFAULT_SOURCES_PATTERN = [
"src/**/*.(ts|tsx)",
"src/**/*.vue"
];
export const SOURCES_EXTRACT_REGEX = /\$dico(\.[\w\d]+)+/gm;
export const DEFAULT_TIMEOUT = 1000;
66 changes: 66 additions & 0 deletions src/core/dicojson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,69 @@ export const write = (config: ConfigJSON): void => {
"utf8"
);
};

// TODO: Better test this
export const createSchema = (
keys: { path: string; key: string }[]
): {
schema: ConfigJSON["schema"];
conflicts: { path: string; key: string }[][];
} => {
const schema: ConfigJSON["schema"] = {};
const conflicts: { path: string; key: string }[][] = [];

const uniqueKeys = keys.filter((keyObj, i, keys) => {
return keys.findIndex(i => i.key === keyObj.key) === i;
});

keyloop: for (const keyObject of uniqueKeys) {
const parents = keyObject.key.replace(/^\$dico\./, "").split(".");
let pointer = schema;
const key = parents.pop() as string;

const traversedPath: string[] = [];
for (const parent of parents) {
traversedPath.push(parent);

if (!(parent in pointer)) {
pointer[parent] = {};
} else if (
// Conflict with a shorter key
typeof pointer[parent] === "string"
) {
const conflictPath = ["$dico", ...traversedPath].join(".");
const conflictKeyObj = uniqueKeys.find(i => i.key === conflictPath);
if (conflictKeyObj) {
const existing = conflicts.find(i => i[0].key === conflictKeyObj.key);
if (existing) {
existing.push(keyObject);
} else {
conflicts.push([conflictKeyObj, keyObject]);
}
} else {
conflicts.push([keyObject]);
}
continue keyloop;
} else if (
// Conflict with a longer key
traversedPath.length === parents.length &&
typeof pointer[parent] === "object" &&
typeof (pointer[parent] as ConfigJSON["schema"])[key] !== "undefined"
) {
const conflictKeyObj = uniqueKeys.filter(
i => i.key !== keyObject.key && i.key.startsWith(keyObject.key)
);
conflicts.push([keyObject, ...conflictKeyObj]);
continue keyloop;
}

pointer = pointer[parent] as ConfigJSON["schema"];
}

if (!(key in pointer)) {
pointer[key] = "";
}
}

return { schema, conflicts };
};
26 changes: 26 additions & 0 deletions src/core/project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import fs from "fs";
import path from "path";

import * as dicojson from "./dicojson";
import globby from "globby";
import { SOURCES_EXTRACT_REGEX } from "../const";

export const findSources = async (sources?: string[]): Promise<string[]> => {
if (!sources) {
sources = dicojson.read().sources;
}

const paths = await globby(sources);

return paths;
};

export const crawlFile = (relativePath: string): string[] => {
const absolutePath = path.join(process.cwd(), relativePath);

const blob = fs.readFileSync(absolutePath, "utf8");

const matches = blob.match(SOURCES_EXTRACT_REGEX);

return matches ? matches : [];
};
8 changes: 8 additions & 0 deletions src/messages.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { NAME } from "./const";

export const InvalidCommand =
"Invalid command: `%s`, run `%s --help` for all commands";

Expand All @@ -14,3 +16,9 @@ export const NoDicoFoundDeveloper =
export const DicoJSONNotFound = "`dico.config.json` not found at `%s`";
export const DicoJSONAlreadyExists =
"`dico.config.json` already exists at `%s`!";

export const NoSourceFilesFound =
"No source files found with current `sources` option. Double-check your `dico.config.json` and try again!";

export const CommandSuccessful = `\`${NAME} %s\` successful!`;
export const CommandWithForce = `Running \`${NAME} %s\` with \`force\` flag`;
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface ConfigRC {

export interface ConfigJSON {
dico: string;
sources: string[];
schema: Structure["schema"];
updated_at: string;
}
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2431,7 +2431,7 @@ globby@10.0.0:
merge2 "^1.2.3"
slash "^3.0.0"

globby@^11.0.1, globby@^11.0.3:
globby@11.0.3, globby@^11.0.1, globby@^11.0.3:
version "11.0.3"
resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb"
integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==
Expand Down

0 comments on commit d1d3c0d

Please sign in to comment.