Repository navigation
feat: add preview command
#1082
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
btea
wants to merge
4
commits into
nodejs:main
Choose a base branch
from
btea:feat/add-preview-script
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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,5 @@ | ||
| --- | ||
| '@doc-kit/cli': minor | ||
| --- | ||
|
|
||
| Add a `doc-kit preview` command that serves the generated docs locally, building first when any `generate` option is passed |
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import generate from './generate.mjs'; | ||
| import preview from './preview.mjs'; | ||
|
|
||
| export default [generate]; | ||
| export default [generate, preview]; |
This file contains hidden or 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,84 @@ | ||
| import { existsSync } from 'node:fs'; | ||
|
|
||
| import createGenerator from '@doc-kit/core'; | ||
| import logger from '@doc-kit/core/logger/index.mjs'; | ||
| import { | ||
| assertRunnableOptions, | ||
| setConfig, | ||
| } from '@doc-kit/core/utils/configuration/index.mjs'; | ||
| import { Command, Option } from 'commander'; | ||
|
|
||
| import { serve } from '../../src/serve.mjs'; | ||
| import { errorWrap } from '../utils.mjs'; | ||
| import { generateOptions } from './generate.mjs'; | ||
|
|
||
| const { runGenerators } = createGenerator(); | ||
|
|
||
| const DEFAULT_OUTPUT = './out'; | ||
|
|
||
| /** | ||
| * Options that only affect serving, and therefore don't trigger a build | ||
| */ | ||
| const SERVE_OPTIONS = ['host', 'port']; | ||
|
|
||
| /** | ||
| * Whether any option besides the serving ones (`--host`, `--port`) was passed | ||
| * on the command line, meaning the docs should be (re)built before serving | ||
| * | ||
| * @param {Command} command - The `preview` command | ||
| * @returns {boolean} | ||
| */ | ||
| const shouldBuild = command => | ||
| command.options.some( | ||
| option => | ||
| !SERVE_OPTIONS.includes(option.attributeName()) && | ||
| command.getOptionValueSource(option.attributeName()) === 'cli' | ||
| ); | ||
|
|
||
| const preview = new Command('preview') | ||
| .description( | ||
| 'Serve the generated docs locally, building first when any ' + | ||
| '`generate` option is passed' | ||
| ) | ||
| .addOption( | ||
| new Option('--port <number>', 'Port to serve on') | ||
| .default(3000) | ||
| .argParser(Number) | ||
| ) | ||
| .addOption( | ||
| new Option('--host <host>', 'Host to listen on').default('localhost') | ||
| ); | ||
|
|
||
| generateOptions.forEach(option => preview.addOption(option)); | ||
|
|
||
| export default preview.action( | ||
| errorWrap(async (opts, command) => { | ||
| if (!Number.isInteger(opts.port) || opts.port < 0 || opts.port > 65535) { | ||
| throw new Error(`Invalid port: ${opts.port}`); | ||
| } | ||
|
|
||
| let output = opts.output ?? DEFAULT_OUTPUT; | ||
|
|
||
| if (shouldBuild(command)) { | ||
| const config = await setConfig(opts); | ||
| assertRunnableOptions(config); | ||
|
|
||
| await runGenerators(config); | ||
|
|
||
| output = config.global.output ?? DEFAULT_OUTPUT; | ||
| } else if (!existsSync(output)) { | ||
| throw new Error( | ||
| `Nothing to serve: \`${output}\` does not exist. Run ` + | ||
| '`doc-kit generate` first, or pass `generate` options to ' + | ||
| '`doc-kit preview` to build before serving.' | ||
| ); | ||
| } | ||
|
|
||
| const server = await serve(output, { host: opts.host, port: opts.port }); | ||
|
|
||
| logger.info( | ||
| `Serving ${output} at http://${opts.host}:${server.address().port}/ ` + | ||
| '(Ctrl+C to stop)' | ||
| ); | ||
| }) | ||
| ); | ||
This file contains hidden or 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,75 @@ | ||
| import assert from 'node:assert/strict'; | ||
| import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; | ||
| import { tmpdir } from 'node:os'; | ||
| import { join } from 'node:path'; | ||
| import { after, before, describe, it } from 'node:test'; | ||
|
|
||
| import { serve } from '../serve.mjs'; | ||
|
|
||
| describe('serve', () => { | ||
| let root; | ||
| let server; | ||
| let base; | ||
|
|
||
| before(async () => { | ||
| root = await mkdtemp(join(tmpdir(), 'doc-kit-serve-')); | ||
|
|
||
| await mkdir(join(root, 'assets'), { recursive: true }); | ||
| await mkdir(join(root, 'empty'), { recursive: true }); | ||
| await writeFile(join(root, 'index.html'), '<h1>Home</h1>'); | ||
| await writeFile(join(root, '404.html'), '<h1>Lost</h1>'); | ||
| await writeFile(join(root, 'assets', 'style.css'), 'body {}'); | ||
|
|
||
| server = await serve(root, { port: 0 }); | ||
| base = `http://localhost:${server.address().port}`; | ||
| }); | ||
|
|
||
| after(async () => { | ||
| server.close(); | ||
| await rm(root, { recursive: true, force: true }); | ||
| }); | ||
|
|
||
| it('serves index.html for the root path', async () => { | ||
| const response = await fetch(`${base}/`); | ||
|
|
||
| assert.equal(response.status, 200); | ||
| assert.equal( | ||
| response.headers.get('content-type'), | ||
| 'text/html; charset=utf-8' | ||
| ); | ||
| assert.equal(await response.text(), '<h1>Home</h1>'); | ||
| }); | ||
|
|
||
| it('serves files with their MIME type', async () => { | ||
| const response = await fetch(`${base}/assets/style.css`); | ||
|
|
||
| assert.equal(response.status, 200); | ||
| assert.equal( | ||
| response.headers.get('content-type'), | ||
| 'text/css; charset=utf-8' | ||
| ); | ||
| assert.equal(await response.text(), 'body {}'); | ||
| }); | ||
|
|
||
| it('falls back to 404.html for missing paths', async () => { | ||
| const response = await fetch(`${base}/missing`); | ||
|
|
||
| assert.equal(response.status, 404); | ||
| assert.equal(await response.text(), '<h1>Lost</h1>'); | ||
| }); | ||
|
|
||
| it('falls back to 404.html for directories without index.html', async () => { | ||
| const response = await fetch(`${base}/empty`); | ||
|
|
||
| assert.equal(response.status, 404); | ||
| assert.equal(await response.text(), '<h1>Lost</h1>'); | ||
| }); | ||
|
|
||
| it('rejects path traversal outside the served directory', async () => { | ||
| // The URL parser normalizes dot segments, but encoded slashes are kept; | ||
| // decoding them turns the path into one that escapes the root | ||
| const response = await fetch(`${base}/a%2F..%2F..%2Fsecret.txt`); | ||
|
|
||
| assert.equal(response.status, 403); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this already defined somewhere? I feel that this command can't really run without an output flag being passed... Is it correct to assume the default should be "out" and if that's the case, then our generator command also should have a default out, no?
cc @avivkeller ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be defined as a default in config