create-outsystems-astro 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 hs2323
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # OutSystems Astro Islands
2
+ Generates [Astro Islands](https://docs.astro.build/en/concepts/islands/) for use in OutSystems that can create self contained interactive code elements from different frameworks. It allows an extension of the front-end with these dynamic libraries.
3
+
4
+ ## When to use this library
5
+ - Custom interactive elements that would not be difficult/not possible to build directly in OutSystems.
6
+ - Wrappers around interactive elements built in other front-end frameworks.
7
+ - Direct migration of traditional code.
8
+
9
+ ## When NOT to use this library
10
+ - You will most likely not need to use this library for most of the front-end development. This is similar in use to the custom code development in for the back-end in [O11](https://success.outsystems.com/documentation/11/integration_with_external_systems/extend_logic_with_your_own_code/) and [ODC](https://success.outsystems.com/documentation/outsystems_developer_cloud/building_apps/extend_your_apps_with_custom_code/).
11
+ - If the functionality is easily buidable in Service Studio.
12
+ - Loading performance of component must be instant. The Astro Island will load after the page/screen has loaded since the initializer and tag will be loaded after.
13
+
14
+ ## Current supported frameworks
15
+ - [React](https://docs.astro.build/en/guides/integrations-guide/react/).
16
+
17
+ ## Getting started
18
+ Run the Create OutSystems Astro generator:
19
+ ```bash
20
+ npx create-outsystems-astro
21
+ ```
22
+ This will create the generated files as well as an example component.
23
+
24
+ ## ๐Ÿš€ Project Structure
25
+
26
+ ```text
27
+ /
28
+ โ”œโ”€โ”€ public/
29
+ โ”œโ”€โ”€ src/
30
+ โ”‚ โ””โ”€โ”€ components/
31
+ โ”‚ โ””โ”€โ”€ Counter.tsx
32
+ โ”‚ โ””โ”€โ”€ images/
33
+ โ”‚ โ””โ”€โ”€ image.png
34
+ โ”‚ โ””โ”€โ”€ pages/
35
+ โ”‚ โ””โ”€โ”€ counter.astro
36
+ โ”‚ โ””โ”€โ”€ styles/
37
+ โ”‚ โ””โ”€โ”€ index.css
38
+ โ””โ”€โ”€ package.json
39
+ ```
40
+
41
+ ### Pages
42
+ Each page inside of the pages file should represent an Island that will be imported into OutSystems.
43
+
44
+ ### Components
45
+ The location of the component code.
46
+
47
+ ### Images
48
+ Any image assets.
49
+
50
+ ### Styles
51
+ Stylesheets that may apply to the component.
52
+
53
+ ## ๐Ÿงž Commands
54
+
55
+ All commands are run from the root of the project, from a terminal:
56
+
57
+ | Command | Action |
58
+ | :------------------------ | :----------------------------------------------- |
59
+ | `npm install` | Installs dependencies |
60
+ | `npm run dev` | Starts local dev server at `localhost:4321` |
61
+ | `npm run build` | Build distribution to `./dist/` |
62
+ | `npm run output` | Build OutSystems production site to `./output/` |
63
+ | `npm run preview` | Preview build locally, before creating output |
64
+ | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
65
+ | `npm run astro -- --help` | Get help using the Astro CLI |
66
+
67
+
68
+ ## Converting to OutSystems
69
+
70
+ Once development is complete, run:
71
+ ```bash
72
+ npm run output
73
+ ```
74
+
75
+ This will create a set of files that will then need to be coverted to OutSystems components.
package/bin/cli.js ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import prompts from "prompts";
6
+ import { execSync } from "child_process";
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ async function main() {
12
+ console.log("๐Ÿš€ Welcome to create-outsystems-astro!");
13
+
14
+ // Ask for project name
15
+ const response = await prompts({
16
+ type: "text",
17
+ name: "projectName",
18
+ message: "What should we name your project?",
19
+ initial: "outsystems-astro-app"
20
+ });
21
+
22
+ const targetDir = path.resolve(process.cwd(), response.projectName);
23
+ const templateDir = path.join(__dirname, "..", "template");
24
+
25
+ // Copy files
26
+ console.log("๐Ÿ“ฆ Copying template...");
27
+ copyDir(templateDir, targetDir);
28
+
29
+ const readmeSrc = path.resolve(__dirname, "../README.md");
30
+ const readmeDest = path.join(targetDir, "README.md");
31
+
32
+ if (fs.existsSync(readmeSrc)) {
33
+ fs.copyFileSync(readmeSrc, readmeDest);
34
+ }
35
+
36
+ // Install dependencies
37
+ console.log("๐Ÿ“ฆ Installing dependencies...");
38
+ try {
39
+ execSync("npm install", { cwd: targetDir, stdio: "inherit" });
40
+ } catch {
41
+ console.warn("โš ๏ธ Failed to automatically install dependencies.");
42
+ }
43
+
44
+ console.log(`
45
+ โœ… All done!
46
+
47
+ Next steps:
48
+ cd ${response.projectName}
49
+ npm run dev
50
+ `);
51
+ }
52
+
53
+ // Simple recursive copy
54
+ function copyDir(src, dest) {
55
+ fs.mkdirSync(dest, { recursive: true });
56
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
57
+ const srcPath = path.join(src, entry.name);
58
+ const destPath = path.join(dest, entry.name);
59
+
60
+ if (entry.isDirectory()) {
61
+ copyDir(srcPath, destPath);
62
+ } else {
63
+ fs.copyFileSync(srcPath, destPath);
64
+ }
65
+ }
66
+ }
67
+
68
+ main().catch(err => {
69
+ console.error(err);
70
+ process.exit(1);
71
+ });
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "create-outsystems-astro",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "description": "Create an OutSystems Astro Island project to import as a component into your OutSystems application",
6
+ "bin": {
7
+ "create-outsystems-astro": "bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "template"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "keywords": [
17
+ "astro",
18
+ "outsystems",
19
+ "starter",
20
+ "create",
21
+ "template"
22
+ ],
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "prompts": "^2.4.2"
26
+ }
27
+ }
@@ -0,0 +1,2 @@
1
+ # Path in OutSystems to image locations.
2
+ ASSET_PATH=
@@ -0,0 +1,29 @@
1
+ // @ts-check
2
+ import react from '@astrojs/react';
3
+ import { defineConfig } from 'astro/config';
4
+
5
+
6
+ // https://astro.build/config
7
+ export default defineConfig({
8
+ integrations: [react()],
9
+ build: {
10
+ inlineStylesheets: 'always'
11
+ },
12
+ vite: {
13
+ build: {
14
+ rollupOptions: {
15
+ output: {
16
+ manualChunks: (id) => {
17
+ if (id.includes('node_modules') || id.includes('src')) {
18
+ return 'app.js';
19
+ }
20
+ return 'app.js';
21
+ },
22
+ entryFileNames: `[name]_[hash].js`,
23
+ chunkFileNames: `[name]_[hash].js`,
24
+ assetFileNames: `assets/[name]_[hash].[ext]`,
25
+ },
26
+ },
27
+ },
28
+ },
29
+ });
@@ -0,0 +1,244 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import beautify from "js-beautify";
4
+ import dotenv from 'dotenv';
5
+ dotenv.config();
6
+
7
+ const { html: beautifyHtml } = beautify;
8
+
9
+ /**
10
+ * Extracts all <astro-island>...</astro-island> blocks from HTML.
11
+ */
12
+ function keepAstroIslands(html: string): string {
13
+ const astroIslandRegex = /<astro-island\b[^>]*>[\s\S]*?<\/astro-island>/gi;
14
+ const islands = html.match(astroIslandRegex) || [];
15
+ return islands.join("\n");
16
+ }
17
+
18
+ function formatAstroIslandAttributes(html: string): string {
19
+ // For each <astro-island ...>, put attributes on new lines
20
+ return html.replace(
21
+ /<astro-island\b([^>]*)>/gi,
22
+ (match, attrs) => {
23
+
24
+ // ๐Ÿ”ฅ Convert HTML-encoded quotes to actual quotes
25
+ const decoded = attrs.replace(/&quot;/g, '""');
26
+
27
+ // Split attributes by whitespace
28
+ const formattedAttrs = decoded
29
+ .trim()
30
+ .split(/\s+/)
31
+ .filter(Boolean)
32
+ .map(attr => ` ${attr}`) // indent each attr
33
+ .join("\n");
34
+
35
+ return `<astro-island\n${formattedAttrs}>`;
36
+ }
37
+ );
38
+ }
39
+
40
+ /**
41
+ * Recursively collects files with a given extension in a directory.
42
+ */
43
+ function getAllFilesWithExtension(dir: string, ext: string): string[] {
44
+ let results: string[] = [];
45
+
46
+ for (const file of fs.readdirSync(dir)) {
47
+ const filePath = path.join(dir, file);
48
+ const stat = fs.statSync(filePath);
49
+
50
+ if (stat.isDirectory()) {
51
+ results = results.concat(getAllFilesWithExtension(filePath, ext));
52
+ } else if (filePath.endsWith(ext)) {
53
+ results.push(filePath);
54
+ }
55
+ }
56
+
57
+ return results;
58
+ }
59
+
60
+ /**
61
+ * Ensures a directory exists before writing or copying.
62
+ */
63
+ function ensureDirExists(dir: string) {
64
+ if (!fs.existsSync(dir)) {
65
+ fs.mkdirSync(dir, { recursive: true });
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Recursively deletes a directory.
71
+ */
72
+ function clearDirectory(dir: string) {
73
+ if (!fs.existsSync(dir)) return;
74
+
75
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
76
+ const entryPath = path.join(dir, entry.name);
77
+ if (entry.isDirectory()) {
78
+ clearDirectory(entryPath);
79
+ fs.rmdirSync(entryPath);
80
+ } else {
81
+ fs.unlinkSync(entryPath);
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Recursively copies a directory and its contents.
88
+ */
89
+ function copyDirectory(src: string, dest: string) {
90
+ if (!fs.existsSync(src)) {
91
+ console.warn(`โš ๏ธ Source directory not found: ${src}`);
92
+ return;
93
+ }
94
+
95
+ ensureDirExists(dest);
96
+
97
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
98
+ const srcPath = path.join(src, entry.name);
99
+ const destPath = path.join(dest, entry.name);
100
+
101
+ if (entry.isDirectory()) {
102
+ copyDirectory(srcPath, destPath);
103
+ } else {
104
+ fs.copyFileSync(srcPath, destPath);
105
+ }
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Converts dist/foo/index.html โ†’ output/foo.html
111
+ * Keeps dist/index.html as output/index.html
112
+ */
113
+ function getFlattenedHtmlOutputPath(inputFile: string, inputDir: string, outputDir: string): string {
114
+ const relative = path.relative(inputDir, inputFile);
115
+ const parts = relative.split(path.sep);
116
+
117
+ if (parts.length > 1 && parts[parts.length - 1] === "index.html") {
118
+ const folderName = parts[parts.length - 2];
119
+ return path.join(outputDir, `${folderName}.html`);
120
+ }
121
+
122
+ return path.join(outputDir, relative);
123
+ }
124
+
125
+ /**
126
+ * Processes and flattens HTML files (expanded output)
127
+ */
128
+ function processAllHTML(inputDir: string, outputDir: string): void {
129
+ const htmlFiles = getAllFilesWithExtension(inputDir, ".html");
130
+ if (htmlFiles.length === 0) {
131
+ console.warn(`โš ๏ธ No HTML files found in ${inputDir}`);
132
+ return;
133
+ }
134
+
135
+ for (const inputFile of htmlFiles) {
136
+ const outputFile = getFlattenedHtmlOutputPath(inputFile, inputDir, outputDir);
137
+ const html = fs.readFileSync(inputFile, "utf-8");
138
+
139
+ // Filter to only keep astro-islands
140
+ const filtered = keepAstroIslands(html);
141
+
142
+ // Beautify / expand the HTML before writing
143
+ const beautified = beautifyHtml(filtered, {
144
+ indent_size: 2,
145
+ preserve_newlines: true,
146
+ max_preserve_newlines: 1,
147
+ wrap_line_length: 120,
148
+ unformatted: ["code", "pre", "em", "strong"],
149
+ });
150
+
151
+ const prettyHtml = formatAstroIslandAttributes(beautified);
152
+
153
+ ensureDirExists(path.dirname(outputFile));
154
+ fs.writeFileSync(outputFile, prettyHtml, "utf-8");
155
+
156
+ console.log(`โœ… Processed HTML: ${path.relative(inputDir, inputFile)} โ†’ ${path.relative(outputDir, outputFile)}`);
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Copies all .js files from dist/ to output/
162
+ */
163
+ function copyAllJSFiles(inputDir: string, outputDir: string): void {
164
+ const jsFiles = getAllFilesWithExtension(inputDir, ".js");
165
+ if (jsFiles.length === 0) {
166
+ console.warn(`โš ๏ธ No JavaScript files found in ${inputDir}`);
167
+ return;
168
+ }
169
+
170
+ for (const inputFile of jsFiles) {
171
+ const relativePath = path.relative(inputDir, inputFile);
172
+ const outputFile = path.join(outputDir, relativePath);
173
+
174
+ ensureDirExists(path.dirname(outputFile));
175
+ fs.copyFileSync(inputFile, outputFile);
176
+
177
+ console.log(`๐Ÿ“œ Copied JS: ${relativePath}`);
178
+ }
179
+ }
180
+
181
+ function prefixAssetPathsInFile(filePath: string) {
182
+ const ASSET_PATH = process.env.ASSET_PATH || "";
183
+ let content = fs.readFileSync(filePath, "utf-8");
184
+
185
+ // Replace "/assets/..." inside string literals with prefixed path
186
+ // Match quotes + /assets/ at start of string
187
+ content = content.replace(/(["'`])\/assets\//g, `$1/${ASSET_PATH}/`);
188
+
189
+ fs.writeFileSync(filePath, content, "utf-8");
190
+ console.log(`Prefixed asset paths in ${filePath}`);
191
+ }
192
+
193
+ function processAllJsFiles(dir: string) {
194
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
195
+
196
+ for (const entry of entries) {
197
+ const fullPath = path.join(dir, entry.name);
198
+ if (entry.isDirectory()) {
199
+ processAllJsFiles(fullPath);
200
+ } else if (entry.name.endsWith(".js")) {
201
+ prefixAssetPathsInFile(fullPath);
202
+ }
203
+ }
204
+ }
205
+
206
+
207
+ /**
208
+ * Main function
209
+ */
210
+ function processOutput() {
211
+ const inputDir = path.resolve("dist");
212
+ const outputDir = path.resolve("output");
213
+ const assetsSrc = path.join(inputDir, "assets");
214
+ const assetsDest = path.join(outputDir, "assets");
215
+
216
+ if (!fs.existsSync(inputDir)) {
217
+ console.error(`โŒ Input directory not found: ${inputDir}`);
218
+ process.exit(1);
219
+ }
220
+
221
+ // ๐Ÿงน Clear output directory before writing
222
+ if (fs.existsSync(outputDir)) {
223
+ console.log("๐Ÿงน Clearing output directory...");
224
+ clearDirectory(outputDir);
225
+ } else {
226
+ ensureDirExists(outputDir);
227
+ }
228
+
229
+ console.log("โš™๏ธ Processing HTML files...");
230
+ processAllHTML(inputDir, outputDir);
231
+
232
+ console.log("๐Ÿ“ฆ Copying assets...");
233
+ copyDirectory(assetsSrc, assetsDest);
234
+
235
+ console.log("๐Ÿ“œ Copying JavaScript files...");
236
+ copyAllJSFiles(inputDir, outputDir);
237
+
238
+ console.log("๐Ÿ“œ Processing asset paths...");
239
+ processAllJsFiles("output");
240
+
241
+ console.log("โœ… Done!");
242
+ }
243
+
244
+ processOutput();