@nutin/cli 2.1.1 → 3.0.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 +268 -0
- package/README.md +13 -5
- package/bin/add-feature.mjs +1 -1
- package/bin/cli.mjs +1 -1
- package/bin/create-app.mjs +1 -1
- package/bin/update-app.mjs +1 -1
- package/lib/index.mjs +6 -2
- package/lib/src/add-feature/base-template-patcher.mjs +139 -0
- package/lib/src/add-feature/feature-adder.mjs +20 -9
- package/lib/src/add-feature/feature-context-builder.mjs +1 -0
- package/lib/src/add-feature/nutin-config-updater.mjs +34 -8
- package/lib/src/add-feature/package-json-updater.mjs +7 -9
- package/lib/src/common/feature-registry.mjs +4 -0
- package/lib/src/common/file-generator.mjs +25 -3
- package/lib/src/common/git.mjs +31 -0
- package/lib/src/common/meta-bootstrap-prompt.mjs +28 -2
- package/lib/src/common/package-json-helper.mjs +14 -4
- package/lib/src/common/project-meta.mjs +2 -2
- package/lib/src/common/template-compiler.mjs +2 -0
- package/lib/src/common/transpile-ts.mjs +94 -0
- package/lib/src/create-app/context-builder.mjs +1 -0
- package/lib/src/create-app/git-manager.mjs +12 -2
- package/lib/src/create-app/json-manager.mjs +21 -18
- package/lib/src/create-app/new-app-prompt.mjs +4 -2
- package/lib/src/create-app/success-message.mjs +1 -1
- package/lib/src/update-app/conflict-report-writer.mjs +33 -21
- package/lib/src/update-app/json-drift.mjs +54 -0
- package/lib/src/update-app/project-updater.mjs +123 -14
- package/lib/src/update-app/template-differ.mjs +5 -5
- package/lib/src/update-app/three-way-merge.mjs +42 -0
- package/lib/src/update-app/update-context-builder.mjs +1 -0
- package/lib/src/update-app/update-summary-printer.mjs +10 -2
- package/package.json +9 -8
- package/templates/base/AGENTS.md.hbs +42 -41
- package/templates/base/GETTING_STARTED.md.hbs +26 -20
- package/templates/base/README.md.hbs +7 -4
- package/templates/base/src/app/globals.d.ts.hbs +6 -3
- package/templates/base/src/app/guards.ts.hbs +1 -3
- package/templates/base/src/app/main.ts.hbs +2 -9
- package/templates/base/src/app/routes.ts.hbs +7 -2
- package/templates/base/src/app/views/home/home.view.html.hbs +1 -1
- package/templates/base/src/app/views/home/home.view.ts.hbs +2 -2
- package/templates/base/src/app/views/not-found/not-found.view.ts.hbs +2 -2
- package/templates/base/src/core/base-classes/base-component/base-component.test.js.hbs +461 -1
- package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +39 -48
- package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.test.js.hbs +36 -1
- package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +47 -77
- package/templates/base/src/core/base-classes/base-component/helpers/children.helper.test.js.hbs +286 -24
- package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +144 -26
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.test.js.hbs +66 -4
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +135 -75
- package/templates/base/src/core/base-classes/base-component/helpers/event.helper.test.js.hbs +85 -4
- package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +93 -54
- package/templates/base/src/core/base-classes/base-component/helpers/html.helper.test.js.hbs +404 -0
- package/templates/base/src/core/base-classes/base-component/helpers/html.helper.ts.hbs +242 -0
- package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.test.js.hbs +70 -1
- package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +54 -15
- package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.test.js.hbs +12 -1
- package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.ts.hbs +39 -36
- package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.test.js.hbs +179 -0
- package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.ts.hbs +93 -0
- package/templates/base/src/core/base-classes/base-component/helpers/security.helper.test.js.hbs +135 -17
- package/templates/base/src/core/base-classes/base-component/helpers/security.helper.ts.hbs +199 -81
- package/templates/base/src/core/base-classes/base-component/helpers/token.helper.test.js.hbs +40 -23
- package/templates/base/src/core/base-classes/base-component/helpers/token.helper.ts.hbs +67 -65
- package/templates/base/src/core/base-classes/component/component.test.js.hbs +17 -17
- package/templates/base/src/core/base-classes/component/component.ts.hbs +8 -41
- package/templates/base/src/core/base-classes/component/helpers/config.helper.test.js.hbs +1 -1
- package/templates/base/src/core/base-classes/component/helpers/config.helper.ts.hbs +37 -39
- package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.test.js.hbs +14 -1
- package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.ts.hbs +33 -31
- package/templates/base/src/core/base-classes/service/service.test.js.hbs +32 -0
- package/templates/base/src/core/base-classes/service/service.ts.hbs +21 -13
- package/templates/base/src/core/base-classes/view/view.test.js.hbs +4 -0
- package/templates/base/src/core/base-classes/view/view.ts.hbs +11 -8
- package/templates/base/src/core/index.ts.hbs +4 -2
- package/templates/base/src/core/internals.d.ts.hbs +199 -20
- package/templates/base/src/core/services/event-bus/event-bus.test.js.hbs +18 -0
- package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +26 -25
- package/templates/base/src/core/services/event-bus/lifecycle.facade.ts.hbs +3 -3
- package/templates/base/src/core/services/event-bus/navigation.facade.ts.hbs +3 -3
- package/templates/base/src/core/services/globals/globals.ts.hbs +7 -32
- package/templates/base/src/core/services/http-client/helpers/http-builder.helper.test.js.hbs +133 -50
- package/templates/base/src/core/services/http-client/helpers/http-builder.helper.ts.hbs +147 -39
- package/templates/base/src/core/services/http-client/helpers/http-manager.helper.test.js.hbs +75 -19
- package/templates/base/src/core/services/http-client/helpers/http-manager.helper.ts.hbs +73 -47
- package/templates/base/src/core/services/http-client/http-client.test.js.hbs +339 -232
- package/templates/base/src/core/services/http-client/http-client.ts.hbs +37 -25
- package/templates/base/src/core/services/i18n/i18n.test.js.hbs +57 -16
- package/templates/base/src/core/services/i18n/i18n.ts.hbs +42 -42
- package/templates/base/src/core/services/i18n/languages.ts.hbs +0 -2
- package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +4 -6
- package/templates/base/src/core/services/pipe-registry/pipes.test.js.hbs +10 -0
- package/templates/base/src/core/services/pipe-registry/pipes.ts.hbs +4 -2
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.test.js.hbs +183 -4
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +207 -84
- package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.test.js.hbs +26 -27
- package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.ts.hbs +38 -60
- package/templates/base/src/core/services/router/helpers/view-render-manager.helper.test.js.hbs +4 -4
- package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +30 -30
- package/templates/base/src/core/services/router/router.test.js.hbs +306 -0
- package/templates/base/src/core/services/router/router.ts.hbs +113 -53
- package/templates/base/tools/builder/builder.config.js.hbs +6 -0
- package/templates/base/tools/builder/builder.js.hbs +18 -6
- package/templates/base/tools/builder/core/app/compile-ts.js.hbs +81 -6
- package/templates/base/tools/builder/core/app/copy-static.js.hbs +12 -1
- package/templates/base/tools/builder/core/app/handler-refs.js.hbs +114 -0
- package/templates/base/tools/builder/core/app/handler-refs.test.js.hbs +70 -0
- package/templates/base/tools/builder/core/app/paths.js.hbs +17 -0
- package/templates/base/tools/builder/core/app/route-keys.js.hbs +47 -0
- package/templates/base/tools/builder/core/app/route-keys.test.js.hbs +97 -0
- package/templates/base/tools/builder/core/app/ts-errors.js.hbs +58 -0
- package/templates/base/tools/builder/core/app/ts-errors.test.js.hbs +84 -0
- package/templates/base/tools/builder/core/app/validate-routes.js.hbs +21 -55
- package/templates/base/tools/builder/core/finalize-build.js.hbs +6 -0
- package/templates/base/tools/builder/core/html-index/validate-html.js.hbs +10 -2
- package/templates/base/tools/builder/core/html-templates/merge-templates.js.hbs +111 -23
- package/templates/base/tools/builder/core/html-templates/minify-templates.js.hbs +8 -0
- package/templates/base/tools/builder/core/html-templates/process-html-templates.js.hbs +5 -3
- package/templates/base/tools/builder/core/html-templates/template-literal.js.hbs +193 -0
- package/templates/base/tools/builder/core/html-templates/template-literal.test.js.hbs +69 -0
- package/templates/base/tools/builder/core/i18n/build-i18n.js.hbs +14 -6
- package/templates/base/tools/builder/core/prod-bundle/bundle-options.js.hbs +82 -0
- package/templates/base/tools/builder/core/prod-bundle/bundle-options.test.js.hbs +65 -0
- package/templates/base/tools/builder/core/prod-bundle/compress-files.js.hbs +6 -6
- package/templates/base/tools/builder/core/prod-bundle/esbuild.js.hbs +15 -7
- package/templates/base/tools/builder/core/prod-bundle/hash-files.js.hbs +4 -4
- package/templates/base/tools/builder/core/seo/generate-robots-txt.js.hbs +31 -13
- package/templates/base/tools/builder/core/seo/generate-robots-txt.test.js.hbs +48 -0
- package/templates/base/tools/builder/core/seo/generate-seo-files.js.hbs +3 -3
- package/templates/base/tools/builder/core/seo/generate-seo-html.js.hbs +43 -13
- package/templates/base/tools/builder/core/seo/generate-sitemap-xml.js.hbs +22 -7
- package/templates/base/tools/builder/core/seo/generate-sitemap-xml.test.js.hbs +30 -0
- package/templates/base/tools/builder/core/seo/html-substitutions.js.hbs +28 -21
- package/templates/base/tools/builder/core/seo/html-substitutions.test.js.hbs +45 -0
- package/templates/base/tools/builder/core/seo/route-output.js.hbs +24 -6
- package/templates/base/tools/builder/core/seo/route-output.test.js.hbs +31 -0
- package/templates/base/tools/builder/core/seo/seo-routes.js.hbs +29 -0
- package/templates/base/tools/builder/core/seo/seo-routes.test.js.hbs +21 -0
- package/templates/base/tools/builder/core/seo/ssr/ssr-bundle.js.hbs +6 -7
- package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.js.hbs +34 -5
- package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.test.js.hbs +48 -0
- package/templates/base/tools/builder/core/seo/ssr/ssr-render.js.hbs +24 -11
- package/templates/base/tools/builder/core/styles/sass.js.hbs +3 -2
- package/templates/base/tools/builder/core/styles/tailwind.js.hbs +19 -59
- package/templates/base/tools/dev/dev-serve.js.hbs +30 -5
- package/templates/base/tools/dev/serve.js.hbs +152 -22
- package/templates/base/tools/dev/watcher.js.hbs +34 -3
- package/templates/base/tools/generator/generator.js.hbs +1 -1
- package/templates/base/tools/generator/handle-file.js.hbs +4 -2
- package/templates/base/tools/generator/handle-file.test.js.hbs +3 -3
- package/templates/base/tools/generator/templates/component/component.template.js.hbs +3 -3
- package/templates/base/tools/generator/templates/component/component.template.test.js.hbs +3 -3
- package/templates/base/tools/generator/templates/service/service.template.js.hbs +1 -1
- package/templates/base/tools/generator/templates/service/service.template.test.js.hbs +1 -1
- package/templates/base/tools/generator/templates/view/view.template.js.hbs +2 -2
- package/templates/base/tools/generator/templates/view/view.template.test.js.hbs +2 -2
- package/templates/base/tools/testin-nutin/core/coverage/write-uncovered-report.js.hbs +1 -1
- package/templates/base/tools/testin-nutin/core/globals/assertion-lib.js.hbs +2 -2
- package/templates/base/tools/testin-nutin/core/{tests/assertions.test.js.hbs → globals/assertion-lib.test.js.hbs} +6 -0
- package/templates/base/tools/testin-nutin/core/globals/register-test-globals.js.hbs +6 -7
- package/templates/base/tools/testin-nutin/core/{tests/globals-each-all.test.js.hbs → globals/register-test-globals.test.js.hbs} +12 -0
- package/templates/base/tools/testin-nutin/core/printer.js.hbs +10 -4
- package/templates/base/tools/testin-nutin/core/queue/test-queue.js.hbs +8 -8
- package/templates/base/tools/testin-nutin/mocks/mock-event-bus.js.hbs +27 -25
- package/templates/base/tools/testin-nutin/mocks/mock-event-bus.test.js.hbs +30 -22
- package/templates/base/tools/testin-nutin/mocks/mock-http-client.js.hbs +7 -10
- package/templates/base/tools/testin-nutin/mocks/mock-http-client.test.js.hbs +12 -0
- package/templates/base/tools/testin-nutin/mocks/mock-i18n.js.hbs +47 -22
- package/templates/base/tools/testin-nutin/mocks/mock-i18n.test.js.hbs +47 -26
- package/templates/base/tools/testin-nutin/mocks/mock-router.js.hbs +19 -11
- package/templates/base/tools/testin-nutin/mocks/mock-router.test.js.hbs +24 -7
- package/templates/base/tools/testin-nutin/runner.js.hbs +12 -3
- package/templates/base/tools/testin-nutin/watch-tests.js.hbs +45 -20
- package/templates/base/tools/utils/build-lock.js.hbs +30 -2
- package/templates/base/tools/utils/ensure-deps.js.hbs +72 -0
- package/templates/base/tools/utils/ensure-deps.test.js.hbs +17 -0
- package/templates/base/tools/utils/format.js.hbs +1 -1
- package/templates/base/tools/utils/index.js.hbs +2 -1
- package/templates/base/tools/utils/print.js.hbs +2 -2
- package/templates/base/tools/utils/resolve-port.js.hbs +30 -0
- package/templates/base/tools/utils/resolve-port.test.js.hbs +47 -0
- package/templates/base/tools/utils/run-command.js.hbs +30 -1
- package/templates/base/tools/utils/run-command.test.js.hbs +12 -1
- package/templates/base/tools/utils/run-script.js.hbs +2 -2
- package/templates/features/docker/tools/docker/Dockerfile.template.hbs +3 -0
- package/templates/features/docker/tools/docker/nginx.conf.template +6 -4
- package/templates/features/docker/tools/docker/scripts/docker-ports.js.hbs +1 -1
- package/templates/features/docker/tools/docker/scripts/docker-ports.test.js.hbs +2 -2
- package/templates/features/docker/tools/docker/scripts/locale-redirects.js.hbs +28 -0
- package/templates/features/docker/tools/docker/scripts/locale-redirects.test.js.hbs +34 -0
- package/templates/features/docker/tools/docker/scripts/markdown-sources-copy.js.hbs +25 -0
- package/templates/features/docker/tools/docker/scripts/markdown-sources-copy.test.js.hbs +34 -0
- package/templates/features/docker/tools/docker/scripts/render-docker-template.js.hbs +3 -1
- package/templates/features/docker/tools/docker/scripts/render-docker-template.test.js.hbs +12 -0
- package/templates/features/docker/tools/docker/scripts/validate-docker.js.hbs +21 -4
- package/templates/features/markdown/markdown-content/getting-started.md.hbs +28 -0
- package/templates/features/markdown/markdown-content/writing-pages.md.hbs +19 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.scss.hbs +75 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.test.js.hbs +40 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.ts.hbs +65 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-landing/markdown-landing.component.test.js.hbs +67 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-landing/markdown-landing.component.ts.hbs +87 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.scss.hbs +42 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.test.js.hbs +59 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.ts.hbs +72 -0
- package/templates/features/markdown/src/app/markdown/locales/en.json.hbs +6 -0
- package/templates/features/markdown/src/app/markdown/markdown-guards.test.js.hbs +63 -0
- package/templates/features/markdown/src/app/markdown/markdown-guards.ts.hbs +31 -0
- package/templates/features/markdown/src/app/markdown/markdown-i18n.test.js.hbs +78 -0
- package/templates/features/markdown/src/app/markdown/markdown-i18n.ts.hbs +31 -0
- package/templates/features/markdown/src/app/markdown/markdown-manifest.test.js.hbs +98 -0
- package/templates/features/markdown/src/app/markdown/markdown-manifest.ts.hbs +157 -0
- package/templates/features/markdown/src/app/markdown/markdown-manifests.service.test.js.hbs +87 -0
- package/templates/features/markdown/src/app/markdown/markdown-manifests.service.ts.hbs +50 -0
- package/templates/features/markdown/src/app/markdown/markdown-routes.test.js.hbs +74 -0
- package/templates/features/markdown/src/app/markdown/markdown-routes.ts.hbs +26 -0
- package/templates/features/markdown/src/app/markdown/markdown-sources.test.js.hbs +22 -0
- package/templates/features/markdown/src/app/markdown/markdown-sources.ts.hbs +42 -0
- package/templates/features/markdown/src/app/markdown/view/markdown.view.html.hbs +4 -0
- package/templates/features/markdown/src/app/markdown/view/markdown.view.scss.hbs +19 -0
- package/templates/features/markdown/src/app/markdown/view/markdown.view.test.js.hbs +125 -0
- package/templates/features/markdown/src/app/markdown/view/markdown.view.ts.hbs +146 -0
- package/templates/features/markdown/tools/builder/core/markdown/generate-markdown-pages.js.hbs +71 -0
- package/templates/features/markdown/tools/builder/core/markdown/markdown-compiler.js.hbs +582 -0
- package/templates/features/markdown/tools/builder/core/markdown/markdown-compiler.test.js.hbs +390 -0
- package/templates/features/markdown/tools/builder/core/markdown/markdown-seo-routes.js.hbs +155 -0
- package/templates/features/markdown/tools/builder/core/markdown/markdown-seo-routes.test.js.hbs +112 -0
- /package/templates/base/tools/testin-nutin/core/{tests → globals}/clock.test.js.hbs +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { View } from '../../core/index.js';
|
|
2
|
+
import { MarkdownGuards } from './markdown-guards.js';
|
|
3
|
+
import { MarkdownManifestsService } from './markdown-manifests.service.js';
|
|
4
|
+
|
|
5
|
+
export interface IMarkdownRoutesOptions {
|
|
6
|
+
// Builds the view of a folder's routes, e.g. a MarkdownView with your own components or a
|
|
7
|
+
// subclass of it. Called for every folder, with its id: return new MarkdownView({ id }) for
|
|
8
|
+
// the folders you don't customize. May return a Promise, to load the view lazily (see
|
|
9
|
+
// ViewFactory): e.g. import('./views/docs/docs.view.js').then((m) => new m.DocsView(id)).
|
|
10
|
+
view?: (id: string) => View | Promise<View>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// One route per nutin.config.js "markdownSources" folder: '/<id>/:slug?', or
|
|
14
|
+
// '/<id>/:section?/:slug?' with "sectionInPath: true". Spread into appRoutes; a route
|
|
15
|
+
// written after the spread with the same path replaces the generated one.
|
|
16
|
+
// By default, the routes are lazy: MarkdownView and its components are loaded on the first
|
|
17
|
+
// visit to one of them, not with the rest of the app.
|
|
18
|
+
export function markdownRoutes(options: IMarkdownRoutesOptions = {}): Nutin.Routes {
|
|
19
|
+
const view = options.view ?? ((id: string) => import('./view/markdown.view.js').then((m) => new m.MarkdownView({ id })));
|
|
20
|
+
return Object.fromEntries(
|
|
21
|
+
MarkdownManifestsService.all.map(({ id, sectionInPath }) => sectionInPath
|
|
22
|
+
? [`/${id}/:section?/:slug?`, { view: () => view(id), guards: [MarkdownGuards.sectionPageExists(id)] }]
|
|
23
|
+
: [`/${id}/:slug?`, { view: () => view(id), guards: [MarkdownGuards.pageExists(id)] }]
|
|
24
|
+
)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { resolveMarkdownSources } from '#root/dist/src/app/markdown/markdown-sources.js';
|
|
2
|
+
|
|
3
|
+
describe('resolveMarkdownSources', () => {
|
|
4
|
+
// Same cases as tools/builder/core/markdown/markdown-compiler.test.js: the compiler names
|
|
5
|
+
// each manifest with this id, so both rules must agree.
|
|
6
|
+
it('derives manifest ids like the compiler does', () => {
|
|
7
|
+
const cases = [
|
|
8
|
+
['content', 'content', false],
|
|
9
|
+
['docs/My Guides', 'my-guides', false],
|
|
10
|
+
['docs/guides/', 'guides', false],
|
|
11
|
+
[{ folder: 'x', routePrefix: 'api' }, 'api', false],
|
|
12
|
+
[{ folder: 'docs', sectionInPath: true }, 'docs', true],
|
|
13
|
+
];
|
|
14
|
+
const sources = resolveMarkdownSources({ sourceFolders: cases.map(([entry]) => entry) });
|
|
15
|
+
expect(sources).toEqual(cases.map(([, id, sectionInPath]) => ({ id, sectionInPath })));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('returns no sources when markdownSources is missing', () => {
|
|
19
|
+
expect(resolveMarkdownSources(undefined)).toEqual([]);
|
|
20
|
+
expect(resolveMarkdownSources({})).toEqual([]);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { IMarkdownSource } from './markdown-manifest.js';
|
|
2
|
+
|
|
3
|
+
export type MarkdownSourceEntry = string | {
|
|
4
|
+
folder: string;
|
|
5
|
+
routePrefix?: string;
|
|
6
|
+
hubFiles?: string[];
|
|
7
|
+
sectionInPath?: boolean;
|
|
8
|
+
prefixReplacements?: [string, string][];
|
|
9
|
+
seo?: boolean;
|
|
10
|
+
ogImage?: string;
|
|
11
|
+
landing?: boolean | { title?: string; description?: string };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
declare global {
|
|
15
|
+
// Set by the builder's esbuild define (tools/builder/core/prod-bundle/bundle-options.js):
|
|
16
|
+
// nutin.config.js's markdownSources.sourceFolders, as JSON. Undefined in tsc output (tests).
|
|
17
|
+
var __NUTIN_MARKDOWN_SOURCES__: string | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface IMarkdownSourcesConfig {
|
|
21
|
+
sourceFolders?: MarkdownSourceEntry[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function slugify(text: string): string {
|
|
25
|
+
return text
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
28
|
+
.replace(/^-+|-+$/g, '');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Mirrors normalizeSource() in tools/builder/core/markdown/markdown-compiler.js, which names
|
|
32
|
+
// each manifest: keep both id rules identical (the build validates entries, not this).
|
|
33
|
+
export function resolveMarkdownSources(config: IMarkdownSourcesConfig | undefined): IMarkdownSource[] {
|
|
34
|
+
return (config?.sourceFolders ?? []).map((entry) => {
|
|
35
|
+
const options = typeof entry === 'string' ? { folder: entry } : entry;
|
|
36
|
+
const folderName = options.folder.replace(/[\\/]+$/, '').split(/[\\/]/).pop() ?? '';
|
|
37
|
+
return {
|
|
38
|
+
id: options.routePrefix ?? slugify(folderName),
|
|
39
|
+
sectionInPath: options.sectionInPath === true,
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Every selector is wrapped in :where(), so it has no specificity: any rule of your own wins.
|
|
2
|
+
// Adjust sizes through the custom properties below, e.g. in your own stylesheet:
|
|
3
|
+
// .markdown-view { --markdown-nav-width: 20rem; }
|
|
4
|
+
:where(.markdown-view) {
|
|
5
|
+
--markdown-gap: 2rem;
|
|
6
|
+
--markdown-nav-width: 16rem;
|
|
7
|
+
--markdown-toc-width: 14rem;
|
|
8
|
+
--markdown-line-height: 1.7;
|
|
9
|
+
|
|
10
|
+
display: flex;
|
|
11
|
+
gap: var(--markdown-gap);
|
|
12
|
+
padding: 2rem 1rem;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@media (max-width: 700px) {
|
|
16
|
+
:where(.markdown-view) {
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { MarkdownView } from '#root/dist/src/app/markdown/view/markdown.view.js';
|
|
2
|
+
import { MarkdownManifest } from '#root/dist/src/app/markdown/markdown-manifest.js';
|
|
3
|
+
import { MarkdownManifestsService } from '#root/dist/src/app/markdown/markdown-manifests.service.js';
|
|
4
|
+
import { MarkdownNavComponent } from '#root/dist/src/app/markdown/components/markdown-nav/markdown-nav.component.js';
|
|
5
|
+
import { MarkdownContentComponent } from '#root/dist/src/app/markdown/components/markdown-content/markdown-content.component.js';
|
|
6
|
+
|
|
7
|
+
const page = (slug, section, title) => ({
|
|
8
|
+
slug, section, title, description: `About ${title}`, group: null, order: 0, source: `${slug}.md`, headings: [], html: `<h1>${title}</h1>`,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const DATA = {
|
|
12
|
+
sections: [
|
|
13
|
+
{ id: 'api', title: 'API', description: 'The API.', pages: ['a'] },
|
|
14
|
+
{ id: 'tools', title: 'Tools', description: '', pages: ['b'] },
|
|
15
|
+
],
|
|
16
|
+
pages: { a: page('a', 'api', 'Page A'), b: page('b', 'tools', 'Page B') },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// A fresh manifest per test: the service's own manifests are shared singletons.
|
|
20
|
+
async function manifestOf(data, sectionInPath = false) {
|
|
21
|
+
const originalFetch = global.fetch;
|
|
22
|
+
global.fetch = () => Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve(data) });
|
|
23
|
+
try {
|
|
24
|
+
const manifest = new MarkdownManifest({ id: 'docs', sectionInPath }, () => null);
|
|
25
|
+
await manifest.load();
|
|
26
|
+
return manifest;
|
|
27
|
+
} finally {
|
|
28
|
+
global.fetch = originalFetch;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function viewOf(manifest, params = {}, options = {}) {
|
|
33
|
+
const view = new MarkdownView({ id: MarkdownManifestsService.ids[0], ...options });
|
|
34
|
+
view.manifest = manifest;
|
|
35
|
+
view.setRouteParams(params);
|
|
36
|
+
return view;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('MarkdownView', () => {
|
|
40
|
+
let replaceSpy;
|
|
41
|
+
let view;
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
// NavigationManager.replaceState() ends in history.replaceState(state, title, url).
|
|
45
|
+
replaceSpy = spyOn(window.history, 'replaceState').andCallFake(() => {});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
view?.onExit();
|
|
50
|
+
view?.destroy();
|
|
51
|
+
view = null;
|
|
52
|
+
replaceSpy.restore();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('renders the page of the route, and uses its title as the document title', async () => {
|
|
56
|
+
view = viewOf(await manifestOf(DATA), { slug: 'b' });
|
|
57
|
+
view.render();
|
|
58
|
+
expect(view.getElement().querySelector('.markdown-content__body h1').textContent).toBe('Page B');
|
|
59
|
+
expect(view.documentTitle()).toBe('Page B');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('without a landing, renders the first page on the bare route and points the URL at it', async () => {
|
|
63
|
+
view = viewOf(await manifestOf(DATA));
|
|
64
|
+
view.render();
|
|
65
|
+
view.onEnter();
|
|
66
|
+
expect(view.getElement().querySelector('.markdown-content__body h1').textContent).toBe('Page A');
|
|
67
|
+
expect(replaceSpy.lastCall[2]).toBe('/docs/a');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('with a landing, renders it on the bare route and keeps the URL', async () => {
|
|
71
|
+
view = viewOf(await manifestOf({ ...DATA, landing: { title: 'Docs', description: 'Everything.' } }));
|
|
72
|
+
view.render();
|
|
73
|
+
view.onEnter();
|
|
74
|
+
const element = view.getElement();
|
|
75
|
+
expect(element.querySelector('.markdown-landing__title').textContent).toBe('Docs');
|
|
76
|
+
expect(element.querySelector('.markdown-landing__description').textContent).toBe('Everything.');
|
|
77
|
+
expect([...element.querySelectorAll('.markdown-landing__link')].map((a) => a.getAttribute('href'))).toEqual(['/docs/a', '/docs/b']);
|
|
78
|
+
expect(element.querySelector('.markdown-content__body')).toBe(null);
|
|
79
|
+
expect(replaceSpy.callCount).toBe(0);
|
|
80
|
+
expect(view.documentTitle()).toBe('Docs');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('with sections in the URL, the folder landing links each section and the nav lists them all', async () => {
|
|
84
|
+
view = viewOf(await manifestOf({ ...DATA, landing: { title: 'Docs', description: '' } }, true));
|
|
85
|
+
view.render();
|
|
86
|
+
const element = view.getElement();
|
|
87
|
+
expect([...element.querySelectorAll('.markdown-landing__link')].map((a) => a.getAttribute('href'))).toEqual(['/docs/api', '/docs/tools']);
|
|
88
|
+
expect([...element.querySelectorAll('.markdown-nav__section-title')].map((s) => s.textContent)).toEqual(['API', 'Tools']);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('with sections in the URL, a section landing lists its pages under its own title', async () => {
|
|
92
|
+
view = viewOf(await manifestOf({ ...DATA, landing: { title: 'Docs', description: '' } }, true), { section: 'api' });
|
|
93
|
+
view.render();
|
|
94
|
+
const element = view.getElement();
|
|
95
|
+
expect(element.querySelector('.markdown-landing__title').textContent).toBe('API');
|
|
96
|
+
expect([...element.querySelectorAll('.markdown-landing__link')].map((a) => a.getAttribute('href'))).toEqual(['/docs/api/a']);
|
|
97
|
+
expect(view.documentTitle()).toBe('API');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('uses the nav and content components passed as options', async () => {
|
|
101
|
+
class MyNav extends MarkdownNavComponent {
|
|
102
|
+
constructor(el, config) { super(el, config, () => '<nav class="my-nav"></nav>'); }
|
|
103
|
+
}
|
|
104
|
+
class MyContent extends MarkdownContentComponent {}
|
|
105
|
+
view = viewOf(await manifestOf(DATA), { slug: 'a' }, { navComponent: MyNav, contentComponent: MyContent });
|
|
106
|
+
view.render();
|
|
107
|
+
expect(view.getElement().querySelector('.my-nav')).toBeTruthy();
|
|
108
|
+
expect(view.getElement().querySelector('.markdown-nav')).toBe(null);
|
|
109
|
+
expect(view.getElement().querySelector('.markdown-content__body')).toBeTruthy();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('calls onContentRendered with the content element and the page', async () => {
|
|
113
|
+
const calls = [];
|
|
114
|
+
view = viewOf(await manifestOf(DATA), { slug: 'a' }, { onContentRendered: (element, shown) => calls.push([element.className, shown.slug]) });
|
|
115
|
+
view.render();
|
|
116
|
+
expect(calls).toEqual([['markdown-content', 'a']]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('renders without a nav when the template has no nav placeholder', async () => {
|
|
120
|
+
view = viewOf(await manifestOf(DATA), { slug: 'a' }, { template: '<div><div data-component="markdown-content"></div></div>' });
|
|
121
|
+
view.render();
|
|
122
|
+
expect(view.getElement().querySelector('.markdown-nav')).toBe(null);
|
|
123
|
+
expect(view.getElement().querySelector('.markdown-content__body')).toBeTruthy();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Component, Navigation, NavigationManager, View, html } from '../../../core/index.js';
|
|
2
|
+
import { IMarkdownPage, IMarkdownSection, MarkdownManifest } from '../markdown-manifest.js';
|
|
3
|
+
import { MarkdownManifestsService } from '../markdown-manifests.service.js';
|
|
4
|
+
import { IMarkdownContentConfig, MarkdownContentComponent } from '../components/markdown-content/markdown-content.component.js';
|
|
5
|
+
import { IMarkdownLandingConfig, MarkdownLandingComponent } from '../components/markdown-landing/markdown-landing.component.js';
|
|
6
|
+
import { IMarkdownNavConfig, MarkdownNavComponent } from '../components/markdown-nav/markdown-nav.component.js';
|
|
7
|
+
import { onMarkdownLanguageChange } from '../markdown-i18n.js';
|
|
8
|
+
|
|
9
|
+
const defaultTemplate = html`__TEMPLATE_PLACEHOLDER__`;
|
|
10
|
+
|
|
11
|
+
export type MarkdownComponentClass<C> = new (mountTarget: HTMLElement, config: C) => Component<HTMLElement, C>;
|
|
12
|
+
|
|
13
|
+
export interface IMarkdownViewOptions {
|
|
14
|
+
// The source's id: its "routePrefix" in nutin.config.js, defaulting to the folder name.
|
|
15
|
+
id: string;
|
|
16
|
+
// A custom layout needs data-component="markdown-nav" and data-component="markdown-content"
|
|
17
|
+
// (the content placeholder also receives the landing). Leave out "markdown-nav" for no nav.
|
|
18
|
+
template?: Nutin.Template;
|
|
19
|
+
viewName?: string;
|
|
20
|
+
// Replace a part of the page with your own component class, e.g. a subclass with its own
|
|
21
|
+
// template (see the components' optional `templateFn` constructor argument).
|
|
22
|
+
navComponent?: MarkdownComponentClass<IMarkdownNavConfig>;
|
|
23
|
+
contentComponent?: MarkdownComponentClass<IMarkdownContentConfig>;
|
|
24
|
+
landingComponent?: MarkdownComponentClass<IMarkdownLandingConfig>;
|
|
25
|
+
// Called after a page's content is rendered, e.g. to highlight its code blocks.
|
|
26
|
+
onContentRendered?: (element: HTMLElement, page: IMarkdownPage) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Renders one page of a "markdownSources" folder. markdownRoutes() registers one per folder;
|
|
30
|
+
// with "sectionInPath: true" the route carries ':section?' before ':slug?' (e.g.
|
|
31
|
+
// '/docs/api/navigate'), the nav only lists that section and every URL includes it.
|
|
32
|
+
// Subclass it to change more: its getters and helpers are protected.
|
|
33
|
+
export class MarkdownView extends View {
|
|
34
|
+
protected readonly manifest: MarkdownManifest;
|
|
35
|
+
protected readonly options: IMarkdownViewOptions;
|
|
36
|
+
private stopLanguageSync: () => void = () => {};
|
|
37
|
+
|
|
38
|
+
constructor(options: IMarkdownViewOptions) {
|
|
39
|
+
const { id, template = defaultTemplate, viewName = id } = options;
|
|
40
|
+
super({ template, viewName });
|
|
41
|
+
this.options = options;
|
|
42
|
+
this.manifest = MarkdownManifestsService.get(id);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
protected get section(): IMarkdownSection | undefined {
|
|
46
|
+
if (!this.manifest.sectionInPath) return undefined;
|
|
47
|
+
return this.manifest.getSection(this.getRouteParam('section') ?? '') ?? this.manifest.sections[0];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// A bare route (no slug) of a folder with "landing": its index is shown instead of a page.
|
|
51
|
+
protected get isLanding(): boolean {
|
|
52
|
+
return !!this.manifest.landing && !this.getRouteParam('slug');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// MarkdownGuards already loaded the manifest and checked this slug resolves to a real
|
|
56
|
+
// page (or that the folder has no pages yet) before the view is constructed.
|
|
57
|
+
protected get slug(): string {
|
|
58
|
+
if (this.isLanding) return '';
|
|
59
|
+
const fallback = this.manifest.sectionInPath ? this.manifest.firstSlugOf(this.section) : this.manifest.firstSlug;
|
|
60
|
+
return this.getRouteParam('slug') || fallback || '';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
protected get page(): IMarkdownPage | undefined {
|
|
64
|
+
return this.slug ? this.manifest.getPage(this.slug) : undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
protected get navSections(): IMarkdownSection[] {
|
|
68
|
+
if (!this.manifest.sectionInPath) return this.manifest.sections;
|
|
69
|
+
// The folder's own landing lists every section, so its nav does too.
|
|
70
|
+
if (this.isLanding && !this.getRouteParam('section')) return this.manifest.sections;
|
|
71
|
+
return this.section ? [this.section] : [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
protected pageHref(slug: string): string {
|
|
75
|
+
const section = this.manifest.sectionInPath ? this.manifest.getPage(slug)?.section : undefined;
|
|
76
|
+
return section ? `/${this.manifest.id}/${section}/${slug}` : `/${this.manifest.id}/${slug}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The landing shown: the folder's (every section), or one section's with "sectionInPath".
|
|
80
|
+
protected landingConfig(): IMarkdownLandingConfig {
|
|
81
|
+
const localized = (path: string) => NavigationManager.addLocalePrefix(path);
|
|
82
|
+
const sectionId = this.getRouteParam('section');
|
|
83
|
+
const section = sectionId ? this.manifest.getSection(sectionId) : undefined;
|
|
84
|
+
const pageHref = (slug: string) => localized(this.pageHref(slug));
|
|
85
|
+
|
|
86
|
+
if (section) {
|
|
87
|
+
return { title: section.title, description: section.description, sections: [section], manifest: this.manifest, pageHref };
|
|
88
|
+
}
|
|
89
|
+
const landing = this.manifest.landing ?? { title: this.viewName, description: '' };
|
|
90
|
+
return {
|
|
91
|
+
title: landing.title,
|
|
92
|
+
description: landing.description,
|
|
93
|
+
sections: this.manifest.sections,
|
|
94
|
+
manifest: this.manifest,
|
|
95
|
+
pageHref,
|
|
96
|
+
// With sections in the URL, each one has its own landing: link to it instead of listing its pages.
|
|
97
|
+
...(this.manifest.sectionInPath ? { sectionHref: (id: string) => localized(`/${this.manifest.id}/${id}`) } : {}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// document.title after each navigation: the page's (or landing's) title, as in its SEO page.
|
|
102
|
+
public override documentTitle(): string | undefined {
|
|
103
|
+
if (this.isLanding) return this.landingConfig().title;
|
|
104
|
+
return this.page?.title;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The bare route (e.g. /content) renders the first page through the fallback above;
|
|
108
|
+
// point the URL at that page so reloads and bookmarks match what is shown.
|
|
109
|
+
public onEnter(): void {
|
|
110
|
+
if (!this.isLanding && !this.getRouteParam('slug') && this.slug) {
|
|
111
|
+
NavigationManager.replaceState(this.pageHref(this.slug));
|
|
112
|
+
}
|
|
113
|
+
// With i18n, a language change reloads the route: MarkdownGuards load the new
|
|
114
|
+
// language's manifest, and the same page renders in that language.
|
|
115
|
+
this.stopLanguageSync = onMarkdownLanguageChange(() => Navigation.reload());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public onExit(): void {
|
|
119
|
+
this.stopLanguageSync();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
public registerChildren(): Nutin.ComponentConfig[] {
|
|
123
|
+
const Nav = this.options.navComponent ?? MarkdownNavComponent;
|
|
124
|
+
const Content = this.options.contentComponent ?? MarkdownContentComponent;
|
|
125
|
+
const Landing = this.options.landingComponent ?? MarkdownLandingComponent;
|
|
126
|
+
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
selector: 'markdown-nav',
|
|
130
|
+
factory: (el) => new Nav(el, {
|
|
131
|
+
sections: this.navSections,
|
|
132
|
+
manifest: this.manifest,
|
|
133
|
+
currentSlug: this.slug,
|
|
134
|
+
// The links' hrefs carry the locale prefix with i18n (replaceState adds it itself).
|
|
135
|
+
pageHref: (slug) => NavigationManager.addLocalePrefix(this.pageHref(slug)),
|
|
136
|
+
}),
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
selector: 'markdown-content',
|
|
140
|
+
factory: (el) => this.isLanding
|
|
141
|
+
? new Landing(el, this.landingConfig())
|
|
142
|
+
: new Content(el, { page: this.page, loadFailed: this.manifest.loadFailed, onRendered: this.options.onContentRendered }),
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
}
|
package/templates/features/markdown/tools/builder/core/markdown/generate-markdown-pages.js.hbs
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { print, ensureDeps, errorExit } from '../../../utils/index.js';
|
|
4
|
+
import { PATHS } from '../app/paths.js';
|
|
5
|
+
import { builderConfig } from '../../builder.config.js';
|
|
6
|
+
import nutinConfig from '../../../../nutin.config.js';
|
|
7
|
+
import { LANGUAGES, DEFAULT_LANGUAGE } from '../../../utils/languages.js';
|
|
8
|
+
import { compileLanguages, normalizeSource, MarkdownCompileError } from './markdown-compiler.js';
|
|
9
|
+
|
|
10
|
+
// Compiles every `markdownSources.sourceFolders` entry of nutin.config.js into
|
|
11
|
+
// dist/generated/<routePrefix>.json — or, with i18n, <routePrefix>.<lang>.json per language —
|
|
12
|
+
// fetched at runtime by a MarkdownManifest.
|
|
13
|
+
// `--check` (the "markdown:check" script) compiles everything but writes nothing.
|
|
14
|
+
|
|
15
|
+
const isCheck = process.argv.includes('--check');
|
|
16
|
+
|
|
17
|
+
const sourceFolders = nutinConfig.markdownSources?.sourceFolders;
|
|
18
|
+
if (!Array.isArray(sourceFolders)) {
|
|
19
|
+
errorExit('nutin.config.js "markdownSources.sourceFolders" must be an array.', 'markdown');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
await ensureDeps({
|
|
23
|
+
feature: 'Markdown (markdownSources)',
|
|
24
|
+
deps: [
|
|
25
|
+
{ name: 'marked', version: '^18.0.14' },
|
|
26
|
+
{ name: 'gray-matter', version: '^4.0.3' },
|
|
27
|
+
],
|
|
28
|
+
isProd: builderConfig.isProd,
|
|
29
|
+
origin: 'markdown',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Loaded only now: they may have just been installed.
|
|
33
|
+
const { Marked } = await import('marked');
|
|
34
|
+
const matter = (await import('gray-matter')).default;
|
|
35
|
+
|
|
36
|
+
const root = process.cwd();
|
|
37
|
+
const outputDir = path.join(PATHS.tempSource, 'generated');
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const sources = sourceFolders.map((entry) => normalizeSource(entry, root));
|
|
41
|
+
|
|
42
|
+
const seenIds = new Map();
|
|
43
|
+
for (const source of sources) {
|
|
44
|
+
if (seenIds.has(source.id)) {
|
|
45
|
+
throw new MarkdownCompileError(`"${seenIds.get(source.id)}" and "${source.folder}" both compile to "${source.id}.json" — give one a different "routePrefix"`);
|
|
46
|
+
}
|
|
47
|
+
seenIds.set(source.id, source.folder);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const languageOptions = { i18n: Boolean(nutinConfig.i18n), languages: LANGUAGES, defaultLanguage: DEFAULT_LANGUAGE };
|
|
51
|
+
|
|
52
|
+
for (const source of sources) {
|
|
53
|
+
const { manifests, warnings } = compileLanguages(source, { Marked, matter, root }, languageOptions);
|
|
54
|
+
warnings.forEach((warning) => print.warn(`markdown: ${warning}`));
|
|
55
|
+
|
|
56
|
+
for (const [lang, manifest] of Object.entries(manifests)) {
|
|
57
|
+
const fileName = lang ? `${source.id}.${lang}.json` : `${source.id}.json`;
|
|
58
|
+
const pageCount = Object.keys(manifest.pages).length;
|
|
59
|
+
if (isCheck) {
|
|
60
|
+
print.info(`markdown: "${source.folder}"${lang ? ` (${lang})` : ''} OK (${pageCount} pages)`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
65
|
+
fs.writeFileSync(path.join(outputDir, fileName), JSON.stringify(manifest));
|
|
66
|
+
print.gray(`markdown: ${pageCount} pages from "${source.folder}" -> /generated/${fileName}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch (err) {
|
|
70
|
+
errorExit(err instanceof MarkdownCompileError ? err.message : err, 'markdown');
|
|
71
|
+
}
|