@duffcloudservices/cms-angular 0.1.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/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # @duffcloudservices/cms-angular
2
+
3
+ Angular services and build scripts for DCS CMS integration.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @duffcloudservices/cms-angular
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### 1. Add Provider
14
+
15
+ ```typescript
16
+ // app.config.ts
17
+ import { ApplicationConfig } from '@angular/core'
18
+ import { provideDcs } from '@duffcloudservices/cms-angular'
19
+
20
+ export const appConfig: ApplicationConfig = {
21
+ providers: [
22
+ provideDcs(),
23
+ ],
24
+ }
25
+ ```
26
+
27
+ ### 2. Use in Components
28
+
29
+ ```typescript
30
+ // pages/home.component.ts
31
+ import { Component, inject, OnInit } from '@angular/core'
32
+ import { DcsContentService, DcsSeoService } from '@duffcloudservices/cms-angular'
33
+
34
+ @Component({
35
+ selector: 'app-home',
36
+ template: `
37
+ <h1>{{ title }}</h1>
38
+ <p>{{ subtitle }}</p>
39
+ `,
40
+ })
41
+ export class HomeComponent implements OnInit {
42
+ private content = inject(DcsContentService)
43
+ private seo = inject(DcsSeoService)
44
+
45
+ get title() {
46
+ return this.content.t('home', 'hero.title', 'Welcome')
47
+ }
48
+
49
+ get subtitle() {
50
+ return this.content.t('home', 'hero.subtitle', 'Build amazing things')
51
+ }
52
+
53
+ ngOnInit() {
54
+ this.seo.setPageSeo('home')
55
+ }
56
+ }
57
+ ```
58
+
59
+ ### 3. Post-Build Injection
60
+
61
+ After running `ng build`, inject the DCS content into index.html:
62
+
63
+ ```bash
64
+ npx dcs-inject dist/my-app
65
+ ```
66
+
67
+ Or add to your package.json scripts:
68
+
69
+ ```json
70
+ {
71
+ "scripts": {
72
+ "build": "ng build && dcs-inject dist/my-app"
73
+ }
74
+ }
75
+ ```
76
+
77
+ ### DCS Configuration Files
78
+
79
+ Create `.dcs/content.yaml`:
80
+
81
+ ```yaml
82
+ version: 1
83
+ global:
84
+ nav.home: Home
85
+ footer.copyright: © 2026 Company
86
+ pages:
87
+ home:
88
+ hero.title: Welcome to Our Site
89
+ hero.subtitle: Build something amazing
90
+ ```
91
+
92
+ Create `.dcs/seo.yaml`:
93
+
94
+ ```yaml
95
+ version: 1
96
+ global:
97
+ siteName: My Site
98
+ defaultTitle: My Site
99
+ titleTemplate: "%s | My Site"
100
+ pages:
101
+ home:
102
+ title: Welcome
103
+ description: The homepage of My Site
104
+ ```
105
+
106
+ ## API
107
+
108
+ ### DcsContentService
109
+
110
+ Service for text content management.
111
+
112
+ **Methods:**
113
+ - `t(page, key, fallback?)`: Get text by page and key
114
+ - `getPageContent(page)`: Get all content for a page
115
+ - `fetchRuntimeContent(siteSlug, apiBaseUrl?)`: Fetch runtime content (premium)
116
+
117
+ **Signals:**
118
+ - `content`: Current content configuration
119
+ - `isLoading`: Loading state
120
+ - `error`: Error message
121
+
122
+ ### DcsSeoService
123
+
124
+ Service for SEO meta tag management.
125
+
126
+ **Methods:**
127
+ - `setPageSeo(page, overrides?)`: Apply SEO for a page
128
+ - `getSeoConfig()`: Get raw SEO configuration
129
+
130
+ ### CLI: dcs-inject
131
+
132
+ Inject content into Angular build output.
133
+
134
+ ```bash
135
+ npx dcs-inject <dist-path> [options]
136
+
137
+ Options:
138
+ --content-path Path to content.yaml (default: .dcs/content.yaml)
139
+ --seo-path Path to seo.yaml (default: .dcs/seo.yaml)
140
+ --index-file Index file name (default: index.html)
141
+ ```
142
+
143
+ ## License
144
+
145
+ MIT
@@ -0,0 +1,111 @@
1
+ import * as _angular_core from '@angular/core';
2
+ import { EnvironmentProviders } from '@angular/core';
3
+ import { ContentConfiguration, ResolvedSeo, SeoConfiguration } from '@duffcloudservices/cms-core/browser';
4
+ export { ContentConfiguration, GlobalSeoConfig, PageSeoConfig, ResolvedSeo, SeoConfiguration } from '@duffcloudservices/cms-core';
5
+
6
+ /**
7
+ * Angular service for DCS text content management.
8
+ */
9
+ declare class DcsContentService {
10
+ /** Build-time content (injected via index.html script) */
11
+ private readonly buildContent;
12
+ /** Runtime content (fetched if runtime mode enabled) */
13
+ private readonly runtimeContent;
14
+ /** Loading state for runtime fetch */
15
+ readonly isLoading: _angular_core.WritableSignal<boolean>;
16
+ /** Error state for runtime fetch */
17
+ readonly error: _angular_core.WritableSignal<string | null>;
18
+ /** Current content (runtime overrides build-time) */
19
+ readonly content: _angular_core.Signal<ContentConfiguration | undefined>;
20
+ /**
21
+ * Safely get build-time content configuration.
22
+ */
23
+ private getBuildTimeContent;
24
+ /**
25
+ * Get text for a specific page and key.
26
+ *
27
+ * @param page - Page slug matching entry in content.yaml
28
+ * @param key - Text key to retrieve
29
+ * @param fallback - Fallback value if key not found
30
+ * @returns The resolved text value
31
+ */
32
+ t(page: string, key: string, fallback?: string): string;
33
+ /**
34
+ * Get all content for a specific page (merged global + page).
35
+ *
36
+ * @param page - Page slug
37
+ * @returns Merged content object
38
+ */
39
+ getPageContent(page: string): Record<string, string>;
40
+ /**
41
+ * Fetch runtime content from DCS API (premium tier only).
42
+ *
43
+ * @param siteSlug - The site's slug identifier
44
+ * @param apiBaseUrl - Optional API base URL override
45
+ */
46
+ fetchRuntimeContent(siteSlug: string, apiBaseUrl?: string): Promise<void>;
47
+ }
48
+
49
+ /**
50
+ * DcsSeoService for Angular
51
+ *
52
+ * Applies SEO meta tags to the document head based on .dcs/seo.yaml configuration.
53
+ */
54
+
55
+ /**
56
+ * Angular service for DCS SEO management.
57
+ */
58
+ declare class DcsSeoService {
59
+ private readonly meta;
60
+ private readonly title;
61
+ /** Build-time SEO config (injected via index.html script) */
62
+ private readonly seoConfig;
63
+ /**
64
+ * Safely get build-time SEO configuration.
65
+ */
66
+ private getBuildTimeSeo;
67
+ /**
68
+ * Apply SEO for a specific page.
69
+ *
70
+ * @param page - Page slug matching entry in seo.yaml
71
+ * @param overrides - Optional overrides for title, description, or image
72
+ * @returns The resolved SEO configuration
73
+ */
74
+ setPageSeo(page: string, overrides?: {
75
+ title?: string;
76
+ description?: string;
77
+ image?: string;
78
+ }): ResolvedSeo | null;
79
+ /**
80
+ * Update or create canonical link element.
81
+ */
82
+ private updateCanonicalLink;
83
+ /**
84
+ * Get the raw SEO configuration.
85
+ */
86
+ getSeoConfig(): SeoConfiguration | undefined;
87
+ }
88
+
89
+ /**
90
+ * Angular providers for DCS CMS integration.
91
+ */
92
+
93
+ /**
94
+ * Provide DCS CMS services for Angular application.
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * // app.config.ts
99
+ * import { ApplicationConfig } from '@angular/core'
100
+ * import { provideDcs } from '@duffcloudservices/cms-angular'
101
+ *
102
+ * export const appConfig: ApplicationConfig = {
103
+ * providers: [
104
+ * provideDcs(),
105
+ * ],
106
+ * }
107
+ * ```
108
+ */
109
+ declare function provideDcs(): EnvironmentProviders;
110
+
111
+ export { DcsContentService, DcsSeoService, provideDcs };
package/dist/index.js ADDED
@@ -0,0 +1,159 @@
1
+ import { Injectable, signal, computed, inject, makeEnvironmentProviders } from '@angular/core';
2
+ import { resolveTextKey, getPageContent, fetchRuntimeContent, resolveSeoForPage, buildMetaTags } from '@duffcloudservices/cms-core/browser';
3
+ import { Meta, Title } from '@angular/platform-browser';
4
+
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __decorateClass = (decorators, target, key, kind) => {
7
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
8
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
9
+ if (decorator = decorators[i])
10
+ result = (decorator(result)) || result;
11
+ return result;
12
+ };
13
+ var DcsContentService = class {
14
+ /** Build-time content (injected via index.html script) */
15
+ buildContent = this.getBuildTimeContent();
16
+ /** Runtime content (fetched if runtime mode enabled) */
17
+ runtimeContent = signal(null);
18
+ /** Loading state for runtime fetch */
19
+ isLoading = signal(false);
20
+ /** Error state for runtime fetch */
21
+ error = signal(null);
22
+ /** Current content (runtime overrides build-time) */
23
+ content = computed(() => this.runtimeContent() ?? this.buildContent);
24
+ /**
25
+ * Safely get build-time content configuration.
26
+ */
27
+ getBuildTimeContent() {
28
+ try {
29
+ if (__DCS_CONTENT__ !== void 0 && __DCS_CONTENT__ !== null) {
30
+ return __DCS_CONTENT__;
31
+ }
32
+ } catch {
33
+ }
34
+ return void 0;
35
+ }
36
+ /**
37
+ * Get text for a specific page and key.
38
+ *
39
+ * @param page - Page slug matching entry in content.yaml
40
+ * @param key - Text key to retrieve
41
+ * @param fallback - Fallback value if key not found
42
+ * @returns The resolved text value
43
+ */
44
+ t(page, key, fallback) {
45
+ const c = this.content();
46
+ if (!c) return fallback ?? key;
47
+ return resolveTextKey(c, page, key) ?? fallback ?? key;
48
+ }
49
+ /**
50
+ * Get all content for a specific page (merged global + page).
51
+ *
52
+ * @param page - Page slug
53
+ * @returns Merged content object
54
+ */
55
+ getPageContent(page) {
56
+ const c = this.content();
57
+ if (!c) return {};
58
+ return getPageContent(c, page);
59
+ }
60
+ /**
61
+ * Fetch runtime content from DCS API (premium tier only).
62
+ *
63
+ * @param siteSlug - The site's slug identifier
64
+ * @param apiBaseUrl - Optional API base URL override
65
+ */
66
+ async fetchRuntimeContent(siteSlug, apiBaseUrl) {
67
+ this.isLoading.set(true);
68
+ this.error.set(null);
69
+ try {
70
+ const data = await fetchRuntimeContent(siteSlug, { apiBaseUrl });
71
+ if (data) {
72
+ this.runtimeContent.set(data);
73
+ }
74
+ } catch (err) {
75
+ this.error.set(err instanceof Error ? err.message : "Failed to fetch content");
76
+ } finally {
77
+ this.isLoading.set(false);
78
+ }
79
+ }
80
+ };
81
+ DcsContentService = __decorateClass([
82
+ Injectable({ providedIn: "root" })
83
+ ], DcsContentService);
84
+ var DcsSeoService = class {
85
+ meta = inject(Meta);
86
+ title = inject(Title);
87
+ /** Build-time SEO config (injected via index.html script) */
88
+ seoConfig = this.getBuildTimeSeo();
89
+ /**
90
+ * Safely get build-time SEO configuration.
91
+ */
92
+ getBuildTimeSeo() {
93
+ try {
94
+ if (__DCS_SEO__ !== void 0 && __DCS_SEO__ !== null) {
95
+ return __DCS_SEO__;
96
+ }
97
+ } catch {
98
+ }
99
+ return void 0;
100
+ }
101
+ /**
102
+ * Apply SEO for a specific page.
103
+ *
104
+ * @param page - Page slug matching entry in seo.yaml
105
+ * @param overrides - Optional overrides for title, description, or image
106
+ * @returns The resolved SEO configuration
107
+ */
108
+ setPageSeo(page, overrides) {
109
+ if (!this.seoConfig) return null;
110
+ const seo = resolveSeoForPage(this.seoConfig, page);
111
+ const resolved = {
112
+ ...seo,
113
+ ...overrides?.title && { title: overrides.title },
114
+ ...overrides?.description && { description: overrides.description },
115
+ ...overrides?.image && { image: overrides.image }
116
+ };
117
+ this.title.setTitle(resolved.title);
118
+ const tags = buildMetaTags(resolved);
119
+ for (const tag of tags) {
120
+ if (tag.property) {
121
+ this.meta.updateTag({ property: tag.property, content: tag.content });
122
+ } else if (tag.name) {
123
+ this.meta.updateTag({ name: tag.name, content: tag.content });
124
+ }
125
+ }
126
+ if (resolved.canonical) {
127
+ this.updateCanonicalLink(resolved.canonical);
128
+ }
129
+ return resolved;
130
+ }
131
+ /**
132
+ * Update or create canonical link element.
133
+ */
134
+ updateCanonicalLink(href) {
135
+ let link = document.querySelector('link[rel="canonical"]');
136
+ if (!link) {
137
+ link = document.createElement("link");
138
+ link.setAttribute("rel", "canonical");
139
+ document.head.appendChild(link);
140
+ }
141
+ link.setAttribute("href", href);
142
+ }
143
+ /**
144
+ * Get the raw SEO configuration.
145
+ */
146
+ getSeoConfig() {
147
+ return this.seoConfig;
148
+ }
149
+ };
150
+ DcsSeoService = __decorateClass([
151
+ Injectable({ providedIn: "root" })
152
+ ], DcsSeoService);
153
+ function provideDcs() {
154
+ return makeEnvironmentProviders([DcsContentService, DcsSeoService]);
155
+ }
156
+
157
+ export { DcsContentService, DcsSeoService, provideDcs };
158
+ //# sourceMappingURL=index.js.map
159
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/services/content.service.ts","../src/services/seo.service.ts","../src/providers.ts"],"names":["Injectable"],"mappings":"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","file":"index.js","sourcesContent":["/**\r\n * DcsContentService for Angular\r\n *\r\n * Provides text content management with build-time injection support and optional\r\n * runtime API overrides for DCS-managed customer sites.\r\n */\r\n\r\nimport { Injectable, signal, computed } from '@angular/core'\r\nimport {\r\n resolveTextKey,\r\n getPageContent,\r\n fetchRuntimeContent,\r\n type ContentConfiguration,\r\n} from '@duffcloudservices/cms-core/browser'\r\n\r\n// Declare the global injected by build script\r\ndeclare const __DCS_CONTENT__: ContentConfiguration | undefined\r\n\r\n/**\r\n * Angular service for DCS text content management.\r\n */\r\n@Injectable({ providedIn: 'root' })\r\nexport class DcsContentService {\r\n /** Build-time content (injected via index.html script) */\r\n private readonly buildContent = this.getBuildTimeContent()\r\n\r\n /** Runtime content (fetched if runtime mode enabled) */\r\n private readonly runtimeContent = signal<ContentConfiguration | null>(null)\r\n\r\n /** Loading state for runtime fetch */\r\n readonly isLoading = signal(false)\r\n\r\n /** Error state for runtime fetch */\r\n readonly error = signal<string | null>(null)\r\n\r\n /** Current content (runtime overrides build-time) */\r\n readonly content = computed(() => this.runtimeContent() ?? this.buildContent)\r\n\r\n /**\r\n * Safely get build-time content configuration.\r\n */\r\n private getBuildTimeContent(): ContentConfiguration | undefined {\r\n try {\r\n if (__DCS_CONTENT__ !== undefined && __DCS_CONTENT__ !== null) {\r\n return __DCS_CONTENT__\r\n }\r\n } catch {\r\n // __DCS_CONTENT__ not defined\r\n }\r\n return undefined\r\n }\r\n\r\n /**\r\n * Get text for a specific page and key.\r\n *\r\n * @param page - Page slug matching entry in content.yaml\r\n * @param key - Text key to retrieve\r\n * @param fallback - Fallback value if key not found\r\n * @returns The resolved text value\r\n */\r\n t(page: string, key: string, fallback?: string): string {\r\n const c = this.content()\r\n if (!c) return fallback ?? key\r\n return resolveTextKey(c, page, key) ?? fallback ?? key\r\n }\r\n\r\n /**\r\n * Get all content for a specific page (merged global + page).\r\n *\r\n * @param page - Page slug\r\n * @returns Merged content object\r\n */\r\n getPageContent(page: string): Record<string, string> {\r\n const c = this.content()\r\n if (!c) return {}\r\n return getPageContent(c, page)\r\n }\r\n\r\n /**\r\n * Fetch runtime content from DCS API (premium tier only).\r\n *\r\n * @param siteSlug - The site's slug identifier\r\n * @param apiBaseUrl - Optional API base URL override\r\n */\r\n async fetchRuntimeContent(siteSlug: string, apiBaseUrl?: string): Promise<void> {\r\n this.isLoading.set(true)\r\n this.error.set(null)\r\n\r\n try {\r\n const data = await fetchRuntimeContent(siteSlug, { apiBaseUrl })\r\n if (data) {\r\n this.runtimeContent.set(data)\r\n }\r\n } catch (err) {\r\n this.error.set(err instanceof Error ? err.message : 'Failed to fetch content')\r\n } finally {\r\n this.isLoading.set(false)\r\n }\r\n }\r\n}\r\n","/**\r\n * DcsSeoService for Angular\r\n *\r\n * Applies SEO meta tags to the document head based on .dcs/seo.yaml configuration.\r\n */\r\n\r\nimport { Injectable, inject } from '@angular/core'\r\nimport { Meta, Title } from '@angular/platform-browser'\r\nimport {\r\n resolveSeoForPage,\r\n buildMetaTags,\r\n type SeoConfiguration,\r\n type ResolvedSeo,\r\n} from '@duffcloudservices/cms-core/browser'\r\n\r\n// Declare the global injected by build script\r\ndeclare const __DCS_SEO__: SeoConfiguration | undefined\r\n\r\n/**\r\n * Angular service for DCS SEO management.\r\n */\r\n@Injectable({ providedIn: 'root' })\r\nexport class DcsSeoService {\r\n private readonly meta = inject(Meta)\r\n private readonly title = inject(Title)\r\n\r\n /** Build-time SEO config (injected via index.html script) */\r\n private readonly seoConfig = this.getBuildTimeSeo()\r\n\r\n /**\r\n * Safely get build-time SEO configuration.\r\n */\r\n private getBuildTimeSeo(): SeoConfiguration | undefined {\r\n try {\r\n if (__DCS_SEO__ !== undefined && __DCS_SEO__ !== null) {\r\n return __DCS_SEO__\r\n }\r\n } catch {\r\n // __DCS_SEO__ not defined\r\n }\r\n return undefined\r\n }\r\n\r\n /**\r\n * Apply SEO for a specific page.\r\n *\r\n * @param page - Page slug matching entry in seo.yaml\r\n * @param overrides - Optional overrides for title, description, or image\r\n * @returns The resolved SEO configuration\r\n */\r\n setPageSeo(\r\n page: string,\r\n overrides?: { title?: string; description?: string; image?: string }\r\n ): ResolvedSeo | null {\r\n if (!this.seoConfig) return null\r\n\r\n const seo = resolveSeoForPage(this.seoConfig, page)\r\n const resolved: ResolvedSeo = {\r\n ...seo,\r\n ...(overrides?.title && { title: overrides.title }),\r\n ...(overrides?.description && { description: overrides.description }),\r\n ...(overrides?.image && { image: overrides.image }),\r\n }\r\n\r\n // Set document title\r\n this.title.setTitle(resolved.title)\r\n\r\n // Build and apply meta tags\r\n const tags = buildMetaTags(resolved)\r\n for (const tag of tags) {\r\n if (tag.property) {\r\n this.meta.updateTag({ property: tag.property, content: tag.content })\r\n } else if (tag.name) {\r\n this.meta.updateTag({ name: tag.name, content: tag.content })\r\n }\r\n }\r\n\r\n // Handle canonical link\r\n if (resolved.canonical) {\r\n this.updateCanonicalLink(resolved.canonical)\r\n }\r\n\r\n return resolved\r\n }\r\n\r\n /**\r\n * Update or create canonical link element.\r\n */\r\n private updateCanonicalLink(href: string): void {\r\n let link = document.querySelector('link[rel=\"canonical\"]')\r\n if (!link) {\r\n link = document.createElement('link')\r\n link.setAttribute('rel', 'canonical')\r\n document.head.appendChild(link)\r\n }\r\n link.setAttribute('href', href)\r\n }\r\n\r\n /**\r\n * Get the raw SEO configuration.\r\n */\r\n getSeoConfig(): SeoConfiguration | undefined {\r\n return this.seoConfig\r\n }\r\n}\r\n","/**\r\n * Angular providers for DCS CMS integration.\r\n */\r\n\r\nimport { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core'\r\nimport { DcsContentService } from './services/content.service'\r\nimport { DcsSeoService } from './services/seo.service'\r\n\r\n/**\r\n * Provide DCS CMS services for Angular application.\r\n *\r\n * @example\r\n * ```typescript\r\n * // app.config.ts\r\n * import { ApplicationConfig } from '@angular/core'\r\n * import { provideDcs } from '@duffcloudservices/cms-angular'\r\n *\r\n * export const appConfig: ApplicationConfig = {\r\n * providers: [\r\n * provideDcs(),\r\n * ],\r\n * }\r\n * ```\r\n */\r\nexport function provideDcs(): EnvironmentProviders {\r\n return makeEnvironmentProviders([DcsContentService, DcsSeoService])\r\n}\r\n"]}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Post-build script to inject DCS content into Angular's index.html
3
+ *
4
+ * Usage:
5
+ * npx dcs-inject dist/my-app
6
+ * node node_modules/@duffcloudservices/cms-angular/dist/scripts/inject-content.js dist/my-app
7
+ *
8
+ * This script:
9
+ * 1. Reads .dcs/content.yaml and .dcs/seo.yaml from the project root
10
+ * 2. Injects them as __DCS_CONTENT__ and __DCS_SEO__ globals in index.html
11
+ * 3. Writes the modified index.html back to the dist folder
12
+ */
13
+ /**
14
+ * Inject DCS content into an Angular build's index.html
15
+ *
16
+ * @param distPath - Path to the Angular build output (e.g., 'dist/my-app')
17
+ * @param options - Optional configuration
18
+ */
19
+ declare function injectContent(distPath: string, options?: {
20
+ contentPath?: string;
21
+ seoPath?: string;
22
+ indexFile?: string;
23
+ }): Promise<void>;
24
+
25
+ export { injectContent };
@@ -0,0 +1,62 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { loadContentYaml, loadSeoYaml } from '@duffcloudservices/cms-core';
4
+
5
+ // src/scripts/inject-content.ts
6
+ async function injectContent(distPath, options = {}) {
7
+ const {
8
+ contentPath = ".dcs/content.yaml",
9
+ seoPath = ".dcs/seo.yaml",
10
+ indexFile = "index.html"
11
+ } = options;
12
+ const indexPath = path.join(distPath, indexFile);
13
+ if (!fs.existsSync(indexPath)) {
14
+ throw new Error(`index.html not found at ${indexPath}`);
15
+ }
16
+ let contentJson = "undefined";
17
+ try {
18
+ const content = await loadContentYaml(contentPath);
19
+ contentJson = JSON.stringify(content);
20
+ console.log(`[dcs-inject] Loaded content from ${contentPath}`);
21
+ } catch {
22
+ console.log(`[dcs-inject] No content.yaml found at ${contentPath}`);
23
+ }
24
+ let seoJson = "undefined";
25
+ try {
26
+ const seo = await loadSeoYaml(seoPath);
27
+ seoJson = JSON.stringify(seo);
28
+ console.log(`[dcs-inject] Loaded SEO config from ${seoPath}`);
29
+ } catch {
30
+ console.log(`[dcs-inject] No seo.yaml found at ${seoPath}`);
31
+ }
32
+ let html = fs.readFileSync(indexPath, "utf8");
33
+ const injection = `
34
+ <script>
35
+ window.__DCS_CONTENT__ = ${contentJson};
36
+ window.__DCS_SEO__ = ${seoJson};
37
+ </script>
38
+ `;
39
+ if (html.includes("</head>")) {
40
+ html = html.replace("</head>", `${injection}</head>`);
41
+ } else {
42
+ html = html.replace("<body", `${injection}<body`);
43
+ }
44
+ fs.writeFileSync(indexPath, html);
45
+ console.log(`[dcs-inject] Content injected into ${indexPath}`);
46
+ }
47
+ if (process.argv[1]?.includes("inject-content")) {
48
+ const distPath = process.argv[2];
49
+ if (!distPath) {
50
+ console.error("Usage: dcs-inject <dist-path>");
51
+ console.error("Example: dcs-inject dist/my-app");
52
+ process.exit(1);
53
+ }
54
+ injectContent(distPath).catch((err) => {
55
+ console.error("[dcs-inject] Error:", err.message);
56
+ process.exit(1);
57
+ });
58
+ }
59
+
60
+ export { injectContent };
61
+ //# sourceMappingURL=inject-content.js.map
62
+ //# sourceMappingURL=inject-content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/scripts/inject-content.ts"],"names":[],"mappings":";;;;;AAwBA,eAAsB,aAAA,CACpB,QAAA,EACA,OAAA,GAII,EAAC,EACU;AACf,EAAA,MAAM;AAAA,IACJ,WAAA,GAAc,mBAAA;AAAA,IACd,OAAA,GAAU,eAAA;AAAA,IACV,SAAA,GAAY;AAAA,GACd,GAAI,OAAA;AAEJ,EAAA,MAAM,SAAA,GAAY,IAAA,CAAK,IAAA,CAAK,QAAA,EAAU,SAAS,CAAA;AAG/C,EAAA,IAAI,CAAC,EAAA,CAAG,UAAA,CAAW,SAAS,CAAA,EAAG;AAC7B,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,wBAAA,EAA2B,SAAS,CAAA,CAAE,CAAA;AAAA,EACxD;AAGA,EAAA,IAAI,WAAA,GAAc,WAAA;AAClB,EAAA,IAAI;AACF,IAAA,MAAM,OAAA,GAAU,MAAM,eAAA,CAAgB,WAAW,CAAA;AACjD,IAAA,WAAA,GAAc,IAAA,CAAK,UAAU,OAAO,CAAA;AACpC,IAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,iCAAA,EAAoC,WAAW,CAAA,CAAE,CAAA;AAAA,EAC/D,CAAA,CAAA,MAAQ;AACN,IAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,sCAAA,EAAyC,WAAW,CAAA,CAAE,CAAA;AAAA,EACpE;AAGA,EAAA,IAAI,OAAA,GAAU,WAAA;AACd,EAAA,IAAI;AACF,IAAA,MAAM,GAAA,GAAM,MAAM,WAAA,CAAY,OAAO,CAAA;AACrC,IAAA,OAAA,GAAU,IAAA,CAAK,UAAU,GAAG,CAAA;AAC5B,IAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,oCAAA,EAAuC,OAAO,CAAA,CAAE,CAAA;AAAA,EAC9D,CAAA,CAAA,MAAQ;AACN,IAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,kCAAA,EAAqC,OAAO,CAAA,CAAE,CAAA;AAAA,EAC5D;AAGA,EAAA,IAAI,IAAA,GAAO,EAAA,CAAG,YAAA,CAAa,SAAA,EAAW,MAAM,CAAA;AAG5C,EAAA,MAAM,SAAA,GAAY;AAAA;AAAA,2BAAA,EAES,WAAW,CAAA;AAAA,uBAAA,EACf,OAAO,CAAA;AAAA;AAAA,CAAA;AAK9B,EAAA,IAAI,IAAA,CAAK,QAAA,CAAS,SAAS,CAAA,EAAG;AAC5B,IAAA,IAAA,GAAO,IAAA,CAAK,OAAA,CAAQ,SAAA,EAAW,CAAA,EAAG,SAAS,CAAA,OAAA,CAAS,CAAA;AAAA,EACtD,CAAA,MAAO;AAEL,IAAA,IAAA,GAAO,IAAA,CAAK,OAAA,CAAQ,OAAA,EAAS,CAAA,EAAG,SAAS,CAAA,KAAA,CAAO,CAAA;AAAA,EAClD;AAGA,EAAA,EAAA,CAAG,aAAA,CAAc,WAAW,IAAI,CAAA;AAChC,EAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,mCAAA,EAAsC,SAAS,CAAA,CAAE,CAAA;AAC/D;AAGA,IAAI,QAAQ,IAAA,CAAK,CAAC,CAAA,EAAG,QAAA,CAAS,gBAAgB,CAAA,EAAG;AAC/C,EAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,IAAA,CAAK,CAAC,CAAA;AAC/B,EAAA,IAAI,CAAC,QAAA,EAAU;AACb,IAAA,OAAA,CAAQ,MAAM,+BAA+B,CAAA;AAC7C,IAAA,OAAA,CAAQ,MAAM,iCAAiC,CAAA;AAC/C,IAAA,OAAA,CAAQ,KAAK,CAAC,CAAA;AAAA,EAChB;AAEA,EAAA,aAAA,CAAc,QAAQ,CAAA,CAAE,KAAA,CAAM,CAAC,GAAA,KAAQ;AACrC,IAAA,OAAA,CAAQ,KAAA,CAAM,qBAAA,EAAuB,GAAA,CAAI,OAAO,CAAA;AAChD,IAAA,OAAA,CAAQ,KAAK,CAAC,CAAA;AAAA,EAChB,CAAC,CAAA;AACH","file":"inject-content.js","sourcesContent":["#!/usr/bin/env node\r\n/**\r\n * Post-build script to inject DCS content into Angular's index.html\r\n *\r\n * Usage:\r\n * npx dcs-inject dist/my-app\r\n * node node_modules/@duffcloudservices/cms-angular/dist/scripts/inject-content.js dist/my-app\r\n *\r\n * This script:\r\n * 1. Reads .dcs/content.yaml and .dcs/seo.yaml from the project root\r\n * 2. Injects them as __DCS_CONTENT__ and __DCS_SEO__ globals in index.html\r\n * 3. Writes the modified index.html back to the dist folder\r\n */\r\n\r\nimport fs from 'node:fs'\r\nimport path from 'node:path'\r\nimport { loadContentYaml, loadSeoYaml } from '@duffcloudservices/cms-core'\r\n\r\n/**\r\n * Inject DCS content into an Angular build's index.html\r\n *\r\n * @param distPath - Path to the Angular build output (e.g., 'dist/my-app')\r\n * @param options - Optional configuration\r\n */\r\nexport async function injectContent(\r\n distPath: string,\r\n options: {\r\n contentPath?: string\r\n seoPath?: string\r\n indexFile?: string\r\n } = {}\r\n): Promise<void> {\r\n const {\r\n contentPath = '.dcs/content.yaml',\r\n seoPath = '.dcs/seo.yaml',\r\n indexFile = 'index.html',\r\n } = options\r\n\r\n const indexPath = path.join(distPath, indexFile)\r\n\r\n // Check if index.html exists\r\n if (!fs.existsSync(indexPath)) {\r\n throw new Error(`index.html not found at ${indexPath}`)\r\n }\r\n\r\n // Try to load content.yaml\r\n let contentJson = 'undefined'\r\n try {\r\n const content = await loadContentYaml(contentPath)\r\n contentJson = JSON.stringify(content)\r\n console.log(`[dcs-inject] Loaded content from ${contentPath}`)\r\n } catch {\r\n console.log(`[dcs-inject] No content.yaml found at ${contentPath}`)\r\n }\r\n\r\n // Try to load seo.yaml\r\n let seoJson = 'undefined'\r\n try {\r\n const seo = await loadSeoYaml(seoPath)\r\n seoJson = JSON.stringify(seo)\r\n console.log(`[dcs-inject] Loaded SEO config from ${seoPath}`)\r\n } catch {\r\n console.log(`[dcs-inject] No seo.yaml found at ${seoPath}`)\r\n }\r\n\r\n // Read index.html\r\n let html = fs.readFileSync(indexPath, 'utf8')\r\n\r\n // Create injection script\r\n const injection = `\r\n<script>\r\n window.__DCS_CONTENT__ = ${contentJson};\r\n window.__DCS_SEO__ = ${seoJson};\r\n</script>\r\n`\r\n\r\n // Inject before </head>\r\n if (html.includes('</head>')) {\r\n html = html.replace('</head>', `${injection}</head>`)\r\n } else {\r\n // Fallback: inject at the start of <body>\r\n html = html.replace('<body', `${injection}<body`)\r\n }\r\n\r\n // Write modified index.html\r\n fs.writeFileSync(indexPath, html)\r\n console.log(`[dcs-inject] Content injected into ${indexPath}`)\r\n}\r\n\r\n// CLI entry point\r\nif (process.argv[1]?.includes('inject-content')) {\r\n const distPath = process.argv[2]\r\n if (!distPath) {\r\n console.error('Usage: dcs-inject <dist-path>')\r\n console.error('Example: dcs-inject dist/my-app')\r\n process.exit(1)\r\n }\r\n\r\n injectContent(distPath).catch((err) => {\r\n console.error('[dcs-inject] Error:', err.message)\r\n process.exit(1)\r\n })\r\n}\r\n"]}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@duffcloudservices/cms-angular",
3
+ "version": "0.1.0",
4
+ "description": "Angular services and build scripts for DCS CMS integration",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ },
11
+ "./scripts": {
12
+ "types": "./dist/scripts/index.d.ts",
13
+ "import": "./dist/scripts/index.js"
14
+ }
15
+ },
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "bin": {
19
+ "dcs-inject": "./dist/scripts/inject-content.js"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "dev": "tsup --watch",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "type-check": "tsc --noEmit",
30
+ "prepublishOnly": "pnpm run build"
31
+ },
32
+ "peerDependencies": {
33
+ "@angular/common": "^17.0.0 || ^18.0.0 || ^19.0.0",
34
+ "@angular/core": "^17.0.0 || ^18.0.0 || ^19.0.0",
35
+ "@angular/platform-browser": "^17.0.0 || ^18.0.0 || ^19.0.0"
36
+ },
37
+ "dependencies": {
38
+ "@duffcloudservices/cms-core": "workspace:*"
39
+ },
40
+ "devDependencies": {
41
+ "@angular/common": "^19.0.0",
42
+ "@angular/core": "^19.0.0",
43
+ "@angular/platform-browser": "^19.0.0",
44
+ "@types/node": "^20.11.0",
45
+ "rxjs": "^7.8.0",
46
+ "tsup": "^8.0.0",
47
+ "typescript": "~5.6.3",
48
+ "vitest": "^3.2.3"
49
+ },
50
+ "keywords": [
51
+ "angular",
52
+ "dcs",
53
+ "cms",
54
+ "content",
55
+ "seo"
56
+ ],
57
+ "author": "Duff Cloud Services",
58
+ "license": "MIT",
59
+ "repository": {
60
+ "type": "git",
61
+ "url": "https://github.com/duffn84/dcs-again",
62
+ "directory": "packages/cms-angular"
63
+ },
64
+ "publishConfig": {
65
+ "access": "public"
66
+ }
67
+ }