create-outsystems-astro 0.10.0 → 0.11.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.
@@ -3,6 +3,7 @@
3
3
  "module": "ESNext",
4
4
  "moduleResolution": "Bundler",
5
5
  "target": "ESNext",
6
+ "esModuleInterop": true,
6
7
  "declaration": true,
7
8
  "declarationMap": true,
8
9
  "outDir": "dist",
@@ -10,6 +11,6 @@
10
11
  "strict": true,
11
12
  "skipLibCheck": true
12
13
  },
13
- "include": ["html/**/*.ts"],
14
+ "include": ["html/**/*.ts", "twig/**/*.ts"],
14
15
  "exclude": ["node_modules", "dist"]
15
16
  }
@@ -0,0 +1,34 @@
1
+ import Twig from "twig";
2
+
3
+ const client_default =
4
+ (element: HTMLElement) =>
5
+ (
6
+ Component: unknown,
7
+ props: Record<string, unknown>,
8
+ { client }: { client: string },
9
+ ) => {
10
+ if (client !== "only" && !element.hasAttribute("ssr")) return;
11
+
12
+ let template: string;
13
+ if (typeof Component === "string") {
14
+ template = Component;
15
+ } else if (typeof Component === "function") {
16
+ template = (Component as (p: Record<string, unknown>) => string)({
17
+ ...props,
18
+ });
19
+ } else {
20
+ template = "";
21
+ }
22
+
23
+ const html = Twig.twig({ data: template }).render({ ...props });
24
+
25
+ element.innerHTML = html;
26
+
27
+ element.querySelectorAll("script").forEach((oldScript) => {
28
+ const newScript = document.createElement("script");
29
+ newScript.textContent = oldScript.textContent;
30
+ oldScript.parentNode?.replaceChild(newScript, oldScript);
31
+ });
32
+ };
33
+
34
+ export default client_default;
@@ -0,0 +1,185 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import type { AstroIntegration, AstroRenderer } from "astro";
6
+
7
+ const VIRTUAL_SERVER_ID = "virtual:islands/twig/server-with-filter";
8
+ const RESOLVED_VIRTUAL_SERVER_ID = `\0${VIRTUAL_SERVER_ID}`;
9
+
10
+ function getRenderer(filtered: boolean): AstroRenderer {
11
+ return {
12
+ clientEntrypoint: "islands-integrations/twig/client",
13
+ name: "islands/twig",
14
+ serverEntrypoint: filtered
15
+ ? VIRTUAL_SERVER_ID
16
+ : "islands-integrations/twig/server",
17
+ };
18
+ }
19
+
20
+ export const getContainerRenderer = (): AstroRenderer => getRenderer(false);
21
+
22
+ export interface Options {
23
+ exclude?: string[];
24
+ include?: string[];
25
+ namespaces?: Record<string, string>;
26
+ }
27
+
28
+ type Namespaces = Record<string, string>;
29
+
30
+ function normalizeNamespaces(
31
+ namespaces: Record<string, string> | undefined,
32
+ root: string,
33
+ ): Namespaces {
34
+ const normalized: Namespaces = {};
35
+ for (const [name, dir] of Object.entries(namespaces ?? {})) {
36
+ normalized[name.replace(/^@/, "")] = path.resolve(root, dir);
37
+ }
38
+ return normalized;
39
+ }
40
+
41
+ const NAMESPACE_PATH = /^@([^/]+)\/(.*)$/;
42
+
43
+ function resolveIncludePath(
44
+ includePath: string,
45
+ dir: string,
46
+ namespaces: Namespaces,
47
+ ): string {
48
+ const match = NAMESPACE_PATH.exec(includePath);
49
+ if (!match) return path.resolve(dir, includePath);
50
+
51
+ const [, name, rest] = match;
52
+ const base = namespaces[name];
53
+ if (!base) {
54
+ throw new Error(
55
+ `Twig namespace "@${name}" is not configured. Add it to the \`namespaces\` option of the twig() integration.`,
56
+ );
57
+ }
58
+ return path.resolve(base, rest);
59
+ }
60
+
61
+ function twigFilterPlugin(include?: string[], exclude?: string[]) {
62
+ return {
63
+ load(id: string) {
64
+ if (id !== RESOLVED_VIRTUAL_SERVER_ID) return;
65
+ return [
66
+ `import { createRenderer } from "islands-integrations/twig/server";`,
67
+ `export default createRenderer(${JSON.stringify(include)}, ${JSON.stringify(exclude)});`,
68
+ ].join("\n");
69
+ },
70
+ name: "islands/twig/filter",
71
+ resolveId(id: string) {
72
+ if (id === VIRTUAL_SERVER_ID) return RESOLVED_VIRTUAL_SERVER_ID;
73
+ },
74
+ };
75
+ }
76
+
77
+ const INCLUDE_TAG = /\{%-?\s*include\s+(['"])([^'"]+)\1([^%]*?)-?%\}/g;
78
+
79
+ interface IncludeModifiers {
80
+ ignoreMissing: boolean;
81
+ only: boolean;
82
+ withExpr?: string;
83
+ }
84
+
85
+ function applyContextScope(
86
+ body: string,
87
+ { only, withExpr }: IncludeModifiers,
88
+ ): string {
89
+ if (!withExpr && !only) return body;
90
+ const vars = withExpr ?? "{}";
91
+ const onlyFlag = only ? " only" : "";
92
+ return `{% with ${vars}${onlyFlag} %}${body}{% endwith %}`;
93
+ }
94
+
95
+ function inlineIncludes(
96
+ filepath: string,
97
+ ancestors: string[],
98
+ namespaces: Namespaces,
99
+ onInclude?: (target: string) => void,
100
+ ): string {
101
+ const resolved = path.resolve(filepath);
102
+ if (ancestors.includes(resolved)) {
103
+ throw new Error(
104
+ `Twig include cycle detected: ${[...ancestors, resolved].join(" -> ")}`,
105
+ );
106
+ }
107
+
108
+ const source = fs.readFileSync(resolved, "utf-8");
109
+ const dir = path.dirname(resolved);
110
+ const trail = [...ancestors, resolved];
111
+
112
+ return source.replace(
113
+ INCLUDE_TAG,
114
+ (_match, _quote, includePath, modifiers) => {
115
+ const parsed = parseModifiers(modifiers);
116
+ const target = resolveIncludePath(includePath, dir, namespaces);
117
+ if (!fs.existsSync(target)) {
118
+ if (parsed.ignoreMissing) return "";
119
+ throw new Error(
120
+ `Twig include "${includePath}" not found (referenced from ${resolved}).`,
121
+ );
122
+ }
123
+ onInclude?.(target);
124
+ const body = inlineIncludes(target, trail, namespaces, onInclude);
125
+ return applyContextScope(body, parsed);
126
+ },
127
+ );
128
+ }
129
+
130
+ function parseModifiers(modifiers: string): IncludeModifiers {
131
+ const ignoreMissing = /\bignore\s+missing\b/.test(modifiers);
132
+ let rest = modifiers.replace(/\bignore\s+missing\b/, " ");
133
+
134
+ const only = /\bonly\s*$/.test(rest);
135
+ rest = rest.replace(/\bonly\s*$/, " ");
136
+
137
+ const withMatch = /\bwith\b([\s\S]*)$/.exec(rest);
138
+ const withExpr = withMatch ? withMatch[1].trim() : undefined;
139
+
140
+ return { ignoreMissing, only, withExpr: withExpr || undefined };
141
+ }
142
+
143
+ function twigLoaderPlugin(namespaces: Namespaces) {
144
+ return {
145
+ enforce: "pre" as const,
146
+ load(this: { addWatchFile?: (id: string) => void }, id: string) {
147
+ const filepath = id.split("?")[0];
148
+ if (!filepath.endsWith(".twig")) return;
149
+ const source = inlineIncludes(filepath, [], namespaces, (target) =>
150
+ this.addWatchFile?.(target),
151
+ );
152
+ return {
153
+ code: `export default ${JSON.stringify(source)};`,
154
+ map: null,
155
+ };
156
+ },
157
+ name: "islands/twig/loader",
158
+ };
159
+ }
160
+
161
+ export default function (options: Options = {}): AstroIntegration {
162
+ const { exclude, include, namespaces } = options;
163
+ const filtered = !!(include?.length || exclude?.length);
164
+
165
+ return {
166
+ hooks: {
167
+ "astro:config:setup": ({ addRenderer, config, updateConfig }) => {
168
+ addRenderer(getRenderer(filtered));
169
+ const resolvedNamespaces = normalizeNamespaces(
170
+ namespaces,
171
+ fileURLToPath(config.root),
172
+ );
173
+ const loader = twigLoaderPlugin(resolvedNamespaces);
174
+ updateConfig({
175
+ vite: {
176
+ plugins: filtered
177
+ ? [loader, twigFilterPlugin(include, exclude)]
178
+ : [loader],
179
+ },
180
+ });
181
+ },
182
+ },
183
+ name: "islands/twig",
184
+ };
185
+ }
@@ -0,0 +1,54 @@
1
+ import type {
2
+ AstroComponentMetadata,
3
+ NamedSSRLoadedRendererValue,
4
+ } from "astro";
5
+
6
+ export function createRenderer(
7
+ include?: string[],
8
+ exclude?: string[],
9
+ ): NamedSSRLoadedRendererValue {
10
+ return {
11
+ check: async (
12
+ Component: unknown,
13
+ _props: unknown,
14
+ _slots: unknown,
15
+ metadata?: AstroComponentMetadata,
16
+ ) => {
17
+ const url = metadata?.componentUrl;
18
+ if (url) {
19
+ if (include && !matchesPatterns(url, include)) return false;
20
+ if (exclude && matchesPatterns(url, exclude)) return false;
21
+ }
22
+ return checkComponent(Component);
23
+ },
24
+ name: "islands/twig",
25
+ renderToStaticMarkup,
26
+ supportsAstroStaticSlot: false,
27
+ };
28
+ }
29
+
30
+ async function checkComponent(Component: unknown): Promise<boolean> {
31
+ if (typeof Component === "string") return true;
32
+ if (typeof Component === "function") {
33
+ try {
34
+ const result = (Component as (p: Record<string, unknown>) => unknown)({});
35
+ return typeof result === "string";
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+ return false;
41
+ }
42
+
43
+ function matchesPatterns(url: string, patterns: string[]): boolean {
44
+ return patterns.some((pattern) => {
45
+ const prefix = pattern.replace(/\/?\*+$/, "");
46
+ return url.includes(prefix);
47
+ });
48
+ }
49
+
50
+ async function renderToStaticMarkup(): Promise<{ html: string }> {
51
+ return { html: "" };
52
+ }
53
+
54
+ export default createRenderer();
@@ -104,6 +104,13 @@ __metadata:
104
104
  languageName: node
105
105
  linkType: hard
106
106
 
107
+ "@babel/runtime@npm:^7.8.4":
108
+ version: 7.29.2
109
+ resolution: "@babel/runtime@npm:7.29.2"
110
+ checksum: 10c0/30b80a0140d16467792e1bbeb06f655b0dab70407da38dfac7fedae9c859f9ae9d846ef14ad77bd3814c064295fe9b1bc551f1541ea14646ae9f22b71a8bc17a
111
+ languageName: node
112
+ linkType: hard
113
+
107
114
  "@babel/types@npm:^7.29.0":
108
115
  version: 7.29.0
109
116
  resolution: "@babel/types@npm:7.29.0"
@@ -1121,6 +1128,22 @@ __metadata:
1121
1128
  languageName: node
1122
1129
  linkType: hard
1123
1130
 
1131
+ "@types/node@npm:^25.9.1":
1132
+ version: 25.9.1
1133
+ resolution: "@types/node@npm:25.9.1"
1134
+ dependencies:
1135
+ undici-types: "npm:>=7.24.0 <7.24.7"
1136
+ checksum: 10c0/9a04682842bebbcf21a1779dfeab9aa733d7bd7bbc0a0edb641ab3a9a3d43eac543225acf669c334f458f1956443ebc072bc3c72840c543b8b356cab5c82d456
1137
+ languageName: node
1138
+ linkType: hard
1139
+
1140
+ "@types/twig@npm:^1.12.17":
1141
+ version: 1.12.17
1142
+ resolution: "@types/twig@npm:1.12.17"
1143
+ checksum: 10c0/a54d62abb979ecc81707c7da9ffe641cdbb12c5e73d782c5fbed69c41123d7c2142c8c79fe7a92295bc13eea7abbafd68863e5f0eb3f54094a26b179e3a2d653
1144
+ languageName: node
1145
+ linkType: hard
1146
+
1124
1147
  "@types/unist@npm:*, @types/unist@npm:^3.0.0":
1125
1148
  version: 3.0.3
1126
1149
  resolution: "@types/unist@npm:3.0.3"
@@ -2802,6 +2825,13 @@ __metadata:
2802
2825
  languageName: node
2803
2826
  linkType: hard
2804
2827
 
2828
+ "foreachasync@npm:^3.0.0":
2829
+ version: 3.0.0
2830
+ resolution: "foreachasync@npm:3.0.0"
2831
+ checksum: 10c0/8ad877008da351fa78939e850c6014e94b8b9c6de3d12751b2b906ef96f8c80945310d998b2a704854e126c508237dc9951f6900685ccc42c93db15b09a0c4b3
2832
+ languageName: node
2833
+ linkType: hard
2834
+
2805
2835
  "fsevents@npm:~2.3.2, fsevents@npm:~2.3.3":
2806
2836
  version: 2.3.3
2807
2837
  resolution: "fsevents@npm:2.3.3"
@@ -3570,6 +3600,8 @@ __metadata:
3570
3600
  dependencies:
3571
3601
  "@eslint/compat": "npm:^2.1.0"
3572
3602
  "@eslint/js": "npm:^9.39.4"
3603
+ "@types/node": "npm:^25.9.1"
3604
+ "@types/twig": "npm:^1.12.17"
3573
3605
  astro: "npm:^6.3.7"
3574
3606
  better-npm-audit: "npm:^3.11.0"
3575
3607
  eslint: "npm:^9.39.4"
@@ -3579,6 +3611,7 @@ __metadata:
3579
3611
  eslint-plugin-perfectionist: "npm:^5.9.0"
3580
3612
  globals: "npm:^17.6.0"
3581
3613
  prettier: "npm:^3.8.3"
3614
+ twig: "npm:^3.0.0"
3582
3615
  typescript: "npm:^5.9.3"
3583
3616
  typescript-eslint: "npm:^8.59.4"
3584
3617
  languageName: unknown
@@ -3669,6 +3702,13 @@ __metadata:
3669
3702
  languageName: node
3670
3703
  linkType: hard
3671
3704
 
3705
+ "locutus@npm:^3.0.9":
3706
+ version: 3.0.36
3707
+ resolution: "locutus@npm:3.0.36"
3708
+ checksum: 10c0/54a04ecddeaeadd924f2362c31d888e333bb717bd48dd9d40ac6ddccb352a924791223e09b782d03196808c1404a1f74210a388ce4475c3650dcb8d7b0a75730
3709
+ languageName: node
3710
+ linkType: hard
3711
+
3672
3712
  "lodash.get@npm:^4.4.2":
3673
3713
  version: 4.4.2
3674
3714
  resolution: "lodash.get@npm:4.4.2"
@@ -4271,7 +4311,7 @@ __metadata:
4271
4311
  languageName: node
4272
4312
  linkType: hard
4273
4313
 
4274
- "minimatch@npm:^10.2.2":
4314
+ "minimatch@npm:^10, minimatch@npm:^10.2.2":
4275
4315
  version: 10.2.5
4276
4316
  resolution: "minimatch@npm:10.2.5"
4277
4317
  dependencies:
@@ -5713,6 +5753,20 @@ __metadata:
5713
5753
  languageName: node
5714
5754
  linkType: hard
5715
5755
 
5756
+ "twig@npm:^3.0.0":
5757
+ version: 3.0.0
5758
+ resolution: "twig@npm:3.0.0"
5759
+ dependencies:
5760
+ "@babel/runtime": "npm:^7.8.4"
5761
+ locutus: "npm:^3.0.9"
5762
+ minimatch: "npm:^10"
5763
+ walk: "npm:2.3.x"
5764
+ bin:
5765
+ twigjs: bin/twigjs
5766
+ checksum: 10c0/a727b665d34c9b7db9cf5fa99072472ef1dd9291d6f29a4b1de88ca95493d16c667f47d7e87f4aaea05275ea184f6550dc3c32cb42026b819d154690f48cff76
5767
+ languageName: node
5768
+ linkType: hard
5769
+
5716
5770
  "type-check@npm:^0.4.0, type-check@npm:~0.4.0":
5717
5771
  version: 0.4.0
5718
5772
  resolution: "type-check@npm:0.4.0"
@@ -5843,6 +5897,13 @@ __metadata:
5843
5897
  languageName: node
5844
5898
  linkType: hard
5845
5899
 
5900
+ "undici-types@npm:>=7.24.0 <7.24.7":
5901
+ version: 7.24.6
5902
+ resolution: "undici-types@npm:7.24.6"
5903
+ checksum: 10c0/d9cd8befb643ac904615c280a095ba4240531f6bb4a5e75a22a7483630ca8d3f1016d2ab6ace6ceda1f63b3a2db2fe037fafe121d6917a0187573aa548ff78ca
5904
+ languageName: node
5905
+ linkType: hard
5906
+
5846
5907
  "undici@npm:^6.25.0":
5847
5908
  version: 6.25.0
5848
5909
  resolution: "undici@npm:6.25.0"
@@ -6151,6 +6212,15 @@ __metadata:
6151
6212
  languageName: node
6152
6213
  linkType: hard
6153
6214
 
6215
+ "walk@npm:2.3.x":
6216
+ version: 2.3.15
6217
+ resolution: "walk@npm:2.3.15"
6218
+ dependencies:
6219
+ foreachasync: "npm:^3.0.0"
6220
+ checksum: 10c0/c390221ff6fdb8e95f9b03d90fa9980edc2c01ce9efac44c0ade2036ee2823bf2bc9abfae388bdf64ab59e9262610e7cf6634ad54acac018e62621b85edad2cf
6221
+ languageName: node
6222
+ linkType: hard
6223
+
6154
6224
  "web-namespaces@npm:^2.0.0":
6155
6225
  version: 2.0.1
6156
6226
  resolution: "web-namespaces@npm:2.0.1"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-outsystems-astro",
3
3
  "type": "module",
4
- "version": "0.10.0",
4
+ "version": "0.11.0",
5
5
  "description": "Create an OutSystems Astro Island project to import as a component into your OutSystems application",
6
6
  "repository": {
7
7
  "type": "git",
@@ -11,6 +11,7 @@
11
11
  - React - Documentation available at https://docs.astro.build/en/guides/integrations-guide/react/
12
12
  - SolidJS - Documentation available at https://docs.astro.build/en/guides/integrations-guide/solid-js/
13
13
  - Svelte - Documentation availabe at https://docs.astro.build/en/guides/integrations-guide/svelte/
14
+ - Twig - Documentation available at https://hs2323.github.io/create-outsystems-astro/guides/integrations/twig/
14
15
  - Vue - Documentation available at https://docs.astro.build/en/guides/integrations-guide/vue/
15
16
  - Prefer to use TypeScript when possible.
16
17
 
@@ -44,6 +45,7 @@ In OutSystems Developer Cloud, the Islands library is available at https://www.o
44
45
  - React: `client:only="react"`
45
46
  - SolidJS: `client:only="solid-js"`
46
47
  - Svelte: `client:only="svelte"`
48
+ - Twig: `client:load`
47
49
  - Vue: `client:only="vue"`
48
50
 
49
51
  ### Components
@@ -94,6 +96,10 @@ Angular does not support the use of slots. Any use of slots with Angular should
94
96
 
95
97
  The HTML integration does not support the use of slots. Any use of slots with the HTML integration should be discouraged.
96
98
 
99
+ ##### Twig
100
+
101
+ The Twig integration does not support the use of slots. Any use of slots with the Twig integration should be discouraged. Pass content in as props and render it with `{{ }}` instead.
102
+
97
103
  ##### Preact
98
104
 
99
105
  - In Preact, slots are handled as props. The default slot is the `children` prop. A named slot will have the name of its slot as the parameter. For example, a slot with the following:
@@ -406,6 +412,47 @@ export default function Counter({}) {
406
412
  <div>{$nanoStoreValue}</div>
407
413
  ```
408
414
 
415
+ #### Twig
416
+
417
+ Like the HTML integration, Twig does not use a Nano Stores binding library. Set up a compatible store on `window.Stores` inside the component's `<script>` tag and subscribe to it directly.
418
+
419
+ ```html
420
+ <div class="nanostore-value"></div>
421
+ <script>
422
+ const nanostoreEl = container.querySelector(".nanostore-value");
423
+
424
+ if (!window.Stores) window.Stores = {};
425
+ if (!window.Stores["twigStore"]) {
426
+ let _value = "Test Value";
427
+ const _subs = [];
428
+ window.Stores["twigStore"] = {
429
+ get: function () {
430
+ return _value;
431
+ },
432
+ set: function (v) {
433
+ _value = v;
434
+ _subs.forEach(function (fn) {
435
+ fn(v);
436
+ });
437
+ },
438
+ subscribe: function (fn) {
439
+ fn(_value);
440
+ _subs.push(fn);
441
+ return function () {
442
+ _subs.splice(_subs.indexOf(fn), 1);
443
+ };
444
+ },
445
+ };
446
+ }
447
+
448
+ const store = window.Stores["twigStore"];
449
+ nanostoreEl.textContent = store.get();
450
+ store.subscribe(function (value) {
451
+ nanostoreEl.textContent = value;
452
+ });
453
+ </script>
454
+ ```
455
+
409
456
  #### Vue
410
457
 
411
458
  ```vue
@@ -7,6 +7,7 @@ import svelte from "@astrojs/svelte";
7
7
  import vue from "@astrojs/vue";
8
8
  import { defineConfig } from "astro/config";
9
9
  import html from "islands-integrations/html";
10
+ import twig from "islands-integrations/twig";
10
11
 
11
12
  // https://astro.build/config
12
13
  export default defineConfig({
@@ -39,6 +40,9 @@ export default defineConfig({
39
40
  svelte({
40
41
  include: ["src/framework/svelte/*"],
41
42
  }),
43
+ twig({
44
+ include: ["src/framework/twig/*"],
45
+ }),
42
46
  vue({
43
47
  include: ["src/framework/vue/*"],
44
48
  }),