diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..275c4355 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,291 @@ +# Thanks for taking the time to contribute to Typewriter! + +This doc provides a walkthrough of developing on, and contributing to, Typewriter. + +Please see our [issue template](ISSUE_TEMPLATE.md) for issues specifically. + +## Issues, Bugfixes and New Language Support + +Have an idea for improving Typewriter? [Submit an issue first](https://github.com/segmentio/typewriter/issues/new), and we'll be happy to help you scope it out and make sure it is a good fit for Typewriter. + +## Developing on Typewriter + +Typewriter is written using [OCLIF](https://oclif.io). + +### Build and run locally + +```sh +# Install dependencies +$ yarn +# Test your Typewriter installation by regenerating Typewriter's typewriter client. +$ yarn build +# Develop and test using OCLIFs dev runner to test any of your changes without transpiling +$ ./bin/dev build -m prod -u +``` + +### Running Tests + +```sh +$ yarn test +``` + +### Deploying + +You can deploy a new version to [`npm`](https://www.npmjs.com/package/typewriter) by running: + +``` +$ yarn release +``` + +### Adding a New Language Target + +> Before working towards adding a new language target, please [open an issue on GitHub](https://github.com/segmentio/typewriter/issues/new) that walks through your proposal for the new language support. See the [issue template](ISSUE_TEMPLATE.md) for details. + +All languages are just objects that implement the [`LanguageGenerator`](src/languages/types.ts) interface. We have a [quick an easy way](#using-quicktype) to use [Handlebars](http://handlebarsjs.com/) and [Quicktype](quicktype.io) which should cover most of the scenarios but you can always write your own [renderer](#using-a-custom-renderer). + +#### Using QuickType + +We have to start by creating the Quicktype required classes: a `Renderer` and a `TargetLanguage` + +We will start with the renderer. The `Renderer` is the class in Quicktype that outputs text to the files. We can customize the quicktype output here, and if you need to do more complex outputs you can check [Customize Quicktype Output](#customizing-quicktypes-output). For now we will stick to the basics and use the default handlebars renderer. Most scenarios will only need this. + +To create a renderer extend the appropiate renderer class of your Language. For Swift for example that is `SwiftRenderer`. We will add a `constructor` with some custom parameters we need and override a few functions. This is pretty much boilerplate code: + +```ts +import { + Name, + RenderContext, + SwiftRenderer, + SwiftTargetLanguage, + TargetLanguage, + Type, +} from 'quicktype-core'; +import { OptionValues } from 'quicktype-core/dist/RendererOptions'; +import { camelCase } from 'quicktype-core/dist/support/Strings'; +import { emitMultiline, executeRenderPlan, makeNameForTopLevelWithPrefixAndSuffix } from './quicktype-utils'; + +// We extend the Quicktype renderer for the language we will output, SwiftRenderer here for Swift +class TypewriterSwiftRenderer extends SwiftRenderer { + // Implement our own constructor to add our typewriterOptions + constructor( + targetLanguage: TargetLanguage, + renderContext: RenderContext, + typescriptOptions: OptionValues, + protected readonly typewriterOptions: QuicktypeTypewriterSettings, + ) { + super(targetLanguage, renderContext, typescriptOptions); + } + + // Override emitMultiline, this way you can customize the indentation size of your template files + emitMultiline(linesString: string) { + emitMultiline(this, linesString, 4); // Replace 4 with your indentation size + } + + // Override emitSource, this is the function that actually outputs code to the files. If you need to customize or prefer to output stuff through Quicktype this is the place! + emitSource(givenOutputFilename: string): void { + super.emitSource(givenOutputFilename); + // executeRenderPlan will render code from the handlebars templates, + executeRenderPlan(this, this.typewriterOptions.generators); + } + + // Override makeNameForTopLevel, this is the function that defines the names for our top level classes, the events in our case. We add custom prefixes and suffixes support through this! + makeNameForTopLevel(t: Type, givenName: string, maybeNamedType: Type | undefined): Name { + return makeNameForTopLevelWithPrefixAndSuffix( + // This is important, we do this to bind `this` as the internal Quicktype implementation relies on it + (...args) => { + return super.makeNameForTopLevel(...args); + }, + this.typewriterOptions, + t, + givenName, + maybeNamedType, + ); + } +} +``` + +Now it's time to create our own `TargetLanguage`. Again this is just boilerplate, we will just extend the appropiate Quicktype language class and make it use our own renderer: + +```ts +// We extend the TargetLanguage class for the language we will output, here for Swift +class TypewriterSwiftLanguage extends SwiftTargetLanguage { + // override the constructor to receive our typewriter options + constructor(protected readonly typewriterOptions: QuicktypeTypewriterSettings) { + super(); + } + + // override the makeRenderer to use the Renderer class we defined before + protected makeRenderer( + renderContext: RenderContext, + untypedOptionValues: { [name: string]: any }, + ): TypewriterSwiftRenderer { + return new TypewriterSwiftRenderer( + this, + renderContext, + // This part is somewhat tricky, `swiftOptions` is an object defined quicktype-core each languague has its own object, it is a good idea to take a peek at quicktype to figure out what's its name. f.e. https://github.com/quicktype/quicktype/blob/b481ea541c93b7e3ca01aaa65d4ec72492fdf699/src/quicktype-core/language/Swift.ts#L48 + getOptionValues(swiftOptions, untypedOptionValues), + this.typewriterOptions, + ); + } +} +``` + +We are done with Quicktype's boilerplate code. Let's get to our actual implementation. We will start by creating our code template. This is a Handlebars file inside `languages/templates` to which we pass in several variables: + +- `version` -> Typewriter Version number +- `type` -> array of all the types generated for the tracking plan + - `functionName` -> the type's function name + - `eventName` -> event name + - `typeName` -> event's generated type name + +A simple template will look like this, iterating over all the types and outputing the functions for each one of them: + +```hbs +import Segment + +extension Analytics { + {{#type}} + func {{functionName}}(properties: {{typeName}}) { + self.track(event: "{{eventName}}", properties: properties) + } + {{/type}} +} +``` + +Time to wrap it up, as we mentioned each language generator just needs to implement [`LanguageGenerator`](src/languages/types.ts) as we mentioned, but you don't have to manually implement the properties with quicktype. We can use [`createQuicktypeLanguageGenerator`](src/languages/quicktype-utils.ts) to create a generator for us with all the pieces: + +```ts +export const swift = createQuicktypeLanguageGenerator({ + name: 'swift', + // We pass in the class we created before for our language + quicktypeLanguage: TypewriterSwiftLanguage, + // We define in this array the SDKs we support and where the templates for each one are located + supportedSDKs: [ + { + name: 'Analytics.Swift', + id: 'swift', + templatePath: 'templates/swift/analytics.hbs', + }, + // You can also define an empty SDK for generating types without additional code + { + name: 'None (Types and validation only)', + id: 'none', + }, + ], + // We pass in any default values for the options + defaultOptions: { + 'just-types': true, + }, + // You can also add unsupported options for quicktype, that way they won't show up during configuration nor let the user set them in the config file + unsupportedOptions: ['framework'], + // Customize here how your functionNames and typeNames should look like, + nameModifiers: { + functionName: camelCase, + } +}); +``` + +Finally let's add the language to the supported languages so that it shows up during the config wizard and it gets generated during build: add your exported language to the package exports `src/languages/index.ts`: + +```ts +export { swift } from './swift'; +``` + +In `src/hooks/prerun/load-languages.ts` add this instance: + +```ts +import { Hook } from '@oclif/core'; +import { kotlin, supportedLanguages, swift, typescript } from '../../languages'; + +const hook: Hook<'init'> = async function (opts) { + // We inject any new languages plugins might support here + supportedLanguages.push(swift, kotlin, typescript); +}; + +export default hook; +``` + +##### Customizing Quicktype's output + +If you need to do something more specific with Quicktype's rendering the `Renderer` is the right place to start. For example if we want to specify exactly the order of the emitted output we can override the `emitSourceStructure`, most of this code is taken verbatim from `TypescriptRenderer` but we add the `emitAnalytics` there to inject our analytics stuff: + +```ts + protected emitSourceStructure() { + if (this.leadingComments !== undefined) { + this.emitCommentLines(this.leadingComments); + } else { + this.emitUsageComments(); + } + this.emitTypes(); + this.emitConvertModule(); + this.emitConvertModuleHelpers(); + executeRenderPlan(this, this.typewriterOptions.generators, { + functionName: camelCase, + typeName: pascalCase, + }); + this.emitModuleExports(); + } +``` + +In `Quicktype` each `Renderer` might have custom functions to emit parts of the generated types. It is always a good idea to take a peek at the available methods in the class you're extending. + +`ConvenienceRenderer` is a superclass that all renderers inherit and has most of the basic functionality you will need. Some pretty handy functions are: + +- `emitMultiline`: outputs a code block with the right indentation +- `emitLine`: will output a single line to the file +- `forEachTopLevel` iterates over each of the top level types +- `changeIndent` to modify indentation levels +- `ensureBlankLine` to add empty lines +- `emitLineOnce` ensures that a line is only output once at most per file. This is very handy for imports. + +If you want to dive deeper into the quicktype renderers, Quicktype has a [good guide](https://blog.quicktype.io/customizing-quicktype/) on how to extend them. + +It's also handy to peek at the Quicktype code files for more ideas: + +- [ConvenienceRenderer](https://github.com/quicktype/quicktype/blob/master/src/quicktype-core/ConvenienceRenderer.ts) +- [Renderer](https://github.com/quicktype/quicktype/blob/master/src/quicktype-core/ConvenienceRenderer.ts) +- [SwiftRenderer](https://github.com/quicktype/quicktype/blob/master/src/quicktype-core/language/Swift.ts) +- [TypescriptRenderer](https://github.com/quicktype/quicktype/blob/2543fa55d0d3208bbb0feb8377cecee69e721caa/src/quicktype-core/language/TypeScriptFlow.ts) + +#### Using a custom renderer + +If your use case is complex or QuickType doesn't support your language you can create your own language from scratch. You only need to implement the interface for `LanguageGenerator`: + +```ts +export interface LanguageGenerator { + /** + * Language ID + */ + id: string; + /** + * Language User-Friendly Name + */ + name: string; + /** + * File extension + */ + extension: string; + /** + * Options for the language generation. + * They are passed in an inquirer.js (https://github.com/SBoudrias/Inquirer.js) friendly version to be asked during configuration + */ + options?: QuestionCollection; + /** + * Key-value pairs of supported SDKs by the language generator. + * Key is the user friendly string + * Value is used in the configuration + */ + supportedSDKs: { + [key: string]: string; + }; + /** + * Generates code from a set of Segment Protocol Rules + * @param rules Segment PublicAPI rules object + * @param options header, sdk and additional renderer options (optional) + * @returns generated code as string + */ + generate: (rules: SegmentAPI.RuleMetadata[], options: GeneratorOptions) => Promise; +} +``` + +A good example is the [`javascript`](src/languages/javascript.ts) generator, which just wraps the typescript generator and compiles to TS according to its own custom options. diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..9c3721a6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Segment + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile deleted file mode 100644 index fd7981e1..00000000 --- a/Makefile +++ /dev/null @@ -1,278 +0,0 @@ -DESTINATION ?= "platform=iOS Simulator,name=iPhone 11" -XC_OBJECTIVE_C_ARGS := -workspace TypewriterExample.xcworkspace -scheme TypewriterExample -destination $(DESTINATION) -XC_SWIFT_ARGS := -workspace TypewriterSwiftExample.xcworkspace -scheme TypewriterSwiftExample -destination $(DESTINATION) - -.PHONY: update build prod bulk -build: COMMAND=build -build: bulk -prod: COMMAND=prod -prod: bulk -update: COMMAND=update -update: bulk -bulk: - @echo " >> Building typewriter" - @yarn build - @echo " >> Running 'typewriter $(COMMAND)' on 'typewriter'" - @NODE_ENV=development node ./dist/src/cli/index.js $(COMMAND) - @echo " >> Running 'typewriter $(COMMAND)' on 'example'" - @NODE_ENV=development node ./dist/src/cli/index.js $(COMMAND) --config=example - @echo " >> Running 'typewriter $(COMMAND)' on 'tests/e2e/node-javascript'" - @NODE_ENV=development node ./dist/src/cli/index.js $(COMMAND) --config=tests/e2e/node-javascript - @echo " >> Running 'typewriter $(COMMAND)' on 'tests/e2e/node-typescript'" - @NODE_ENV=development node ./dist/src/cli/index.js $(COMMAND) --config=tests/e2e/node-typescript - @echo " >> Running 'typewriter $(COMMAND)' on 'tests/e2e/web-javascript'" - @NODE_ENV=development node ./dist/src/cli/index.js $(COMMAND) --config=tests/e2e/web-javascript - @echo " >> Running 'typewriter $(COMMAND)' on 'tests/e2e/web-typescript'" - @NODE_ENV=development node ./dist/src/cli/index.js $(COMMAND) --config=tests/e2e/web-typescript - @echo " >> Running 'typewriter $(COMMAND)' on 'tests/e2e/ios-objc'" - @NODE_ENV=development node ./dist/src/cli/index.js $(COMMAND) --config=tests/e2e/ios-objc - @echo " >> Running 'typewriter $(COMMAND)' on 'tests/e2e/ios-swift'" - @NODE_ENV=development node ./dist/src/cli/index.js $(COMMAND) --config=tests/e2e/ios-swift - @echo " >> Running 'typewriter $(COMMAND)' on 'tests/e2e/android-java'" - @NODE_ENV=development node ./dist/src/cli/index.js $(COMMAND) --config=tests/e2e/android-java - @# Changes to the Tracking Plan JSON files will need to be run through our - @# linter again to reduce git deltas. - @make lint - -# e2e: launches our end-to-end test for each client library. -.PHONY: e2e -e2e: - @### Boot the sidecar API to capture API requests. - @make docker - - @### Example App - @make build-example - - @### JavaScript node - @make test-node-javascript - @### TypeScript node - @make test-node-typescript - - @### JavaScript web - @make test-web-javascript - @### TypeScript web - @make test-web-typescript - - @### Objective-C iOS - @make test-ios-objc - @### Swift iOS - @make test-ios-swift - - @### Android - @make test-android-java - -.PHONY: lint -lint: - @yarn run eslint --fix 'src/**/*.ts' 'src/**/*.tsx' - @yarn run -s prettier --write --loglevel warn '**/*.json' '**/*.yml' - -# docker: launches segmentio/mock which we use to mock the Segment API for e2e testing. -.PHONY: docker -docker: - @docker-compose -f tests/e2e/docker-compose.yml up -d - @while [ "`docker inspect -f {{.State.Health.Status}} e2e_mock_1`" != "healthy" ]; do sleep 1; done - @make clear-mock - -# clear-mock: Clears segmentio/mock to give an e2e test a clean slate. -.PHONY: clear-mock -clear-mock: - @curl -f "http://localhost:8765/messages" > /dev/null 2>&1 || (echo "Failed to clear segmentio/mock. Is it running? Try 'make docker'"; exit 1) - -# teardown: shuts down the sidecar. -.PHONY: teardown -teardown: - @docker-compose -f tests/e2e/docker-compose.yml down - -.PHONY: build-example -build-example: - @yarn run -s dev build --config=./example && \ - cd example && \ - yarn && \ - yarn build - -.PHONY: test-node-javascript -test-node-javascript: test-node-javascript-dev test-node-javascript-prod - -.PHONY: test-node-javascript-dev -test-node-javascript-dev: - @echo "\n>>> 🏃 Running dev JavaScript Node client test suite...\n" - @make clear-mock && \ - yarn run -s dev build --config=./tests/e2e/node-javascript && \ - cd tests/e2e/node-javascript && \ - yarn && \ - NODE_ENV=test yarn run -s test && \ - cd ../../.. && \ - SDK=analytics-node LANGUAGE=javascript IS_DEVELOPMENT=true yarn run -s jest ./tests/e2e/suite.test.ts - -.PHONY: test-node-javascript-prod -test-node-javascript-prod: - @echo "\n>>> 🏃 Running prod JavaScript Node client test suite...\n" - @make clear-mock && \ - yarn run -s dev prod --config=./tests/e2e/node-javascript && \ - cd tests/e2e/node-javascript && \ - yarn && \ - NODE_ENV=test yarn run -s test && \ - cd ../../.. && \ - SDK=analytics-node LANGUAGE=javascript IS_DEVELOPMENT=false yarn run -s jest ./tests/e2e/suite.test.ts - -.PHONY: test-node-typescript -test-node-typescript: test-node-typescript-dev test-node-typescript-prod - -.PHONY: test-node-typescript-dev -test-node-typescript-dev: - @echo "\n>>> 🏃 Running dev TypeScript Node client test suite...\n" - @make clear-mock && \ - yarn run -s dev build --config=./tests/e2e/node-typescript && \ - cd tests/e2e/node-typescript && \ - yarn && \ - NODE_ENV=test yarn run -s test && \ - cd ../../.. && \ - SDK=analytics-node LANGUAGE=typescript IS_DEVELOPMENT=true yarn run -s jest ./tests/e2e/suite.test.ts - -.PHONY: test-node-typescript-prod -test-node-typescript-prod: - @echo "\n>>> 🏃 Running prod TypeScript Node client test suite...\n" - @make clear-mock && \ - yarn run -s dev prod --config=./tests/e2e/node-typescript && \ - cd tests/e2e/node-typescript && \ - yarn && \ - yarn run -s test && \ - cd ../../.. && \ - SDK=analytics-node LANGUAGE=typescript IS_DEVELOPMENT=false yarn run -s jest ./tests/e2e/suite.test.ts - -.PHONY: test-web-javascript -test-web-javascript: test-web-javascript-dev test-web-javascript-prod - -.PHONY: test-web-javascript-dev -test-web-javascript-dev: - @echo "\n>>> 🏃 Running dev JavaScript analytics.js client test suite...\n" - @make clear-mock && \ - yarn run -s dev build --config=./tests/e2e/web-javascript && \ - cd tests/e2e/web-javascript && \ - yarn && \ - yarn run -s build && \ - NODE_ENV=test yarn run -s test && \ - cd ../../.. && \ - SDK=analytics.js LANGUAGE=javascript IS_DEVELOPMENT=true yarn run -s jest ./tests/e2e/suite.test.ts - -.PHONY: test-web-javascript-prod -test-web-javascript-prod: - @echo "\n>>> 🏃 Running prod JavaScript analytics.js client test suite...\n" - @make clear-mock && \ - yarn run -s dev prod --config=./tests/e2e/web-javascript && \ - cd tests/e2e/web-javascript && \ - yarn && \ - yarn run -s build && \ - yarn run -s test && \ - cd ../../.. && \ - SDK=analytics.js LANGUAGE=javascript IS_DEVELOPMENT=false yarn run -s jest ./tests/e2e/suite.test.ts - -.PHONY: test-web-typescript -test-web-typescript: test-web-typescript-dev test-web-typescript-prod - -.PHONY: test-web-typescript-dev -test-web-typescript-dev: - @echo "\n>>> 🏃 Running dev TypeScript analytics.js client test suite...\n" - @make clear-mock && \ - yarn run -s dev build --config=./tests/e2e/web-typescript && \ - cd tests/e2e/web-typescript && \ - yarn && \ - yarn run -s build && \ - NODE_ENV=test yarn run -s test && \ - cd ../../.. && \ - SDK=analytics.js LANGUAGE=typescript IS_DEVELOPMENT=true yarn run -s jest ./tests/e2e/suite.test.ts - -.PHONY: test-web-typescript-prod -test-web-typescript-prod: - @echo "\n>>> 🏃 Running prod TypeScript analytics.js client test suite...\n" - @make clear-mock && \ - yarn run -s dev prod --config=./tests/e2e/web-typescript && \ - cd tests/e2e/web-typescript && \ - yarn && \ - yarn run -s build && \ - yarn run -s test && \ - cd ../../.. && \ - SDK=analytics.js LANGUAGE=typescript IS_DEVELOPMENT=false yarn run -s jest ./tests/e2e/suite.test.ts - -.PHONY: test-ios-objc -test-ios-objc: - @# TODO: verify that xcodebuild and xcpretty are available - @cd tests/e2e/ios-objc && pod install - @make test-ios-objc-dev test-ios-objc-prod - -.PHONY: test-ios-swift -test-ios-swift: - @# TODO: verify that xcodebuild and xcpretty are available - @cd tests/e2e/ios-swift && pod install - @make test-ios-swift-dev test-ios-swift-prod - -.PHONY: test-android-java -test-android-java: - @cd tests/e2e/android-java - @make test-android-java-dev test-android-java-prod - -.PHONY: test-android-java-dev test-android-java-prod test-android-java-runner test-android-java-runner -test-android-java-dev: IS_DEVELOPMENT=true -test-android-java-dev: TYPEWRITER_COMMAND=build -test-android-java-dev: test-android-java-runner - -test-android-java-prod: IS_DEVELOPMENT=false -test-android-java-prod: TYPEWRITER_COMMAND=prod -test-android-java-prod: test-android-java-runner - -test-android-java-runner: LANGUAGE=java -test-android-java-runner: - @echo "\n>>> 🏃 Running Android client test suite ($(TYPEWRITER_COMMAND), $(LANGUAGE))...\n" - @make clear-mock - @yarn run -s dev $(TYPEWRITER_COMMAND) --config=./tests/e2e/android-java - @cd tests/e2e/android-java && ./gradlew --rerun-tasks testDebugUnitTest - @SDK=analytics-android LANGUAGE=$(LANGUAGE) IS_DEVELOPMENT=$(IS_DEVELOPMENT) yarn run -s jest ./tests/e2e/suite.test.ts - -.PHONY: test-ios-objc-dev test-ios-objc-prod test-ios-objc-runner test-ios-swift-dev test-ios-swift-prod test-ios-swift-runner test-ios-runner -test-ios-objc-dev: IS_DEVELOPMENT=true -test-ios-objc-dev: TYPEWRITER_COMMAND=build -test-ios-objc-dev: test-ios-objc-runner - -test-ios-objc-prod: IS_DEVELOPMENT=false -test-ios-objc-prod: TYPEWRITER_COMMAND=prod -test-ios-objc-prod: test-ios-objc-runner - -test-ios-objc-runner: LANGUAGE=objc -test-ios-objc-runner: XC_ARGS=$(XC_OBJECTIVE_C_ARGS) -test-ios-objc-runner: test-ios-runner - -test-ios-swift-dev: IS_DEVELOPMENT=true -test-ios-swift-dev: TYPEWRITER_COMMAND=build -test-ios-swift-dev: test-ios-swift-runner - -test-ios-swift-prod: IS_DEVELOPMENT=false -test-ios-swift-prod: TYPEWRITER_COMMAND=prod -test-ios-swift-prod: test-ios-swift-runner - -test-ios-swift-runner: LANGUAGE=swift -test-ios-swift-runner: XC_ARGS=$(XC_SWIFT_ARGS) -test-ios-swift-runner: test-ios-runner - -test-ios-runner: - @echo "\n>>> 🏃 Running iOS client test suite ($(TYPEWRITER_COMMAND), $(LANGUAGE))...\n" - @make clear-mock - @yarn run -s dev $(TYPEWRITER_COMMAND) --config=./tests/e2e/ios-$(LANGUAGE) - @cd tests/e2e/ios-$(LANGUAGE) && set -o pipefail && xcodebuild test $(XC_ARGS) | xcpretty - @SDK=analytics-ios LANGUAGE=$(LANGUAGE) IS_DEVELOPMENT=$(IS_DEVELOPMENT) yarn run -s jest ./tests/e2e/suite.test.ts - -.PHONY: precommit -precommit: - @make build - - @# Lint the working directory: - @yarn run lint-staged - -.PHONY: update-bridging-header -update-bridging-header: - @echo "// Generated Typewriter Headers:" > \ - tests/e2e/ios-swift/TypewriterSwiftExample/TypewriterSwiftExample-Bridging-Header.h - @ls -l tests/e2e/ios-swift/TypewriterSwiftExample/Analytics | \ - grep '.h$$' | \ - sed -e 's/^.*SEG/#import "Analytics\/SEG/' | \ - sed -e 's/$$/"/' >> \ - tests/e2e/ios-swift/TypewriterSwiftExample/TypewriterSwiftExample-Bridging-Header.h diff --git a/README.md b/README.md index 0a1696f1..7a42b857 100644 --- a/README.md +++ b/README.md @@ -47,3 +47,16 @@ For more instructions on setting up your `typewriter` client, such as adding it - To submit a bug report or feature request, [file an issue here](issues). - To develop on `typewriter` or propose support for a new language, see [our contributors documentation](./.github/CONTRIBUTING.md). + + +## Migrating from v7 + +Check the instructions on our [documentation](https://segment.com/docs/protocols/typewriter) + +- You'll need to change your Segment Config API Token for a Public API Token +- v8 doesn't support **Analytics-iOS** nor **Analytics-Android**. We recommend using [Analytics-Swift]() and [Analytics-Kotlin]() instead which are supported. +If you need to use these libraries you can run v7 specifying the version with your commands: + +```sh +$ npx typewriter@7 build +``` \ No newline at end of file diff --git a/bin/dev b/bin/dev new file mode 100755 index 00000000..0929bd13 --- /dev/null +++ b/bin/dev @@ -0,0 +1,18 @@ +#!/usr/bin/env node + +const oclif = require('@oclif/core') + +const path = require('path') +const project = path.join(__dirname, '..', 'tsconfig.json') + +// In dev mode -> use ts-node and dev plugins +process.env.NODE_ENV = 'development' + +require('ts-node').register({project}) + +// In dev mode, always show stack traces +oclif.settings.debug = true; +oclif.settings.tsnodeEnabled = true; + +// Start the CLI +oclif.run().then(oclif.flush).catch(oclif.Errors.handle) diff --git a/bin/dev.cmd b/bin/dev.cmd new file mode 100644 index 00000000..077b57ae --- /dev/null +++ b/bin/dev.cmd @@ -0,0 +1,3 @@ +@echo off + +node "%~dp0\dev" %* \ No newline at end of file diff --git a/bin/run b/bin/run new file mode 100755 index 00000000..a7635de8 --- /dev/null +++ b/bin/run @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +const oclif = require('@oclif/core') + +oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle')) diff --git a/bin/run.cmd b/bin/run.cmd new file mode 100644 index 00000000..968fc307 --- /dev/null +++ b/bin/run.cmd @@ -0,0 +1,3 @@ +@echo off + +node "%~dp0\run" %* diff --git a/example/README.md b/example/README.md deleted file mode 100644 index 25e9b5c8..00000000 --- a/example/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Typewriter Example - -This example repo demonstrates how to setup and use Typewriter in a JavaScript/TypeScript web environment, as a strongly-typed wrapper for [`analytics.js`](https://segment.com/docs/sources/website/analytics.js/). - -## Setup - -First, install dependencies: - -```sh -$ yarn -``` - -Then, generate a Typewriter client: - -```sh -$ yarn typewriter dev -``` - -Update the Segment write key in [`_document.tsx`](./pages/_document.tsx#L48) for the source you want to report analytics to: - -```typescript -const analyticsSnippet = snippetFn({ - apiKey: '', - page: false, -}) -``` - -Run the development server: - -```sh -$ yarn run dev -DONE Compiled successfully in 1409ms 18:15:03 - -> Ready on http://localhost:3000 -No type errors found -Version: typescript 3.1.1 -Time: 2219ms -``` - -Once you run the app, go the Debugger to see events coming in! - -## More Documentation - -See the [`Typewriter docs`](https://segment.com/docs/protocols/typewriter) for more information on instrumenting your app with Typewriter. diff --git a/example/analytics/plan.json b/example/analytics/plan.json deleted file mode 100644 index 952fe972..00000000 --- a/example/analytics/plan.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "create_time": "2019-10-21T17:15:38.000Z", - "display_name": "Typewriter Example Tracking Plan", - "name": "workspaces/segment_prod/tracking-plans/rs_1SWT1hC4xpwcltyCUud43XMIlQo", - "rules": { - "events": [ - { - "description": "Fired after a user's signin attempt fails to pass validation.", - "name": "Sign In Failed", - "rules": { - "$schema": "http://json-schema.org/draft-07/schema#", - "labels": {}, - "properties": { - "context": {}, - "properties": { - "properties": { - "id": { - "description": "The user's ID.", - "type": "string" - }, - "numAttempts": { - "description": "How many times the user has attempted to sign-in.", - "type": "integer" - }, - "rememberMe": { - "description": "Whether the user has indicated that the browser should store their login credentials.", - "type": "boolean" - } - }, - "required": ["id"], - "type": "object" - }, - "traits": { - "type": "object" - } - }, - "required": ["properties"], - "type": "object" - }, - "version": 1 - }, - { - "description": "Fired when a user submits a sign in, prior to validating that user's login.", - "name": "Sign In Submitted", - "rules": { - "$schema": "http://json-schema.org/draft-07/schema#", - "labels": {}, - "properties": { - "context": {}, - "properties": { - "properties": { - "id": { - "description": "The user's ID.", - "type": "string" - }, - "numAttempts": { - "description": "How many times the user has attempted to sign-in.", - "type": "integer" - }, - "rememberMe": { - "description": "Whether the user has indicated that the browser should store their login credentials.", - "type": "boolean" - } - }, - "required": ["id"], - "type": "object" - }, - "traits": { - "type": "object" - } - }, - "type": "object" - }, - "version": 1 - }, - { - "description": "Fired when a user successfully submits a sign in, prior to redirecting into the app.", - "name": "Sign In Succeeded", - "rules": { - "$schema": "http://json-schema.org/draft-07/schema#", - "labels": {}, - "properties": { - "context": {}, - "properties": { - "properties": { - "id": { - "description": "The user's ID.", - "type": "string" - }, - "numAttempts": { - "description": "How many times the user has attempted to sign-in.", - "type": "integer" - }, - "rememberMe": { - "description": "Whether the user has indicated that the browser should store their login credentials.", - "type": "boolean" - } - }, - "required": ["id"], - "type": "object" - }, - "traits": { - "type": "object" - } - }, - "type": "object" - }, - "version": 1 - }, - { - "description": "Fired when a user successfully submits a sign in, prior to redirecting into the app.", - "name": "User Signed Out", - "rules": { - "$schema": "http://json-schema.org/draft-07/schema#", - "labels": {}, - "properties": { - "context": {}, - "properties": { - "properties": { - "id": { - "description": "The user's ID.", - "type": "string" - }, - "numAttempts": { - "description": "How many times the user has attempted to sign-in.", - "type": "integer" - }, - "rememberMe": { - "description": "Whether the user has indicated that the browser should store their login credentials.", - "type": "boolean" - } - }, - "required": ["id"], - "type": "object" - }, - "traits": { - "type": "object" - } - }, - "type": "object" - }, - "version": 1 - } - ], - "group_traits": [], - "identify_traits": [] - }, - "update_time": "2020-03-04T18:20:43.000Z" -} diff --git a/example/components/Home.tsx b/example/components/Home.tsx deleted file mode 100644 index a5b1f7ad..00000000 --- a/example/components/Home.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react' -import { Pane, Button, majorScale, Card, Paragraph, Text, Link } from 'evergreen-ui' -import Router, { withRouter } from 'next/router' -import { get } from 'lodash' - -type Props = { - onSignOut: (props: { id: string }) => void -} - -class HomeComponent extends React.Component { - private onSignOut = () => { - const id = get(this.props, 'router.query.id') - - this.props.onSignOut({ - id, - }) - - Router.push('/login') - } - - public render() { - return ( - - - - - - - - - Getting started with Typewriter is as simple as: -
npx typewriter@next init
-
-
- - - You can learn more from our documentation: {''} - - https://segment.com/docs/protocols/typewriter - - - -
-
-
- ) - } -} - -// HOCs are a pain. any = temporary fix. -export const Home: React.FC = withRouter(HomeComponent as any) as any diff --git a/example/components/LoginForm.tsx b/example/components/LoginForm.tsx deleted file mode 100644 index ad96cf3a..00000000 --- a/example/components/LoginForm.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import * as React from 'react' -import { - Pane, - Button, - Text, - TextInput, - Heading, - Card, - Checkbox, - toaster, - Paragraph, - Link, - majorScale, -} from 'evergreen-ui' -import Router from 'next/router' - -interface Props { - onSubmit: (props: { id: string; numAttempts: number; rememberMe: boolean }) => void - onSuccess: (props: { id: string; numAttempts: number; rememberMe: boolean }) => void - onError: (props: { id: string; numAttempts: number; rememberMe: boolean }) => void -} - -interface State { - id: string - password: string - rememberMe: boolean - isLoading: boolean - success: boolean - numAttempts: number -} - -export class LoginForm extends React.Component { - public state = { - id: '', - password: '', - rememberMe: true, - isLoading: false, - success: false, - numAttempts: 0, - } - - // Much secret. Many legit. 🐕 - private superSecretEncryptedUserStore: Record = { - // The Protocols EPD Squad 🙌 - andy: 'password', - archana: 'password', - caledona: 'password', - catherine: 'password', - colin: 'password', - daniel: 'password', - frances: 'password', - francisco: 'password', - gurdas: 'password', - hareem: 'password', - heidi: 'password', - kat: 'password', - niels: 'password', - pengcheng: 'password', - } - - private onChangeUserId = (event: React.ChangeEvent) => { - this.setState({ - id: event.target.value, - }) - } - - private onChangePassword = (event: React.ChangeEvent) => { - this.setState({ - password: event.target.value, - }) - } - - private onToggleRememberMe = (event: React.ChangeEvent) => { - this.setState({ - rememberMe: event.target.checked, - }) - } - - private onSubmit = () => { - this.setState( - prev => { - return { - isLoading: true, - numAttempts: prev.numAttempts + 1, - } - }, - () => { - this.props.onSubmit({ - id: this.state.id, - rememberMe: this.state.rememberMe, - numAttempts: this.state.numAttempts, - }) - - const fakeNetworkLatency = (Math.random() + 1) * 500 - setTimeout(() => { - if (this.superSecretEncryptedUserStore[this.state.id] !== this.state.password) { - // This isn't a valid login, show an error state. - this.setState({ - isLoading: false, - }) - - toaster.danger("Hmm. That didn't work.", { - description: 'The username or password you entered was incorrect.', - }) - - this.props.onError({ - id: this.state.id, - rememberMe: this.state.rememberMe, - numAttempts: this.state.numAttempts, - }) - } else { - // Successful login, go ahead and redirect to the user's home. - this.setState({ - success: true, - }) - - this.props.onSuccess({ - id: this.state.id, - rememberMe: this.state.rememberMe, - numAttempts: this.state.numAttempts, - }) - - Router.push(`/home?id=${this.state.id}`) - } - }, fakeNetworkLatency) - } - ) - } - - public render() { - return ( - - - - Log in to Segment - - Email * - - Password * - - - - - - Forgot your password? {''} - - Reset your password - - - - Don't have an account? {''} - - Sign up - - - - - - ) - } -} diff --git a/example/declarations.d.ts b/example/declarations.d.ts deleted file mode 100644 index 896cba79..00000000 --- a/example/declarations.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare module 'evergreen-ui' -declare module '@segment/snippet' diff --git a/example/next-env.d.ts b/example/next-env.d.ts deleted file mode 100644 index 7b7aa2c7..00000000 --- a/example/next-env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/example/package.json b/example/package.json deleted file mode 100644 index 388dfcf6..00000000 --- a/example/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "typewriter-example", - "version": "1.0.0", - "description": "Example web app using Typewriter's TS analytics.js client", - "repository": "https://github.com/segmentio/typewriter", - "author": "Colin King ", - "license": "MIT", - "private": true, - "scripts": { - "dev": "next", - "build": "next build", - "start": "next start", - "test": "jest" - }, - "dependencies": { - "@segment/snippet": "^4.14.2", - "evergreen-ui": "^6.1.0", - "next": "^10.0.0", - "react": "^16.14.0", - "react-dom": "^16.14.0", - "request": "^2.88.0", - "typescript": "^4.3.5" - }, - "devDependencies": { - "@types/react": "^16.14.11", - "@types/segment-analytics": "^0.0.34", - "@types/styled-jsx": "^2.2.9", - "enzyme": "^3.11.0", - "enzyme-adapter-react-16": "^1.15.6", - "fork-ts-checker-webpack-plugin": "^5.2.1", - "jest": "^27.0.6", - "typewriter": "7.2.1" - }, - "resolutions": { - "lodash": ">=4.17.21", - "node-fetch": ">=2.6.1" - }, - "jest": { - "setupFilesAfterEnv": [ - "tests/setup-tests.js" - ] - } -} diff --git a/example/pages/_document.tsx b/example/pages/_document.tsx deleted file mode 100644 index 0768b73b..00000000 --- a/example/pages/_document.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react' -import Document, { - Head, - Main, - NextScript, - DocumentContext, - DocumentInitialProps, -} from 'next/document' -import { extractStyles } from 'evergreen-ui' -import * as snippet from '@segment/snippet' - -interface Props extends Document { - css: string - hydrationScript: string -} - -export default class SSRDoc extends Document { - // Inject Evergreen's styles so that Next can perform SSR with it. - public static getInitialProps({ renderPage }: DocumentContext): Promise { - const page = renderPage() - const { css, hydrationScript } = extractStyles() - - return { - ...page, - css, - hydrationScript, - } as any - } - - public render() { - const { css, hydrationScript } = this.props - - const globalStyles = ` - html { - height: '100%'; - } - body { - height: '100%'; - background: rgb(247, 248, 250); - margin: 0; - } - ` - - // Generate and inject the Segment analytics.js snippet. - const snippetFn = process.env.NODE_ENV === 'production' ? snippet.min : snippet.max - // https://app.segment.com/segment_prod/sources/typewriter-source/overview - const analyticsSnippet = snippetFn({ - apiKey: 'ZgsqNXhzqQ3nBsvPGXqZQn2RWoGBhlqC', - page: false, - }) - - return ( - - - - - - -
- {hydrationScript} - - -