Questions: Entrypoint subcommands + optimizations
jsejcksn opened this issue ยท 9 comments
Hello @hayd ๐
I'm just looking at this project this week, and I have a couple of questions; since the Discussions feature isn't available in this repo, I'm opening this issue.
First, in the example Dockerfile:
It seems like lines 11โ14 are not necessary since line 17 adds all files (including deps.ts
) and line 19 caches any dependencies imported by main.ts
(including deps.ts
if it even exists and is used). Am I overlooking something?
Second, I was wondering about the list of subcommands in the entrypoint file (line 5):
https://github.com/hayd/deno-docker/blob/b118b31a7b3afe860f83ff7e2de6607810f18f0d/_entry.sh#L1-L9
It seems that some of the deno subcommands available in deno --help
differ from what's in the list. I wrote a script to compare them and here are the results using deno v1.8.3
:
parse_subcommands_from_help.ts
:
// deno --help | deno run parse_subcommands_from_help.ts <pipe-separated command list>
const parseError = new Error('Subcommands could not be parsed');
const parseLine = (line: string): [name: string, description: string] => {
const regex = /^\s*(?<name>[^\s]+)\s+(?<description>.+)$/u;
const {name, description} = line.trim().match(regex)?.groups ?? {};
if (!name || !description) throw parseError;
return [name, description];
};
export const parseSubcommands = (helpText: string): Record<string, string> => {
const lines = helpText.split('\n');
const startIndex = lines.findIndex(line => line.trim().toLowerCase() === 'subcommands:') + 1;
if (startIndex === 0) throw parseError;
const subcommands = {} as Record<string, string>;
for (let i = startIndex; i < lines.length - 1; i += 1) {
const line = lines[i];
if (!line.trim()) break;
const [name, description] = parseLine(line);
subcommands[name] = description;
}
return subcommands;
};
const compareSets = <T extends string | number>(setA: Set<T>, setB: Set<T>): {
a: T[];
b: T[];
common: T[];
same: boolean;
} => {
let same = setA.size === setB.size;
if (same) {
for (const val of setA) {
if (setB.has(val)) continue;
same = false;
break;
}
}
if (same) return {a: [], b: [], common: [...setA].sort(), same};
const common = [...setA].filter(val => setB.has(val)).sort();
const a = [...setA].filter(val => !setB.has(val)).sort();
const b = [...setB].filter(val => !setA.has(val)).sort();
return {a, b, common, same};
};
const joinListWithIndent = (list: string[], spaces = 2): string => {
return list.map(str => str.padStart(str.length + spaces, ' ')).join('\n');
};
const main = async (): Promise<void> => {
const arg = Deno.args[0] ?? '';
const inputList = new Set(arg.split('|')
.map(str => str.trim())
.filter(str => str.length));
if (inputList.size === 0) throw new Error('No input list provided');
const stdin = new TextDecoder().decode(await Deno.readAll(Deno.stdin));
const subcommands = parseSubcommands(stdin);
const helpList = new Set(Object.keys(subcommands));
const {a, b, common} = compareSets(inputList, helpList);
console.log(`common:\n${joinListWithIndent(common)}`);
console.log(`only in input list:\n${joinListWithIndent(a)}`);
console.log(`only in deno help:\n${joinListWithIndent(b)}`);
};
if (import.meta.main) main();
% deno --help | deno run parse_subcommands_from_help.ts "bundle | cache | completions | doc | eval | fmt | help | info | link | repl | run | test | types"
common:
bundle
cache
completions
doc
eval
fmt
help
info
repl
run
test
types
only in input list:
link
only in deno help:
compile
coverage
install
lint
lsp
upgrade
What does link
do? Also, are the other commands omitted intentionally or were they introduced after you last updated the file?
caching deps.ts and they're download/compilation is useful for local development (faster iteration if change in main.ts).
good detective work re the other commands, no idea what link is (perhaps a removed subcommand?) but definitely looks like the new ones should be added to the _entry.sh list. parse_subcommands might make a good test!
caching deps.ts and they're download/compilation is useful for local development (faster iteration if change in main.ts).
Does this mean the case where deps.ts
is modified but main.ts
is not?
No, the opposite (and more frequent case) main.ts is modified but deps.ts was unchanged (and don't require re-download and re-compilation).
A more complex example: https://github.com/hayd/deno-lambda/blob/c9447feee548d91cbe9a91fb1cce268a1e427f16/tests/Dockerfile#L40
If there are no dependencies (and therefore no deps.ts
) will those lines cause an error?
yes...
This idea is really nice, but parsing the output of deno
may be a bit dangerous.
Another approach that I can think of would be to make the deno
main repository to upload an additional artifact in the releases such as binary-metadata.json
containing stuff like:
{
"options": ["--help", "--version", "-v"],
"commands": ["repl", "run", "compile"]
}
In which its generation could be automated somehow in the Rust level.
Closing this as the list of subcommands in the _entry.sh
script match the current subcommand as of 1.13.2. As for caching deps in the example; it can help by creating a layer including deps that is easily cached, but is likely only useful for local dev, I have no issues removing it, but can be reopened in another issue if it's something you feel strongly about.
Closing this as the list of subcommands in the
_entry.sh
script match the current subcommand as of 1.13.2
@wperron What if the list of subcommands change in Deno? What is the maintainers' plan for ensuring that _entry.sh
stays in sync?
Closing this as the list of subcommands in the
_entry.sh
script match the current subcommand as of 1.13.2@wperron What if the list of subcommands change in Deno? What is the maintainers' plan for ensuring that
_entry.sh
stays in sync?
Given new subcommand are added very rarely now I think we can manage by handling it manually. If it becomes a burden in the future we can reopen then.