denoland/deno_docker

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:

https://github.com/hayd/deno-docker/blob/b118b31a7b3afe860f83ff7e2de6607810f18f0d/example/Dockerfile#L1-L21

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?

hayd commented

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?

hayd commented

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?

hayd commented

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.