@kenjura/ursa 0.93.0 → 0.96.0
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/CHANGELOG.md +54 -0
- package/README.md +109 -0
- package/bin/ursa.js +14 -3
- package/meta/templates/default-template/content-hooks.js +45 -0
- package/meta/templates/default-template/index.html +1 -0
- package/meta/templates/default-template/sticky.js +7 -1
- package/meta/templates/default-template/toc-generator.js +58 -38
- package/package.json +1 -1
- package/src/dev.js +29 -0
- package/src/helper/__test__/folderConfig.test.js +89 -0
- package/src/helper/__test__/mdxRenderer.test.js +159 -0
- package/src/helper/__test__/sourceTimestamps.test.js +0 -0
- package/src/helper/automenu.js +4 -2
- package/src/helper/build/__test__/autoIndex.test.js +67 -0
- package/src/helper/build/autoIndex.js +22 -5
- package/src/helper/folderConfig.js +34 -4
- package/src/helper/mdxRenderer.js +199 -22
- package/src/helper/sourceTimestamps.js +139 -0
- package/src/helper/ursaConfig.js +3 -49
- package/src/jobs/__test__/generateJsonOnly.test.js +154 -0
- package/src/jobs/generate.js +297 -220
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { mkdtemp, writeFile, mkdir, rm } from 'fs/promises';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { renderMDX, generateHydrationScript } from '../mdxRenderer.js';
|
|
5
|
+
|
|
6
|
+
// Helper: a temp source tree with a _components/ directory
|
|
7
|
+
let tempDir;
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
tempDir = await mkdtemp(join(tmpdir(), 'ursa-mdx-'));
|
|
10
|
+
await mkdir(join(tempDir, '_components'), { recursive: true });
|
|
11
|
+
});
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
async function writeComponent(name, body) {
|
|
17
|
+
const path = join(tempDir, '_components', `${name}.jsx`);
|
|
18
|
+
await writeFile(path, body);
|
|
19
|
+
return path;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function render(mdx, { hydrate = true } = {}) {
|
|
23
|
+
const filePath = join(tempDir, 'page.mdx');
|
|
24
|
+
await writeFile(filePath, mdx);
|
|
25
|
+
return renderMDX({ source: mdx, filePath, sourceRoot: tempDir, hydrate });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const COUNTER = `
|
|
29
|
+
import React from 'react';
|
|
30
|
+
export default function Counter({ label }) {
|
|
31
|
+
const [n, setN] = React.useState(0);
|
|
32
|
+
return <button onClick={() => setN(n + 1)}>{label}: {n}</button>;
|
|
33
|
+
}
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Island wrapping
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
describe('renderMDX islands', () => {
|
|
40
|
+
test('wraps a component imported from the MDX in <ursa-island>', async () => {
|
|
41
|
+
await writeComponent('Counter', COUNTER);
|
|
42
|
+
const { html } = await render(`
|
|
43
|
+
import Counter from '_components/Counter.jsx';
|
|
44
|
+
|
|
45
|
+
# Page
|
|
46
|
+
|
|
47
|
+
<Counter label="Clicks" />
|
|
48
|
+
`);
|
|
49
|
+
expect(html).toMatch(/<ursa-island data-island="0" data-component="Counter"[^>]*>/);
|
|
50
|
+
expect(html).toContain('<button>Clicks<!-- -->: <!-- -->0</button>');
|
|
51
|
+
expect(html).toContain('</ursa-island>');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('numbers islands in document order', async () => {
|
|
55
|
+
await writeComponent('Counter', COUNTER);
|
|
56
|
+
const { html } = await render(`
|
|
57
|
+
import Counter from '_components/Counter.jsx';
|
|
58
|
+
|
|
59
|
+
<Counter label="A" />
|
|
60
|
+
|
|
61
|
+
Some prose.
|
|
62
|
+
|
|
63
|
+
<Counter label="B" />
|
|
64
|
+
`);
|
|
65
|
+
const ids = [...html.matchAll(/<ursa-island data-island="(\d+)"/g)].map((m) => m[1]);
|
|
66
|
+
expect(ids).toEqual(['0', '1']);
|
|
67
|
+
expect(html.indexOf('>A<')).toBeLessThan(html.indexOf('>B<'));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('does not nest islands: a component imported by a component is rendered plainly', async () => {
|
|
71
|
+
await writeComponent('Inner', `
|
|
72
|
+
import React from 'react';
|
|
73
|
+
export default function Inner() { return <em>inner</em>; }
|
|
74
|
+
`);
|
|
75
|
+
await writeComponent('Outer', `
|
|
76
|
+
import React from 'react';
|
|
77
|
+
import Inner from './Inner.jsx';
|
|
78
|
+
export default function Outer() { return <div>outer <Inner /></div>; }
|
|
79
|
+
`);
|
|
80
|
+
const { html } = await render(`
|
|
81
|
+
import Outer from '_components/Outer.jsx';
|
|
82
|
+
|
|
83
|
+
<Outer />
|
|
84
|
+
`);
|
|
85
|
+
expect(html.match(/<ursa-island/g)).toHaveLength(1);
|
|
86
|
+
expect(html).toContain('data-component="Outer"');
|
|
87
|
+
expect(html).toContain('<em>inner</em>');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('leaves the surrounding markdown outside any island', async () => {
|
|
91
|
+
await writeComponent('Counter', COUNTER);
|
|
92
|
+
const { html } = await render(`
|
|
93
|
+
import Counter from '_components/Counter.jsx';
|
|
94
|
+
|
|
95
|
+
# Heading
|
|
96
|
+
|
|
97
|
+
<Counter label="x" />
|
|
98
|
+
|
|
99
|
+
## Sub
|
|
100
|
+
`);
|
|
101
|
+
// Headings are siblings of the island, not children of it
|
|
102
|
+
expect(html).toMatch(/<h1>Heading<\/h1>\s*<ursa-island/);
|
|
103
|
+
expect(html).toMatch(/<\/ursa-island>\s*<h2>Sub<\/h2>/);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('client bundle contains the island runtime', async () => {
|
|
107
|
+
await writeComponent('Counter', COUNTER);
|
|
108
|
+
const { clientCode } = await render(`
|
|
109
|
+
import Counter from '_components/Counter.jsx';
|
|
110
|
+
|
|
111
|
+
<Counter label="x" />
|
|
112
|
+
`);
|
|
113
|
+
expect(clientCode).toContain('hydrateRoot');
|
|
114
|
+
expect(clientCode).toContain('data-island');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('hydrate: false still renders islands but emits no client code', async () => {
|
|
118
|
+
await writeComponent('Counter', COUNTER);
|
|
119
|
+
const result = await render(`
|
|
120
|
+
import Counter from '_components/Counter.jsx';
|
|
121
|
+
|
|
122
|
+
<Counter label="x" />
|
|
123
|
+
`, { hydrate: false });
|
|
124
|
+
expect(result.html).toContain('<ursa-island');
|
|
125
|
+
expect(result.clientCode).toBeUndefined();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Preload stripping
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
describe('renderMDX preload stripping', () => {
|
|
133
|
+
test('removes React 19 hoisted <link rel="preload"> so the body starts with its heading', async () => {
|
|
134
|
+
const { html } = await render(`
|
|
135
|
+
# Title
|
|
136
|
+

|
|
137
|
+
`);
|
|
138
|
+
expect(html).not.toContain('rel="preload"');
|
|
139
|
+
expect(html.trimStart().startsWith('<h1>')).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Hydration script
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
describe('generateHydrationScript', () => {
|
|
147
|
+
test('renders into a detached root and looks up islands rather than a page container', () => {
|
|
148
|
+
const script = generateHydrationScript('return { default: function () { return null; } };');
|
|
149
|
+
expect(script).toContain('ursa-island[data-island]');
|
|
150
|
+
expect(script).toContain('createRoot(detached)');
|
|
151
|
+
expect(script).not.toContain("getElementById('main-content')");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('escapes the bundle for embedding in a template literal', () => {
|
|
155
|
+
const script = generateHydrationScript('var s = `a${b}`; // </script>');
|
|
156
|
+
expect(script).toContain('\\`a\\${b}\\`');
|
|
157
|
+
expect(script).toContain('<\\/script>');
|
|
158
|
+
});
|
|
159
|
+
});
|
|
Binary file
|
package/src/helper/automenu.js
CHANGED
|
@@ -218,8 +218,10 @@ function buildMenuData(tree, source, validPaths, parentPath = '', includeDebug =
|
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
// Check if this folder is hidden via config.json
|
|
222
|
-
|
|
221
|
+
// Check if this folder is hidden via config.json.
|
|
222
|
+
// Not gated on hasChildren: a hidden folder is ignored whether or not the
|
|
223
|
+
// tree walker found children under it.
|
|
224
|
+
if (isFolderHidden(item.path, source)) {
|
|
223
225
|
continue; // Skip hidden folders
|
|
224
226
|
}
|
|
225
227
|
|
|
@@ -3,6 +3,7 @@ import { mkdtemp, mkdir, writeFile, rm, readFile } from "fs/promises";
|
|
|
3
3
|
import { existsSync } from "fs";
|
|
4
4
|
import { tmpdir } from "os";
|
|
5
5
|
import { generateAutoIndices, generateAutoIndexHtmlFromSource } from "../autoIndex.js";
|
|
6
|
+
import { clearConfigCache } from "../../folderConfig.js";
|
|
6
7
|
|
|
7
8
|
let tempDir;
|
|
8
9
|
let source;
|
|
@@ -160,3 +161,69 @@ describe("auto-index naming matches the automenu", () => {
|
|
|
160
161
|
expect(rootIndex).toContain('<a href="bnw/index.html">BNW - Brave New World</a>');
|
|
161
162
|
});
|
|
162
163
|
});
|
|
164
|
+
|
|
165
|
+
describe("folders ignored via config.json { hidden: true }", () => {
|
|
166
|
+
beforeEach(() => {
|
|
167
|
+
// getFolderConfig memoizes per absolute path; temp dirs are unique per
|
|
168
|
+
// test, but clearing keeps the cache from growing across the suite.
|
|
169
|
+
clearConfigCache();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("omits a hidden folder from an auto-index built from source", async () => {
|
|
173
|
+
await mkdir(join(source, "_art"), { recursive: true });
|
|
174
|
+
await writeFile(join(source, "_art", "config.json"), JSON.stringify({ hidden: true }));
|
|
175
|
+
await writeFile(join(source, "_art", "prompts.md"), "# Prompts\n");
|
|
176
|
+
await mkdir(join(source, "people"), { recursive: true });
|
|
177
|
+
await writeFile(join(source, "people", "alice.md"), "# Alice\n");
|
|
178
|
+
|
|
179
|
+
const html = await generateAutoIndexHtmlFromSource(source, 2);
|
|
180
|
+
|
|
181
|
+
expect(html).toContain("people");
|
|
182
|
+
expect(html).not.toContain("_art");
|
|
183
|
+
expect(html).not.toContain("prompts");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("omits a hidden folder even when stale output for it still exists", async () => {
|
|
187
|
+
// A folder generated before it was hidden leaves files behind in output.
|
|
188
|
+
// The listing is built from output, so without a source-side check the
|
|
189
|
+
// hidden folder would reappear in the index.
|
|
190
|
+
await mkdir(join(source, "_art"), { recursive: true });
|
|
191
|
+
await writeFile(join(source, "_art", "config.json"), JSON.stringify({ hidden: true }));
|
|
192
|
+
await mkdir(join(source, "people"), { recursive: true });
|
|
193
|
+
await writeFile(join(source, "people", "alice.md"), "# Alice\n");
|
|
194
|
+
|
|
195
|
+
await mkdir(join(output, "_art"), { recursive: true });
|
|
196
|
+
await writeFile(join(output, "_art", "prompts.html"), "<html><body>stale</body></html>");
|
|
197
|
+
await mkdir(join(output, "people"), { recursive: true });
|
|
198
|
+
await writeFile(join(output, "people", "alice.html"), "<html><body>Alice</body></html>");
|
|
199
|
+
|
|
200
|
+
const progress = makeProgress();
|
|
201
|
+
await runAutoIndices(
|
|
202
|
+
[source, join(source, "people")],
|
|
203
|
+
[join(source, "people", "alice.md")],
|
|
204
|
+
progress
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const index = await readFile(join(output, "index.html"), "utf8");
|
|
208
|
+
expect(index).toContain("people");
|
|
209
|
+
expect(index).not.toContain("_art");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("does not count documents inside a hidden subfolder when deciding a folder has content", async () => {
|
|
213
|
+
// `notes` holds nothing but a hidden subfolder, so it produces no pages
|
|
214
|
+
// and must not be linked as though it did.
|
|
215
|
+
await mkdir(join(source, "notes", "_art"), { recursive: true });
|
|
216
|
+
await writeFile(
|
|
217
|
+
join(source, "notes", "_art", "config.json"),
|
|
218
|
+
JSON.stringify({ hidden: true })
|
|
219
|
+
);
|
|
220
|
+
await writeFile(join(source, "notes", "_art", "prompts.md"), "# Prompts\n");
|
|
221
|
+
await mkdir(join(source, "people"), { recursive: true });
|
|
222
|
+
await writeFile(join(source, "people", "alice.md"), "# Alice\n");
|
|
223
|
+
|
|
224
|
+
const html = await generateAutoIndexHtmlFromSource(source, 2);
|
|
225
|
+
|
|
226
|
+
expect(html).toContain("people");
|
|
227
|
+
expect(html).not.toContain("notes");
|
|
228
|
+
});
|
|
229
|
+
});
|
|
@@ -8,7 +8,7 @@ import { findAllScriptJs } from "../findScriptJs.js";
|
|
|
8
8
|
import { addTimestampToHtmlStaticRefs } from "./cacheBust.js";
|
|
9
9
|
import { isMetadataOnly, extractMetadata, getAutoIndexConfig } from "../metadataExtractor.js";
|
|
10
10
|
import { getCustomMenuForFile } from "./menu.js";
|
|
11
|
-
import { getFolderConfig } from "../folderConfig.js";
|
|
11
|
+
import { getFolderConfig, isFolderSelfHidden } from "../folderConfig.js";
|
|
12
12
|
import {
|
|
13
13
|
toDisplayName,
|
|
14
14
|
getFolderLabel,
|
|
@@ -26,11 +26,18 @@ const OUTPUT_DOC_EXTENSIONS = ['.html'];
|
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Recursively check if a directory contains any document files.
|
|
29
|
+
*
|
|
30
|
+
* Documents inside a config-hidden subfolder do not count: they produce no
|
|
31
|
+
* output, so a folder whose only contents are hidden must not be linked as if
|
|
32
|
+
* it had pages. When `dir` is an output directory, pass the matching source
|
|
33
|
+
* directory as `sourceDir` — the config.json lives in the source tree.
|
|
34
|
+
*
|
|
29
35
|
* @param {string} dir - Directory path to check
|
|
30
36
|
* @param {string[]} extensions - File extensions that count as documents
|
|
37
|
+
* @param {string|null} [sourceDir=dir] - Matching source directory, for hidden lookups
|
|
31
38
|
* @returns {Promise<boolean>} True if the directory (or any subdirectory) contains at least one document
|
|
32
39
|
*/
|
|
33
|
-
async function directoryHasDocuments(dir, extensions) {
|
|
40
|
+
async function directoryHasDocuments(dir, extensions, sourceDir = dir) {
|
|
34
41
|
try {
|
|
35
42
|
const children = await readdir(dir, { withFileTypes: true });
|
|
36
43
|
for (const child of children) {
|
|
@@ -38,7 +45,9 @@ async function directoryHasDocuments(dir, extensions) {
|
|
|
38
45
|
const fullPath = join(dir, child.name);
|
|
39
46
|
if (child.isDirectory()) {
|
|
40
47
|
if (child.name === 'img') continue;
|
|
41
|
-
if (
|
|
48
|
+
if (sourceDir && isFolderSelfHidden(join(sourceDir, child.name))) continue;
|
|
49
|
+
const childSource = sourceDir ? join(sourceDir, child.name) : null;
|
|
50
|
+
if (await directoryHasDocuments(fullPath, extensions, childSource)) return true;
|
|
42
51
|
} else {
|
|
43
52
|
const ext = extname(child.name).toLowerCase();
|
|
44
53
|
if (extensions.includes(ext)) return true;
|
|
@@ -111,6 +120,9 @@ export async function generateAutoIndexHtml(dir, depth = 1, currentDepth = 0, pa
|
|
|
111
120
|
if (child.name === 'index.html') return false;
|
|
112
121
|
// Skip img folders (contain images, not content)
|
|
113
122
|
if (child.isDirectory() && child.name === 'img') return false;
|
|
123
|
+
// Skip folders config.json marks hidden — they are ignored entirely,
|
|
124
|
+
// so a stale output directory must not resurrect them in a listing
|
|
125
|
+
if (child.isDirectory() && sourceDir && isFolderSelfHidden(join(sourceDir, child.name))) return false;
|
|
114
126
|
// Include directories and html files
|
|
115
127
|
return child.isDirectory() || child.name.endsWith('.html');
|
|
116
128
|
})
|
|
@@ -131,7 +143,8 @@ export async function generateAutoIndexHtml(dir, depth = 1, currentDepth = 0, pa
|
|
|
131
143
|
// Skip directories that contain no documents
|
|
132
144
|
if (isDir) {
|
|
133
145
|
const childDir = join(dir, child.name);
|
|
134
|
-
|
|
146
|
+
const childSource = sourceDir ? join(sourceDir, child.name) : null;
|
|
147
|
+
if (!await directoryHasDocuments(childDir, OUTPUT_DOC_EXTENSIONS, childSource)) continue;
|
|
135
148
|
}
|
|
136
149
|
// Use pathPrefix to ensure hrefs are correct relative to the document root
|
|
137
150
|
const childPath = pathPrefix ? `${pathPrefix}/${child.name}` : child.name;
|
|
@@ -186,6 +199,8 @@ export async function generateAutoIndexHtmlFromSource(sourceDir, depth = 1, curr
|
|
|
186
199
|
if (child.name.match(/^index\.(md|mdx|txt|yml|html)$/i)) return false;
|
|
187
200
|
// Skip img folders (contain images, not content)
|
|
188
201
|
if (child.isDirectory() && child.name === 'img') return false;
|
|
202
|
+
// Skip folders config.json marks hidden — they produce no output
|
|
203
|
+
if (child.isDirectory() && isFolderSelfHidden(join(sourceDir, child.name))) return false;
|
|
189
204
|
// Include directories and article files (md, mdx, txt, yml, html)
|
|
190
205
|
return child.isDirectory() || child.name.match(/\.(md|mdx|txt|yml|html)$/i);
|
|
191
206
|
})
|
|
@@ -362,6 +377,8 @@ export async function generateAutoIndices(output, directories, source, templates
|
|
|
362
377
|
// Skip hidden files and index alternates we just checked
|
|
363
378
|
if (child.name.startsWith('.')) return false;
|
|
364
379
|
if (child.name === 'index.html') return false;
|
|
380
|
+
// Skip folders config.json marks hidden — they produce no output
|
|
381
|
+
if (child.isDirectory() && isFolderSelfHidden(join(sourceDir, child.name))) return false;
|
|
365
382
|
// Include directories and html files
|
|
366
383
|
return child.isDirectory() || child.name.endsWith('.html');
|
|
367
384
|
})
|
|
@@ -377,7 +394,7 @@ export async function generateAutoIndices(output, directories, source, templates
|
|
|
377
394
|
for (const { child, isDir, label } of filteredItems) {
|
|
378
395
|
if (isDir) {
|
|
379
396
|
const childDir = join(dir, child.name);
|
|
380
|
-
if (!await directoryHasDocuments(childDir, OUTPUT_DOC_EXTENSIONS)) continue;
|
|
397
|
+
if (!await directoryHasDocuments(childDir, OUTPUT_DOC_EXTENSIONS, join(sourceDir, child.name))) continue;
|
|
381
398
|
}
|
|
382
399
|
// For directories, link to /folder/index.html; for files, use the filename directly
|
|
383
400
|
const href = isDir ? `${child.name}/index.html` : child.name;
|
|
@@ -11,9 +11,16 @@ const configCache = new Map();
|
|
|
11
11
|
* {
|
|
12
12
|
* label?: string, // Custom label for menu display
|
|
13
13
|
* icon?: string, // URL to icon image for menu
|
|
14
|
-
* hidden?: boolean, // If true,
|
|
14
|
+
* hidden?: boolean, // If true, ignore the folder entirely (see below)
|
|
15
15
|
* openMenuItems?: string[] // (root only) Array of folder names to expand by default
|
|
16
16
|
* }
|
|
17
|
+
*
|
|
18
|
+
* `hidden: true` means *ignored*, not merely unlisted. The folder and its
|
|
19
|
+
* whole subtree take no part in the build: no HTML is rendered from its
|
|
20
|
+
* documents, its images and other static assets are not copied, it does not
|
|
21
|
+
* appear in the sidebar menu, in any auto-index, in breadcrumbs, or in the
|
|
22
|
+
* search index, and `ursa serve` will not render its pages on demand. The
|
|
23
|
+
* files stay in the docroot; the site behaves as if they were not there.
|
|
17
24
|
*/
|
|
18
25
|
|
|
19
26
|
/**
|
|
@@ -60,10 +67,33 @@ export function getRootConfig(sourceRoot) {
|
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
70
|
+
* True when this exact folder's own config.json says `hidden: true`, ignoring
|
|
71
|
+
* its ancestors.
|
|
72
|
+
*
|
|
73
|
+
* Use this where the ancestors have already been ruled out — walking a tree
|
|
74
|
+
* top-down, say, where reaching a node means every folder above it was
|
|
75
|
+
* visible. It needs no docroot, which is what makes it usable in the
|
|
76
|
+
* auto-index builders, where only the folder being listed is known.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} folderPath - Absolute path to a folder
|
|
79
|
+
* @returns {boolean} True if that folder is marked hidden
|
|
80
|
+
*/
|
|
81
|
+
export function isFolderSelfHidden(folderPath) {
|
|
82
|
+
return getFolderConfig(folderPath.replace(/\/$/, ''))?.hidden === true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if a path lies in a folder — its own, or any ancestor up to the
|
|
87
|
+
* docroot — that config.json marks `hidden: true`.
|
|
88
|
+
*
|
|
89
|
+
* Accepts file paths as well as directories: the walk starts at `folderPath`
|
|
90
|
+
* itself, and a file simply has no config.json of its own, so the first step
|
|
91
|
+
* misses and the ancestors decide. That is what lets the build filter a mixed
|
|
92
|
+
* list of files and directories through one predicate.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} folderPath - Absolute path to check (file or directory)
|
|
65
95
|
* @param {string} sourceRoot - The source root directory (stop checking at this level)
|
|
66
|
-
* @returns {boolean} True if this
|
|
96
|
+
* @returns {boolean} True if this path should be ignored
|
|
67
97
|
*/
|
|
68
98
|
export function isFolderHidden(folderPath, sourceRoot) {
|
|
69
99
|
let currentPath = folderPath.replace(/\/$/, '');
|