@lukoweb/apitogo 0.2.18 → 0.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/cli/setup.md CHANGED
@@ -26,13 +26,25 @@ commands add it to your project and `npm install` puts it on `node_modules/.bin`
26
26
 
27
27
  ### New project (recommended)
28
28
 
29
- `npx @lukoweb/apitogo@latest init` scaffolds into `./dev-portal` by default (pass a folder name to
30
- override). It installs dependencies, writes `apitogo.config.json` and secrets artifacts, and
31
- auto-wires nearby OpenAPI (`openapi.yaml` / `swagger.yaml`) plus markdown in `docs/` / `doc/` /
32
- `documents/` / `documentation/` folders:
29
+ `npx @lukoweb/apitogo@latest init` creates a config version 2 project in the current folder. It
30
+ shows what it will create and asks first (pass `--yes` to skip the question), writes
31
+ `apitogo.config.json`, uses an existing `docs/` folder in place, and, in a new folder, creates
32
+ `package.json` and installs `@lukoweb/apitogo`, `react` and `react-dom`:
33
33
 
34
34
  ```bash
35
- npx @lukoweb/apitogo@latest init
35
+ mkdir my-portal && cd my-portal
36
+ npx @lukoweb/apitogo@latest init --name "Books API"
37
+ npx apitogo run
38
+ ```
39
+
40
+ In an existing project the tool never edits your `package.json`. It prints the install command to
41
+ run instead.
42
+
43
+ The older scaffold (a `dev-portal` folder with secrets artifacts and automatic OpenAPI wiring) is
44
+ still available with `--no-v2`:
45
+
46
+ ```bash
47
+ npx @lukoweb/apitogo@latest init --no-v2
36
48
  cd dev-portal
37
49
  npx apitogo run
38
50
  ```
@@ -47,14 +59,16 @@ npx apitogo run
47
59
 
48
60
  #### Scaffold options
49
61
 
50
- | Flag | Effect |
51
- | ------------------------- | ------------------------------------------------------------ |
52
- | `[dir]` | Target folder (default `dev-portal`) |
53
- | `--name <string>` | Site / package name (also used when `dir` is `.`) |
54
- | `--no-install` | Skip `npm install` after scaffold |
55
- | `--overwrite` | Replace an existing non-empty target directory |
56
- | `--json` | Print a JSON result payload on stdout |
57
- | `--openapi-search <mode>` | Auto-discover OpenAPI: `nearby` (default), `site`, or `none` |
62
+ | Flag | Effect |
63
+ | ------------------------- | ------------------------------------------------------------------------ |
64
+ | `[dir]` | Target folder (default: the current folder; `dev-portal` with `--no-v2`) |
65
+ | `--name <string>` | Site / package name (also used when `dir` is `.`) |
66
+ | `--no-install` | Skip `npm install` after scaffold |
67
+ | `--no-v2` | Use the older `dev-portal` scaffold instead of config version 2 |
68
+ | `--yes` | Do not ask for confirmation (config version 2) |
69
+ | `--overwrite` | Replace an existing non-empty target directory |
70
+ | `--json` | Print a JSON result payload on stdout |
71
+ | `--openapi-search <mode>` | Auto-discover OpenAPI: `nearby` (default), `site`, or `none` |
58
72
 
59
73
  `npx @lukoweb/apitogo init` is local only — it does not call the platform. After `config`, run
60
74
  `npx apitogo run` to preview, then `npx apitogo deploy` when ready to go live.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo",
3
- "version": "0.2.18",
3
+ "version": "0.2.21",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -10,6 +10,7 @@ import { docRoutes, type HomeRedirect, homeRedirect } from "./home-redirect.js";
10
10
  import {
11
11
  hasCapability,
12
12
  missingManifestServices,
13
+ requiredServices,
13
14
  type ServiceManifest,
14
15
  serviceReady,
15
16
  stubManifest,
@@ -405,7 +406,8 @@ export async function preparePortal(
405
406
  if (mode === "production") {
406
407
  issues.push(...missingManifestServices(resolved, manifest));
407
408
  }
408
- } else if (mode === "development") {
409
+ } else if (mode === "development" && requiredServices(resolved).length > 0) {
410
+ // Only a portal that depends on a service gets the stand-in: a docs-only portal has nothing to stand in for.
409
411
  effective = stubManifest(resolved);
410
412
  manifestSource = "preview-stub";
411
413
  } else {
@@ -19,6 +19,7 @@ import { loadDevPortalViteDefines } from "./dev-portal-env.js";
19
19
  import { findPackageRoot } from "./package-root.js";
20
20
  import { ensurePagefindDevStub } from "./pagefind-dev-stub.js";
21
21
  import vitePlugin from "./plugin.js";
22
+ import { devServerAllow, stateIgnore } from "./watch-ignore.js";
22
23
  import { getZuploSystemConfigurations } from "./zuplo.js";
23
24
 
24
25
  /** Resolved absolute public directory (Vite default: `<root>/public`). */
@@ -85,6 +86,9 @@ export async function getViteConfig(
85
86
  ]),
86
87
  );
87
88
 
89
+ // The workspace of a config version 2 portal links back to its own project; the watcher must not follow that.
90
+ const ignoreState = stateIgnore(dir);
91
+
88
92
  if (ZuploEnv.isZuplo) {
89
93
  dotenv.config({
90
94
  path: path.resolve(config.__meta.rootDir, "../.env.zuplo"),
@@ -140,7 +144,7 @@ export async function getViteConfig(
140
144
  middlewareMode: true,
141
145
  open: true,
142
146
  fs: {
143
- allow: [dir, path.dirname(dir)],
147
+ allow: devServerAllow(dir, config.__meta.moduleDir),
144
148
  },
145
149
  watch: {
146
150
  ignored: [
@@ -149,6 +153,7 @@ export async function getViteConfig(
149
153
  `${dir}/.git`,
150
154
  `${config.__meta.moduleDir}/src/vite`,
151
155
  `${config.__meta.moduleDir}/src/cli`,
156
+ ...(ignoreState ? [ignoreState] : []),
152
157
  ],
153
158
  },
154
159
  },
@@ -0,0 +1,70 @@
1
+ import { lstatSync, realpathSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * A config version 2 portal builds from `<state>/generated/`, where `content` is a link to the project folder.
6
+ * That folder holds the state folder itself (`.apitogo/` or `apitogo/.state/`), which holds `generated/content`
7
+ * again, so a file watcher that follows the link goes round forever (ENAMETOOLONG).
8
+ */
9
+ const STATE_INSIDE_CONTENT =
10
+ /[\\/]content[\\/](?:\.apitogo|apitogo[\\/]\.state)(?:[\\/]|$)/;
11
+
12
+ const within = (base: string, file: string) =>
13
+ file === base || file.startsWith(base + path.sep);
14
+
15
+ const unique = (paths: string[]) => [...new Set(paths)];
16
+
17
+ /** Whether `root` is the site root (`<state>/generated`) of a config version 2 workspace: it has the `content` link. */
18
+ function isWorkspace(root: string): boolean {
19
+ try {
20
+ return (
21
+ path.basename(root) === "generated" &&
22
+ lstatSync(path.join(root, "content")).isSymbolicLink()
23
+ );
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * The tenant's own folder of a config version 2 workspace (what `content` links to), or `undefined` for any other
31
+ * project. The site root sits inside the state folder, so the folder that holds `node_modules` and the pages is
32
+ * outside it and the dev server has to be told it may serve files from there.
33
+ */
34
+ export function tenantDir(root: string): string | undefined {
35
+ return isWorkspace(root)
36
+ ? realpathSync(path.join(root, "content"))
37
+ : undefined;
38
+ }
39
+
40
+ /**
41
+ * What the dev server's watcher must leave alone in a config version 2 workspace, or `undefined` for any other
42
+ * project. The watcher meets the state folder twice: below the `content` link and, after resolving the link,
43
+ * by its real path. So the whole state folder is skipped, except `generated/` itself (the site root) and the
44
+ * tenant's own files.
45
+ */
46
+ export function stateIgnore(
47
+ root: string,
48
+ ): ((file: string) => boolean) | undefined {
49
+ if (!isWorkspace(root)) return undefined;
50
+
51
+ const state = path.dirname(root);
52
+ const states = unique([state, realpathSync(state)]);
53
+ const roots = unique([root, realpathSync(root)]);
54
+
55
+ return (file) => {
56
+ if (!states.some((base) => within(base, file))) return false;
57
+ if (!roots.some((base) => within(base, file))) return true;
58
+ return STATE_INSIDE_CONTENT.test(file);
59
+ };
60
+ }
61
+
62
+ /**
63
+ * The folders the dev server may serve files from. The site root of a config version 2 workspace sits inside the
64
+ * state folder, so the package (`node_modules`) and the tenant's pages are outside it. Without them the page's main
65
+ * script fails with "Failed to load url ... Does the file exist?" and the server stops.
66
+ */
67
+ export function devServerAllow(root: string, moduleDir: string): string[] {
68
+ const tenant = tenantDir(root);
69
+ return [root, path.dirname(root), moduleDir, ...(tenant ? [tenant] : [])];
70
+ }